Library-Thumbnails: Auto-Capture + Base64-Preview in der UI

Beim Hinzufuegen oder Importieren eines Library-Items wird automatisch
ein PNG-Thumbnail vom Item generiert (Top-View, 128x128) und in
library/previews/<id>.png abgelegt. Frontend rendert die Previews als
Base64-Data-URIs (sicher gegen WebKit-file://-Restriktionen).

library.py:
- _previews_dir(): legt previews/-Folder an
- _preview_rel_for(asset_rel): predictable PNG-Pfad pro Item
- _capture_thumbnail_of_objects(): hided andere Objekte temporaer,
  switcht auf Top-Parallel, ZoomBoundingBox, CaptureToBitmap → PNG,
  restored Viewport + Hidden-State
- read_preview_data_uri(): liest PNG + encoded als data:image/png;base64
- Hook in convert_to_3dm_via_import (vor Cleanup) + save_selection_to_asset

rhinopanel.py:
- _enrich_library_items_with_previews(): haengt previewDataUri an
  jedes Item das ein preview-Feld hat
- Initial-Params + _send_library + ElementeBridge._cmd_list_library
  liefern angereicherte Items
- _add_library_file + _save_selection_as_library setzen preview-Pfad
  im Item wenn Thumbnail-Datei existiert

Frontend:
- SymbolPicker.ItemPreview: rendert <div backgroundImage> mit Base64-URI
  wenn vorhanden, sonst Icon-Fallback
- ProjectSettingsDialog Symbole-Tab: List-Row + Detail-Identity zeigen
  Thumbnail (32px in Liste, 56px im Detail), Icon nur als Fallback

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 19:07:33 +02:00
parent 827bd8d4d7
commit e1b63aa4e6
5 changed files with 236 additions and 23 deletions
+33 -9
View File
@@ -1115,8 +1115,8 @@ export default function ProjectSettingsDialog({
onClick={() => setSelLib(it.id)}
style={{
display: 'grid',
gridTemplateColumns: '20px 1fr',
alignItems: 'center', gap: 6,
gridTemplateColumns: '32px 1fr',
alignItems: 'center', gap: 8,
padding: '5px 10px',
cursor: 'pointer',
background: isSel ? 'var(--accent-dim)' : 'transparent',
@@ -1129,9 +1129,21 @@ export default function ProjectSettingsDialog({
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={{
width: 32, height: 32, borderRadius: 4,
background: it.previewDataUri
? `url("${it.previewDataUri}") center/contain no-repeat, var(--bg-input)`
: 'var(--bg-input)',
border: '1px solid var(--border-light)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
flexShrink: 0,
}}>
{!it.previewDataUri && (
<Icon name={it.type === 'symbol' ? 'navigation' : 'forest'}
size={14}
style={{ color: 'var(--text-muted)' }} />
)}
</div>
<div style={{ minWidth: 0 }}>
<div style={{ fontSize: 11,
color: 'var(--text-primary)',
@@ -1177,10 +1189,22 @@ export default function ProjectSettingsDialog({
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)' }} />
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{
width: 56, height: 56, borderRadius: 6,
background: it.previewDataUri
? `url("${it.previewDataUri}") center/contain no-repeat, var(--bg-input)`
: 'var(--bg-input)',
border: '1px solid var(--border)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
flexShrink: 0,
}}>
{!it.previewDataUri && (
<Icon name={it.type === 'symbol' ? 'navigation' : 'forest'}
size={22}
style={{ color: 'var(--accent)' }} />
)}
</div>
<input type="text" value={it.name || ''}
onChange={(ev) => {
const nm = ev.target.value