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