Initial commit — Dossier Rhino 8 Plugin
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>
This commit is contained in:
+273
@@ -0,0 +1,273 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user