From f760d1c54baa836687148eaf100568931321a753 Mon Sep 17 00:00:00 2001 From: karim Date: Sun, 24 May 2026 16:57:42 +0200 Subject: [PATCH] Library Phase A.2 (Symbol/Object-Import) + Oberleiste-Pill-Restyle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Library Phase A.2: - import_symbol/import_object via File3dm.Read + InstanceDefinitions.Add - Stabile Block-Namen 'dossier_lib_' fuer Dedupe - Seed-Manifest erweitert um Nordpfeil (symbol) + Laubbaum (object) - ItemCard rendert type-spezifische Preview (Color-Swatch fuer material, Material-Icon fuer symbol/object) Oberleiste-Pill-Restyle: - OberleisteApp: Version unter DOSSIER-Logo, Settings-Icons vertikal gestapelt - ProjectSettingsDialog: Pill-Tabs, BarToggle-Footer, MaterialRow mit Hover-Highlight, Header entfernt (Eto.Form hat eigenen) - LibraryBrowser: BarButton-Reload, Pill-Typ-Filter, MaterialCard mit BarToggle-Pill, Header entfernt - Globaler select-Stil: bg-input statt bg-item (dunkler im Dark-Mode, konsistent zu Oberleiste-BarCombo) Routing: - OberleisteBridge delegiert OPEN_PROJECT_SETTINGS + OPEN_LIBRARY an EbenenBridge (sticky ebenen_bridge_ref) — vorher kamen die Messages an der falschen Bridge an und wurden verschluckt Co-Authored-By: Claude Opus 4.7 --- rhino/library.py | 159 +++++++++++++++++- rhino/oberleiste.py | 16 ++ src/OberleisteApp.jsx | 68 ++++---- src/components/LibraryBrowser.jsx | 143 ++++++++-------- src/components/ProjectSettingsDialog.jsx | 198 ++++++++++------------- src/index.css | 8 +- 6 files changed, 375 insertions(+), 217 deletions(-) diff --git a/rhino/library.py b/rhino/library.py index 973d4f4..cb50fc3 100644 --- a/rhino/library.py +++ b/rhino/library.py @@ -70,7 +70,9 @@ def ensure_library(): def _write_seed_manifest(path): - """Bootstrappt 4 typische Architektur-Materialien als Start.""" + """Bootstrappt 4 Materialien + 2 Symbol/Object-Beispiel-Eintraege als + Start. Die Symbol/Object-Files muss der User selber ablegen unter + library/assets/ — sonst schlaegt der Import fehl (graceful).""" seed = { "schemaVersion": _SCHEMA_VERSION, "name": "Dossier-Library (lokal)", @@ -103,6 +105,20 @@ def _write_seed_manifest(path): "tags": ["holz", "ausbau"], "data": {"color": "#c8a06a", "hatch": "Solid", "scale": 1.0}, }, + { + "id": "sym-nordpfeil-01", + "type": "symbol", "version": 1, + "name": "Nordpfeil", + "tags": ["plan", "nordpfeil"], + "files": ["assets/sym-nordpfeil-01.3dm"], + }, + { + "id": "obj-baum-laubbaum-01", + "type": "object", "version": 1, + "name": "Baum — Laubbaum", + "tags": ["aussen", "vegetation"], + "files": ["assets/obj-baum-laubbaum-01.3dm"], + }, ], } try: @@ -148,6 +164,9 @@ def _normalize_item(it): "tags": list(it.get("tags") or []), "preview": it.get("preview"), "data": it.get("data") or {}, + # files: relative Pfade (zur library_root()) auf .3dm-Fragmente. + # Symbol/Object-Import liest die ueber File3dm.Read. + "files": list(it.get("files") or []), } return out @@ -191,13 +210,143 @@ def import_material(doc, item): return True, "Material importiert" +# --- Symbol/Object-Import via File3dm + InstanceDefinitions ---------------- + +def _lib_asset_path(rel_path): + """Absoluter Pfad zu einer Library-Asset-Datei (Eintrag aus item.files).""" + if not rel_path: return None + return os.path.join(library_root(), rel_path) + + +def _block_name_for(item): + """Stabiler Block-Name fuer InstanceDefinition. Format: + 'dossier_lib_' — Dedup ueber Lib-ID, nicht Item-Name (sonst + Konflikt bei Umbenennen).""" + lid = item.get("id") or "" + safe = "".join(c if (c.isalnum() or c in "-_") else "_" for c in lid) + return "dossier_lib_" + safe + + +def _read_3dm_geometry(abs_path): + """Liest alle Top-Level-Objekte aus einer .3dm-Datei. Returns Liste von + (GeometryBase, ObjectAttributes). Bei Fehler leere Liste.""" + if not abs_path or not os.path.isfile(abs_path): + print("[LIBRARY] _read_3dm: Datei nicht gefunden:", abs_path) + return [] + try: + from Rhino.FileIO import File3dm + f3 = File3dm.Read(abs_path) + if f3 is None: + print("[LIBRARY] _read_3dm: File3dm.Read returned None:", abs_path) + return [] + out = [] + for obj in f3.Objects: + try: + g = obj.Geometry + a = obj.Attributes.Duplicate() if obj.Attributes else None + if g is not None: + out.append((g.Duplicate(), a)) + except Exception as ex: + print("[LIBRARY] _read_3dm obj:", ex) + return out + except Exception as ex: + print("[LIBRARY] _read_3dm:", ex) + return [] + + +def _ensure_block_definition(doc, item, geometry_attrs): + """Erstellt InstanceDefinition fuer dieses Library-Item wenn noch nicht + da. Returns (idx, was_created). idx<0 bei Fehler.""" + if Rhino is None: return -1, False + name = _block_name_for(item) + try: + existing = doc.InstanceDefinitions.Find(name) + except Exception: + existing = None + if existing is not None: + return existing.Index, False + # Geometry + Attributes separat sammeln + geoms = [g for g, _ in geometry_attrs if g is not None] + attrs = [a for _, a in geometry_attrs] + if not geoms: + return -1, False + base_pt = Rhino.Geometry.Point3d(0, 0, 0) + desc = "Dossier-Library: " + (item.get("name") or "") + try: + idx = doc.InstanceDefinitions.Add(name, desc, base_pt, geoms, attrs) + return idx, True + except Exception as ex: + print("[LIBRARY] InstanceDef Add:", ex) + return -1, False + + +def import_symbol(doc, item): + """Importiert ein Symbol-Item (= 2D-Block) in das Doc. Liest die + .3dm-Datei(en) aus item.files, erstellt eine InstanceDefinition mit + stabilem Namen, fuegt eine Instanz am Ursprung ein. + Returns (ok, message).""" + return _import_block_like(doc, item, kind="symbol") + + +def import_object(doc, item): + """Importiert ein Object-Item (= 3D-Block, BIM-Element) in das Doc. + Gleiche Pipeline wie import_symbol — Symbol/Object unterscheiden sich + nur in der UI-Kategorisierung.""" + return _import_block_like(doc, item, kind="object") + + +def _import_block_like(doc, item, kind): + if doc is None: return False, "Kein aktives Dokument" + files = item.get("files") or [] + if not files: + return False, "Item hat keine .3dm-Files: " + str(item.get("id")) + # Alle Files zusammen in eine Block-Definition packen. + all_geom = [] + for f in files: + abs_p = _lib_asset_path(f) + all_geom.extend(_read_3dm_geometry(abs_p)) + if not all_geom: + return False, "Keine importierbare Geometrie in {}".format(files) + idx, created = _ensure_block_definition(doc, item, all_geom) + if idx < 0: + return False, "InstanceDefinition konnte nicht erstellt werden" + # Instanz am Ursprung einfuegen — User kann danach verschieben. + try: + xform = Rhino.Geometry.Transform.Identity + inst_id = doc.Objects.AddInstanceObject(idx, xform) + if inst_id == System_Guid_Empty(): + return False, "AddInstanceObject fehlgeschlagen" + try: doc.Views.Redraw() + except Exception: pass + msg = ("Block '{}' importiert + am Ursprung eingefuegt".format( + item.get("name") or "") + if created else + "Block bereits vorhanden — neue Instanz eingefuegt") + return True, msg + except Exception as ex: + print("[LIBRARY] AddInstanceObject:", ex) + return False, str(ex) + + +def System_Guid_Empty(): + """Helper — System.Guid.Empty Vergleichswert.""" + try: + import System + return System.Guid.Empty + except Exception: + return None + + def import_item(doc, item_id): - """Type-dispatching Import. Phase A: nur material. symbol/object kommen - spaeter via File3dm.Read + InstanceDefinition.""" + """Type-dispatching Import. material → Project-Settings-Liste. + symbol/object → InstanceDefinition im Doc via File3dm.Read.""" item = find_item(item_id) if item is None: return False, "Item nicht gefunden: " + str(item_id) t = item.get("type") if t == "material": return import_material(doc, item) - # Phase A: symbol/object noch nicht - return False, "Typ '{}' wird erst in Phase A.2 unterstuetzt".format(t) + if t == "symbol": + return import_symbol(doc, item) + if t == "object": + return import_object(doc, item) + return False, "Unbekannter Typ: '{}'".format(t) diff --git a/rhino/oberleiste.py b/rhino/oberleiste.py index d5a0828..22ae33c 100644 --- a/rhino/oberleiste.py +++ b/rhino/oberleiste.py @@ -1066,6 +1066,22 @@ class OberleisteBridge(panel_base.BaseBridge): masse_settings.open_as_window() except Exception as ex: print("[OBERLEISTE] open masse:", ex) + elif t == "OPEN_PROJECT_SETTINGS": + # Delegiert an EBENEN-Bridge (sie haelt die Satellite-Logik fuer + # Projekt-Settings + Library). + try: + eb = sc.sticky.get("ebenen_bridge_ref") + if eb is not None: eb._open_project_settings() + else: print("[OBERLEISTE] open project-settings: ebenen_bridge_ref nicht da") + except Exception as ex: + print("[OBERLEISTE] open project-settings:", ex) + elif t == "OPEN_LIBRARY": + try: + eb = sc.sticky.get("ebenen_bridge_ref") + if eb is not None: eb._open_library() + else: print("[OBERLEISTE] open library: ebenen_bridge_ref nicht da") + except Exception as ex: + print("[OBERLEISTE] open library:", ex) # --- Darstellung (SIA-400 LoD globaler Override) ----------------- elif t == "SET_DARSTELLUNG": diff --git a/src/OberleisteApp.jsx b/src/OberleisteApp.jsx index 05c01fd..bd8e6a1 100644 --- a/src/OberleisteApp.jsx +++ b/src/OberleisteApp.jsx @@ -253,12 +253,13 @@ export default function OberleisteApp() { overflowX: 'auto', overflowY: 'hidden', flexShrink: 0, }}> - {/* Logo: DOSSIER. (Petrol-Punkt) — Klick = About-Fenster */} + {/* Logo: DOSSIER. + Version darunter (Klick = About-Fenster) */} - - + {/* Settings-Icons: Launcher + Projekt — vertikal gestapelt */} +
+ + +
{/* ====== VIEW 2x4 Grid ====== Reihe 1: TOP / ISO / PERSP / 📷 (Kamera-Settings) diff --git a/src/components/LibraryBrowser.jsx b/src/components/LibraryBrowser.jsx index b8e9ea2..853a95d 100644 --- a/src/components/LibraryBrowser.jsx +++ b/src/components/LibraryBrowser.jsx @@ -1,27 +1,67 @@ import { useState, useMemo } from 'react' import Icon from './Icon' +import { BarToggle, BarButton, BAR_H } from './BarControls' -/* MaterialCard — Preview-Swatch + Name + Tags + Import-Button. */ -function MaterialCard({ item, imported, onImport }) { - const color = item.data?.color || '#888888' +/* Preview — abhaengig vom Typ: + material → Color-Swatch + symbol/object → Typ-Icon-Placeholder (spaeter: PNG-Thumbnail aus + library/previews/) */ +function ItemPreview({ item }) { + if (item.type === 'material') { + return ( +
+ ) + } + const iconName = item.type === 'symbol' ? 'navigation' : 'forest' + return ( +
+ +
+ ) +} + +/* ItemCard — Preview + Name + Tags + Import-Pill */ +function ItemCard({ item, imported, onImport }) { + // material wird "importiert" gezeigt wenn schon in Project-Settings. + // symbol/object koennen N-mal eingefuegt werden → "Importiert" wird + // nicht persistent, jeder Klick legt eine weitere Instanz an. + const isMaterial = item.type === 'material' + const ctaLabel = isMaterial + ? (imported ? 'Importiert' : 'Importieren') + : 'Einfügen' + const ctaDisabled = isMaterial && imported return (
{ if (!imported) onImport(item.id) }} - title={imported ? 'Bereits importiert' : 'Doppelklick = importieren'}> -
-
+ onMouseEnter={(e) => { + if (ctaDisabled) return + e.currentTarget.style.borderColor = 'var(--accent)' + }} + onMouseLeave={(e) => { + e.currentTarget.style.borderColor = 'var(--border)' + }} + onDoubleClick={() => { if (!ctaDisabled) onImport(item.id) }} + title={ctaDisabled ? 'Bereits importiert' : 'Doppelklick = importieren'}> + +
{item.name} - {imported && ( + {ctaDisabled && ( )} @@ -40,23 +80,17 @@ function MaterialCard({ item, imported, onImport }) { {t} ))}
)} - + { if (!ctaDisabled) onImport(item.id) }} + disabled={ctaDisabled} />
) @@ -97,30 +131,7 @@ export default function LibraryBrowser({ overflow: 'hidden', fontFamily: 'var(--font)', color: 'var(--text-primary)', fontSize: 11, }}> - {/* Header */} -
- - - {manifest?.name || 'Dossier-Library'} - - - -
- - {/* Toolbar */} + {/* Toolbar — Suche + Pill-Filter + Reload */}
setSearch(ev.target.value)} placeholder="Suchen (Name oder Tag)…" - style={{ flex: 1, fontSize: 11, padding: '4px 8px', - borderRadius: 999 }} /> -
+ style={{ flex: 1, height: BAR_H, padding: '0 12px', + fontSize: 11 }} /> +
{types.map(t => ( - + setTypeFilter(t)} /> ))}
+
{/* Grid */} @@ -171,7 +176,7 @@ export default function LibraryBrowser({ gap: 8, }}> {filtered.map(it => ( - diff --git a/src/components/ProjectSettingsDialog.jsx b/src/components/ProjectSettingsDialog.jsx index bc2c1d3..17bfad9 100644 --- a/src/components/ProjectSettingsDialog.jsx +++ b/src/components/ProjectSettingsDialog.jsx @@ -1,14 +1,16 @@ import { useState } from 'react' import Icon from './Icon' +import { BarToggle, BarButton, BAR_H } from './BarControls' import { openLibrary } from '../lib/rhinoBridge' -/* Inline Field + Tab helpers fuer kompaktes Layout im Satellite. */ +/* Pill-Stil Field — Label klein in Caps, Inhalt darunter, optional Hint */ function Field({ label, hint, children, style }) { return ( -
- {label} +
+ {label}
{children}
{hint && ( @@ -19,56 +21,54 @@ function Field({ label, hint, children, style }) { ) } +/* Pill-Tabs — gleicher Stil wie BarToggle aus der Oberleiste */ function TabBar({ tabs, active, onChange }) { return (
{tabs.map(t => ( - + onChange(t.key)} /> ))}
) } function MaterialRow({ mat, hatchPatterns, onChange, onDelete, builtin }) { + const isBuiltin = builtin || mat.source === 'builtin' + const isLibrary = mat.source === 'library' return (
+ borderRadius: 6, + background: isBuiltin ? 'var(--bg-section)' : 'transparent', + transition: 'background 0.12s', + }} + onMouseEnter={(e) => { if (!isBuiltin) e.currentTarget.style.background = 'var(--bg-item-hover)' }} + onMouseLeave={(e) => { if (!isBuiltin) e.currentTarget.style.background = 'transparent' }} + > onChange({ ...mat, color: ev.target.value })} title="Farbe" - style={{ width: 14, height: 14, padding: 0, border: 'none', + style={{ width: 18, height: 18, padding: 0, border: 'none', background: 'transparent', cursor: 'pointer' }} /> onChange({ ...mat, name: ev.target.value })} - disabled={builtin} + disabled={isBuiltin} placeholder="Name" - style={{ flex: 1, fontSize: 11, minWidth: 0, - opacity: builtin ? 0.7 : 1 }} /> + style={{ minWidth: 0, height: BAR_H, padding: '0 10px', + fontSize: 11, + opacity: isBuiltin ? 0.7 : 1 }} /> setDefault('geschossHoehe', parseFloat(ev.target.value) || 3.0)} - style={{ flex: 1, textAlign: 'right' }} /> + style={numberInputStyle} /> - setDefault('schnitthoehe', parseFloat(ev.target.value) || 1.0)} - style={{ flex: 1, textAlign: 'right' }} /> + style={numberInputStyle} />
- + margin: '12px 0' }} /> + setDefault('schnittDepthBack', parseFloat(ev.target.value) || 8.0)} - style={{ flex: 1, textAlign: 'right' }} /> + style={numberInputStyle} /> -
- +
+ setDefault('schnittHeightMin', parseFloat(ev.target.value))} - style={{ flex: 1, textAlign: 'right' }} /> + style={numberInputStyle} /> - + setDefault('schnittHeightMax', parseFloat(ev.target.value))} - style={{ flex: 1, textAlign: 'right' }} /> + style={numberInputStyle} />
@@ -218,21 +214,19 @@ export default function ProjectSettingsDialog({ {tab === 'materials' && ( <> -
- Eingebaute Materialien (B) koennen nicht umbenannt werden, - aber Farbe + Hatch sind anpassbar. Eigene Materialien - koennen frei angelegt werden. +
+ Eingebaute Materialien (B) — Farbe + Hatch anpassbar, Name fix. + Library-Materialien (L) aus Dossier-Library importiert. + Lokale Materialien frei editierbar.
- {/* Built-in materials (read-only name) */} - {builtin.map((m, i) => ( + {builtin.map((m) => ( {/* read-only fuer Phase 1 */}} /> + onChange={() => {/* read-only Phase 1 */}} /> ))} - {/* User materials */} {draft.materials.map((m, i) => ( setMat(i, nm)} onDelete={() => delMat(i)} /> ))} -
- - +
+ +
)}
- {/* Footer */} + {/* Footer — Pill-Buttons */}
- - + + onSave(draft)} />
diff --git a/src/index.css b/src/index.css index 89a99c2..daacda1 100644 --- a/src/index.css +++ b/src/index.css @@ -189,11 +189,13 @@ input[type="range"]:hover { border-color: var(--border); } -/* Pill-shaped select */ +/* Pill-shaped select — gleicher Hintergrund wie input für konsistentes + Oberleiste-Pill-Look (in dark mode = dunkler, light mode = leicht + gehoben). Vorher --bg-item wirkte im dark mode "hell" gegen Inputs. */ select { appearance: none; -webkit-appearance: none; - background-color: var(--bg-item); + background-color: var(--bg-input); background-image: var(--select-arrow); background-repeat: no-repeat; background-position: right 10px center; @@ -209,7 +211,7 @@ select { } select:hover { background-color: var(--bg-item-hover); - border-color: var(--text-muted); + border-color: var(--accent); } /* Opt-out: System-native Chrome (Oberleiste). Setzt die globalen