961b3c0396
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>
294 lines
13 KiB
React
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>
|
|
)
|
|
}
|