Symbol-Funktion in Elemente-Panel (Phase S1+S2)

Schema (library.py):
- Item-Format erweitert: files2d + files3d (Backwards-compat zu 'files')
- _build_variant_block + _place_instance + Layer-Routing pro Variante
- import_item akzeptiert at_point + layer2d/layer3d
- _ensure_block_definition mit variant-Suffix (dossier_lib_<id>_2d/_3d)

Backend (elemente.py):
- _layer_path_symbole(geschoss_name, variant) → <geschoss>::40_SYMBOLE::
  SYMBOLE_2D bzw. SYMBOLE_3D
- Default-Ebene 40 SYMBOLE via _find_ebene_sublayer_name
- LIST_LIBRARY-Handler: sendet Library-Manifest als LIBRARY_LIST
- CREATE_SYMBOL-Handler: interactive GetPoint im aktiven Viewport,
  laedt Block-Def + platziert Instanz(en) auf den richtigen Ebenen
- Pair-Items (2D+3D) werden an gleichem Punkt beidseitig platziert →
  Top zeigt 2D-Layer, Persp zeigt 3D-Layer wenn User entsprechend
  Sichtbarkeit setzt

Frontend:
- SymbolPicker Modal-Component: Grid mit Symbol/Object-Cards, Search,
  Type-Filter (Alle/Symbole/Objekte), Doppelklick = Pick
- Symbol-Button in ElementeApp (PillGroup "Library") oeffnet Modal +
  triggert listLibrary() fuer aktuelle Items
