Files
DOSSIER/src/App.jsx
T
karim 961b3c0396 Snapshot: Wand/Öffnung Multi-Surface-Select + Z-Drag + Brüstungs-Mitnahme
Stable working state after a long iteration session. The plugin now supports:
- Multi-Surface-Select für alle Element-Typen (Türen/Fenster/Treppen/Tragwerk)
- Wand-Z-Drag → unbound mode (UK/OK-Override, Wand vom Geschoss entkoppelt)
- Wand-Z-Drag nimmt verknüpfte Öffnungen mit (Brüstung += delta_z via Idle-Pfad)
- Öffnungs-XY-Drag snapt direktional auf Wand-Tangente
- Öffnungs-Z-Drag passt Brüstung an (Fenster sofort sync, Tür deferred)
- Wand-Delete kaskadiert Öffnungen (deferred via Idle, robust gegen _Rotate/_Move)
- Source-Cascade beim Öffnungs-Delete (deferred analog Wand-Kaskade)
- Listener-Cleanup robust gegen _reset_panels.py Reload (Refs in
  _dossier_runtime_event_refs gespeichert, vor Re-Install deregistriert)
- _count_same_id_type filtert IsDeleted (verhindert Source-Duplikat-Bug bei Move)
- Frontend: Brüstungs-Slider für Tür ("Schwelle"), Flügel-Block nur bei Fenster

Plus aus früherer Phase dieser Session:
- Dossier-Launcher Auto-Load via Rhinos StartupCommands-XML
- Default-Pfad zeigt auf gebundeltes startup.py (out-of-the-box für neue User)
- Splash-Window beim Plugin-Load mit native macOS rounded corners
- Diverse Launcher-Verbesserungen (Brüstungs-Default, tauri.conf, capabilities)

Known issue: bei Multi-Select-Move mit vielen Sub-Volumen kann sporadisch
"Unable to transform" auftreten (Rhinos Move-Operation kollidiert mit Wand-
Regen). Tür-spezifischer Defer-Pfad mildert das, Fenster läuft sync.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 01:50:45 +02:00

294 lines
13 KiB
React

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,
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 },
{ 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() {
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)
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', ({ 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)
}
}
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. Felder ohne `visible`/`locked`
// werden mit Defaults ergaenzt damit die UI-Komponenten keine undefineds
// sehen.
const useEbenen = (Array.isArray(defaultEbenen) && defaultEbenen.length)
? defaultEbenen.map(e => ({
visible: true, locked: false,
...e,
}))
: INITIAL_EBENEN
setEbenen(useEbenen)
applyAll(INITIAL_ZEICHNUNGSEBENEN, useEbenen)
setAppliedZ(INITIAL_ZEICHNUNGSEBENEN)
setAppliedE(useEbenen)
const active = INITIAL_ZEICHNUNGSEBENEN.find(zz => zz.id === activeId) || INITIAL_ZEICHNUNGSEBENEN[0]
if (active) {
setActiveZeichnungsebene(active)
if (activeCode) setActiveEbene(activeCode)
}
})
notifyReady()
// Initial Liste der Kombinationen holen
setTimeout(() => getCombination(), 200)
// Native Browser-Context-Menu global unterdruecken
const blockContext = (ev) => ev.preventDefault()
document.addEventListener('contextmenu', blockContext)
return () => document.removeEventListener('contextmenu', blockContext)
}, [])
// Sichtbarkeit live anwenden — bei relevanten Aenderungen
const visibilityKey = useMemo(() => (
activeId + '|' + activeCode + '|' + zMode + '|' + eMode + '|' +
zeichnungsebenen.map(z => `${z.id}:${z.visible !== false ? 1 : 0}`).join(',') + '|' +
ebenen.map(e => `${e.code}:${e.visible !== false ? 1 : 0}:${e.locked === true ? 1 : 0}`).join(',')
), [activeId, activeCode, zMode, eMode, zeichnungsebenen, ebenen])
useEffect(() => {
const activeZ = zeichnungsebenen.find(z => z.id === activeId)
if (activeZ) applyVisibility(activeZ, zeichnungsebenen, activeCode, ebenen, zMode, 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).
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])
const appliedStructureKey = useMemo(() => (
appliedZ.map(zSig).join(',') + '|' +
appliedE.map(e => `${e.code}:${e.name}:${fillSig(e)}`).join(',')
), [appliedZ, appliedE])
useEffect(() => {
if (structureKey === appliedStructureKey) return
const t = setTimeout(() => {
applyAll(zeichnungsebenen, ebenen)
setAppliedZ(zeichnungsebenen)
setAppliedE(ebenen)
}, 200)
return () => clearTimeout(t)
// 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
// 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,
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()
}
// 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',
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}
/>
<EbenenManager
ebenen={ebenen}
activeCode={activeCode}
onActiveChange={(code) => { setActiveCode(code); setActiveEbene(code) }}
onChange={setEbenen}
mode={eMode}
onModeChange={setEMode}
hatchPatterns={hatchPatterns}
combinations={combinations}
activeCombName={activeCombName}
onPickCombination={handlePickCombination}
onSaveCurrentCombination={handleSaveCurrentCombination}
onDeleteCombination={handleDeleteCombination}
onEditCombinations={handleOpenCombDialog}
onUserVisibilityChange={handleUserVisibilityChange}
/>
</div>
{combDialog && (
<AusschnittLayerDialog
snapName="Ebenenkombinationen bearbeiten"
layers={combDialog.layers}
presets={combDialog.presets}
onClose={() => 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)
}}
/>
)}
</div>
)
}