#! python3 # -*- coding: utf-8 -*- """ library.py — Dossier-Library (Phase A: lokal, read-only) Library = wiederverwendbare Material-/Symbol-/Object-Templates die ein User oder ein Team teilt. Phase A: lokaler Ordner mit library.json + Assets. Spaeter (Phase B/C): Cloud-Sync via GitHub-Releases. Library-Root: ~/Library/Application Support/Dossier/library/ Struktur: library/ library.json # Manifest (Schema-Version + items[]) previews/ # PNG-Thumbnails assets/ # .3dm-Fragmente fuer symbol/object Manifest-Item: { "id": "mat-beton-roh-v1", # global eindeutig (UUID/Slug) "type": "material", # material | symbol | object "name": "Beton roh", "version": 1, # Schema-Version des Items "tags": ["beton", "tragwerk"], "preview": "previews/mat-beton-roh.png", "data": { ... } # Typ-spezifische Felder } Material-data: { color, hatch, scale } Symbol/Object-data: { files: ["assets/sym-foo.3dm"] } """ import os import json # Lazy Import von Rhino — library.py soll auch in einem Test-Skript ohne # Rhino-Kontext importierbar sein (z.B. fuer Seed-Logik). try: import Rhino # noqa: F401 except Exception: Rhino = None _LIB_DIRNAME = "library" _MANIFEST_FN = "library.json" _SCHEMA_VERSION = 1 def library_root(): """Mac-Pfad zur lokalen Library. Wird bei Bedarf angelegt.""" base = os.path.expanduser( "~/Library/Application Support/Dossier") return os.path.join(base, _LIB_DIRNAME) def ensure_library(): """Legt Library-Folder + Sub-Folders + Seed-Manifest an wenn leer.""" root = library_root() if not os.path.isdir(root): try: os.makedirs(root) except Exception as ex: print("[LIBRARY] mkdir:", ex); return root for sub in ("previews", "assets"): p = os.path.join(root, sub) if not os.path.isdir(p): try: os.makedirs(p) except Exception: pass manifest_path = os.path.join(root, _MANIFEST_FN) if not os.path.isfile(manifest_path): _write_seed_manifest(manifest_path) return root def _write_seed_manifest(path): """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)", "items": [ { "id": "mat-beton-sichtbeton-v1", "type": "material", "version": 1, "name": "Beton — Sichtbeton", "tags": ["beton", "tragwerk", "roh"], "data": {"color": "#a8a39b"}, }, { "id": "mat-mauerwerk-backstein-v1", "type": "material", "version": 1, "name": "Mauerwerk — Backstein", "tags": ["mauerwerk", "stein"], "data": {"color": "#a45a3c"}, }, { "id": "mat-daemmung-mineralwolle-v1", "type": "material", "version": 1, "name": "Daemmung — Mineralwolle", "tags": ["daemmung", "weich"], "data": {"color": "#e8d36b"}, }, { "id": "mat-holz-fichte-v1", "type": "material", "version": 1, "name": "Holz — Fichte", "tags": ["holz", "ausbau"], "data": {"color": "#c8a06a"}, }, { "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: with open(path, "w") as f: json.dump(seed, f, indent=2, ensure_ascii=False) print("[LIBRARY] Seed geschrieben:", path) except Exception as ex: print("[LIBRARY] seed write:", ex) def load_manifest(): """Liest library.json — legt Library bei Bedarf an. Returns dict mit schemaVersion + name + items[]. Bei Fehler leeres Manifest.""" ensure_library() path = os.path.join(library_root(), _MANIFEST_FN) try: with open(path, "r") as f: data = json.load(f) if not isinstance(data, dict): return _empty_manifest() items = data.get("items") if not isinstance(items, list): data["items"] = [] else: data["items"] = [_normalize_item(x) for x in items if _normalize_item(x) is not None] return data except Exception as ex: print("[LIBRARY] load_manifest:", ex) return _empty_manifest() def _empty_manifest(): return {"schemaVersion": _SCHEMA_VERSION, "name": "Dossier-Library", "items": []} def _normalize_item(it): if not isinstance(it, dict): return None if not it.get("id") or not it.get("type"): return None out = { "id": str(it["id"]), "type": str(it["type"]), "name": str(it.get("name") or "Unbenannt"), "version": int(it.get("version") or 1), "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 def find_item(item_id): """Sucht ein Item per id im Manifest. Returns None wenn nicht da.""" if not item_id: return None for it in load_manifest().get("items", []): if it.get("id") == item_id: return it return None # --- Import-Logik ----------------------------------------------------------- def import_material(doc, item): """Importiert ein Material-Item in die Projekt-Settings des Doc. Dedupe per libraryId — wenn schon importiert, kein Doppel-Eintrag. Returns (ok, message).""" if doc is None: return False, "Kein aktives Dokument" if not isinstance(item, dict) or item.get("type") != "material": return False, "Item ist kein Material" data = item.get("data") or {} new_mat = { "name": item.get("name") or "Unbenannt", "color": data.get("color", "#888888"), "source": "library", "libraryId": item.get("id"), } # PBR + Textur-Felder, falls Library-Item welche hat for k in ("roughness", "reflection", "transparency", "iorN", "uvScaleM", "textures"): if k in data: new_mat[k] = data[k] # Lazy-Import um Zyklen zu vermeiden import rhinopanel settings = rhinopanel.load_project_settings(doc) mats = list(settings.get("materials", [])) for m in mats: if m.get("libraryId") == new_mat["libraryId"]: return False, "Material bereits importiert" mats.append(new_mat) settings["materials"] = mats rhinopanel.save_project_settings(doc, settings) 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. 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) if t == "symbol": return import_symbol(doc, item) if t == "object": return import_object(doc, item) return False, "Unbekannter Typ: '{}'".format(t)