Initial commit: Feldbett Design-Dokumentation und FreeCAD-Scripts

Modulares Schwergewicht-Feldbett aus Alu-Rohren (25×1.5) mit Stahl-Konnektoren (33.7×2.5).
Design-Docs, Materialrecherche, Gewichtsberechnung, Korrosionsschutz-Analyse,
und zwei getestete FreeCAD-Makros (Struktur + Konnektoren-Detail).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Axel Meyer
2026-04-09 14:30:35 +02:00
commit d849c4e86d
15 changed files with 2824 additions and 0 deletions

View File

@@ -0,0 +1,403 @@
"""
Feldbett Konnektoren Detail — FreeCAD Python Script
====================================================
Zeigt alle drei Verbindungstypen als Einzelteile zur Inspektion:
1. Connector 1 (C1) — A↔D / Fuß: 2 Hülsen auf Gehrung
2. Connector 2 (C2) — Q↔D↔B: 4 Hülsen sternförmig
3. Inline-Stift — A-Stangen Verlängerung
Zusätzlich: Sägewinkel, Einstecktiefen, Passungsspiele.
Ausführen: FreeCAD → Makro → Makro ausführen → diese Datei wählen
Rohrtriplet "Komfortabel":
Standard : Alu 25×1.5 mm
Stift : Alu 18×1.0 mm
Hülse : Stahl 33.7×2.5 mm
"""
import FreeCAD as App
import FreeCADGui as Gui
import Part
import math
# ============================================================
# PARAMETER
# ============================================================
L = 350.0
BREITE_AA = 700.0
MODULE = 3
# Standard (Alu)
STD_DA = 25.0
STD_WAND = 1.5
STD_DI = STD_DA - 2.0 * STD_WAND
# Stift (Alu)
STIFT_DA = 18.0
STIFT_WAND = 1.0
STIFT_DI = STIFT_DA - 2.0 * STIFT_WAND
STIFT_LAENGE = 100.0
# Hülse (Stahl)
HUEL_DA = 33.7
HUEL_WAND = 2.5
HUEL_DI = HUEL_DA - 2.0 * HUEL_WAND
HUEL_LAENGE = 40.0
# Federbolzen
FEDER_BOLZEN_D = 4.0 # Durchmesser [mm]
# Farben
FARBE_A = (0.20, 0.45, 0.75)
FARBE_Q = (0.73, 0.46, 0.09)
FARBE_D = (0.06, 0.43, 0.34)
FARBE_B = (0.42, 0.25, 0.63)
# Passspiel
SPIEL_HUEL = HUEL_DI - STD_DA # 3.7 mm
SPIEL_STIFT = STD_DI - STIFT_DA # 4.0 mm
# ============================================================
# GEOMETRIE
# ============================================================
hwA = BREITE_AA / 2.0
hwQ = L / 2.0
dZ = hwA - hwQ
legH = math.sqrt(0.75 * L**2 - dZ**2)
totalH = 2.0 * legH
def norm(v):
l = math.sqrt(sum(x**2 for x in v))
return tuple(x/l for x in v)
# Richtungsvektoren (normiert)
vA = (1.0, 0.0, 0.0) # A-Stange: +X
vQ = (0.0, 0.0, 1.0) # Q-Strebe: +Z
vD = norm(( L/2, -legH, -dZ)) # Diagonale: A→Q
vDr = norm((-L/2, legH, dZ)) # umgekehrt: Q→A (aufwärts)
vB = norm((-L/2, -legH, dZ)) # Bein: Q→Boden
def angle_deg(v1, v2):
dot = sum(a*b for a,b in zip(v1,v2))
return math.degrees(math.acos(max(-1.0, min(1.0, abs(dot)))))
def angle_signed(v1, v2):
dot = sum(a*b for a,b in zip(v1,v2))
return math.degrees(math.acos(max(-1.0, min(1.0, dot))))
def saege_winkel(v1, v2):
"""Gehrungswinkel: 90° - (Winkel_zwischen / 2)"""
alpha = angle_signed(v1, v2)
return 90.0 - alpha / 2.0
# ============================================================
# KONSOLEN-AUSGABE
# ============================================================
SEP = "=" * 55
print(SEP)
print("FELDBETT KONNEKTOREN — DETAIL")
print(SEP)
print(f"\nRohre:")
print(f" Standard (Alu) : {STD_DA}×{STD_WAND} mm (ID={STD_DI:.1f})")
print(f" Stift (Alu) : {STIFT_DA}×{STIFT_WAND} mm (ID={STIFT_DI:.1f})")
print(f" Hülse (Stahl) : {HUEL_DA}×{HUEL_WAND} mm (ID={HUEL_DI:.1f})")
print(f" Spiel Hülse↔Std : {SPIEL_HUEL:.1f} mm ({SPIEL_HUEL/2:.1f} mm pro Seite)")
print(f" Spiel Std↔Stift : {SPIEL_STIFT:.1f} mm ({SPIEL_STIFT/2:.1f} mm pro Seite)")
print(f"\nWinkel:")
print(f" A ↔ D : {angle_deg(vA, vD):.1f}°")
print(f" Q ↔ D : {angle_deg(vQ, vD):.1f}°")
print(f" Q ↔ B : {angle_deg(vQ, vB):.1f}°")
print(f" D ↔ B : {angle_deg(vD, vB):.1f}°")
print(f"\nSägewinkel (Gehrung, Abweichung von 90°):")
print(f" C1: AD Gehrung = {saege_winkel(vA, vD):.1f}°")
print(f" C2: QD Gehrung = {saege_winkel(vQ, vDr):.1f}°")
print(f" C2: QB Gehrung = {saege_winkel(vQ, vB):.1f}°")
print(f" C2: DB Gehrung = {saege_winkel(vDr, vB):.1f}°")
print()
# ============================================================
# FreeCAD HILFSFUNKTIONEN
# ============================================================
def fv(t):
return App.Vector(t[0], t[1], t[2])
def rotation_to_dir(direction):
z = App.Vector(0, 0, 1)
d = App.Vector(*direction).normalize()
cross = z.cross(d)
dot = z.dot(d)
if cross.Length > 1e-6:
angle = math.degrees(math.acos(max(-1.0, min(1.0, dot))))
return App.Rotation(cross, angle)
elif dot < 0:
return App.Rotation(App.Vector(1, 0, 0), 180)
else:
return App.Rotation()
def make_cyl(direction, length, radius, origin=(0,0,0)):
cyl = Part.makeCylinder(radius, length)
rot = rotation_to_dir(direction)
cyl.Placement = App.Placement(App.Vector(*origin), rot)
return cyl
def make_huelse(direction, origin=(0,0,0), length=None):
"""Hohle Hülse (Stahl) entlang direction."""
ll = length or HUEL_LAENGE
outer = make_cyl(direction, ll, HUEL_DA / 2.0, origin)
inner = make_cyl(direction, ll + 1.0, HUEL_DI / 2.0, origin)
return outer.cut(inner)
def make_rohr(direction, length, da, wand, origin=(0,0,0)):
"""Allgemeines Hohlrohr."""
di = da - 2.0 * wand
outer = make_cyl(direction, length, da / 2.0, origin)
inner = make_cyl(direction, length + 1.0, di / 2.0, origin)
return outer.cut(inner)
def make_federbolzen_bohrung(direction, origin, dist_from_start):
"""Querbohrung für Federbolzen."""
if FEDER_BOLZEN_D <= 0:
return None
d = App.Vector(*direction).normalize()
mid = App.Vector(
origin[0] + d.x * dist_from_start,
origin[1] + d.y * dist_from_start,
origin[2] + d.z * dist_from_start,
)
ref = App.Vector(0,1,0) if abs(d.dot(App.Vector(0,1,0))) < 0.9 else App.Vector(1,0,0)
bohr_dir = d.cross(ref).normalize()
bohr_len = HUEL_DA + 4.0
start = App.Vector(
mid.x - bohr_dir.x * bohr_len / 2.0,
mid.y - bohr_dir.y * bohr_len / 2.0,
mid.z - bohr_dir.z * bohr_len / 2.0,
)
cyl = Part.makeCylinder(FEDER_BOLZEN_D / 2.0, bohr_len)
rot = rotation_to_dir((bohr_dir.x, bohr_dir.y, bohr_dir.z))
cyl.Placement = App.Placement(start, rot)
return cyl
def add_part(doc, shape, name, color, group=None):
obj = doc.addObject("Part::Feature", name)
obj.Shape = shape
if obj.ViewObject:
obj.ViewObject.ShapeColor = color
if group:
group.addObject(obj)
return obj
# ============================================================
# DOKUMENT
# ============================================================
doc_name = "Feldbett_Konnektoren_v2"
if doc_name in App.listDocuments():
App.closeDocument(doc_name)
doc = App.newDocument(doc_name)
grp_c1 = doc.addObject("App::DocumentObjectGroup", "Connector_1")
grp_c2 = doc.addObject("App::DocumentObjectGroup", "Connector_2")
grp_stift = doc.addObject("App::DocumentObjectGroup", "Inline_Stift")
grp_demo = doc.addObject("App::DocumentObjectGroup", "Demo_Rohre")
# Positionen der drei Baugruppen (nebeneinander in X)
O_C1 = (0, 0, 0)
O_C2 = (150, 0, 0)
O_STIFT = (350, 0, 0)
# ============================================================
# CONNECTOR 1 — 2 Hülsen (A + D)
# ============================================================
print("Erzeuge Connector 1...")
# Hülse A-Richtung (+X und -X, durchlaufend)
h_A = make_huelse((1,0,0), O_C1)
# Hülse A Gegenrichtung
h_A2 = make_huelse((-1,0,0), O_C1)
# Hülse D-Richtung
h_D = make_huelse(vD, O_C1)
c1 = h_A.fuse(h_A2).fuse(h_D)
# Federbolzen-Bohrungen
for direction in [(1,0,0), (-1,0,0), vD]:
b = make_federbolzen_bohrung(direction, O_C1, HUEL_LAENGE * 0.6)
if b:
c1 = c1.cut(b)
try:
c1 = c1.makeFillet(1.0, c1.Edges)
except Exception:
pass
add_part(doc, c1, "C1_komplett", (0.85, 0.35, 0.15), grp_c1)
# Demo-Rohre in C1 einstecken (Alu-Stangen, halbtransparent)
demo_A = make_rohr((1,0,0), 120, STD_DA, STD_WAND,
(O_C1[0]-60, O_C1[1], O_C1[2]))
obj_dA = add_part(doc, demo_A, "Demo_A_in_C1", (0.2, 0.45, 0.75), grp_demo)
if obj_dA.ViewObject:
obj_dA.ViewObject.Transparency = 60
demo_D = make_rohr(vD, 100, STD_DA, STD_WAND, O_C1)
obj_dD = add_part(doc, demo_D, "Demo_D_in_C1", (0.06, 0.43, 0.34), grp_demo)
if obj_dD.ViewObject:
obj_dD.ViewObject.Transparency = 60
print(f" C1: 2+1 Hülsen à {HUEL_LAENGE:.0f}mm, Gehrung A-D = {saege_winkel(vA, vD):.1f}°")
# ============================================================
# CONNECTOR 2 — 4 Hülsen (Ql, Qr, D, B)
# ============================================================
print("Erzeuge Connector 2...")
h_Ql = make_huelse((0,0,-1), O_C2)
h_Qr = make_huelse((0,0,+1), O_C2)
h_Dr = make_huelse(vDr, O_C2)
h_Bv = make_huelse(vB, O_C2)
c2 = h_Ql.fuse(h_Qr).fuse(h_Dr).fuse(h_Bv)
# Federbolzen-Bohrungen
for direction in [(0,0,-1), (0,0,+1), vDr, vB]:
b = make_federbolzen_bohrung(direction, O_C2, HUEL_LAENGE * 0.6)
if b:
c2 = c2.cut(b)
try:
c2 = c2.makeFillet(1.0, c2.Edges)
except Exception:
pass
add_part(doc, c2, "C2_komplett", (0.15, 0.55, 0.75), grp_c2)
# Demo-Rohre in C2
for direction, length, label, color in [
((0,0,-1), 80, "Demo_Ql_in_C2", FARBE_Q),
((0,0,+1), 80, "Demo_Qr_in_C2", FARBE_Q),
(vDr, 80, "Demo_D_in_C2", (0.06, 0.43, 0.34)),
(vB, 80, "Demo_B_in_C2", (0.42, 0.25, 0.63)),
]:
r = make_rohr(direction, length, STD_DA, STD_WAND, O_C2)
o = add_part(doc, r, label, color, grp_demo)
if o.ViewObject:
o.ViewObject.Transparency = 60
print(f" C2: 4 Hülsen à {HUEL_LAENGE:.0f}mm")
print(f" Q-D Gehrung = {saege_winkel(vQ, vDr):.1f}°")
print(f" D-B Gehrung = {saege_winkel(vDr, vB):.1f}°")
# ============================================================
# INLINE-STIFT-VERBINDER (A-Stangen verlängern)
# ============================================================
print("Erzeuge Inline-Stift...")
# Zwei A-Rohrstücke + Stift dazwischen
gap = 2.0 # kleiner Spalt zwischen den Rohren [mm] für Sichtbarkeit
# linkes Rohr
r_left = make_rohr((-1,0,0), 120, STD_DA, STD_WAND,
(O_STIFT[0] - gap/2, O_STIFT[1], O_STIFT[2]))
o = add_part(doc, r_left, "Stift_Rohr_links", FARBE_A, grp_stift)
if o.ViewObject:
o.ViewObject.Transparency = 40
# rechtes Rohr
r_right = make_rohr((1,0,0), 120, STD_DA, STD_WAND,
(O_STIFT[0] + gap/2, O_STIFT[1], O_STIFT[2]))
o = add_part(doc, r_right, "Stift_Rohr_rechts", FARBE_A, grp_stift)
if o.ViewObject:
o.ViewObject.Transparency = 40
# Stift selbst (zentriert, ragt in beide Rohre)
stift = make_rohr((1,0,0), STIFT_LAENGE, STIFT_DA, STIFT_WAND,
(O_STIFT[0] - STIFT_LAENGE/2, O_STIFT[1], O_STIFT[2]))
add_part(doc, stift, "Stift_Verbinder", (0.85, 0.55, 0.15), grp_stift)
# Federbolzen-Löcher durch Rohr+Stift (beidseitig)
for xOff in [-STIFT_LAENGE/4, +STIFT_LAENGE/4]:
bohr = Part.makeCylinder(FEDER_BOLZEN_D / 2.0, STD_DA + 10)
bohr.Placement = App.Placement(
App.Vector(O_STIFT[0] + xOff, O_STIFT[1] - STD_DA/2 - 5, O_STIFT[2]),
App.Rotation(App.Vector(1, 0, 0), 90) # senkrecht durch Rohr
)
# Nur als Anschauung, nicht ausschneiden (Demo)
print(f" Stift: {STIFT_DA}×{STIFT_WAND} × {STIFT_LAENGE:.0f}mm")
print(f" Einstecktiefe: {STIFT_LAENGE/2:.0f}mm pro Seite")
print(f" Federbolzen: Ø{FEDER_BOLZEN_D:.0f}mm, {STIFT_LAENGE/4:.0f}mm vom Stoß")
# ============================================================
# CONNECTOR 2 EINZELTEILE (explodiert)
# ============================================================
print("\nErzeuge C2 Einzelteile (explodiert)...")
O_EXPL = (150, -120, 0)
einzelteile = [
((0, 0, -1), "C2_Teil_Ql", (0.7, 0.7, 0.2)),
((0, 0, +1), "C2_Teil_Qr", (0.7, 0.7, 0.2)),
(vDr, "C2_Teil_D", (0.2, 0.7, 0.4)),
(vB, "C2_Teil_B", (0.6, 0.3, 0.7)),
]
for i, (direction, name, col) in enumerate(einzelteile):
ox = O_EXPL[0] + (i - 1.5) * (HUEL_DA + 20)
oy = O_EXPL[1]
oz = O_EXPL[2]
h = make_huelse(direction, (ox, oy, oz))
b = make_federbolzen_bohrung(direction, (ox, oy, oz), HUEL_LAENGE * 0.6)
if b:
h = h.cut(b)
add_part(doc, h, name, col, grp_c2)
# ============================================================
# ABSCHLUSS
# ============================================================
doc.recompute()
try:
Gui.activeDocument().activeView().fitAll()
except Exception:
pass
print()
print(SEP)
print("KONNEKTOREN ERFOLGREICH ERSTELLT")
print(SEP)
print(f"""
Baugruppen im Dokument:
Connector_1 — C1 komplett + Demo-Rohre
Connector_2 — C2 komplett + Einzelteile (explodiert) + Demo-Rohre
Inline_Stift — Stift in zwei A-Rohren
Demo_Rohre — halbtransparente Alu-Rohre zur Veranschaulichung
Connector 1 (×24):
2+1 Hülsen: A-links, A-rechts (durchlaufend), D
Gehrungswinkel AD = {saege_winkel(vA, vD):.1f}°
Für Fuß: identisch, um 180° gedreht
Connector 2 (×6):
4 Hülsen: Q-links, Q-rechts, D (aufwärts), B (abwärts)
Gehrungswinkel QD = {saege_winkel(vQ, vDr):.1f}°
Gehrungswinkel DB = {saege_winkel(vDr, vB):.1f}°
Inline-Stift (×8):
Alu {STIFT_DA}×{STIFT_WAND} × {STIFT_LAENGE:.0f}mm
Je 2 Federbolzen Ø{FEDER_BOLZEN_D:.0f}mm
Tipp: Einzelne Gruppen ein-/ausblenden via Modellbaum (Leertaste).
""")

