Swisstopo + OSM Importer + Höhenlinien + Bulk-Op Performance
Swisstopo Iter 3:
- Ortho-Drape: TIN-Mesh aus Terrain-Grid mit per-vertex UVs + PictureFrame-Material
- Project-Cache: TIFs werden neben .3dm gespeichert (SMB-shareable)
- Layer-Restruktur: 80_swisstopo/{Terrain, Luftbild} Sub-Ebenen
- TIFs direkt (kein PNG-Downsampling) für volle Auflösung
- UV-Inset gegen weisse Streifen zwischen Kacheln
- Hoehenlinien (2D, swissALTI3D) auf aktives Geschoss OKFF projiziert
- TIN-Mesh + Schichtenmodell aus Contours (separate Optionen)
- TLM3D entfernt (swisstopo liefert nur GDB/SHP, kein DXF)
OSM Importer (neu):
- rhino/osm.py: Overpass-API-Client
- src/OsmApp.jsx: React-Dialog mit Adresse + Radius + 7 Kategorien
- Strassen/Gebäude/Wasser/Wasserläufe/Parks/Wald/Fusswege (Codes 7101-7107)
- ElementeApp: PillGroup "Importer" mit Swisstopo + OSM Buttons
Sub-Ebenen — rekursiv durch hierarchische Ebenen:
- Visibility-Toggle: slimEbene rekursiv (children bleiben erhalten)
- Settings-Dialog: _find_sublayer_by_code_recursive + _replace_in_tree
- Hatch Auto-Fill: refresh_layer_fills + _fill_signature + _ebene_fill_for_layer
alle rekursiv durch children
- EbenenSettingsApp: flattenEbenen-Helper
Bulk-Op Performance (Delete/Cut/etc.):
- _USER_BULK_CMDS + _BULK_ACTIVE_KEY Sticky-Flag
- CommandBegin: doc.Views.RedrawEnabled = False + Listener-Bail aktiv
- CommandEnd: RedrawEnabled restore + 1× Redraw + Selection-Refresh
- Bail-outs in dimensionen.on_idle/on_select, elemente._on_idle_selection,
gestaltung.on_idle_flush/on_delete
- Verhindert das sichtbare "Runterzählen" pro Element bei Bulk-Delete
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+565
-90
@@ -4661,6 +4661,7 @@ class ElementeBridge(panel_base.BaseBridge):
|
||||
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 == "OPEN_OSM_DIALOG": self._cmd_open_osm_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"))
|
||||
@@ -6810,6 +6811,15 @@ class ElementeBridge(panel_base.BaseBridge):
|
||||
|
||||
before_all = set(o.Id for o in d.Objects if o and not o.IsDeleted)
|
||||
|
||||
# Cache-Folder pro Projekt setzen (neben der .3dm-Datei).
|
||||
# Damit reisen die Tiles mit dem Projekt — bei SMB-Sharing
|
||||
# findet Rhino die TIFs auch von anderen Maschinen, sofern
|
||||
# der Mount-Pfad identisch ist. Falls Doc unsaved: globaler
|
||||
# Cache.
|
||||
cache_dir = swisstopo.get_cache_dir_for_doc(d)
|
||||
swisstopo.set_cache_dir(cache_dir)
|
||||
self._push_log("Cache: {}".format(cache_dir))
|
||||
|
||||
# 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,
|
||||
@@ -6820,8 +6830,11 @@ class ElementeBridge(panel_base.BaseBridge):
|
||||
if "buildings" in kinds:
|
||||
variant = (opts.get("buildVariant") or "separated").strip().lower()
|
||||
if variant not in ("separated", "solid"): variant = "separated"
|
||||
version = (opts.get("buildVersion") or "v2").strip().lower()
|
||||
if version not in ("v2", "v3"): version = "v2"
|
||||
paths = swisstopo.fetch_buildings_dwg(
|
||||
bbox, progress=self._push_log, variant=variant)
|
||||
bbox, progress=self._push_log,
|
||||
variant=variant, version=version)
|
||||
for idx, p in enumerate(paths):
|
||||
try: size_mb = os.path.getsize(p) / 1e6
|
||||
except Exception: size_mb = 0
|
||||
@@ -6952,16 +6965,18 @@ class ElementeBridge(panel_base.BaseBridge):
|
||||
new_obj_ids.extend(o.Id for o in kept)
|
||||
|
||||
# --- Terrain (XYZ → Mesh) ------------------------------
|
||||
if "terrain" in kinds:
|
||||
# Terrain-Daten (XYZ + Grid) holen, sobald Mesh ODER
|
||||
# Hoehenlinien gewuenscht sind — beide nutzen das Grid.
|
||||
need_dem = any(k in kinds for k in
|
||||
("terrain", "contours", "contour_tin", "contour_schicht"))
|
||||
mesh_objects = []
|
||||
merged_grid = None
|
||||
if need_dem:
|
||||
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 = []
|
||||
# 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("Parse {}...".format(os.path.basename(p)))
|
||||
@@ -6980,20 +6995,137 @@ class ElementeBridge(panel_base.BaseBridge):
|
||||
if merged is None:
|
||||
self._push_log("Merge lieferte None")
|
||||
else:
|
||||
merged_grid = merged
|
||||
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))
|
||||
self._push_log("Grid-Merge fehlgeschlagen: {}".format(ex))
|
||||
|
||||
# 3D-Mesh bauen wenn Terrain gewuenscht — unabhaengig vom
|
||||
# Ortho. Wenn Ortho auch an ist: Drape-Mesh liegt ueber
|
||||
# dem Plain-Mesh (User togglet im Layer-Panel was er
|
||||
# sehen will).
|
||||
if "terrain" in kinds and merged_grid is not None:
|
||||
try:
|
||||
mesh = swisstopo.mesh_from_grid(
|
||||
merged_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, merged_grid["bbox"]))
|
||||
except Exception as ex:
|
||||
self._push_log("Mesh-Bau fehlgeschlagen: {}".format(ex))
|
||||
|
||||
# Contours sind die Grundlage fuer drei moegliche Outputs:
|
||||
# 'contours' → flache 2D-Curves auf OKFF
|
||||
# 'contour_tin' → TIN-Mesh aus Contour-Vertices
|
||||
# 'contour_schicht' → Planare Flaechen pro Hoehe
|
||||
# Wir generieren einmal die echten 3D-Curves und teilen
|
||||
# sie auf die drei Outputs auf.
|
||||
contour_kinds = ("contours", "contour_tin", "contour_schicht")
|
||||
need_contours = any(k in kinds for k in contour_kinds) and merged_grid is not None
|
||||
raw_contours = []
|
||||
if need_contours:
|
||||
try:
|
||||
interval_c = float(opts.get("contourInterval") or 2.0)
|
||||
except Exception: interval_c = 2.0
|
||||
try:
|
||||
self._push_log("Hoehenlinien generieren (Abstand {} m, real Z)...".format(interval_c))
|
||||
raw_contours = swisstopo.generate_contour_curves(
|
||||
merged_grid, origin_shift, m_to_unit,
|
||||
interval=interval_c,
|
||||
progress=self._push_log)
|
||||
except Exception as ex:
|
||||
self._push_log("Contour-Generation-Fehler: {}".format(ex))
|
||||
raw_contours = []
|
||||
|
||||
# 2D-Hoehenlinien auf OKFF des aktiven Geschosses
|
||||
if "contours" in kinds and raw_contours:
|
||||
project_zero_doc = 0.0 if shift else project_zero_mum * m_to_unit
|
||||
active_okff = 0.0
|
||||
try:
|
||||
z_raw = d.Strings.GetValue("dossier_zeichnungsebenen")
|
||||
zlist = json.loads(z_raw) if z_raw else []
|
||||
for z_ in zlist:
|
||||
if isinstance(z_, dict) and z_.get("id") == z_id:
|
||||
active_okff = float(z_.get("okff", 0) or 0)
|
||||
break
|
||||
except Exception: pass
|
||||
flatten_z_doc = project_zero_doc + active_okff * m_to_unit
|
||||
self._push_log("2D-Hoehenlinien auf OKFF Z={:.3f}...".format(flatten_z_doc))
|
||||
contour_objs = []
|
||||
for c in raw_contours:
|
||||
# Wichtig: duplizieren, damit das Original (mit
|
||||
# echtem Z) fuer TIN/Schichten erhalten bleibt.
|
||||
try:
|
||||
c_flat = c.DuplicateCurve()
|
||||
bb_c = c_flat.GetBoundingBox(True)
|
||||
z_mid = (bb_c.Min.Z + bb_c.Max.Z) * 0.5
|
||||
dz = flatten_z_doc - z_mid
|
||||
if abs(dz) > 1e-9:
|
||||
c_flat.Translate(rg.Vector3d(0, 0, dz))
|
||||
gid = d.Objects.AddCurve(c_flat)
|
||||
if gid and gid != System.Guid.Empty:
|
||||
ob = d.Objects.Find(gid)
|
||||
if ob: contour_objs.append(ob)
|
||||
except Exception: pass
|
||||
if z_id and contour_objs:
|
||||
self._move_to_sublayer(
|
||||
d, contour_objs, z_id, "14",
|
||||
tag="contour",
|
||||
fallback_name="14_Höhenlinien",
|
||||
fallback_color="#909050")
|
||||
elif contour_objs:
|
||||
self._tag_objects(d, contour_objs, "contour")
|
||||
self._push_log("→ {} Hoehenlinien (2D) auf '14_Höhenlinien'".format(
|
||||
len(contour_objs)))
|
||||
|
||||
# TIN-Mesh aus Hoehenlinien
|
||||
if "contour_tin" in kinds and raw_contours:
|
||||
try:
|
||||
tin_obj = swisstopo.generate_mesh_from_contours(
|
||||
d, raw_contours,
|
||||
m_to_unit=m_to_unit,
|
||||
progress=self._push_log)
|
||||
if tin_obj:
|
||||
# Tag + auf 80_swisstopo Parent
|
||||
at = tin_obj.Attributes.Duplicate()
|
||||
at.SetUserString("dossier_swisstopo_kind", "contour_tin")
|
||||
d.Objects.ModifyAttributes(tin_obj, at, True)
|
||||
if z_id:
|
||||
self._move_to_sublayer(
|
||||
d, [tin_obj], z_id, "80",
|
||||
tag="contour_tin",
|
||||
fallback_name="80_swisstopo",
|
||||
fallback_color="#909090")
|
||||
except Exception as ex:
|
||||
self._push_log("TIN-Mesh-Fehler: {}".format(ex))
|
||||
|
||||
# Schichtenmodell (planare Flaechen pro Hoehe)
|
||||
if "contour_schicht" in kinds and raw_contours:
|
||||
try:
|
||||
schicht_objs = swisstopo.generate_schichtenmodell(
|
||||
d, raw_contours, progress=self._push_log)
|
||||
for s in schicht_objs:
|
||||
try:
|
||||
at = s.Attributes.Duplicate()
|
||||
at.SetUserString("dossier_swisstopo_kind", "contour_schicht")
|
||||
d.Objects.ModifyAttributes(s, at, True)
|
||||
except Exception: pass
|
||||
if z_id and schicht_objs:
|
||||
self._move_to_sublayer(
|
||||
d, schicht_objs, z_id, "80",
|
||||
tag="contour_schicht",
|
||||
fallback_name="80_swisstopo",
|
||||
fallback_color="#909090")
|
||||
self._push_log("→ Schichtenmodell: {} Flaechen auf '80_swisstopo'".format(
|
||||
len(schicht_objs)))
|
||||
except Exception as ex:
|
||||
self._push_log("Schichtenmodell-Fehler: {}".format(ex))
|
||||
# Layer-Move auf aktive Geschoss/80_swisstopo Sublayer
|
||||
if z_id and mesh_objects:
|
||||
sub_name = _find_ebene_sublayer_name(
|
||||
@@ -7008,47 +7140,29 @@ class ElementeBridge(panel_base.BaseBridge):
|
||||
elif mesh_objects:
|
||||
objs = [m[0] for m in mesh_objects]
|
||||
self._tag_objects(d, objs, "terrain")
|
||||
if "ortho" in kinds and mesh_objects:
|
||||
if "ortho" in kinds and merged_grid is not None:
|
||||
self._push_log("Hole Orthofoto...")
|
||||
ortho_paths = swisstopo.fetch_orthophoto(
|
||||
bbox, resolution="2.0", progress=self._push_log)
|
||||
if ortho_paths:
|
||||
# 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:
|
||||
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))
|
||||
# Tile-IDs vorab extrahieren + Sub-Ebenen unter
|
||||
# 80_swisstopo registrieren BEVOR die Pictures
|
||||
# erzeugt werden. Damit kann jede _-Picture direkt
|
||||
# auf dem richtigen Sub-Layer landen statt nach-
|
||||
# traeglich verschoben zu werden (das brach die
|
||||
# Textur).
|
||||
import re as _re_o
|
||||
tile_id_per_path = {}
|
||||
for op in ortho_paths:
|
||||
m = _re_o.search(r"(\d{3,4}-\d{2,4})",
|
||||
os.path.basename(op))
|
||||
if m: tile_id_per_path[op] = m.group(1)
|
||||
if z_id and tile_id_per_path:
|
||||
# Pro Tile:
|
||||
# - Drape-Mesh (Foto folgt Topo) auf '80T_Terrain'
|
||||
# - flache PictureFrame (fuer 2D-Zeichnen) auf '80L_Luftbild'
|
||||
self._push_log("→ {} Ortho-Tile(s) als Terrain (Drape) + Luftbild (flach)".format(
|
||||
len(ortho_paths)))
|
||||
# Sub-Ebenen Terrain + Luftbild sicherstellen
|
||||
sub_codes = {}
|
||||
if z_id:
|
||||
_find_ebene_sublayer_name(
|
||||
d, ["swisstopo", "gelaende_topo"],
|
||||
"80", "swisstopo",
|
||||
default_color="#909090", default_lw=0.18)
|
||||
self._ensure_ortho_tile_ebenen(
|
||||
d, list(tile_id_per_path.values()))
|
||||
# Target-Sub-Layer-Index pro Tile holen
|
||||
sub_codes = self._ensure_swisstopo_subebenen(d)
|
||||
# Target-Layer-Indices fuer Terrain + Luftbild
|
||||
import layer_builder as _lb_o
|
||||
tile_layer_idx = {}
|
||||
if z_id:
|
||||
terrain_idx = -1
|
||||
luftbild_idx = -1
|
||||
if z_id and sub_codes:
|
||||
parent_idx = _lb_o._find_top_by_id(d, z_id)
|
||||
if parent_idx >= 0:
|
||||
parent_id_ = d.Layers[parent_idx].Id
|
||||
@@ -7056,10 +7170,20 @@ class ElementeBridge(panel_base.BaseBridge):
|
||||
d, parent_id_, "80")
|
||||
if base_idx >= 0:
|
||||
base_id_ = d.Layers[base_idx].Id
|
||||
for op, tid in tile_id_per_path.items():
|
||||
idx = _lb_o._find_sublayer_by_code(
|
||||
d, base_id_, tid)
|
||||
if idx >= 0: tile_layer_idx[op] = idx
|
||||
if sub_codes.get("terrain"):
|
||||
terrain_idx = _lb_o._find_sublayer_by_code(
|
||||
d, base_id_, sub_codes["terrain"])
|
||||
if sub_codes.get("luftbild"):
|
||||
luftbild_idx = _lb_o._find_sublayer_by_code(
|
||||
d, base_id_, sub_codes["luftbild"])
|
||||
# Max-Z des Terrains fuer flache Luftbild-Plane
|
||||
terr_max_z_doc = 0.0
|
||||
if merged_grid:
|
||||
try:
|
||||
max_z_m = max(z for z in merged_grid["points"].values())
|
||||
terr_max_z_doc = (max_z_m - origin_shift[2]) * m_to_unit
|
||||
except Exception: pass
|
||||
flat_z = terr_max_z_doc + max(0.001, terr_max_z_doc * 1e-4)
|
||||
ortho_objs = []
|
||||
for ortho_path in ortho_paths:
|
||||
tile_bbox = _parse_swisstopo_tile_bbox(
|
||||
@@ -7068,24 +7192,41 @@ class ElementeBridge(panel_base.BaseBridge):
|
||||
self._push_log(" → Tile-bbox nicht ableitbar aus {}".format(
|
||||
os.path.basename(ortho_path)))
|
||||
continue
|
||||
tgt_idx = tile_layer_idx.get(ortho_path, -1)
|
||||
# 1) Drape-Mesh auf '80T_Terrain'
|
||||
try:
|
||||
obj = swisstopo.add_ortho_plane(
|
||||
d, ortho_path, tile_bbox,
|
||||
origin_shift, m_to_unit, z_doc=plane_z,
|
||||
target_layer_idx=tgt_idx)
|
||||
if obj:
|
||||
ortho_objs.append(obj)
|
||||
# Tag fuer Replace-Detection bei naechstem Import
|
||||
drape = swisstopo.add_ortho_draped_mesh(
|
||||
d, ortho_path, tile_bbox, merged_grid,
|
||||
origin_shift, m_to_unit,
|
||||
z_lift=0.05,
|
||||
target_layer_idx=terrain_idx)
|
||||
if drape:
|
||||
ortho_objs.append(drape)
|
||||
try:
|
||||
at = obj.Attributes.Duplicate()
|
||||
at = drape.Attributes.Duplicate()
|
||||
at.SetUserString(
|
||||
"dossier_swisstopo_kind", "ortho")
|
||||
d.Objects.ModifyAttributes(obj, at, True)
|
||||
d.Objects.ModifyAttributes(drape, at, True)
|
||||
except Exception: pass
|
||||
except Exception as ex:
|
||||
self._push_log("Ortho-Apply: {}".format(ex))
|
||||
self._push_log("→ {} Ortho-Plane(s) auf eigene Sub-Layer".format(
|
||||
self._push_log("Drape-Apply: {}".format(ex))
|
||||
# 2) Flache Picture auf '80L_Luftbild'
|
||||
try:
|
||||
flat = swisstopo.add_ortho_plane(
|
||||
d, ortho_path, tile_bbox,
|
||||
origin_shift, m_to_unit,
|
||||
z_doc=flat_z,
|
||||
target_layer_idx=luftbild_idx)
|
||||
if flat:
|
||||
ortho_objs.append(flat)
|
||||
try:
|
||||
at = flat.Attributes.Duplicate()
|
||||
at.SetUserString(
|
||||
"dossier_swisstopo_kind", "ortho")
|
||||
d.Objects.ModifyAttributes(flat, at, True)
|
||||
except Exception: pass
|
||||
except Exception as ex:
|
||||
self._push_log("Flat-Apply: {}".format(ex))
|
||||
self._push_log("→ {} Ortho-Objekte (Drape+Flat) auf eigene Sub-Ebenen".format(
|
||||
len(ortho_objs)))
|
||||
# End-Diagnose mit BBox-Koords damit wir sehen
|
||||
# wo die Pictures tatsaechlich gelandet sind.
|
||||
@@ -7138,6 +7279,77 @@ class ElementeBridge(panel_base.BaseBridge):
|
||||
new_obj_ids.extend(o.Id for o in ortho_objs)
|
||||
new_obj_ids.extend(o.Id for o, _ in mesh_objects)
|
||||
|
||||
# --- TLM3D Vektor (Strassen/Wasser/Bahn/Vegetation) ---
|
||||
if "tlm" in kinds:
|
||||
tlm_kinds = opts.get("tlmKinds") or []
|
||||
if tlm_kinds:
|
||||
self._push_log("TLM3D Vektor holen ({} Kategorien)...".format(
|
||||
len(tlm_kinds)))
|
||||
try:
|
||||
tlm_paths = swisstopo.fetch_tlm3d_vector(
|
||||
bbox, tlm_kinds, progress=self._push_log)
|
||||
except Exception as ex:
|
||||
self._push_log("TLM Fetch-Fehler: {}".format(ex))
|
||||
tlm_paths = {}
|
||||
# Layer-Mapping: TLM-Kategorie → Dossier-Ebenen-Code
|
||||
tlm_layer_map = {
|
||||
"streets": "11", # 11_Strasse (Default-Ebene)
|
||||
"waterways": "15", # 15_Gewässer (auto-add)
|
||||
"railways": "16", # 16_Bahn (auto-add)
|
||||
"landcover": "13", # 13_Bäume (Default-Ebene)
|
||||
}
|
||||
tlm_fallback_names = {
|
||||
"11": "11_Strasse", "13": "13_Bäume",
|
||||
"15": "15_Gewässer", "16": "16_Bahn",
|
||||
}
|
||||
for cat, paths_list in tlm_paths.items():
|
||||
for tlm_p in paths_list:
|
||||
self._push_log("Import TLM {}: {}".format(
|
||||
cat, os.path.basename(tlm_p)))
|
||||
before_tlm = set(o.Id for o in d.Objects
|
||||
if o and not o.IsDeleted)
|
||||
cmd = '_-Import "{}" _Enter'.format(
|
||||
tlm_p.replace('"', '\\"'))
|
||||
try: Rhino.RhinoApp.RunScript(cmd, False)
|
||||
except Exception as ex:
|
||||
self._push_log(" Import-Fail: {}".format(ex))
|
||||
continue
|
||||
new_tlm = [o for o in d.Objects
|
||||
if o and not o.IsDeleted
|
||||
and o.Id not in before_tlm]
|
||||
self._push_log(" → {} Objekte".format(len(new_tlm)))
|
||||
# Auto-Skala falls noetig (gleiche Logik wie Buildings)
|
||||
if new_tlm and abs(eC) > 1.0:
|
||||
try:
|
||||
import math as _m
|
||||
sx = sum(o.Geometry.GetBoundingBox(True).Center.X
|
||||
for o in new_tlm[:30]) / min(30, len(new_tlm))
|
||||
ratio = (eC * m_to_unit) / sx if sx else 1
|
||||
snap = 10 ** round(_m.log10(abs(ratio)))
|
||||
if abs(snap - 1.0) > 0.01:
|
||||
self._push_log(" TLM Auto-Skala {}×".format(snap))
|
||||
self._apply_xform_fast(d, new_tlm,
|
||||
scale_factor=snap,
|
||||
translate=(-origin_shift_doc[0],
|
||||
-origin_shift_doc[1], 0))
|
||||
elif shift:
|
||||
self._apply_xform_fast(d, new_tlm,
|
||||
translate=(-origin_shift_doc[0],
|
||||
-origin_shift_doc[1], 0))
|
||||
except Exception as ex:
|
||||
self._push_log(" TLM Skala/Shift: {}".format(ex))
|
||||
# Layer + Tag
|
||||
code = tlm_layer_map.get(cat)
|
||||
fallback = tlm_fallback_names.get(code)
|
||||
if z_id and new_tlm and code:
|
||||
self._move_to_sublayer(d, new_tlm, z_id,
|
||||
code, tag="tlm_" + cat,
|
||||
fallback_name=fallback,
|
||||
fallback_color="#707080")
|
||||
elif new_tlm:
|
||||
self._tag_objects(d, new_tlm, "tlm_" + cat)
|
||||
new_obj_ids.extend(o.Id for o in new_tlm)
|
||||
|
||||
self._push_log("Import fertig: {} neue Objekte".format(len(new_obj_ids)))
|
||||
|
||||
# Auto-Zoom NOCH IM TRY-Block: sticky-Flag bleibt True
|
||||
@@ -7279,11 +7491,15 @@ class ElementeBridge(panel_base.BaseBridge):
|
||||
except Exception as ex:
|
||||
self._push_log(" ortho-per-tile: {}".format(ex))
|
||||
|
||||
def _ensure_ortho_tile_ebenen(self, doc, tile_ids):
|
||||
"""Registriert jeden Tile als Child unter '80_swisstopo' in
|
||||
dossier_ebenen JSON, baut Layer einmal synchron, broadcastet
|
||||
an die UI. Duplikate werden uebersprungen."""
|
||||
if not tile_ids: return
|
||||
def _ensure_swisstopo_subebenen(self, doc):
|
||||
"""Stellt sicher dass 80_swisstopo zwei Children hat:
|
||||
'Terrain' (Drape-Mesh — Foto folgt Topographie) und
|
||||
'Luftbild' (flache Picture ueber max-Z — fuer 2D-Zeichnen).
|
||||
Liefert {'terrain': '80T', 'luftbild': '80L'}."""
|
||||
CHILD_SPEC = [
|
||||
("80T", "Terrain", "#909090", "terrain"),
|
||||
("80L", "Luftbild", "#888888", "luftbild"),
|
||||
]
|
||||
raw = doc.Strings.GetValue("dossier_ebenen")
|
||||
try: ebenen = json.loads(raw) if raw else []
|
||||
except Exception: ebenen = []
|
||||
@@ -7303,27 +7519,27 @@ class ElementeBridge(panel_base.BaseBridge):
|
||||
have = {c.get("code") for c in parent["children"]
|
||||
if isinstance(c, dict)}
|
||||
changed = False
|
||||
for tile_id in set(tile_ids):
|
||||
if tile_id in have: continue
|
||||
parent["children"].append({
|
||||
"code": tile_id, "name": "Ortho",
|
||||
"color": "#909090", "lw": 0.13,
|
||||
"visible": True, "locked": False,
|
||||
})
|
||||
changed = True
|
||||
if not changed: return
|
||||
try:
|
||||
doc.Strings.SetString("dossier_ebenen",
|
||||
json.dumps(ebenen, ensure_ascii=False))
|
||||
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)
|
||||
import rhinopanel
|
||||
rhinopanel._broadcast_state(doc)
|
||||
except Exception as ex:
|
||||
self._push_log(" ortho-ebenen build: {}".format(ex))
|
||||
for ccode, cname, ccol, _key in CHILD_SPEC:
|
||||
if ccode not in have:
|
||||
parent["children"].append({
|
||||
"code": ccode, "name": cname, "color": ccol,
|
||||
"lw": 0.13, "visible": True, "locked": False,
|
||||
})
|
||||
changed = True
|
||||
if changed:
|
||||
try:
|
||||
doc.Strings.SetString("dossier_ebenen",
|
||||
json.dumps(ebenen, ensure_ascii=False))
|
||||
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)
|
||||
import rhinopanel
|
||||
rhinopanel._broadcast_state(doc)
|
||||
except Exception as ex:
|
||||
self._push_log(" swisstopo-ebenen build: {}".format(ex))
|
||||
return {key: ccode for ccode, _n, _col, key in CHILD_SPEC}
|
||||
|
||||
def _ensure_sub_sublayer(self, doc, parent_id, name,
|
||||
color_hex="#888888", lw=0.25):
|
||||
@@ -7557,6 +7773,218 @@ class ElementeBridge(panel_base.BaseBridge):
|
||||
size=(560, 620),
|
||||
bridge=b)
|
||||
|
||||
def _cmd_open_osm_dialog(self, p):
|
||||
"""Oeffnet das OSM-Importer-Satelliten-Fenster mit Overpass-API:
|
||||
Strassen, Gebaeudeumrisse, Wasser, Gruenflaechen, Wege als 2D-Linien."""
|
||||
outer = self
|
||||
bridge_holder = {"form": None}
|
||||
|
||||
class _OsmBridge(panel_base.BaseBridge):
|
||||
def __init__(self):
|
||||
panel_base.BaseBridge.__init__(self, "osm")
|
||||
def _push_log(self, msg):
|
||||
try: self.send("OSM_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":
|
||||
pass # nothing to send initially
|
||||
elif t == "GEOCODE":
|
||||
import swisstopo
|
||||
res = swisstopo.geocode(pp.get("text") or "")
|
||||
self.send("GEOCODE_RESULT", {"result": res})
|
||||
elif t == "RUN_OSM_IMPORT":
|
||||
self._run_osm_import(pp)
|
||||
elif t == "CANCEL":
|
||||
try:
|
||||
f = bridge_holder.get("form")
|
||||
if f is not None: f.Close()
|
||||
except Exception: pass
|
||||
|
||||
def _run_osm_import(self, opts):
|
||||
d = Rhino.RhinoDoc.ActiveDoc
|
||||
if d is None:
|
||||
self._push_log("Kein aktives Doc"); return
|
||||
try:
|
||||
import osm, 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 200)
|
||||
except Exception:
|
||||
self._push_log("Center/Radius ungueltig"); return
|
||||
categories = opts.get("categories") or []
|
||||
if not categories:
|
||||
self._push_log("Keine Kategorien gewaehlt"); return
|
||||
shift = bool(opts.get("shiftToOrigin", True))
|
||||
replace_existing = bool(opts.get("replaceExisting", True))
|
||||
# Doc-Unit
|
||||
try:
|
||||
m_to_unit = Rhino.RhinoMath.UnitScale(
|
||||
Rhino.UnitSystem.Meters, d.ModelUnitSystem)
|
||||
except Exception:
|
||||
m_to_unit = 1.0
|
||||
# Projekt-Nullpunkt (z-Offset wie bei swisstopo)
|
||||
try:
|
||||
z_raw = d.Strings.GetValue("dossier_project_zero_mum")
|
||||
project_zero_mum = float(z_raw) if z_raw else 0.0
|
||||
except Exception:
|
||||
project_zero_mum = 0.0
|
||||
z_offset_m = project_zero_mum if shift else 0.0
|
||||
# bbox in LV95-Metern + WGS84 fuer Overpass
|
||||
bbox_lv95 = (eC - r, nC - r, eC + r, nC + r)
|
||||
bbox_wgs = swisstopo.lv95_bbox_to_wgs84_bbox(*bbox_lv95)
|
||||
self._push_log("Center LV95: E={:.1f} N={:.1f} Radius={}m".format(eC, nC, r))
|
||||
self._push_log("BBox WGS84: {:.5f},{:.5f} – {:.5f},{:.5f}".format(*bbox_wgs))
|
||||
origin_shift = (eC, nC, z_offset_m) if shift else (0, 0, 0)
|
||||
z_id = d.Strings.GetValue("dossier_active_id")
|
||||
|
||||
# Listener-Suppression
|
||||
sc.sticky["dossier_swisstopo_busy"] = True
|
||||
try:
|
||||
# Bestehende OSM-Objekte loeschen?
|
||||
if replace_existing:
|
||||
self._push_log("Loesche bestehende OSM-Objekte...")
|
||||
removed = 0
|
||||
for obj in list(d.Objects):
|
||||
if obj is None or obj.IsDeleted: continue
|
||||
try:
|
||||
tag = obj.Attributes.GetUserString("dossier_osm_kind")
|
||||
except Exception: tag = None
|
||||
if tag:
|
||||
d.Objects.Delete(obj.Id, True); removed += 1
|
||||
self._push_log("→ {} alte OSM-Objekte geloescht".format(removed))
|
||||
# Sub-Ebenen-Struktur unter '70_osm' sicherstellen
|
||||
osm_sub_codes = self._ensure_osm_ebenen(d, categories)
|
||||
# Layer-Indices ermitteln
|
||||
cat_layer_idx = {}
|
||||
if z_id:
|
||||
parent_idx = layer_builder._find_top_by_id(d, z_id)
|
||||
if parent_idx >= 0:
|
||||
parent_id_ = d.Layers[parent_idx].Id
|
||||
base_idx = layer_builder._find_sublayer_by_code(
|
||||
d, parent_id_, "70")
|
||||
if base_idx >= 0:
|
||||
base_id_ = d.Layers[base_idx].Id
|
||||
for cat, ccode in osm_sub_codes.items():
|
||||
idx = layer_builder._find_sublayer_by_code(
|
||||
d, base_id_, ccode)
|
||||
if idx >= 0: cat_layer_idx[cat] = idx
|
||||
# Import via osm-Modul
|
||||
self._push_log("Hole OSM-Daten...")
|
||||
created = osm.import_osm_to_doc(
|
||||
d, bbox_wgs, categories,
|
||||
shift_lv95=origin_shift,
|
||||
m_to_unit=m_to_unit,
|
||||
z_doc=0.0,
|
||||
progress=self._push_log)
|
||||
# Layer-Move + Tag pro Objekt
|
||||
new_obj_ids = []
|
||||
moved_by_cat = {}
|
||||
for item in created:
|
||||
cat = item["category"]
|
||||
obj = item["obj"]
|
||||
tgt_idx = cat_layer_idx.get(cat, -1)
|
||||
try:
|
||||
at = obj.Attributes.Duplicate()
|
||||
if tgt_idx >= 0: at.LayerIndex = tgt_idx
|
||||
at.SetUserString("dossier_osm_kind", cat)
|
||||
d.Objects.ModifyAttributes(obj, at, True)
|
||||
new_obj_ids.append(obj.Id)
|
||||
moved_by_cat[cat] = moved_by_cat.get(cat, 0) + 1
|
||||
except Exception: pass
|
||||
for cat, n in moved_by_cat.items():
|
||||
if cat in cat_layer_idx:
|
||||
self._push_log(" → {} {} auf '{}'".format(
|
||||
n, cat, d.Layers[cat_layer_idx[cat]].FullPath))
|
||||
else:
|
||||
self._push_log(" → {} {} (Layer fallback)".format(n, cat))
|
||||
self._push_log("Import fertig: {} OSM-Objekte".format(
|
||||
len(new_obj_ids)))
|
||||
# Auto-Zoom
|
||||
if opts.get("autoZoom") and new_obj_ids:
|
||||
try:
|
||||
combined = rg.BoundingBox.Empty
|
||||
for oid in new_obj_ids:
|
||||
ob = d.Objects.Find(oid)
|
||||
if ob is None: continue
|
||||
bb = ob.Geometry.GetBoundingBox(True)
|
||||
if bb.IsValid: combined.Union(bb)
|
||||
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 _ensure_osm_ebenen(self, doc, categories):
|
||||
"""Stellt sicher dass '70_osm' Parent + Children fuer jede
|
||||
gewuenschte Kategorie in dossier_ebenen existieren. Liefert
|
||||
{category_key: code} Map."""
|
||||
import osm
|
||||
raw = doc.Strings.GetValue("dossier_ebenen")
|
||||
try: ebenen = json.loads(raw) if raw else []
|
||||
except Exception: ebenen = []
|
||||
if not isinstance(ebenen, list): ebenen = []
|
||||
parent = next((e for e in ebenen if isinstance(e, dict)
|
||||
and e.get("code") == "70"), None)
|
||||
if parent is None:
|
||||
parent = {
|
||||
"code": "70", "name": "osm",
|
||||
"color": "#707080", "lw": 0.13,
|
||||
"visible": True, "locked": False,
|
||||
"children": [],
|
||||
}
|
||||
ebenen.append(parent)
|
||||
if not isinstance(parent.get("children"), list):
|
||||
parent["children"] = []
|
||||
have = {c.get("code") for c in parent["children"]
|
||||
if isinstance(c, dict)}
|
||||
code_map = {}
|
||||
changed = False
|
||||
for cat_key in categories:
|
||||
spec = osm.CATEGORIES.get(cat_key)
|
||||
if not spec: continue
|
||||
code = spec["code"]
|
||||
code_map[cat_key] = code
|
||||
if code in have: continue
|
||||
parent["children"].append({
|
||||
"code": code, "name": spec["name"],
|
||||
"color": spec["color"], "lw": 0.13,
|
||||
"visible": True, "locked": False,
|
||||
})
|
||||
changed = True
|
||||
if changed:
|
||||
try:
|
||||
doc.Strings.SetString("dossier_ebenen",
|
||||
json.dumps(ebenen, ensure_ascii=False))
|
||||
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
|
||||
zlist = json.loads(z_raw) if z_raw else []
|
||||
if zlist:
|
||||
import layer_builder
|
||||
layer_builder.build_layers(doc, zlist, ebenen)
|
||||
import rhinopanel
|
||||
rhinopanel._broadcast_state(doc)
|
||||
except Exception as ex:
|
||||
self._push_log("osm-ebenen build: {}".format(ex))
|
||||
return code_map
|
||||
|
||||
b = _OsmBridge()
|
||||
bridge_holder["form"] = panel_base.open_satellite_window(
|
||||
"osm",
|
||||
title="OSM Importer",
|
||||
size=(520, 620),
|
||||
bridge=b)
|
||||
|
||||
def _update_wall(self, p):
|
||||
"""Properties eines Elements aendern (Wand/Decke/Dach/Oeffnung).
|
||||
Volumen wird anschliessend regeneriert."""
|
||||
@@ -8547,6 +8975,9 @@ def _on_object_deleted(sender, e):
|
||||
"""
|
||||
# Waehrend Swisstopo-Import: keine DOSSIER-Metas vorhanden, nur Overhead
|
||||
if sc.sticky.get("dossier_swisstopo_busy"): return
|
||||
# Bulk-Delete (z.B. SelAll + Delete bei 6000 OSM-Curves): pro-Event-
|
||||
# Arbeit waere reiner Overhead. CommandEnd refresht einmalig.
|
||||
if sc.sticky.get(_BULK_ACTIVE_KEY): 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.
|
||||
@@ -8794,6 +9225,9 @@ def _on_idle_selection(sender, e):
|
||||
Replace-Event) ausgefuehrt. So vermeiden wir Volume-Flicker waehrend
|
||||
fortlaufenden Gumball-/Move-Operationen — der finale Regen rendert
|
||||
nach Drag-Ende, bis dahin uebernimmt Rhinos Transform die Geometrie."""
|
||||
# Waehrend Bulk-Op (z.B. _Delete bei 6000 OSM-Curves): nicht pollen.
|
||||
# Wuerde sonst pro Idle-Tick alle Objekte iterieren = Quasi-Stall.
|
||||
if sc.sticky.get(_BULK_ACTIVE_KEY): return
|
||||
b = sc.sticky.get("elemente_bridge")
|
||||
if b is None: return
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
@@ -8990,6 +9424,16 @@ _USER_TRANSFORM_CMDS = frozenset((
|
||||
"Drag", "Gumball", "Orient", "Orient3Pt", "RemapCPlane", "Transform",
|
||||
))
|
||||
|
||||
# Bulk-Operations: User selektiert N Objekte + ausfuehrt die Operation
|
||||
# einmal. Wir suspenden Redraws + Listener-Arbeit damit das nicht
|
||||
# pro-Object visuell durchrieselt. Beispiel: SelAll + Delete bei 6000
|
||||
# Curves → ohne Suspend dauert das ewig + man sieht jedes Element
|
||||
# einzeln verschwinden.
|
||||
_USER_BULK_CMDS = frozenset((
|
||||
"Delete", "DeleteSelected", "DeleteSubObject", "Cut",
|
||||
))
|
||||
_BULK_ACTIVE_KEY = "_dossier_bulk_op_active"
|
||||
|
||||
# Undo/Redo: Rhino restored Objekte aus dem Undo-Stack → feuert Add/Delete-
|
||||
# Events fuer ALLE betroffenen Objekte. Unsere Handler wuerden fuer jedes
|
||||
# einen Regen queuen → Storm. Wir suppressen die Handler komplett; Undo hat
|
||||
@@ -9076,6 +9520,21 @@ def _on_command_begin(sender, e):
|
||||
if name in _USER_UNDO_CMDS:
|
||||
sc.sticky[_UNDO_ACTIVE_KEY] = name
|
||||
return
|
||||
# Bulk-Ops (z.B. _Delete mit 6000 Selektion): RedrawEnabled aus +
|
||||
# Listener bail-out — am Ende einmal redrawn.
|
||||
if name in _USER_BULK_CMDS:
|
||||
sc.sticky[_BULK_ACTIVE_KEY] = name
|
||||
print("[ELEMENTE] Bulk-Op start: '{}' — Listener bail aktiv".format(name))
|
||||
try:
|
||||
sc.sticky["_dossier_bulk_redraw_prev"] = bool(doc.Views.RedrawEnabled)
|
||||
doc.Views.RedrawEnabled = False
|
||||
except Exception: pass
|
||||
return
|
||||
# Diagnose: andere Commands sehen wir hier vorbeiziehen — wenn _Delete
|
||||
# einen anderen Namen hat als 'Delete', sehen wir's und koennen den
|
||||
# frozenset anpassen.
|
||||
if name and "delete" in name.lower():
|
||||
print("[ELEMENTE] CmdBegin '{}' (nicht im Bulk-Set — anpassen?)".format(name))
|
||||
if name not in _USER_TRANSFORM_CMDS: return
|
||||
sc.sticky[_UT_SNAPSHOT_KEY] = _snapshot_source_positions(doc)
|
||||
sc.sticky[_UT_ACTIVE_KEY] = name
|
||||
@@ -9095,6 +9554,22 @@ def _on_command_begin(sender, e):
|
||||
|
||||
|
||||
def _on_command_end(sender, e):
|
||||
# Bulk-Op fertig: RedrawEnabled zurueck + EINMAL redrawn + selection
|
||||
# refresh ans Gestaltung-Panel.
|
||||
if sc.sticky.get(_BULK_ACTIVE_KEY):
|
||||
sc.sticky[_BULK_ACTIVE_KEY] = None
|
||||
try:
|
||||
prev = sc.sticky.pop("_dossier_bulk_redraw_prev", True)
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
if doc is not None:
|
||||
doc.Views.RedrawEnabled = prev
|
||||
doc.Views.Redraw()
|
||||
except Exception: pass
|
||||
gb = sc.sticky.get("gestaltung_bridge")
|
||||
if gb is not None:
|
||||
try: gb._send_selection()
|
||||
except Exception: pass
|
||||
return
|
||||
# Undo/Redo abschliessen: nur Flag clearen, kein Regen + ein Selection-
|
||||
# Refresh fuers Gestaltung-Panel (Listener waren waehrend Undo aus).
|
||||
if sc.sticky.get(_UNDO_ACTIVE_KEY):
|
||||
|
||||
Reference in New Issue
Block a user