- createSymbol(id) → Backend → GetPoint → Place

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 03:31:54 +02:00
parent 8f691e37c4
commit 8184f559fc
5 changed files with 394 additions and 56 deletions
+100 -56
View File
@@ -156,6 +156,14 @@ def _empty_manifest():
def _normalize_item(it):
if not isinstance(it, dict): return None
if not it.get("id") or not it.get("type"): return None
# 2D/3D-Files: ein Item kann beide haben (Objekt mit Plan-Darstellung 2D
# + perspektivischem 3D-Modell) oder nur eines. Legacy 'files'-Feld wird
# als files2d interpretiert (Symbole = nur 2D historisch).
files_legacy = list(it.get("files") or [])
files2d = list(it.get("files2d") or [])
files3d = list(it.get("files3d") or [])
if not files2d and not files3d and files_legacy:
files2d = files_legacy
out = {
"id": str(it["id"]),
"type": str(it["type"]),
@@ -164,9 +172,10 @@ 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 []),
"files2d": files2d,
"files3d": files3d,
# legacy "files" mitgeben fuer Backwards-Kompatibilitaet
"files": files_legacy or files2d,
}
return out
@@ -220,13 +229,14 @@ def _lib_asset_path(rel_path):
return os.path.join(library_root(), rel_path)
def _block_name_for(item):
def _block_name_for(item, variant=""):
"""Stabiler Block-Name fuer InstanceDefinition. Format:
'dossier_lib_<libraryId>' — Dedup ueber Lib-ID, nicht Item-Name (sonst
Konflikt bei Umbenennen)."""
'dossier_lib_<libraryId>[_<variant>]'. variant '2d'/'3d' fuer Pair-Items."""
lid = item.get("id") or ""
safe = "".join(c if (c.isalnum() or c in "-_") else "_" for c in lid)
return "dossier_lib_" + safe
name = "dossier_lib_" + safe
if variant: name += "_" + variant
return name
def _read_3dm_geometry(abs_path):
@@ -256,24 +266,24 @@ def _read_3dm_geometry(abs_path):
return []
def _ensure_block_definition(doc, item, geometry_attrs):
def _ensure_block_definition(doc, item, geometry_attrs, variant=""):
"""Erstellt InstanceDefinition fuer dieses Library-Item wenn noch nicht
da. Returns (idx, was_created). idx<0 bei Fehler."""
da. variant unterscheidet '2d'/'3d'. Returns (idx, was_created)."""
if Rhino is None: return -1, False
name = _block_name_for(item)
name = _block_name_for(item, variant=variant)
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 "")
desc_suffix = " ({})".format(variant.upper()) if variant else ""
desc = "Dossier-Library: " + (item.get("name") or "") + desc_suffix
try:
idx = doc.InstanceDefinitions.Add(name, desc, base_pt, geoms, attrs)
return idx, True
@@ -282,52 +292,83 @@ def _ensure_block_definition(doc, item, geometry_attrs):
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.
def _build_variant_block(doc, item, variant_files, variant_label):
"""Liest .3dm-Files und legt eine InstanceDefinition (variant) an.
Returns Block-Index oder -1."""
if not variant_files: return -1
all_geom = []
for f in files:
for f in variant_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.
return -1
idx, _ = _ensure_block_definition(doc, item, all_geom, variant=variant_label)
return idx
def _place_instance(doc, block_idx, point, layer_idx=-1):
"""Platziert eine InstanceObject am gegebenen Punkt, optional auf
spezifischem Layer. Returns Guid oder None."""
if block_idx < 0: return None
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
xform = Rhino.Geometry.Transform.Translation(
point.X, point.Y, point.Z)
attrs = Rhino.DocObjects.ObjectAttributes()
if layer_idx >= 0:
attrs.LayerIndex = layer_idx
gid = doc.Objects.AddInstanceObject(block_idx, xform, attrs)
return gid
except Exception as ex:
print("[LIBRARY] AddInstanceObject:", ex)
return False, str(ex)
print("[LIBRARY] _place_instance:", ex)
return None
def import_symbol(doc, item, at_point=None, layer2d=-1, layer3d=-1):
return _import_block_like(doc, item, at_point=at_point,
layer2d=layer2d, layer3d=layer3d)
def import_object(doc, item, at_point=None, layer2d=-1, layer3d=-1):
return _import_block_like(doc, item, at_point=at_point,
layer2d=layer2d, layer3d=layer3d)
def _import_block_like(doc, item, at_point=None, layer2d=-1, layer3d=-1):
"""Platziert ein Library-Item im Doc. Item kann files2d und/oder files3d
haben → beide Varianten werden geladen + an gleichem Punkt platziert auf
ihren respektiven Layern.
at_point: Point3d (default = Origin).
layer2d/3d: optionale Layer-Indizes (default = aktiver Layer)."""
if doc is None: return False, "Kein aktives Dokument"
files2d = item.get("files2d") or []
files3d = item.get("files3d") or []
if not files2d and not files3d:
# Legacy fallback
legacy = item.get("files") or []
if legacy: files2d = legacy
if not files2d and not files3d:
return False, "Item hat keine .3dm-Files: " + str(item.get("id"))
if at_point is None:
at_point = Rhino.Geometry.Point3d(0, 0, 0)
placed = []
if files2d:
idx2 = _build_variant_block(doc, item, files2d, "2d")
if idx2 >= 0:
gid = _place_instance(doc, idx2, at_point, layer_idx=layer2d)
if gid is not None and gid != System_Guid_Empty():
placed.append("2D")
if files3d:
idx3 = _build_variant_block(doc, item, files3d, "3d")
if idx3 >= 0:
gid = _place_instance(doc, idx3, at_point, layer_idx=layer3d)
if gid is not None and gid != System_Guid_Empty():
placed.append("3D")
if not placed:
return False, "Konnte keinen Block platzieren (Files fehlen?)"
try: doc.Views.Redraw()
except Exception: pass
return True, "{} eingefuegt ({})".format(
item.get("name") or "", "+".join(placed))
def System_Guid_Empty():
@@ -339,16 +380,19 @@ def System_Guid_Empty():
return None
def import_item(doc, item_id):
def import_item(doc, item_id, at_point=None, layer2d=-1, layer3d=-1):
"""Type-dispatching Import. material → Project-Settings-Liste.
symbol/object → InstanceDefinition im Doc via File3dm.Read."""
symbol/object → InstanceDefinition im Doc via File3dm.Read.
at_point + layer2d/3d nur fuer symbol/object."""
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)
return import_symbol(doc, item, at_point=at_point,
layer2d=layer2d, layer3d=layer3d)
if t == "object":
return import_object(doc, item)
return import_object(doc, item, at_point=at_point,
layer2d=layer2d, layer3d=layer3d)
return False, "Unbekannter Typ: '{}'".format(t)