Symbole-Tab in Project-Settings (Library-Item-Management)
Project-Settings hat jetzt 5 Tabs. Neuer 'Symbole'-Tab managt die Dossier-Library: List/Detail wie Materialien, mit 2D + 3D Slot pro Item. Backend (library.py): - save_manifest, update_item, delete_item, add_item — full CRUD aufs library.json - copy_to_assets: kopiert User-Dateien in library/assets/ mit Konflikt-Resolution (auto-suffix) Backend (rhinopanel.py / ProjectSettingsBridge): - _send_library: aktuelle Items + libraryRoot an Frontend - _add_library_file: File-Picker (.3dm direkt; .dwg/.obj/etc. zeigt Hinweis fuer kuenftige Konvertierung), kopiert + appended ans Item (variant 2d/3d) oder erstellt neues Item - _update_library_item: patch by id - _delete_library_item: entfernt Eintrag aus Manifest - LIBRARY_ITEMS + LIBRARY_ERROR Messages ans Frontend Frontend: - Neuer 'Symbole'-Tab mit List/Detail - Liste: Name, Type-Icon, '2D'/'3D' Status-Badge - Detail rechts: Name-Edit (live persist on blur), Type-Toggle (Symbol/Objekt), 2D/3D-File-Slots mit Datei-Picker, Tags-Editor - 'Neues Objekt' Button im Listen-Footer Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
openLibrary, pickTextureFile, onMessage,
|
||||
renameLinetype, deleteLinetype, loadLinetypeDefaults, importLinetypeFile,
|
||||
renameHatch, deleteHatch, importHatchFile,
|
||||
listLibraryItems, addLibraryFile, updateLibraryItem, deleteLibraryItem,
|
||||
} from '../lib/rhinoBridge'
|
||||
|
||||
/* Field — Stack-Layout fuer komplexe Inputs (mehrere Felder nebeneinander) */
|
||||
@@ -638,6 +639,9 @@ export default function ProjectSettingsDialog({
|
||||
const [hatches, setHatches] = useState(initial.hatchPatternsFull || [])
|
||||
const [selLt, setSelLt] = useState(null)
|
||||
const [selHt, setSelHt] = useState(null)
|
||||
const [libItems, setLibItems] = useState(initial.libraryItems || [])
|
||||
const [libRoot, setLibRoot] = useState(initial.libraryRoot || '')
|
||||
const [selLib, setSelLib] = useState(null)
|
||||
const builtin = initial.builtinMaterials || []
|
||||
|
||||
// Aktuell ausgewaehltes Material aus Selection ableiten
|
||||
@@ -668,6 +672,13 @@ export default function ProjectSettingsDialog({
|
||||
if (lts) setLinetypes(lts)
|
||||
if (hps) setHatches(hps)
|
||||
})
|
||||
onMessage('LIBRARY_ITEMS', ({ items, libraryRoot }) => {
|
||||
if (Array.isArray(items)) setLibItems(items)
|
||||
if (libraryRoot) setLibRoot(libraryRoot)
|
||||
})
|
||||
onMessage('LIBRARY_ERROR', ({ msg }) => {
|
||||
if (msg) alert(msg)
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Backend-File-Picker-Antwort: aktualisiert das Slot im aktuell
|
||||
@@ -741,6 +752,7 @@ export default function ProjectSettingsDialog({
|
||||
{ key: 'materials', label: 'Materialien' },
|
||||
{ key: 'linetypes', label: 'Linientypen' },
|
||||
{ key: 'hatches', label: 'Schraffuren' },
|
||||
{ key: 'symbols', label: 'Symbole' },
|
||||
]} active={tab} onChange={setTab} />
|
||||
|
||||
{/* Body */}
|
||||
@@ -1074,6 +1086,195 @@ export default function ProjectSettingsDialog({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tab === 'symbols' && (
|
||||
<div style={{ display: 'flex', height: '100%',
|
||||
margin: '-8px -14px', minHeight: 0 }}>
|
||||
{/* Links: Items-Liste */}
|
||||
<div style={{
|
||||
width: 240, flexShrink: 0,
|
||||
display: 'flex', flexDirection: 'column',
|
||||
borderRight: '1px solid var(--border)',
|
||||
background: 'var(--bg-dialog)',
|
||||
}}>
|
||||
<div style={{ flex: 1, overflowY: 'auto', padding: '4px 0' }}>
|
||||
{libItems.length === 0 && (
|
||||
<div style={{ padding: 20, textAlign: 'center',
|
||||
color: 'var(--text-muted)', fontSize: 10 }}>
|
||||
Noch keine Symbole/Objekte.
|
||||
<div style={{ marginTop: 6 }}>
|
||||
Erstes Item via ⬇ unten hinzufügen.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{libItems.map((it) => {
|
||||
const isSel = selLib === it.id
|
||||
return (
|
||||
<div key={it.id}
|
||||
onClick={() => setSelLib(it.id)}
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '20px 1fr',
|
||||
alignItems: 'center', gap: 6,
|
||||
padding: '5px 10px',
|
||||
cursor: 'pointer',
|
||||
background: isSel ? 'var(--accent-dim)' : 'transparent',
|
||||
borderLeft: '2px solid ' + (isSel ? 'var(--accent)' : 'transparent'),
|
||||
transition: 'background 0.12s',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isSel) e.currentTarget.style.background = 'var(--bg-item-hover)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isSel) e.currentTarget.style.background = 'transparent'
|
||||
}}>
|
||||
<Icon name={it.type === 'symbol' ? 'navigation' : 'forest'}
|
||||
size={13}
|
||||
style={{ color: 'var(--text-muted)' }} />
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontSize: 11,
|
||||
color: 'var(--text-primary)',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap' }}>
|
||||
{it.name || 'Unbenannt'}
|
||||
</div>
|
||||
<div style={{ fontSize: 9,
|
||||
color: 'var(--text-muted)' }}>
|
||||
{(it.files2d || []).length > 0 ? '2D ' : ''}
|
||||
{(it.files3d || []).length > 0 ? '3D' : ''}
|
||||
{!(it.files2d || []).length && !(it.files3d || []).length && '— leer'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 4,
|
||||
padding: '6px 8px',
|
||||
borderTop: '1px solid var(--border-light)' }}>
|
||||
<BarToggle icon="add" label="Neues Objekt"
|
||||
onClick={() => addLibraryFile('2d', null, 'object')}
|
||||
title="Datei waehlen, kopiert in Library + neues Item" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Rechts: Item-Detail */}
|
||||
<div style={{ flex: 1, minWidth: 0, overflow: 'hidden',
|
||||
padding: '12px 14px', overflowY: 'auto' }}>
|
||||
{(() => {
|
||||
const it = libItems.find(x => x.id === selLib)
|
||||
if (!it) return (
|
||||
<div style={{ padding: 40, textAlign: 'center',
|
||||
color: 'var(--text-muted)', fontSize: 11 }}>
|
||||
Item links auswählen oder neues anlegen.
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<DetailSection title="Identität">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Icon name={it.type === 'symbol' ? 'navigation' : 'forest'}
|
||||
size={28}
|
||||
style={{ color: 'var(--accent)' }} />
|
||||
<input type="text" value={it.name || ''}
|
||||
onChange={(ev) => {
|
||||
const nm = ev.target.value
|
||||
setLibItems(arr => arr.map(x =>
|
||||
x.id === it.id ? { ...x, name: nm } : x))
|
||||
}}
|
||||
onBlur={(ev) => updateLibraryItem(it.id,
|
||||
{ name: ev.target.value })}
|
||||
style={{ flex: 1, height: BAR_H, padding: '0 12px',
|
||||
fontSize: 12, fontWeight: 500 }} />
|
||||
<BarButton icon="delete"
|
||||
onClick={() => {
|
||||
deleteLibraryItem(it.id)
|
||||
setSelLib(null)
|
||||
}}
|
||||
title="Item löschen" />
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 6 }}>
|
||||
<BarToggle label="Symbol (nur 2D)"
|
||||
active={it.type === 'symbol'}
|
||||
onClick={() => updateLibraryItem(it.id,
|
||||
{ type: 'symbol' })} />
|
||||
<BarToggle label="Objekt (2D + 3D)"
|
||||
active={it.type === 'object'}
|
||||
onClick={() => updateLibraryItem(it.id,
|
||||
{ type: 'object' })} />
|
||||
</div>
|
||||
</DetailSection>
|
||||
|
||||
<DetailSection title="2D-Darstellung (Plan)">
|
||||
{(it.files2d || []).length === 0 ? (
|
||||
<div style={{ fontSize: 10,
|
||||
color: 'var(--text-muted)',
|
||||
padding: '6px 0 8px' }}>
|
||||
Keine 2D-Datei zugewiesen.
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ fontSize: 10,
|
||||
fontFamily: 'var(--font-mono)',
|
||||
color: 'var(--text-muted)',
|
||||
padding: '4px 0' }}>
|
||||
{(it.files2d || []).join(', ')}
|
||||
</div>
|
||||
)}
|
||||
<BarToggle icon="upload_file" label="Datei wählen"
|
||||
onClick={() => addLibraryFile('2d', it.id, it.type)} />
|
||||
</DetailSection>
|
||||
|
||||
<DetailSection title="3D-Darstellung (Perspektive)">
|
||||
{(it.files3d || []).length === 0 ? (
|
||||
<div style={{ fontSize: 10,
|
||||
color: 'var(--text-muted)',
|
||||
padding: '6px 0 8px' }}>
|
||||
Keine 3D-Datei zugewiesen.
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ fontSize: 10,
|
||||
fontFamily: 'var(--font-mono)',
|
||||
color: 'var(--text-muted)',
|
||||
padding: '4px 0' }}>
|
||||
{(it.files3d || []).join(', ')}
|
||||
</div>
|
||||
)}
|
||||
<BarToggle icon="upload_file" label="Datei wählen"
|
||||
onClick={() => addLibraryFile('3d', it.id, it.type)} />
|
||||
</DetailSection>
|
||||
|
||||
<DetailSection title="Tags">
|
||||
<input type="text"
|
||||
value={(it.tags || []).join(', ')}
|
||||
placeholder="komma-getrennt"
|
||||
onChange={(ev) => {
|
||||
const tg = ev.target.value
|
||||
setLibItems(arr => arr.map(x =>
|
||||
x.id === it.id ? {
|
||||
...x, tags: tg.split(',').map(s => s.trim()).filter(Boolean)
|
||||
} : x))
|
||||
}}
|
||||
onBlur={(ev) => updateLibraryItem(it.id, {
|
||||
tags: ev.target.value.split(',')
|
||||
.map(s => s.trim()).filter(Boolean)
|
||||
})}
|
||||
style={{ width: '100%', height: BAR_H,
|
||||
padding: '0 12px', fontSize: 11,
|
||||
boxSizing: 'border-box' }} />
|
||||
</DetailSection>
|
||||
|
||||
<div style={{ fontSize: 9, color: 'var(--text-muted)',
|
||||
marginTop: 14, lineHeight: 1.5 }}>
|
||||
ID: <span style={{ fontFamily: 'var(--font-mono)' }}>{it.id}</span><br />
|
||||
Library: <span style={{ fontFamily: 'var(--font-mono)' }}>{libRoot}</span>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer — Pill-Buttons */}
|
||||
|
||||
Reference in New Issue
Block a user