diff --git a/rhino/elemente.py b/rhino/elemente.py index c33ea48..74bea27 100644 --- a/rhino/elemente.py +++ b/rhino/elemente.py @@ -8096,9 +8096,10 @@ class ElementeBridge(panel_base.BaseBridge): Library-Items + Handling von PICK (User waehlt Item → CREATE_SYMBOL im aktiven Doc) und CANCEL (Fenster schliessen).""" try: - import library + import library, rhinopanel manifest = library.load_manifest() - items = manifest.get("items", []) + items = rhinopanel._enrich_library_items_with_previews( + manifest.get("items", [])) except Exception as ex: print("[ELEMENTE] list library:", ex) items = [] diff --git a/rhino/library.py b/rhino/library.py index e0388ec..31ac065 100644 --- a/rhino/library.py +++ b/rhino/library.py @@ -210,6 +210,137 @@ def add_item(item): return ok, load_manifest() +def _previews_dir(): + """Pfad zum previews/-Subfolder. Wird angelegt falls fehlt.""" + p = os.path.join(library_root(), "previews") + if not os.path.isdir(p): + try: os.makedirs(p) + except Exception: pass + return p + + +def _preview_rel_for(asset_rel_or_id): + """Erzeugt einen relativen Preview-Pfad fuer ein Asset (oder Item-ID). + Liefert z.B. 'previews/.png'. Wir benutzen den Stamm des + .3dm-Files damit Asset + Preview gleichen Namen haben (debuggbar).""" + stem = asset_rel_or_id + if "/" in stem: stem = stem.split("/")[-1] + if "\\" in stem: stem = stem.split("\\")[-1] + if stem.lower().endswith(".3dm"): stem = stem[:-4] + safe = "".join(c if (c.isalnum() or c in "-_") else "_" for c in stem) + return "previews/" + safe + ".png" + + +def _capture_thumbnail_of_objects(target_objects, png_abs_path, size=128): + """Captured einen Top-View der gegebenen Objekte als PNG. Hided + temporaer alle anderen Objekte damit der Background sauber ist. + Returns True/False.""" + if Rhino is None: return False + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None or not target_objects: return False + try: + from System.Drawing import Size + import scriptcontext as sc + except Exception: + return False + # IDs der Ziel-Objekte + target_ids = set() + for o in target_objects: + try: + if not o.IsDeleted: target_ids.add(str(o.Id)) + except Exception: pass + if not target_ids: return False + # Andere Objekte temporaer ausblenden + hidden_by_us = [] + try: + for o in list(doc.Objects): + try: + if o.IsDeleted: continue + if str(o.Id) in target_ids: continue + if o.IsHidden: continue + if not o.IsNormal: continue # bereits hidden/locked → skip + doc.Objects.Hide(o.Id, True) + hidden_by_us.append(o.Id) + except Exception: pass + except Exception: pass + capture_ok = False + try: + view = doc.Views.ActiveView + if view is None: + return False + vp = view.ActiveViewport + # Viewport-State sichern damit User nichts verliert + saved_target = vp.CameraTarget + saved_loc = vp.CameraLocation + saved_proj = vp.IsParallelProjection + try: + # Auf Top-Parallel wechseln + Zoom auf Ziel + vp.SetProjection(Rhino.Display.DefinedViewportProjection.Top, "Top", True) + try: + bbox = Rhino.Geometry.BoundingBox.Empty + for o in target_objects: + g = o.Geometry + if g is None: continue + try: + bb = g.GetBoundingBox(True) + if bb.IsValid: bbox.Union(bb) + except Exception: pass + if bbox.IsValid: + # Etwas Padding + bbox.Inflate(bbox.Diagonal.Length * 0.1) + vp.ZoomBoundingBox(bbox) + except Exception as ex: + print("[LIBRARY] thumbnail zoom:", ex) + view.Redraw() + # Capture + try: + bmp = view.CaptureToBitmap(Size(int(size), int(size))) + if bmp is not None: + # Sicherstellen dass Verzeichnis da ist + try: + d = os.path.dirname(png_abs_path) + if d and not os.path.isdir(d): os.makedirs(d) + except Exception: pass + bmp.Save(png_abs_path) + capture_ok = True + except Exception as ex: + print("[LIBRARY] thumbnail capture:", ex) + finally: + # Viewport wiederherstellen + try: + vp.SetCameraLocation(saved_loc, False) + vp.SetCameraTarget(saved_target, True) + if saved_proj: + vp.IsParallelProjection = True + else: + vp.IsParallelProjection = False + except Exception: pass + finally: + # Hidden Objekte wieder einblenden + for gid in hidden_by_us: + try: doc.Objects.Show(gid, True) + except Exception: pass + try: doc.Views.Redraw() + except Exception: pass + return capture_ok + + +def read_preview_data_uri(rel_path): + """Liest die PNG-Vorschau als data:image/png;base64-URI fuer + direktes Einsetzen in . Liefert None wenn Datei fehlt.""" + if not rel_path: return None + abs_p = os.path.join(library_root(), rel_path) + if not os.path.isfile(abs_p): return None + try: + import base64 + with open(abs_p, "rb") as f: + data = f.read() + return "data:image/png;base64," + base64.b64encode(data).decode("ascii") + except Exception as ex: + print("[LIBRARY] read_preview:", ex) + return None + + def convert_to_3dm_via_import(src_path, target_name): """Konvertiert eine beliebige CAD-Datei (.dwg/.obj/.fbx/.dae/.stl/...) nach .3dm. Strategie: Rhinos _-Import in den aktiven Doc, dann die @@ -321,6 +452,14 @@ def convert_to_3dm_via_import(src_path, target_name): print("[LIBRARY] convert_to_3dm File3dm:", ex) return None finally: + # Thumbnail-Capture BEVOR die Objekte geloescht werden. + try: + rel_for_preview = os.path.relpath(target, library_root()) + preview_rel = _preview_rel_for(rel_for_preview) + preview_abs = os.path.join(library_root(), preview_rel) + _capture_thumbnail_of_objects(new_objs, preview_abs) + except Exception as ex: + print("[LIBRARY] convert thumbnail:", ex) # Cleanup: importierte Objekte wieder loeschen for o in new_objs: try: doc.Objects.Delete(o.Id, True) @@ -419,6 +558,13 @@ def save_selection_to_asset(doc, target_name): print("[LIBRARY] save_selection write:", ex) return None rel = os.path.relpath(target, library_root()) + # Thumbnail aus den (noch selektierten) Objekten capturen + try: + preview_rel = _preview_rel_for(rel) + preview_abs = os.path.join(library_root(), preview_rel) + _capture_thumbnail_of_objects(sel, preview_abs) + except Exception as ex: + print("[LIBRARY] save_selection thumbnail:", ex) print("[LIBRARY] save_selection: {} objs → {}".format(len(geoms), rel)) return rel except Exception as ex: diff --git a/rhino/rhinopanel.py b/rhino/rhinopanel.py index 21055b5..85fce8d 100644 --- a/rhino/rhinopanel.py +++ b/rhino/rhinopanel.py @@ -147,6 +147,24 @@ def _list_hatch_patterns_full(doc): return out +def _enrich_library_items_with_previews(items): + """Liefert Liste mit zusaetzlichem 'previewDataUri'-Feld pro Item + (base64-PNG falls preview-Datei existiert). Items werden NICHT + persistiert — nur fuer den Transport ans Frontend angereichert.""" + import library + out = [] + for it in (items or []): + if not isinstance(it, dict): + out.append(it); continue + enriched = dict(it) + prev = it.get("preview") + if prev: + uri = library.read_preview_data_uri(prev) + if uri: enriched["previewDataUri"] = uri + out.append(enriched) + return out + + def _list_linetypes_full(doc): """Vollstaendiges Linetype-Listing fuer die Verwaltungs-UI. Mac Rhino 8 GetSegment(i) returnt (length: float, isLine: bool): @@ -893,7 +911,8 @@ class EbenenBridge(panel_base.BaseBridge): try: import library lib_manifest = library.load_manifest() - lib_items = lib_manifest.get("items", []) + lib_items = _enrich_library_items_with_previews( + lib_manifest.get("items", [])) lib_root = library.library_root() except Exception: lib_items = []; lib_root = "" @@ -1183,12 +1202,15 @@ class EbenenBridge(panel_base.BaseBridge): # ---- Library-Item CRUD ---- def _send_library(self): """Sendet aktuelle Library-Items ans Frontend - (LIBRARY_ITEMS-Message).""" + (LIBRARY_ITEMS-Message). Items kommen mit + previewDataUri (base64 PNG) wenn Vorschau vorhanden.""" try: import library m = library.load_manifest() + enriched = _enrich_library_items_with_previews( + m.get("items", [])) self.send("LIBRARY_ITEMS", { - "items": m.get("items", []), + "items": enriched, "libraryRoot": library.library_root(), }) except Exception as ex: @@ -1247,18 +1269,21 @@ class EbenenBridge(panel_base.BaseBridge): self.send("LIBRARY_ERROR", { "msg": "Konnte Datei nicht importieren — siehe Log."}) return + # Preview-Pfad falls Thumbnail erzeugt wurde + preview_rel = library._preview_rel_for(rel) + preview_abs = os.path.join(library.library_root(), preview_rel) + has_preview = os.path.isfile(preview_abs) if target_id: - # Bestehendes Item updaten m = library.load_manifest() for it in m.get("items", []): if it.get("id") == target_id: key = "files" + variant it[key] = [rel] if not it.get("type"): it["type"] = item_type + if has_preview: it["preview"] = preview_rel break library.save_manifest(m) else: - # Neues Item — stem aus dem Pfad oben bereits berechnet import uuid as _uuid new_id = "obj-" + _uuid.uuid4().hex[:10] item = { @@ -1269,6 +1294,7 @@ class EbenenBridge(panel_base.BaseBridge): "tags": [], "files" + variant: [rel], } + if has_preview: item["preview"] = preview_rel library.add_item(item) self._send_library() except Exception as ex: @@ -1343,10 +1369,13 @@ class EbenenBridge(panel_base.BaseBridge): self.send("LIBRARY_ERROR", { "msg": "Konnte Selection nicht speichern."}) return + preview_rel = library._preview_rel_for(rel) + preview_abs = os.path.join(library.library_root(), preview_rel) + has_preview = os.path.isfile(preview_abs) if target_id: - library.update_item(target_id, { - ("files" + variant): [rel], - }) + patch = {("files" + variant): [rel]} + if has_preview: patch["preview"] = preview_rel + library.update_item(target_id, patch) else: import uuid as _uuid new_id = "obj-" + _uuid.uuid4().hex[:10] @@ -1358,6 +1387,7 @@ class EbenenBridge(panel_base.BaseBridge): "tags": [], ("files" + variant): [rel], } + if has_preview: item["preview"] = preview_rel library.add_item(item) self._send_library() b = _ProjectSettingsBridge() diff --git a/src/components/ProjectSettingsDialog.jsx b/src/components/ProjectSettingsDialog.jsx index 56db889..4e88c34 100644 --- a/src/components/ProjectSettingsDialog.jsx +++ b/src/components/ProjectSettingsDialog.jsx @@ -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' }}> - +
+ {!it.previewDataUri && ( + + )} +
-
- +
+
+ {!it.previewDataUri && ( + + )} +
{ const nm = ev.target.value diff --git a/src/components/SymbolPicker.jsx b/src/components/SymbolPicker.jsx index 6e22178..231c7a3 100644 --- a/src/components/SymbolPicker.jsx +++ b/src/components/SymbolPicker.jsx @@ -6,17 +6,29 @@ import { BarToggle, BarButton, BAR_H } from './BarControls' 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/. + // Thumbnail wenn vorhanden, sonst type-spezifisches Icon + if (item.previewDataUri) { + return ( +
+ ) + } const iconName = item.type === 'symbol' ? 'navigation' : 'forest' return (
-
)