Ebenen-Panel in zwei UI-Panels splitten: Zeichnungsebenen + Ebenen

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 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 23:42:02 +02:00
parent 53fea10cba
commit a458b4c47d
4 changed files with 247 additions and 137 deletions
+19 -100
View File
@@ -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 (
<div style={{
display: 'flex', flexDirection: 'column',
@@ -233,15 +161,6 @@ export default function App() {
position: 'relative',
}}>
<div style={{ flex: 1, overflowY: 'auto' }}>
<GeschossManager
zeichnungsebenen={zeichnungsebenen}
activeId={activeId}
onActiveChange={handleActiveChange}
onChange={(updated) => setZeichnungsebenen(recalcOkff(updated))}
recalcOkff={recalcOkff}
mode={zMode}
onModeChange={setZMode}
/>
<EbenenManager
ebenen={ebenen}
activeCode={activeCode}
+125
View File
@@ -0,0 +1,125 @@
import { useState, useEffect, useMemo } from 'react'
import GeschossManager from './components/GeschossManager'
import {
applyAll, setActiveZeichnungsebene,
onMessage, notifyReady, applyVisibility,
} 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 },
])
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 (
<div style={{
display: 'flex', flexDirection: 'column',
height: '100vh', overflow: 'hidden',
background: 'var(--bg-base)',
position: 'relative',
}}>
<div style={{ flex: 1, overflowY: 'auto' }}>
<GeschossManager
zeichnungsebenen={zeichnungsebenen}
activeId={activeId}
onActiveChange={handleActiveChange}
onChange={(updated) => setZeichnungsebenen(recalcOkff(updated))}
recalcOkff={recalcOkff}
mode={zMode}
onModeChange={setZMode}
/>
</div>
</div>
)
}
+11 -9
View File
@@ -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) {