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>
437 lines
15 KiB
Python
437 lines
15 KiB
Python
"""
|
||
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")
|