Swisstopo Iter 2 + hierarchische Ebenen + 0-Kote m.ü.M
Swisstopo
- swissBUILDINGS3D 3.0 + Variant-Toggle (separated/solid) im Dialog
- Auto-Fallback auf 2.0 wenn 3.0-Tiles ueber 200 MB sind (Stadt-Fall)
- Defensiver Variant-Filter auf 3 Ebenen (Item, Asset, ZIP-Extract) — keine
Doppelimporte mehr
- Auto-Skala korrigiert jetzt die importierten Objekte (×1000) statt die
User-bbox zu schrumpfen — Buildings bleiben in m-Doc-Skala
- merge_grids: XYZ-Tiles werden vor dem Mesh-Bau vereint, kein 1m-Streifen
zwischen Tiles mehr
- Layer-Konsolidierung: Build_*/Roof_*/Wall_*/Floor_* DWG-Source-Layer
werden auf Sub-Sub-Layer unter 81_Swissbuildings/{Build,Roof,Wall,Floor}
gemappt; solid-Variante landet flach direkt auf dem Parent
- 0-Kote m.ü.M (Projekt-Nullpunkt) wird beim Import als Z-Offset angewandt
Hierarchische Ebenen
- dossier_ebenen unterstuetzt jetzt 'children'-Array (rekursiv)
- layer_builder.build_layers rekursiv (Parent + Children unter jedem Geschoss)
- apply_visibility/update_layer_style/set_ebene_visible/set_ebene_locked
walken den Tree (Sub-Sub-Layer mit gleichem Code-Prefix werden mit-gepflegt)
- EbenenManager mit Chevron-Toggle + Indent pro Level + Context-Menue-Item
'Sub-Ebene hinzufuegen'
- rhinoBridge.applyVisibility schickt Children-Tree (nicht nur Top-Level) —
sonst kommen Sub-Toggles nicht beim Backend an
- Visibility-Key in App.jsx rekursiv durch Children — useEffect feuert jetzt
auch bei Sub-Eye-Toggles
0-Kote m.ü.M
- Eingabefeld im Geschoss-Settings-Dialog (projektweit)
- Speicherung als dossier_project_zero_mum in doc.Strings
- Wird im Swisstopo-Import als Z-Offset (m + doc-units) angewandt
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+431
-93
@@ -353,18 +353,48 @@ def _find_ebene_sublayer_name(doc, keywords, default_code, default_name,
|
||||
json.dumps(ebenen, ensure_ascii=False))
|
||||
print("[ELEMENTE] Ebene '{}_{}' automatisch hinzugefuegt".format(
|
||||
default_code, default_name))
|
||||
# Ebenen-Manager UI mit-informieren
|
||||
b = sc.sticky.get("ebenen_bridge_ref") \
|
||||
or sc.sticky.get("ebenen_bridge") \
|
||||
or sc.sticky.get("rhinopanel_bridge")
|
||||
if b is not None and hasattr(b, "_send_state"):
|
||||
try: b._send_state()
|
||||
except Exception: pass
|
||||
# build_layers synchron damit Rhino-Layer existieren bevor
|
||||
# Objekte verschoben werden
|
||||
try:
|
||||
import layer_builder
|
||||
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
|
||||
zlist = json.loads(z_raw) if z_raw else []
|
||||
if zlist: layer_builder.build_layers(doc, zlist, ebenen)
|
||||
except Exception as ex:
|
||||
print("[ELEMENTE] build_layers nach auto-add:", ex)
|
||||
# Ebenen-Manager UI mit-informieren via broadcast_state
|
||||
try:
|
||||
import rhinopanel
|
||||
rhinopanel._broadcast_state(doc)
|
||||
except Exception as ex:
|
||||
print("[ELEMENTE] broadcast_state:", ex)
|
||||
except Exception as ex:
|
||||
print("[ELEMENTE] Auto-Add fehler:", ex)
|
||||
return "{}_{}".format(default_code, default_name)
|
||||
|
||||
|
||||
def _parse_swisstopo_tile_bbox(filename):
|
||||
"""Aus einem swisstopo-Filename die LV95-Tile-bbox ableiten.
|
||||
|
||||
Filename-Pattern:
|
||||
swissimage-dop10_2025_2763-1254_0.1_2056.tif (1km x 1km Tile)
|
||||
swissimage-dop10_2025_2763-1254-12_0.5_2056.tif (250m Sub-Tile)
|
||||
SWISSALTI3D_..._2763_1254.xyz (LV95-1km)
|
||||
|
||||
Tile-Coords sind in 100m-Einheiten (E/N x 100). 2763-1254 = LV95
|
||||
E=2'763'000, N=1'254'000 → bbox = (2763000, 1254000, 2764000, 1255000).
|
||||
Liefert (e_min, n_min, e_max, n_max) in Metern oder None."""
|
||||
import re as _re
|
||||
if not filename: return None
|
||||
# Erst per-1km-Tile probieren: _NNNN-NNNN_ oder _NNNN_NNNN_
|
||||
m = _re.search(r"[_-](\d{4})[-_](\d{4})(?:[-_]|\.)", filename)
|
||||
if not m: return None
|
||||
e_k = int(m.group(1)); n_k = int(m.group(2))
|
||||
e_min = e_k * 1000.0
|
||||
n_min = n_k * 1000.0
|
||||
return (e_min, n_min, e_min + 1000.0, n_min + 1000.0)
|
||||
|
||||
|
||||
def _layer_path_axis(doc, geschoss_name):
|
||||
"""Wand-Achse + Volumen — Sublayer 'Wände' (Code 20)."""
|
||||
sub = _find_ebene_sublayer_name(doc, ["wand", "wände", "waende"],
|
||||
@@ -6725,13 +6755,26 @@ class ElementeBridge(panel_base.BaseBridge):
|
||||
Rhino.UnitSystem.Meters, d.ModelUnitSystem)
|
||||
except Exception:
|
||||
m_to_unit = 1.0
|
||||
# Projekt-Nullpunkt in m.ü.M lesen — wird als Z-Offset
|
||||
# angewandt damit Real-Welt-Höhen auf Doc-Z relativ zu OKFF=0
|
||||
# liegen (sonst zeichnet man Geschosse 400m unter dem Terrain).
|
||||
try:
|
||||
z_mum_raw = d.Strings.GetValue("dossier_project_zero_mum")
|
||||
project_zero_mum = float(z_mum_raw) if z_mum_raw else 0.0
|
||||
except Exception:
|
||||
project_zero_mum = 0.0
|
||||
eC_u = eC * m_to_unit
|
||||
nC_u = nC * m_to_unit
|
||||
r_u = r * m_to_unit
|
||||
z_offset_m = project_zero_mum if shift else 0.0 # m
|
||||
z_offset_u = z_offset_m * m_to_unit # doc-units
|
||||
bbox = (eC - r, nC - r, eC + r, nC + r) # m (fuer STAC-Query)
|
||||
bbox_doc = (eC_u - r_u, nC_u - r_u, eC_u + r_u, nC_u + r_u) # in Doc-Units
|
||||
origin_shift = (eC, nC, 0) if shift else (0, 0, 0)
|
||||
origin_shift_doc = (eC_u, nC_u, 0) if shift else (0, 0, 0)
|
||||
origin_shift = (eC, nC, z_offset_m) if shift else (0, 0, 0)
|
||||
origin_shift_doc = (eC_u, nC_u, z_offset_u) if shift else (0, 0, 0)
|
||||
if shift and abs(project_zero_mum) > 1e-6:
|
||||
self._push_log("Projekt-Nullpunkt: {:g} m.ü.M → Z-Offset {:g}m".format(
|
||||
project_zero_mum, z_offset_m))
|
||||
self._push_log("Center LV95: E={:.1f} N={:.1f} Radius={}m".format(eC, nC, r))
|
||||
self._push_log("BBox (m): {:.0f}-{:.0f} / {:.0f}-{:.0f}".format(*bbox))
|
||||
if m_to_unit != 1.0:
|
||||
@@ -6775,7 +6818,10 @@ class ElementeBridge(panel_base.BaseBridge):
|
||||
try:
|
||||
# --- Buildings (DWG) -----------------------------------
|
||||
if "buildings" in kinds:
|
||||
paths = swisstopo.fetch_buildings_dwg(bbox, progress=self._push_log)
|
||||
variant = (opts.get("buildVariant") or "separated").strip().lower()
|
||||
if variant not in ("separated", "solid"): variant = "separated"
|
||||
paths = swisstopo.fetch_buildings_dwg(
|
||||
bbox, progress=self._push_log, variant=variant)
|
||||
for idx, p in enumerate(paths):
|
||||
try: size_mb = os.path.getsize(p) / 1e6
|
||||
except Exception: size_mb = 0
|
||||
@@ -6792,10 +6838,13 @@ class ElementeBridge(panel_base.BaseBridge):
|
||||
self._push_log("→ Import fertig: {} neue Objekte".format(len(new)))
|
||||
# Auto-Skala-Erkennung: Rhinos DXF-Parser kann je
|
||||
# nach $INSUNITS und Doc-Unit unerwartet 1000x rauf/
|
||||
# runter skalieren. Wir messen aus den Objekten und
|
||||
# SNAPPEN auf naechste Zehnerpotenz (1, 0.001, 1000)
|
||||
# damit kleine Mess-Streuung nicht eine off-by-1m
|
||||
# bbox produziert.
|
||||
# runter skalieren. swissBUILDINGS3D 3.0 z.B. liefert
|
||||
# Werte in KM (Center bei ~2764, statt 2'763'800m).
|
||||
# Wir korrigieren das per Scale-Faktor auf den
|
||||
# importierten Objekten (nicht durch Verkleinern
|
||||
# der User-bbox — sonst sind die Objekte spaeter
|
||||
# 1000x zu klein relativ zu allem anderen im Doc).
|
||||
scale_correction = 1.0
|
||||
if new and idx == 0:
|
||||
try:
|
||||
import math as _m
|
||||
@@ -6805,19 +6854,14 @@ class ElementeBridge(panel_base.BaseBridge):
|
||||
sum_x += bb.Center.X
|
||||
samples += 1
|
||||
avg_x = sum_x / max(1, samples)
|
||||
if abs(eC) > 1.0 and avg_x != 0:
|
||||
raw_ratio = avg_x / eC
|
||||
snapped = 10 ** round(_m.log10(abs(raw_ratio)))
|
||||
if abs(snapped - m_to_unit) > 1e-9:
|
||||
self._push_log("AUTO-SKALA: raw_ratio={:.6f} → snap to 1m={:g} doc-units (war {:g})".format(
|
||||
raw_ratio, snapped, m_to_unit))
|
||||
m_to_unit = snapped
|
||||
eC_u = eC * m_to_unit
|
||||
nC_u = nC * m_to_unit
|
||||
r_u = r * m_to_unit
|
||||
bbox_doc = (eC_u - r_u, nC_u - r_u,
|
||||
eC_u + r_u, nC_u + r_u)
|
||||
origin_shift_doc = (eC_u, nC_u, 0) if shift else (0, 0, 0)
|
||||
expected_x = eC * m_to_unit
|
||||
if abs(expected_x) > 1.0 and avg_x != 0:
|
||||
ratio = expected_x / avg_x
|
||||
snap = 10 ** round(_m.log10(abs(ratio)))
|
||||
if abs(snap - 1.0) > 0.01:
|
||||
scale_correction = snap
|
||||
self._push_log("AUTO-SKALA: imports {}× off — scale-up {:g}×".format(
|
||||
"klein" if snap > 1 else "gross", snap))
|
||||
except Exception as ex:
|
||||
self._push_log("Auto-Skala-Erkennung: {}".format(ex))
|
||||
# Diagnose
|
||||
@@ -6868,36 +6912,42 @@ class ElementeBridge(panel_base.BaseBridge):
|
||||
kept = new
|
||||
try: swisstopo._yield_ui()
|
||||
except Exception: pass
|
||||
# Shift falls aktiviert — Batch via System.List[Guid]
|
||||
# damit Python.NET den richtigen Overload erwischt.
|
||||
if shift and kept:
|
||||
self._push_log("→ Shift {} Objekte zum Welt-Origin (Batch)...".format(len(kept)))
|
||||
xform = rg.Transform.Translation(-origin_shift_doc[0],
|
||||
-origin_shift_doc[1],
|
||||
-origin_shift_doc[2])
|
||||
try:
|
||||
from System.Collections.Generic import List as _List
|
||||
from System import Guid as _Guid
|
||||
ids = _List[_Guid]()
|
||||
for o in kept: ids.Add(o.Id)
|
||||
n_shifted = d.Objects.Transform(ids, xform, True)
|
||||
self._push_log(" → {} Objekte verschoben".format(n_shifted))
|
||||
except Exception as ex:
|
||||
self._push_log(" Batch-Shift fehlgeschlagen, Loop-Fallback: {}".format(ex))
|
||||
for o in kept:
|
||||
try: d.Objects.Transform(o.Id, xform, True)
|
||||
except Exception: pass
|
||||
# Layer-Move + Tag
|
||||
# Scale + Move via Rhinos eingebaute Commands auf
|
||||
# Selektion — die batchen intern und sind bei 7000
|
||||
# Objekten in Sekunden durch (statt Minuten mit
|
||||
# einzeln-Transform-Loop).
|
||||
translate_doc = None
|
||||
if shift:
|
||||
translate_doc = (-origin_shift_doc[0],
|
||||
-origin_shift_doc[1],
|
||||
-origin_shift_doc[2])
|
||||
ops = []
|
||||
if abs(scale_correction - 1.0) > 1e-6:
|
||||
ops.append("Scale {}×".format(scale_correction))
|
||||
if shift: ops.append("Shift→Origin")
|
||||
if ops and kept:
|
||||
self._push_log("→ {} ({} Obj)...".format(
|
||||
" + ".join(ops), len(kept)))
|
||||
self._apply_xform_fast(
|
||||
d, kept,
|
||||
scale_factor=scale_correction,
|
||||
translate=translate_doc)
|
||||
# Layer-Konsolidierung:
|
||||
# 81_Swissbuildings ist hierarchische Ebene mit
|
||||
# Children Build/Roof/Wall/Floor (codes 8101-8104).
|
||||
# _consolidate_buildings stellt die Hierarchie in
|
||||
# dossier_ebenen sicher + verschiebt Objekte auf
|
||||
# die richtige Child-Layer + loescht leere
|
||||
# DWG-Source-Layer. Im Ebenen-Manager sind die
|
||||
# Children dann als Sub-Ebenen sichtbar (aufklappen).
|
||||
if z_id and kept:
|
||||
self._push_log("→ Layer-Move auf 12_Gebäude...")
|
||||
sub_name = _find_ebene_sublayer_name(
|
||||
d, ["gebaeude", "gebäude", "buildings"],
|
||||
"12", "Gebäude",
|
||||
default_color="#888888", default_lw=0.25)
|
||||
self._move_to_sublayer(d, kept, z_id,
|
||||
sub_name.split("_", 1)[0], tag="buildings")
|
||||
if variant == "solid":
|
||||
self._push_log("→ Buildings auf '81_Swissbuildings' (solid)...")
|
||||
else:
|
||||
self._push_log("→ Layer konsolidieren (Build/Roof/Wall/Floor)...")
|
||||
self._consolidate_buildings(d, kept, z_id,
|
||||
target_code="81", variant=variant)
|
||||
else:
|
||||
# Kein aktives Geschoss → nur Tag setzen
|
||||
self._tag_objects(d, kept, "buildings")
|
||||
new_obj_ids.extend(o.Id for o in kept)
|
||||
|
||||
@@ -6909,41 +6959,52 @@ class ElementeBridge(panel_base.BaseBridge):
|
||||
xyz_paths = swisstopo.fetch_terrain_xyz(
|
||||
bbox, resolution=res, progress=self._push_log)
|
||||
mesh_objects = []
|
||||
# Erst ALLE Tiles in Grids parsen, dann mergen, dann
|
||||
# EIN Mesh bauen — sonst gibt es einen 1m-Streifen
|
||||
# ohne Faces zwischen benachbarten Tiles.
|
||||
grids = []
|
||||
for p in xyz_paths:
|
||||
self._push_log("Mesh aus {}...".format(os.path.basename(p)))
|
||||
self._push_log("Parse {}...".format(os.path.basename(p)))
|
||||
try:
|
||||
# xyz_to_grid arbeitet in LV95-Metern (Quelle).
|
||||
# Erst grid bauen, dann beim mesh_from_grid auf
|
||||
# doc-units skalieren + shiften.
|
||||
grid = swisstopo.xyz_to_grid(
|
||||
p,
|
||||
target_step=target_step,
|
||||
clip_bbox=bbox, # User-bbox in m!
|
||||
clip_bbox=bbox,
|
||||
progress=self._push_log)
|
||||
if grid is None:
|
||||
self._push_log("→ leeres Grid"); continue
|
||||
# Mesh in Doc-Units bauen: shift in m (LV95),
|
||||
# dann beim Vertex-Add * m_to_unit
|
||||
mesh = swisstopo.mesh_from_grid(
|
||||
grid,
|
||||
origin_shift=origin_shift,
|
||||
unit_scale=m_to_unit)
|
||||
self._push_log("→ Mesh: {} Vertices / {} Faces".format(
|
||||
mesh.Vertices.Count, mesh.Faces.Count))
|
||||
gid = d.Objects.AddMesh(mesh)
|
||||
obj = d.Objects.Find(gid)
|
||||
if obj: mesh_objects.append((obj, grid["bbox"]))
|
||||
if grid is not None: grids.append(grid)
|
||||
except Exception as ex:
|
||||
self._push_log("XYZ-Parse fail: {}".format(ex))
|
||||
if grids:
|
||||
try:
|
||||
merged = swisstopo.merge_grids(grids)
|
||||
if merged is None:
|
||||
self._push_log("Merge lieferte None")
|
||||
else:
|
||||
self._push_log("Merge: {} Tiles → {} Punkte ({}×{} Raster)".format(
|
||||
len(grids), len(merged["points"]),
|
||||
len(merged["es"]), len(merged["ns"])))
|
||||
mesh = swisstopo.mesh_from_grid(
|
||||
merged,
|
||||
origin_shift=origin_shift,
|
||||
unit_scale=m_to_unit)
|
||||
self._push_log("→ Mesh: {} Vertices / {} Faces".format(
|
||||
mesh.Vertices.Count, mesh.Faces.Count))
|
||||
gid = d.Objects.AddMesh(mesh)
|
||||
obj = d.Objects.Find(gid)
|
||||
if obj: mesh_objects.append((obj, merged["bbox"]))
|
||||
except Exception as ex:
|
||||
self._push_log("Mesh-Bau fehlgeschlagen: {}".format(ex))
|
||||
# Layer-Move + Ortho-Drape
|
||||
# Layer-Move auf aktive Geschoss/80_swisstopo Sublayer
|
||||
if z_id and mesh_objects:
|
||||
sub_name = _find_ebene_sublayer_name(
|
||||
d, ["situation", "terrain", "gelaende"],
|
||||
"10", "Situation",
|
||||
d, ["swisstopo", "gelaende_topo"],
|
||||
"80", "swisstopo",
|
||||
default_color="#909090", default_lw=0.18)
|
||||
objs = [m[0] for m in mesh_objects]
|
||||
self._move_to_sublayer(d, objs, z_id,
|
||||
sub_name.split("_", 1)[0], tag="terrain")
|
||||
sub_name.split("_", 1)[0], tag="terrain",
|
||||
fallback_name=sub_name,
|
||||
fallback_color="#909090")
|
||||
elif mesh_objects:
|
||||
objs = [m[0] for m in mesh_objects]
|
||||
self._tag_objects(d, objs, "terrain")
|
||||
@@ -6952,23 +7013,50 @@ class ElementeBridge(panel_base.BaseBridge):
|
||||
ortho_paths = swisstopo.fetch_orthophoto(
|
||||
bbox, resolution="2.0", progress=self._push_log)
|
||||
if ortho_paths:
|
||||
# Erstes Ortho-Tile auf alle Meshes (MVP — pro Mesh
|
||||
# eigenes Mapping waere genauer, kommt spaeter)
|
||||
for obj, mbbox in mesh_objects:
|
||||
# Max-Z des Terrains finden — Plane sitzt knapp darueber
|
||||
# damit sie in Top-View ueber dem Terrain liegt.
|
||||
terr_max_z = 0.0
|
||||
for tobj, _ in mesh_objects:
|
||||
try:
|
||||
if shift:
|
||||
# Mesh ist verschoben → bbox auch
|
||||
mbbox_shifted = (
|
||||
mbbox[0] - origin_shift[0],
|
||||
mbbox[1] - origin_shift[1],
|
||||
mbbox[2] - origin_shift[0],
|
||||
mbbox[3] - origin_shift[1])
|
||||
else:
|
||||
mbbox_shifted = mbbox
|
||||
swisstopo.apply_ortho_material(
|
||||
d, obj, ortho_paths[0], mbbox_shifted)
|
||||
bb = tobj.Geometry.GetBoundingBox(True)
|
||||
if bb.IsValid and bb.Max.Z > terr_max_z:
|
||||
terr_max_z = bb.Max.Z
|
||||
except Exception: pass
|
||||
z_offset = max(0.001, terr_max_z * 1e-4) # winziges Epsilon
|
||||
plane_z = terr_max_z + z_offset
|
||||
self._push_log("→ {} Ortho-Tile(s), platziere Plane bei Z={:.3f}".format(
|
||||
len(ortho_paths), plane_z))
|
||||
ortho_objs = []
|
||||
for ortho_path in ortho_paths:
|
||||
# tile_bbox aus Filename ableiten — swissimage
|
||||
# tile_id = "1076-33" o.ae. → LV95 Tile-Origin
|
||||
tile_bbox = _parse_swisstopo_tile_bbox(
|
||||
os.path.basename(ortho_path))
|
||||
if tile_bbox is None:
|
||||
self._push_log(" → Tile-bbox nicht ableitbar aus {}".format(
|
||||
os.path.basename(ortho_path)))
|
||||
continue
|
||||
try:
|
||||
obj = swisstopo.add_ortho_plane(
|
||||
d, ortho_path, tile_bbox,
|
||||
origin_shift, m_to_unit, z_doc=plane_z)
|
||||
if obj: ortho_objs.append(obj)
|
||||
except Exception as ex:
|
||||
self._push_log("Ortho-Apply: {}".format(ex))
|
||||
self._push_log("→ {} Ortho-Plane(s) erstellt".format(len(ortho_objs)))
|
||||
# Layer (gleicher Geschoss-Sublayer 80_swisstopo wie Terrain)
|
||||
if z_id and ortho_objs:
|
||||
sub_name = _find_ebene_sublayer_name(
|
||||
d, ["swisstopo", "gelaende_topo"],
|
||||
"80", "swisstopo",
|
||||
default_color="#909090", default_lw=0.18)
|
||||
self._move_to_sublayer(d, ortho_objs, z_id,
|
||||
sub_name.split("_", 1)[0], tag="ortho",
|
||||
fallback_name=sub_name,
|
||||
fallback_color="#909090")
|
||||
elif ortho_objs:
|
||||
self._tag_objects(d, ortho_objs, "ortho")
|
||||
new_obj_ids.extend(o.Id for o in ortho_objs)
|
||||
new_obj_ids.extend(o.Id for o, _ in mesh_objects)
|
||||
|
||||
self._push_log("Import fertig: {} neue Objekte".format(len(new_obj_ids)))
|
||||
@@ -6999,6 +7087,57 @@ class ElementeBridge(panel_base.BaseBridge):
|
||||
finally:
|
||||
sc.sticky["dossier_swisstopo_busy"] = False
|
||||
|
||||
def _apply_xform_fast(self, doc, objs, scale_factor=1.0,
|
||||
translate=None):
|
||||
"""Scale+Move via Rhinos eingebaute _-Scale/_-Move Commands
|
||||
auf einer Selektion. Die sind C++-intern hochoptimiert und
|
||||
deutlich schneller als RhinoCommon API-Calls — bei 7000+
|
||||
Objekten Sekunden statt Minuten.
|
||||
|
||||
Scale-Syntax: 3-Punkt-Form `_-Scale base ref target` mit
|
||||
ref=1 Einheit, target=N Einheiten → Faktor N eindeutig.
|
||||
Move-Syntax: 2-Punkt-Form `_-Move base target`."""
|
||||
if not objs: return True
|
||||
need_scale = abs(scale_factor - 1.0) > 1e-6
|
||||
need_move = translate is not None and any(
|
||||
abs(v) > 1e-9 for v in translate)
|
||||
if not (need_scale or need_move): return True
|
||||
try:
|
||||
# Selektion via Batch-Select
|
||||
doc.Objects.UnselectAll()
|
||||
from System.Collections.Generic import List as _List
|
||||
from System import Guid as _Guid
|
||||
sel = _List[_Guid]()
|
||||
for o in objs: sel.Add(o.Id)
|
||||
try: n_sel = doc.Objects.Select(sel, True)
|
||||
except Exception:
|
||||
n_sel = 0
|
||||
for o in objs:
|
||||
if doc.Objects.Select(o.Id, True): n_sel += 1
|
||||
self._push_log(" {} Obj selektiert".format(n_sel))
|
||||
# Scale: 3-Punkt
|
||||
if need_scale:
|
||||
cmd = "_-Scale 0,0,0 1,0,0 {:.0f},0,0 _Enter".format(
|
||||
scale_factor)
|
||||
ok = Rhino.RhinoApp.RunScript(cmd, False)
|
||||
self._push_log(" _-Scale {:g}× → {}".format(
|
||||
scale_factor, ok))
|
||||
# Move: 2-Punkt
|
||||
if need_move:
|
||||
dx, dy, dz = translate
|
||||
cmd = "_-Move 0,0,0 {:.6f},{:.6f},{:.6f} _Enter".format(
|
||||
dx, dy, dz)
|
||||
ok = Rhino.RhinoApp.RunScript(cmd, False)
|
||||
self._push_log(" _-Move {} → {}".format(
|
||||
(round(dx), round(dy), round(dz)), ok))
|
||||
doc.Objects.UnselectAll()
|
||||
return True
|
||||
except Exception as ex:
|
||||
self._push_log(" _apply_xform_fast: {}".format(ex))
|
||||
try: doc.Objects.UnselectAll()
|
||||
except Exception: pass
|
||||
return False
|
||||
|
||||
def _tag_objects(self, doc, objs, tag):
|
||||
"""Setzt nur den dossier_swisstopo_kind UserString — fuer
|
||||
den Fall dass kein Geschoss aktiv ist und wir den Layer-Move
|
||||
@@ -7011,11 +7150,201 @@ class ElementeBridge(panel_base.BaseBridge):
|
||||
doc.Objects.ModifyAttributes(o, attrs, True)
|
||||
except Exception: pass
|
||||
|
||||
def _move_to_sublayer(self, doc, objs, z_id, code, tag=None):
|
||||
def _ensure_sub_sublayer(self, doc, parent_id, name,
|
||||
color_hex="#888888", lw=0.25):
|
||||
"""Findet oder erstellt einen Sub-Layer mit Name <name> direkt
|
||||
unter parent_id. Liefert layer_index oder -1."""
|
||||
try:
|
||||
import System.Drawing as SD
|
||||
for i in range(doc.Layers.Count):
|
||||
lay = doc.Layers[i]
|
||||
if lay is None or lay.IsDeleted: continue
|
||||
if lay.ParentLayerId == parent_id and lay.Name == name:
|
||||
return i
|
||||
new_lay = Rhino.DocObjects.Layer()
|
||||
new_lay.Name = name
|
||||
new_lay.ParentLayerId = parent_id
|
||||
try:
|
||||
h = color_hex.lstrip("#")
|
||||
r = int(h[0:2], 16); g = int(h[2:4], 16); b = int(h[4:6], 16)
|
||||
new_lay.Color = SD.Color.FromArgb(255, r, g, b)
|
||||
except Exception: pass
|
||||
try: new_lay.PlotWeight = float(lw)
|
||||
except Exception: pass
|
||||
return doc.Layers.Add(new_lay)
|
||||
except Exception as ex:
|
||||
self._push_log("ensure_sub_sublayer: {}".format(ex))
|
||||
return -1
|
||||
|
||||
def _ensure_swissbuildings_ebene(self, doc, with_children=True):
|
||||
"""Stellt sicher dass 81_Swissbuildings in dossier_ebenen
|
||||
existiert. Bei with_children=True (separated-Variante) auch
|
||||
die vier Children Build/Roof/Wall/Floor; bei False (solid)
|
||||
bleibt sie ein flacher Layer ohne Sub-Aufteilung.
|
||||
Triggert build_layers synchron, damit die Rhino-Layer real
|
||||
existieren bevor wir Objekte verschieben.
|
||||
Liefert {build,roof,wall,floor} → Sub-Sub-Layer-Code wenn
|
||||
with_children=True, sonst {}."""
|
||||
CHILD_SPEC = [
|
||||
("8101", "Build", "#888888", "build"),
|
||||
("8102", "Roof", "#a64d4d", "roof"),
|
||||
("8103", "Wall", "#666666", "wall"),
|
||||
("8104", "Floor", "#555555", "floor"),
|
||||
]
|
||||
raw = doc.Strings.GetValue("dossier_ebenen")
|
||||
try: ebenen = json.loads(raw) if raw else []
|
||||
except Exception: ebenen = []
|
||||
if not isinstance(ebenen, list): ebenen = []
|
||||
sb = next((e for e in ebenen if isinstance(e, dict)
|
||||
and e.get("code") == "81"), None)
|
||||
changed = False
|
||||
if sb is None:
|
||||
sb = {
|
||||
"code": "81", "name": "Swissbuildings",
|
||||
"color": "#888888", "lw": 0.25,
|
||||
"visible": True, "locked": False,
|
||||
"children": [],
|
||||
}
|
||||
ebenen.append(sb)
|
||||
changed = True
|
||||
if with_children:
|
||||
if not isinstance(sb.get("children"), list):
|
||||
sb["children"] = []
|
||||
changed = True
|
||||
have_codes = {c.get("code") for c in sb["children"]
|
||||
if isinstance(c, dict)}
|
||||
for ccode, cname, ccol, _key in CHILD_SPEC:
|
||||
if ccode not in have_codes:
|
||||
sb["children"].append({
|
||||
"code": ccode, "name": cname, "color": ccol,
|
||||
"lw": 0.25, "visible": True, "locked": False,
|
||||
})
|
||||
changed = True
|
||||
if changed:
|
||||
try:
|
||||
doc.Strings.SetString("dossier_ebenen",
|
||||
json.dumps(ebenen, ensure_ascii=False))
|
||||
except Exception as ex:
|
||||
self._push_log("save dossier_ebenen: {}".format(ex))
|
||||
# Layers synchron erzeugen
|
||||
try:
|
||||
import layer_builder
|
||||
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
|
||||
zlist = json.loads(z_raw) if z_raw else []
|
||||
if zlist:
|
||||
layer_builder.build_layers(doc, zlist, ebenen)
|
||||
except Exception as ex:
|
||||
self._push_log("build_layers: {}".format(ex))
|
||||
# UI informieren — broadcast_state schickt STATE_SYNC an
|
||||
# ebenen_bridge_ref + zeichnungsebenen_bridge_ref
|
||||
try:
|
||||
import rhinopanel
|
||||
rhinopanel._broadcast_state(doc)
|
||||
except Exception as ex:
|
||||
self._push_log("broadcast_state: {}".format(ex))
|
||||
if not with_children: return {}
|
||||
return {key: ccode for ccode, _n, _col, key in CHILD_SPEC}
|
||||
|
||||
def _consolidate_buildings(self, doc, objs, z_id,
|
||||
target_code="81",
|
||||
target_name="Swissbuildings",
|
||||
variant="separated"):
|
||||
"""Verschiebt Buildings auf den 81_Swissbuildings-Layer.
|
||||
- separated: Sub-Sub-Layer Build/Roof/Wall/Floor basierend
|
||||
auf dem DWG-Source-Layer-Prefix.
|
||||
- solid: alles direkt auf den Parent-Sublayer (keine Children).
|
||||
Loescht leere DWG-Source-Layer am Ende."""
|
||||
if not objs: return
|
||||
solid = (variant == "solid")
|
||||
try:
|
||||
import layer_builder
|
||||
# Ebene + Children (bei separated) sicherstellen + bauen
|
||||
child_codes = self._ensure_swissbuildings_ebene(
|
||||
doc, with_children=not solid)
|
||||
parent_idx = layer_builder._find_top_by_id(doc, z_id)
|
||||
if parent_idx < 0:
|
||||
self._push_log(" Geschoss nicht gefunden"); return
|
||||
parent_id = doc.Layers[parent_idx].Id
|
||||
base_idx = layer_builder._find_sublayer_by_code(
|
||||
doc, parent_id, target_code)
|
||||
if base_idx < 0:
|
||||
self._push_log(" 81_Swissbuildings nicht im aktiven Geschoss")
|
||||
return
|
||||
base_id = doc.Layers[base_idx].Id
|
||||
# Target-Mapping
|
||||
if solid:
|
||||
# Alle Objekte landen direkt auf base_idx
|
||||
target = {"all": base_idx}
|
||||
else:
|
||||
target = {}
|
||||
for key, ccode in child_codes.items():
|
||||
idx = layer_builder._find_sublayer_by_code(
|
||||
doc, base_id, ccode)
|
||||
if idx >= 0: target[key] = idx
|
||||
if not target:
|
||||
self._push_log(" Children-Layer fehlen — Build_layers nicht durchgelaufen?")
|
||||
return
|
||||
# Objekte umlayern
|
||||
source_indices = set()
|
||||
counts = {k: 0 for k in target}
|
||||
for o in objs:
|
||||
try:
|
||||
src_idx = o.Attributes.LayerIndex
|
||||
source_indices.add(src_idx)
|
||||
if solid:
|
||||
tgt_idx = target["all"]
|
||||
counts["all"] += 1
|
||||
else:
|
||||
src_name = doc.Layers[src_idx].Name.lower()
|
||||
tgt_idx = None
|
||||
for key in ("roof", "wall", "floor", "build"):
|
||||
if src_name.startswith(key):
|
||||
tgt_idx = target.get(key)
|
||||
if tgt_idx is not None: counts[key] += 1
|
||||
break
|
||||
if tgt_idx is None:
|
||||
tgt_idx = target.get("build")
|
||||
if tgt_idx is None: continue
|
||||
attrs = o.Attributes.Duplicate()
|
||||
attrs.LayerIndex = tgt_idx
|
||||
attrs.SetUserString("dossier_swisstopo_kind",
|
||||
"buildings")
|
||||
doc.Objects.ModifyAttributes(o, attrs, True)
|
||||
except Exception: pass
|
||||
for key, n in counts.items():
|
||||
if n > 0:
|
||||
self._push_log(" → {} Obj auf '{}'".format(
|
||||
n, doc.Layers[target[key]].FullPath))
|
||||
# Leere DWG-Source-Layer loeschen (descending index)
|
||||
target_set = set(target.values())
|
||||
deleted = 0
|
||||
for src_idx in sorted(source_indices, reverse=True):
|
||||
if src_idx in target_set: continue
|
||||
try:
|
||||
lay = doc.Layers[src_idx]
|
||||
if lay is None or lay.IsDeleted: continue
|
||||
has = False
|
||||
for o in doc.Objects:
|
||||
if o and not o.IsDeleted \
|
||||
and o.Attributes.LayerIndex == src_idx:
|
||||
has = True; break
|
||||
if not has:
|
||||
if doc.Layers.Delete(src_idx, True): deleted += 1
|
||||
except Exception: pass
|
||||
if deleted:
|
||||
self._push_log(" {} leere Source-Layer geloescht".format(deleted))
|
||||
except Exception as ex:
|
||||
self._push_log("Konsolidieren: {}".format(ex))
|
||||
|
||||
def _move_to_sublayer(self, doc, objs, z_id, code, tag=None,
|
||||
fallback_name=None, fallback_color="#888888"):
|
||||
"""Verschiebt Liste von Rhino-Objekten auf den DOSSIER-Sublayer
|
||||
<z_id>/<code>_*. Optional: Tag (UserString
|
||||
dossier_swisstopo_kind) setzen — wird beim naechsten Import
|
||||
erkannt + ggf. geloescht."""
|
||||
erkannt + ggf. geloescht.
|
||||
fallback_name: wenn Sublayer noch nicht existiert (Ebene wurde
|
||||
gerade erst angelegt, build_layers noch nicht gelaufen), wird
|
||||
er hiermit erzeugt — sonst landen Objekte gar nirgends."""
|
||||
if not objs: return
|
||||
try:
|
||||
import layer_builder
|
||||
@@ -7023,6 +7352,15 @@ class ElementeBridge(panel_base.BaseBridge):
|
||||
if parent_idx < 0: return
|
||||
parent_id = doc.Layers[parent_idx].Id
|
||||
sub_idx = layer_builder._find_sublayer_by_code(doc, parent_id, code)
|
||||
if sub_idx < 0 and fallback_name:
|
||||
sub_idx = self._ensure_sub_sublayer(
|
||||
doc, parent_id, fallback_name,
|
||||
color_hex=fallback_color)
|
||||
if sub_idx >= 0:
|
||||
try:
|
||||
doc.Layers[sub_idx].SetUserString(
|
||||
"dossier_code", code)
|
||||
except Exception: pass
|
||||
if sub_idx < 0: return
|
||||
n = 0
|
||||
for o in objs:
|
||||
|
||||
Reference in New Issue
Block a user