Files
DOSSIER/rhino/library.py
T
karim 8d3b3af882 Material/Ebene-Separation: Hatch raus aus Material (Refactor a)
Material ist jetzt rein 3D — Section-Hatch (2D-Schnitt) wird nicht mehr
am Material definiert, sondern am Layer (via Rhino-Layer-Dialog oder
spaeter via Ebenen-Settings + neuer Hatch-Tab im Project-Settings).

Schema-Aenderungen:
- _normalize_material: hatch + scale entfernt
- _MATERIAL_LIBRARY (elemente.py): hatch + scale aus allen Builtin-Mats
- _get_all_materials: ohne hatch
- _send_state materials payload: nur {name, color}
- Library import_material: PBR + Texturen werden weitergegeben

Backend:
- _ensure_material_sublayer: erstellt Sublayer mit Color, RESETTET aber
  alten SectionHatchIndex auf -1 (= "kein eigener Hatch") damit
  Inheritance/User-Override greift. Vorher wurden alte Material-Hatch-
  Werte da haengen geblieben.

Frontend:
- MaterialDetail: Schraffur-Section entfernt
- hatchPatterns-Prop entfernt

Konsequenz: existierende Waende verlieren ihren Section-Hatch beim
naechsten Regen. User definiert Section-Hatches jetzt auf Layer-Ebene.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 02:10:04 +02:00

355 lines
12 KiB
Python

#! 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_<libraryId>' — 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)