From 0c5f8055a5b1882ca02b84116b6a482364c0c9d4 Mon Sep 17 00:00:00 2001 From: karim Date: Fri, 22 May 2026 12:34:15 +0200 Subject: [PATCH] Fenster/Tueren LoD + Stile + Phase-3-Ausschnitt-Darstellung + UI-Konsistenz MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fenster/Tueren: - 3-stufige SIA-400-Darstellung pro Element: einfach (1:100, flache Scheibe ohne Tiefe in Wand-Mittelebene), standard (1:50, Rahmen + Glas + Sims), detail (1:20, Doppelverglasung). - Aussenseite-Flag mit Auto-Detection aus der Click-Richtung beim Setzen — Sim sitzt automatisch aussen. Im Panel als Umkehren-Toggle. - Tueren-Rahmen-Typ Zarge|Block — Blockrahmen ragt seitlich raus. - Rahmen-Offset (m von Wand-Innenseite) ersetzt das 3-Preset Lage- Feld. Wirkt auch in der einfachen Darstellung (Pane sitzt auf der Rahmen-Mittelebene, nicht in Wand-Mitte). - Sims nur AUSSEN. Innen entfaellt — der Sim ist gleichzeitig der visuelle Indikator fuer die Aussenseite. - Oeffnungs-Stile: list/save/delete-API mit 6 Default-Presets (Fenster Standard/Gross/Bandlage, Tuer Innen/Eingang/Verglast). Style-ID per UserString am Objekt persistiert. Im Panel BarCombo mit "Aktuelle als Stil speichern…". Beim Rhino-Command "Stil"- Option zum Picken vor dem Klick. Ausschnitt-Darstellung (Phase 3): - Doc-Level Override dossier_aktive_darstellung gewinnt vor per- Object-Setting. Wechsel triggert Regen aller Oeffnungen via neuer regenerate_all_oeffnungen-API. - Ausschnitt-Capture speichert die Darstellung mit, Restore wendet sie an und regeneriert. - Oberleiste-Quick-Switch BarCombo mit 4 Optionen. - AusschnittSettings-Dialog: Darstellungs-Dropdown. Gestaltung (SectionStyle Phase 2): - _set_section_style schreibt per-Object SectionHatchIndex/Scale/ Rotation/Color mit Multi-Fallback (Property-Namen varieren je Rhino-Build). _selection_summary liest die selben zurueck. - HatchEditor als shared Component fuer Fill + Section. - geometryKind ignoriert DOSSIER-Source-Curves damit Wand-Selektion (Axis + Volume) als 3D klassifiziert wird. UI-Konsistenz Panels: - Ebenenkombi zurueck als eigene Section oben im Ebenen-Panel, Modelldarstellung-Dropdown an die freigewordene Position in der Oberleiste (Row 1 Col 2 im 2x2-Preset-Block). - BarCombo erweitert: stretch-Prop (Pill waechst auf Container- Breite), onSecond/secondIcon/secondTitle fuer 2. Trailing-Button, gearIcon-Prop. Plus-Slot immer ganz aussen rechts, Settings-Slot direkt nach dem Caret. - Ebenen + Zeichnungsebenen visuell kohaerent: identisches Padding (1px 12px 1px 0), Chevron/Spacer-Slot 12px, Master-Row mit Eye 16x16 + Lock 14x14, gleiche Border + Borderfarbe. Eye-Icons in beiden Panels untereinander ausgerichtet. - Properties-Container ohne Border (war zuvor accent-gruen, dann border — User wollte gar nichts mehr). - ElementList raus aus dem Elemente-Panel (Uebersicht via Tree- Window erreichbar). NeuesElement bleibt voll sichtbar bei Selektion (kein Collapse), Properties oben. Co-Authored-By: Claude Opus 4.7 --- rhino/ausschnitte.py | 41 ++- rhino/elemente.py | 558 ++++++++++++++++++++++++++--- rhino/oberleiste.py | 18 + rhino/rhinopanel.py | 27 ++ src/App.jsx | 9 +- src/AusschnittSettingsApp.jsx | 15 + src/ElementeApp.jsx | 172 ++++++--- src/ElementePropertiesApp.jsx | 1 + src/OberleisteApp.jsx | 48 +-- src/components/BarControls.jsx | 42 ++- src/components/EbenenManager.jsx | 125 +++++-- src/components/GeschossManager.jsx | 58 +-- src/lib/rhinoBridge.js | 5 + 13 files changed, 899 insertions(+), 220 deletions(-) diff --git a/rhino/ausschnitte.py b/rhino/ausschnitte.py index d622419..94aff06 100644 --- a/rhino/ausschnitte.py +++ b/rhino/ausschnitte.py @@ -327,6 +327,8 @@ class AusschnittBridge(panel_base.BaseBridge): elif t == "DELETE": self._delete(p.get("id")) elif t == "SET_FOLDER": self._set_field(p.get("id"), "folder", p.get("folder") or "") elif t == "SET_SCALE": self._set_field(p.get("id"), "scale", p.get("scale") or "") + elif t == "SET_DARSTELLUNG": self._set_field(p.get("id"), "darstellung", + p.get("darstellung") or "") elif t == "DUPLICATE": self._duplicate(p.get("id")) elif t == "ADD_FOLDER": self._add_folder(p.get("name")) elif t == "REMOVE_FOLDER": self._remove_folder(p.get("name")) @@ -494,13 +496,21 @@ class AusschnittBridge(panel_base.BaseBridge): print("[AUSSCHNITTE] Live-Skala (Fallback):", ex) if not scale_str and prior_scale: scale_str = prior_scale # Perspective -> alten Wert nicht ueberschreiben + # Darstellungs-Override aus dem aktuellen Doc-Setting uebernehmen. + # Leer = "kein Override, per-Object respektieren". + darst = "" + try: + import elemente + darst = elemente.get_aktive_darstellung(doc) or "" + except Exception: pass return { - "id": existing_id or "snap_" + uuid.uuid4().hex[:8], - "name": name, - "scale": scale_str, - "camera": _capture_camera(vp), - "layers": _capture_layers(doc), - "dossier": _capture_dossier_state(doc), + "id": existing_id or "snap_" + uuid.uuid4().hex[:8], + "name": name, + "scale": scale_str, + "camera": _capture_camera(vp), + "layers": _capture_layers(doc), + "dossier": _capture_dossier_state(doc), + "darstellung": darst, } def _save(self, name): @@ -558,6 +568,21 @@ class AusschnittBridge(panel_base.BaseBridge): rhinopanel._notify_oberleiste_combs() except Exception: pass _apply_dossier_state(doc, snap.get("dossier") or snap.get("pause") or {}) + # Darstellung anwenden + Oeffnungen regenerieren + try: + import elemente + new_darst = snap.get("darstellung") or "" + cur_darst = elemente.get_aktive_darstellung(doc) or "" + if new_darst != cur_darst: + elemente.set_aktive_darstellung(doc, new_darst) + elemente.regenerate_all_oeffnungen(doc) + # Oberleiste-Topbar muss neuen Wert spiegeln + try: + b = sc.sticky.get("oberleiste_bridge") + if b is not None: b._send_state(force=True) + except Exception: pass + except Exception as ex: + print("[AUSSCHNITTE] darstellung apply:", ex) # Overrides: nur anwenden wenn das Snap "applyOverrides" gesetzt hat. # Sonst bleibt der aktuelle User-Override-State unangetastet. if snap.get("applyOverrides"): @@ -786,6 +811,7 @@ class AusschnittBridge(panel_base.BaseBridge): "overridesEnabled": bool(sn.get("overridesEnabled", False)), "overridesPreset": sn.get("overridesPreset") or "", "layerCombination": sn.get("layerCombination") or "", + "darstellung": sn.get("darstellung") or "", }, "displayModes": display_modes, "overridesPresets": overrides_presets, @@ -815,6 +841,9 @@ class AusschnittBridge(panel_base.BaseBridge): target["overridesPreset"] = (settings.get("overridesPreset") or "").strip() # Ebenenkombi target["layerCombination"] = (settings.get("layerCombination") or "").strip() + # Darstellung (SIA-400 LoD Override fuer diesen Ausschnitt) + darst = (settings.get("darstellung") or "").strip() + target["darstellung"] = darst if darst in ("einfach", "standard", "detail") else "" outer._store(d, snaps) outer._send_list() print("[AUSSCHNITTE] Settings fuer '{}' aktualisiert".format(target.get("name"))) diff --git a/rhino/elemente.py b/rhino/elemente.py index e4fc5e0..a4f89f5 100644 --- a/rhino/elemente.py +++ b/rhino/elemente.py @@ -59,6 +59,182 @@ _KEY_OEFF_SIMS_AUS = "dossier_oeff_sims_aus" # Style: "ohne"|"schmal"|" _KEY_OEFF_SIMS_IN = "dossier_oeff_sims_in" # Style: "ohne"|"schmal"|"standard"|"breit" _KEY_OEFF_GLAS = "dossier_oeff_glas" # "1"|"0" — sichtbare Glas-Scheibe _KEY_OEFF_REFERENZ = "dossier_oeff_referenz" # "mid" | "links" | "rechts" — Lage des Klick-Punkts in der Oeffnung +_KEY_OEFF_DARSTELLUNG = "dossier_oeff_darstellung" # "einfach" | "standard" | "detail" — SIA-400 LoD +_KEY_OEFF_AUSSENSEITE = "dossier_oeff_aussenseite" # "links" | "rechts" — welche Wand-Seite ist aussen +_KEY_OEFF_TUER_RAHMEN = "dossier_oeff_tuer_rahmen" # "zarge" | "block" — Tueren-Rahmen-Typ +_KEY_OEFF_RAHMEN_OFFSET = "dossier_oeff_rahmen_offset" # float (m): Abstand Rahmen-Innenkante von Wand-Innenseite +_KEY_AKTIVE_DARSTELLUNG = "dossier_aktive_darstellung" # doc-level global override: einfach|standard|detail|"" (= per-object) +_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) + +_OEFF_DARSTELLUNGEN = ("einfach", "standard", "detail") + + +def get_aktive_darstellung(doc): + """Liest die doc-level Darstellungs-Override. Leer → per-object.""" + if doc is None: return "" + try: + v = doc.Strings.GetValue(_KEY_AKTIVE_DARSTELLUNG) or "" + except Exception: + v = "" + return v if v in _OEFF_DARSTELLUNGEN else "" + + +def set_aktive_darstellung(doc, value): + """Setzt die doc-level Darstellungs-Override. Leer → clear.""" + if doc is None: return + try: + if value and value in _OEFF_DARSTELLUNGEN: + doc.Strings.SetString(_KEY_AKTIVE_DARSTELLUNG, value) + else: + doc.Strings.Delete(_KEY_AKTIVE_DARSTELLUNG) + except Exception as ex: + print("[ELEMENTE] set_aktive_darstellung:", ex) + + +# --------------------------------------------------------------------------- +# Fenster/Tueren-Styles (Presets) — analog text_create.list_styles + +_OEFF_STYLE_FIELDS = ( + "typ", "breite", "hoehe", "brueest", + "rahmenB", "rahmenTiefe", "rahmenOffset", + "fluegel", "simsAus", "glas", + "darstellung", "tuerRahmen", +) + +_OEFF_DEFAULT_STYLES = [ + {"name": "Fenster Standard", "typ": "fenster", + "breite": 1.20, "hoehe": 1.40, "brueest": 0.90, + "rahmenB": 0.06, "rahmenTiefe": 0.08, "rahmenOffset": 0.05, + "fluegel": 2, "simsAus": "standard", "glas": True, + "darstellung": "standard"}, + {"name": "Fenster Gross", "typ": "fenster", + "breite": 2.00, "hoehe": 1.80, "brueest": 0.40, + "rahmenB": 0.08, "rahmenTiefe": 0.10, "rahmenOffset": 0.05, + "fluegel": 3, "simsAus": "breit", "glas": True, + "darstellung": "standard"}, + {"name": "Fenster Bandlage (boden)", "typ": "fenster", + "breite": 3.00, "hoehe": 0.60, "brueest": 0.00, + "rahmenB": 0.06, "rahmenTiefe": 0.08, "rahmenOffset": 0.05, + "fluegel": 3, "simsAus": "schmal", "glas": True, + "darstellung": "standard"}, + {"name": "Tuer Innen", "typ": "tuer", + "breite": 0.90, "hoehe": 2.10, "brueest": 0.00, + "rahmenB": 0.05, "rahmenTiefe": 0.08, "rahmenOffset": 0.05, + "fluegel": 1, "simsAus": "ohne", "glas": False, + "darstellung": "standard", "tuerRahmen": "zarge"}, + {"name": "Tuer Eingang", "typ": "tuer", + "breite": 1.00, "hoehe": 2.20, "brueest": 0.00, + "rahmenB": 0.08, "rahmenTiefe": 0.10, "rahmenOffset": 0.05, + "fluegel": 1, "simsAus": "ohne", "glas": False, + "darstellung": "standard", "tuerRahmen": "block"}, + {"name": "Tuer Verglast", "typ": "tuer", + "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"}, +] + + +def _normalize_oeff_style(s): + """Filtert auf erlaubte Felder + setzt sinnvolle Defaults.""" + out = {} + for k in _OEFF_STYLE_FIELDS: + if k in s: out[k] = s[k] + return out + + +def list_oeff_styles(doc, typ=None): + """Liste aller Window/Tuer-Styles. typ='fenster'|'tuer' filtert. + Seedet Defaults beim ersten Zugriff.""" + if doc is None: return [] + import json, uuid + try: + raw = doc.Strings.GetValue(_KEY_OEFF_STYLES) + if not raw: + seeded = [] + for i, s in enumerate(_OEFF_DEFAULT_STYLES): + norm = _normalize_oeff_style(s) + norm["id"] = "os_default_" + str(i) + norm["name"] = s["name"] + seeded.append(norm) + try: doc.Strings.SetString(_KEY_OEFF_STYLES, json.dumps(seeded)) + except Exception: pass + items = seeded + else: + parsed = json.loads(raw) + items = parsed if isinstance(parsed, list) else [] + out = [] + for it in items: + if not isinstance(it, dict): continue + n = _normalize_oeff_style(it) + n["id"] = it.get("id") or ("os_" + uuid.uuid4().hex[:8]) + n["name"] = it.get("name") or "Stil" + out.append(n) + if typ in ("fenster", "tuer"): + out = [s for s in out if s.get("typ") == typ] + return out + except Exception as ex: + print("[ELEMENTE] list_oeff_styles:", ex) + return [] + + +def save_oeff_style(doc, name, settings): + """Speichert (oder updated) einen Style unter `name`. Returns ID.""" + if doc is None or not name: return None + import json, uuid + items_all = [] + try: + raw = doc.Strings.GetValue(_KEY_OEFF_STYLES) + if raw: + parsed = json.loads(raw) + if isinstance(parsed, list): + items_all = [it for it in parsed if isinstance(it, dict)] + except Exception: pass + sid = None + for it in items_all: + if it.get("name") == name: + sid = it.get("id"); break + norm = _normalize_oeff_style(settings or {}) + norm["id"] = sid or ("os_" + uuid.uuid4().hex[:8]) + norm["name"] = name + if sid: + items_all = [norm if it.get("id") == sid else it for it in items_all] + else: + items_all.append(norm) + try: doc.Strings.SetString(_KEY_OEFF_STYLES, json.dumps(items_all)) + except Exception as ex: + print("[ELEMENTE] save_oeff_style:", ex) + return norm["id"] + + +def delete_oeff_style(doc, sid): + if doc is None or not sid: return + import json + try: + items = list_oeff_styles(doc) + items = [it for it in items if it.get("id") != sid] + doc.Strings.SetString(_KEY_OEFF_STYLES, json.dumps(items)) + except Exception: pass + + +def get_active_oeff_style_id(doc, typ): + if doc is None: return None + try: + raw = doc.Strings.GetValue(_KEY_OEFF_STYLE_ACTIVE + "_" + typ) + return raw or None + except Exception: + return None + + +def set_active_oeff_style_id(doc, typ, sid): + if doc is None or typ not in ("fenster", "tuer"): return + try: + doc.Strings.SetString(_KEY_OEFF_STYLE_ACTIVE + "_" + typ, sid or "") + except Exception: pass +_OEFF_AUSSENSEITEN = ("links", "rechts") +_OEFF_TUER_RAHMEN = ("zarge", "block") _OEFF_REFERENZ_OPTIONS = ("mid", "links", "rechts") @@ -1705,7 +1881,9 @@ def _attach_meta(obj_attrs, wall_id, type_, geschoss, dicke, uk_over, ok_over, oeff_rahmen_b=None, oeff_rahmen_tiefe=None, oeff_rahmen_pos=None, oeff_fluegel=None, oeff_sims_aus=None, oeff_sims_in=None, oeff_glas=None, - oeff_referenz=None, + oeff_referenz=None, oeff_darstellung=None, + oeff_aussenseite=None, oeff_tuer_rahmen=None, + oeff_rahmen_offset=None, oeff_style_id=None, geschoss_end=None, treppe_breite=None, treppe_n=None, treppe_referenz=None, treppe_modus=None, treppe_lauf_d=None, treppe_art=None, @@ -1775,6 +1953,20 @@ def _attach_meta(obj_attrs, wall_id, type_, geschoss, dicke, uk_over, ok_over, obj_attrs.SetUserString(_KEY_OEFF_GLAS, "1" if bool(oeff_glas) else "0") if oeff_referenz is not None and oeff_referenz in _OEFF_REFERENZ_OPTIONS: obj_attrs.SetUserString(_KEY_OEFF_REFERENZ, oeff_referenz) + if oeff_darstellung is not None and oeff_darstellung in _OEFF_DARSTELLUNGEN: + obj_attrs.SetUserString(_KEY_OEFF_DARSTELLUNG, oeff_darstellung) + if oeff_aussenseite is not None and oeff_aussenseite in _OEFF_AUSSENSEITEN: + obj_attrs.SetUserString(_KEY_OEFF_AUSSENSEITE, oeff_aussenseite) + if oeff_tuer_rahmen is not None and oeff_tuer_rahmen in _OEFF_TUER_RAHMEN: + obj_attrs.SetUserString(_KEY_OEFF_TUER_RAHMEN, oeff_tuer_rahmen) + if oeff_rahmen_offset is not None: + try: + v = max(0.0, float(oeff_rahmen_offset)) + obj_attrs.SetUserString(_KEY_OEFF_RAHMEN_OFFSET, "{:.4f}".format(v)) + except Exception: pass + if oeff_style_id is not None: + try: obj_attrs.SetUserString(_KEY_OEFF_STYLE_ID, str(oeff_style_id) or "") + except Exception: pass # --- Treppen-Felder --- if geschoss_end is not None: obj_attrs.SetUserString(_KEY_GESCHOSS_END, geschoss_end or "") @@ -1925,6 +2117,19 @@ def _read_meta(obj): ogl = (og_raw == "1") if og_raw in ("0", "1") else is_fenster oref = a.GetUserString(_KEY_OEFF_REFERENZ) or "mid" if oref not in _OEFF_REFERENZ_OPTIONS: oref = "mid" + odarst = a.GetUserString(_KEY_OEFF_DARSTELLUNG) or "standard" + if odarst not in _OEFF_DARSTELLUNGEN: odarst = "standard" + oauss = a.GetUserString(_KEY_OEFF_AUSSENSEITE) or "rechts" + if oauss not in _OEFF_AUSSENSEITEN: oauss = "rechts" + otrah = a.GetUserString(_KEY_OEFF_TUER_RAHMEN) or "zarge" + if otrah not in _OEFF_TUER_RAHMEN: otrah = "zarge" + # Rahmen-Offset (m, von Wand-Innenseite). Default 5cm. Wenn Legacy- + # Wert (rahmen_pos) gesetzt aber kein offset, benutzt build-Logik + # weiterhin den Preset. + try: oro = float(a.GetUserString(_KEY_OEFF_RAHMEN_OFFSET) or "0.05") + except Exception: oro = 0.05 + if oro < 0: oro = 0.0 + ostyle = a.GetUserString(_KEY_OEFF_STYLE_ID) or "" # Treppen-Felder gend = a.GetUserString(_KEY_GESCHOSS_END) or "" try: tb = float(a.GetUserString(_KEY_TREPPE_BREITE) or "1.0") @@ -2041,6 +2246,11 @@ def _read_meta(obj): "oeff_sims_in": osi, "oeff_glas": ogl, "oeff_referenz": oref, + "oeff_darstellung": odarst, + "oeff_aussenseite": oauss, + "oeff_tuer_rahmen": otrah, + "oeff_rahmen_offset": oro, + "oeff_style_id": ostyle, "geschoss_end": gend, "treppe_breite": tb, "treppe_n": tn, @@ -2281,6 +2491,26 @@ def _make_oeffnung_pieces(axis_curve, point_on_axis, wall_dicke, oeff_meta, base sims_in_style = oeff_meta.get("oeff_sims_in", "ohne") has_glas = bool(oeff_meta.get("oeff_glas", False)) is_tuer = (oeff_meta.get("oeff_typ") == "tuer") + # Doc-level Darstellungs-Override gewinnt vor per-Object-Setting — + # damit Ausschnitt-Wechsel / Oberleiste-Quick-Switch alle Oeffnungen + # konsistent zwingen koennen. + _doc = Rhino.RhinoDoc.ActiveDoc + global_darst = get_aktive_darstellung(_doc) if _doc is not None else "" + darstellung = global_darst or oeff_meta.get("oeff_darstellung", "standard") + if darstellung not in _OEFF_DARSTELLUNGEN: + darstellung = "standard" + # Aussenseite: +1 = aussen ist +Plane.ZAxis (default), -1 = aussen ist + # -Plane.ZAxis (Wand "umgedreht"). Wird verwendet um Sims aus/in + # konsistent zu platzieren unabhaengig von der Wand-Achsen-Richtung. + aussenseite = oeff_meta.get("oeff_aussenseite", "rechts") + aus_sign = +1 if aussenseite == "rechts" else -1 + # Tueren-Rahmen-Typ: 'zarge' (klassisch, sitzt IM Wandquerschnitt) oder + # 'block' (Blockrahmen, sitzt UM die Oeffnung und ragt seitlich raus). + tuer_rahmen = oeff_meta.get("oeff_tuer_rahmen", "zarge") + # Rahmen-Offset (m): Abstand der Rahmen-Innenkante von der Wand- + # Innenseite. 0 = bündig innen, wand_dicke-rahmen_t = bündig aussen. + try: rahmen_offset = max(0.0, float(oeff_meta.get("oeff_rahmen_offset", 0.05))) + except Exception: rahmen_offset = 0.05 half_b = breite * 0.5 half_d = float(wall_dicke) * 0.5 @@ -2301,14 +2531,60 @@ def _make_oeffnung_pieces(axis_curve, point_on_axis, wall_dicke, oeff_meta, base if inner_l >= inner_r - 1e-6 or payload_z_lo >= payload_z_hi - 1e-6: return [] # Rahmen-Profil zu dick fuer Oeffnung - frame_perp_lo, frame_perp_hi = _resolve_rahmen_perp_range( - half_d, rahmen_t, rahmen_pos) + # Rahmen-Position aus Offset bestimmen. aus_sign=+1 -> Innen ist + # -perp-Seite. Inside-Edge = -half_d + offset. Outside-Edge = inside + # + rahmen_t. Maximaler Offset = wand_dicke - rahmen_t (sonst ragt + # der Rahmen raus). Clamping mit 1mm Inset gegen Z-Fight. + rt = max(0.01, float(rahmen_t)) + rt = min(rt, 2.0 * half_d - 0.002) + max_offset = 2.0 * half_d - rt - 0.001 + eff_offset = min(max(0.0, rahmen_offset), max(0.0, max_offset)) + if aus_sign > 0: + frame_perp_lo = -half_d + eff_offset + frame_perp_hi = frame_perp_lo + rt + else: + frame_perp_hi = +half_d - eff_offset + frame_perp_lo = frame_perp_hi - rt + + # --- EINFACH (1:100): nur eine flache Scheibe OHNE Tiefe in der + # Rahmen-Mittelebene (= dort wo das Glas im Standard sitzt). Im + # 2D-Plan ergibt das eine einzelne Linie quer durch die Oeffnung, + # auf dem korrekten Tiefen-Offset. + if darstellung == "einfach": + try: + pane_perp = (frame_perp_lo + frame_perp_hi) * 0.5 + # Plane.Origin ans Wand-Achsenpunkt verschoben um pane_perp + # entlang der Plane-Normale (=tan x Z = (0,0,1) cross tan). + normal = rg.Vector3d.CrossProduct(tan, rg.Vector3d(0, 0, 1)) + try: normal.Unitize() + except Exception: pass + origin = rg.Point3d(pt.X + normal.X * pane_perp, + pt.Y + normal.Y * pane_perp, + 0) + plane = rg.Plane(origin, tan, rg.Vector3d(0, 0, 1)) + surf = rg.PlaneSurface(plane, + rg.Interval(-half_b, +half_b), + rg.Interval(z_lo, z_hi)) + brep = surf.ToBrep() + return [brep] if brep is not None else [] + except Exception as ex: + print("[ELEMENTE] einfach pane:", ex) + return [] pieces = [] - # --- RAHMEN: outer box - inner box, sauberer single-Brep + # --- RAHMEN: outer box - inner box, sauberer single-Brep. + # Tueren mit 'block'-Rahmen: outer box ist breiter+hoeher als die + # Oeffnung (Blockrahmen sitzt UM den Wanddurchbruch). Sonst klassische + # Zarge: outer = Oeffnungsmasse. + is_block = (is_tuer and tuer_rahmen == "block") + block_overlap = 0.05 # 5cm seitlich + oben + out_l = (-half_b - block_overlap) if is_block else -half_b + out_r = (+half_b + block_overlap) if is_block else +half_b + out_z_hi = (z_hi + block_overlap) if is_block else z_hi + out_z_lo = z_lo # unten immer bei z_lo (Boden / Bruestung) try: - outer_box = _make_oeff_box(pt, tan, -half_b, +half_b, z_lo, z_hi, + outer_box = _make_oeff_box(pt, tan, out_l, out_r, out_z_lo, out_z_hi, frame_perp_lo, frame_perp_hi) # Inner box leicht laenger in perp Richtung damit der Diff sauber # durchschneidet (keine Hauchschicht uebrig). @@ -2348,45 +2624,52 @@ def _make_oeffnung_pieces(axis_curve, point_on_axis, wall_dicke, oeff_meta, base if fill_t > 0: fill_mid = (frame_perp_lo + frame_perp_hi) * 0.5 - fill_lo = fill_mid - fill_t * 0.5 - fill_hi = fill_mid + fill_t * 0.5 - if fluegel > 1: - span = inner_r - inner_l - for i in range(fluegel): - fx_lo = inner_l + span * (float(i) / fluegel) - fx_hi = inner_l + span * (float(i + 1) / fluegel) - if i > 0: fx_lo += rahmen_b * 0.5 - if i < fluegel - 1: fx_hi -= rahmen_b * 0.5 - fp = _make_oeff_box(pt, tan, fx_lo, fx_hi, - payload_z_lo, payload_z_hi, - fill_lo, fill_hi) - if fp is not None: pieces.append(fp) + # DETAIL (1:20): Doppelverglasung — 2 Scheiben a 6mm, 16mm SZR + is_double_glas = (darstellung == "detail" and has_glas and not is_tuer) + if is_double_glas: + single_t = 0.006 + szr = 0.016 + total = single_t * 2 + szr + outer_lo = fill_mid - total * 0.5 + pane_specs = [ + (outer_lo, outer_lo + single_t), + (outer_lo + single_t + szr, + outer_lo + single_t + szr + single_t), + ] else: - fp = _make_oeff_box(pt, tan, inner_l, inner_r, - payload_z_lo, payload_z_hi, - fill_lo, fill_hi) - if fp is not None: pieces.append(fp) + pane_specs = [(fill_mid - fill_t * 0.5, fill_mid + fill_t * 0.5)] + for (fl, fh) in pane_specs: + if fluegel > 1: + span = inner_r - inner_l + for i in range(fluegel): + fx_lo = inner_l + span * (float(i) / fluegel) + fx_hi = inner_l + span * (float(i + 1) / fluegel) + if i > 0: fx_lo += rahmen_b * 0.5 + if i < fluegel - 1: fx_hi -= rahmen_b * 0.5 + fp = _make_oeff_box(pt, tan, fx_lo, fx_hi, + payload_z_lo, payload_z_hi, + fl, fh) + if fp is not None: pieces.append(fp) + else: + fp = _make_oeff_box(pt, tan, inner_l, inner_r, + payload_z_lo, payload_z_hi, + fl, fh) + if fp is not None: pieces.append(fp) - # --- SIMS AUSSEN (+Plane.ZAxis-Seite) — Platte unter der Oeffnung + # --- SIMS — nur AUSSEN. aus_sign=+1 -> sims auf +perp, =-1 -> auf + # -perp. Drinnen nie. Sim ist gleichzeitig der visuelle Indikator + # fuer die Aussenseite (Test: aussenseite togglen → Sim wechselt). sa = _OEFF_SIMS_STYLES.get(sims_aus_style) if sa is not None: s_t = sa["dicke"]; s_pr = sa["aus"]; s_oh = sa["ueberhang"] s_lo = z_lo - s_t + if aus_sign > 0: + p_lo, p_hi = +half_d, +half_d + s_pr + else: + p_lo, p_hi = -half_d - s_pr, -half_d sb = _make_oeff_box(pt, tan, -half_b - s_oh, +half_b + s_oh, - s_lo, z_lo, - +half_d, +half_d + s_pr) - if sb is not None: pieces.append(sb) - - # --- SIMS INNEN (-Plane.ZAxis-Seite) — Platte unter der Oeffnung - si = _OEFF_SIMS_STYLES.get(sims_in_style) - if si is not None: - s_t = si["dicke"]; s_pr = si["aus"]; s_oh = si["ueberhang"] - s_lo = z_lo - s_t - sb = _make_oeff_box(pt, tan, - -half_b - s_oh, +half_b + s_oh, - s_lo, z_lo, - -half_d - s_pr, -half_d) + s_lo, z_lo, p_lo, p_hi) if sb is not None: pieces.append(sb) return pieces @@ -4206,7 +4489,12 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name oeff_sims_aus=op_meta.get("oeff_sims_aus"), oeff_sims_in=op_meta.get("oeff_sims_in"), oeff_glas=op_meta.get("oeff_glas"), - oeff_referenz=op_meta.get("oeff_referenz")) + oeff_referenz=op_meta.get("oeff_referenz"), + oeff_darstellung=op_meta.get("oeff_darstellung"), + 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")) doc.Objects.AddBrep(pbrep, op_attrs) # Source-Layer migrieren + Volumen-Layer-Index ermitteln @@ -4684,6 +4972,18 @@ class ElementeBridge(panel_base.BaseBridge): elif t == "DELETE_WALL": self._delete_wall(p.get("id")) elif t == "DELETE_ELEMENT": self._delete_wall(p.get("id")) elif t == "REGENERATE_ALL": self._regenerate_all() + elif t == "SAVE_OEFF_STYLE": + try: + doc = Rhino.RhinoDoc.ActiveDoc + save_oeff_style(doc, p.get("name") or "Stil", p.get("settings") or {}) + except Exception as ex: print("[ELEMENTE] save oeff style:", ex) + self._send_state() + elif t == "DELETE_OEFF_STYLE": + try: + doc = Rhino.RhinoDoc.ActiveDoc + delete_oeff_style(doc, p.get("id")) + except Exception as ex: print("[ELEMENTE] del oeff style:", ex) + self._send_state() elif t == "OPEN_ELEMENTE_UEBERSICHT": try: import elemente_uebersicht @@ -4802,6 +5102,11 @@ class ElementeBridge(panel_base.BaseBridge): "simsIn": meta.get("oeff_sims_in", "ohne"), "glas": bool(meta.get("oeff_glas", False)), "oeffReferenz": meta.get("oeff_referenz", "mid"), + "darstellung": meta.get("oeff_darstellung", "standard"), + "aussenseite": meta.get("oeff_aussenseite", "rechts"), + "tuerRahmen": meta.get("oeff_tuer_rahmen", "zarge"), + "rahmenOffset": meta.get("oeff_rahmen_offset", 0.05), + "styleId": meta.get("oeff_style_id", ""), }) elif meta["type"] == "treppe_axis": gs = _geschoss_by_id(doc, meta["geschoss"]) @@ -4911,6 +5216,7 @@ class ElementeBridge(panel_base.BaseBridge): {"name": n, "color": m["color"], "hatch": m.get("hatch", ""), "scale": m.get("scale", 1.0)} for n, m in _MATERIAL_LIBRARY.items()], + "oeffStyles": list_oeff_styles(doc), } self.send("STATE", payload) # An Properties-Satellite-Window forwarden falls offen @@ -5691,6 +5997,37 @@ class ElementeBridge(panel_base.BaseBridge): try: preview_base_z = float(axis_curve.PointAtStart.Z) except Exception: preview_base_z = 0.0 + # Entwurfs-Defaults pro Typ — VOR der Loop damit der Stil-Picker + # sie ueberschreiben kann. + is_fenster = (typ == "fenster") + rahmen_b_def = _last("oeff_rahmen_b", 0.06) + rahmen_t_def = _last("oeff_rahmen_tiefe", 0.08) + rahmen_offset_def = _last("oeff_rahmen_offset", 0.05) + fluegel_def = _last("{}_fluegel".format(typ), 2 if is_fenster else 1) + simsa_def = "standard" if is_fenster else "ohne" + glas_def = is_fenster + referenz_def = _last("oeff_referenz", "mid") + darst_def = "standard" + tuer_rahmen_def = "zarge" + # Pending-Style-ID aus sticky (von Stil-Picker gesetzt). Falls noch + # kein Style gepickt, nutzen wir den zuletzt-aktiven fuer diesen typ + # als Default-Source. + active_sid = get_active_oeff_style_id(doc, typ) + if active_sid: + for s in list_oeff_styles(doc, typ): + if s.get("id") == active_sid: + if "rahmenB" in s: rahmen_b_def = float(s["rahmenB"]) + if "rahmenTiefe" in s: rahmen_t_def = float(s["rahmenTiefe"]) + if "rahmenOffset" in s: rahmen_offset_def = float(s["rahmenOffset"]) + if "fluegel" in s: fluegel_def = int(s["fluegel"]) + if "simsAus" in s: simsa_def = s["simsAus"] + if "glas" in s: glas_def = bool(s["glas"]) + if "darstellung" in s: darst_def = s["darstellung"] + if typ == "tuer" and "tuerRahmen" in s: + tuer_rahmen_def = s["tuerRahmen"] + break + pending_sid = active_sid or "" + # 2) Punkt auf der Achse — constrained an die Wand-Achse try: while True: @@ -5701,8 +6038,12 @@ class ElementeBridge(panel_base.BaseBridge): prompt += ", Br={:.2f}".format(brueest) prompt += "]" gp.SetCommandPrompt(prompt) - try: gp.Constrain(axis_curve, False) - except Exception: pass + # KEINE Constrain mehr — der User soll perpendicular zur + # Achse klicken koennen damit wir die Aussenseite ableiten + # koennen. Position wird via ClosestPoint auf die Achse + # projiziert. Das DynamicDraw-Preview projiziert intern + # auch, der Quader sitzt also weiterhin sauber auf der + # Achse — nur die Cursor-Position bleibt frei. # Live-Preview: gruener Oeffnungs-Quader mit Glas-Diagonalen, # Brueest-Marker und Mass-Label oberhalb des Sturzes try: @@ -5717,9 +6058,47 @@ class ElementeBridge(panel_base.BaseBridge): opt_b = gp.AddOption("Breite") opt_h = gp.AddOption("Hoehe") opt_br = gp.AddOption("Bruestung") if typ == "fenster" else None + # Stil-Picker: zeigt verfuegbare Styles als Sub-Optionen + opt_st = gp.AddOption("Stil") rp = gp.Get() if rp == GetResult.Option: idx = gp.OptionIndex() + if idx == opt_st: + styles = list_oeff_styles(doc, typ) + if not styles: + print("[ELEMENTE] Keine Styles fuer {}".format(typ)) + else: + try: + go = ric.GetOption() + go.SetCommandPrompt("Stil waehlen") + opt_map = [] + for s in styles: + safe = s["name"].replace(" ", "_") + opt_map.append((go.AddOption(safe), s)) + if go.Get() == GetResult.Option: + oi = go.OptionIndex() + chosen = next((s for (i, s) in opt_map + if i == oi), None) + if chosen is not None: + if "breite" in chosen: breite = float(chosen["breite"]) + if "hoehe" in chosen: hoehe = float(chosen["hoehe"]) + if typ == "fenster" and "brueest" in chosen: + brueest = float(chosen["brueest"]) + if "rahmenB" in chosen: rahmen_b_def = float(chosen["rahmenB"]) + if "rahmenTiefe" in chosen: rahmen_t_def = float(chosen["rahmenTiefe"]) + if "rahmenOffset" in chosen: rahmen_offset_def = float(chosen["rahmenOffset"]) + if "fluegel" in chosen: fluegel_def = int(chosen["fluegel"]) + if "simsAus" in chosen: simsa_def = chosen["simsAus"] + if "glas" in chosen: glas_def = bool(chosen["glas"]) + if "darstellung" in chosen: darst_def = chosen["darstellung"] + if typ == "tuer" and "tuerRahmen" in chosen: + tuer_rahmen_def = chosen["tuerRahmen"] + pending_sid = chosen["id"] + set_active_oeff_style_id(doc, typ, chosen["id"]) + print("[ELEMENTE] Stil '{}' geladen".format(chosen["name"])) + except Exception as ex: + print("[ELEMENTE] Stil-Picker:", ex) + continue if idx == opt_b: gn = ric.GetNumber() gn.SetCommandPrompt("Breite") @@ -5753,6 +6132,22 @@ class ElementeBridge(panel_base.BaseBridge): except Exception as ex: print("[ELEMENTE] ClosestPoint:", ex); return + # Aussenseite aus Click-Richtung ableiten: Vektor on_axis→click_pt + # mit der Perp-Richtung der Achse vergleichen. Vorzeichen entscheidet + # ob aussen=+perp (=rechts) oder aussen=-perp (=links). Bei Klick + # direkt auf der Achse: Default 'rechts'. + detected_aussen = "rechts" + try: + tan_at = axis_curve.TangentAt(t) + perp = rg.Vector3d.CrossProduct(tan_at, rg.Vector3d(0, 0, 1)) + dx = click_pt.X - on_axis.X + dy = click_pt.Y - on_axis.Y + side = perp.X * dx + perp.Y * dy + if side < -1e-6: detected_aussen = "links" + elif side > 1e-6: detected_aussen = "rechts" + except Exception as ex: + print("[ELEMENTE] aussenseite detect:", ex) + # Point-Objekt mit Metadaten anlegen prefix = "fenster_" if typ == "fenster" else "tuer_" oeff_id = prefix + uuid.uuid4().hex[:10] @@ -5761,17 +6156,6 @@ class ElementeBridge(panel_base.BaseBridge): geschoss_name = g.get("name", "EG") if g else "EG" layer = _ensure_layer(doc, _layer_path_axis(doc, geschoss_name)) - # Entwurfs-Defaults pro Typ - is_fenster = (typ == "fenster") - rahmen_b_def = _last("oeff_rahmen_b", 0.06) - rahmen_t_def = _last("oeff_rahmen_tiefe", 0.08) - rahmen_p_def = _last("oeff_rahmen_pos", "mid") - fluegel_def = _last("{}_fluegel".format(typ), 2 if is_fenster else 1) - simsa_def = "standard" if is_fenster else "ohne" - simsi_def = "standard" if is_fenster else "ohne" - glas_def = is_fenster - referenz_def = _last("oeff_referenz", "mid") - attrs = Rhino.DocObjects.ObjectAttributes() attrs.LayerIndex = layer _attach_meta(attrs, oeff_id, "oeffnung_point", geschoss, @@ -5781,12 +6165,16 @@ class ElementeBridge(panel_base.BaseBridge): oeff_brueest=brueest, oeff_rahmen_b=rahmen_b_def, oeff_rahmen_tiefe=rahmen_t_def, - oeff_rahmen_pos=rahmen_p_def, + oeff_rahmen_offset=rahmen_offset_def, oeff_fluegel=fluegel_def, oeff_sims_aus=simsa_def, - oeff_sims_in=simsi_def, + oeff_sims_in="ohne", oeff_glas=glas_def, - oeff_referenz=referenz_def) + oeff_referenz=referenz_def, + oeff_aussenseite=detected_aussen, + oeff_darstellung=darst_def, + oeff_tuer_rahmen=tuer_rahmen_def, + oeff_style_id=pending_sid) # 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 @@ -8210,6 +8598,41 @@ class ElementeBridge(panel_base.BaseBridge): glas = bool(p.get("glas", old_meta.get("oeff_glas", otyp == "fenster"))) oref = p.get("oeffReferenz", old_meta.get("oeff_referenz", "mid")) if oref not in _OEFF_REFERENZ_OPTIONS: oref = "mid" + odarst = p.get("darstellung", old_meta.get("oeff_darstellung", "standard")) + if odarst not in _OEFF_DARSTELLUNGEN: odarst = "standard" + oauss = p.get("aussenseite", old_meta.get("oeff_aussenseite", "rechts")) + if oauss not in _OEFF_AUSSENSEITEN: oauss = "rechts" + otrah = p.get("tuerRahmen", old_meta.get("oeff_tuer_rahmen", "zarge")) + if otrah not in _OEFF_TUER_RAHMEN: otrah = "zarge" + try: oro = float(p.get("rahmenOffset", + old_meta.get("oeff_rahmen_offset", 0.05))) + except Exception: oro = 0.05 + if oro < 0: oro = 0.0 + # Style-Apply: wenn ein styleId im Patch ist, alle Felder + # aus dem Style ueberschreiben (User-Patch in derselben + # Operation gewinnt aber ueber Style — sonst koennte ein + # Stil das eben gemachte Field-Edit wegmaecken). + o_style_id = p.get("styleId", + old_meta.get("oeff_style_id", "")) or "" + 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) + stl = next((s for s in styles if s.get("id") == p["styleId"]), None) + if stl is not None and stl.get("typ") == otyp: + if "breite" in stl: breite = float(stl["breite"]) + if "hoehe" in stl: hoehe = float(stl["hoehe"]) + if otyp == "fenster" and "brueest" in stl: + brueest = float(stl["brueest"]) + if "rahmenB" in stl: rahmen_b = float(stl["rahmenB"]) + if "rahmenTiefe" in stl: rahmen_t = float(stl["rahmenTiefe"]) + if "rahmenOffset" in stl: oro = float(stl["rahmenOffset"]) + if "fluegel" in stl: fluegel = int(stl["fluegel"]) + if "simsAus" in stl: simsa = stl["simsAus"] + if "glas" in stl: glas = bool(stl["glas"]) + if "darstellung" in stl: odarst = stl["darstellung"] + if otyp == "tuer" and "tuerRahmen" in stl: + otrah = stl["tuerRahmen"] + set_active_oeff_style_id(doc, otyp, p["styleId"]) attrs = axis_obj.Attributes _attach_meta(attrs, wall_id, "oeffnung_point", old_meta["geschoss"], old_meta["dicke"], @@ -8224,7 +8647,12 @@ class ElementeBridge(panel_base.BaseBridge): oeff_fluegel=fluegel, oeff_sims_aus=simsa, oeff_sims_in=simsi, oeff_glas=glas, - oeff_referenz=oref) + oeff_referenz=oref, + oeff_darstellung=odarst, + oeff_aussenseite=oauss, + oeff_tuer_rahmen=otrah, + oeff_rahmen_offset=oro, + oeff_style_id=o_style_id) axis_obj.Attributes = attrs axis_obj.CommitChanges() parent_id = old_meta.get("oeff_parent", "") @@ -8345,6 +8773,28 @@ class ElementeBridge(panel_base.BaseBridge): self._send_state() +def regenerate_all_oeffnungen(doc): + """Modul-API: regen aller Oeffnungen + ihrer Parent-Waende. Wird vom + Ausschnitt-Restore / Oberleiste-Darstellungs-Switch gerufen damit + der doc-level Darstellungs-Override sofort wirkt.""" + if doc is None: return 0 + seen_walls = set() + n = 0 + for obj in list(doc.Objects): + meta = _read_meta(obj) + if meta is None: continue + if meta.get("type") != "oeffnung_point": continue + parent = meta.get("oeff_parent") or "" + if parent and parent not in seen_walls: + seen_walls.add(parent) + _regenerate_element(doc, parent) + n += 1 + try: doc.Views.Redraw() + except Exception: pass + print("[ELEMENTE] regen all oeffnungen via {} Waende".format(n)) + return n + + # --- Event-Listener --------------------------------------------------------- # Re-Entry-Guard: wenn _regenerate_volume die Brep ersetzt, feuert das diff --git a/rhino/oberleiste.py b/rhino/oberleiste.py index 3c7a29a..442c2a9 100644 --- a/rhino/oberleiste.py +++ b/rhino/oberleiste.py @@ -981,6 +981,18 @@ class OberleisteBridge(panel_base.BaseBridge): except Exception as ex: print("[OBERLEISTE] open masse:", ex) + # --- Darstellung (SIA-400 LoD globaler Override) ----------------- + elif t == "SET_DARSTELLUNG": + try: + import elemente + doc = Rhino.RhinoDoc.ActiveDoc + new_v = p.get("darstellung") or "" + elemente.set_aktive_darstellung(doc, new_v) + elemente.regenerate_all_oeffnungen(doc) + except Exception as ex: + print("[OBERLEISTE] set darstellung:", ex) + self._send_state(force=True) + # --- Display-Mode ----------------------------------------------- elif t == "SET_DISPLAY_MODE": n = p.get("name") @@ -1226,6 +1238,12 @@ class OberleisteBridge(panel_base.BaseBridge): info["textFonts"] = [] info["textStyles"] = [] info["textStyleActiveId"] = None + # Aktive Darstellung (SIA-400 LoD globaler Override) + try: + import elemente + info["aktiveDarstellung"] = elemente.get_aktive_darstellung(doc) or "" + except Exception: + info["aktiveDarstellung"] = "" # Norden-Rotation fuer N/O/S/W-Buttons try: import kamera diff --git a/rhino/rhinopanel.py b/rhino/rhinopanel.py index b90dbda..0e35134 100644 --- a/rhino/rhinopanel.py +++ b/rhino/rhinopanel.py @@ -98,6 +98,8 @@ def _broadcast_state(doc=None, hatch_patterns=None): "projectZeroMum": zero_mum, "hatchPatterns": hatch_patterns if hatch_patterns is not None else _hatch_pattern_names(doc), + "layerCombinations": list_layer_preset_names(doc), + "layerCombinationActive": get_active_comb_name(doc), } except Exception as ex: print("[EBENEN] broadcast prepare:", ex) @@ -470,6 +472,31 @@ class EbenenBridge(panel_base.BaseBridge): p.get("hatchPatterns") or []) elif t == "OPEN_GESCHOSS_DIALOG": self._open_geschoss_dialog(p.get("zeichnungsebenen") or []) + elif t == "PICK_LAYER_COMBINATION": + doc = Rhino.RhinoDoc.ActiveDoc + name = (p.get("name") or "").strip() + if name: + apply_layer_preset_by_name(doc, name) + else: + set_active_comb_name(doc, None) + _broadcast_state(doc) + _notify_oberleiste_combs() + elif t == "SAVE_LAYER_COMBINATION": + doc = Rhino.RhinoDoc.ActiveDoc + name = (p.get("name") or "").strip() + if name: + save_current_as_layer_preset(doc, name) + _broadcast_state(doc) + _notify_oberleiste_combs() + elif t == "DELETE_LAYER_COMBINATION": + doc = Rhino.RhinoDoc.ActiveDoc + delete_layer_preset(doc, p.get("name") or "") + _broadcast_state(doc) + _notify_oberleiste_combs() + elif t == "OPEN_LAYER_COMBINATIONS_DIALOG": + try: open_layer_combinations_window() + except Exception as ex: + print("[EBENEN] open layer-combinations:", ex) # ---- Helpers ---- diff --git a/src/App.jsx b/src/App.jsx index 2de3936..e141de2 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -32,11 +32,16 @@ export default function App() { const [appliedE, setAppliedE] = useState(INITIAL_EBENEN) const [eMode, setEMode] = useState('all') const [hatchPatterns, setHatchPatterns] = useState(['Solid', 'Hatch1', 'Hatch2', 'Hatch3', 'Plus', 'Squares', 'Grid', 'Grid60']) + const [layerCombinations, setLayerCombinations] = useState([]) + const [activeKombi, setActiveKombi] = useState(null) useEffect(() => { - onMessage('STATE_SYNC', ({ ebenen: e, hatchPatterns: hp }) => { + onMessage('STATE_SYNC', ({ ebenen: e, hatchPatterns: hp, + layerCombinations: lc, layerCombinationActive: ac }) => { if (e) { setEbenen(e); setAppliedE(e) } if (Array.isArray(hp) && hp.length > 0) setHatchPatterns(hp) + if (Array.isArray(lc)) setLayerCombinations(lc) + if (ac !== undefined) setActiveKombi(ac) }) onMessage('FIRST_RUN', ({ defaultEbenen } = {}) => { // Wenn der Dossier-Launcher ein eigenes Schema definiert hat, nutzen wir @@ -132,6 +137,8 @@ export default function App() { mode={eMode} onModeChange={setEMode} hatchPatterns={hatchPatterns} + layerCombinations={layerCombinations} + activeKombi={activeKombi} /> diff --git a/src/AusschnittSettingsApp.jsx b/src/AusschnittSettingsApp.jsx index f9b4c95..a0a1291 100644 --- a/src/AusschnittSettingsApp.jsx +++ b/src/AusschnittSettingsApp.jsx @@ -65,6 +65,7 @@ export default function AusschnittSettingsApp() { overridesEnabled: !!snap.overridesEnabled, overridesPreset: snap.overridesPreset || '', layerCombination: snap.layerCombination || '', + darstellung: snap.darstellung || '', }, }) } @@ -88,6 +89,20 @@ export default function AusschnittSettingsApp() { /> + + + + m - {/* Rahmen-Lage im Wandquerschnitt */} + {/* Rahmen-Lage: Abstand der Rahmen-Innenkante von der Wand-Innenseite */}
+ title="Abstand der Rahmen-Innenkante von der Wand-Innenseite (Aussenseite-Flag oben bestimmt welche Seite innen ist)"> Lage -
- {RAHMEN_POS_OPTIONS.map(o => ( - onUpdate({ rahmenPos: o.code })} - title={o.hint} /> - ))} -
+ { + const v = parseFloat(e.target.value.replace(',', '.')) + if (!Number.isNaN(v) && v >= 0) onUpdate({ rahmenOffset: v }) + }} + onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }} + style={{ flex: 1, fontSize: 11, fontFamily: 'DM Mono, monospace' }} /> + m v. innen
{/* Fluegel-Anzahl — nur fuer Fenster (Tueren haben ein einzelnes Tuerblatt) */} @@ -1846,34 +1930,24 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) { )} - {/* Sims-Stile (aussen / innen) — nur fuer Fenster */} + {/* Sims — nur aussen. Innen gibt's bewusst nicht. Dient zugleich + als visueller Indikator fuer die Aussenseite-Einstellung. */} {isFenster && ( - <> -
- - Sims a. - - +
-
- - Sims i. - - -
- + )} {/* Glas-Toggle: bei Tueren ersetzt Glas das Tuerblatt (verglaste Tuer) */} diff --git a/src/ElementePropertiesApp.jsx b/src/ElementePropertiesApp.jsx index 6296cfa..50de69a 100644 --- a/src/ElementePropertiesApp.jsx +++ b/src/ElementePropertiesApp.jsx @@ -33,6 +33,7 @@ export default function ElementePropertiesApp() { geschosse={state.geschosse || []} materials={state.materials || []} hatchPatterns={state.hatchPatterns} + oeffStyles={state.oeffStyles || []} /> ) : (
{dm.name} ))} - {/* Reihe 1, Spalte 2: Ebenenkombination */} + {/* Reihe 1, Spalte 2: Modelldarstellung (SIA-400 LoD) */} { - if (v === '__configure__') { openLayerCombinationsDialog(); return } - if (v === '__save__') { - const suggested = state.layerCombinationActive - || `Kombi ${(state.layerCombinations || []).length + 1}` - const name = (window.prompt('Name für Ebenenkombination:', suggested) || '').trim() - if (!name) return - if ((state.layerCombinations || []).includes(name) && - !window.confirm(`"${name}" überschreiben?`)) return - saveLayerCombination(name) - return - } - if (v === '__delete__') { - if (state.layerCombinationActive && - window.confirm(`Kombination "${state.layerCombinationActive}" löschen?`)) - deleteLayerCombination(state.layerCombinationActive) - return - } - pickLayerCombination(v === '__none__' ? null : v) - }} - title={state.layerCombinationActive - ? `Aktive Kombi: ${state.layerCombinationActive}` - : 'Keine Kombination — manuelle Sichtbarkeit'} + icon="tune" + value={state.aktiveDarstellung || ''} + onChange={(v) => setDarstellung(v)} + title="Darstellungs-Override fuer Fenster/Tueren (SIA-400 LoD)" width={PRESET_W} - onGear={openLayerCombinationsDialog} - gearTitle="Ebenenkombinationen bearbeiten" > - - {(state.layerCombinations || []).map(name => ( - - ))} - - - {state.layerCombinationActive && ( - - )} - + + + + {/* Reihe 2, Spalte 1: Overrides (Toggle als Icon links) */} {icon && (iconClickable ? ( + )} + {onSecond && ( + )} diff --git a/src/components/EbenenManager.jsx b/src/components/EbenenManager.jsx index 837271f..c00f47f 100644 --- a/src/components/EbenenManager.jsx +++ b/src/components/EbenenManager.jsx @@ -3,7 +3,9 @@ import Icon from './Icon' import ConfirmDeleteEbene from './ConfirmDeleteEbene' import ContextMenu from './ContextMenu' import { BarCombo, BarButton } from './BarControls' -import { setLayerStyle, deleteEbene, moveSelectionToEbene, openEbenenSettings } from '../lib/rhinoBridge' +import { setLayerStyle, deleteEbene, moveSelectionToEbene, openEbenenSettings, + pickLayerCombination, saveLayerCombination, deleteLayerCombination, + openLayerCombinationsDialog } from '../lib/rhinoBridge' const MODES = [ { value: 'all_force', label: 'Alle anzeigen' }, @@ -245,8 +247,8 @@ function EbeneRow({ e, depth, hasChildren, expanded, onToggleExpand, active, mod onContextMenu={onContextMenu} style={{ display: 'flex', alignItems: 'center', gap: 4, - padding: '1px 8px', - paddingLeft: 6 + (depth || 0) * 10, + padding: '1px 12px 1px 0', + paddingLeft: (depth || 0) * 12, margin: 0, background: active ? 'var(--active-dim)' : (e.visible !== false) ? 'var(--bg-item)' @@ -345,6 +347,7 @@ function SortHeader({ label, sortKey, sortBy, sortDir, onSort, style }) { export default function EbenenManager({ ebenen, activeCode, onActiveChange, onChange, mode, onModeChange, hatchPatterns, + layerCombinations = [], activeKombi = null, }) { const [sortBy, setSortBy] = useState('code') const [sortDir, setSortDir] = useState('asc') @@ -548,70 +551,114 @@ export default function EbenenManager({ background: 'var(--bg-section)', borderBottom: '1px solid var(--border-light)', }}> - Sichtbarkeit -
-
- - {MODES.map(m => )} - -
- + Ebenenkombination +
+ { + if (v === '__configure__') { openLayerCombinationsDialog(); return } + if (v === '__save__') { + const suggested = activeKombi || `Kombi ${layerCombinations.length + 1}` + const name = (window.prompt('Name für Ebenenkombination:', suggested) || '').trim() + if (!name) return + if (layerCombinations.includes(name) && + !window.confirm(`"${name}" überschreiben?`)) return + saveLayerCombination(name) + return + } + if (v === '__delete__') { + if (activeKombi && + window.confirm(`Kombination "${activeKombi}" löschen?`)) + deleteLayerCombination(activeKombi) + return + } + pickLayerCombination(v === '__none__' ? null : v) + }} + title={activeKombi + ? `Aktive Kombi: ${activeKombi}` + : 'Keine Kombination — manuelle Sichtbarkeit'} + onGear={openLayerCombinationsDialog} + gearTitle="Ebenenkombinationen bearbeiten" + > + + {layerCombinations.map(n => ( + + ))} + + + {activeKombi && ( + + )} + +
- {/* Master-Eye: alle Ebenen sichtbar/unsichtbar */} + Sichtbarkeit +
+ + {MODES.map(m => )} + +
+
+ + {/* Sort-Header-Row + Master-Eye/Lock. Padding-Left identisch zu + den Data-Rows damit Eye-Icons aligned sind. Erste 12px-Spanne + spiegelt den Expand-Chevron-Slot der Data-Rows wider. */} +
+ - +
- {/* Master-Lock: alle Ebenen sperren/entsperren */} -
+
{(() => { diff --git a/src/components/GeschossManager.jsx b/src/components/GeschossManager.jsx index 40b25ca..1c5896b 100644 --- a/src/components/GeschossManager.jsx +++ b/src/components/GeschossManager.jsx @@ -48,7 +48,7 @@ function ZeichnungsebeneRow({ onContextMenu={onContextMenu} style={{ display: 'flex', alignItems: 'center', gap: 4, - padding: '1px 12px', + padding: '1px 12px 1px 0', margin: 0, background: active ? 'var(--active-dim)' : 'var(--bg-item)', borderRadius: active ? 999 : 0, @@ -58,6 +58,9 @@ function ZeichnungsebeneRow({ minHeight: 24, }} > + {/* Spacer-Slot — spiegelt den Chevron-Slot bei Ebenen-Rows wider + damit die Eye-Icons beider Panels untereinander stehen. */} + @@ -267,14 +275,14 @@ export default function GeschossManager({ title={zeichnungsebenen.every(z => z.locked === true) ? 'Alle Zeichnungsebenen entsperren' : 'Alle Zeichnungsebenen sperren'} - style={{ width: 18, height: 18 }} + style={{ width: 14, height: 14 }} > z.locked === true) ? 'lock' : 'lock_open'} size={11} /> -
+
diff --git a/src/lib/rhinoBridge.js b/src/lib/rhinoBridge.js index 8ce8509..4ad0945 100644 --- a/src/lib/rhinoBridge.js +++ b/src/lib/rhinoBridge.js @@ -172,6 +172,11 @@ export function openOverridesPanel() { send('OPEN_OVERRIDES_PANEL', {}) } export function openKameraPanel() { send('OPEN_KAMERA_PANEL', {}) } export function openElementeUebersicht() { send('OPEN_ELEMENTE_UEBERSICHT', {}) } export function openElementeProperties() { send('OPEN_ELEMENTE_PROPERTIES', {}) } +export function setDarstellung(d) { send('SET_DARSTELLUNG', { darstellung: d || '' }) } +export function saveOeffStyle(name, settings) { + send('SAVE_OEFF_STYLE', { name, settings }) +} +export function deleteOeffStyle(id) { send('DELETE_OEFF_STYLE', { id }) } export function setSectionStyle(enabled, source, color, pattern, scale, rotation) { send('SET_SECTION_STYLE', { enabled, source, color, pattern, scale, rotation }) }