9dc191be4f
OpenStudio-Suite Architektur-Plugin fuer Rhino 8 (Mac): - Smart-Elemente: Wand, Decke, Dach (Pult/Sattel/Walm/Mansarde), Oeffnungen (Fenster/Tueren mit Rahmen + Sims + Glas + Fluegel), Treppen (gerade · L · Wendel mit Schrittmass-Validierung) - Live-Previews mit Step-Lines + Soll-Range-Clamping - Bidirektionale Selection-Sync zwischen Source-Linie und Volume - Geschoss-/Ebenen-Verwaltung mit OKFF-Persistenz - Layouts mit PDF-Export - Ausschnitte / Massstab / Override-Regeln - Petrol-Gruen Theme (Rapport-konform) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
274 lines
12 KiB
React
274 lines
12 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', () => {
|
|
applyAll(INITIAL_ZEICHNUNGSEBENEN, INITIAL_EBENEN)
|
|
setAppliedZ(INITIAL_ZEICHNUNGSEBENEN)
|
|
setAppliedE(INITIAL_EBENEN)
|
|
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('|')
|
|
}
|
|
const structureKey = useMemo(() => (
|
|
zeichnungsebenen.map(z => `${z.id}:${z.name}:${z.isGeschoss ? 1 : 0}`).join(',') + '|' +
|
|
ebenen.map(e => `${e.code}:${e.name}:${fillSig(e)}`).join(',')
|
|
), [zeichnungsebenen, ebenen])
|
|
|
|
const appliedStructureKey = useMemo(() => (
|
|
appliedZ.map(z => `${z.id}:${z.name}:${z.isGeschoss ? 1 : 0}`).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}" ueberschreiben?`)) 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>
|
|
)
|
|
}
|