Elemente: BIM Project Browser + Properties-Satellite-Window

Zwei neue Satellite-Windows (analog Kamera/Text-Editor):

1) Projekt-Übersicht (elemente_uebersicht.py + ElementeUebersichtApp.jsx)
   - Tree Geschoss → Kind → Element-Instanzen
   - Suche + Kind-Filter-Chips
   - Klick = selektieren in Rhino, Shift+Klick = zoomen
   - Erreichbar via account_tree-Button im Elemente-Panel-Header

2) Properties-Satellite (elemente_properties.py + ElementePropertiesApp.jsx)
   - Eigenes Fenster mit der PropertiesView (gemeinsame Komponente)
   - Live-Updates: elemente._send_state forwarded zu satellite-bridge via sticky
   - Erreichbar via open_in_new-Icon oben rechts in der Properties-Karte
   - Inline-Properties im Panel bleiben — Satellite ist für mehr Platz

Plus ElementeApp-Cleanup:
- ElementList (alle Elemente-Liste) raus — wird jetzt von Projekt-
  Übersicht abgedeckt.
- Properties springen bei Selektion nach oben, NeuesElement bleibt
  voll sichtbar darunter (kein Scrollen mehr).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-22 01:17:31 +02:00
parent d5bcee2157
commit 15fb0a6037
8 changed files with 782 additions and 241 deletions
+164 -239
View File
@@ -1,13 +1,13 @@
import { useEffect, useRef, useState } from 'react'
import Icon from './components/Icon'
import { BarToggle, BarButton } from './components/BarControls'
import {
onMessage, notifyReady,
listElemente, createWall, createDecke, createDach,
createWall, createDecke, createDach,
createFenster, createTuer, createAussparung, createTreppe,
createStuetze, createTraeger, createRaum,
exportRaeume,
openSwisstopo, openSwisstopoDialog, openOsmDialog,
updateElement, deleteElement, regenerateAllElements,
updateElement, deleteElement, openElementeUebersicht, openElementeProperties,
} from './lib/rhinoBridge'
const labelXs = {
@@ -29,17 +29,15 @@ function ReferenzSelector({ value, onChange }) {
{ code: 'right', label: 'Rechts', hint: 'Achse auf rechter Aussenseite' },
]
return (
<div style={{ flex: 1, display: 'flex', gap: 2 }}>
<div style={{ flex: 1, display: 'flex', gap: 3 }}>
{opts.map(o => (
<button
<BarToggle
key={o.code}
label={o.label}
active={value === o.code}
onClick={() => onChange(o.code)}
className={value === o.code ? 'btn-contained' : 'btn-outlined'}
style={{ flex: 1, padding: '3px 6px', fontSize: 10 }}
title={o.hint}
>
{o.label}
</button>
/>
))}
</div>
)
@@ -213,10 +211,6 @@ function ElementList({ elements }) {
return (
<div style={{
display: 'flex', flexDirection: 'column', gap: 8,
padding: 10,
background: 'var(--bg-section)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
}}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<span style={{ ...labelXs, flex: 1 }}>Alle Elemente</span>
@@ -328,7 +322,7 @@ function ElementListRow({ el, meta }) {
}
function NeuesElementSection({ noGeschoss, activeName }) {
function NeuesElementSection({ noGeschoss, activeName, elementsCount }) {
const [treppeMenuOpen, setTreppeMenuOpen] = useState(false)
const [stuetzeMenuOpen, setStuetzeMenuOpen] = useState(false)
const [traegerMenuOpen, setTraegerMenuOpen] = useState(false)
@@ -380,13 +374,17 @@ function NeuesElementSection({ noGeschoss, activeName }) {
return (
<div style={{
display: 'flex', flexDirection: 'column', gap: 10,
padding: 10, marginBottom: 8,
background: 'var(--bg-section)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
marginBottom: 8,
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 10 }}>
<span style={labelXs}>Neues Element</span>
<BarToggle
icon="account_tree"
label={`Projektübersicht${elementsCount > 0 ? ' · ' + elementsCount : ''}`}
onClick={() => openElementeUebersicht()}
disabled={elementsCount === 0}
title={elementsCount > 0
? `Projektübersicht öffnen — ${elementsCount} Elemente`
: 'Noch keine Elemente vorhanden'} />
<div style={{ flex: 1 }} />
<span style={{ color: noGeschoss ? 'var(--danger)' : 'var(--text-muted)' }}>
{noGeschoss ? 'Kein Geschoss aktiv' : 'auf'}
@@ -494,6 +492,39 @@ function NeuesElementSection({ noGeschoss, activeName }) {
}
// PropertiesView: gemeinsame Komponente, rendert die passende Property-
// Form je nach Element-Typ. Wiederverwendbar in Inline + Satellite-Window.
export function PropertiesView({ selected, geschosse, materials, hatchPatterns }) {
if (!selected) return null
const upd = (p) => updateElement(selected.id, p)
const del = (label) => () => { if (window.confirm(`${label} löschen?`)) deleteElement(selected.id) }
if (selected.kind === 'wand')
return <WallProperties wall={selected} geschosse={geschosse} materials={materials || []}
onUpdate={upd} onDelete={del('Wand')} />
if (selected.kind === 'decke')
return <DeckenProperties decke={selected} geschosse={geschosse}
onUpdate={upd} onDelete={del('Decke')} />
if (selected.kind === 'dach')
return <DachProperties dach={selected} geschosse={geschosse}
onUpdate={upd} onDelete={del('Dach')} />
if (selected.kind === 'treppe')
return <TreppeProperties treppe={selected} geschosse={geschosse}
onUpdate={upd} onDelete={del('Treppe')} />
if (selected.kind === 'stuetze' || selected.kind === 'traeger') {
const lbl = (KIND_META[selected.kind] || {}).label || 'Element'
return <TragwerkProperties el={selected} onUpdate={upd} onDelete={del(lbl)} />
}
if (selected.kind === 'raum')
return <RaumProperties raum={selected} geschosse={geschosse}
hatchPatterns={hatchPatterns} onUpdate={upd} onDelete={del('Raum')} />
if (selected.kind === 'aussparung')
return <AussparungProperties aussp={selected} onDelete={del('Aussparung')} />
// fenster/tuer
return <OeffnungProperties oeff={selected} onUpdate={upd}
onDelete={del(selected.kind === 'fenster' ? 'Fenster' : 'Tür')} />
}
export default function ElementeApp() {
const [state, setState] = useState({
elements: [], geschosse: [], selection: null,
@@ -520,110 +551,29 @@ export default function ElementeApp() {
background: 'var(--bg-base)', color: 'var(--text-primary)',
fontFamily: 'var(--font)', fontSize: 11,
}}>
{/* Header */}
<div style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '8px 10px', borderBottom: '1px solid var(--border)',
flexShrink: 0,
}}>
<span style={{ flex: 1, fontWeight: 600 }}>Elemente</span>
<span className="chip" style={{ fontSize: 8 }}>{elements.length}</span>
<button
onClick={() => exportRaeume()}
className="btn-icon-tonal"
disabled={!elements.some(e => e.kind === 'raum')}
title="Raumliste als CSV exportieren"
>
<Icon name="download" size={14} />
</button>
<button
onClick={() => regenerateAllElements()}
className="btn-icon-tonal"
disabled={elements.length === 0}
title="Alle Elemente neu generieren (z.B. nach Geschoss-Änderung)"
>
<Icon name="sync" size={14} />
</button>
<button
onClick={() => listElemente()}
className="btn-icon-tonal"
title="Aktualisieren"
>
<Icon name="refresh" size={14} />
</button>
</div>
<div style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden', padding: 8 }}>
{/* Element-Toolbar: kategorisierte Pills */}
{/* Bei Selektion: Properties OBEN, NeuesElement darunter.
Ohne Selektion: nur NeuesElement. Die volle Element-Liste
kommt jetzt aus der Projekt-Uebersicht (account_tree-Button). */}
{selected && (
<div style={{ position: 'relative' }}>
<div style={{ position: 'absolute', top: 8, right: 38, zIndex: 1 }}>
<BarButton icon="open_in_new"
onClick={() => openElementeProperties()}
title="Eigenschaften in eigenem Fenster öffnen" />
</div>
<PropertiesView
selected={selected}
geschosse={geschosse}
materials={state.materials || []}
hatchPatterns={state.hatchPatterns} />
</div>
)}
<NeuesElementSection
noGeschoss={noGeschoss}
activeName={activeName}
elementsCount={elements.length}
/>
{/* Properties */}
{selected ? (
selected.kind === 'wand' ? (
<WallProperties wall={selected} geschosse={geschosse}
materials={state.materials || []}
onUpdate={(p) => updateElement(selected.id, p)}
onDelete={() => { if (window.confirm('Wand löschen?')) deleteElement(selected.id) }} />
) : selected.kind === 'decke' ? (
<DeckenProperties decke={selected} geschosse={geschosse}
onUpdate={(p) => updateElement(selected.id, p)}
onDelete={() => { if (window.confirm('Decke löschen?')) deleteElement(selected.id) }} />
) : selected.kind === 'dach' ? (
<DachProperties dach={selected} geschosse={geschosse}
onUpdate={(p) => updateElement(selected.id, p)}
onDelete={() => { if (window.confirm('Dach löschen?')) deleteElement(selected.id) }} />
) : selected.kind === 'treppe' ? (
<TreppeProperties treppe={selected} geschosse={geschosse}
onUpdate={(p) => updateElement(selected.id, p)}
onDelete={() => { if (window.confirm('Treppe löschen?')) deleteElement(selected.id) }} />
) : (selected.kind === 'stuetze' || selected.kind === 'traeger') ? (
<TragwerkProperties el={selected}
onUpdate={(p) => updateElement(selected.id, p)}
onDelete={() => {
const lbl = (KIND_META[selected.kind] || {}).label || 'Element'
if (window.confirm(`${lbl} löschen?`)) deleteElement(selected.id)
}} />
) : selected.kind === 'raum' ? (
<RaumProperties raum={selected} geschosse={geschosse}
hatchPatterns={state.hatchPatterns}
onUpdate={(p) => updateElement(selected.id, p)}
onDelete={() => { if (window.confirm('Raum löschen?')) deleteElement(selected.id) }} />
) : selected.kind === 'aussparung' ? (
<AussparungProperties aussp={selected}
onDelete={() => { if (window.confirm('Aussparung löschen?')) deleteElement(selected.id) }} />
) : (
<OeffnungProperties oeff={selected}
onUpdate={(p) => updateElement(selected.id, p)}
onDelete={() => {
const label = selected.kind === 'fenster' ? 'Fenster' : 'Tür'
if (window.confirm(`${label} löschen?`)) deleteElement(selected.id)
}} />
)
) : (
<div style={{
padding: '20px 16px', textAlign: 'center',
color: 'var(--text-muted)', fontSize: 11,
border: '1px dashed var(--border)',
borderRadius: 'var(--r-lg)',
background: 'var(--bg-section)',
marginBottom: 8,
}}>
<Icon name="touch_app" size={24} style={{ color: 'var(--text-muted)', opacity: 0.5 }} />
<div style={{ marginTop: 6 }}>Kein Element selektiert.</div>
<div style={{ marginTop: 4, fontSize: 10 }}>
Eine Wand-Achse oder Decken-Outline in Rhino auswählen.
</div>
</div>
)}
{/* Liste aller Elemente */}
{elements.length > 0 && (
<ElementList elements={elements} />
)}
</div>
</div>
)
@@ -665,7 +615,7 @@ function TragwerkProperties({ el, onUpdate, onDelete }) {
display: 'flex', flexDirection: 'column', gap: 8,
padding: 10, marginBottom: 8,
background: 'var(--bg-section)',
border: '1px solid var(--accent)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
}}>
<div style={{ display: 'flex', alignItems: 'center' }}>
@@ -681,7 +631,7 @@ function TragwerkProperties({ el, onUpdate, onDelete }) {
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 60 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>
Profil
</span>
<select value={profil}
@@ -715,7 +665,7 @@ function TragwerkProperties({ el, onUpdate, onDelete }) {
onCommit={(v) => onUpdate({ angle: v })} />
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 60 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>
{isStuetze ? 'Höhe' : 'OK über UK'}
</span>
<input type="text" value={zRaw}
@@ -767,7 +717,7 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns })
display: 'flex', flexDirection: 'column', gap: 8,
padding: 10, marginBottom: 8,
background: 'var(--bg-section)',
border: '1px solid var(--accent)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
}}>
<div style={{ display: 'flex', alignItems: 'center' }}>
@@ -783,7 +733,7 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns })
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 60 }}>Geschoss</span>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Geschoss</span>
<select value={raum.geschoss}
onChange={(e) => onUpdate({ geschoss: e.target.value })}
style={{ flex: 1, fontSize: 11 }}>
@@ -792,7 +742,7 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns })
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 60 }}>Nummer</span>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Nummer</span>
<input type="text" value={nummer}
onChange={(e) => setNummer(e.target.value)}
onBlur={() => {
@@ -804,7 +754,7 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns })
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 60 }}>Name</span>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Name</span>
<input type="text" value={name}
onChange={(e) => setName(e.target.value)}
onBlur={() => {
@@ -817,7 +767,7 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns })
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 60 }}>Typ</span>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Typ</span>
<select value={raum.sia || ''}
onChange={(e) => onUpdate({ sia: e.target.value })}
title="SIA 416 Flaechenklassifikation"
@@ -832,7 +782,7 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns })
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 60 }}>Füllung</span>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Füllung</span>
<select value={fuell}
onChange={(e) => onUpdate({ fuellung: e.target.value })}
title="Hatch-Pattern im Normalmodus. Bei aktivem SIA-Modus wird klassifizierten Raeumen automatisch Solid forciert."
@@ -847,7 +797,7 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns })
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 60 }}>Rundung</span>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Rundung</span>
<select value={raum.rundung || ''}
onChange={(e) => onUpdate({ rundung: e.target.value })}
style={{ flex: 1, fontSize: 11 }}
@@ -862,24 +812,20 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns })
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 60 }}>Ausrichtung</span>
<div style={{ flex: 1, display: 'flex', gap: 2 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Ausrichtung</span>
<div style={{ flex: 1, display: 'flex', gap: 3 }}>
{RAUM_ALIGN.map(a => (
<button key={a.code}
<BarToggle key={a.code}
icon={a.icon}
active={(raum.align || 'mid') === a.code}
onClick={() => onUpdate({ align: a.code })}
className={(raum.align || 'mid') === a.code ? 'btn-contained' : 'btn-outlined'}
style={{ flex: 1, padding: '3px 4px', fontSize: 10,
display: 'flex', alignItems: 'center',
justifyContent: 'center', gap: 3 }}
title={a.label}>
<Icon name={a.icon} size={12} />
</button>
title={a.label} />
))}
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 60 }}>Texthöhe</span>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Texthöhe</span>
<input type="text" value={txtH}
onChange={(e) => setTxtH(e.target.value)}
onBlur={() => {
@@ -912,7 +858,7 @@ function AussparungProperties({ aussp, onDelete }) {
display: 'flex', flexDirection: 'column', gap: 8,
padding: 10, marginBottom: 8,
background: 'var(--bg-section)',
border: '1px solid var(--accent)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
}}>
<div style={{ display: 'flex', alignItems: 'center' }}>
@@ -964,7 +910,7 @@ function WallProperties({ wall, geschosse, materials, onUpdate, onDelete }) {
display: 'flex', flexDirection: 'column', gap: 8,
padding: 10, marginBottom: 8,
background: 'var(--bg-section)',
border: '1px solid var(--accent)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
}}>
<div style={{ display: 'flex', alignItems: 'center' }}>
@@ -978,7 +924,7 @@ function WallProperties({ wall, geschosse, materials, onUpdate, onDelete }) {
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Geschoss</span>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Geschoss</span>
<select value={wall.geschoss}
onChange={(e) => onUpdate({ geschoss: e.target.value })}
style={{ flex: 1, fontSize: 11 }}>
@@ -987,25 +933,20 @@ function WallProperties({ wall, geschosse, materials, onUpdate, onDelete }) {
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Aufbau</span>
<div style={{ flex: 1, display: 'flex', gap: 2 }}>
<button
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Aufbau</span>
<div style={{ flex: 1, display: 'flex', gap: 3 }}>
<BarToggle label="Solid" active={!wall.layered}
onClick={() => onUpdate({ layered: false })}
className={!wall.layered ? 'btn-contained' : 'btn-outlined'}
style={{ flex: 1, padding: '3px 6px', fontSize: 10 }}
title="Eine homogene Wand-Schicht (Standard)">Solid</button>
<button
title="Eine homogene Wand-Schicht (Standard)" />
<BarToggle label="Mehrschichtig" active={wall.layered}
onClick={() => onUpdate({ layered: true })}
className={wall.layered ? 'btn-contained' : 'btn-outlined'}
style={{ flex: 1, padding: '3px 6px', fontSize: 10 }}
title="Mehrere Schichten mit individuellen Dicken und Farben"
>Mehrschichtig</button>
title="Mehrere Schichten mit individuellen Dicken und Farben" />
</div>
</div>
{!wall.layered && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Dicke</span>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Dicke</span>
<input type="text" value={dicke}
onChange={(e) => setDicke(e.target.value)}
onBlur={() => {
@@ -1025,7 +966,7 @@ function WallProperties({ wall, geschosse, materials, onUpdate, onDelete }) {
)}
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Referenz</span>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Referenz</span>
<ReferenzSelector value={wall.referenz || 'mid'}
onChange={(v) => onUpdate({ referenz: v })} />
</div>
@@ -1090,11 +1031,10 @@ function LayersEditor({ layers, onChange, materials }) {
onChange={(patch) => updateAt(i, patch)}
onRemove={() => removeAt(i)} />
))}
<button onClick={addLayer} className="btn-outlined"
style={{ padding: '3px 6px', fontSize: 10, marginTop: 2 }}
title="Neue Schicht unten anfügen">
+ Schicht
</button>
<div style={{ marginTop: 2 }}>
<BarToggle label="+ Schicht" onClick={addLayer}
title="Neue Schicht unten anfügen" />
</div>
</div>
)
}
@@ -1183,7 +1123,7 @@ function DeckenProperties({ decke, geschosse, onUpdate, onDelete }) {
display: 'flex', flexDirection: 'column', gap: 8,
padding: 10, marginBottom: 8,
background: 'var(--bg-section)',
border: '1px solid var(--accent)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
}}>
<div style={{ display: 'flex', alignItems: 'center' }}>
@@ -1197,7 +1137,7 @@ function DeckenProperties({ decke, geschosse, onUpdate, onDelete }) {
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Geschoss</span>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Geschoss</span>
<select value={decke.geschoss}
onChange={(e) => onUpdate({ geschoss: e.target.value })}
style={{ flex: 1, fontSize: 11 }}>
@@ -1206,7 +1146,7 @@ function DeckenProperties({ decke, geschosse, onUpdate, onDelete }) {
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Dicke</span>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Dicke</span>
<input type="text" value={dicke}
onChange={(e) => setDicke(e.target.value)}
onBlur={() => {
@@ -1259,7 +1199,7 @@ function DachProperties({ dach, geschosse, onUpdate, onDelete }) {
display: 'flex', flexDirection: 'column', gap: 8,
padding: 10, marginBottom: 8,
background: 'var(--bg-section)',
border: '1px solid var(--accent)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
}}>
<div style={{ display: 'flex', alignItems: 'center' }}>
@@ -1274,7 +1214,7 @@ function DachProperties({ dach, geschosse, onUpdate, onDelete }) {
{/* Dach-Typ */}
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Typ</span>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Typ</span>
<select value={dachTyp}
onChange={(e) => onUpdate({ dachTyp: e.target.value })}
style={{ flex: 1, fontSize: 11 }}>
@@ -1286,7 +1226,7 @@ function DachProperties({ dach, geschosse, onUpdate, onDelete }) {
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Geschoss</span>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Geschoss</span>
<select value={dach.geschoss}
onChange={(e) => onUpdate({ geschoss: e.target.value })}
style={{ flex: 1, fontSize: 11 }}>
@@ -1295,7 +1235,7 @@ function DachProperties({ dach, geschosse, onUpdate, onDelete }) {
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Dicke</span>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Dicke</span>
<input type="text" value={dicke}
onChange={(e) => setDicke(e.target.value)}
onBlur={() => {
@@ -1308,7 +1248,7 @@ function DachProperties({ dach, geschosse, onUpdate, onDelete }) {
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Neigung</span>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Neigung</span>
<input type="text" value={neigung}
onChange={(e) => setNeigung(e.target.value)}
onBlur={() => {
@@ -1323,7 +1263,7 @@ function DachProperties({ dach, geschosse, onUpdate, onDelete }) {
{dachTyp === 'pult' && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
title="Index der Traufkante (0 = Kante zwischen 1. und 2. Punkt)">
Traufe
</span>
@@ -1344,7 +1284,7 @@ function DachProperties({ dach, geschosse, onUpdate, onDelete }) {
{dachTyp === 'mansarde' && (
<>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Variante</span>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Variante</span>
<select value={dach.dachVariante || 'walm'}
onChange={(e) => onUpdate({ dachVariante: e.target.value })}
style={{ flex: 1, fontSize: 11 }}>
@@ -1379,7 +1319,7 @@ function MansardeFields({ dach, onUpdate }) {
return (
<>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
title="Untere (steile) Neigung — bis zum Knick">
Steil
</span>
@@ -1395,7 +1335,7 @@ function MansardeFields({ dach, onUpdate }) {
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>°</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
title="Höhe über Traufe wo der Knick sitzt">
Knick H
</span>
@@ -1577,7 +1517,7 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
display: 'flex', flexDirection: 'column', gap: 8,
padding: 10, marginBottom: 8,
background: 'var(--bg-section)',
border: '1px solid var(--accent)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
}}>
<div style={{ display: 'flex', alignItems: 'center' }}>
@@ -1591,7 +1531,7 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Start</span>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Start</span>
<select value={treppe.geschoss}
onChange={(e) => onUpdate({ geschoss: e.target.value })}
style={{ flex: 1, fontSize: 11 }}>
@@ -1600,7 +1540,7 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Ziel</span>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Ziel</span>
<select
value={hasHOver ? '__custom__' : (treppe.geschossEnd || '')}
onChange={(e) => {
@@ -1621,7 +1561,7 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Breite</span>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Breite</span>
<input type="text" value={breite}
onChange={(e) => setBreite(e.target.value)}
onBlur={() => {
@@ -1635,7 +1575,7 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Stufen</span>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Stufen</span>
<input type="text" value={nStufen}
onChange={(e) => setNStufen(e.target.value)}
onBlur={() => {
@@ -1649,15 +1589,13 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Lage</span>
<div style={{ flex: 1, display: 'flex', gap: 2 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Lage</span>
<div style={{ flex: 1, display: 'flex', gap: 3 }}>
{REF_OPTIONS.map(o => (
<button key={o.code}
onClick={() => onUpdate({ treppeReferenz: o.code })}
className={ref === o.code ? 'btn-contained' : 'btn-outlined'}
style={{ flex: 1, padding: '3px 6px', fontSize: 10 }}>
{o.label}
</button>
<BarToggle key={o.code}
label={o.label}
active={ref === o.code}
onClick={() => onUpdate({ treppeReferenz: o.code })} />
))}
</div>
</div>
@@ -1666,19 +1604,17 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
{/* Unterseite-Modus */}
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
title="Form der Treppen-Unterseite">
Unten
</span>
<div style={{ flex: 1, display: 'flex', gap: 2 }}>
<div style={{ flex: 1, display: 'flex', gap: 3 }}>
{MODUS_OPTIONS.map(o => (
<button key={o.code}
<BarToggle key={o.code}
label={o.label}
active={modus === o.code}
onClick={() => onUpdate({ treppeModus: o.code })}
className={modus === o.code ? 'btn-contained' : 'btn-outlined'}
style={{ flex: 1, padding: '3px 6px', fontSize: 10 }}
title={o.hint}>
{o.label}
</button>
title={o.hint} />
))}
</div>
</div>
@@ -1686,7 +1622,7 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
{/* Lauf-Plattendicke (nur fuer flach + plattenrand relevant) */}
{modus !== 'massiv' && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
title="Dicke der Lauf-Platte (Materialdicke unter den Stufen)">
Platte
</span>
@@ -1782,7 +1718,7 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
display: 'flex', flexDirection: 'column', gap: 8,
padding: 10, marginBottom: 8,
background: 'var(--bg-section)',
border: '1px solid var(--accent)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
}}>
<div style={{ display: 'flex', alignItems: 'center' }}>
@@ -1796,7 +1732,7 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Breite</span>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Breite</span>
<input type="text" value={breite}
onChange={(e) => setBreite(e.target.value)}
onBlur={() => commit('breite', breite, setBreite, oeff.breite)}
@@ -1806,7 +1742,7 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Höhe</span>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Höhe</span>
<input type="text" value={hoehe}
onChange={(e) => setHoehe(e.target.value)}
onBlur={() => commit('hoehe', hoehe, setHoehe, oeff.hoehe)}
@@ -1816,7 +1752,7 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
title={isFenster
? 'Brüstungshöhe über UK der Wand'
: 'Türschwelle / Höhe über UK der Wand'}>
@@ -1836,19 +1772,17 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
{/* Referenz-Lage: wo sitzt der Klick-Punkt in der Oeffnung */}
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
title="Lage des Klick-Punkts in der Öffnung">
Ref
</span>
<div style={{ flex: 1, display: 'flex', gap: 2 }}>
<div style={{ flex: 1, display: 'flex', gap: 3 }}>
{OEFF_REFERENZ_OPTIONS.map(o => (
<button key={o.code}
<BarToggle key={o.code}
label={o.label}
active={(oeff.oeffReferenz || 'mid') === o.code}
onClick={() => onUpdate({ oeffReferenz: o.code })}
className={(oeff.oeffReferenz || 'mid') === o.code ? 'btn-contained' : 'btn-outlined'}
style={{ flex: 1, padding: '3px 6px', fontSize: 10 }}
title={o.hint}>
{o.label}
</button>
title={o.hint} />
))}
</div>
</div>
@@ -1857,7 +1791,7 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
{/* Rahmen-Profil (Breite × Tiefe) — beide Felder gleich breit */}
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
title="Rahmen-Profil: Breite (in Wandflaeche) × Tiefe (entlang Wandnormale)">
Rahmen
</span>
@@ -1879,19 +1813,17 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
{/* Rahmen-Lage im Wandquerschnitt */}
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
title="Lage des Rahmens im Wandquerschnitt">
Lage
</span>
<div style={{ flex: 1, display: 'flex', gap: 2 }}>
<div style={{ flex: 1, display: 'flex', gap: 3 }}>
{RAHMEN_POS_OPTIONS.map(o => (
<button key={o.code}
<BarToggle key={o.code}
label={o.label}
active={rahmenPos === o.code}
onClick={() => onUpdate({ rahmenPos: o.code })}
className={rahmenPos === o.code ? 'btn-contained' : 'btn-outlined'}
style={{ flex: 1, padding: '3px 6px', fontSize: 10 }}
title={o.hint}>
{o.label}
</button>
title={o.hint} />
))}
</div>
</div>
@@ -1899,18 +1831,16 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
{/* Fluegel-Anzahl — nur fuer Fenster (Tueren haben ein einzelnes Tuerblatt) */}
{isFenster && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
title="Anzahl Flügel (vertikale Unterteilung)">
Flügel
</span>
<div style={{ flex: 1, display: 'flex', gap: 2 }}>
<div style={{ flex: 1, display: 'flex', gap: 3 }}>
{[1, 2, 3, 4].map(n => (
<button key={n}
onClick={() => onUpdate({ fluegel: n })}
className={fluegel === n ? 'btn-contained' : 'btn-outlined'}
style={{ flex: 1, padding: '3px 6px', fontSize: 10 }}>
{n}
</button>
<BarToggle key={n}
label={String(n)}
active={fluegel === n}
onClick={() => onUpdate({ fluegel: n })} />
))}
</div>
</div>
@@ -1920,7 +1850,7 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
{isFenster && (
<>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
title="Aussensims — Platte unter Öffnung, ragt aussen heraus">
Sims a.
</span>
@@ -1932,7 +1862,7 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
</select>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
title="Innensims — Platte unter Öffnung, ragt innen heraus">
Sims i.
</span>
@@ -1948,13 +1878,11 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
{/* Glas-Toggle: bei Tueren ersetzt Glas das Tuerblatt (verglaste Tuer) */}
<div style={{ display: 'flex', gap: 4 }}>
<button
<BarToggle
label={(isFenster ? 'Glas' : 'Verglast') + (oeff.glas ? ' ✓' : '')}
active={!!oeff.glas}
onClick={() => onUpdate({ glas: !oeff.glas })}
className={oeff.glas ? 'btn-contained' : 'btn-outlined'}
style={{ flex: 1, padding: '3px 6px', fontSize: 10 }}
title={isFenster ? 'Glasscheibe sichtbar' : 'Verglaste Tür (statt Türblatt)'}>
{isFenster ? 'Glas' : 'Verglast'} {oeff.glas ? '✓' : ''}
</button>
title={isFenster ? 'Glasscheibe sichtbar' : 'Verglaste Tür (statt Türblatt)'} />
</div>
</div>
)
@@ -1965,13 +1893,10 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
function AutoOverrideField({ label, auto, autoValue, rawValue, onChangeRaw, onToggle, onCommit }) {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>{label}</span>
<button onClick={onToggle}
className={auto ? 'btn-contained' : 'btn-outlined'}
style={{ padding: '3px 8px', fontSize: 10 }}
title={auto ? 'Folgt Geschoss' : 'Eigener Wert'}>
{auto ? 'Auto' : 'Custom'}
</button>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>{label}</span>
<BarToggle label={auto ? 'Auto' : 'Custom'} active={auto}
onClick={onToggle}
title={auto ? 'Folgt Geschoss' : 'Eigener Wert'} />
<input type="text"
value={auto ? fmtNum(autoValue) : rawValue}
disabled={auto}
+51
View File
@@ -0,0 +1,51 @@
import { useEffect, useState } from 'react'
import Icon from './components/Icon'
import { onMessage, notifyReady } from './lib/rhinoBridge'
import ElementeApp, { PropertiesView } from './ElementeApp.jsx'
// Satellite-Window — zeigt nur die Properties des aktuell selektierten
// Elements in einem groesseren Fenster. Daten kommen vom ElementeBridge
// (via STATE-Forward in elemente.py).
export default function ElementePropertiesApp() {
const [state, setState] = useState({
elements: [], geschosse: [], selection: null,
materials: [], hatchPatterns: [],
})
useEffect(() => {
onMessage('STATE', (s) => setState(prev => ({ ...prev, ...s })))
notifyReady()
}, [])
const elements = state.elements || []
const selected = elements.find(el => el.id === state.selection)
return (
<div style={{
display: 'flex', flexDirection: 'column',
height: '100vh', overflow: 'hidden',
background: 'var(--bg-base)', color: 'var(--text-primary)',
fontFamily: 'var(--font)', fontSize: 11,
}}>
<div style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden', padding: 12 }}>
{selected ? (
<PropertiesView
selected={selected}
geschosse={state.geschosse || []}
materials={state.materials || []}
hatchPatterns={state.hatchPatterns}
/>
) : (
<div style={{
padding: 60, textAlign: 'center',
color: 'var(--text-muted)',
display: 'flex', flexDirection: 'column', gap: 10, alignItems: 'center',
}}>
<Icon name="touch_app" size={32} style={{ color: 'var(--text-muted)', opacity: 0.5 }} />
<div>Kein Element selektiert.</div>
<div style={{ fontSize: 10 }}>Im Viewport ein Element wählen.</div>
</div>
)}
</div>
</div>
)
}
+283
View File
@@ -0,0 +1,283 @@
import { useEffect, useState, useMemo } from 'react'
import Icon from './components/Icon'
import { BarToggle, BarButton } from './components/BarControls'
import { onMessage, notifyReady, send } from './lib/rhinoBridge'
// BIM-artige Project-Tree-Ansicht: Geschoss → Kind → Element.
// Klick → selektiert in Rhino. Shift-Klick → Zoom-to-Element.
const KIND_ORDER = [
'wand', 'decke', 'dach', 'fenster', 'tuer', 'aussparung',
'treppe', 'stuetze', 'traeger', 'raum',
]
const KIND_META = {
wand: { icon: 'view_week', label: 'Wände', color: '#888888' },
decke: { icon: 'layers', label: 'Decken', color: '#605850' },
dach: { icon: 'roofing', label: 'Dächer', color: '#7a4a3a' },
fenster: { icon: 'window', label: 'Fenster', color: '#5080c8' },
tuer: { icon: 'sensor_door', label: 'Türen', color: '#5080c8' },
aussparung: { icon: 'rectangle', label: 'Aussparungen', color: '#a89070' },
treppe: { icon: 'stairs', label: 'Treppen', color: '#c87050' },
stuetze: { icon: 'square_foot', label: 'Stützen', color: '#c87050' },
traeger: { icon: 'horizontal_rule', label: 'Träger', color: '#a87858' },
raum: { icon: 'crop_free', label: 'Räume', color: '#5fa896' },
}
export default function ElementeUebersichtApp() {
const [state, setState] = useState({ geschosse: [], items: [] })
const [expanded, setExpanded] = useState({}) // { 'g_id': true, 'g_id::kind': true }
const [filter, setFilter] = useState('') // text search
const [filterKind, setFilterKind] = useState('') // single kind filter
useEffect(() => {
onMessage('STATE', (s) => setState(s || { geschosse: [], items: [] }))
notifyReady()
}, [])
const items = state.items || []
const geschosse = state.geschosse || []
const filtered = useMemo(() => {
let r = items
if (filterKind) r = r.filter(it => it.kind === filterKind)
if (filter.trim()) {
const q = filter.toLowerCase()
r = r.filter(it =>
(it.name || '').toLowerCase().includes(q) ||
(it.info || '').toLowerCase().includes(q) ||
it.kind.toLowerCase().includes(q))
}
return r
}, [items, filter, filterKind])
// Pre-grouped: g_id -> kind -> [items]
const tree = useMemo(() => {
const out = {}
for (const it of filtered) {
const g = it.geschossId || '__keingeschoss__'
const k = it.kind
if (!out[g]) out[g] = {}
if (!out[g][k]) out[g][k] = []
out[g][k].push(it)
}
return out
}, [filtered])
// Counts per kind across all (unfiltered) items — für Filter-Chips
const kindCounts = useMemo(() => {
const m = {}
for (const it of items) m[it.kind] = (m[it.kind] || 0) + 1
return m
}, [items])
const toggle = (key) => setExpanded(s => ({ ...s, [key]: !s[key] }))
const expandAll = () => {
const next = {}
for (const g of geschosse) {
next[g.id] = true
for (const k of KIND_ORDER) {
if (tree[g.id]?.[k]) next[g.id + '::' + k] = true
}
}
setExpanded(next)
}
const collapseAll = () => setExpanded({})
const onSelect = (item, ev) => {
if (ev.shiftKey) {
send('ZOOM_TO_ELEMENT', { objectId: item.objectId })
} else {
send('SELECT_ELEMENT', { objectId: item.objectId })
}
}
const totalCount = items.length
const filteredCount = filtered.length
return (
<div style={{
display: 'flex', flexDirection: 'column',
height: '100vh', overflow: 'hidden',
background: 'var(--bg-base)', color: 'var(--text-primary)',
fontFamily: 'var(--font)', fontSize: 11,
}}>
{/* Toolbar */}
<div style={{
display: 'flex', flexDirection: 'column', gap: 6,
padding: '8px 10px',
borderBottom: '1px solid var(--border)',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<input type="text" placeholder="Suchen…"
value={filter}
onChange={(e) => setFilter(e.target.value)}
style={{
flex: 1, fontSize: 11,
padding: '4px 10px',
background: 'var(--bg-input)',
border: '1px solid var(--border)',
borderRadius: 999,
color: 'var(--text-primary)',
outline: 'none',
}} />
<BarButton icon="unfold_more" onClick={expandAll} title="Alle aufklappen" />
<BarButton icon="unfold_less" onClick={collapseAll} title="Alle einklappen" />
</div>
{/* Kind-Filter Chips */}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 3 }}>
<BarToggle label={`Alle ${totalCount > 0 ? '· ' + totalCount : ''}`}
active={!filterKind}
onClick={() => setFilterKind('')} />
{KIND_ORDER.filter(k => kindCounts[k]).map(k => {
const meta = KIND_META[k]
return (
<BarToggle key={k}
icon={meta.icon}
label={`${meta.label} ${kindCounts[k]}`}
active={filterKind === k}
onClick={() => setFilterKind(filterKind === k ? '' : k)} />
)
})}
</div>
</div>
{/* Tree */}
<div style={{ flex: 1, overflowY: 'auto' }}>
{totalCount === 0 ? (
<div style={{
padding: 40, textAlign: 'center',
color: 'var(--text-muted)',
display: 'flex', flexDirection: 'column', gap: 8, alignItems: 'center',
}}>
<Icon name="inventory_2" size={32} style={{ color: 'var(--text-muted)', opacity: 0.5 }} />
<div>Keine Elemente im Projekt.</div>
<div style={{ fontSize: 10 }}>Wände, Decken, Türen etc. via Elemente-Panel anlegen.</div>
</div>
) : (
geschosse.map(g => {
const groupForG = tree[g.id] || {}
const total = Object.values(groupForG).reduce((s, arr) => s + arr.length, 0)
if (total === 0) return null
const gOpen = expanded[g.id] !== false // default: open
return (
<div key={g.id}>
<div onClick={() => toggle(g.id)}
style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '6px 10px',
background: 'var(--bg-section)',
borderBottom: '1px solid var(--border)',
cursor: 'pointer', userSelect: 'none',
}}>
<Icon name={gOpen ? 'expand_more' : 'chevron_right'}
size={14} style={{ color: 'var(--text-muted)' }} />
<span style={{
fontSize: 11, fontWeight: 600,
color: 'var(--text-primary)',
letterSpacing: '0.04em', textTransform: 'uppercase',
}}>{g.name}</span>
{g.okff != null && (
<span style={{
fontSize: 9, color: 'var(--text-muted)',
fontFamily: 'DM Mono, monospace',
}}>+{g.okff.toFixed(2)} m</span>
)}
<span style={{ flex: 1 }} />
<span style={{
fontSize: 10, color: 'var(--text-muted)',
fontFamily: 'DM Mono, monospace',
}}>{total}</span>
</div>
{gOpen && KIND_ORDER.map(k => {
const arr = groupForG[k]
if (!arr || arr.length === 0) return null
const meta = KIND_META[k]
const kKey = g.id + '::' + k
const kOpen = expanded[kKey] !== false // default: open
return (
<div key={kKey}>
<div onClick={() => toggle(kKey)}
style={{
display: 'flex', alignItems: 'center', gap: 5,
padding: '3px 14px',
background: 'var(--bg-item)',
borderBottom: '1px solid var(--border-light)',
cursor: 'pointer', userSelect: 'none',
}}>
<Icon name={kOpen ? 'expand_more' : 'chevron_right'}
size={11} style={{ color: 'var(--text-muted)' }} />
<Icon name={meta.icon} size={12} style={{ color: meta.color }} />
<span style={{
fontSize: 10, color: 'var(--text-muted)',
letterSpacing: '0.06em', textTransform: 'uppercase',
fontWeight: 600,
}}>{meta.label}</span>
<span style={{ flex: 1 }} />
<span style={{
fontSize: 9, color: 'var(--text-muted)',
fontFamily: 'DM Mono, monospace',
}}>{arr.length}</span>
</div>
{kOpen && arr.map((it, idx) => (
<div key={it.id}
onClick={(ev) => onSelect(it, ev)}
title="Klick: selektieren · Shift+Klick: zoomen"
style={{
display: 'flex', alignItems: 'center', gap: 8,
padding: '2px 14px 2px 30px',
background: it.selected ? 'var(--active-dim)' : 'transparent',
borderBottom: '1px solid var(--border-light)',
cursor: 'pointer',
minHeight: 22,
}}
onMouseEnter={(e) => {
if (!it.selected) e.currentTarget.style.background = 'var(--bg-item-hover)'
}}
onMouseLeave={(e) => {
if (!it.selected) e.currentTarget.style.background = 'transparent'
}}>
<span style={{
fontSize: 9, color: 'var(--text-muted)',
fontFamily: 'DM Mono, monospace',
minWidth: 24, textAlign: 'right',
}}>{String(idx + 1).padStart(2, '0')}</span>
<span style={{
flex: 1, minWidth: 0,
fontSize: 11,
color: it.selected ? 'var(--accent-light)' : 'var(--text-label)',
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>{it.name || meta.label}</span>
<span style={{
fontSize: 10, color: 'var(--text-muted)',
fontFamily: 'DM Mono, monospace',
}}>{it.info}</span>
</div>
))}
</div>
)
})}
</div>
)
})
)}
</div>
{/* Footer */}
<div style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '4px 10px',
background: 'var(--bg-section)',
borderTop: '1px solid var(--border)',
fontSize: 10, color: 'var(--text-muted)',
}}>
<span>{filteredCount} {filteredCount !== totalCount && `von ${totalCount}`} Elemente</span>
<span style={{ flex: 1 }} />
<span style={{ fontStyle: 'italic' }}>
Klick = selektieren · Shift+Klick = zoomen
</span>
</div>
</div>
)
}
+5
View File
@@ -170,6 +170,11 @@ export function setOverridesPreset(name) { send('SET_OVERRIDES_PRESET', { name:
export function saveOverridesPreset(name) { send('SAVE_OVERRIDES_PRESET', { name }) }
export function openOverridesPanel() { send('OPEN_OVERRIDES_PANEL', {}) }
export function openKameraPanel() { send('OPEN_KAMERA_PANEL', {}) }
export function openElementeUebersicht() { send('OPEN_ELEMENTE_UEBERSICHT', {}) }
export function openElementeProperties() { send('OPEN_ELEMENTE_PROPERTIES', {}) }
export function setSectionStyle(enabled, source, color, pattern, scale, rotation) {
send('SET_SECTION_STYLE', { enabled, source, color, pattern, scale, rotation })
}
export function openAbout() { send('OPEN_ABOUT', {}) }
export function createText() { send('CREATE_TEXT', {}) }
export function setTextSettings(settings) { send('SET_TEXT_SETTINGS', { settings }) }
+4
View File
@@ -24,6 +24,8 @@ import OverridesApp from './OverridesApp.jsx'
import DimensionenApp from './DimensionenApp.jsx'
import LayoutsApp from './LayoutsApp.jsx'
import ElementeApp from './ElementeApp.jsx'
import ElementeUebersichtApp from './ElementeUebersichtApp.jsx'
import ElementePropertiesApp from './ElementePropertiesApp.jsx'
const mode = (typeof window !== 'undefined' && window.PANEL_MODE) || 'ebenen'
const RootApp = mode === 'gestaltung' ? GestaltungApp
@@ -48,6 +50,8 @@ const RootApp = mode === 'gestaltung' ? GestaltungApp
: mode === 'masse_settings' ? MasseSettingsApp
: mode === 'about' ? AboutApp
: mode === 'text_editor' ? TextEditorApp
: mode === 'elemente_uebersicht' ? ElementeUebersichtApp
: mode === 'elemente_properties' ? ElementePropertiesApp
: App
window.onerror = function (msg, src, line, col, err) {