Symbol-Funktion in Elemente-Panel (Phase S1+S2)

Schema (library.py):
- Item-Format erweitert: files2d + files3d (Backwards-compat zu 'files')
- _build_variant_block + _place_instance + Layer-Routing pro Variante
- import_item akzeptiert at_point + layer2d/layer3d
- _ensure_block_definition mit variant-Suffix (dossier_lib_<id>_2d/_3d)

Backend (elemente.py):
- _layer_path_symbole(geschoss_name, variant) → <geschoss>::40_SYMBOLE::
  SYMBOLE_2D bzw. SYMBOLE_3D
- Default-Ebene 40 SYMBOLE via _find_ebene_sublayer_name
- LIST_LIBRARY-Handler: sendet Library-Manifest als LIBRARY_LIST
- CREATE_SYMBOL-Handler: interactive GetPoint im aktiven Viewport,
  laedt Block-Def + platziert Instanz(en) auf den richtigen Ebenen
- Pair-Items (2D+3D) werden an gleichem Punkt beidseitig platziert →
  Top zeigt 2D-Layer, Persp zeigt 3D-Layer wenn User entsprechend
  Sichtbarkeit setzt

Frontend:
- SymbolPicker Modal-Component: Grid mit Symbol/Object-Cards, Search,
  Type-Filter (Alle/Symbole/Objekte), Doppelklick = Pick
- Symbol-Button in ElementeApp (PillGroup "Library") oeffnet Modal +
  triggert listLibrary() fuer aktuelle Items
- createSymbol(id) → Backend → GetPoint → Place

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 03:31:54 +02:00
parent 8f691e37c4
commit 8184f559fc
5 changed files with 394 additions and 56 deletions
+177
View File
@@ -0,0 +1,177 @@
import { useState, useMemo } from 'react'
import Icon from './Icon'
import { BarToggle, BarButton, BAR_H } from './BarControls'
/* SymbolPicker — Modal-Overlay: zeigt Library-Items (symbol/object) als
Grid. Klick auf Item → onPick(id). Schliesst via onClose. */
function ItemPreview({ item }) {
// Vorschau: type-spezifisches Icon im Center auf bg-input.
// Spaeter: PNG-Thumbnail aus library/previews/.
const iconName = item.type === 'symbol' ? 'navigation' : 'forest'
return (
<div style={{
height: 56,
background: 'var(--bg-input)',
borderBottom: '1px solid var(--border-light)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<Icon name={iconName} size={24}
style={{ color: 'var(--text-muted)' }} />
</div>
)
}
function ItemCard({ item, onPick }) {
return (
<button onClick={() => onPick(item.id)}
onDoubleClick={() => onPick(item.id)}
style={{
display: 'flex', flexDirection: 'column',
border: '1px solid var(--border)', borderRadius: 8,
background: 'var(--bg-section)',
overflow: 'hidden',
cursor: 'pointer', padding: 0, textAlign: 'left',
transition: 'border-color 0.15s, transform 0.1s',
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = 'var(--accent)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = 'var(--border)'
}}
title="Klick zum Platzieren"
>
<ItemPreview item={item} />
<div style={{ padding: '6px 8px',
display: 'flex', flexDirection: 'column', gap: 3 }}>
<span style={{
fontSize: 11, fontWeight: 600,
color: 'var(--text-primary)',
overflow: 'hidden', textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}>{item.name}</span>
<span style={{ fontSize: 9, color: 'var(--text-muted)',
textTransform: 'uppercase', letterSpacing: '0.05em' }}>
{item.type === 'symbol' ? '2D-Symbol'
: item.type === 'object' ? '2D + 3D'
: item.type}
{((item.tags || []).length > 0) && ' · ' + item.tags.slice(0, 2).join(' · ')}
</span>
</div>
</button>
)
}
export default function SymbolPicker({ items, onPick, onClose }) {
const [search, setSearch] = useState('')
const [typeFilter, setTypeFilter] = useState('all')
// Nur symbol/object — material wird hier nicht gepicked
const placable = useMemo(() =>
(items || []).filter(it =>
it.type === 'symbol' || it.type === 'object'),
[items])
const filtered = useMemo(() => {
const q = search.trim().toLowerCase()
return placable.filter(it => {
if (typeFilter !== 'all' && it.type !== typeFilter) return false
if (!q) return true
if ((it.name || '').toLowerCase().includes(q)) return true
if ((it.tags || []).some(t => t.toLowerCase().includes(q))) return true
return false
})
}, [placable, search, typeFilter])
return (
<div style={{
position: 'fixed', inset: 0, zIndex: 200,
background: 'var(--bg-overlay)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}} onClick={onClose}>
<div onClick={(e) => e.stopPropagation()}
style={{
width: '90%', maxWidth: 640, maxHeight: '80vh',
background: 'var(--bg-dialog)',
border: '1px solid var(--border)',
borderRadius: 12,
display: 'flex', flexDirection: 'column',
overflow: 'hidden',
color: 'var(--text-primary)',
fontFamily: 'var(--font)', fontSize: 11,
boxShadow: '0 8px 32px rgba(0,0,0,0.4)',
}}>
{/* Header */}
<div style={{
display: 'flex', alignItems: 'center', gap: 8,
padding: '10px 14px',
borderBottom: '1px solid var(--border)',
}}>
<Icon name="inventory_2" size={14}
style={{ color: 'var(--accent)' }} />
<span style={{ flex: 1, fontWeight: 600, fontSize: 12 }}>
Symbol / Objekt einfuegen
</span>
<BarButton icon="close" onClick={onClose} title="Schliessen" />
</div>
{/* Toolbar */}
<div style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '8px 14px',
borderBottom: '1px solid var(--border)',
}}>
<input type="text" value={search}
onChange={(ev) => setSearch(ev.target.value)}
placeholder="Suchen (Name oder Tag)…"
autoFocus
style={{ flex: 1, height: BAR_H, padding: '0 12px',
fontSize: 11 }} />
<BarToggle label="Alle" active={typeFilter === 'all'}
onClick={() => setTypeFilter('all')} />
<BarToggle label="Symbole" active={typeFilter === 'symbol'}
onClick={() => setTypeFilter('symbol')} />
<BarToggle label="Objekte" active={typeFilter === 'object'}
onClick={() => setTypeFilter('object')} />
</div>
{/* Grid */}
<div style={{ flex: 1, minHeight: 200, overflowY: 'auto',
padding: 12 }}>
{filtered.length === 0 ? (
<div style={{ padding: 40, textAlign: 'center',
color: 'var(--text-muted)' }}>
{placable.length === 0
? 'Keine Symbole/Objekte in der Library.'
: 'Nichts gefunden.'}
</div>
) : (
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(130px, 1fr))',
gap: 8,
}}>
{filtered.map(it => (
<ItemCard key={it.id} item={it} onPick={onPick} />
))}
</div>
)}
</div>
{/* Footer */}
<div style={{
padding: '8px 14px',
borderTop: '1px solid var(--border)',
background: 'var(--bg-section)',
fontSize: 10, color: 'var(--text-muted)',
}}>
Klick auf Item im Viewport Punkt waehlen zum Platzieren.
{filtered.length > 0 && (
<span> · {filtered.length} / {placable.length}</span>
)}
</div>
</div>
</div>
)
}