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:
260
scripts/original/feldbett.py
Normal file
260
scripts/original/feldbett.py
Normal 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 (A–A) [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.0–1.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 A–A
|
||||
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 A–A = {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°")
|
||||
316
scripts/original/feldbett_connectors.py
Normal file
316
scripts/original/feldbett_connectors.py
Normal 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 A–A [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")
|
||||
436
scripts/original/feldbett_connectors_v2.py
Normal file
436
scripts/original/feldbett_connectors_v2.py
Normal 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 A–A = {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: A–D = {saege_winkel(vA, vD):.2f}° (jedes Teil um diesen Winkel sägen)")
|
||||
print(f" Connector 2: Q–D = {saege_winkel(vQ, vDr):.2f}°")
|
||||
print(f" Connector 2: Q–B = {saege_winkel(vQ, vB):.2f}°")
|
||||
print(f" Connector 2: D–B = {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")
|
||||
314
scripts/original/feldbett_fem.py
Normal file
314
scripts/original/feldbett_fem.py
Normal 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)")
|
||||
Reference in New Issue
Block a user