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:
@@ -929,6 +929,20 @@ def _layer_path_raum(doc, geschoss_name):
|
||||
return "{}::{}".format(geschoss_name, sub)
|
||||
|
||||
|
||||
def _layer_path_symbole(doc, geschoss_name, variant):
|
||||
"""Symbol-Layer (Library-Items). variant = '2d' oder '3d'.
|
||||
Pfad: <geschoss>::40_SYMBOLE::SYMBOLE_2D bzw. SYMBOLE_3D.
|
||||
User kann die Top-Level-Ebene 40 frei rein-/raus-schalten — die
|
||||
Sub-Layer 2D/3D regeln View-spezifische Sichtbarkeit (Top zeigt 2D,
|
||||
Persp zeigt 3D)."""
|
||||
sub = _find_ebene_sublayer_name(doc, ["symbol"],
|
||||
"40", "SYMBOLE",
|
||||
default_color="#5fa896", default_lw=0.13)
|
||||
if variant.lower() == "3d":
|
||||
return "{}::{}::SYMBOLE_3D".format(geschoss_name, sub)
|
||||
return "{}::{}::SYMBOLE_2D".format(geschoss_name, sub)
|
||||
|
||||
|
||||
def _ensure_layer(doc, path):
|
||||
"""Stellt sicher, dass ein Layer-Pfad existiert. Liefert Layer-Index."""
|
||||
idx = doc.Layers.FindByFullPath(path, -1)
|
||||
@@ -6030,6 +6044,8 @@ class ElementeBridge(panel_base.BaseBridge):
|
||||
elif t == "CREATE_TRAEGER": self._cmd_create_traeger(p)
|
||||
elif t == "CREATE_RAUM": self._cmd_create_raum(p)
|
||||
elif t == "EXPORT_RAEUME": self._cmd_export_raeume(p)
|
||||
elif t == "LIST_LIBRARY": self._cmd_list_library(p)
|
||||
elif t == "CREATE_SYMBOL": self._cmd_create_symbol(p)
|
||||
elif t == "OPEN_SWISSTOPO": self._cmd_open_swisstopo(p)
|
||||
elif t == "IMPORT_SWISSTOPO": self._cmd_import_swisstopo(p)
|
||||
elif t == "OPEN_SWISSTOPO_DIALOG": self._cmd_open_swisstopo_dialog(p)
|
||||
@@ -8071,6 +8087,75 @@ class ElementeBridge(panel_base.BaseBridge):
|
||||
# unter dem aktiven Geschoss.
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Library-Symbol/Object — listet + platziert Library-Items im Doc
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _cmd_list_library(self, p):
|
||||
"""Liefert Library-Manifest ans Frontend (fuer den Symbol-Picker
|
||||
im Elemente-Panel). Antwort: LIBRARY_LIST message."""
|
||||
try:
|
||||
import library
|
||||
manifest = library.load_manifest()
|
||||
self.send("LIBRARY_LIST", {
|
||||
"items": manifest.get("items", []),
|
||||
"name": manifest.get("name", "Dossier-Library"),
|
||||
})
|
||||
except Exception as ex:
|
||||
print("[ELEMENTE] list library:", ex)
|
||||
self.send("LIBRARY_LIST", {"items": [], "name": ""})
|
||||
|
||||
def _cmd_create_symbol(self, p):
|
||||
"""Platziert ein Library-Item (symbol/object) im Doc. Interactive
|
||||
GetPoint im aktiven Viewport — User klickt Position.
|
||||
Payload: { id: libraryId }. Layer-Routing auf 40_SYMBOLE::2D/3D
|
||||
unter aktivem Geschoss."""
|
||||
lib_id = (p.get("id") or "").strip()
|
||||
if not lib_id:
|
||||
print("[ELEMENTE] CREATE_SYMBOL ohne id")
|
||||
return
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
if doc is None: return
|
||||
import library
|
||||
item = library.find_item(lib_id)
|
||||
if item is None:
|
||||
print("[ELEMENTE] CREATE_SYMBOL: item not found", lib_id)
|
||||
return
|
||||
if item.get("type") not in ("symbol", "object"):
|
||||
print("[ELEMENTE] CREATE_SYMBOL: ungueltiger Typ", item.get("type"))
|
||||
return
|
||||
# Interactive point pick
|
||||
try:
|
||||
import Rhino.Input.Custom as ric
|
||||
from Rhino.Input import GetResult
|
||||
except Exception as ex:
|
||||
print("[ELEMENTE] Imports:", ex); return
|
||||
try:
|
||||
gp = ric.GetPoint()
|
||||
gp.SetCommandPrompt("Symbol platzieren — Punkt waehlen")
|
||||
res = gp.Get()
|
||||
if res != GetResult.Point: return
|
||||
pt = gp.Point()
|
||||
except Exception as ex:
|
||||
print("[ELEMENTE] GetPoint:", ex); return
|
||||
# Layer-Routing
|
||||
geschoss_id = _active_geschoss_id(doc)
|
||||
g = _geschoss_by_id(doc, geschoss_id) if geschoss_id else None
|
||||
geschoss_name = (g.get("name") if g else "EG") or "EG"
|
||||
layer2d = _ensure_layer(doc, _layer_path_symbole(doc, geschoss_name, "2d"))
|
||||
layer3d = _ensure_layer(doc, _layer_path_symbole(doc, geschoss_name, "3d"))
|
||||
# Import + place
|
||||
undo_serial = doc.BeginUndoRecord(
|
||||
"Symbol einfuegen: " + (item.get("name") or ""))
|
||||
try:
|
||||
ok, msg = library.import_item(doc, lib_id, at_point=pt,
|
||||
layer2d=layer2d, layer3d=layer3d)
|
||||
print("[ELEMENTE] CREATE_SYMBOL '{}': ok={} ({})".format(
|
||||
item.get("name"), ok, msg))
|
||||
finally:
|
||||
try: doc.EndUndoRecord(undo_serial)
|
||||
except Exception: pass
|
||||
|
||||
def _cmd_open_swisstopo(self, p):
|
||||
"""Oeffnet map.geo.admin.ch im Default-Browser mit den swisstopo-
|
||||
Layern voreingestellt. Param `mode`: 'buildings' | 'terrain' | 'both'.
|
||||
|
||||
+100
-56
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user