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 (
{/* Toolbar */}
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', }} />
{/* Kind-Filter Chips */}
0 ? '· ' + totalCount : ''}`} active={!filterKind} onClick={() => setFilterKind('')} /> {KIND_ORDER.filter(k => kindCounts[k]).map(k => { const meta = KIND_META[k] return ( setFilterKind(filterKind === k ? '' : k)} /> ) })}
{/* Tree */}
{totalCount === 0 ? (
Keine Elemente im Projekt.
Wände, Decken, Türen etc. via Elemente-Panel anlegen.
) : ( 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 (
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', }}> {g.name} {g.okff != null && ( +{g.okff.toFixed(2)} m )} {total}
{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 (
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', }}> {meta.label} {arr.length}
{kOpen && arr.map((it, idx) => (
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' }}> {String(idx + 1).padStart(2, '0')} {it.name || meta.label} {it.info}
))}
) })}
) }) )}
{/* Footer */}
{filteredCount} {filteredCount !== totalCount && `von ${totalCount}`} Elemente Klick = selektieren · Shift+Klick = zoomen
) }