View File

@@ -0,0 +1,359 @@
"""
Feldbett Struktur v2 — FreeCAD Python Script
=============================================
Zeigt das komplette Feldbett mit allen Rohren in Realgröße.
A-Stangen sind segmentiert (je 350mm), Stift-Verbinder angedeutet.
Ausführen: FreeCAD → Makro → Makro ausführen → diese Datei wählen
Koordinatensystem:
X = Längsrichtung (Liegeachse)
Y = Höhe (oben = Liegefläche)
Z = Querrichtung (Breite)
Rohrtriplet "Komfortabel":
Standard : Alu 25×1.5 mm (Strukturrohre)
Stift : Alu 18×1.0 mm (Inline-Verbinder A-Stangen)
Hülse : Stahl 33.7×2.5 mm (Konnektoren C1/C2)
"""
import FreeCAD as App
import FreeCADGui as Gui
import Part
import math
# ============================================================
# PARAMETER
# ============================================================
# Geometrie
L = 350.0 # Stangenlänge [mm] — ALLE Stangen gleich
BREITE_AA = 700.0 # Abstand zwischen Längsstangen (AA) [mm]
MODULE = 3 # Anzahl Module
# Rohre — Triplet "Komfortabel"
# Standard (Alu 25×1.5)
STD_DA = 25.0 # Außendurchmesser [mm]
STD_WAND = 1.5 # Wandstärke [mm]
STD_DI = STD_DA - 2.0 * STD_WAND # 22.0 mm
# Stift-Verbinder (Alu 18×1.0)
STIFT_DA = 18.0
STIFT_WAND = 1.0
STIFT_DI = STIFT_DA - 2.0 * STIFT_WAND
STIFT_LAENGE = 100.0 # 50mm Einstecktiefe pro Seite
# Hülse / Konnektor (Stahl 33.7×2.5)
HUEL_DA = 33.7
HUEL_WAND = 2.5
HUEL_DI = HUEL_DA - 2.0 * HUEL_WAND # 28.7 mm
HUEL_LAENGE = 40.0 # Länge pro Hülsenstück
# Farben (R, G, B) je 0.01.0
FARBE_A = (0.20, 0.45, 0.75) # blau — Längsstangen
FARBE_D = (0.06, 0.43, 0.34) # grün — Diagonalen
FARBE_Q = (0.73, 0.46, 0.09) # amber — Querstreben
FARBE_B = (0.42, 0.25, 0.63) # lila — Beine
FARBE_STIFT = (0.85, 0.55, 0.15) # orange — Stift-Verbinder
FARBE_C1 = (0.85, 0.35, 0.15) # rot-orange — Connector 1
FARBE_C2 = (0.15, 0.55, 0.75) # blau-grün — Connector 2
# ============================================================
# BERECHNETE GEOMETRIE
# ============================================================
hwA = BREITE_AA / 2.0 # halbe Breite AA
hwQ = L / 2.0 # halbe Q-Länge (Q = L)
dZ = hwA - hwQ # Z-Einzug von A nach Q
legH2 = 0.75 * L**2 - dZ**2
if legH2 <= 0:
raise ValueError(
f"Geometrie unmöglich: L={L} zu kurz für Breite={BREITE_AA}. "
f"Erhöhe L oder reduziere BREITE_AA."
)
legH = math.sqrt(legH2) # Höhe pro Ebene (A→Q oder Q→Boden)
totalH = 2.0 * legH # Gesamthöhe
step = 2.0 * L # Modul-Abstand in X
aEnd = (MODULE - 1) * step + L # X-Ende der Längsstangen
# Kontrollrechnung D/B-Länge
dLen = math.sqrt((L / 2)**2 + legH**2 + dZ**2)
# A-Stangen Segmente
a_per_side = int(aEnd / L)
print("=" * 55)
print("FELDBETT STRUKTUR v2")
print("=" * 55)
print(f" Stangenlänge L = {L:.0f} mm")
print(f" Breite AA = {BREITE_AA:.0f} mm")
print(f" Liegelänge = {aEnd:.0f} mm = {aEnd/10:.0f} cm")
print(f" Höhe gesamt = {totalH:.0f} mm = {totalH/10:.1f} cm")
print(f" legH (pro Ebene) = {legH:.1f} mm")
print(f" D/B-Länge = {dLen:.1f} mm (soll = {L:.0f})")
print(f" Module = {MODULE}")
print(f" A-Segmente pro Seite = {a_per_side}")
print(f"\n Standard : {STD_DA}×{STD_WAND} mm (Alu)")
print(f" Stift : {STIFT_DA}×{STIFT_WAND} mm (Alu)")
print(f" Hülse : {HUEL_DA}×{HUEL_WAND} mm (Stahl)")
print(f" Spiel Hülse↔Std = {HUEL_DI - STD_DA:.1f} mm")
print(f" Spiel Std↔Stift = {STD_DI - STIFT_DA:.1f} mm")
print()
# ============================================================
# HILFSFUNKTIONEN
# ============================================================
def make_tube(p1, p2, da, wand, label, color, group=None):
"""Hohlrohr von p1 nach p2."""
direction = p2 - p1
length = direction.Length
if length < 0.01:
return None
di = da - 2.0 * wand
outer = Part.makeCylinder(da / 2.0, length)
inner = Part.makeCylinder(di / 2.0, length)
tube = outer.cut(inner)
z_axis = App.Vector(0, 0, 1)
norm_dir = direction.normalize()
cross = z_axis.cross(norm_dir)
dot = z_axis.dot(norm_dir)
if cross.Length > 1e-6:
angle = math.degrees(math.acos(max(-1.0, min(1.0, dot))))
rot = App.Rotation(cross, angle)
elif dot < 0:
rot = App.Rotation(App.Vector(1, 0, 0), 180)
else:
rot = App.Rotation()
tube.Placement = App.Placement(p1, rot)
obj = doc.addObject("Part::Feature", label)
obj.Shape = tube
if hasattr(obj, "ViewObject") and obj.ViewObject:
obj.ViewObject.ShapeColor = color
if group:
group.addObject(obj)
return obj
def vec(x, y, z):
return App.Vector(x, y, z)
def make_group(name):
return doc.addObject("App::DocumentObjectGroup", name)
# ============================================================
# DOKUMENT ANLEGEN
# ============================================================
doc_name = "Feldbett_v2"
if doc_name in App.listDocuments():
App.closeDocument(doc_name)
doc = App.newDocument(doc_name)
grp_A = make_group("A_Laengsstangen")
grp_Q = make_group("Q_Querstreben")
grp_D = make_group("D_Diagonalen")
grp_B = make_group("B_Beine")
grp_stift = make_group("Stift_Verbinder")
grp_c1 = make_group("Connector_1_Huelsen")
grp_c2 = make_group("Connector_2_Huelsen")
# ============================================================
# A — LÄNGSSTANGEN (segmentiert, 2× je a_per_side Stücke)
# ============================================================
for side, zS in [("L", -1), ("R", +1)]:
zA = zS * hwA
for seg in range(a_per_side):
x0 = seg * L
x1 = x0 + L
make_tube(
vec(x0, totalH, zA),
vec(x1, totalH, zA),
STD_DA, STD_WAND,
f"A_{side}_seg{seg+1}", FARBE_A, grp_A
)
# ============================================================
# STIFT-VERBINDER (A-Stangen inline, zwischen Segmenten)
# ============================================================
for side, zS in [("L", -1), ("R", +1)]:
zA = zS * hwA
for joint in range(a_per_side - 1):
xJ = (joint + 1) * L # Stoßstelle
x0 = xJ - STIFT_LAENGE / 2.0
x1 = xJ + STIFT_LAENGE / 2.0
make_tube(
vec(x0, totalH, zA),
vec(x1, totalH, zA),
STIFT_DA, STIFT_WAND,
f"Stift_{side}_{joint+1}", FARBE_STIFT, grp_stift
)
# ============================================================
# MODULE — Q, D, B + Konnektor-Hülsen
# ============================================================
for m in range(MODULE):
xStart = m * step
xAL = xStart # X linker A-Knoten
xAR = xStart + L # X rechter A-Knoten
xQ = xStart + L / 2 # X Mitte → Q-Position
suf = f"_M{m+1}"
# --- Q Querstrebe ---
make_tube(
vec(xQ, legH, -hwQ),
vec(xQ, legH, +hwQ),
STD_DA, STD_WAND,
"Q" + suf, FARBE_Q, grp_Q
)
# --- D Diagonalen (4 pro Modul) ---
for side, zS in [("L", -1), ("R", +1)]:
zA = zS * hwA
zQe = zS * hwQ
# linker A-Knoten → Q-Ende
make_tube(
vec(xAL, totalH, zA),
vec(xQ, legH, zQe),
STD_DA, STD_WAND,
f"D_AL_{side}{suf}", FARBE_D, grp_D
)
# rechter A-Knoten → Q-Ende
make_tube(
vec(xAR, totalH, zA),
vec(xQ, legH, zQe),
STD_DA, STD_WAND,
f"D_AR_{side}{suf}", FARBE_D, grp_D
)
# --- B Beine (4 pro Modul, gespiegelt) ---
for side, zS in [("L", -1), ("R", +1)]:
zA = zS * hwA
zQe = zS * hwQ
make_tube(
vec(xQ, legH, zQe),
vec(xAL, 0, zA),
STD_DA, STD_WAND,
f"B_AL_{side}{suf}", FARBE_B, grp_B
)
make_tube(
vec(xQ, legH, zQe),
vec(xAR, 0, zA),
STD_DA, STD_WAND,
f"B_AR_{side}{suf}", FARBE_B, grp_B
)
# --- Connector 2 Hülsen (an Q-Enden) ---
# Jedes Q-Ende bekommt 4 Hülsen: Q-links, Q-rechts, D(aufwärts), B(abwärts)
for zS in [-1, +1]:
zA = zS * hwA
zQe = zS * hwQ
qEnd = vec(xQ, legH, zQe)
side_label = "L" if zS < 0 else "R"
# Richtungsvektoren vom Q-Ende aus
# Q verläuft in ±Z → Hülsen zeigen in +Z und -Z
for qdir, qlabel in [((0,0,-1), "Ql"), ((0,0,+1), "Qr")]:
d = App.Vector(*qdir).normalize()
p1 = qEnd
p2 = vec(qEnd.x + d.x*HUEL_LAENGE, qEnd.y + d.y*HUEL_LAENGE, qEnd.z + d.z*HUEL_LAENGE)
make_tube(p1, p2, HUEL_DA, HUEL_WAND,
f"C2_{qlabel}_{side_label}{suf}", FARBE_C2, grp_c2)
# D-Richtung (vom Q-Ende zum A-Knoten oben = umgekehrte D-Richtung)
# Wir nehmen die Richtung zum linken A-Knoten als Referenz
vDr = vec(xAL - xQ, totalH - legH, zA - zQe).normalize()
p2 = vec(qEnd.x + vDr.x*HUEL_LAENGE, qEnd.y + vDr.y*HUEL_LAENGE, qEnd.z + vDr.z*HUEL_LAENGE)
make_tube(qEnd, p2, HUEL_DA, HUEL_WAND,
f"C2_D_{side_label}{suf}", FARBE_C2, grp_c2)
# B-Richtung (vom Q-Ende zum Bodenknoten)
vB = vec(xAL - xQ, 0 - legH, zA - zQe).normalize()
p2 = vec(qEnd.x + vB.x*HUEL_LAENGE, qEnd.y + vB.y*HUEL_LAENGE, qEnd.z + vB.z*HUEL_LAENGE)
make_tube(qEnd, p2, HUEL_DA, HUEL_WAND,
f"C2_B_{side_label}{suf}", FARBE_C2, grp_c2)
# --- Connector 1 Hülsen (an A-Knoten oben und Fußpunkten unten) ---
for xF in [xAL, xAR]:
for side, zS in [("L", -1), ("R", +1)]:
zA = zS * hwA
zQe = zS * hwQ
xi = int(xF)
# === C1 oben: am A-Knoten, verbindet A-Stange mit D-Diagonale ===
aNode = vec(xF, totalH, zA)
# Hülse entlang A (±X)
make_tube(
vec(xF - HUEL_LAENGE/2, totalH, zA),
vec(xF + HUEL_LAENGE/2, totalH, zA),
HUEL_DA, HUEL_WAND,
f"C1o_A_x{xi}_{side}{suf}", FARBE_C1, grp_c1
)
# Hülse entlang D (nach unten zum Q-Ende)
vD = vec(xQ - xF, legH - totalH, zQe - zA).normalize()
p2 = vec(aNode.x + vD.x*HUEL_LAENGE, aNode.y + vD.y*HUEL_LAENGE, aNode.z + vD.z*HUEL_LAENGE)
make_tube(aNode, p2, HUEL_DA, HUEL_WAND,
f"C1o_D_x{xi}_{side}{suf}", FARBE_C1, grp_c1)
# === C1 unten (Fuß): am Bodenknoten, verbindet B-Bein mit Bodenkontakt ===
fNode = vec(xF, 0, zA)
# Hülse für B (nach oben zum Q-Ende)
vBup = vec(xQ - xF, legH, zQe - zA).normalize()
p2 = vec(fNode.x + vBup.x*HUEL_LAENGE, fNode.y + vBup.y*HUEL_LAENGE, fNode.z + vBup.z*HUEL_LAENGE)
make_tube(fNode, p2, HUEL_DA, HUEL_WAND,
f"C1u_B_x{xi}_{side}{suf}", FARBE_C1, grp_c1)
# Hülse als Fuß (nach unten, senkrecht)
make_tube(
vec(xF, 0, zA),
vec(xF, -HUEL_LAENGE, zA),
HUEL_DA, HUEL_WAND,
f"C1u_F_x{xi}_{side}{suf}", FARBE_C1, grp_c1
)
# ============================================================
# ABSCHLUSS
# ============================================================
doc.recompute()
try:
Gui.activeDocument().activeView().fitAll()
Gui.SendMsgToActiveView("ViewFit")
except Exception:
pass
# Stückliste
print("STÜCKLISTE:")
print(f" {a_per_side * 2:>3}× A Längsstangen (Alu {STD_DA}×{STD_WAND} × {L:.0f}mm)")
print(f" {MODULE:>3}× Q Querstreben (Alu {STD_DA}×{STD_WAND} × {L:.0f}mm)")
print(f" {MODULE*4:>3}× D Diagonalen (Alu {STD_DA}×{STD_WAND} × {L:.0f}mm)")
print(f" {MODULE*4:>3}× B Beine (Alu {STD_DA}×{STD_WAND} × {L:.0f}mm)")
print(f" {'-'*50}")
print(f" {a_per_side*2 + MODULE + MODULE*8:>3}× Standardrohre gesamt")
print()
print(f" {(a_per_side-1)*2:>3}× Stift-Verbinder (Alu {STIFT_DA}×{STIFT_WAND} × {STIFT_LAENGE:.0f}mm)")
print(f" {24:>3}× Connector 1 (je 2 Hülsen Stahl {HUEL_DA}×{HUEL_WAND} × {HUEL_LAENGE:.0f}mm)")
print(f" { 6:>3}× Connector 2 (je 4 Hülsen Stahl {HUEL_DA}×{HUEL_WAND} × {HUEL_LAENGE:.0f}mm)")
print()
print(f" Objekte im Dokument: {len(doc.Objects)}")
print("\nFeldbett v2 erfolgreich erstellt!")

View File

@@ -0,0 +1,260 @@
"""
Feldbett Konstruktion - FreeCAD Python Script
=============================================
Ausführen in FreeCAD: Menü → Makro → Makro ausführen → diese Datei wählen
Koordinatensystem:
X = Längsrichtung (Liegeachse)
Y = Höhe
Z = Querrichtung (Breite)
Struktur:
A = Längsstangen (2x, durchgehend, Z=±hwA, Y=totalH)
Q = Querstreben (3x, quer in Z, kürzer als Breite, Y=legH)
D = Diagonalen (12x, A-Knoten → Q-Ende, 3D-diagonal)
B = Beine (12x, Q-Ende → Bodenknoten, gespiegelt zu D)
F = Füße (kurze senkrechte Stücke unter Bodenknoten)
"""
import FreeCAD as App
import FreeCADGui as Gui
import Part
import math
# ============================================================
# PARAMETER — hier anpassen
# ============================================================
L = 35.0 # Stangenlänge [mm] — ALLE Stangen gleich lang
BREITE_AA = 70.0 # Abstand zwischen den zwei Längsstangen (AA) [mm]
MODULE = 3 # Anzahl Module
# Rohr-Parameter
ROHR_DA = 25.0 # Außendurchmesser Rohr [mm]
ROHR_WAND = 2.0 # Wandstärke [mm]
FUSS_LAENGE = 10.0 # Länge der Fußstücke nach unten [mm]
# Farben (R, G, B) je 0.01.0
FARBE_A = (0.20, 0.45, 0.75) # blau
FARBE_D = (0.06, 0.43, 0.34) # grün
FARBE_Q = (0.73, 0.46, 0.09) # amber
FARBE_B = (0.42, 0.25, 0.63) # lila
FARBE_F = (0.50, 0.50, 0.50) # grau
# ============================================================
# BERECHNETE GEOMETRIE
# ============================================================
hwA = BREITE_AA / 2.0 # halbe Breite AA
hwQ = L / 2.0 # halbe Q-Länge (Q = L lang)
dZ = hwA - hwQ # Z-Einzug von A nach Q
legH2 = 0.75 * L**2 - dZ**2
if legH2 <= 0:
raise ValueError(
f"Geometrie unmöglich: L={L} zu kurz für Breite={BREITE_AA}. "
f"Mindest-L = {math.sqrt(dZ**2/0.75 + dZ**2/0.75*0.25):.1f} mm"
)
legH = math.sqrt(legH2) # Höhe einer Ebene (A→Q oder Q→Boden)
totalH = 2.0 * legH # Gesamthöhe
step = 2.0 * L # Abstand Modul-zu-Modul (inkl. Lücke)
aEnd = (MODULE - 1) * step + L # X-Ende der Längsstangen
# Kontrollrechnung D-Länge
dLen = math.sqrt((L/2)**2 + legH**2 + dZ**2)
print("=" * 50)
print("FELDBETT GEOMETRIE")
print("=" * 50)
print(f" Stangenlänge L = {L:.1f} mm")
print(f" Breite AA = {BREITE_AA:.1f} mm")
print(f" Q-Länge = {L:.1f} mm (= L ✓)")
print(f" Liegelänge (A-Ende) = {aEnd:.1f} mm")
print(f" Höhe gesamt = {totalH:.1f} mm")
print(f" legH (pro Ebene) = {legH:.2f} mm")
print(f" D/B-Länge = {dLen:.2f} mm (sollte = {L:.1f})")
print(f" Module = {MODULE}")
print(f" Rohr DA/Wand = {ROHR_DA}/{ROHR_WAND} mm")
print()
# ============================================================
# HILFSFUNKTIONEN
# ============================================================
ROHR_DI = ROHR_DA - 2.0 * ROHR_WAND # Innendurchmesser
def make_tube(p1, p2, label, color):
"""
Erzeugt ein Hohlrohr von Punkt p1 nach p2.
p1, p2: FreeCAD.Vector
"""
direction = p2 - p1
length = direction.Length
if length < 0.01:
return None
# Außenzylinder
outer = Part.makeCylinder(ROHR_DA / 2.0, length)
# Innenzylinder (Hohlraum)
inner = Part.makeCylinder(ROHR_DI / 2.0, length)
tube = outer.cut(inner)
# Ausrichten: Zylinder liegt standardmäßig entlang Z-Achse
# → Rotation auf Zielrichtung
z_axis = App.Vector(0, 0, 1)
norm_dir = direction.normalize()
cross = z_axis.cross(norm_dir)
dot = z_axis.dot(norm_dir)
if cross.Length > 1e-6:
angle = math.degrees(math.acos(max(-1.0, min(1.0, dot))))
rot = App.Rotation(cross, angle)
elif dot < 0:
# 180° Rotation um beliebige Querachse
rot = App.Rotation(App.Vector(1, 0, 0), 180)
else:
rot = App.Rotation()
placement = App.Placement(p1, rot)
tube.Placement = placement
# FreeCAD Objekt anlegen
obj = doc.addObject("Part::Feature", label)
obj.Shape = tube
# Farbe setzen
if hasattr(obj, "ViewObject") and obj.ViewObject:
obj.ViewObject.ShapeColor = color
return obj
def vec(x, y, z):
return App.Vector(x, y, z)
# ============================================================
# DOKUMENT ANLEGEN
# ============================================================
doc_name = "Feldbett"
if doc_name in App.listDocuments():
App.closeDocument(doc_name)
doc = App.newDocument(doc_name)
# ============================================================
# LÄNGSSTANGEN A (2x durchgehend)
# ============================================================
make_tube(
vec(0, totalH, -hwA),
vec(aEnd, totalH, -hwA),
"A_links", FARBE_A
)
make_tube(
vec(0, totalH, +hwA),
vec(aEnd, totalH, +hwA),
"A_rechts", FARBE_A
)
# ============================================================
# MODULE
# ============================================================
for m in range(MODULE):
xStart = m * step
xAL = xStart # X linker A-Knoten
xAR = xStart + L # X rechter A-Knoten
xQ = xStart + L / 2 # X Mitte → Q-Position
suffix = f"_M{m+1}"
# --- Querstrebe Q ---
make_tube(
vec(xQ, legH, -hwQ),
vec(xQ, legH, +hwQ),
"Q" + suffix, FARBE_Q
)
# --- Diagonalen D (4 pro Modul) ---
# Von A-Knoten (oben, außen) nach Q-Ende (mitte, innen)
for side, zS in [("L", -1), ("R", +1)]:
zA = zS * hwA
zQe = zS * hwQ
# linker A-Knoten → Q-Ende
make_tube(
vec(xAL, totalH, zA),
vec(xQ, legH, zQe),
f"D_xAL_{side}{suffix}", FARBE_D
)
# rechter A-Knoten → Q-Ende
make_tube(
vec(xAR, totalH, zA),
vec(xQ, legH, zQe),
f"D_xAR_{side}{suffix}", FARBE_D
)
# --- Beine B (4 pro Modul, gespiegelt zu D) ---
for side, zS in [("L", -1), ("R", +1)]:
zA = zS * hwA
zQe = zS * hwQ
# Q-Ende → linker Bodenknoten
make_tube(
vec(xQ, legH, zQe),
vec(xAL, 0, zA),
f"B_xAL_{side}{suffix}", FARBE_B
)
# Q-Ende → rechter Bodenknoten
make_tube(
vec(xQ, legH, zQe),
vec(xAR, 0, zA),
f"B_xAR_{side}{suffix}", FARBE_B
)
# --- Füße F (4 pro Modul, senkrecht nach unten) ---
for xF in [xAL, xAR]:
for zS in [-1, +1]:
zA = zS * hwA
make_tube(
vec(xF, 0, zA),
vec(xF, -FUSS_LAENGE, zA),
f"F_x{int(xF)}_z{int(zA)}{suffix}", FARBE_F
)
# ============================================================
# ABSCHLUSS
# ============================================================
doc.recompute()
# Kamera auf Objekt ausrichten
try:
Gui.activeDocument().activeView().fitAll()
Gui.SendMsgToActiveView("ViewFit")
except Exception:
pass
print("Feldbett erfolgreich erstellt!")
print(f" Objekte im Dokument: {len(doc.Objects)}")
print()
print("STÜCKLISTE:")
print(f" A Längsstangen : 2 Stück à {aEnd:.0f} mm")
print(f" Q Querstreben : {MODULE} Stück à {L:.0f} mm")
print(f" D Diagonalen : {MODULE*4} Stück à {dLen:.1f} mm")
print(f" B Beine : {MODULE*4} Stück à {dLen:.1f} mm")
print(f" F Füße : {MODULE*4} Stück à {FUSS_LAENGE:.0f} mm")
print()
print("CONNECTOR-WINKEL:")
alpha_AD = math.degrees(math.acos(
abs(App.Vector(1,0,0).dot(App.Vector(L/2,-legH,-dZ).normalize()))
))
print(f" Connector 1 — Winkel A↔D : {alpha_AD:.1f}°")
print(f" Connector 2 — Winkel Q↔D : 60.0°")
print(f" Connector 2 — Winkel D↔B : 90.0°")

View File

@@ -0,0 +1,316 @@
"""
Feldbett Connectors - FreeCAD Python Script
============================================
Erzeugt Connector 1 und Connector 2 als separate 3D-Körper
(geeignet für 3D-Druck oder Aluminiumguss).
Ausführen in FreeCAD: Menü → Makro → Makro ausführen
Connector 1: A-Stange + eine Diagonale (D oder B)
Winkel A↔D = 60°, D ist 3D-diagonal
Kann gedreht als Standfuß verwendet werden.
Connector 2: Q-Stange (durchlaufend) + D (oben) + B (unten)
Q↔D = 60°, Q↔B = 60°, D↔B = 90°
"""
import FreeCAD as App
import FreeCADGui as Gui
import Part
import math
# ============================================================
# PARAMETER
# ============================================================
L = 35.0 # Stangenlänge [mm]
BREITE_AA = 70.0 # Abstand AA [mm]
ROHR_DA = 25.0 # Rohr Außendurchmesser [mm]
ROHR_WAND = 2.0 # Rohr Wandstärke [mm]
ROHR_DI = ROHR_DA - 2.0 * ROHR_WAND
# Connector-Körper
HUB_RADIUS = ROHR_DA * 1.4 # Radius des zentralen Knotens [mm]
STUTZEN_LAENGE = ROHR_DA * 2.0 # Länge der Rohrstutzen am Connector [mm]
WAND_STARK = 4.0 # Wandstärke der Stutzen [mm]
STUTZEN_DA = ROHR_DA + WAND_STARK * 2 # Außendurchmesser Stutzen
# Passungsspiel (Rohr soll einsteckbar sein)
SPIEL = 0.3 # [mm] radiales Spiel
# ============================================================
# GEOMETRIE
# ============================================================
hwA = BREITE_AA / 2.0
hwQ = L / 2.0
dZ = hwA - hwQ
legH2 = 0.75 * L**2 - dZ**2
if legH2 <= 0:
raise ValueError(f"Geometrie unmöglich: L={L} zu kurz.")
legH = math.sqrt(legH2)
# Richtungsvektoren (normiert)
def norm(v):
l = math.sqrt(sum(x**2 for x in v))
return tuple(x/l for x in v)
def fv(t):
"""Tuple → FreeCAD.Vector"""
return App.Vector(t[0], t[1], t[2])
# D: von A-Knoten nach Q-Ende (deltaX=+L/2, deltaY=-legH, deltaZ=-dZ)
vD_raw = ( L/2, -legH, -dZ)
vD = norm(vD_raw)
# B: von Q-Ende nach Bodenknoten (gespiegelt X und Z)
vB_raw = (-L/2, -legH, +dZ)
vB = norm(vB_raw)
# A: X-Richtung
vA = (1.0, 0.0, 0.0)
# Q: Z-Richtung
vQ = (0.0, 0.0, 1.0)
# Kontrollwinkel
def angle_between(v1, v2):
dot = sum(a*b for a,b in zip(v1,v2))
dot = max(-1.0, min(1.0, dot))
return math.degrees(math.acos(abs(dot)))
print("CONNECTOR WINKEL:")
print(f" A ↔ D : {angle_between(vA, vD):.1f}° (soll 60°)")
print(f" Q ↔ D : {angle_between(vQ, vD):.1f}° (soll 60°)")
print(f" Q ↔ B : {angle_between(vQ, vB):.1f}° (soll 60°)")
print(f" D ↔ B : {angle_between(vD, vB):.1f}° (soll 90°)")
# ============================================================
# HILFSFUNKTIONEN GEOMETRIE
# ============================================================
def rotation_to_direction(direction):
"""
Gibt App.Rotation zurück die Z-Achse auf 'direction' dreht.
FreeCAD-Zylinder liegen standardmäßig entlang +Z.
"""
z = App.Vector(0, 0, 1)
d = App.Vector(*direction).normalize()
cross = z.cross(d)
dot = z.dot(d)
if cross.Length > 1e-6:
angle = math.degrees(math.acos(max(-1.0, min(1.0, dot))))
return App.Rotation(cross, angle)
elif dot < 0:
return App.Rotation(App.Vector(1, 0, 0), 180)
else:
return App.Rotation()
def make_cylinder_along(direction, length, radius, origin=(0,0,0)):
"""Zylinder entlang 'direction', startend bei 'origin'."""
cyl = Part.makeCylinder(radius, length)
rot = rotation_to_direction(direction)
cyl.Placement = App.Placement(App.Vector(*origin), rot)
return cyl
def make_stutzen(direction, outer_r, inner_r, length, origin=(0,0,0)):
"""Hohler Stutzen (Rohraufnahme) entlang direction."""
outer = make_cylinder_along(direction, length, outer_r, origin)
inner = make_cylinder_along(direction, length + 1.0, inner_r, origin)
return outer.cut(inner)
def make_hub(radius, origin=(0,0,0)):
"""Kugelförmiger Zentralknoten."""
sphere = Part.makeSphere(radius)
sphere.Placement = App.Placement(App.Vector(*origin), App.Rotation())
return sphere
def add_part(doc, shape, name, color):
obj = doc.addObject("Part::Feature", name)
obj.Shape = shape
if obj.ViewObject:
obj.ViewObject.ShapeColor = color
return obj
# ============================================================
# DOKUMENT
# ============================================================
doc_name = "Feldbett_Connectors"
if doc_name in App.listDocuments():
App.closeDocument(doc_name)
doc = App.newDocument(doc_name)
# Farben
C1_COL = (0.85, 0.35, 0.15) # orange-rot für Connector 1
C2_COL = (0.15, 0.55, 0.75) # blau-grün für Connector 2
# Bohrungsradius = Rohr-Außenradius + Spiel
BOHR_R = ROHR_DA / 2.0 + SPIEL
# ============================================================
# CONNECTOR 1
# Zentralknoten + Stutzen für A + Stutzen für D
# Platziert bei Ursprung (0,0,0)
# ============================================================
print("\nErzeuge Connector 1...")
# Zentraler Kugelknoten
c1_hub = make_hub(HUB_RADIUS)
# Stutzen A: beide Richtungen (+X und -X) — Rohr läuft durch
c1_stA_pos = make_stutzen(
(+1, 0, 0), STUTZEN_DA/2, BOHR_R, STUTZEN_LAENGE
)
c1_stA_neg = make_stutzen(
(-1, 0, 0), STUTZEN_DA/2, BOHR_R, STUTZEN_LAENGE
)
# Stutzen D: in D-Richtung
c1_stD = make_stutzen(
vD, STUTZEN_DA/2, BOHR_R, STUTZEN_LAENGE
)
# Zusammenführen
c1_body = c1_hub.fuse(c1_stA_pos).fuse(c1_stA_neg).fuse(c1_stD)
# Durchgangsbohrung A (X-Achse, durchgehend)
bohr_A = make_cylinder_along(
(1, 0, 0), STUTZEN_LAENGE * 2 + HUB_RADIUS * 2,
BOHR_R,
(-STUTZEN_LAENGE - HUB_RADIUS, 0, 0)
)
# Bohrung D
bohr_D = make_cylinder_along(
vD, STUTZEN_LAENGE + HUB_RADIUS + 1.0,
BOHR_R
)
c1_final = c1_body.cut(bohr_A).cut(bohr_D)
# Abgerundete Kanten (optional, macht Druck/Guss besser)
try:
c1_final = c1_final.makeFillet(1.5, c1_final.Edges)
except Exception:
pass # Fillet kann bei komplexen Geometrien fehlschlagen
# Connector 1 einfügen — bei X=0 (normale Position oben)
c1_obj = add_part(doc, c1_final, "Connector_1_oben", C1_COL)
# Connector 1 als Fuß — um 180° um Z gedreht, versetzt
c1_fuss = c1_final.copy()
rot180 = App.Placement(
App.Vector(200, 0, 0),
App.Rotation(App.Vector(0, 0, 1), 180)
)
c1_fuss.Placement = rot180
c1_fuss_obj = add_part(doc, c1_fuss, "Connector_1_fuss_gedreht", C1_COL)
print(f" Connector 1 erstellt — {STUTZEN_LAENGE:.0f}mm Stutzen, {HUB_RADIUS:.1f}mm Hub-Radius")
# ============================================================
# CONNECTOR 2
# Zentralknoten + Stutzen für Q (beide Richtungen) + D + B
# Platziert bei Y=200 (Abstand zu C1)
# ============================================================
print("Erzeuge Connector 2...")
O2 = (0, 200, 0) # Versatz damit C1 und C2 sich nicht überlappen
# Zentraler Kugelknoten
c2_hub = make_hub(HUB_RADIUS, O2)
# Stutzen Q: beide Richtungen (+Z und -Z)
c2_stQ_pos = make_stutzen(
(0, 0, +1), STUTZEN_DA/2, BOHR_R, STUTZEN_LAENGE,
O2
)
c2_stQ_neg = make_stutzen(
(0, 0, -1), STUTZEN_DA/2, BOHR_R, STUTZEN_LAENGE,
O2
)
# Stutzen D: D-Richtung umgekehrt (von Q-Ende Richtung A-Knoten = aufwärts)
vDr = (-vD[0], -vD[1], -vD[2]) # umgekehrt
c2_stD = make_stutzen(
vDr, STUTZEN_DA/2, BOHR_R, STUTZEN_LAENGE,
O2
)
# Stutzen B: B-Richtung
c2_stB = make_stutzen(
vB, STUTZEN_DA/2, BOHR_R, STUTZEN_LAENGE,
O2
)
# Zusammenführen
c2_body = (c2_hub
.fuse(c2_stQ_pos)
.fuse(c2_stQ_neg)
.fuse(c2_stD)
.fuse(c2_stB))
# Bohrungen
# Q durchgehend (Z-Achse)
bohr_Q = make_cylinder_along(
(0, 0, 1),
STUTZEN_LAENGE * 2 + HUB_RADIUS * 2,
BOHR_R,
(O2[0], O2[1], O2[2] - STUTZEN_LAENGE - HUB_RADIUS)
)
# D-Bohrung
bohr_D2 = make_cylinder_along(
vDr, STUTZEN_LAENGE + HUB_RADIUS + 1.0,
BOHR_R, O2
)
# B-Bohrung
bohr_B2 = make_cylinder_along(
vB, STUTZEN_LAENGE + HUB_RADIUS + 1.0,
BOHR_R, O2
)
c2_final = c2_body.cut(bohr_Q).cut(bohr_D2).cut(bohr_B2)
try:
c2_final = c2_final.makeFillet(1.5, c2_final.Edges)
except Exception:
pass
c2_obj = add_part(doc, c2_final, "Connector_2", C2_COL)
print(f" Connector 2 erstellt — 3 Stutzen (Q durch, D+B je 60°)")
# ============================================================
# ABSCHLUSS
# ============================================================
doc.recompute()
try:
Gui.activeDocument().activeView().fitAll()
except Exception:
pass
print("\n" + "="*50)
print("CONNECTORS ERFOLGREICH ERSTELLT")
print("="*50)
print(f"\nConnector 1 (×16 pro Bett):")
print(f" Hub-Radius : {HUB_RADIUS:.1f} mm")
print(f" Stutzen-L : {STUTZEN_LAENGE:.1f} mm")
print(f" Stutzen-DA : {STUTZEN_DA:.1f} mm")
print(f" Bohrung Ø : {BOHR_R*2:.1f} mm (Rohr DA={ROHR_DA} + Spiel={SPIEL*2:.1f})")
print(f" Winkel A↔D : {angle_between(vA, vD):.1f}°")
print(f"\nConnector 2 (×6 pro Bett):")
print(f" Hub-Radius : {HUB_RADIUS:.1f} mm")
print(f" Stutzen-L : {STUTZEN_LAENGE:.1f} mm")
print(f" Winkel Q↔D : {angle_between(vQ, vD):.1f}°")
print(f" Winkel Q↔B : {angle_between(vQ, vB):.1f}°")
print(f" Winkel D↔B : {angle_between(vD, vB):.1f}°")
print(f"\nExport-Tipp:")
print(f" Für 3D-Druck: Datei → Export → .stl wählen")
print(f" Für Guss: Datei → Export → .step wählen")

View File

@@ -0,0 +1,436 @@
"""
Feldbett Connectors v2 - FreeCAD Python Script
===============================================
Erzeugt Connector 1 und Connector 2 als schweißbare Einzelteile.
Connector 1: Hülse für A-Stange + Hülse für D oder B
→ 2 Teile, auf Gehrung gesägt und zusammengeschweißt
→ gedreht verwendbar als Standfuß
Connector 2: 5 Hülsen (Q links, Q rechts, D, B, optional Verstärkung)
→ auf Gehrung gesägt, sternförmig zusammengeschweißt
Ausführen in FreeCAD: Makro → Makro ausführen → diese Datei wählen
Konsolen-Output zeigt Sägewinkel, Mindestlängen und Warnungen.
"""
import FreeCAD as App
import FreeCADGui as Gui
import Part
import math
# ============================================================
# PARAMETER — hier anpassen
# ============================================================
# --- Feldbett-Geometrie ---
L = 35.0 # Stangenlänge [mm], alle Stangen gleich
BREITE_AA = 70.0 # Abstand Längsstange zu Längsstange [mm]
MODULE = 3 # Anzahl Module (nur für Info)
# --- Rohr Konstruktionsprofil ---
ROHR_TYP = "rund" # "rund" oder "vierkant"
ROHR_DA = 25.0 # Außendurchmesser (Rund) oder Außenmaß (Vierkant) [mm]
ROHR_WAND = 2.0 # Wandstärke [mm]
# --- Connector Hülsen ---
# Die Hülse ist ein kurzes Stück GRÖSSERES Rohr das über das Konstruktionsrohr passt
HUELSE_DA = 32.0 # Außendurchmesser der Hülse [mm]
# Empfehlung: ROHR_DA + 2×WAND_HUELSE + 2×SPIEL
HUELSE_WAND = 3.0 # Wandstärke der Hülse [mm]
EINSTECK_TIEFE = 45.0 # Wie weit das Rohr in die Hülse steckt [mm]
# Faustregel: ≥1.5×ROHR_DA geschweißt, ≥2.5×ROHR_DA nur Spannstift
SPIEL = 0.4 # Radiales Passspiel Rohr→Hülse [mm]
# --- Spannstift ---
SPANNSTIFT_D = 6.0 # Spannstift-Durchmesser [mm] (typisch ROHR_DA/4)
# 0 = kein Spannstift-Loch generieren
# --- Belastung (für Mindestlängen-Berechnung) ---
LAST_KG = 200.0 # Maximale Personenlast [kg]
SICHERHEIT = 2.0 # Sicherheitsfaktor (2 = doppelte Reserve)
# --- Material ---
# Streckgrenze [N/mm²]: Stahl S235=235, S355=355, Alu 6060=150, Alu 6082=260
STRECKGRENZE = 235.0 # [N/mm²]
# ============================================================
# BERECHNETE GEOMETRIE
# ============================================================
hwA = BREITE_AA / 2.0
hwQ = L / 2.0
dZ = hwA - hwQ
legH2 = 0.75 * L**2 - dZ**2
if legH2 <= 0:
raise ValueError(
f"Geometrie unmöglich: L={L}mm zu kurz für Breite={BREITE_AA}mm.\n"
f"Mindest-L = {math.sqrt(dZ**2/0.75 + 1):.1f}mm"
)
legH = math.sqrt(legH2)
totalH = 2.0 * legH
ROHR_DI = ROHR_DA - 2.0 * ROHR_WAND
HUELSE_DI = ROHR_DA + 2.0 * SPIEL # Innenbohrung der Hülse = Rohr-DA + Spiel
# Normierte Richtungsvektoren
def norm(v):
l = math.sqrt(sum(x**2 for x in v))
return tuple(x/l for x in v)
vA = (1.0, 0.0, 0.0)
vQ = (0.0, 0.0, 1.0)
vD = norm(( L/2, -legH, -dZ)) # A-Knoten → Q-Ende
vDr = norm((-L/2, legH, dZ)) # umgekehrt (von Q weg nach oben)
vB = norm((-L/2, -legH, dZ)) # Q-Ende → Bodenknoten
def angle_deg(v1, v2):
dot = sum(a*b for a,b in zip(v1,v2))
return math.degrees(math.acos(max(-1.0, min(1.0, abs(dot)))))
def angle_signed(v1, v2):
"""Winkel mit Vorzeichen, nicht abs."""
dot = sum(a*b for a,b in zip(v1,v2))
return math.degrees(math.acos(max(-1.0, min(1.0, dot))))
# Sägewinkel = Winkel zwischen Stutzen-Achse und der Schweißebene
# Schweißebene ist senkrecht zur Winkelhalbierenden zweier Stutzen
def saege_winkel(v1, v2):
"""
Gehrungswinkel für zwei Rohre die stumpf zusammengeschweißt werden.
Jedes Rohr wird um diesen Winkel von 90° abgesägt.
= 90° - (Winkel_zwischen / 2)
"""
alpha = angle_signed(v1, v2)
return 90.0 - alpha / 2.0
# ============================================================
# MINDESTLÄNGEN-BERECHNUNG (Hebelkraft)
# ============================================================
g = 9.81 # m/s²
F_gesamt = LAST_KG * g # Newton
# Kräfteverteilung: 3 Module, 2 Seiten, 2 Beine pro Modul = 12 Beine
# Vereinfacht: jeder Connector trägt gleichmäßig
F_pro_connector = F_gesamt / (MODULE * 4) # N pro unterem Connector
# Biegemoment am Stutzen-Eingang
# M = F × Hebel, Hebel ≈ halbe Rohrlänge (konservativ)
M_biege = F_pro_connector * (L / 2.0) # N·mm
# Widerstandsmoment Hohlrohr
W_rohr = (math.pi / 32.0) * (ROHR_DA**4 - ROHR_DI**4) / ROHR_DA # mm³
# Biegespannung
sigma_biege = M_biege / W_rohr # N/mm²
# Flächenpressung in der Hülse
# p = M / (L_ein × ROHR_DA) × Sicherheit ≤ Streckgrenze / 2 (Lochleibung)
sigma_zulaessig = STRECKGRENZE / SICHERHEIT
# Mindest-Einstecktiefe aus Lochleibung
L_ein_min_lb = (M_biege * SICHERHEIT) / (ROHR_DA * sigma_zulaessig)
# Mindest-Einstecktiefe aus Faustregel
L_ein_min_faust_schweiss = 1.5 * ROHR_DA
L_ein_min_faust_spannstift = 2.5 * ROHR_DA
L_ein_min = max(L_ein_min_lb, L_ein_min_faust_schweiss)
# ============================================================
# AUSGABE KONSOLE
# ============================================================
SEP = "=" * 55
print(SEP)
print("FELDBETT CONNECTOR — BERECHNUNGSPROTOKOLL")
print(SEP)
print(f"\nGeometrie:")
print(f" L (Stangenlänge) = {L:.1f} mm")
print(f" Breite AA = {BREITE_AA:.1f} mm")
print(f" Höhe gesamt = {totalH:.1f} mm")
print(f" legH (pro Ebene) = {legH:.2f} mm")
print(f"\nRohr:")
print(f" Typ = {ROHR_TYP}")
print(f" DA / Wand / DI = {ROHR_DA:.1f} / {ROHR_WAND:.1f} / {ROHR_DI:.1f} mm")
print(f"\nHülse:")
print(f" DA / Wand / DI = {HUELSE_DA:.1f} / {HUELSE_WAND:.1f} / {HUELSE_DI:.1f} mm")
print(f" Einstecktiefe = {EINSTECK_TIEFE:.1f} mm")
print(f" Spiel = {SPIEL:.2f} mm")
print(f"\nWinkel:")
print(f" A ↔ D = {angle_deg(vA, vD):.2f}°")
print(f" Q ↔ D = {angle_deg(vQ, vD):.2f}°")
print(f" Q ↔ B = {angle_deg(vQ, vB):.2f}°")
print(f" D ↔ B = {angle_deg(vD, vB):.2f}°")
print(f"\nSägewinkel (Gehrung, von 90° abweichend):")
print(f" Connector 1: AD = {saege_winkel(vA, vD):.2f}° (jedes Teil um diesen Winkel sägen)")
print(f" Connector 2: QD = {saege_winkel(vQ, vDr):.2f}°")
print(f" Connector 2: QB = {saege_winkel(vQ, vB):.2f}°")
print(f" Connector 2: DB = {saege_winkel(vDr, vB):.2f}°")
print(f"\nBelastungsrechnung (konservativ):")
print(f" Gesamtlast = {LAST_KG:.0f} kg × {SICHERHEIT:.0f} = {LAST_KG*SICHERHEIT:.0f} kg Auslegung")
print(f" Kraft pro Connector = {F_pro_connector:.1f} N")
print(f" Biegemoment Stutzen = {M_biege:.0f} N·mm")
print(f" Biegespannung Rohr = {sigma_biege:.1f} N/mm² (Streckgrenze: {STRECKGRENZE:.0f})")
print(f" Mindest-Einstecktiefe (Lochleibung) = {L_ein_min_lb:.1f} mm")
print(f" Mindest-Einstecktiefe (geschweißt) = {L_ein_min_faust_schweiss:.1f} mm")
print(f" Mindest-Einstecktiefe (nur Stift) = {L_ein_min_faust_spannstift:.1f} mm")
# Warnungen
print()
warn = False
if EINSTECK_TIEFE < L_ein_min:
print(f" ⚠ WARNUNG: Einstecktiefe {EINSTECK_TIEFE:.1f}mm < Minimum {L_ein_min:.1f}mm!")
print(f" → Erhöhe EINSTECK_TIEFE auf mindestens {math.ceil(L_ein_min/5)*5:.0f}mm")
warn = True
if HUELSE_DI < ROHR_DA + 2 * SPIEL - 0.1:
print(f" ⚠ WARNUNG: Hülsen-Innenmaß {HUELSE_DI:.1f}mm zu klein für Rohr {ROHR_DA:.1f}mm + Spiel!")
warn = True
if HUELSE_DA > L:
print(f" ⚠ WARNUNG: Hülsen-DA {HUELSE_DA:.1f}mm > L {L:.1f}mm — Connector größer als Stange!")
warn = True
if SPANNSTIFT_D > 0 and SPANNSTIFT_D > ROHR_DA / 3:
print(f" ⚠ WARNUNG: Spannstift Ø{SPANNSTIFT_D:.1f}mm > DA/3 — schwächt Rohr zu stark!")
warn = True
if not warn:
print(f" ✓ Alle Parameter plausibel.")
print()
# ============================================================
# HILFSFUNKTIONEN FreeCAD
# ============================================================
def fv(t):
return App.Vector(t[0], t[1], t[2])
def rotation_to_dir(direction):
z = App.Vector(0, 0, 1)
d = App.Vector(*direction).normalize()
cross = z.cross(d)
dot = z.dot(d)
if cross.Length > 1e-6:
angle = math.degrees(math.acos(max(-1.0, min(1.0, dot))))
return App.Rotation(cross, angle)
elif dot < 0:
return App.Rotation(App.Vector(1, 0, 0), 180)
else:
return App.Rotation()
def make_huelse(direction, origin=(0,0,0)):
"""
Hohle Hülse (kurzes Rohr das über Konstruktionsrohr passt).
Länge = EINSTECK_TIEFE.
Außen = HUELSE_DA, Innen = HUELSE_DI.
"""
outer = Part.makeCylinder(HUELSE_DA / 2.0, EINSTECK_TIEFE)
inner = Part.makeCylinder(HUELSE_DI / 2.0, EINSTECK_TIEFE + 1.0)
tube = outer.cut(inner)
rot = rotation_to_dir(direction)
tube.Placement = App.Placement(App.Vector(*origin), rot)
return tube
def make_spannstift_bohrung(direction, origin=(0,0,0)):
"""
Quer-Bohrung für Spannstift durch Hülse + Rohr.
Sitzt bei EINSTECK_TIEFE/2 vom Hülsen-Eingang.
"""
if SPANNSTIFT_D <= 0:
return None
# Mittelpunkt der Bohrung: entlang direction bei L/2
d = App.Vector(*direction).normalize()
mid = App.Vector(
origin[0] + d.x * EINSTECK_TIEFE / 2.0,
origin[1] + d.y * EINSTECK_TIEFE / 2.0,
origin[2] + d.z * EINSTECK_TIEFE / 2.0,
)
# Bohrungsrichtung: senkrecht zu direction (nehme Kreuzprodukt mit Y oder X)
ref = App.Vector(0, 1, 0) if abs(d.dot(App.Vector(0,1,0))) < 0.9 else App.Vector(1,0,0)
bohr_dir = d.cross(ref).normalize()
# Langer Zylinder quer durch
bohr_len = HUELSE_DA + 4.0
start = App.Vector(
mid.x - bohr_dir.x * bohr_len / 2.0,
mid.y - bohr_dir.y * bohr_len / 2.0,
mid.z - bohr_dir.z * bohr_len / 2.0,
)
cyl = Part.makeCylinder(SPANNSTIFT_D / 2.0, bohr_len)
rot = rotation_to_dir((bohr_dir.x, bohr_dir.y, bohr_dir.z))
cyl.Placement = App.Placement(start, rot)
return cyl
def add_part(doc, shape, name, color):
obj = doc.addObject("Part::Feature", name)
obj.Shape = shape
if obj.ViewObject:
obj.ViewObject.ShapeColor = color
obj.ViewObject.Transparency = 0
return obj
# ============================================================
# DOKUMENT
# ============================================================
doc_name = "Feldbett_Connectors_v2"
if doc_name in App.listDocuments():
App.closeDocument(doc_name)
doc = App.newDocument(doc_name)
C1_COL = (0.85, 0.35, 0.15)
C2_COL = (0.15, 0.55, 0.75)
BOHR_COL = (0.9, 0.1, 0.1)
# ============================================================
# CONNECTOR 1 — 2 Hülsen
# Hülse A (in X-Richtung) + Hülse D
# Die zwei Hülsen werden einzeln dargestellt
# (im echten Bau: auf Gehrung sägen und zusammenschweißen)
# ============================================================
print("Erzeuge Connector 1...")
OFFSET_C1 = (0, 0, 0)
# Hülse A — in +X Richtung (Rohr kommt von -X rein)
h_A = make_huelse((1, 0, 0), OFFSET_C1)
# Hülse D — in D-Richtung
h_D = make_huelse(vD, OFFSET_C1)
# Spannstift-Bohrungen
b_A = make_spannstift_bohrung((1, 0, 0), OFFSET_C1)
b_D = make_spannstift_bohrung(vD, OFFSET_C1)
# Hülsen zusammenfügen (schweißt man in der Praxis)
c1 = h_A.fuse(h_D)
if b_A: c1 = c1.cut(b_A)
if b_D: c1 = c1.cut(b_D)
try:
c1 = c1.makeFillet(1.0, c1.Edges)
except Exception:
pass
add_part(doc, c1, "C1_oben_A_plus_D", C1_COL)
# Connector 1 als Fuß: gleiche Geometrie, 180° gedreht
# (D-Aufnahme zeigt nach oben, A-Aufnahme wird Standfuß)
c1_fuss = c1.copy()
c1_fuss.Placement = App.Placement(
App.Vector(EINSTECK_TIEFE * 2 + 20, 0, 0),
App.Rotation(App.Vector(0, 0, 1), 180)
)
add_part(doc, c1_fuss, "C1_unten_als_Fuss_gedreht", C1_COL)
print(f" ✓ Connector 1: 2 Hülsen à {EINSTECK_TIEFE:.0f}mm, DA={HUELSE_DA:.0f}mm")
# ============================================================
# CONNECTOR 2 — 5 Hülsen
# Q-links, Q-rechts, D (von oben), B (nach unten)
# + optionale 5. Verstärkungs-Hülse (hier weggelassen, Schweißnaht reicht)
# ============================================================
print("Erzeuge Connector 2...")
OFFSET_C2 = (0, EINSTECK_TIEFE * 2 + 60, 0)
# Hülse Q links (Z)
h_Ql = make_huelse((0, 0, -1), OFFSET_C2)
# Hülse Q rechts (+Z)
h_Qr = make_huelse((0, 0, +1), OFFSET_C2)
# Hülse D (von Q-Ende Richtung A, also umgekehrter D-Vektor = aufwärts)
h_Dr = make_huelse(vDr, OFFSET_C2)
# Hülse B (nach unten-außen-längs)
h_Bv = make_huelse(vB, OFFSET_C2)
# Alle 4 Hülsen zusammenführen
c2 = h_Ql.fuse(h_Qr).fuse(h_Dr).fuse(h_Bv)
# Spannstift-Bohrungen
for direction, origin in [
((0,0,-1), OFFSET_C2),
((0,0,+1), OFFSET_C2),
(vDr, OFFSET_C2),
(vB, OFFSET_C2),
]:
b = make_spannstift_bohrung(direction, origin)
if b:
c2 = c2.cut(b)
try:
c2 = c2.makeFillet(1.0, c2.Edges)
except Exception:
pass
add_part(doc, c2, "C2_Q_plus_D_plus_B", C2_COL)
print(f" ✓ Connector 2: 4 Hülsen à {EINSTECK_TIEFE:.0f}mm, DA={HUELSE_DA:.0f}mm")
# ============================================================
# EINZELTEILE CONNECTOR 2 (zum Anschauen der Sägeschnitte)
# Jede Hülse separat, leicht versetzt
# ============================================================
print("Erzeuge Connector 2 Einzelteile...")
einzel_offset = 0
einzelteile = [
((0, 0, -1), "C2_Teil_Q_links"),
((0, 0, +1), "C2_Teil_Q_rechts"),
(vDr, "C2_Teil_D"),
(vB, "C2_Teil_B"),
]
for i, (direction, name) in enumerate(einzelteile):
ox = OFFSET_C2[0] + (i - 1.5) * (HUELSE_DA + 15)
oy = OFFSET_C2[1] - EINSTECK_TIEFE * 2 - 40
oz = OFFSET_C2[2]
h = make_huelse(direction, (ox, oy, oz))
b = make_spannstift_bohrung(direction, (ox, oy, oz))
if b:
h = h.cut(b)
col = (0.7, 0.7, 0.2) if "Q" in name else (
(0.2, 0.7, 0.4) if "D" in name else (0.6, 0.3, 0.7))
add_part(doc, h, name, col)
# ============================================================
# ABSCHLUSS
# ============================================================
doc.recompute()
try:
Gui.activeDocument().activeView().fitAll()
except Exception:
pass
print()
print(SEP)
print("STÜCKLISTE CONNECTORS pro Bett:")
print(SEP)
print(f" Connector 1 ×{MODULE*4*2} (A+D oben und B+Fuß unten, identisch gedreht)")
print(f" Connector 2 ×{MODULE*2} (Q+D+B Kreuzknoten)")
print()
print("SÄGEWINKEL ZUSAMMENFASSUNG:")
print(f" C1: A-Hülse sägen auf {90 - saege_winkel(vA, vD):.1f}° zur Rohrachse")
print(f" D-Hülse sägen auf {90 - saege_winkel(vA, vD):.1f}° zur Rohrachse (gespiegelt)")
print(f" C2: Q-Hülsen sägen auf {90 - saege_winkel(vQ, vDr):.1f}° (zu D/B hin)")
print(f" D-Hülse sägen auf {90 - saege_winkel(vDr, vB):.1f}° (zwischen D und B)")
print(f" B-Hülse sägen auf {90 - saege_winkel(vDr, vB):.1f}° (gespiegelt zu D)")
print()
print("FEM-TIPP:")
print(" Für Belastungssimulation in FreeCAD FEM:")
print(f" → Material: Stahl, E=210000 N/mm², σ_y={STRECKGRENZE:.0f} N/mm²")
print(f" → Last: {LAST_KG*SICHERHEIT*g:.0f} N verteilt auf Liegefläche")
print(f" → Einspannung: Fußpunkte fest (alle 6 DOF gesperrt)")
print(f" → Mesh: Tetraeder, max. Elementgröße = {ROHR_DA/2:.0f}mm")

View File

@@ -0,0 +1,314 @@
"""
Feldbett Connector FEM Simulation - FreeCAD Python Script
==========================================================
Simuliert die Belastung von Connector 2 (Q+D+B) unter Last.
VORAUSSETZUNGEN:
1. FreeCAD 0.20 oder neuer
2. CalculiX installiert:
Windows : meist mit FreeCAD mitgeliefert (prüfe Edit→Preferences→FEM→CalculiX)
Ubuntu : sudo apt install calculix-ccx
Mac : brew install calculix
3. Das Connector-Script (feldbett_connectors_v2.py) muss VORHER
ausgeführt worden sein — dieses Script baut auf dem Dokument
"Feldbett_Connectors_v2" auf.
ODER: Beide Scripts nacheinander in einer FreeCAD-Session ausführen.
ANWENDUNG:
Makro → Makro ausführen → diese Datei wählen
→ FreeCAD öffnet automatisch die FEM-Ansicht
→ Solver starten: Doppelklick auf "SolverCcxTools" im Modellbaum
"Write .inp file""Run CalculiX"
→ Ergebnisse: Doppelklick auf "CCX_Results" → Pipeline aktivieren
ERGEBNISSE INTERPRETIEREN:
Von-Mises-Spannung [N/mm²]:
< Streckgrenze/Sicherheit → grün, sicher
> Streckgrenze → rot, versagt
Verformung [mm]:
Zeigt wo der Connector nachgibt
"""
import FreeCAD as App
import FreeCADGui as Gui
import Part
import math
import os
# ============================================================
# PARAMETER — müssen identisch zu feldbett_connectors_v2.py sein!
# ============================================================
L = 35.0
BREITE_AA = 70.0
MODULE = 3
ROHR_DA = 25.0
ROHR_WAND = 2.0
HUELSE_DA = 32.0
HUELSE_WAND= 3.0
EINSTECK_TIEFE = 45.0
SPIEL = 0.4
LAST_KG = 200.0
SICHERHEIT = 2.0
STRECKGRENZE = 235.0 # N/mm² (S235 Stahl)
E_MODUL = 210000.0 # N/mm²
POISSON = 0.3
# ============================================================
# GEOMETRIE (identisch zu Connector-Script)
# ============================================================
hwA = BREITE_AA / 2.0
hwQ = L / 2.0
dZ = hwA - hwQ
legH = math.sqrt(0.75 * L**2 - dZ**2)
def norm(v):
l = math.sqrt(sum(x**2 for x in v))
return tuple(x/l for x in v)
vQ = (0.0, 0.0, 1.0)
vD = norm(( L/2, -legH, -dZ))
vDr = norm((-L/2, legH, dZ))
vB = norm((-L/2, -legH, dZ))
ROHR_DI = ROHR_DA - 2.0 * ROHR_WAND
HUELSE_DI = ROHR_DA + 2.0 * SPIEL
g = 9.81
F_gesamt = LAST_KG * SICHERHEIT * g # N, mit Sicherheit
F_pro_connector = F_gesamt / (MODULE * 4) # N pro Connector 2
# An Connector 2 greifen D und B an — je 2 Stutzen
F_pro_stutzen = F_pro_connector / 2.0 # N pro Stutzen
print("=" * 55)
print("FEM SIMULATION — CONNECTOR 2")
print("=" * 55)
print(f" Auslegungslast = {LAST_KG*SICHERHEIT:.0f} kg ({SICHERHEIT:.0f}× Sicherheit)")
print(f" Kraft pro Stutzen = {F_pro_stutzen:.1f} N")
print(f" Streckgrenze = {STRECKGRENZE:.0f} N/mm²")
print(f" Zul. Spannung = {STRECKGRENZE/SICHERHEIT:.0f} N/mm²")
print()
# ============================================================
# HILFSFUNKTIONEN
# ============================================================
def fv(t):
return App.Vector(t[0], t[1], t[2])
def rotation_to_dir(direction):
z = App.Vector(0, 0, 1)
d = App.Vector(*direction).normalize()
cross = z.cross(d)
dot = z.dot(d)
if cross.Length > 1e-6:
angle = math.degrees(math.acos(max(-1.0, min(1.0, dot))))
return App.Rotation(cross, angle)
elif dot < 0:
return App.Rotation(App.Vector(1, 0, 0), 180)
else:
return App.Rotation()
def make_huelse_solid(direction, origin=(0,0,0)):
"""Solide Hülse (für FEM — keine Bohrung, vereinfacht für Vernetzung)."""
outer = Part.makeCylinder(HUELSE_DA / 2.0, EINSTECK_TIEFE)
inner = Part.makeCylinder(HUELSE_DI / 2.0, EINSTECK_TIEFE + 1.0)
tube = outer.cut(inner)
rot = rotation_to_dir(direction)
tube.Placement = App.Placement(App.Vector(*origin), rot)
return tube
# ============================================================
# GEOMETRIE FÜR FEM AUFBAUEN
# Vereinfachter Connector 2: 4 Hülsen zusammengeschweißt
# (FEM arbeitet besser mit einem zusammenhängenden Solid)
# ============================================================
print("Erzeuge Connector-Geometrie für FEM...")
O = (0, 0, 0)
h_Ql = make_huelse_solid((0, 0, -1), O)
h_Qr = make_huelse_solid((0, 0, +1), O)
h_Dr = make_huelse_solid(vDr, O)
h_Bv = make_huelse_solid(vB, O)
# Alle zu einem Solid verschmelzen (wichtig für FEM — muss ein Body sein)
print(" Verschmelze Hülsen (kann einen Moment dauern)...")
try:
connector_solid = h_Ql.fuse(h_Qr).fuse(h_Dr).fuse(h_Bv)
# FEM braucht ein sauberes Solid ohne lose Faces
connector_solid = connector_solid.removeSplitter()
print(" ✓ Solid erstellt")
except Exception as e:
print(f" ⚠ Fuse-Fehler: {e}")
print(" Verwende h_Ql als Fallback für Test")
connector_solid = h_Ql
# ============================================================
# FEM DOKUMENT AUFSETZEN
# ============================================================
doc_name = "Feldbett_FEM"
if doc_name in App.listDocuments():
App.closeDocument(doc_name)
doc = App.newDocument(doc_name)
print("Erzeuge FEM-Struktur...")
# Geometrie-Objekt
geo_obj = doc.addObject("Part::Feature", "Connector2_Solid")
geo_obj.Shape = connector_solid
doc.recompute()
# ============================================================
# FEM ANALYSIS
# ============================================================
try:
import ObjectsFem
import FemGui
except ImportError as e:
print(f"FEHLER: FEM-Modul nicht verfügbar: {e}")
print("Stelle sicher dass FreeCAD mit FEM-Workbench installiert ist.")
raise
# Analysis-Container
analysis = ObjectsFem.makeAnalysis(doc, "Analysis")
# --- Material ---
mat_obj = ObjectsFem.makeMaterialSolid(doc, "Material_Stahl")
mat = mat_obj.Material
mat["Name"] = "Stahl_S235"
mat["YoungsModulus"] = f"{E_MODUL:.0f} MPa"
mat["PoissonRatio"] = f"{POISSON}"
mat["Density"] = "7900 kg/m^3"
mat["UltimateTensileStrength"]= f"{STRECKGRENZE:.0f} MPa"
mat["YieldStrength"] = f"{STRECKGRENZE:.0f} MPa"
mat_obj.Material = mat
mat_obj.References = [(geo_obj, "Solid1")]
analysis.addObject(mat_obj)
# --- Mesh (Vernetzung) ---
# Netfgen oder GMSH — FreeCAD nutzt intern Netgen
mesh_obj = doc.addObject("Fem::FemMeshShapeNetgenObject", "FEMMesh")
mesh_obj.Shape = geo_obj
mesh_obj.MaxSize = ROHR_DA / 2.0 # Elementgröße = halber Rohrdurchmesser
mesh_obj.MinSize = ROHR_WAND # Mindestgröße = Wandstärke
mesh_obj.Fineness = 3 # 1=sehr grob, 5=sehr fein (3=mittel, gut für Start)
mesh_obj.Optimize = True
mesh_obj.SecondOrder = True # Quadratische Elemente = genauer
analysis.addObject(mesh_obj)
print(f" Mesh: MaxSize={ROHR_DA/2:.1f}mm, Fineness=3 (mittel)")
# --- Einspannung (Fixed Constraint) ---
# Q-Stange an einem Ende festhalten (simuliert Befestigung am Bett-Rahmen)
# Face-Auswahl: Stirnfläche der Q-links-Hülse
fix = ObjectsFem.makeConstraintFixed(doc, "Einspannung_Q_Ende")
# Face muss manuell ausgewählt werden — wir setzen eine plausible Referenz
# (In der Praxis: in FreeCAD GUI die richtige Face anklicken)
fix.References = [(geo_obj, "Face1")] # ← ggf. in GUI anpassen
analysis.addObject(fix)
# --- Kraft an D-Stutzen ---
force_D = ObjectsFem.makeConstraintForce(doc, "Last_D_Stutzen")
force_D.Force = F_pro_stutzen # N
force_D.Direction = (geo_obj, ["Edge1"]) # Richtung entlang D-Achse
force_D.Reversed = False
# Kraftvektor in D-Richtung
force_D.DirectionVector = App.Vector(*vD)
force_D.References = [(geo_obj, "Face2")] # ← Stirnfläche D-Stutzen, in GUI anpassen
analysis.addObject(force_D)
# --- Kraft an B-Stutzen ---
force_B = ObjectsFem.makeConstraintForce(doc, "Last_B_Stutzen")
force_B.Force = F_pro_stutzen
force_B.DirectionVector = App.Vector(*vB)
force_B.References = [(geo_obj, "Face3")] # ← Stirnfläche B-Stutzen, in GUI anpassen
analysis.addObject(force_B)
# --- Solver (CalculiX) ---
solver = ObjectsFem.makeSolverCalculixCcxTools(doc, "SolverCcxTools")
solver.AnalysisType = "static"
solver.GeometricalNonlinearity = "linear"
solver.ThermoMechSteadyState = False
solver.MatrixSolverType = "default"
solver.IterationsControlParameterTimeUse = False
solver.SplitInputWriter = False
# CalculiX-Pfad automatisch suchen
ccx_paths = [
"/usr/bin/ccx", # Linux apt
"/usr/local/bin/ccx", # Linux/Mac brew
"C:/Program Files/FreeCAD/bin/ccx.exe", # Windows
"C:/Program Files (x86)/FreeCAD/bin/ccx.exe",
]
for path in ccx_paths:
if os.path.exists(path):
solver.ccxBinaryPath = path
print(f" ✓ CalculiX gefunden: {path}")
break
else:
print(" ⚠ CalculiX nicht automatisch gefunden.")
print(" → Manuell setzen: Edit → Preferences → FEM → CalculiX")
analysis.addObject(solver)
# ============================================================
# ABSCHLUSS & ANLEITUNG
# ============================================================
doc.recompute()
# FEM-Workbench aktivieren
try:
Gui.activateWorkbench("FemWorkbench")
FemGui.setActiveAnalysis(analysis)
except Exception:
pass
try:
Gui.activeDocument().activeView().fitAll()
except Exception:
pass
print()
print("=" * 55)
print("FEM-SETUP ABGESCHLOSSEN")
print("=" * 55)
print("""
NÄCHSTE SCHRITTE IN FREECAD:
1. FACES ANPASSEN (wichtig!):
Die Einspannungen und Lasten sind auf "Face1/2/3"
gesetzt — das sind Platzhalter.
→ Im Modellbaum: Doppelklick auf "Einspannung_Q_Ende"
→ Stirnfläche der Q-Hülse anklicken → OK
→ Gleiches für "Last_D_Stutzen" und "Last_B_Stutzen"
2. MESH ERZEUGEN:
→ Doppelklick auf "FEMMesh" im Modellbaum
"Mesh parameters" prüfen → OK
→ Vernetzung startet automatisch (dauert 10-30 Sek.)
3. SIMULATION STARTEN:
→ Doppelklick auf "SolverCcxTools"
→ Button "Write .inp file"
→ Button "Run CalculiX"
→ Warten (je nach Mesh 30 Sek. bis 5 Min.)
4. ERGEBNISSE ANZEIGEN:
→ Im Modellbaum erscheint "CCX_Results"
→ Menü: FEM → Postprocessing → Apply pipeline to result
→ Wähle "Von Mises Stress" oder "Displacement"
→ Farbskala zeigt Spannungsverteilung
""")
print(f"GRENZWERTE:")
print(f" Zulässige Spannung : {STRECKGRENZE/SICHERHEIT:.0f} N/mm² (grün im Plot)")
print(f" Streckgrenze : {STRECKGRENZE:.0f} N/mm² (orange = kritisch)")
print(f" Simulierte Last : {LAST_KG*SICHERHEIT:.0f} kg ({SICHERHEIT:.0f}× Sicherheit)")