Swisstopo Importer: STAC-API + Terrain-Mesh + Ortho-Drape (Iteration 1)

Frontend:
- src/SwisstopoApp.jsx NEU: Satelliten-Fenster mit Adresse-Suche, Radius-
  Wahl, Daten-Checkboxen (Gebäude/Terrain/Luftbild), Origin-Handling, Live-
  Log
- ElementeApp Swisstopo-Gruppe: Importer-Button + Karte-Button

Backend:
- rhino/swisstopo.py NEU: STAC-API-Client, Geocoding via swisstopo Search,
  LV95↔WGS84-Konvertierung, GeoTIFF/XYZ-Cache, mesh_from_grid + Ortho-Material
- swissBUILDINGS3D 2.0 (DXF/DWG) via STAC; Per-Tile-Filter (_NNNN-NN_)
  schuetzt vor versehentlichem Download der 3.5 GB National-Geodatabase
- swissALTI3D als XYZ-ZIP mit zipfile-Extraction, raeumliches Sub-Sampling
  statt Zeilen-Step (keine Streifen mehr im Terrain-Mesh)
- SWISSIMAGE 10cm GeoTIFF als RenderMaterial-DiffuseBitmap mit planarem
  UV-Mapping auf den Terrain-Mesh-bbox

Robustheit:
- Auto-Skala-Erkennung: Rhinos DXF-Parser scaliert je nach \$INSUNITS auf
  unerwartete Doc-Units; wir messen aus ersten 50 Objekten + snappen auf
  Zehnerpotenz (1, 0.001, 1000)
- bbox + origin_shift in doc-units (m_to_unit aus UnitScale + Auto-Detect)
- Tags via UserString dossier_swisstopo_kind=buildings/terrain fuer
  Replace-Detection bei erneutem Import desselben Gebiets
- BBox-Clip jetzt OPTIONAL (Default OFF, Checkbox) — bei InstanceReferences
  GetBoundingBox + Delete teuer
- Batch-Transform via System.Collections.Generic.List[Guid] statt
  Python-Loop (Python.NET-Overload-Match)
- Listener-Suppression in elemente.py + gestaltung.py + dimensionen.py
  via sticky dossier_swisstopo_busy — kein Per-Object-Spam mehr bei
  Selection/Add/Delete waehrend 5000+ Imports
- Auto-Zoom via view.ZoomBoundingBox(combined) statt Select-Loop
- Year-Dedupe auf Tile-Coord (Pattern YYYY oder YYYY-MM unterstuetzt) fuer
  alle Collections — aeltere Versionen werden ausgefiltert
