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,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)")