From a458b4c47d79a261969df87d84d92d9375e30aa6 Mon Sep 17 00:00:00 2001 From: karim Date: Mon, 18 May 2026 23:42:02 +0200 Subject: [PATCH] Ebenen-Panel in zwei UI-Panels splitten: Zeichnungsebenen + Ebenen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UX: Geschoss-Liste und globale Layer-Liste lebten bisher in einem Panel und mussten beide gescrollt werden. Jetzt zwei getrennte Tabs. Backend (rhino/rhinopanel.py): - Selbe EbenenBridge-Klasse, zwei Mode-Instanzen ("ebenen" + "zeichnungsebenen"). Beide registrieren sich in sticky-Slots (`ebenen_bridge_ref` / `zeichnungsebenen_bridge_ref`). - `_broadcast_state(doc)` Helper: liest aktuell Zustand aus doc.Strings und schickt STATE_SYNC an beide Bridges. Wird nach jeder state- aendernden Aktion gefeuert (apply, set_active_zeichnungsebene, toggle_clipping, remove/update ebene, layer-table-event). - `handle(APPLY)`: wenn nur eine Slice (z oder e) im Payload, fehlende aus doc.Strings nachladen → Backend baut mit vollem Zustand. - `_apply_visibility`: zMode/eMode/activeId/activeCode aus Payload ODER aus doc.Strings (dossier_z_mode/dossier_e_mode/dossier_active_id/ dossier_active_code) faellen lassen — Split-Sends werden korrekt gemergt. - Layer-Table-Event broadcastet jetzt statt nur das eine Panel zu benachrichtigen. - Zweite `register_and_open("zeichnungsebenen", ...)` Zeile mit eigener GUID + Icon "levels". Frontend: - Neues src/ZeichnungsebenenApp.jsx: enthaelt nur GeschossManager, haelt Zeichnungsebenen + activeId + zMode lokal, schickt applyAll([z], []) und applyVisibility mit leerer Ebenen-Slice. - src/App.jsx geschrumpft: nur noch EbenenManager + AusschnittLayer- Dialog. Haelt Ebenen + activeCode + eMode + Combinations. Schickt applyAll([], [e]) und applyVisibility mit leerer Z-Slice. - src/main.jsx: neuer case fuer mode="zeichnungsebenen" → lädt ZeichnungsebenenApp. Existierende User mit altem DOSSIERUI.rhw Workspace muessen das neue Panel einmal manuell oeffnen (Rechtsklick Panel-Area → Panel hinzu- fuegen → "Zeichnungsebenen"); Rhino persistiert die Anordnung danach. Co-Authored-By: Claude Opus 4.7 --- rhino/rhinopanel.py | 120 ++++++++++++++++++++++++++-------- src/App.jsx | 119 ++++++---------------------------- src/ZeichnungsebenenApp.jsx | 125 ++++++++++++++++++++++++++++++++++++ src/main.jsx | 20 +++--- 4 files changed, 247 insertions(+), 137 deletions(-) create mode 100644 src/ZeichnungsebenenApp.jsx diff --git a/rhino/rhinopanel.py b/rhino/rhinopanel.py index cc9b3b2..2ab4475 100644 --- a/rhino/rhinopanel.py +++ b/rhino/rhinopanel.py @@ -18,6 +18,11 @@ import panel_base import layer_builder PANEL_GUID_STR = "3a7f2e1d-4b8c-4d9e-a1b2-c3d4e5f60718" +# Zweites Panel fuer Zeichnungsebenen (Geschoss-Liste + Clipping). UX-Split +# damit der User nicht beide grossen Sections in einem Panel scrollen muss. +# Beide Bridges sharen den State via doc.Strings und synchronisieren sich +# gegenseitig via STATE_SYNC-Broadcast. +PANEL_GUID_STR_Z = "3a7f2e1d-4b8c-4d9e-a1b2-c3d4e5f60719" # Loop-Guard fuer Layer-Events (verhindert Endlos-Schleife bei eigenen Aenderungen) def _is_processing(): @@ -72,9 +77,41 @@ def _read_launcher_schema(): return None +def _broadcast_state(doc=None, hatch_patterns=None): + """STATE_SYNC an alle registrierten Bridges schicken — beide Panels (Ebenen + + Zeichnungsebenen) sollen denselben State sehen. Liest aktuell aus + doc.Strings; jede React-App pickt sich ihre Slice.""" + if doc is None: + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: return + try: + z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen") + e_raw = doc.Strings.GetValue("dossier_ebenen") + payload = { + "zeichnungsebenen": json.loads(z_raw) if z_raw else None, + "ebenen": json.loads(e_raw) if e_raw else None, + "hatchPatterns": hatch_patterns if hatch_patterns is not None + else _hatch_pattern_names(doc), + } + except Exception as ex: + print("[EBENEN] broadcast prepare:", ex) + return + for key in ("ebenen_bridge_ref", "zeichnungsebenen_bridge_ref"): + b = sc.sticky.get(key) + if b is None: continue + try: b.send("STATE_SYNC", payload) + except Exception: pass + + class EbenenBridge(panel_base.BaseBridge): - def __init__(self): - panel_base.BaseBridge.__init__(self, "ebenen") + """Gemeinsame Bridge-Klasse fuer beide Panels (Ebenen + Zeichnungsebenen). + Mode bestimmt nur welches WebView die Bridge bedient + welcher sticky-Slot + fuer die Cross-Sync genutzt wird. Beide Bridges koennen die gleichen + Messages verarbeiten — jede React-App schickt nur die fuer ihre Section + relevanten.""" + def __init__(self, mode="ebenen"): + panel_base.BaseBridge.__init__(self, mode) + self._mode = mode def _on_ready(self): doc = Rhino.RhinoDoc.ActiveDoc @@ -118,7 +155,18 @@ class EbenenBridge(panel_base.BaseBridge): if t == "READY": self._on_ready() elif t == "APPLY": - self._apply(p.get("zeichnungsebenen") or [], p.get("ebenen") or []) + # Beide Panels koennen APPLY schicken. Wenn nur eine Slice + # gesendet wird, fehlende aus doc.Strings nachladen damit + # build_layers nicht mit leerer Liste arbeitet. + z_payload = p.get("zeichnungsebenen") + e_payload = p.get("ebenen") + if not z_payload: + z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen") + z_payload = json.loads(z_raw) if z_raw else [] + if not e_payload: + e_raw = doc.Strings.GetValue("dossier_ebenen") + e_payload = json.loads(e_raw) if e_raw else [] + self._apply(z_payload, e_payload) elif t == "LAYER_STYLE": layer_builder.update_layer_style(doc, p["code"], p.get("color"), p.get("lw")) if p.get("color") is not None: @@ -254,6 +302,9 @@ class EbenenBridge(panel_base.BaseBridge): self._update_clipping() print("[EBENEN] _apply: send APPLY_OK") self.send("APPLY_OK", {}) + # Anderes Panel (Zeichnungsebenen/Ebenen) ueber den neuen State + # informieren — sonst hinkt es hinter der DOM-Persistenz her. + _broadcast_state(doc) print("[EBENEN] _apply: DONE") def _ensure_active_sublayer(self): @@ -309,20 +360,31 @@ class EbenenBridge(panel_base.BaseBridge): merged_e.append(m) doc.Strings.SetString("dossier_zeichnungsebenen", json.dumps(merged_z, ensure_ascii=False)) doc.Strings.SetString("dossier_ebenen", json.dumps(merged_e, ensure_ascii=False)) + # 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. + z_mode = p.get("zMode") or doc.Strings.GetValue("dossier_z_mode") or "active" + e_mode = p.get("eMode") or doc.Strings.GetValue("dossier_e_mode") or "all" + try: + doc.Strings.SetString("dossier_z_mode", z_mode) + doc.Strings.SetString("dossier_e_mode", e_mode) + except Exception: pass active_z = p.get("activeZ") or {} 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") layer_builder.apply_visibility( - doc, merged_z, merged_e, - active_z.get("id"), - p.get("activeCode"), - p.get("zMode") or "active", - p.get("eMode") or "all", - ) + doc, merged_z, merged_e, active_z_id, active_code, z_mode, e_mode) + # Cross-Panel-Sync + _broadcast_state(doc) def _set_active_zeichnungsebene(self, z): doc = Rhino.RhinoDoc.ActiveDoc z_id = z.get("id", "") doc.Strings.SetString("dossier_active_id", z_id) + # 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) # Elemente-Panel informieren: das aktive Geschoss hat gewechselt, @@ -385,15 +447,9 @@ class EbenenBridge(panel_base.BaseBridge): except Exception as ex: print("[CLIP] toggle SetString:", ex) self._update_clipping(active_z=active_z) - # State an React zurueckspiegeln, damit das Eye-Icon im GeschossManager - # synchron bleibt. - try: - self.send("STATE_SYNC", { - "zeichnungsebenen": z_list, - "ebenen": json.loads(doc.Strings.GetValue("dossier_ebenen") or "[]"), - "hatchPatterns": _hatch_pattern_names(doc), - }) - except Exception: pass + # State an BEIDE React-Panels zuruekspiegeln. Eye-Icon im + # ZeichnungsebenenPanel + Status im Ebenen-Panel synchron halten. + _broadcast_state(doc) def _update_clipping(self, active_z=None): """Clipping-Plane folgt aktivem Geschoss — nur wenn dessen hasClipping=True. @@ -489,6 +545,7 @@ class EbenenBridge(panel_base.BaseBridge): try: ebenen = [e for e in json.loads(raw) if e.get("code") != code] doc.Strings.SetString("dossier_ebenen", json.dumps(ebenen, ensure_ascii=False)) + _broadcast_state(doc) except Exception as ex: print("[EBENEN] remove:", ex) @@ -504,6 +561,7 @@ class EbenenBridge(panel_base.BaseBridge): e[field] = value break doc.Strings.SetString("dossier_ebenen", json.dumps(ebenen, ensure_ascii=False)) + _broadcast_state(doc) except Exception as ex: print("[EBENEN] update:", ex) @@ -816,17 +874,25 @@ class EbenenBridge(panel_base.BaseBridge): def _ebenen_bridge_factory(): - bridge = EbenenBridge() + bridge = EbenenBridge(mode="ebenen") + sc.sticky["ebenen_bridge_ref"] = bridge _install_layer_listener(bridge) return bridge +def _zeichnungsebenen_bridge_factory(): + bridge = EbenenBridge(mode="zeichnungsebenen") + sc.sticky["zeichnungsebenen_bridge_ref"] = bridge + return bridge + + def _install_layer_listener(bridge): - """Reagiert auf externe Aenderungen in Rhinos Layer-Tabelle (Rename, Delete).""" + """Reagiert auf externe Aenderungen in Rhinos Layer-Tabelle (Rename, Delete). + Nur EINMAL global registrieren — Bridge-Referenz kommt aus dem + Cross-Sync-Broadcast (alle aktuell offenen Panels werden benachrichtigt). + """ if sc.sticky.get("ebenen_layer_listener"): - sc.sticky["ebenen_bridge_ref"] = bridge return - sc.sticky["ebenen_bridge_ref"] = bridge def on_layer_event(sender, args): if _is_processing(): @@ -878,12 +944,7 @@ def _install_layer_listener(bridge): except Exception: pass if updated: - b = sc.sticky.get("ebenen_bridge_ref") - if b is not None: - try: - b._on_ready() # sendet aktualisiertes STATE_SYNC - except Exception: - pass + _broadcast_state(doc) except Exception as ex: print("[EBENEN] Layer-Event:", ex) @@ -894,3 +955,6 @@ def _install_layer_listener(bridge): 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, + _zeichnungsebenen_bridge_factory, + icon_spec=("levels", "#3a6fa8")) diff --git a/src/App.jsx b/src/App.jsx index 0f8c5c8..2de9dec 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,33 +1,14 @@ import { useState, useEffect, useMemo, useRef } from 'react' -import GeschossManager from './components/GeschossManager' import EbenenManager from './components/EbenenManager' import AusschnittLayerDialog from './components/AusschnittLayerDialog' import { - applyAll, setActiveZeichnungsebene, setActiveEbene, + applyAll, setActiveEbene, onMessage, notifyReady, applyVisibility, getCombination, applyCombination, saveCurrentAsCombination, deleteCombinationPreset, saveCombinationPreset, } from './lib/rhinoBridge' -export function recalcOkff(list) { - let acc = 0 - return list.map(z => { - if (z.isGeschoss) { - const next = { ...z, okff: parseFloat(acc.toFixed(3)) } - acc += (z.hoehe ?? 3.0) - return next - } - return { ...z, okff: undefined } - }) -} - -const INITIAL_ZEICHNUNGSEBENEN = recalcOkff([ - { id: 'eg', name: 'EG', isGeschoss: true, hoehe: 3.50, schnitthoehe: 1.00, visible: true }, - { id: '1og', name: '1OG', isGeschoss: true, hoehe: 3.00, schnitthoehe: 1.00, visible: true }, - { id: '2og', name: '2OG', isGeschoss: true, hoehe: 3.00, schnitthoehe: 1.00, visible: true }, -]) - 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 }, @@ -50,13 +31,9 @@ const INITIAL_EBENEN = [ ] export default function App() { - const [zeichnungsebenen, setZeichnungsebenen] = useState(INITIAL_ZEICHNUNGSEBENEN) const [ebenen, setEbenen] = useState(INITIAL_EBENEN) - const [activeId, setActiveId] = useState('eg') const [activeCode, setActiveCode] = useState('20') - const [appliedZ, setAppliedZ] = useState(INITIAL_ZEICHNUNGSEBENEN) const [appliedE, setAppliedE] = useState(INITIAL_EBENEN) - const [zMode, setZMode] = useState('active') const [eMode, setEMode] = useState('all') const [hatchPatterns, setHatchPatterns] = useState(['Solid', 'Hatch1', 'Hatch2', 'Hatch3', 'Plus', 'Squares', 'Grid', 'Grid60']) // Ebenenkombinationen (geteilter Store mit Ausschnitten) @@ -67,18 +44,7 @@ export default function App() { const wantCombDialogRef = useRef(false) useEffect(() => { - onMessage('STATE_SYNC', ({ zeichnungsebenen: z, ebenen: e, hatchPatterns: hp }) => { - if (z) { - const r = recalcOkff(z); setZeichnungsebenen(r); setAppliedZ(r) - const active = r.find(zz => zz.id === activeId) || r[0] - if (active) { - setActiveZeichnungsebene(active) - // Auch den Sublayer-Code aktiv setzen, damit Rhino's Current-Layer - // beim Panel-Start sofort auf der Wahl im Panel landet (sonst bleibt - // "Default" und neue Objekte landen dort). - if (activeCode) setActiveEbene(activeCode) - } - } + onMessage('STATE_SYNC', ({ ebenen: e, hatchPatterns: hp }) => { if (e) { setEbenen(e); setAppliedE(e) } if (Array.isArray(hp) && hp.length > 0) setHatchPatterns(hp) }) @@ -94,24 +60,16 @@ export default function App() { }) onMessage('FIRST_RUN', ({ defaultEbenen } = {}) => { // Wenn der Dossier-Launcher ein eigenes Schema definiert hat, nutzen wir - // das statt der hardcoded INITIAL_EBENEN. Felder ohne `visible`/`locked` - // werden mit Defaults ergaenzt damit die UI-Komponenten keine undefineds - // sehen. + // das statt der hardcoded INITIAL_EBENEN. const useEbenen = (Array.isArray(defaultEbenen) && defaultEbenen.length) - ? defaultEbenen.map(e => ({ - visible: true, locked: false, - ...e, - })) + ? defaultEbenen.map(e => ({ visible: true, locked: false, ...e })) : INITIAL_EBENEN setEbenen(useEbenen) - applyAll(INITIAL_ZEICHNUNGSEBENEN, useEbenen) - setAppliedZ(INITIAL_ZEICHNUNGSEBENEN) + // Nur unsere Slice an Backend — Zeichnungsebenen-Slice kommt vom anderen + // Panel (oder Backend fuellt aus doc.Strings). + applyAll([], useEbenen) setAppliedE(useEbenen) - const active = INITIAL_ZEICHNUNGSEBENEN.find(zz => zz.id === activeId) || INITIAL_ZEICHNUNGSEBENEN[0] - if (active) { - setActiveZeichnungsebene(active) - if (activeCode) setActiveEbene(activeCode) - } + if (activeCode) setActiveEbene(activeCode) }) notifyReady() // Initial Liste der Kombinationen holen @@ -121,56 +79,39 @@ export default function App() { const blockContext = (ev) => ev.preventDefault() document.addEventListener('contextmenu', blockContext) return () => document.removeEventListener('contextmenu', blockContext) + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) - // Sichtbarkeit live anwenden — bei relevanten Aenderungen + // Sichtbarkeit live anwenden bei Layer-Aenderungen. Zeichnungsebenen-Slice + // bleibt leer — Backend mergt mit doc.Strings. const visibilityKey = useMemo(() => ( - activeId + '|' + activeCode + '|' + zMode + '|' + eMode + '|' + - zeichnungsebenen.map(z => `${z.id}:${z.visible !== false ? 1 : 0}`).join(',') + '|' + + activeCode + '|' + eMode + '|' + ebenen.map(e => `${e.code}:${e.visible !== false ? 1 : 0}:${e.locked === true ? 1 : 0}`).join(',') - ), [activeId, activeCode, zMode, eMode, zeichnungsebenen, ebenen]) + ), [activeCode, eMode, ebenen]) useEffect(() => { - const activeZ = zeichnungsebenen.find(z => z.id === activeId) - if (activeZ) applyVisibility(activeZ, zeichnungsebenen, activeCode, ebenen, zMode, eMode) + applyVisibility(null, [], activeCode, ebenen, null, eMode) // eslint-disable-next-line react-hooks/exhaustive-deps }, [visibilityKey]) - // Auto-Apply bei strukturellen Aenderungen (add/remove/rename) UND - // wenn die Ebenen-Settings (z.B. fill) sich aendern — Python braucht den - // neuen Stand in doc.Strings damit Auto-Fill und 'Nach Ebene' korrekt lesen. - // Kanonische Signatur: leere/None-Fills sind alle aequivalent — sonst loest - // schon das blosse Oeffnen+Schliessen des Settings-Dialogs ein applyAll aus - // (Dialog initialisiert fill mit Default-Werten). + // Auto-Apply bei strukturellen Aenderungen (name, fill) — wieder nur unsere + // Slice, Backend mergt. const fillSig = (e) => { const f = e.fill if (!f || !f.pattern || f.pattern === 'None') return '' return [f.pattern, f.source || 'layer', f.color || '', f.scale ?? 1, f.rotation ?? 0].join('|') } - // WICHTIG: alle Felder die das Backend braucht hier mit drin haben — sonst - // triggert Aenderung an z.B. hasClipping/schnitthoehe kein Apply, und das - // Backend sieht den neuen Stand nie. Frueher waren nur id/name/isGeschoss - // drin -> Clipping-Toggle blieb wirkungslos. - const zSig = (z) => [ - z.id, z.name, z.isGeschoss ? 1 : 0, - z.hoehe ?? '', z.schnitthoehe ?? '', - z.hasClipping ? 1 : 0, - ].join(':') const structureKey = useMemo(() => ( - zeichnungsebenen.map(zSig).join(',') + '|' + ebenen.map(e => `${e.code}:${e.name}:${fillSig(e)}`).join(',') - ), [zeichnungsebenen, ebenen]) - + ), [ebenen]) const appliedStructureKey = useMemo(() => ( - appliedZ.map(zSig).join(',') + '|' + appliedE.map(e => `${e.code}:${e.name}:${fillSig(e)}`).join(',') - ), [appliedZ, appliedE]) + ), [appliedE]) useEffect(() => { if (structureKey === appliedStructureKey) return const t = setTimeout(() => { - applyAll(zeichnungsebenen, ebenen) - setAppliedZ(zeichnungsebenen) + applyAll([], ebenen) setAppliedE(ebenen) }, 200) return () => clearTimeout(t) @@ -182,8 +123,6 @@ export default function App() { if (!name) { setActiveCombName(null); return } const preset = combinations.find(p => p.name === name) if (!preset) return - // Eye-State bevorzugen wenn im Preset vorhanden (= verlustfreie Wiederherstellung, - // beruecksichtigt z_mode/e_mode); fallback auf doc.Layer-Liste fuer alte Presets. applyCombination({ layers: preset.layers || [], dossierEbenen: preset.dossierEbenen, @@ -210,21 +149,10 @@ export default function App() { wantCombDialogRef.current = true getCombination() } - // Wenn der User Sichtbarkeit/Lock manuell aendert -> "Eigene". - // Wird direkt von EbenenManager aufgerufen, kein Effect-Race. const handleUserVisibilityChange = () => { if (activeCombName !== null) setActiveCombName(null) } - const handleActiveChange = (id) => { - setActiveId(id) - const z = zeichnungsebenen.find(x => x.id === id) - if (z) { - setActiveZeichnungsebene(z) - if (activeCode) setActiveEbene(activeCode) - } - } - return (
- setZeichnungsebenen(recalcOkff(updated))} - recalcOkff={recalcOkff} - mode={zMode} - onModeChange={setZMode} - /> { + if (z.isGeschoss) { + const next = { ...z, okff: parseFloat(acc.toFixed(3)) } + acc += (z.hoehe ?? 3.0) + return next + } + return { ...z, okff: undefined } + }) +} + +const INITIAL_ZEICHNUNGSEBENEN = recalcOkff([ + { id: 'eg', name: 'EG', isGeschoss: true, hoehe: 3.50, schnitthoehe: 1.00, visible: true }, + { id: '1og', name: '1OG', isGeschoss: true, hoehe: 3.00, schnitthoehe: 1.00, visible: true }, + { id: '2og', name: '2OG', isGeschoss: true, hoehe: 3.00, schnitthoehe: 1.00, visible: true }, +]) + +export default function ZeichnungsebenenApp() { + const [zeichnungsebenen, setZeichnungsebenen] = useState(INITIAL_ZEICHNUNGSEBENEN) + const [activeId, setActiveId] = useState('eg') + const [appliedZ, setAppliedZ] = useState(INITIAL_ZEICHNUNGSEBENEN) + const [zMode, setZMode] = useState('active') + + useEffect(() => { + onMessage('STATE_SYNC', ({ zeichnungsebenen: z }) => { + if (z) { + const r = recalcOkff(z); setZeichnungsebenen(r); setAppliedZ(r) + const active = r.find(zz => zz.id === activeId) || r[0] + if (active && !r.find(zz => zz.id === activeId)) { + setActiveId(active.id) + setActiveZeichnungsebene(active) + } + } + }) + onMessage('FIRST_RUN', () => { + // Backend hat keine doc.Strings → wir senden den Default an Backend + // (nur unsere Slice; ebenen fuellt Backend aus doc.Strings oder leerer + // Liste vom anderen Panel beim FIRST_RUN dort). + applyAll(INITIAL_ZEICHNUNGSEBENEN, []) + setAppliedZ(INITIAL_ZEICHNUNGSEBENEN) + const active = INITIAL_ZEICHNUNGSEBENEN.find(zz => zz.id === activeId) || INITIAL_ZEICHNUNGSEBENEN[0] + if (active) setActiveZeichnungsebene(active) + }) + notifyReady() + const blockContext = (ev) => ev.preventDefault() + document.addEventListener('contextmenu', blockContext) + return () => document.removeEventListener('contextmenu', blockContext) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + // Sichtbarkeit live anwenden bei Mode-/Visibility-Aenderungen + const visibilityKey = useMemo(() => ( + activeId + '|' + zMode + '|' + + zeichnungsebenen.map(z => `${z.id}:${z.visible !== false ? 1 : 0}`).join(',') + ), [activeId, zMode, zeichnungsebenen]) + + useEffect(() => { + const activeZ = zeichnungsebenen.find(z => z.id === activeId) + if (activeZ) { + // Backend merged mit doc.Strings: aktiveCode + eMode kommen von dort + applyVisibility(activeZ, zeichnungsebenen, null, [], zMode, null) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [visibilityKey]) + + // Auto-Apply bei strukturellen Aenderungen (add/remove/rename, hoehe, + // schnitthoehe, hasClipping). Nur ZEICHNUNGSEBENEN-Slice senden — Backend + // mergt mit doc.Strings. + const zSig = (z) => [ + z.id, z.name, z.isGeschoss ? 1 : 0, + z.hoehe ?? '', z.schnitthoehe ?? '', + z.hasClipping ? 1 : 0, + ].join(':') + const structureKey = useMemo(() => ( + zeichnungsebenen.map(zSig).join(',') + ), [zeichnungsebenen]) + const appliedStructureKey = useMemo(() => ( + appliedZ.map(zSig).join(',') + ), [appliedZ]) + + useEffect(() => { + if (structureKey === appliedStructureKey) return + const t = setTimeout(() => { + applyAll(zeichnungsebenen, []) + setAppliedZ(zeichnungsebenen) + }, 200) + return () => clearTimeout(t) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [structureKey, appliedStructureKey]) + + const handleActiveChange = (id) => { + setActiveId(id) + const z = zeichnungsebenen.find(x => x.id === id) + if (z) setActiveZeichnungsebene(z) + } + + return ( +
+
+ setZeichnungsebenen(recalcOkff(updated))} + recalcOkff={recalcOkff} + mode={zMode} + onModeChange={setZMode} + /> +
+
+ ) +} diff --git a/src/main.jsx b/src/main.jsx index b6e9fc1..1ac03cc 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -2,6 +2,7 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' import App from './App.jsx' +import ZeichnungsebenenApp from './ZeichnungsebenenApp.jsx' import GestaltungApp from './GestaltungApp.jsx' import AusschnitteApp from './AusschnitteApp.jsx' import MassstabApp from './MassstabApp.jsx' @@ -13,15 +14,16 @@ import LayoutsApp from './LayoutsApp.jsx' import ElementeApp from './ElementeApp.jsx' const mode = (typeof window !== 'undefined' && window.PANEL_MODE) || 'ebenen' -const RootApp = mode === 'gestaltung' ? GestaltungApp - : mode === 'ausschnitte' ? AusschnitteApp - : mode === 'massstab' ? MassstabApp - : mode === 'werkzeuge' ? WerkzeugeApp - : mode === 'oberleiste' ? OberleisteApp - : mode === 'overrides' ? OverridesApp - : mode === 'dimensionen' ? DimensionenApp - : mode === 'layouts' ? LayoutsApp - : mode === 'elemente' ? ElementeApp +const RootApp = mode === 'gestaltung' ? GestaltungApp + : mode === 'ausschnitte' ? AusschnitteApp + : mode === 'massstab' ? MassstabApp + : mode === 'werkzeuge' ? WerkzeugeApp + : mode === 'oberleiste' ? OberleisteApp + : mode === 'overrides' ? OverridesApp + : mode === 'dimensionen' ? DimensionenApp + : mode === 'layouts' ? LayoutsApp + : mode === 'elemente' ? ElementeApp + : mode === 'zeichnungsebenen' ? ZeichnungsebenenApp : App window.onerror = function (msg, src, line, col, err) {