#! 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 typische Architektur-Materialien als Start.""" 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", "hatch": "Solid", "scale": 1.0}, }, { "id": "mat-mauerwerk-backstein-v1", "type": "material", "version": 1, "name": "Mauerwerk — Backstein", "tags": ["mauerwerk", "stein"], "data": {"color": "#a45a3c", "hatch": "Solid", "scale": 1.0}, }, { "id": "mat-daemmung-mineralwolle-v1", "type": "material", "version": 1, "name": "Daemmung — Mineralwolle", "tags": ["daemmung", "weich"], "data": {"color": "#e8d36b", "hatch": "Solid", "scale": 1.0}, }, { "id": "mat-holz-fichte-v1", "type": "material", "version": 1, "name": "Holz — Fichte", "tags": ["holz", "ausbau"], "data": {"color": "#c8a06a", "hatch": "Solid", "scale": 1.0}, }, ], } 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 {}, } 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"), "hatch": data.get("hatch", "Solid"), "scale": float(data.get("scale", 1.0) or 1.0), "source": "library", "libraryId": item.get("id"), } # 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" def import_item(doc, item_id): """Type-dispatching Import. Phase A: nur material. symbol/object kommen spaeter via File3dm.Read + InstanceDefinition.""" 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)