diff --git a/launcher/src/App.jsx b/launcher/src/App.jsx index 462b9af..a121cab 100644 --- a/launcher/src/App.jsx +++ b/launcher/src/App.jsx @@ -1076,24 +1076,24 @@ const VIEW_COLOR_FIELDS = [ // src/App.jsx des Rhino-Panels bleiben — wenn der User keine eigene Schema // definiert, schickt das Plugin diese hier als FIRST_RUN-Default. const DEFAULT_LAYER_SCHEMA = [ - { code: '00', name: 'RASTER', color: '#484850', lw: 0.13 }, - { code: '01', name: 'VERMESSUNG', color: '#707078', lw: 0.18 }, - { code: '10', name: 'SITUATION', color: '#909090', lw: 0.18 }, - { code: '11', name: 'STRASSE', color: '#a89070', lw: 0.18 }, - { code: '12', name: 'GEBAEUDE', color: '#888888', lw: 0.25 }, - { code: '13', name: 'BAEUME', color: '#50a050', lw: 0.13 }, - { code: '14', name: 'HOEHENLINIEN', color: '#909050', lw: 0.18 }, - { code: '20', name: 'WAENDE', color: '#0a0a0a', lw: 0.50 }, - { code: '21', name: 'TUEREN_FENSTER', color: '#5080c8', lw: 0.25 }, - { code: '22', name: 'MOEBEL', color: '#909090', lw: 0.13 }, - { code: '25', name: 'STUETZEN', color: '#c87050', lw: 0.50 }, - { code: '30', name: 'DECKEN', color: '#605850', lw: 0.35 }, - { code: '31', name: 'DAECHER', color: '#7a4a3a', lw: 0.35 }, - { code: '35', name: 'TRAEGER', color: '#a87858', lw: 0.50 }, - { code: '50', name: 'TEXT', color: '#d0d0d0', lw: 0.13 }, - { code: '60', name: 'PLANGRAFIK', color: '#c0a040', lw: 0.13 }, - { code: '90', name: 'REFERENZEN', color: '#585860', lw: 0.13 }, - { code: '99', name: 'KONSTRUKTION', color: '#404048', lw: 0.13 }, + { code: '00', name: 'Raster', color: '#484850', lw: 0.13 }, + { code: '01', name: 'Vermessung', color: '#707078', lw: 0.18 }, + { code: '10', name: 'Situation', color: '#909090', lw: 0.18 }, + { code: '11', name: 'Strasse', color: '#a89070', lw: 0.18 }, + { code: '12', name: 'Gebaeude', color: '#888888', lw: 0.25 }, + { code: '13', name: 'Baeume', color: '#50a050', lw: 0.13 }, + { code: '14', name: 'Hoehenlinien', color: '#909050', lw: 0.18 }, + { code: '20', name: 'Waende', color: '#0a0a0a', lw: 0.50 }, + { code: '21', name: 'Tueren_Fenster', color: '#5080c8', lw: 0.25 }, + { code: '22', name: 'Moebel', color: '#909090', lw: 0.13 }, + { code: '25', name: 'Stuetzen', color: '#c87050', lw: 0.50 }, + { code: '30', name: 'Decken', color: '#605850', lw: 0.35 }, + { code: '31', name: 'Daecher', color: '#7a4a3a', lw: 0.35 }, + { code: '35', name: 'Traeger', color: '#a87858', lw: 0.50 }, + { code: '50', name: 'Text', color: '#d0d0d0', lw: 0.13 }, + { code: '60', name: 'Plangrafik', color: '#c0a040', lw: 0.13 }, + { code: '90', name: 'Referenzen', color: '#585860', lw: 0.13 }, + { code: '99', name: 'Konstruktion', color: '#404048', lw: 0.13 }, ] // Built-in Presets — nicht in den Settings gespeichert, immer verfuegbar. diff --git a/rhino/ausschnitte.py b/rhino/ausschnitte.py index 0b6de77..d622419 100644 --- a/rhino/ausschnitte.py +++ b/rhino/ausschnitte.py @@ -334,6 +334,7 @@ class AusschnittBridge(panel_base.BaseBridge): elif t == "UPDATE_LAYERS": self._update_layers(p.get("id"), p.get("layers") or []) elif t == "SAVE_PRESET": self._save_preset(p.get("name"), p.get("layers") or []) elif t == "DELETE_PRESET": self._delete_preset(p.get("name")) + elif t == "OPEN_SETTINGS": self._open_settings_window(p.get("id")) def _load(self, doc): raw = doc.Strings.GetValue(_STORE_KEY) @@ -537,8 +538,42 @@ class AusschnittBridge(panel_base.BaseBridge): if view is None: return vp = view.ActiveViewport _apply_camera(vp, snap.get("camera")) - _apply_layers_global(doc, snap.get("layers", [])) + # Layer-Sichtbarkeit: bevorzugt die referenzierte Ebenenkombi (live — + # zeigt aktuelle Kombi-Definition). Fallback: snap.layers (per-snap + # eingefrorener Zustand). + kombi = (snap.get("layerCombination") or "").strip() + if kombi: + try: + import rhinopanel + rhinopanel.apply_layer_preset_by_name(doc, kombi) + except Exception as ex: + print("[AUSSCHNITTE] kombi-apply '{}':".format(kombi), ex) + _apply_layers_global(doc, snap.get("layers", [])) + else: + _apply_layers_global(doc, snap.get("layers", [])) + # Eigene Sichtbarkeit → active_comb_name clearen + try: + import rhinopanel + rhinopanel.set_active_comb_name(doc, None) + rhinopanel._notify_oberleiste_combs() + except Exception: pass _apply_dossier_state(doc, snap.get("dossier") or snap.get("pause") or {}) + # Overrides: nur anwenden wenn das Snap "applyOverrides" gesetzt hat. + # Sonst bleibt der aktuelle User-Override-State unangetastet. + if snap.get("applyOverrides"): + try: + import overrides + overrides.set_enabled(doc, bool(snap.get("overridesEnabled"))) + overrides.set_active_preset(doc, snap.get("overridesPreset") or None) + # Oberleiste-Cache invalidieren damit Topbar das neue Preset zeigt + try: + b = sc.sticky.get("oberleiste_bridge") + if b is not None: + b._cached_overrides = None + b._send_state(force=True) + except Exception: pass + except Exception as ex: + print("[AUSSCHNITTE] overrides-apply:", ex) # Viewport ZUERST umbenennen — der per-Viewport-Massstab in massstab.py # wird unter vp.Name geschluesselt. Erst nach dem Rename schreibt # _apply_scale unter dem neuen Namen, sonst landet der Wert beim alten @@ -703,6 +738,120 @@ class AusschnittBridge(panel_base.BaseBridge): self._store(doc, snaps) self._send_list() + def _open_settings_window(self, snap_id): + """Oeffnet ein Satelliten-Fenster (Eto.Form + WebView) mit dem + Ausschnittseinstellungen-Dialog. Lets User editieren: Massstab, + Display-Mode, Overrides, Ebenenkombi.""" + if not snap_id: return + doc = Rhino.RhinoDoc.ActiveDoc + snap = next((s for s in self._load(doc) if s.get("id") == snap_id), None) + if not snap: + print("[AUSSCHNITTE] open_settings: snap nicht gefunden", snap_id) + return + outer = self + bridge_holder = {"form": None, "id": snap_id} -panel_base.register_and_open("ausschnitte", "AUSSCHNITTE", PANEL_GUID_STR, AusschnittBridge, + def _payload(): + d = Rhino.RhinoDoc.ActiveDoc + sn = next((s for s in outer._load(d) if s.get("id") == bridge_holder["id"]), None) + if sn is None: sn = {} + # Listen fuer Dropdowns + display_modes = [] + try: + import oberleiste + display_modes = oberleiste._list_display_modes() + except Exception as ex: + print("[AUSSCHNITTE] display_modes:", ex) + overrides_presets = [] + try: + import overrides + overrides_presets = [item.get("name") for item in overrides.list_presets() if item.get("name")] + except Exception as ex: + print("[AUSSCHNITTE] overrides_presets:", ex) + layer_kombis = [] + try: + import rhinopanel + layer_kombis = rhinopanel.list_layer_preset_names(d) + except Exception as ex: + print("[AUSSCHNITTE] layer_kombis:", ex) + cam = sn.get("camera") or {} + return { + "snap": { + "id": sn.get("id"), + "name": sn.get("name"), + "scale": sn.get("scale", ""), + "displayMode": cam.get("displayMode"), + "displayModeName": cam.get("displayModeName"), + "applyOverrides": bool(sn.get("applyOverrides", False)), + "overridesEnabled": bool(sn.get("overridesEnabled", False)), + "overridesPreset": sn.get("overridesPreset") or "", + "layerCombination": sn.get("layerCombination") or "", + }, + "displayModes": display_modes, + "overridesPresets": overrides_presets, + "layerKombis": layer_kombis, + } + + def _persist(settings): + d = Rhino.RhinoDoc.ActiveDoc + snaps = outer._load(d) + sid = bridge_holder["id"] + target = next((s for s in snaps if s.get("id") == sid), None) + if target is None: + print("[AUSSCHNITTE] persist settings: snap weg"); return + # Massstab + sc_val = (settings.get("scale") or "").strip() + target["scale"] = sc_val + # Display Mode in camera nested + cam = target.get("camera") or {} + dm_id = settings.get("displayMode") + dm_nm = settings.get("displayModeName") + if dm_id is not None: cam["displayMode"] = dm_id or None + if dm_nm is not None: cam["displayModeName"] = dm_nm or None + target["camera"] = cam + # Overrides + target["applyOverrides"] = bool(settings.get("applyOverrides")) + target["overridesEnabled"] = bool(settings.get("overridesEnabled")) + target["overridesPreset"] = (settings.get("overridesPreset") or "").strip() + # Ebenenkombi + target["layerCombination"] = (settings.get("layerCombination") or "").strip() + outer._store(d, snaps) + outer._send_list() + print("[AUSSCHNITTE] Settings fuer '{}' aktualisiert".format(target.get("name"))) + + class _AusschnittSettingsBridge(panel_base.BaseBridge): + def __init__(self): + panel_base.BaseBridge.__init__(self, "ausschnitt_settings") + def _on_ready(self): + self._send_state() + def _send_state(self): + self.send("AUSSCHNITT_SETTINGS_STATE", _payload()) + def handle(self, data): + if not isinstance(data, dict): return + t = data.get("type", "") + p = data.get("payload") or {} + if t == "READY": + self._on_ready() + elif t == "SAVE": + _persist(p.get("settings") or {}) + try: + f = bridge_holder.get("form") + if f is not None: f.Close() + except Exception: pass + elif t == "CANCEL": + try: + f = bridge_holder.get("form") + if f is not None: f.Close() + except Exception: pass + + b = _AusschnittSettingsBridge() + bridge_holder["form"] = panel_base.open_satellite_window( + "ausschnitt_settings", + params=_payload(), + title="Ausschnitt: {}".format(snap.get("name", "")), + size=(420, 540), + bridge=b) + + +panel_base.register_and_open("ausschnitte", "Ausschnitte", PANEL_GUID_STR, AusschnittBridge, icon_spec=("crop", "#c87050")) diff --git a/rhino/dimensionen.py b/rhino/dimensionen.py index caa59ea..dbfc7e9 100644 --- a/rhino/dimensionen.py +++ b/rhino/dimensionen.py @@ -608,6 +608,6 @@ def _bridge_factory(): return b -panel_base.register_and_open("dimensionen", "DIMENSIONEN", PANEL_GUID_STR, +panel_base.register_and_open("dimensionen", "Dimensionen", PANEL_GUID_STR, _bridge_factory, icon_spec=("aspect_ratio", "#9e7050")) diff --git a/rhino/elemente.py b/rhino/elemente.py index 2179366..6c01f9f 100644 --- a/rhino/elemente.py +++ b/rhino/elemente.py @@ -366,9 +366,9 @@ def _find_ebene_sublayer_name(doc, keywords, default_code, default_name, def _layer_path_axis(doc, geschoss_name): - """Wand-Achse + Volumen — Sublayer 'WÄNDE' (Code 20).""" + """Wand-Achse + Volumen — Sublayer 'Wände' (Code 20).""" sub = _find_ebene_sublayer_name(doc, ["wand", "wände", "waende"], - "20", "WÄNDE", + "20", "Wände", default_color="#0a0a0a", default_lw=0.50) return "{}::{}".format(geschoss_name, sub) @@ -4543,6 +4543,23 @@ class ElementeBridge(panel_base.BaseBridge): elif t == "DELETE_ELEMENT": self._delete_wall(p.get("id")) elif t == "REGENERATE_ALL": self._regenerate_all() + def _notify_active_geschoss(self): + """Schlanker Partial-Push: nur activeGeschoss + activeGeschossName. + Wird vom Ebenen-Bridge bei Geschoss-Wechsel gerufen — die Element- + Liste ist davon nicht betroffen, ein voller _send_state mit Re- + Enumeration aller Smart-Elemente (200+ in echten Projekten) waere + teuer und unnoetig. React-State macht Shallow-Merge, der Rest des + States bleibt.""" + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: return + try: + self.send("STATE", { + "activeGeschoss": _active_geschoss_id(doc), + "activeGeschossName": _active_geschoss_name(doc), + }) + except Exception as ex: + print("[ELEMENTE] _notify_active_geschoss:", ex) + def _send_state(self): doc = Rhino.RhinoDoc.ActiveDoc if doc is None: @@ -8509,6 +8526,6 @@ def _bridge_factory(): return b -panel_base.register_and_open("elemente", "ELEMENTE", PANEL_GUID_STR, +panel_base.register_and_open("elemente", "Elemente", PANEL_GUID_STR, _bridge_factory, icon_spec=("foundation", "#5fa896")) diff --git a/rhino/gestaltung.py b/rhino/gestaltung.py index eaf322b..032c39b 100644 --- a/rhino/gestaltung.py +++ b/rhino/gestaltung.py @@ -1695,5 +1695,5 @@ def _bridge_factory(): return b -panel_base.register_and_open("gestaltung", "GESTALTUNG", PANEL_GUID_STR, _bridge_factory, +panel_base.register_and_open("gestaltung", "Gestaltung", PANEL_GUID_STR, _bridge_factory, icon_spec=("palette", "#5fa896")) diff --git a/rhino/layer_builder.py b/rhino/layer_builder.py index fc8b44b..886acfd 100644 --- a/rhino/layer_builder.py +++ b/rhino/layer_builder.py @@ -578,6 +578,7 @@ def apply_visibility(doc, zeichnungsebenen, ebenen, active_z_id, active_code, z_ children = children_by_parent.get(parent.Id, []) is_active_z = z["id"] == active_z_id z_visible_flag = z.get("visible", True) + z_locked_flag = bool(z.get("locked", False)) # Z-Mode -> Parent-Zustand if is_active_z: @@ -593,6 +594,11 @@ def apply_visibility(doc, zeichnungsebenen, ebenen, active_z_id, active_code, z_ else: # grey p_vis, p_grey, p_lock = True, True, False + # Per-Z explizites Sperren ueberlagert (auch fuer die aktive Z) — wer + # eine Geschoss-Ebene sperrt, will dass Klicks ins Leere gehen. + if z_locked_flag: + p_lock = True + parent_changed = False if parent.IsVisible != p_vis: parent.IsVisible = p_vis diff --git a/rhino/layouts.py b/rhino/layouts.py index 2631ec0..a1cfbb6 100644 --- a/rhino/layouts.py +++ b/rhino/layouts.py @@ -281,6 +281,7 @@ class LayoutsBridge(panel_base.BaseBridge): elif t == "ADD_FOLDER": self._add_folder(p.get("name")) elif t == "REMOVE_FOLDER": self._remove_folder(p.get("name")) elif t == "SET_FOLDER": self._set_folder(p.get("id"), p.get("folder") or "") + elif t == "OPEN_LAYOUT_DIALOG": self._open_layout_dialog(p) # --- State-Snapshot ----------------------------------------------------- @@ -737,6 +738,69 @@ class LayoutsBridge(panel_base.BaseBridge): print("[LAYOUTS] sync layout:", ex) self._send_state() + def _open_layout_dialog(self, p): + """Oeffnet ein Satelliten-Fenster mit dem Layout-Erstellen/Bearbeiten + Dialog. mode = 'new' | 'edit'. Bei 'edit' wird `layout` (id, name, + width, height) mitgeschickt.""" + outer = self + mode = (p.get("mode") or "new") + layout = p.get("layout") or None + bridge_holder = {"form": None} + + def _apply(payload): + if mode == "new": + outer._new_layout({ + "name": payload.get("name") or "", + "format": payload.get("format") or "A3", + "landscape": bool(payload.get("landscape", True)), + "customWidth": payload.get("customWidth"), + "customHeight": payload.get("customHeight"), + }) + elif mode == "edit" and layout and layout.get("id"): + outer._set_page_size({ + "id": layout.get("id"), + "format": payload.get("format") or "A3", + "landscape": bool(payload.get("landscape", True)), + "customWidth": payload.get("customWidth"), + "customHeight": payload.get("customHeight"), + }) + + class _LayoutDialogBridge(panel_base.BaseBridge): + def __init__(self): + panel_base.BaseBridge.__init__(self, "layout_dialog") + def _on_ready(self): + self.send("LAYOUT_DIALOG_STATE", { + "mode": mode, + "layout": layout, + }) + def handle(self, data): + if not isinstance(data, dict): return + t = data.get("type", "") + pp = data.get("payload") or {} + if t == "READY": + self._on_ready() + elif t == "SAVE": + _apply(pp) + try: + f = bridge_holder.get("form") + if f is not None: f.Close() + except Exception: pass + elif t == "CANCEL": + try: + f = bridge_holder.get("form") + if f is not None: f.Close() + except Exception: pass + + b = _LayoutDialogBridge() + title = "Neues Layout" if mode == "new" else "Papierformat: {}".format( + (layout or {}).get("name", "")) + bridge_holder["form"] = panel_base.open_satellite_window( + "layout_dialog", + params={"mode": mode, "layout": layout}, + title=title, + size=(440, 380), + bridge=b) + def _bridge_factory(): b = LayoutsBridge() @@ -744,6 +808,6 @@ def _bridge_factory(): return b -panel_base.register_and_open("layouts", "LAYOUTS", PANEL_GUID_STR, +panel_base.register_and_open("layouts", "Layouts", PANEL_GUID_STR, _bridge_factory, icon_spec=("view_quilt", "#7a5fa8")) diff --git a/rhino/massstab.py b/rhino/massstab.py index 5b64105..bbd555d 100644 --- a/rhino/massstab.py +++ b/rhino/massstab.py @@ -1090,7 +1090,7 @@ def _bridge_factory(): # register_standalone_panel() aufrufen oder die Zeile darunter auskommentieren. def register_standalone_panel(): - panel_base.register_and_open("massstab", "MASSSTAB", PANEL_GUID_STR, _bridge_factory, + panel_base.register_and_open("massstab", "Massstab", PANEL_GUID_STR, _bridge_factory, icon_spec=("straighten", "#c87050")) # register_standalone_panel() # auskommentiert — Standard ist OBERLEISTE diff --git a/rhino/oberleiste.py b/rhino/oberleiste.py index c397735..c81ecad 100644 --- a/rhino/oberleiste.py +++ b/rhino/oberleiste.py @@ -20,6 +20,7 @@ if _HERE not in sys.path: import panel_base import massstab import overrides +import rhinopanel PANEL_GUID_STR = "7e1f6a5b-8e2f-4f3c-d5e6-f70819203b51" OVERRIDES_PANEL_GUID_STR = "8f2a7b6c-9f3a-4f4d-e6f7-08192a3c4d62" @@ -770,6 +771,7 @@ class OberleisteBridge(panel_base.BaseBridge): self._last_prompt = "" self._last_state_sig = None # Fingerprint des letzten Push — dedupe self._cached_overrides = None # (enabled, count) — invalidiert bei Toggle/Update + self._cached_combinations = None # (names, active) — invalidiert bei jeder Comb-Aenderung # Command-Liste einmalig laden (kann teuer sein -> cachen) try: self._all_commands = _list_all_command_names() @@ -920,6 +922,34 @@ class OberleisteBridge(panel_base.BaseBridge): except Exception as ex: print("[OBERLEISTE] open_as_window Overrides:", ex) + # --- Ebenenkombinationen ---------------------------------------- + elif t == "PICK_LAYER_COMBINATION": + doc = Rhino.RhinoDoc.ActiveDoc + name = (p.get("name") or "").strip() + if name: + rhinopanel.apply_layer_preset_by_name(doc, name) + else: + # "Eigene" — kein Apply, nur active_comb_name clearen + rhinopanel.set_active_comb_name(doc, None) + self._cached_combinations = None + self._send_state(force=True) + elif t == "SAVE_LAYER_COMBINATION": + doc = Rhino.RhinoDoc.ActiveDoc + name = (p.get("name") or "").strip() + if name: + rhinopanel.save_current_as_layer_preset(doc, name) + self._cached_combinations = None + self._send_state(force=True) + elif t == "DELETE_LAYER_COMBINATION": + doc = Rhino.RhinoDoc.ActiveDoc + rhinopanel.delete_layer_preset(doc, p.get("name") or "") + self._cached_combinations = None + self._send_state(force=True) + elif t == "OPEN_LAYER_COMBINATIONS_DIALOG": + try: rhinopanel.open_layer_combinations_window() + except Exception as ex: + print("[OBERLEISTE] open layer-combinations:", ex) + # --- Command-Line Integration ----------------------------------- elif t == "RUN_COMMAND": cmd = (p.get("cmd") or "").strip() @@ -1035,6 +1065,18 @@ class OberleisteBridge(panel_base.BaseBridge): info["overridesActivePreset"], _presets_tuple) = self._cached_overrides info["overridesPresets"] = list(_presets_tuple) + # Ebenenkombinationen — cached (Liste + active). Invalidiert bei + # PICK/SAVE/DELETE und durch Cross-Bridge-Notify aus rhinopanel.py. + if self._cached_combinations is None: + try: + names = tuple(rhinopanel.list_layer_preset_names(doc)) + active = rhinopanel.get_active_comb_name(doc) + self._cached_combinations = (names, active) + except Exception: + self._cached_combinations = ((), None) + _names_tuple, _active_comb = self._cached_combinations + info["layerCombinations"] = list(_names_tuple) + info["layerCombinationActive"] = _active_comb # Command-Line State prompt = _get_command_prompt() info["cmdPrompt"] = prompt @@ -1057,6 +1099,7 @@ class OberleisteBridge(panel_base.BaseBridge): info["overridesEnabled"], info["overridesCount"], info.get("overridesActivePreset"), tuple(info.get("overridesPresets") or ()), + _names_tuple, _active_comb, prompt, ) if not force and sig == self._last_state_sig: @@ -1197,5 +1240,5 @@ def _bridge_factory(): return b -panel_base.register_and_open("oberleiste", "OBERLEISTE", PANEL_GUID_STR, _bridge_factory, +panel_base.register_and_open("oberleiste", "Oberleiste", PANEL_GUID_STR, _bridge_factory, icon_spec=("menu", "#2f5d54")) diff --git a/rhino/overrides_panel.py b/rhino/overrides_panel.py index 31ce4eb..a151dc0 100644 --- a/rhino/overrides_panel.py +++ b/rhino/overrides_panel.py @@ -266,7 +266,7 @@ def open_as_window(): sc.sticky["overrides_bridge"] = b panel_base.open_satellite_window( "overrides", - title="OVERRIDES", + title="Overrides", size=(760, 580), bridge=b) diff --git a/rhino/rhinopanel.py b/rhino/rhinopanel.py index feff71b..06b4963 100644 --- a/rhino/rhinopanel.py +++ b/rhino/rhinopanel.py @@ -103,6 +103,243 @@ def _broadcast_state(doc=None, hatch_patterns=None): except Exception: pass +# --- Layer-Kombinationen: Modul-Level Helpers ------------------------------ +# Diese Helfer werden sowohl von EbenenBridge (Ebenen-Panel) als auch von +# OberleisteBridge (Top-Bar) und LayerCombinationsBridge (Satelliten-Editor) +# benutzt. doc.Strings ist die einzige Quelle der Wahrheit; nach jedem Write +# rufen die Caller _broadcast_state(doc) + invalidate cross-bridge caches. + +_PRESETS_KEY = "dossier_layer_presets" +_ACTIVE_COMB_KEY = "dossier_layer_active_comb" + + +def load_layer_presets(doc): + raw = doc.Strings.GetValue(_PRESETS_KEY) + if not raw: return [] + try: + data = json.loads(raw) + return data if isinstance(data, list) else [] + except Exception: + return [] + + +def store_layer_presets(doc, presets): + try: + doc.Strings.SetString(_PRESETS_KEY, + json.dumps(presets, ensure_ascii=False)) + except Exception as ex: + print("[EBENEN] store_layer_presets:", ex) + + +def get_active_comb_name(doc): + try: + v = doc.Strings.GetValue(_ACTIVE_COMB_KEY) + return v if v else None + except Exception: + return None + + +def set_active_comb_name(doc, name): + try: + doc.Strings.SetString(_ACTIVE_COMB_KEY, name or "") + except Exception as ex: + print("[EBENEN] set_active_comb_name:", ex) + + +def list_layer_preset_names(doc): + return [p.get("name") for p in load_layer_presets(doc) + if isinstance(p, dict) and p.get("name")] + + +def _notify_oberleiste_combs(): + """Cache der Oberleiste invalidieren + force-send. Wird gerufen wenn + die Combinations-Liste oder activeCombName sich aendert.""" + try: + b = sc.sticky.get("oberleiste_bridge") + if b is not None: + b._cached_combinations = None + b._send_state(force=True) + except Exception as ex: + print("[EBENEN] notify oberleiste combs:", ex) + + +def _notify_layer_combinations_editor(): + """Satelliten-Fenster (Editor) informieren falls offen — Layer-/Preset- + Liste hat sich geaendert.""" + try: + b = sc.sticky.get("layer_combinations_bridge") + if b is not None: b._send_state() + except Exception as ex: + print("[EBENEN] notify layer-combinations editor:", ex) + + +def apply_layer_preset_by_name(doc, name): + """Laedt Preset `name` und wendet es an. Setzt active_comb_name. + Liefert True wenn erfolgreich.""" + if not name: return False + presets = load_layer_presets(doc) + preset = next((p for p in presets if p.get("name") == name), None) + if preset is None: + print("[EBENEN] apply_layer_preset_by_name: '{}' nicht gefunden".format(name)) + return False + payload = { + "layers": preset.get("layers") or [], + "dossierEbenen": preset.get("dossierEbenen"), + "dossierZeichnungsebenen": preset.get("dossierZeichnungsebenen"), + } + # Routing: wenn die EbenenBridge existiert, delegiere — die hat den + # vollen Eye-State-Pfad inkl. STATE_SYNC + Redraw. Sonst inline applien. + eb = sc.sticky.get("ebenen_bridge_ref") + if eb is not None: + try: + eb._apply_combination(payload) + except Exception as ex: + print("[EBENEN] apply via bridge:", ex) + return False + else: + # Fallback: direkt doc.Strings + doc.Layer setzen (kein Bridge offen) + _apply_layer_preset_inline(doc, payload) + set_active_comb_name(doc, name) + _notify_oberleiste_combs() + return True + + +def _apply_layer_preset_inline(doc, payload): + """Fallback wenn keine EbenenBridge offen ist — minimaler Layer-State- + Pfad. Setzt doc.Strings + doc.Layer.IsVisible direkt.""" + pe = payload.get("dossierEbenen") + pz = payload.get("dossierZeichnungsebenen") + try: + e_raw = doc.Strings.GetValue("dossier_ebenen") or "[]" + z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen") or "[]" + e_list = json.loads(e_raw) or [] + z_list = json.loads(z_raw) or [] + if isinstance(pe, list): + by_code = {x.get("code"): x for x in pe if isinstance(x, dict) and x.get("code")} + for e in e_list: + if not isinstance(e, dict): continue + s = by_code.get(e.get("code")) + if s is None: continue + e["visible"] = bool(s.get("visible", True)) + e["locked"] = bool(s.get("locked", False)) + if isinstance(pz, list): + by_id = {x.get("id"): x for x in pz if isinstance(x, dict) and x.get("id")} + for z in z_list: + if not isinstance(z, dict): continue + s = by_id.get(z.get("id")) + if s is None: continue + z["visible"] = bool(s.get("visible", True)) + doc.Strings.SetString("dossier_ebenen", json.dumps(e_list, ensure_ascii=False)) + doc.Strings.SetString("dossier_zeichnungsebenen", json.dumps(z_list, ensure_ascii=False)) + except Exception as ex: + print("[EBENEN] inline preset-apply (eye-state):", ex) + # Layer-ID-Pfad als Sekundaer (AUSSCHNITTE-Kompat) + layer_states = payload.get("layers") or [] + if layer_states: + by_id = {} + try: + for layer in doc.Layers: + if not layer.IsDeleted: by_id[str(layer.Id)] = layer + except Exception: pass + _set_processing(True) + try: + for ls in layer_states: + layer = by_id.get(ls.get("id")) + if layer is None: continue + try: + want_vis = bool(ls.get("visible", True)) + want_lck = bool(ls.get("locked", False)) + if layer.IsVisible != want_vis: layer.IsVisible = want_vis + if layer.IsLocked != want_lck: layer.IsLocked = want_lck + except Exception: pass + finally: + _set_processing(False) + try: doc.Views.Redraw() + except Exception: pass + _broadcast_state(doc) + + +def save_current_as_layer_preset(doc, name): + """Speichert die aktuellen Eye-States als Preset. Setzt active_comb_name.""" + name = (name or "").strip() + if not name: return False + # 1) doc.Layer state (Kompat mit AUSSCHNITTE) + layers = [] + try: + for layer in doc.Layers: + if layer is None or layer.IsDeleted: continue + layers.append({ + "id": str(layer.Id), + "visible": bool(layer.IsVisible), + "locked": bool(layer.IsLocked), + }) + except Exception as ex: + print("[EBENEN] save_current_as_layer_preset enum:", ex) + # 2) Eye-States aus dossier_ebenen / dossier_zeichnungsebenen + pe_state, pz_state = [], [] + try: + e_raw = doc.Strings.GetValue("dossier_ebenen") + if e_raw: + for e in (json.loads(e_raw) or []): + if isinstance(e, dict) and e.get("code"): + pe_state.append({ + "code": e["code"], + "visible": bool(e.get("visible", True)), + "locked": bool(e.get("locked", False)), + }) + z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen") + if z_raw: + for z in (json.loads(z_raw) or []): + if isinstance(z, dict) and z.get("id"): + pz_state.append({ + "id": z["id"], + "visible": bool(z.get("visible", True)), + }) + except Exception as ex: + print("[EBENEN] save_current_as_layer_preset eye-states:", ex) + presets = load_layer_presets(doc) + new_data = { + "name": name, + "layers": layers, + "dossierEbenen": pe_state, + "dossierZeichnungsebenen": pz_state, + } + existing = next((p for p in presets if p.get("name") == name), None) + if existing is not None: + existing.update(new_data) + else: + presets.append(new_data) + store_layer_presets(doc, presets) + set_active_comb_name(doc, name) + _notify_oberleiste_combs() + _notify_layer_combinations_editor() + print("[EBENEN] '{}' gespeichert: {} Layer + {} Ebenen Eye-State".format( + name, len(layers), len(pe_state))) + return True + + +def delete_layer_preset(doc, name): + name = (name or "").strip() + if not name: return False + presets = [p for p in load_layer_presets(doc) if p.get("name") != name] + store_layer_presets(doc, presets) + if get_active_comb_name(doc) == name: + set_active_comb_name(doc, None) + _notify_oberleiste_combs() + _notify_layer_combinations_editor() + print("[EBENEN] Kombination '{}' geloescht".format(name)) + return True + + +def clear_active_comb_name(doc): + """Wird vom EbenenBridge SET_VISIBILITY / APPLY-Pfad gerufen wenn der + User per Hand etwas am Layer-State aendert — dann passt das Preset nicht + mehr und wir markieren 'Eigene'.""" + if get_active_comb_name(doc): + set_active_comb_name(doc, None) + _notify_oberleiste_combs() + + class EbenenBridge(panel_base.BaseBridge): """Gemeinsame Bridge-Klasse fuer beide Panels (Ebenen + Zeichnungsebenen). Mode bestimmt nur welches WebView die Bridge bedient + welcher sticky-Slot @@ -266,40 +503,80 @@ class EbenenBridge(panel_base.BaseBridge): on_save=on_save) def _open_ebenen_settings(self, ebene, hatch_patterns): - """Oeffnet ein echtes Rhino-Fenster mit dem EbenenSettingsDialog.""" + """Oeffnet ein echtes Rhino-Fenster mit dem EbenenSettingsDialog. + Mit Dropdown zum Wechsel zwischen Ebenen; jeder Switch persistiert + die aktuelle Ebene live (SAVE_KEEP), Schliess-/Übernehmen-Knopf + persistiert + schliesst (SAVE).""" if not isinstance(ebene, dict) or not ebene.get("code"): print("[EBENEN] open_ebenen_settings: kein Ebene-Payload") return - old_code = ebene["code"] - def on_save(updated): - doc = Rhino.RhinoDoc.ActiveDoc - if doc is None: return - e_raw = doc.Strings.GetValue("dossier_ebenen") - if not e_raw: - print("[EBENEN] save_ebene: kein e-Store"); return - try: - e_list = json.loads(e_raw) - except Exception as ex: - print("[EBENEN] save_ebene JSON:", ex); return - replaced = False - for i, e in enumerate(e_list): - if isinstance(e, dict) and e.get("code") == old_code: - e_list[i] = updated - replaced = True - break - if not replaced: - print("[EBENEN] save_ebene: code {} nicht gefunden".format(old_code)) - return - z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen") - try: z_list = json.loads(z_raw) if z_raw else [] - except Exception: z_list = [] - self._apply(z_list, e_list, save_z=False, save_e=True) - panel_base.open_satellite_window( + bridge_holder = {"form": None} + apply_self = self + class _EbenenSettingsBridge(panel_base.BaseBridge): + def __init__(self): + panel_base.BaseBridge.__init__(self, "ebenen_settings") + def _on_ready(self): + self._send_state() + def _send_state(self): + doc = Rhino.RhinoDoc.ActiveDoc + e_raw = doc.Strings.GetValue("dossier_ebenen") if doc else None + try: e_list = json.loads(e_raw) if e_raw else [] + except Exception: e_list = [] + self.send("EBENEN_SETTINGS_STATE", { + "ebenen": e_list, + "hatchPatterns": hatch_patterns, + }) + def handle(self, data): + if not isinstance(data, dict): return + t = data.get("type", "") + p = data.get("payload") or {} + if t == "READY": + self._on_ready() + elif t == "SAVE_KEEP": + self._persist(p) + elif t == "SAVE": + self._persist(p) + try: + f = bridge_holder.get("form") + if f is not None: f.Close() + except Exception: pass + elif t == "CANCEL": + try: + f = bridge_holder.get("form") + if f is not None: f.Close() + except Exception: pass + def _persist(self, p): + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: return + updated = p.get("ebene") or {} + orig_code = p.get("originalCode") or updated.get("code") + if not (isinstance(updated, dict) and updated.get("code")): return + e_raw = doc.Strings.GetValue("dossier_ebenen") + if not e_raw: return + try: e_list = json.loads(e_raw) + except Exception as ex: + print("[EBENEN] save_ebene JSON:", ex); return + replaced = False + for i, e in enumerate(e_list): + if isinstance(e, dict) and e.get("code") == orig_code: + e_list[i] = updated + replaced = True + break + if not replaced: + print("[EBENEN] save_ebene: code {} nicht gefunden".format(orig_code)) + return + z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen") + try: z_list = json.loads(z_raw) if z_raw else [] + except Exception: z_list = [] + apply_self._apply(z_list, e_list, save_z=False, save_e=True) + self._send_state() + b = _EbenenSettingsBridge() + bridge_holder["form"] = panel_base.open_satellite_window( "ebenen_settings", - params={"ebene": ebene, "hatchPatterns": hatch_patterns}, - title="Ebene: {}_{}".format(ebene.get("code", ""), ebene.get("name", "")), + params={"currentCode": ebene["code"], "hatchPatterns": hatch_patterns}, + title="Ebenen-Einstellungen", size=(420, 600), - on_save=on_save) + bridge=b) def _open_geschoss_dialog(self, zeichnungsebenen): """Oeffnet den vollen GeschossDialog (Mehrfach-Editor) als @@ -416,6 +693,9 @@ class EbenenBridge(panel_base.BaseBridge): self._update_clipping() print("[EBENEN] _apply: send APPLY_OK") self.send("APPLY_OK", {}) + # Strukturelle Aenderung (neue/umbenannte/geloeschte Ebene) → aktives + # Preset passt nicht mehr exakt. + clear_active_comb_name(doc) # Anderes Panel (Zeichnungsebenen/Ebenen) ueber den neuen State # informieren — sonst hinkt es hinter der DOM-Persistenz her. _broadcast_state(doc) @@ -476,6 +756,7 @@ class EbenenBridge(panel_base.BaseBridge): s = z_state.get(z.get("id")) if s is not None: m["visible"] = s.get("visible", True) + m["locked"] = s.get("locked", False) merged_z.append(m) merged_e = [] for e in e_full: @@ -486,11 +767,33 @@ class EbenenBridge(panel_base.BaseBridge): m["visible"] = s.get("visible", True) m["locked"] = s.get("locked", False) merged_e.append(m) + # Detect whether the merge actually changed any visible/locked values. + # Wenn nicht: das ist nur der Echo-Roundtrip eines apply_layer_preset + # (React-State == doc.Strings → kein User-Click) und wir wollen das + # aktive Preset NICHT clearen. + def _vis_lock_changed(old, new): + old_by = {x.get("id") or x.get("code"): x for x in old if isinstance(x, dict)} + for nx in new: + if not isinstance(nx, dict): continue + key = nx.get("id") or nx.get("code") + if key is None: continue + ox = old_by.get(key) + if ox is None: continue + if (ox.get("visible", True) != nx.get("visible", True) + or ox.get("locked", False) != nx.get("locked", False)): + return True + return False + any_changed = (_vis_lock_changed(z_full, merged_z) + or _vis_lock_changed(e_full, merged_e)) if has_new_structural: print("[EBENEN] _apply_visibility: structural change pending → skip save (waiting for APPLY)") else: doc.Strings.SetString("dossier_zeichnungsebenen", json.dumps(merged_z, ensure_ascii=False)) doc.Strings.SetString("dossier_ebenen", json.dumps(merged_e, ensure_ascii=False)) + # User hat per Hand Eye/Lock geaendert → aktives Preset passt nicht + # mehr, auf "Eigene" zuruecksetzen. + if any_changed: + clear_active_comb_name(doc) # zMode + eMode persistieren, damit bei Split-Send (nur eine # Panel-Slice) der andere Mode aus dem Doc-Storage faellt anstatt # auf den Default zu rutschen. @@ -504,6 +807,24 @@ class EbenenBridge(panel_base.BaseBridge): if not isinstance(active_z, dict): active_z = {} active_z_id = active_z.get("id") or doc.Strings.GetValue("dossier_active_id") active_code = p.get("activeCode") or doc.Strings.GetValue("dossier_active_code") + # Dedupe: identisches SET_VISIBILITY (z.B. STATE_SYNC-Echo nach + # Preset-Apply) loopt sonst unnoetig durch alle ~100 Doc-Layer. + # Signatur aus active-id/code + mode + vis/lock-Liste. + def _sig(zlist, elist): + zs = tuple((z.get("id"), + bool(z.get("visible", True)), + bool(z.get("locked", False))) + for z in zlist if isinstance(z, dict)) + es = tuple((e.get("code"), + bool(e.get("visible", True)), + bool(e.get("locked", False))) + for e in elist if isinstance(e, dict)) + return (active_z_id, active_code, z_mode, e_mode, zs, es) + cur_sig = _sig(merged_z, merged_e) + if sc.sticky.get("_vis_last_sig") == cur_sig and not any_changed: + # Nichts Neues — Rhino-Layer-State ist schon korrekt. + return + sc.sticky["_vis_last_sig"] = cur_sig layer_builder.apply_visibility( doc, merged_z, merged_e, active_z_id, active_code, z_mode, e_mode) # Cross-Panel-Sync NUR wenn wir nicht in einem structural-pending @@ -515,17 +836,29 @@ class EbenenBridge(panel_base.BaseBridge): def _set_active_zeichnungsebene(self, z): doc = Rhino.RhinoDoc.ActiveDoc z_id = z.get("id", "") + # Vorigen Stand merken um redundante teure Operationen zu sparen + prev_active_id = doc.Strings.GetValue("dossier_active_id") or "" doc.Strings.SetString("dossier_active_id", z_id) + # Aktiven Sublayer auf die GLEICHE Ebene unter dem neuen Geschoss + # umschalten — wenn User auf "20 Wände" steht und das Geschoss + # wechselt, soll Rhino's aktiver Layer "1OG::20_Wände" werden statt + # auf der vorigen Geschoss-Ebene haengen zu bleiben. + self._ensure_active_sublayer() # Cross-Panel-Sync: Ebenen-Panel muss aktive Geschoss-Auswahl # mitbekommen falls es im "active"-Filter-Mode laeuft. _broadcast_state(doc) - # Clipping ggf. mitziehen - self._update_clipping(active_z=z) + # Clipping nur antasten wenn entweder das alte oder das neue Geschoss + # eine Clipping-Plane hatte — sonst sparen wir Plane-Delete + Build + # + View-Redraw bei jedem Geschoss-Wechsel ganz. + if self._needs_clipping_update(doc, prev_active_id, z): + self._update_clipping(active_z=z) # Elemente-Panel informieren: das aktive Geschoss hat gewechselt, # neue Elemente sollen jetzt automatisch dort verlinkt werden. + # Wichtig: NICHT _send_state() rufen (re-enumeriert alle Elemente, + # 200+ in echten Projekten = spuerbar). Schlanker Partial-Push. try: eb = sc.sticky.get("elemente_bridge") - if eb is not None: eb._send_state() + if eb is not None: eb._notify_active_geschoss() except Exception: pass if not (z.get("isGeschoss") and z.get("okff") is not None): return @@ -546,12 +879,33 @@ class EbenenBridge(panel_base.BaseBridge): plane.XAxis, plane.YAxis, ) vp.SetConstructionPlane(new_plane) - view.Redraw() updated += 1 except Exception as ex: print("[EBENEN] CPlane fehler ({}): {}".format(vp.Name if vp else "?", ex)) + # KEIN doc.Views.Redraw() hier — die folgende SET_VISIBILITY-Round- + # trip (30 ms debounce in React) feuert ohnehin layer_builder + # .apply_visibility() das am Ende selbst redrawt. Sparen wir uns + # einen doppelten Full-Repaint pro Geschoss-Klick. print("[EBENEN] CPlane Z={} auf {} Top-Style View(s) gesetzt".format(okff, updated)) + def _needs_clipping_update(self, doc, prev_active_id, new_z): + """Liefert True wenn entweder das alte oder das neue Geschoss + hasClipping=True hat. Sonst kann _update_clipping skipped werden + (Plane existiert nicht und muss auch nicht neu gebaut werden).""" + new_has = bool(new_z.get("hasClipping")) + if new_has: + return True + if not prev_active_id: + return False + try: + z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen") + if not z_raw: return False + for z in (json.loads(z_raw) or []): + if isinstance(z, dict) and z.get("id") == prev_active_id: + return bool(z.get("hasClipping")) + except Exception: pass + return False + def _toggle_clipping_for_active(self, enabled): """Setzt hasClipping fuer das aktuell aktive Geschoss + persistiert in doc.Strings + triggert plane-update. Wird vom React-Toggle 'Clipping @@ -704,20 +1058,10 @@ class EbenenBridge(panel_base.BaseBridge): _PRESETS_KEY = "dossier_layer_presets" def _load_presets(self, doc): - raw = doc.Strings.GetValue(self._PRESETS_KEY) - if not raw: return [] - try: - data = json.loads(raw) - return data if isinstance(data, list) else [] - except Exception: - return [] + return load_layer_presets(doc) def _store_presets(self, doc, presets): - try: - doc.Strings.SetString(self._PRESETS_KEY, - json.dumps(presets, ensure_ascii=False)) - except Exception as ex: - print("[EBENEN] _store_presets:", ex) + store_layer_presets(doc, presets) def _send_combination(self): """Schickt aktuelles Layer-State + alle Presets ans Frontend.""" @@ -917,7 +1261,7 @@ class EbenenBridge(panel_base.BaseBridge): name = (name or "").strip() if not name: return doc = Rhino.RhinoDoc.ActiveDoc - presets = self._load_presets(doc) + presets = load_layer_presets(doc) clean = [] for ls in (layers or []): lid = ls.get("id") @@ -932,7 +1276,9 @@ class EbenenBridge(panel_base.BaseBridge): existing["layers"] = clean else: presets.append({"name": name, "layers": clean}) - self._store_presets(doc, presets) + store_layer_presets(doc, presets) + _notify_oberleiste_combs() + _notify_layer_combinations_editor() print("[EBENEN] Kombination '{}' gespeichert ({} Layer)".format(name, len(clean))) def _save_current_as_preset(self, name): @@ -944,67 +1290,118 @@ class EbenenBridge(panel_base.BaseBridge): layers (doc.Layer-Liste) wird parallel mitgespeichert fuer Kompat mit AUSSCHNITTE (das vom doc.Layer-State liest).""" - name = (name or "").strip() - if not name: return + save_current_as_layer_preset(Rhino.RhinoDoc.ActiveDoc, name) + + def _delete_preset(self, name): + delete_layer_preset(Rhino.RhinoDoc.ActiveDoc, name) + + +class LayerCombinationsBridge(panel_base.BaseBridge): + """Bridge fuer das Satelliten-Fenster mit dem grossen Ebenenkombinationen- + Editor (AusschnittLayerDialog). Wird vom Oberleiste-Bridge geoeffnet bei + OPEN_LAYER_COMBINATIONS_DIALOG. State wird beim READY-Event geschickt und + bei jeder Aenderung re-emittet.""" + def __init__(self): + panel_base.BaseBridge.__init__(self, "layer_combinations") + + def _on_ready(self): + self._send_state() + + def _send_state(self): doc = Rhino.RhinoDoc.ActiveDoc - # 1) doc.Layer state (Kompat mit AUSSCHNITTE) - layers = [] + if doc is None: return + layers_out = [] try: for layer in doc.Layers: if layer is None or layer.IsDeleted: continue - layers.append({ - "id": str(layer.Id), - "visible": bool(layer.IsVisible), - "locked": bool(layer.IsLocked), + lid = str(layer.Id) + try: fp = layer.FullPath or layer.Name + except Exception: fp = layer.Name or "" + try: col = "#%02x%02x%02x" % (layer.Color.R, layer.Color.G, layer.Color.B) + except Exception: col = "#888888" + layers_out.append({ + "id": lid, + "name": layer.Name, + "fullPath": fp, + "color": col, + "visible": bool(layer.IsVisible), + "locked": bool(layer.IsLocked), }) + layers_out.sort(key=lambda x: x["fullPath"]) except Exception as ex: - print("[EBENEN] _save_current_as_preset enum:", ex) - # 2) Eye-States aus dossier_ebenen / dossier_zeichnungsebenen - pe_state = [] - pz_state = [] - try: - e_raw = doc.Strings.GetValue("dossier_ebenen") - if e_raw: - for e in (json.loads(e_raw) or []): - if isinstance(e, dict) and e.get("code"): - pe_state.append({ - "code": e["code"], - "visible": bool(e.get("visible", True)), - "locked": bool(e.get("locked", False)), - }) - z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen") - if z_raw: - for z in (json.loads(z_raw) or []): - if isinstance(z, dict) and z.get("id"): - pz_state.append({ - "id": z["id"], - "visible": bool(z.get("visible", True)), - }) - except Exception as ex: - print("[EBENEN] _save_current_as_preset eye-states:", ex) - presets = self._load_presets(doc) - new_data = { - "name": name, - "layers": layers, - "dossierEbenen": pe_state, - "dossierZeichnungsebenen": pz_state, - } - existing = next((p for p in presets if p.get("name") == name), None) - if existing is not None: - existing.update(new_data) - else: - presets.append(new_data) - self._store_presets(doc, presets) - print("[EBENEN] '{}' gespeichert: {} Layer + {} Ebenen Eye-State".format( - name, len(layers), len(pe_state))) + print("[LAYER-COMB] enum:", ex) + self.send("LAYER_COMBINATIONS_STATE", { + "layers": layers_out, + "presets": load_layer_presets(doc), + }) - def _delete_preset(self, name): - name = (name or "").strip() - if not name: return + def handle(self, data): + if not isinstance(data, dict): return + t = data.get("type", "") + p = data.get("payload") or {} + if not isinstance(p, dict): p = {} doc = Rhino.RhinoDoc.ActiveDoc - presets = [p for p in self._load_presets(doc) if p.get("name") != name] - self._store_presets(doc, presets) - print("[EBENEN] Kombination '{}' geloescht".format(name)) + + if t == "READY" or t == "REQUEST_STATE": + self._on_ready() + elif t == "APPLY_COMBINATION": + # Editor wendet eine Layer-State-Liste an (ohne Preset-Name — + # also "Eigene"). Wir delegieren an die Ebenen-Bridge wenn offen, + # sonst inline. activeCombName auf None setzen. + eb = sc.sticky.get("ebenen_bridge_ref") + if eb is not None: + try: eb._apply_combination(p) + except Exception as ex: + print("[LAYER-COMB] apply via bridge:", ex) + else: + _apply_layer_preset_inline(doc, p) + set_active_comb_name(doc, None) + _notify_oberleiste_combs() + self._send_state() + elif t == "SAVE_PRESET": + # Editor speichert die im Dialog kuratierte Liste unter Namen. + name = (p.get("name") or "").strip() + layers = p.get("layers") or [] + if name: + presets = load_layer_presets(doc) + clean = [] + for ls in layers: + lid = ls.get("id") + if not lid: continue + clean.append({ + "id": lid, + "visible": bool(ls.get("visible", True)), + "locked": bool(ls.get("locked", False)), + }) + existing = next((pp for pp in presets if pp.get("name") == name), None) + if existing is not None: + existing["layers"] = clean + else: + presets.append({"name": name, "layers": clean}) + store_layer_presets(doc, presets) + _notify_oberleiste_combs() + self._send_state() + elif t == "DELETE_PRESET": + delete_layer_preset(doc, p.get("name") or "") + self._send_state() + elif t == "CANCEL": + try: + form = sc.sticky.get("layer_combinations_form") + if form is not None: form.Close() + except Exception: pass + + +def open_layer_combinations_window(): + """Oeffnet den Editor als echtes Rhino-Fenster (Eto.Form + WebView). + Wird vom Oberleiste-Bridge bei OPEN_LAYER_COMBINATIONS_DIALOG gerufen.""" + b = LayerCombinationsBridge() + sc.sticky["layer_combinations_bridge"] = b + form = panel_base.open_satellite_window( + "layer_combinations", + title="Ebenenkombinationen", + size=(540, 640), + bridge=b) + sc.sticky["layer_combinations_form"] = form def _ebenen_bridge_factory(): @@ -1087,8 +1484,8 @@ def _install_layer_listener(bridge): print("[EBENEN] Layer-Listener aktiv") -panel_base.register_and_open("ebenen", "EBENEN", PANEL_GUID_STR, _ebenen_bridge_factory, +panel_base.register_and_open("ebenen", "Ebenen", PANEL_GUID_STR, _ebenen_bridge_factory, icon_spec=("layers", "#3a6fa8")) -panel_base.register_and_open("zeichnungsebenen", "ZEICHNUNGSEBENEN", PANEL_GUID_STR_Z, +panel_base.register_and_open("zeichnungsebenen", "Zeichnungsebenen", PANEL_GUID_STR_Z, _zeichnungsebenen_bridge_factory, icon_spec=("levels", "#3a6fa8")) diff --git a/rhino/werkzeuge.py b/rhino/werkzeuge.py index 4bbe901..1d4fd3e 100644 --- a/rhino/werkzeuge.py +++ b/rhino/werkzeuge.py @@ -54,5 +54,5 @@ def _bridge_factory(): return WerkzeugeBridge() -panel_base.register_and_open("werkzeuge", "WERKZEUGE", PANEL_GUID_STR, _bridge_factory, +panel_base.register_and_open("werkzeuge", "Werkzeuge", PANEL_GUID_STR, _bridge_factory, icon_spec=("build", "#3a6fa8")) diff --git a/src/App.jsx b/src/App.jsx index 5edd95d..4bbd844 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,33 +1,29 @@ -import { useState, useEffect, useMemo, useRef } from 'react' +import { useState, useEffect, useMemo } from 'react' import EbenenManager from './components/EbenenManager' -import AusschnittLayerDialog from './components/AusschnittLayerDialog' import { applyAll, setActiveEbene, onMessage, notifyReady, applyVisibility, - getCombination, applyCombination, - saveCurrentAsCombination, deleteCombinationPreset, - saveCombinationPreset, } from './lib/rhinoBridge' const INITIAL_EBENEN = [ - { code: '00', name: 'RASTER', color: '#484850', lw: 0.13, visible: true, locked: false }, - { code: '01', name: 'VERMESSUNG', color: '#707078', lw: 0.18, visible: true, locked: false }, - { code: '10', name: 'SITUATION', color: '#909090', lw: 0.18, visible: true, locked: false }, - { code: '11', name: 'STRASSE', color: '#a89070', lw: 0.18, visible: true, locked: false }, - { code: '12', name: 'GEBÄUDE', color: '#888888', lw: 0.25, visible: true, locked: false }, - { code: '13', name: 'BÄUME', color: '#50a050', lw: 0.13, visible: true, locked: false }, - { code: '14', name: 'HÖHENLINIEN', color: '#909050', lw: 0.18, visible: true, locked: false }, - { code: '20', name: 'WÄNDE', color: '#0a0a0a', lw: 0.50, visible: true, locked: false }, - { code: '21', name: 'TÜREN_FENSTER', color: '#5080c8', lw: 0.25, visible: true, locked: false }, - { code: '22', name: 'MÖBEL', color: '#909090', lw: 0.13, visible: true, locked: false }, - { code: '25', name: 'STÜTZEN', color: '#c87050', lw: 0.50, visible: true, locked: false }, - { code: '30', name: 'DECKEN', color: '#605850', lw: 0.35, visible: true, locked: false }, - { code: '31', name: 'DÄCHER', color: '#7a4a3a', lw: 0.35, visible: true, locked: false }, - { code: '35', name: 'TRÄGER', color: '#a87858', lw: 0.50, visible: true, locked: false }, - { code: '50', name: 'TEXT', color: '#d0d0d0', lw: 0.13, visible: true, locked: false }, - { code: '60', name: 'PLANGRAFIK', color: '#c0a040', lw: 0.13, visible: true, locked: false }, - { code: '90', name: 'REFERENZEN', color: '#585860', lw: 0.13, visible: true, locked: false }, - { code: '99', name: 'KONSTRUKTION', color: '#404048', lw: 0.13, visible: true, locked: false }, + { code: '00', name: 'Raster', color: '#484850', lw: 0.13, visible: true, locked: false }, + { code: '01', name: 'Vermessung', color: '#707078', lw: 0.18, visible: true, locked: false }, + { code: '10', name: 'Situation', color: '#909090', lw: 0.18, visible: true, locked: false }, + { code: '11', name: 'Strasse', color: '#a89070', lw: 0.18, visible: true, locked: false }, + { code: '12', name: 'Gebäude', color: '#888888', lw: 0.25, visible: true, locked: false }, + { code: '13', name: 'Bäume', color: '#50a050', lw: 0.13, visible: true, locked: false }, + { code: '14', name: 'Höhenlinien', color: '#909050', lw: 0.18, visible: true, locked: false }, + { code: '20', name: 'Wände', color: '#0a0a0a', lw: 0.50, visible: true, locked: false }, + { code: '21', name: 'Türen_Fenster', color: '#5080c8', lw: 0.25, visible: true, locked: false }, + { code: '22', name: 'Möbel', color: '#909090', lw: 0.13, visible: true, locked: false }, + { code: '25', name: 'Stützen', color: '#c87050', lw: 0.50, visible: true, locked: false }, + { code: '30', name: 'Decken', color: '#605850', lw: 0.35, visible: true, locked: false }, + { code: '31', name: 'Dächer', color: '#7a4a3a', lw: 0.35, visible: true, locked: false }, + { code: '35', name: 'Träger', color: '#a87858', lw: 0.50, visible: true, locked: false }, + { code: '50', name: 'Text', color: '#d0d0d0', lw: 0.13, visible: true, locked: false }, + { code: '60', name: 'Plangrafik', color: '#c0a040', lw: 0.13, visible: true, locked: false }, + { code: '90', name: 'Referenzen', color: '#585860', lw: 0.13, visible: true, locked: false }, + { code: '99', name: 'Konstruktion', color: '#404048', lw: 0.13, visible: true, locked: false }, ] export default function App() { @@ -36,28 +32,12 @@ 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']) - // Ebenenkombinationen (geteilter Store mit Ausschnitten) - const [combinations, setCombinations] = useState([]) // Liste { name, layers } - const [activeCombName, setActiveCombName] = useState(null) // null = "Eigene" - // Dialog fuer "alle bearbeiten" (Pencil-Button) - const [combDialog, setCombDialog] = useState(null) // { layers, presets } oder null - const wantCombDialogRef = useRef(false) useEffect(() => { onMessage('STATE_SYNC', ({ ebenen: e, hatchPatterns: hp }) => { if (e) { setEbenen(e); setAppliedE(e) } if (Array.isArray(hp) && hp.length > 0) setHatchPatterns(hp) }) - onMessage('COMBINATION_DATA', ({ layers, presets }) => { - setCombinations(presets || []) - if (wantCombDialogRef.current) { - wantCombDialogRef.current = false - setCombDialog({ layers: layers || [], presets: presets || [] }) - } else if (combDialog) { - // Dialog ist offen — Layer-Liste live aktualisieren (z.B. nach Preset-Save) - setCombDialog(d => d ? { ...d, layers: layers || d.layers, presets: presets || [] } : d) - } - }) onMessage('FIRST_RUN', ({ defaultEbenen } = {}) => { // Wenn der Dossier-Launcher ein eigenes Schema definiert hat, nutzen wir // das statt der hardcoded INITIAL_EBENEN. @@ -72,8 +52,6 @@ export default function App() { if (activeCode) setActiveEbene(activeCode) }) notifyReady() - // Initial Liste der Kombinationen holen - setTimeout(() => getCombination(), 200) // Native Browser-Context-Menu global unterdruecken const blockContext = (ev) => ev.preventDefault() @@ -122,41 +100,6 @@ export default function App() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [structureKey, appliedStructureKey]) - // --- Ebenen-Kombinationen ---------------------------------------------- - const handlePickCombination = (name) => { - if (!name) { setActiveCombName(null); return } - const preset = combinations.find(p => p.name === name) - if (!preset) return - applyCombination({ - layers: preset.layers || [], - dossierEbenen: preset.dossierEbenen, - dossierZeichnungsebenen: preset.dossierZeichnungsebenen, - }) - setActiveCombName(name) - } - const handleSaveCurrentCombination = () => { - const suggested = activeCombName || `Kombi ${combinations.length + 1}` - const name = (window.prompt('Name für Ebenenkombination:', suggested) || '').trim() - if (!name) return - if (combinations.some(p => p.name === name) && - !window.confirm(`"${name}" überschreiben?`)) return - saveCurrentAsCombination(name) - setActiveCombName(name) - } - const handleDeleteCombination = (name) => { - if (!name) return - if (!window.confirm(`Ebenenkombination "${name}" löschen?`)) return - deleteCombinationPreset(name) - if (activeCombName === name) setActiveCombName(null) - } - const handleOpenCombDialog = () => { - wantCombDialogRef.current = true - getCombination() - } - const handleUserVisibilityChange = () => { - if (activeCombName !== null) setActiveCombName(null) - } - return (
- - {combDialog && ( -