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 && ( - setCombDialog(null)} - onSave={(layers) => { - applyCombination(layers) - setActiveCombName(null) - setCombDialog(null) - }} - onSavePreset={(name, layers) => { - saveCombinationPreset(name, layers) - setCombDialog(d => d ? { - ...d, - presets: [...d.presets.filter(p => p.name !== name), { name, layers }], - } : d) - }} - onDeletePreset={(name) => { - deleteCombinationPreset(name) - setCombDialog(d => d ? { - ...d, - presets: d.presets.filter(p => p.name !== name), - } : d) - if (activeCombName === name) setActiveCombName(null) - }} - /> - )} ) } diff --git a/src/AusschnittSettingsApp.jsx b/src/AusschnittSettingsApp.jsx new file mode 100644 index 0000000..f9b4c95 --- /dev/null +++ b/src/AusschnittSettingsApp.jsx @@ -0,0 +1,187 @@ +import { useEffect, useState } from 'react' +import { onMessage, notifyReady } from './lib/rhinoBridge' + +function send(type, payload = {}) { + if (!window.RHINO_MODE) { console.log('[AusschnittSettings] →', type, payload); return } + document.title = 'RHINOMSG::' + JSON.stringify({ type, payload }) +} + +function Field({ label, hint, children }) { + return ( +
+ + {label} + +
{children}
+ {hint && ( + {hint} + )} +
+ ) +} + +function SectionLabel({ children }) { + return ( +
{children}
+ ) +} + +export default function AusschnittSettingsApp() { + const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {} + const [snap, setSnap] = useState(initial.snap || {}) + const [displayModes, setDisplayModes] = useState(initial.displayModes || []) + const [overridesPresets, setOverridesPresets] = useState(initial.overridesPresets || []) + const [layerKombis, setLayerKombis] = useState(initial.layerKombis || []) + + useEffect(() => { + onMessage('AUSSCHNITT_SETTINGS_STATE', (p) => { + if (p.snap) setSnap(p.snap) + if (Array.isArray(p.displayModes)) setDisplayModes(p.displayModes) + if (Array.isArray(p.overridesPresets)) setOverridesPresets(p.overridesPresets) + if (Array.isArray(p.layerKombis)) setLayerKombis(p.layerKombis) + }) + notifyReady() + const blockContext = (ev) => ev.preventDefault() + document.addEventListener('contextmenu', blockContext) + return () => document.removeEventListener('contextmenu', blockContext) + }, []) + + const set = (patch) => setSnap(s => ({ ...s, ...patch })) + + const saveAndClose = () => { + send('SAVE', { + settings: { + scale: snap.scale || '', + displayMode: snap.displayMode || null, + displayModeName: snap.displayModeName || null, + applyOverrides: !!snap.applyOverrides, + overridesEnabled: !!snap.overridesEnabled, + overridesPreset: snap.overridesPreset || '', + layerCombination: snap.layerCombination || '', + }, + }) + } + + return ( +
+ {/* Body */} +
+ + set({ scale: ev.target.value })} + placeholder="1:50" + style={{ flex: 1, fontSize: 11, fontFamily: 'var(--font-mono)', minWidth: 0 }} + /> + + + + + + + Grafische Overrides + + + set({ applyOverrides: ev.target.checked })} + style={{ marginRight: 6 }} + /> + + {snap.applyOverrides + ? 'Overrides werden gesetzt' + : 'Aktueller Overrides-Zustand bleibt'} + + + + {snap.applyOverrides && ( + <> + + + + + + + + + )} + + Ebenenkombination + + + + +
+ + {/* Footer */} +
+
+ + +
+
+ ) +} diff --git a/src/AusschnitteApp.jsx b/src/AusschnitteApp.jsx index 0f51ec9..c4966aa 100644 --- a/src/AusschnitteApp.jsx +++ b/src/AusschnitteApp.jsx @@ -1,7 +1,6 @@ import { useState, useEffect, useMemo } from 'react' import Icon from './components/Icon' import ContextMenu from './components/ContextMenu' -import AusschnittLayerDialog from './components/AusschnittLayerDialog' import { onMessage, notifyReady, listAusschnitte, saveAusschnitt, updateAusschnitt, @@ -9,8 +8,7 @@ import { renameAusschnitt, deleteAusschnitt, setAusschnittFolder, setAusschnittScale, duplicateAusschnitt, addAusschnittFolder, removeAusschnittFolder, - getAusschnittLayers, updateAusschnittLayers, - saveLayerPreset, deleteLayerPreset, + openAusschnittSettings, } from './lib/rhinoBridge' function EditableInline({ value, onCommit, autoEdit, style, fontSize }) { @@ -247,22 +245,16 @@ function RootDropZone({ children, onDragOver, onDragLeave, onDrop, dragOver, emp export default function AusschnitteApp() { const [snaps, setSnaps] = useState([]) const [extraFolders, setExtraFolders] = useState([]) - const [presets, setPresets] = useState([]) const [newName, setNewName] = useState('') const [ctxMenu, setCtxMenu] = useState(null) const [collapsed, setCollapsed] = useState({}) const [draggingId, setDraggingId] = useState(null) const [dragTarget, setDragTarget] = useState(null) - const [layerDialog, setLayerDialog] = useState(null) useEffect(() => { - onMessage('LIST', ({ snapshots, folders, presets }) => { + onMessage('LIST', ({ snapshots, folders }) => { setSnaps(snapshots || []) setExtraFolders(folders || []) - setPresets(presets || []) - }) - onMessage('LAYERS_DATA', ({ id, name, layers, presets }) => { - setLayerDialog({ id, name, layers: layers || [], presets: presets || [] }) }) notifyReady() const blockContext = (ev) => ev.preventDefault() @@ -301,7 +293,7 @@ export default function AusschnitteApp() { { label: 'Wiederherstellen', icon: 'restore', onClick: () => restoreAusschnitt(id) }, { label: 'Auf Detail anwenden', icon: 'crop_landscape', onClick: () => applyAusschnittToDetail(id) }, { divider: true }, - { label: 'Sichtbarkeit bearbeiten…', icon: 'layers', onClick: () => getAusschnittLayers(id) }, + { label: 'Ausschnittseinstellungen…', icon: 'tune', onClick: () => openAusschnittSettings(id) }, { divider: true }, { label: 'Duplizieren', icon: 'content_copy', onClick: () => duplicateAusschnitt(id) }, { label: 'Aktualisieren', icon: 'sync', onClick: () => updateAusschnitt(id) }, @@ -361,17 +353,6 @@ export default function AusschnitteApp() { /> ) - const actions = ( -
- - -
- ) - const rootItems = groups[''] || [] const isEmpty = snaps.length === 0 && allFolders.length === 0 @@ -382,21 +363,6 @@ export default function AusschnitteApp() { background: 'var(--bg-base)', position: 'relative', }}> - {/* Fixed Header — wie Layouts/Overrides Pattern */} -
- - AUSSCHNITTE - - {snaps.length} - {actions} -
-
{/* Save-Bar als Card */}
+ {/* Sticky Footer: Anzahl + Ordner erstellen + Reload */} +
+ {snaps.length} + + Ausschnitte + + + +
+ {ctxMenu && ( )} - {layerDialog && ( - { - updateAusschnittLayers(layerDialog.id, - layers.map(l => ({ id: l.id, visible: l.visible, locked: l.locked }))) - setLayerDialog(null) - }} - onClose={() => setLayerDialog(null)} - onSavePreset={(name, layers) => { - saveLayerPreset(name, layers) - setLayerDialog(d => d ? { ...d, presets: [...d.presets.filter(p => p.name !== name), { name, layers }] } : d) - }} - onDeletePreset={(name) => { - deleteLayerPreset(name) - setLayerDialog(d => d ? { ...d, presets: d.presets.filter(p => p.name !== name) } : d) - }} - /> - )}
) } diff --git a/src/DimensionenApp.jsx b/src/DimensionenApp.jsx index dea4dc2..f4e9290 100644 --- a/src/DimensionenApp.jsx +++ b/src/DimensionenApp.jsx @@ -50,24 +50,22 @@ function NumInput({ value, onCommit, disabled, suffix, width }) { ) } -// 9-Punkt-Referenzpunkt-Selektor im Illustrator-Stil: sichtbarer BBox-Rahmen, -// die Punkte sitzen AUF Ecken / Kantenmitten / Zentrum. +// 9-Punkt-Referenzpunkt-Selektor: sichtbarer BBox-Rahmen, Kreise auf den +// Eckpunkten / Kantenmitten / Zentrum. function RefPointGrid({ ref, onChange }) { - const SIZE = 26 // Aussenkanten-Quadrat (px) - const DOT = 5 // Punkt-Durchmesser (px) - // Position pro Code: 0% (min), 50% (mid), 100% (max) + const SIZE = 28 // Aussenkanten-Quadrat (px) + const DOT = 6 // Kreis-Durchmesser (px) const pct = (c) => c === 'min' ? '0%' : c === 'max' ? '100%' : '50%' return (
{REF_CODES.map(yc => REF_CODES.map(xc => { const active = ref.x === xc && ref.y === yc - // yc 'max' = top in user mental model (Vectorworks/Illustrator) const topPct = yc === 'max' ? '0%' : yc === 'min' ? '100%' : '50%' return ( +
{[-90, -45, 45, 90].map(a => ( + ))} + +
+ + + {format === 'custom' ? ( + +
+ setCw(e.target.value)} + placeholder="Breite" + style={{ flex: 1, fontFamily: 'DM Mono, monospace', + fontSize: 12, textAlign: 'right', padding: '6px 8px' }} + /> + × + setCh(e.target.value)} + placeholder="Höhe" + style={{ flex: 1, fontFamily: 'DM Mono, monospace', + fontSize: 12, textAlign: 'right', padding: '6px 8px' }} + /> + mm +
+
+ ) : ( + +
+ + +
+
+ )} + + {/* Preview */} + +
+ + {/* Footer */} +
+
+ + +
+
+ ) +} + +function Field({ label, children }) { + return ( +
+ {label} + {children} +
+ ) +} + +const PAPER_DIMS = { + A0: [841, 1189], A1: [594, 841], A2: [420, 594], A3: [297, 420], + A4: [210, 297], Letter: [216, 279], +} + +function FormatPreview({ format, landscape, customW, customH }) { + let w, h + if (format === 'custom') { w = customW; h = customH } + else { + const dims = PAPER_DIMS[format] + if (!dims) return null + w = dims[0]; h = dims[1] + if (landscape) { w = dims[1]; h = dims[0] } + } + if (!(w > 0) || !(h > 0)) return null + const MAX = 120 + const scale = Math.min(MAX / w, MAX / h) + const pw = w * scale, ph = h * scale + return ( +
+
+ + {Math.round(w)} × {Math.round(h)} mm + +
+ ) +} diff --git a/src/LayoutsApp.jsx b/src/LayoutsApp.jsx index d676165..c2d2030 100644 --- a/src/LayoutsApp.jsx +++ b/src/LayoutsApp.jsx @@ -3,14 +3,13 @@ import Icon from './components/Icon' import ContextMenu from './components/ContextMenu' import { onMessage, notifyReady, - listLayouts, newLayout, deleteLayout, renameLayout, activateLayout, + listLayouts, deleteLayout, renameLayout, activateLayout, addDetail, deleteDetail, bindAusschnitt, syncDetail, syncLayout, - setPageSize, exportPdf, exportPdfAll, exportPdfMany, + exportPdf, exportPdfAll, exportPdfMany, addLayoutFolder, removeLayoutFolder, setLayoutFolder, + openLayoutDialog, } from './lib/rhinoBridge' -const PAPER_SIZES = ['A4', 'A3', 'A2', 'A1', 'A0', 'Letter'] - const PAPER_FORMATS_MM = { A0: [841, 1189], A1: [594, 841], A2: [420, 594], A3: [297, 420], A4: [210, 297], Letter: [216, 279], @@ -112,7 +111,6 @@ function EditableName({ value, onCommit, style, title, forceEdit, onEditDone }) export default function LayoutsApp() { const [state, setState] = useState({ layouts: [], snapshots: [], details: {}, folders: [] }) const [selectedId, setSelectedId] = useState(null) - const [dialog, setDialog] = useState(null) // { mode: 'new' | 'edit', layout? } const [checked, setChecked] = useState(new Set()) // Multi-Select IDs const [collapsedFolders, setCollapsedFolders] = useState(new Set()) const [draggingId, setDraggingId] = useState(null) @@ -187,8 +185,10 @@ export default function LayoutsApp() { { divider: true }, { label: 'Als PDF exportieren', icon: 'picture_as_pdf', onClick: () => exportPdf(l.id, 300) }, - { label: 'Papierformat aendern', icon: 'aspect_ratio', - onClick: () => setDialog({ mode: 'edit', layout: l }) }, + { label: 'Papierformat ändern', icon: 'aspect_ratio', + onClick: () => openLayoutDialog('edit', { + id: l.id, name: l.name, width: l.widthMm, height: l.heightMm, + }) }, { divider: true }, ...(folders.length > 0 ? [ ...folders.map(f => ({ @@ -276,57 +276,6 @@ export default function LayoutsApp() { background: 'var(--bg-base)', color: 'var(--text-primary)', fontFamily: 'var(--font)', fontSize: 11, }}> - {/* Header */} -
- LAYOUTS - - - - - -
-
{/* Layout-Liste */} {layouts.length === 0 && folders.length === 0 ? ( @@ -340,7 +289,7 @@ export default function LayoutsApp() {
Noch keine Layouts.
- Oben klicken um ein neues Layout anzulegen. + Unten klicken um ein neues Layout anzulegen.
) : ( @@ -610,6 +559,72 @@ export default function LayoutsApp() { )}
+ {/* Sticky Footer: Anzahl + Aktionen */} +
+ {layouts.length} + + Layouts + + {/* PDF-Aktionen: feste Breite damit das Auswahl-Counter den Footer + nicht horizontal verschiebt. */} + + +
+ + + +
+ {/* Kontextmenue */} {ctxMenu && ( )} - {/* Layout-Dialog: New oder Edit (Papierformat aendern) */} - {dialog && ( - setDialog(null)} - onSubmit={(p) => { - if (dialog.mode === 'new') { - newLayout(p.name, p.format, p.landscape, p.customWidth, p.customHeight) - } else { - setPageSize(dialog.layout.id, p.format, p.landscape, p.customWidth, p.customHeight) - } - setDialog(null) - }} - /> - )} -
- ) -} - -function LayoutDialog({ mode, layout, onCancel, onSubmit }) { - const editing = mode === 'edit' - const [name, setName] = useState('') - const [format, setFormat] = useState('A3') - const [landscape, setLandscape] = useState(true) - const [cw, setCw] = useState('420') // mm - const [ch, setCh] = useState('297') // mm - - // Wenn editieren: aktuelle Layout-Groesse pre-fillen (custom-Mode default) - useEffect(() => { - if (editing && layout) { - // BBox in Doc-Einheiten — wir kennen die Einheit nicht direkt im - // Frontend. Fuer den Edit-Modus zeigen wir die Groesse als Zahlen an - // und schicken sie als "custom" mit der mm-Annahme. Wenn das Doc nicht - // auf mm steht, ergibt sich eine kleine Konvertier-Unschaerfe — das - // Backend rechnet mm-Werte konsistent in Doc-Units um. - setFormat('custom') - setCw(String(Math.round(layout.width))) - setCh(String(Math.round(layout.height))) - } - }, [editing, layout]) - - return ( -
{ if (e.target === e.currentTarget) onCancel() }}> -
-
- {editing ? `Papierformat: ${layout?.name}` : 'Neues Layout'} -
-
- {!editing && ( -
-
Name
- setName(e.target.value)} - placeholder="z.B. Grundriss EG" - autoFocus - style={{ width: '100%', marginTop: 4 }} /> -
- )} -
-
Papierformat
-
- {PAPER_SIZES.map(f => ( - - ))} - -
-
- {format === 'custom' ? ( -
-
Eigene Groesse (mm)
-
- setCw(e.target.value)} - placeholder="Breite" - style={{ flex: 1, fontFamily: 'DM Mono, monospace', fontSize: 11 }} /> - × - setCh(e.target.value)} - placeholder="Höhe" - style={{ flex: 1, fontFamily: 'DM Mono, monospace', fontSize: 11 }} /> - mm -
-
- ) : ( -
-
Ausrichtung
-
- - -
-
- )} -
-
- - -
-
) } diff --git a/src/OberleisteApp.jsx b/src/OberleisteApp.jsx index dd8eb22..5747f91 100644 --- a/src/OberleisteApp.jsx +++ b/src/OberleisteApp.jsx @@ -7,6 +7,8 @@ import { setMassstabDpi, detectMassstabDpi, setView, setDisplayMode, toggleOverrides, setOverridesPreset, openOverridesPanel, + pickLayerCombination, saveLayerCombination, + deleteLayerCombination, openLayerCombinationsDialog, openDossierSettings, } from './lib/rhinoBridge' @@ -125,6 +127,7 @@ export default function OberleisteApp() { overridesEnabled: false, overridesCount: 0, cmdPrompt: '', cmdOptions: [], overridesActivePreset: null, overridesPresets: [], + layerCombinations: [], layerCombinationActive: null, }) const [appliedScale, setAppliedScale] = useState(null) const appliedScaleRef = useRef(null) @@ -380,6 +383,60 @@ export default function OberleisteApp() { title="Overrides-Regel-Editor öffnen" /> +
+ + {/* ====== GRUPPE: EBENENKOMBINATION ====== */} + Kombi + + { + 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) + }} + icon="add" + title="Aktuelle Sichtbarkeit als neue Kombination speichern" + /> + + {/* Spacer am rechten Rand */}
diff --git a/src/ZeichnungsebenenApp.jsx b/src/ZeichnungsebenenApp.jsx index 1c35cfd..e3d2065 100644 --- a/src/ZeichnungsebenenApp.jsx +++ b/src/ZeichnungsebenenApp.jsx @@ -56,10 +56,10 @@ export default function ZeichnungsebenenApp() { // eslint-disable-next-line react-hooks/exhaustive-deps }, []) - // Sichtbarkeit live anwenden bei Mode-/Visibility-Aenderungen + // Sichtbarkeit live anwenden bei Mode-/Visibility-/Lock-Aenderungen const visibilityKey = useMemo(() => ( activeId + '|' + zMode + '|' + - zeichnungsebenen.map(z => `${z.id}:${z.visible !== false ? 1 : 0}`).join(',') + zeichnungsebenen.map(z => `${z.id}:${z.visible !== false ? 1 : 0}:${z.locked ? 1 : 0}`).join(',') ), [activeId, zMode, zeichnungsebenen]) useEffect(() => { diff --git a/src/components/AusschnittLayerDialog.jsx b/src/components/AusschnittLayerDialog.jsx index a4b4341..d2449dd 100644 --- a/src/components/AusschnittLayerDialog.jsx +++ b/src/components/AusschnittLayerDialog.jsx @@ -22,6 +22,7 @@ export default function AusschnittLayerDialog({ snapName, layers, presets, onSave, onClose, onSavePreset, onDeletePreset, + embedded = false, }) { // Welche Kombination wird gerade angezeigt? null = aktueller Doc-State const [selectedPreset, setSelectedPreset] = useState(null) @@ -104,41 +105,47 @@ export default function AusschnittLayerDialog({ onSave(draft) } - return ( -
-
- {/* Header */} -
- - - {snapName} - - {dirty && ( - - )} - -
+ overflow: 'hidden' } + return ( +
+
+ {/* Header — im embedded-Modus weggelassen (Satellite-Fenster hat schon + seine native Title-Bar mit Close-Button) */} + {!embedded && ( +
+ + + {snapName} + + {dirty && ( + + )} + +
+ )} {/* Preset-Auswahl */}
{/* Layer-Liste */} -
+
{filtered.length === 0 ? (
Keine Ebenen gefunden. diff --git a/src/components/EbenenManager.jsx b/src/components/EbenenManager.jsx index 26cb3a1..a887123 100644 --- a/src/components/EbenenManager.jsx +++ b/src/components/EbenenManager.jsx @@ -255,9 +255,6 @@ function SortHeader({ label, sortKey, sortBy, sortDir, onSort, style }) { export default function EbenenManager({ ebenen, activeCode, onActiveChange, onChange, mode, onModeChange, hatchPatterns, - combinations = [], activeCombName = null, - onPickCombination, onSaveCurrentCombination, onDeleteCombination, - onEditCombinations, onUserVisibilityChange, }) { const [sortBy, setSortBy] = useState('code') const [sortDir, setSortDir] = useState('asc') @@ -306,12 +303,10 @@ export default function EbenenManager({ const handleToggleVisible = (code) => { const cur = ebenen.find(e => e.code === code) if (cur) updateByCode(code, { visible: !(cur.visible !== false) }) - if (onUserVisibilityChange) onUserVisibilityChange() } const handleToggleLock = (code) => { const cur = ebenen.find(e => e.code === code) if (cur) updateByCode(code, { locked: !cur.locked }) - if (onUserVisibilityChange) onUserVisibilityChange() } const handleColorChange = (code, color) => { updateByCode(code, { color }) @@ -426,58 +421,6 @@ export default function EbenenManager({ return ( <> - {/* Ebenenkombinationen — Label + Dropdown + Save-As-Plus */} -
- Ebenenkombination -
- - - -
-
-
e.visible !== false) // Wenn irgendeine sichtbar -> alle aus. Wenn keine sichtbar -> alle an. onChange(ebenen.map(e => ({ ...e, visible: !anyVisible }))) - if (onUserVisibilityChange) onUserVisibilityChange() }} title={ebenen.every(e => e.visible !== false) ? 'Alle Ebenen ausblenden' @@ -534,7 +476,6 @@ export default function EbenenManager({ onClick={() => { const anyLocked = ebenen.some(e => e.locked === true) onChange(ebenen.map(e => ({ ...e, locked: !anyLocked }))) - if (onUserVisibilityChange) onUserVisibilityChange() }} title={ebenen.every(e => e.locked === true) ? 'Alle Ebenen entsperren' diff --git a/src/components/EbenenSettingsDialog.jsx b/src/components/EbenenSettingsDialog.jsx index 4941f05..47c06b4 100644 --- a/src/components/EbenenSettingsDialog.jsx +++ b/src/components/EbenenSettingsDialog.jsx @@ -27,7 +27,10 @@ function SectionLabel({ children }) { ) } -export default function EbenenSettingsDialog({ ebene, hatchPatterns = ['Solid'], onSave, onClose, embedded = false }) { +export default function EbenenSettingsDialog({ + ebene, hatchPatterns = ['Solid'], onSave, onClose, embedded = false, + pickerEbenen = null, pickerSelected = null, onPickEbene = null, +}) { const [draft, setDraft] = useState({ ...ebene, fill: { @@ -107,21 +110,49 @@ export default function EbenenSettingsDialog({ ebene, hatchPatterns = ['Solid'], return (
- {/* Header */} -
- - - {ebene.code} — {ebene.name} - - -
+ + Ebene + + +
+ ) : !embedded && ( +
+ + + {ebene.code} — {ebene.name} + + +
+ )} {/* Body */}
diff --git a/src/components/GeschossManager.jsx b/src/components/GeschossManager.jsx index 3c61dae..f11c3d4 100644 --- a/src/components/GeschossManager.jsx +++ b/src/components/GeschossManager.jsx @@ -1,23 +1,29 @@ +import { useState } from 'react' import Icon from './Icon' +import ContextMenu from './ContextMenu' import { openGeschossSettings, openGeschossDialog } from '../lib/rhinoBridge' function GeschossBadge({ name }) { return {name} } -function ZeichnungsebeneRow({ z, active, mode, onClick, onToggleVisible, onSettings }) { - // Eye-State auch fuer die aktive Zeichnungsebene anzeigen (User-Intention) +function ZeichnungsebeneRow({ + z, active, mode, onClick, onContextMenu, + onToggleVisible, onToggleLock, onDelete, +}) { const eyeShown = mode !== 'active' const isGeschoss = !!z.isGeschoss return (
{z.name} @@ -65,9 +71,16 @@ function ZeichnungsebeneRow({ z, active, mode, onClick, onToggleVisible, onSetti + onClick={(ev) => { ev.stopPropagation(); onToggleLock() }} + title={z.locked ? 'Entsperren' : 'Sperren'} + style={{ color: z.locked ? 'var(--warn)' : undefined }} + > + +
) } @@ -83,8 +96,7 @@ export default function GeschossManager({ zeichnungsebenen, activeId, onActiveChange, onChange, recalcOkff, mode, onModeChange, }) { - // dialogOpen-State entfaellt — Bearbeiten-Dialog laeuft jetzt als - // Satelliten-Fenster via openGeschossDialog(). + const [ctxMenu, setCtxMenu] = useState(null) // { x, y, id } const sorted = [...zeichnungsebenen].reverse() const gesamthoehe = zeichnungsebenen @@ -93,9 +105,8 @@ export default function GeschossManager({ const addQuick = () => { // Standard: NICHT-Geschoss-Zeichnungsebene (z.B. Möblierung, Bemassung, - // Plangrafik etc.). User kann via Row-Settings-Cog auf Geschoss - // umschalten, oder via Bearbeiten-Dialog (Pencil) ein Geschoss - // direkt erstellen. + // Plangrafik etc.). User kann via Row-Kontextmenue auf Geschoss + // umschalten oder via Bearbeiten-Dialog (Pencil) ein Geschoss erstellen. const nonGeschossCount = zeichnungsebenen.filter(z => !z.isGeschoss).length const newZ = { id: `z_${Date.now()}`, @@ -103,7 +114,6 @@ export default function GeschossManager({ isGeschoss: false, visible: true, } - console.log('[ZEICHNUNGSEBENEN-UI] addQuick →', { newZ, countBefore: zeichnungsebenen.length }) onChange([...zeichnungsebenen, newZ]) } @@ -111,6 +121,56 @@ export default function GeschossManager({ onChange(zeichnungsebenen.map(z => z.id === id ? { ...z, visible: !(z.visible !== false) } : z)) } + const toggleLock = (id) => { + onChange(zeichnungsebenen.map(z => z.id === id ? { ...z, locked: !z.locked } : z)) + } + + const duplicate = (id) => { + const src = zeichnungsebenen.find(z => z.id === id) + if (!src) return + const clone = { + ...src, + id: `z_${Date.now()}`, + name: `${src.name} Kopie`, + } + // Direkt nach dem Original einfuegen + const idx = zeichnungsebenen.findIndex(z => z.id === id) + const next = [...zeichnungsebenen] + next.splice(idx + 1, 0, clone) + onChange(next) + } + + const remove = (id) => { + if (zeichnungsebenen.length <= 1) return + const target = zeichnungsebenen.find(z => z.id === id) + if (!target) return + if (!window.confirm(`"${target.name}" wirklich löschen?`)) return + onChange(zeichnungsebenen.filter(z => z.id !== id)) + if (activeId === id) { + const next = zeichnungsebenen.find(z => z.id !== id) + if (next) onActiveChange(next.id) + } + } + + const openContextMenu = (ev, id) => { + ev.preventDefault(); ev.stopPropagation() + setCtxMenu({ x: ev.clientX, y: ev.clientY, id }) + } + + const ctxItems = (id) => { + const z = zeichnungsebenen.find(x => x.id === id) + if (!z) return [] + return [ + { label: 'Einstellungen…', icon: 'settings', onClick: () => openGeschossSettings(z) }, + { divider: true }, + { label: 'Duplizieren', icon: 'content_copy', onClick: () => duplicate(id) }, + { divider: true }, + { label: 'Löschen', icon: 'delete', danger: true, + disabled: zeichnungsebenen.length <= 1, + onClick: () => remove(id) }, + ] + } + return ( <>
onActiveChange(z.id)} + onContextMenu={(ev) => openContextMenu(ev, z.id)} onToggleVisible={() => toggleVisible(z.id)} - onSettings={() => openGeschossSettings(z)} + onToggleLock={() => toggleLock(z.id)} + onDelete={() => remove(z.id)} /> ))}
+ {ctxMenu && ( + setCtxMenu(null)} + /> + )} ) } diff --git a/src/lib/rhinoBridge.js b/src/lib/rhinoBridge.js index 907c18f..0bc7e9e 100644 --- a/src/lib/rhinoBridge.js +++ b/src/lib/rhinoBridge.js @@ -112,6 +112,7 @@ export function getAusschnittLayers(id) { send('GET_LAYERS', { i export function updateAusschnittLayers(id, layers) { send('UPDATE_LAYERS', { id, layers }) } export function saveLayerPreset(name, layers) { send('SAVE_PRESET', { name, layers }) } export function deleteLayerPreset(name) { send('DELETE_PRESET', { name }) } +export function openAusschnittSettings(id) { send('OPEN_SETTINGS', { id }) } // --- Gestaltung-Panel --- export function requestSelection() { @@ -168,6 +169,11 @@ export function toggleOverrides(on) { send('TOGGLE_OVERRIDES', { enabled: ! export function setOverridesPreset(name) { send('SET_OVERRIDES_PRESET', { name: name || null }) } export function saveOverridesPreset(name) { send('SAVE_OVERRIDES_PRESET', { name }) } export function openOverridesPanel() { send('OPEN_OVERRIDES_PANEL', {}) } +// Ebenenkombinationen (gehosted in Oberleiste, gleicher Store wie EBENEN) +export function pickLayerCombination(name) { send('PICK_LAYER_COMBINATION', { name: name || null }) } +export function saveLayerCombination(name) { send('SAVE_LAYER_COMBINATION', { name }) } +export function deleteLayerCombination(name) { send('DELETE_LAYER_COMBINATION', { name }) } +export function openLayerCombinationsDialog() { send('OPEN_LAYER_COMBINATIONS_DIALOG', {}) } export function runCommand(cmd) { send('RUN_COMMAND', { cmd }) } export function sendKeys(text, enter) { send('SEND_KEYS', { text, enter: enter !== false }) } export function cancelCommand() { send('CANCEL_COMMAND', {}) } @@ -232,6 +238,7 @@ export function setPageSize(id, format, landscape, customWidth, customHeight) { customWidth, customHeight }) } export function exportPdf(id, dpi) { send('EXPORT_PDF', { id, dpi: dpi || 300 }) } +export function openLayoutDialog(mode, layout) { send('OPEN_LAYOUT_DIALOG', { mode: mode || 'new', layout: layout || null }) } export function exportPdfAll(dpi) { send('EXPORT_PDF', { dpi: dpi || 300 }) } export function exportPdfMany(ids, dpi) { send('EXPORT_PDF', { ids, dpi: dpi || 300 }) } export function addLayoutFolder(name) { send('ADD_FOLDER', { name }) } @@ -304,7 +311,9 @@ export function applyVisibility(activeZ, zeichnungsebenen, activeCode, ebenen, z const zList = Array.isArray(a.zeichnungsebenen) ? a.zeichnungsebenen : [] const eList = Array.isArray(a.ebenen) ? a.ebenen : [] const slimZ = zList.map(z => ({ - id: z.id, name: z.name, visible: z.visible !== false, + id: z.id, name: z.name, + visible: z.visible !== false, + locked: z.locked === true, })) const slimE = eList.map(e => ({ code: e.code, visible: e.visible !== false, locked: e.locked === true, diff --git a/src/main.jsx b/src/main.jsx index 89d2f54..76f3d8b 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -6,6 +6,9 @@ import ZeichnungsebenenApp from './ZeichnungsebenenApp.jsx' import GeschossSettingsApp from './GeschossSettingsApp.jsx' import EbenenSettingsApp from './EbenenSettingsApp.jsx' import GeschossDialogApp from './GeschossDialogApp.jsx' +import LayerCombinationsApp from './LayerCombinationsApp.jsx' +import AusschnittSettingsApp from './AusschnittSettingsApp.jsx' +import LayoutDialogApp from './LayoutDialogApp.jsx' import GestaltungApp from './GestaltungApp.jsx' import AusschnitteApp from './AusschnitteApp.jsx' import MassstabApp from './MassstabApp.jsx' @@ -30,6 +33,9 @@ const RootApp = mode === 'gestaltung' ? GestaltungApp : mode === 'geschoss_settings' ? GeschossSettingsApp : mode === 'ebenen_settings' ? EbenenSettingsApp : mode === 'geschoss_dialog' ? GeschossDialogApp + : mode === 'layer_combinations' ? LayerCombinationsApp + : mode === 'ausschnitt_settings' ? AusschnittSettingsApp + : mode === 'layout_dialog' ? LayoutDialogApp : App window.onerror = function (msg, src, line, col, err) {