diff --git a/rhino/elemente.py b/rhino/elemente.py index a4f89f5..456e5ab 100644 --- a/rhino/elemente.py +++ b/rhino/elemente.py @@ -67,6 +67,13 @@ _KEY_AKTIVE_DARSTELLUNG = "dossier_aktive_darstellung" # doc-level global overr _KEY_OEFF_STYLE_ID = "dossier_oeff_style_id" # per-Object: referenziert einen Style aus dossier_oeff_styles _KEY_OEFF_STYLES = "dossier_oeff_styles" # JSON-Liste aller Fenster/Tueren-Styles _KEY_OEFF_STYLE_ACTIVE = "dossier_oeff_style_active" # zuletzt benutzte Style-ID (pro typ) +_KEY_OEFF_TUER_TYP = "dossier_oeff_tuer_typ" # "normal" | "wandoeffnung" +_KEY_OEFF_HINGE_SIDE = "dossier_oeff_hinge_side" # "links" | "rechts" — Bandseite (welche Tuerflueg-Seite) +_KEY_OEFF_OPEN_ANGLE = "dossier_oeff_open_angle" # float Grad 0–180 (Plan-Oeffnungswinkel) +_KEY_OEFF_SWING_INVERT = "dossier_oeff_swing_invert" # "1"/"0" — flippt die Schwung-Richtung + +_OEFF_TUER_TYPEN = ("normal", "wandoeffnung") +_OEFF_HINGE_SIDES = ("links", "rechts") _OEFF_DARSTELLUNGEN = ("einfach", "standard", "detail") @@ -101,6 +108,7 @@ _OEFF_STYLE_FIELDS = ( "rahmenB", "rahmenTiefe", "rahmenOffset", "fluegel", "simsAus", "glas", "darstellung", "tuerRahmen", + "tuerTyp", "hingeSide", "openAngle", ) _OEFF_DEFAULT_STYLES = [ @@ -133,7 +141,14 @@ _OEFF_DEFAULT_STYLES = [ "breite": 0.90, "hoehe": 2.10, "brueest": 0.00, "rahmenB": 0.06, "rahmenTiefe": 0.08, "rahmenOffset": 0.05, "fluegel": 1, "simsAus": "ohne", "glas": True, - "darstellung": "standard", "tuerRahmen": "zarge"}, + "darstellung": "standard", "tuerRahmen": "zarge", + "tuerTyp": "normal", "hingeSide": "links", "openAngle": 90.0}, + {"name": "Wandöffnung", "typ": "tuer", + "breite": 1.20, "hoehe": 2.10, "brueest": 0.00, + "rahmenB": 0.04, "rahmenTiefe": 0.04, "rahmenOffset": 0.05, + "fluegel": 1, "simsAus": "ohne", "glas": False, + "darstellung": "standard", "tuerRahmen": "zarge", + "tuerTyp": "wandoeffnung", "hingeSide": "links", "openAngle": 0.0}, ] @@ -599,6 +614,15 @@ def _layer_path_volume(doc, geschoss_name): return _layer_path_axis(doc, geschoss_name) +def _layer_path_oeff_swing(doc, geschoss_name): + """Türschwung-Linien — eigener Sublayer (Code 23) damit User die + Schwung-Bögen unabhängig von den Türen-Volumes ausblenden kann.""" + sub = _find_ebene_sublayer_name(doc, ["schwung", "tuerschwung"], + "23", "Türschwung", + default_color="#888888", default_lw=0.13) + return "{}::{}".format(geschoss_name, sub) + + def _layer_path_decke(doc, geschoss_name): """Decken-Outline + Volumen — Sublayer 'DECKEN' (Code 30).""" sub = _find_ebene_sublayer_name(doc, ["decke"], "30", "DECKEN", @@ -1884,6 +1908,8 @@ def _attach_meta(obj_attrs, wall_id, type_, geschoss, dicke, uk_over, ok_over, oeff_referenz=None, oeff_darstellung=None, oeff_aussenseite=None, oeff_tuer_rahmen=None, oeff_rahmen_offset=None, oeff_style_id=None, + oeff_tuer_typ=None, oeff_hinge_side=None, oeff_open_angle=None, + oeff_swing_invert=None, geschoss_end=None, treppe_breite=None, treppe_n=None, treppe_referenz=None, treppe_modus=None, treppe_lauf_d=None, treppe_art=None, @@ -1967,6 +1993,19 @@ def _attach_meta(obj_attrs, wall_id, type_, geschoss, dicke, uk_over, ok_over, if oeff_style_id is not None: try: obj_attrs.SetUserString(_KEY_OEFF_STYLE_ID, str(oeff_style_id) or "") except Exception: pass + if oeff_tuer_typ is not None and oeff_tuer_typ in _OEFF_TUER_TYPEN: + obj_attrs.SetUserString(_KEY_OEFF_TUER_TYP, oeff_tuer_typ) + if oeff_hinge_side is not None and oeff_hinge_side in _OEFF_HINGE_SIDES: + obj_attrs.SetUserString(_KEY_OEFF_HINGE_SIDE, oeff_hinge_side) + if oeff_open_angle is not None: + try: + v = max(0.0, min(180.0, float(oeff_open_angle))) + obj_attrs.SetUserString(_KEY_OEFF_OPEN_ANGLE, "{:.2f}".format(v)) + except Exception: pass + if oeff_swing_invert is not None: + try: obj_attrs.SetUserString(_KEY_OEFF_SWING_INVERT, + "1" if bool(oeff_swing_invert) else "0") + except Exception: pass # --- Treppen-Felder --- if geschoss_end is not None: obj_attrs.SetUserString(_KEY_GESCHOSS_END, geschoss_end or "") @@ -2130,6 +2169,15 @@ def _read_meta(obj): except Exception: oro = 0.05 if oro < 0: oro = 0.0 ostyle = a.GetUserString(_KEY_OEFF_STYLE_ID) or "" + ottyp = a.GetUserString(_KEY_OEFF_TUER_TYP) or "normal" + if ottyp not in _OEFF_TUER_TYPEN: ottyp = "normal" + ohinge = a.GetUserString(_KEY_OEFF_HINGE_SIDE) or "links" + if ohinge not in _OEFF_HINGE_SIDES: ohinge = "links" + try: oangle = float(a.GetUserString(_KEY_OEFF_OPEN_ANGLE) or "90") + except Exception: oangle = 90.0 + if oangle < 0: oangle = 0.0 + elif oangle > 180: oangle = 180.0 + oswinv = (a.GetUserString(_KEY_OEFF_SWING_INVERT) == "1") # Treppen-Felder gend = a.GetUserString(_KEY_GESCHOSS_END) or "" try: tb = float(a.GetUserString(_KEY_TREPPE_BREITE) or "1.0") @@ -2251,6 +2299,10 @@ def _read_meta(obj): "oeff_tuer_rahmen": otrah, "oeff_rahmen_offset": oro, "oeff_style_id": ostyle, + "oeff_tuer_typ": ottyp, + "oeff_hinge_side": ohinge, + "oeff_open_angle": oangle, + "oeff_swing_invert": oswinv, "geschoss_end": gend, "treppe_breite": tb, "treppe_n": tn, @@ -2471,6 +2523,109 @@ def _resolve_rahmen_perp_range(half_d, rahmen_tiefe, rahmen_pos): return lo, hi +def _make_tuer_swing_curves(axis_curve, point_on_axis, wall_dicke, + oeff_meta, base_z): + """Generiert die 2D-Plan-Schwung-Linien einer Tuer: + - 1 Linie: Tuerblatt im geoeffneten Zustand (vom Scharnier um + open_angle gedreht) + - 1 Arc: Schwung-Bogen vom geschlossenen Endpunkt zum offenen + Endpunkt + Alle Kurven liegen auf Floor-Level (z = base_z). + + Returns [Curve] oder leere Liste wenn Tuer-Typ='wandoeffnung' + oder Oeffnung keine Tuer ist.""" + import math + if oeff_meta.get("oeff_typ") != "tuer": return [] + if oeff_meta.get("oeff_tuer_typ", "normal") != "normal": return [] + frame = _oeff_axis_frame(axis_curve, point_on_axis) + if frame is None: return [] + pt, tan, perp = frame + breite = float(oeff_meta.get("oeff_breite", 0.9)) + rahmen_b = float(oeff_meta.get("oeff_rahmen_b", 0.06)) + darstellung = oeff_meta.get("oeff_darstellung", "standard") + aussenseite = oeff_meta.get("oeff_aussenseite", "rechts") + aus_sign = +1 if aussenseite == "rechts" else -1 + hinge_side = oeff_meta.get("oeff_hinge_side", "links") + try: open_angle = float(oeff_meta.get("oeff_open_angle", 90)) + except Exception: open_angle = 90.0 + if open_angle <= 0: return [] # tuer geschlossen — keine Schwung-Curves + swing_invert = bool(oeff_meta.get("oeff_swing_invert", False)) + + half_b = breite * 0.5 + # Lichte-Breite je nach Darstellung: einfach = volle Breite, + # standard/detail = Innenmass abzueglich Rahmen + if darstellung == "einfach": + leaf_t_lo, leaf_t_hi = -half_b, +half_b + else: + leaf_t_lo, leaf_t_hi = -half_b + rahmen_b, +half_b - rahmen_b + leaf_len = leaf_t_hi - leaf_t_lo + if leaf_len <= 1e-6: return [] + + # Scharnier-Tangentenkoordinate (along axis) + if hinge_side == "links": + hinge_t = leaf_t_lo + # Schwung-Richtung: vom Scharnier nach aussen → Aussenseite-Vorzeichen + # Linke Bandseite: Tuer schwingt entlang +tan-Richtung im geschlossenen + # Zustand, perpendikulaer ins Innere (= -aus_sign perp) im offenen. + sweep_sign = -1 # gegen Uhrzeigersinn um Scharnier wenn aus +Z gesehen + else: + hinge_t = leaf_t_hi + sweep_sign = +1 + + # Hinge-Punkt: sitzt auf der WAND-INNENKANTE (nicht auf der Achse). + # Innen-Richtung: aussenseite='rechts' (aus_sign=+1) bedeutet aussen + # liegt in box-perp-Richtung (tan.Y, -tan.X). Innen ist dann das + # Gegenstueck (-tan.Y, +tan.X). aus_sign=-1 flippt das. + half_d = float(wall_dicke) * 0.5 + inside_x = aus_sign * (-tan.Y) + inside_y = aus_sign * tan.X + z0 = float(base_z) + hinge_pt = rg.Point3d( + pt.X + hinge_t * tan.X + half_d * inside_x, + pt.Y + hinge_t * tan.Y + half_d * inside_y, + z0) + # Geschlossener Endpunkt (Tuer-Blatt in Wand-Flucht, parallel zur Achse): + closed_pt = rg.Point3d( + pt.X + (hinge_t + sweep_sign * leaf_len) * tan.X + half_d * inside_x, + pt.Y + (hinge_t + sweep_sign * leaf_len) * tan.Y + half_d * inside_y, + z0) + # Offener Endpunkt: hinge + leaf_len * direction(sweep_angle) + # Direction startet bei +sweep_sign * tan (geschlossen) und rotiert + # um Z-Achse um sweep_sign * (-aus_sign) * open_angle Grad — Tuer + # schwingt in den Innenraum (-aus_sign * perp). + rad = math.radians(open_angle) * sweep_sign * aus_sign + if swing_invert: rad = -rad + cos_r, sin_r = math.cos(rad), math.sin(rad) + base_dx = sweep_sign * tan.X + base_dy = sweep_sign * tan.Y + open_dx = base_dx * cos_r - base_dy * sin_r + open_dy = base_dx * sin_r + base_dy * cos_r + open_pt = rg.Point3d(hinge_pt.X + open_dx * leaf_len, + hinge_pt.Y + open_dy * leaf_len, z0) + + curves = [] + # 1) Tuerblatt-Linie (Hinge → Open) + try: + leaf_line = rg.LineCurve(hinge_pt, open_pt) + curves.append(leaf_line) + except Exception as ex: + print("[ELEMENTE] swing leaf:", ex) + # 2) Schwung-Arc — Mittelpunkt = Scharnier, Radius = Tuerblatt-Laenge. + # Plane mit X→closed_pt + Y→open_pt; Arc(plane, radius, angle) startet + # auf der X-Achse und sweept CCW um angle Radian. Damit liegt der + # Bogen genau auf einem Kreis um das Scharnier. + try: + dir_closed = closed_pt - hinge_pt + dir_open = open_pt - hinge_pt + arc_plane = rg.Plane(hinge_pt, dir_closed, dir_open) + arc = rg.Arc(arc_plane, leaf_len, math.radians(open_angle)) + if arc.IsValid: + curves.append(rg.ArcCurve(arc)) + except Exception as ex: + print("[ELEMENTE] swing arc:", ex) + return curves + + def _make_oeffnung_pieces(axis_curve, point_on_axis, wall_dicke, oeff_meta, base_z): """Baut die einzelnen Brep-Pieces der Oeffnung — Rahmen (single Brep via Boolean-Differenz), Mittelpfosten (pro Fluegel), Glas, Sims aussen, @@ -2680,7 +2835,7 @@ SOURCE_TYPES = ("wand_axis", "decke_outline", "dach_outline", "stuetze_point", "traeger_axis", "raum_outline", "decke_aussparung_outline") VOLUME_TYPES = ("wand_volume", "decke_volume", "dach_volume", - "oeffnung_volume", "treppe_volume", + "oeffnung_volume", "oeffnung_swing", "treppe_volume", "stuetze_volume", "traeger_volume", "raum_stamp", "raum_fill") # Oeffnungs-Cutout: Boolean-Difference aus Wand. Zusaetzlich kriegt die @@ -4458,6 +4613,30 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name # Aenderung (z.B. Fluegel-Wechsel) Fallback auf Delete+Add. op_layer = _ensure_layer(doc, _layer_path_volume(doc, geschoss_name)) for op_meta, pt_loc, op_uk in opening_jobs: + # Schwung-Curves IMMER zuerst regenerieren — unabhaengig davon + # ob die Volume-Pieces sich geaendert haben. Andernfalls greifen + # Aenderungen an openAngle/hingeSide/swingInvert nicht weil der + # Replace-Pfad weiter unten via `continue` rausspringt. + if op_meta.get("oeff_typ") == "tuer": + old_sw = list(_find_objects_by_wall_id(doc, op_meta["id"], + "oeffnung_swing")) + for o, _m in old_sw: + try: doc.Objects.Delete(o.Id, True) + except Exception: pass + swings = _make_tuer_swing_curves(geom, pt_loc, meta["dicke"], + op_meta, op_uk) + if swings: + sw_layer_idx = _ensure_layer(doc, + _layer_path_oeff_swing(doc, geschoss_name)) + for crv in swings: + sw_attrs = Rhino.DocObjects.ObjectAttributes() + sw_attrs.LayerIndex = sw_layer_idx + _attach_meta(sw_attrs, op_meta["id"], "oeffnung_swing", + op_meta["geschoss"], meta["dicke"], "", "", + oeff_typ="tuer", + oeff_parent=op_meta.get("oeff_parent")) + doc.Objects.AddCurve(crv, sw_attrs) + old_objs = list(_find_objects_by_wall_id(doc, op_meta["id"], "oeffnung_volume")) pieces = _make_oeffnung_pieces(geom, pt_loc, meta["dicke"], @@ -4494,7 +4673,11 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name oeff_aussenseite=op_meta.get("oeff_aussenseite"), oeff_tuer_rahmen=op_meta.get("oeff_tuer_rahmen"), oeff_rahmen_offset=op_meta.get("oeff_rahmen_offset"), - oeff_style_id=op_meta.get("oeff_style_id")) + oeff_style_id=op_meta.get("oeff_style_id"), + oeff_tuer_typ=op_meta.get("oeff_tuer_typ"), + oeff_hinge_side=op_meta.get("oeff_hinge_side"), + oeff_open_angle=op_meta.get("oeff_open_angle"), + oeff_swing_invert=op_meta.get("oeff_swing_invert")) doc.Objects.AddBrep(pbrep, op_attrs) # Source-Layer migrieren + Volumen-Layer-Index ermitteln @@ -5107,6 +5290,10 @@ class ElementeBridge(panel_base.BaseBridge): "tuerRahmen": meta.get("oeff_tuer_rahmen", "zarge"), "rahmenOffset": meta.get("oeff_rahmen_offset", 0.05), "styleId": meta.get("oeff_style_id", ""), + "tuerTyp": meta.get("oeff_tuer_typ", "normal"), + "hingeSide": meta.get("oeff_hinge_side", "links"), + "openAngle": meta.get("oeff_open_angle", 90.0), + "swingInvert": bool(meta.get("oeff_swing_invert", False)), }) elif meta["type"] == "treppe_axis": gs = _geschoss_by_id(doc, meta["geschoss"]) @@ -6174,7 +6361,10 @@ class ElementeBridge(panel_base.BaseBridge): oeff_aussenseite=detected_aussen, oeff_darstellung=darst_def, oeff_tuer_rahmen=tuer_rahmen_def, - oeff_style_id=pending_sid) + oeff_style_id=pending_sid, + oeff_tuer_typ="normal", + oeff_hinge_side="links", + oeff_open_angle=90.0) # Oeffnungs-Punkt auf UK+Brueestung-Hoehe platzieren (= visuell auf # Unterkante Oeffnung). Constraint vergleicht spaeter pt.Z mit # UK+brueest — wenn der Punkt am axis.Z=0 saesse, wuerde der erste @@ -8614,6 +8804,17 @@ class ElementeBridge(panel_base.BaseBridge): # Stil das eben gemachte Field-Edit wegmaecken). o_style_id = p.get("styleId", old_meta.get("oeff_style_id", "")) or "" + # Tueren-Schwung-Felder + o_tuer_typ = p.get("tuerTyp", old_meta.get("oeff_tuer_typ", "normal")) + if o_tuer_typ not in _OEFF_TUER_TYPEN: o_tuer_typ = "normal" + o_hinge = p.get("hingeSide", old_meta.get("oeff_hinge_side", "links")) + if o_hinge not in _OEFF_HINGE_SIDES: o_hinge = "links" + try: o_angle = float(p.get("openAngle", + old_meta.get("oeff_open_angle", 90))) + except Exception: o_angle = 90.0 + o_angle = max(0.0, min(180.0, o_angle)) + o_swinv = bool(p.get("swingInvert", + old_meta.get("oeff_swing_invert", False))) if p.get("styleId") and p.get("styleId") != old_meta.get("oeff_style_id"): # Style wurde NEU gewaehlt → seine Werte applizieren styles = list_oeff_styles(doc) @@ -8632,6 +8833,12 @@ class ElementeBridge(panel_base.BaseBridge): if "darstellung" in stl: odarst = stl["darstellung"] if otyp == "tuer" and "tuerRahmen" in stl: otrah = stl["tuerRahmen"] + if otyp == "tuer" and "tuerTyp" in stl: + o_tuer_typ = stl["tuerTyp"] + if otyp == "tuer" and "hingeSide" in stl: + o_hinge = stl["hingeSide"] + if otyp == "tuer" and "openAngle" in stl: + o_angle = float(stl["openAngle"]) set_active_oeff_style_id(doc, otyp, p["styleId"]) attrs = axis_obj.Attributes _attach_meta(attrs, wall_id, "oeffnung_point", @@ -8652,7 +8859,11 @@ class ElementeBridge(panel_base.BaseBridge): oeff_aussenseite=oauss, oeff_tuer_rahmen=otrah, oeff_rahmen_offset=oro, - oeff_style_id=o_style_id) + oeff_style_id=o_style_id, + oeff_tuer_typ=o_tuer_typ, + oeff_hinge_side=o_hinge, + oeff_open_angle=o_angle, + oeff_swing_invert=o_swinv) axis_obj.Attributes = attrs axis_obj.CommitChanges() parent_id = old_meta.get("oeff_parent", "") diff --git a/src/ElementeApp.jsx b/src/ElementeApp.jsx index 9b636ff..3f06510 100644 --- a/src/ElementeApp.jsx +++ b/src/ElementeApp.jsx @@ -1746,6 +1746,8 @@ function OeffnungProperties({ oeff, onUpdate, onDelete, oeffStyles = [] }) { fluegel: oeff.fluegel, simsAus: oeff.simsAus, glas: oeff.glas, darstellung: oeff.darstellung, tuerRahmen: oeff.tuerRahmen, + tuerTyp: oeff.tuerTyp, hingeSide: oeff.hingeSide, + openAngle: oeff.openAngle, }) return } @@ -1799,6 +1801,63 @@ function OeffnungProperties({ oeff, onUpdate, onDelete, oeffStyles = [] }) { {!isFenster && ( +
+ + Typ + +
+ onUpdate({ tuerTyp: 'normal' })} + title="Tuere mit Tuerblatt + Schwung-Bogen" /> + onUpdate({ tuerTyp: 'wandoeffnung' })} + title="Nur Wand-Cutout ohne Schwung" /> +
+
+ )} + + {!isFenster && (oeff.tuerTyp || 'normal') === 'normal' && ( + <> +
+ + Band + +
+ onUpdate({ hingeSide: 'links' })} /> + onUpdate({ hingeSide: 'rechts' })} /> +
+
+ +
+ + Öffn. + + { + const v = parseFloat(e.target.value.replace(',', '.')) + if (!Number.isNaN(v) && v >= 0 && v <= 180) onUpdate({ openAngle: v }) + }} + onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }} + style={{ flex: 1, fontSize: 11, fontFamily: 'DM Mono, monospace' }} /> + ° + onUpdate({ swingInvert: !oeff.swingInvert })} + title="Schwung-Richtung umkehren (nach aussen statt innen)" /> +
+ + )} + + {!isFenster && (oeff.tuerTyp || 'normal') === 'normal' && (