- Download-Safety: > 200 MB wird abgebrochen + Live-Progress alle 2 MB
  mit UI-Yield via Rhino.RhinoApp.Wait()

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-19 18:22:48 +02:00
parent 41b6f8ac51
commit 4111f12f32
8 changed files with 1557 additions and 0 deletions
+544
View File
@@ -4628,6 +4628,9 @@ 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 == "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)
elif t == "UPDATE_WALL": self._update_wall(p)
elif t == "UPDATE_ELEMENT": self._update_wall(p) # gleiche Logik fuer alle
elif t == "DELETE_WALL": self._delete_wall(p.get("id"))
@@ -6507,6 +6510,540 @@ class ElementeBridge(panel_base.BaseBridge):
except Exception as ex:
print("[ELEMENTE] CSV schreiben:", ex)
# ------------------------------------------------------------------
# Swisstopo — Option-A-Workflow:
# 1) "Karte oeffnen": map.geo.admin.ch im Browser mit vorausgewaehlten
# Layern swissALTI3D + swissBUILDINGS3D. User waehlt sein Gebiet,
# laedt DWG/OBJ/DAE runter.
# 2) "Importieren": File-Picker -> Rhinos _-Import -> Plugin verschiebt
# die NEU importierten Objekte auf den gewuenschten DOSSIER-Sublayer
# unter dem aktiven Geschoss.
# ------------------------------------------------------------------
def _cmd_open_swisstopo(self, p):
"""Oeffnet map.geo.admin.ch im Default-Browser mit den swisstopo-
Layern voreingestellt. Param `mode`: 'buildings' | 'terrain' | 'both'.
Optionaler `center`: 'CH1903_E,CH1903_N' fuer initiale Karten-
Position; sonst Default-Center (Schweiz Mitte)."""
import subprocess
mode = (p.get("mode") or "both").lower()
if mode == "buildings":
layers = "ch.swisstopo.swissbuildings3d_3"
elif mode == "terrain":
layers = "ch.swisstopo.swissalti3d"
else:
layers = "ch.swisstopo.swissalti3d,ch.swisstopo.swissbuildings3d_3"
# Default-Zentrum (Schweiz Mitte LV95)
center = (p.get("center") or "2660000,1190000").strip()
try:
E, N = [s.strip() for s in center.split(",", 1)]
except Exception:
E, N = "2660000", "1190000"
url = ("https://map.geo.admin.ch/"
"?lang=de&topic=ech&bgLayer=ch.swisstopo.pixelkarte-farbe"
"&E={}&N={}&zoom=8&layers={}").format(E, N, layers)
try:
subprocess.Popen(["open", url])
print("[ELEMENTE] Swisstopo Karte geoeffnet:", url)
except Exception as ex:
print("[ELEMENTE] Karte oeffnen fehlgeschlagen:", ex)
def _cmd_import_swisstopo(self, p):
"""File-Picker -> Rhino _-Import -> bewege neue Objekte auf den
DOSSIER-Sublayer. `kind`: 'buildings' (12_Gebaeude) | 'terrain'
(10_Situation) | 'vermessung' (01_Vermessung) | 'other' (aktiver
Layer wird nicht geaendert)."""
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return
kind = (p.get("kind") or "buildings").lower()
# Target-Sublayer ableiten (auto-anlegen wenn nicht vorhanden)
if kind == "buildings":
sub_name = _find_ebene_sublayer_name(
doc, ["gebaeude", "gebäude", "buildings"],
"12", "Gebäude", default_color="#888888", default_lw=0.25)
elif kind == "terrain":
sub_name = _find_ebene_sublayer_name(
doc, ["situation", "terrain", "gelaende"],
"10", "Situation", default_color="#909090", default_lw=0.18)
elif kind == "vermessung":
sub_name = _find_ebene_sublayer_name(
doc, ["vermessung", "survey"],
"01", "Vermessung", default_color="#707078", default_lw=0.18)
else:
sub_name = None
# File-Picker
try:
from Rhino.UI import OpenFileDialog
ofd = OpenFileDialog()
ofd.Filter = ("Swisstopo Imports (*.dwg;*.dxf;*.obj;*.dae;*.ifc;*.3dm;*.ply;*.stl)|"
"*.dwg;*.dxf;*.obj;*.dae;*.ifc;*.3dm;*.ply;*.stl|"
"Alle Dateien (*.*)|*.*")
ok = False
try: ok = ofd.ShowOpenDialog()
except Exception:
try: ok = ofd.ShowDialog()
except Exception: ok = False
if not ok:
print("[ELEMENTE] Swisstopo-Import abgebrochen"); return
path = ofd.FileName
except Exception as ex:
print("[ELEMENTE] OpenFileDialog:", ex); return
if not path or not os.path.isfile(path):
print("[ELEMENTE] Pfad ungueltig:", path); return
# Snapshot vor Import: existierende Object-IDs
before_ids = set()
try:
for obj in doc.Objects:
if obj is None or obj.IsDeleted: continue
before_ids.add(obj.Id)
except Exception: pass
# Pfad fuer Rhino-Command escapen (Spaces!)
cmd_path = path.replace('"', '\\"')
cmd = '_-Import "{}" _Enter'.format(cmd_path)
print("[ELEMENTE] Swisstopo-Import:", cmd)
try:
Rhino.RhinoApp.RunScript(cmd, False)
except Exception as ex:
print("[ELEMENTE] _-Import fehlgeschlagen:", ex); return
# Differenz: neu hinzugekommene Objekte
new_objs = []
try:
for obj in doc.Objects:
if obj is None or obj.IsDeleted: continue
if obj.Id not in before_ids:
new_objs.append(obj)
except Exception: pass
print("[ELEMENTE] Swisstopo-Import: {} neue Objekte".format(len(new_objs)))
if not new_objs: return
# Target-Layer finden + Objekte verschieben (nur wenn sub_name gesetzt)
if sub_name:
z_id = doc.Strings.GetValue("dossier_active_id")
if not z_id:
print("[ELEMENTE] Swisstopo: kein aktives Geschoss — Objekte "
"bleiben auf Import-Default-Layer"); return
try:
import layer_builder
parent_idx = layer_builder._find_top_by_id(doc, z_id)
if parent_idx < 0:
print("[ELEMENTE] Swisstopo: Parent-Layer nicht gefunden")
return
parent_id = doc.Layers[parent_idx].Id
code = sub_name.split("_", 1)[0]
sub_idx = layer_builder._find_sublayer_by_code(doc, parent_id, code)
if sub_idx < 0:
print("[ELEMENTE] Swisstopo: Sublayer {} nicht gefunden "
"— bitte erst Ebenen-Apply ausloesen".format(code))
return
moved = 0
for obj in new_objs:
try:
attrs = obj.Attributes.Duplicate()
attrs.LayerIndex = sub_idx
if doc.Objects.ModifyAttributes(obj, attrs, True):
moved += 1
except Exception: pass
print("[ELEMENTE] Swisstopo: {} Objekte auf {} verschoben".format(
moved, doc.Layers[sub_idx].FullPath))
except Exception as ex:
print("[ELEMENTE] Layer-Move fehler:", ex)
try: doc.Views.Redraw()
except Exception: pass
def _cmd_open_swisstopo_dialog(self, p):
"""Oeffnet das volle Swisstopo-Importer-Satelliten-Fenster mit API-
Anbindung (Adresse-Suche, Auto-Tiles, Terrain+Orthofoto)."""
outer = self
bridge_holder = {"form": None}
# Initial-State fuer den Dialog: aktuelle Ebenen-Liste + Default-
# Layer-Codes fuer die Auto-Sublayer-Erkennung
doc = Rhino.RhinoDoc.ActiveDoc
try:
e_raw = doc.Strings.GetValue("dossier_ebenen") if doc else None
ebenen = json.loads(e_raw) if e_raw else []
except Exception: ebenen = []
class _SwisstopoBridge(panel_base.BaseBridge):
def __init__(self):
panel_base.BaseBridge.__init__(self, "swisstopo")
def _on_ready(self):
self.send("SWISSTOPO_STATE", {
"ebenen": ebenen,
"cacheDir": __import__("swisstopo").CACHE_DIR,
})
def _push_log(self, msg):
try: self.send("SWISSTOPO_LOG", {"msg": str(msg)})
except Exception: pass
def handle(self, data):
if not isinstance(data, dict): return
t = data.get("type", "")
pp = data.get("payload") or {}
if t == "READY":
self._on_ready()
elif t == "GEOCODE":
import swisstopo
res = swisstopo.geocode(pp.get("text") or "")
self.send("GEOCODE_RESULT", {"result": res})
elif t == "RUN_IMPORT":
self._run_import(pp)
elif t == "CANCEL":
try:
f = bridge_holder.get("form")
if f is not None: f.Close()
except Exception: pass
def _run_import(self, opts):
"""opts = {centerE, centerN, radius, kinds: ['buildings','terrain','ortho'],
shiftToOrigin, autoZoom,
layerBuildings, layerTerrain, terrainResolution}"""
d = Rhino.RhinoDoc.ActiveDoc
if d is None:
self._push_log("Kein aktives Doc"); return
try:
import swisstopo, layer_builder
except Exception as ex:
self._push_log("Module-Import-Fehler: {}".format(ex)); return
try:
eC = float(opts.get("centerE")); nC = float(opts.get("centerN"))
r = float(opts.get("radius") or 100)
except Exception:
self._push_log("Center/Radius ungueltig"); return
kinds = set(opts.get("kinds") or ["buildings"])
shift = bool(opts.get("shiftToOrigin", True))
replace_existing = bool(opts.get("replaceExisting", True))
clip_to_bbox = bool(opts.get("clipToBbox", False))
# Doc-Unit-Skalierung. LV95-Werte sind in Metern. Wenn der
# Rhino-Doc in mm/km/inch laeuft, muessen wir die bbox + den
# shift in Doc-Units umrechnen — sonst stimmt der Clip-Check
# mit den (auto-skaliert importierten) DXF-Coords nicht ueberein.
try:
m_to_unit = Rhino.RhinoMath.UnitScale(
Rhino.UnitSystem.Meters, d.ModelUnitSystem)
except Exception:
m_to_unit = 1.0
eC_u = eC * m_to_unit
nC_u = nC * m_to_unit
r_u = r * m_to_unit
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)
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:
self._push_log("Doc-Unit: {} → m_to_unit={} (Skalierung aktiv)".format(
d.ModelUnitSystem, m_to_unit))
z_id = d.Strings.GetValue("dossier_active_id")
if not z_id:
self._push_log("Achtung: kein aktives Geschoss — Objekte bleiben auf Default-Layer")
# Bestehende swisstopo-Objekte loeschen wenn gewuenscht.
# Tag wird beim Import gesetzt (UserString dossier_swisstopo_kind).
if replace_existing:
self._push_log("Loesche bestehende swisstopo-Objekte (alte Imports)...")
removed = 0
for obj in list(d.Objects):
if obj is None or obj.IsDeleted: continue
try:
tag = obj.Attributes.GetUserString("dossier_swisstopo_kind")
except Exception: tag = None
if tag:
d.Objects.Delete(obj.Id, True)
removed += 1
self._push_log("{} alte swisstopo-Objekte geloescht".format(removed))
new_obj_ids = []
def _track_new(before_ids):
out = []
for obj in d.Objects:
if obj is None or obj.IsDeleted: continue
if obj.Id not in before_ids: out.append(obj)
return out
before_all = set(o.Id for o in d.Objects if o and not o.IsDeleted)
# Listener-Suppression: elemente.py + gestaltung.py haben Add/
# Replace-Listener die pro neu importiertem Objekt feuern. Bei
# 5000+ DXF-Objekten erstickt das den Import. Sticky-Flag setzen,
# die Listener bailen früh (siehe _is_swisstopo_busy unten).
sc.sticky["dossier_swisstopo_busy"] = True
try:
# --- Buildings (DWG) -----------------------------------
if "buildings" in kinds:
paths = swisstopo.fetch_buildings_dwg(bbox, progress=self._push_log)
for idx, p in enumerate(paths):
try: size_mb = os.path.getsize(p) / 1e6
except Exception: size_mb = 0
self._push_log("Import {}/{}: {} ({:.0f} MB) — Rhinos DXF-Parser, kann ein paar Sekunden dauern...".format(
idx + 1, len(paths), os.path.basename(p), size_mb))
try: swisstopo._yield_ui()
except Exception: pass
before = set(o.Id for o in d.Objects if o and not o.IsDeleted)
cmd = '_-Import "{}" _Enter'.format(p.replace('"', '\\"'))
try: Rhino.RhinoApp.RunScript(cmd, False)
except Exception as ex:
self._push_log("Import {}: {}".format(p, ex)); continue
new = _track_new(before)
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.
if new and idx == 0:
try:
import math as _m
samples = 0; sum_x = 0.0
for o in new[:50]:
bb = o.Geometry.GetBoundingBox(True)
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)
except Exception as ex:
self._push_log("Auto-Skala-Erkennung: {}".format(ex))
# Diagnose
if new:
try:
obb0 = new[0].Geometry.GetBoundingBox(True)
self._push_log(" Erste obj bbox (Doc-Units): "
"({:.3f},{:.3f},{:.3f}) - ({:.3f},{:.3f},{:.3f})".format(
obb0.Min.X, obb0.Min.Y, obb0.Min.Z,
obb0.Max.X, obb0.Max.Y, obb0.Max.Z))
self._push_log(" bbox_doc (nach Auto-Skala): "
"{:.3f}-{:.3f} / {:.3f}-{:.3f}".format(*bbox_doc))
except Exception: pass
# Post-Clip nur wenn User es will (Default OFF) — bei
# InstanceReferences ist GetBoundingBox + Delete teuer.
# Tile = 1km², User-radius typisch 100m → ohne Clip
# hast du das ganze Dorf, aber Import bleibt schnell.
if clip_to_bbox:
self._push_log("→ Clippe auf User-bbox...")
kept = []
outside = []
for o in new:
try:
obb = o.Geometry.GetBoundingBox(True)
cx = (obb.Min.X + obb.Max.X) * 0.5
cy = (obb.Min.Y + obb.Max.Y) * 0.5
if (bbox_doc[0] <= cx <= bbox_doc[2] and
bbox_doc[1] <= cy <= bbox_doc[3]):
kept.append(o)
else:
outside.append(o)
except Exception:
kept.append(o)
if not kept and outside:
self._push_log(" → Clip waere {}/{} → bbox passt nicht zu Doc-Coords, alle behalten".format(
len(outside), len(new)))
kept = new
else:
# Batch-Delete: deutlich schneller als per-obj
out_ids = [o.Id for o in outside]
for oid in out_ids:
try: d.Objects.Delete(oid, True)
except Exception: pass
self._push_log("{} behalten, {} ausserhalb bbox geloescht".format(
len(kept), len(out_ids)))
else:
# Kein Clip — alle behalten
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
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")
else:
# Kein aktives Geschoss → nur Tag setzen
self._tag_objects(d, kept, "buildings")
new_obj_ids.extend(o.Id for o in kept)
# --- Terrain (XYZ → Mesh) ------------------------------
if "terrain" in kinds:
res = (opts.get("terrainResolution") or "2.0").strip()
try: target_step = float(res)
except Exception: target_step = 2.0
xyz_paths = swisstopo.fetch_terrain_xyz(
bbox, resolution=res, progress=self._push_log)
mesh_objects = []
for p in xyz_paths:
self._push_log("Mesh aus {}...".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!
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"]))
except Exception as ex:
self._push_log("Mesh-Bau fehlgeschlagen: {}".format(ex))
# Layer-Move + Ortho-Drape
if z_id and mesh_objects:
sub_name = _find_ebene_sublayer_name(
d, ["situation", "terrain", "gelaende"],
"10", "Situation",
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")
elif mesh_objects:
objs = [m[0] for m in mesh_objects]
self._tag_objects(d, objs, "terrain")
if "ortho" in kinds and mesh_objects:
self._push_log("Hole Orthofoto...")
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:
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)
except Exception as ex:
self._push_log("Ortho-Apply: {}".format(ex))
new_obj_ids.extend(o.Id for o, _ in mesh_objects)
self._push_log("Import fertig: {} neue Objekte".format(len(new_obj_ids)))
# Auto-Zoom NOCH IM TRY-Block: sticky-Flag bleibt True
# damit der Select-Roundtrip nicht 3000 Listener weckt.
# ZoomBoundingBox + UnionBBox aller neuen → 1 API-Call
# statt Select-Loop.
if opts.get("autoZoom") and new_obj_ids:
try:
combined = rg.BoundingBox.Empty
for oid in new_obj_ids:
obj = d.Objects.Find(oid)
if obj is None: continue
try:
bb = obj.Geometry.GetBoundingBox(True)
if bb.IsValid: combined.Union(bb)
except Exception: pass
if combined.IsValid:
view = d.Views.ActiveView
if view is not None:
view.ActiveViewport.ZoomBoundingBox(combined)
except Exception as ex:
self._push_log("Auto-Zoom: {}".format(ex))
try: d.Views.Redraw()
except Exception: pass
self.send("IMPORT_DONE", {"count": len(new_obj_ids)})
finally:
sc.sticky["dossier_swisstopo_busy"] = 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
ueberspringen, aber den Marker fuers Replace-Erkennen
brauchen."""
for o in objs:
try:
attrs = o.Attributes.Duplicate()
attrs.SetUserString("dossier_swisstopo_kind", tag)
doc.Objects.ModifyAttributes(o, attrs, True)
except Exception: pass
def _move_to_sublayer(self, doc, objs, z_id, code, tag=None):
"""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."""
if not objs: return
try:
import layer_builder
parent_idx = layer_builder._find_top_by_id(doc, z_id)
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: return
n = 0
for o in objs:
try:
attrs = o.Attributes.Duplicate()
attrs.LayerIndex = sub_idx
if tag:
attrs.SetUserString("dossier_swisstopo_kind", tag)
if doc.Objects.ModifyAttributes(o, attrs, True): n += 1
except Exception: pass
self._push_log("{} Obj auf '{}'".format(n, doc.Layers[sub_idx].FullPath))
except Exception as ex:
self._push_log("Layer-Move: {}".format(ex))
b = _SwisstopoBridge()
bridge_holder["form"] = panel_base.open_satellite_window(
"swisstopo",
title="swisstopo Importer",
size=(560, 620),
bridge=b)
def _update_wall(self, p):
"""Properties eines Elements aendern (Wand/Decke/Dach/Oeffnung).
Volumen wird anschliessend regeneriert."""
@@ -7388,6 +7925,9 @@ def _on_object_added(sender, e):
UserStrings auf das neue Objekt mit. Source-Duplikate kriegen eine
neue UUID, Volume-Duplikate werden geloescht (Regen baut das neue
Volumen am richtigen Ort)."""
# Swisstopo-Import importiert tausende Objekte am Stueck — die haben
# keine DOSSIER-Metas, jeder Listener-Call ist reine Verschwendung.
if sc.sticky.get("dossier_swisstopo_busy"): return
if sc.sticky.get(_REGEN_BUSY): return
# Waehrend Move/Rotate/Mirror/Scale: Rhino feuert intern Delete+Add fuer
# jedes transformierte Objekt. CommandEnd uebernimmt die Re-Sync —
@@ -7492,6 +8032,8 @@ def _on_object_deleted(sender, e):
wenn die Source mit gleicher ID zurueckkommt (= Transform, kein User-
Delete).
"""
# Waehrend Swisstopo-Import: keine DOSSIER-Metas vorhanden, nur Overhead
if sc.sticky.get("dossier_swisstopo_busy"): return
# Waehrend Move/Rotate/Mirror/Scale: CommandEnd-Pfad uebernimmt das
# Re-Sync. Sonst queued der Delete-Event ueberfluessige Regen-Calls die
# den Pure-Translate-Skip wieder zunichtemachen.
@@ -7646,6 +8188,7 @@ def _on_select_objects(sender, e):
So bewegen sich beide synchron bei Move/Gumball, und die Endpunkte
der Lauflinie sind als Grips zum Drag verfuegbar."""
if sc.sticky.get("dossier_swisstopo_busy"): return
if sc.sticky.get(_SELECT_BUSY): return
if sc.sticky.get(_REGEN_BUSY): return
try:
@@ -7680,6 +8223,7 @@ def _on_deselect_objects(sender, e):
"""Bidirektional zu _on_select_objects:
- Volume deselektiert Source deselektieren + Grips aus
- Source deselektiert Volume deselektieren + Grips aus"""
if sc.sticky.get("dossier_swisstopo_busy"): return
if sc.sticky.get(_SELECT_BUSY): return
if sc.sticky.get(_REGEN_BUSY): return
try: