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))
|
json.dumps(ebenen, ensure_ascii=False))
|
||||||
print("[ELEMENTE] Ebene '{}_{}' automatisch hinzugefuegt".format(
|
print("[ELEMENTE] Ebene '{}_{}' automatisch hinzugefuegt".format(
|
||||||
default_code, default_name))
|
default_code, default_name))
|
||||||
# Ebenen-Manager UI mit-informieren
|
# build_layers synchron damit Rhino-Layer existieren bevor
|
||||||
b = sc.sticky.get("ebenen_bridge_ref") \
|
# Objekte verschoben werden
|
||||||
or sc.sticky.get("ebenen_bridge") \
|
try:
|
||||||
or sc.sticky.get("rhinopanel_bridge")
|
import layer_builder
|
||||||
if b is not None and hasattr(b, "_send_state"):
|
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
|
||||||
try: b._send_state()
|
zlist = json.loads(z_raw) if z_raw else []
|
||||||
except Exception: pass
|
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:
|
except Exception as ex:
|
||||||
print("[ELEMENTE] Auto-Add fehler:", ex)
|
print("[ELEMENTE] Auto-Add fehler:", ex)
|
||||||
return "{}_{}".format(default_code, default_name)
|
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):
|
def _layer_path_axis(doc, geschoss_name):
|
||||||
"""Wand-Achse + Volumen — Sublayer 'Wände' (Code 20)."""
|
"""Wand-Achse + Volumen — Sublayer 'Wände' (Code 20)."""
|
||||||
sub = _find_ebene_sublayer_name(doc, ["wand", "wände", "waende"],
|
sub = _find_ebene_sublayer_name(doc, ["wand", "wände", "waende"],
|
||||||
@@ -6725,13 +6755,26 @@ class ElementeBridge(panel_base.BaseBridge):
|
|||||||
Rhino.UnitSystem.Meters, d.ModelUnitSystem)
|
Rhino.UnitSystem.Meters, d.ModelUnitSystem)
|
||||||
except Exception:
|
except Exception:
|
||||||
m_to_unit = 1.0
|
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
|
eC_u = eC * m_to_unit
|
||||||
nC_u = nC * m_to_unit
|
nC_u = nC * m_to_unit
|
||||||
r_u = r * 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 = (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
|
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 = (eC, nC, z_offset_m) if shift else (0, 0, 0)
|
||||||
origin_shift_doc = (eC_u, nC_u, 0) 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("Center LV95: E={:.1f} N={:.1f} Radius={}m".format(eC, nC, r))
|
||||||
self._push_log("BBox (m): {:.0f}-{:.0f} / {:.0f}-{:.0f}".format(*bbox))
|
self._push_log("BBox (m): {:.0f}-{:.0f} / {:.0f}-{:.0f}".format(*bbox))
|
||||||
if m_to_unit != 1.0:
|
if m_to_unit != 1.0:
|
||||||
@@ -6775,7 +6818,10 @@ class ElementeBridge(panel_base.BaseBridge):
|
|||||||
try:
|
try:
|
||||||
# --- Buildings (DWG) -----------------------------------
|
# --- Buildings (DWG) -----------------------------------
|
||||||
if "buildings" in kinds:
|
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):
|
for idx, p in enumerate(paths):
|
||||||
try: size_mb = os.path.getsize(p) / 1e6
|
try: size_mb = os.path.getsize(p) / 1e6
|
||||||
except Exception: size_mb = 0
|
except Exception: size_mb = 0
|
||||||
@@ -6792,10 +6838,13 @@ class ElementeBridge(panel_base.BaseBridge):
|
|||||||
self._push_log("→ Import fertig: {} neue Objekte".format(len(new)))
|
self._push_log("→ Import fertig: {} neue Objekte".format(len(new)))
|
||||||
# Auto-Skala-Erkennung: Rhinos DXF-Parser kann je
|
# Auto-Skala-Erkennung: Rhinos DXF-Parser kann je
|
||||||
# nach $INSUNITS und Doc-Unit unerwartet 1000x rauf/
|
# nach $INSUNITS und Doc-Unit unerwartet 1000x rauf/
|
||||||
# runter skalieren. Wir messen aus den Objekten und
|
# runter skalieren. swissBUILDINGS3D 3.0 z.B. liefert
|
||||||
# SNAPPEN auf naechste Zehnerpotenz (1, 0.001, 1000)
|
# Werte in KM (Center bei ~2764, statt 2'763'800m).
|
||||||
# damit kleine Mess-Streuung nicht eine off-by-1m
|
# Wir korrigieren das per Scale-Faktor auf den
|
||||||
# bbox produziert.
|
# 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:
|
if new and idx == 0:
|
||||||
try:
|
try:
|
||||||
import math as _m
|
import math as _m
|
||||||
@@ -6805,19 +6854,14 @@ class ElementeBridge(panel_base.BaseBridge):
|
|||||||
sum_x += bb.Center.X
|
sum_x += bb.Center.X
|
||||||
samples += 1
|
samples += 1
|
||||||
avg_x = sum_x / max(1, samples)
|
avg_x = sum_x / max(1, samples)
|
||||||
if abs(eC) > 1.0 and avg_x != 0:
|
expected_x = eC * m_to_unit
|
||||||
raw_ratio = avg_x / eC
|
if abs(expected_x) > 1.0 and avg_x != 0:
|
||||||
snapped = 10 ** round(_m.log10(abs(raw_ratio)))
|
ratio = expected_x / avg_x
|
||||||
if abs(snapped - m_to_unit) > 1e-9:
|
snap = 10 ** round(_m.log10(abs(ratio)))
|
||||||
self._push_log("AUTO-SKALA: raw_ratio={:.6f} → snap to 1m={:g} doc-units (war {:g})".format(
|
if abs(snap - 1.0) > 0.01:
|
||||||
raw_ratio, snapped, m_to_unit))
|
scale_correction = snap
|
||||||
m_to_unit = snapped
|
self._push_log("AUTO-SKALA: imports {}× off — scale-up {:g}×".format(
|
||||||
eC_u = eC * m_to_unit
|
"klein" if snap > 1 else "gross", snap))
|
||||||
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:
|
except Exception as ex:
|
||||||
self._push_log("Auto-Skala-Erkennung: {}".format(ex))
|
self._push_log("Auto-Skala-Erkennung: {}".format(ex))
|
||||||
# Diagnose
|
# Diagnose
|
||||||
@@ -6868,36 +6912,42 @@ class ElementeBridge(panel_base.BaseBridge):
|
|||||||
kept = new
|
kept = new
|
||||||
try: swisstopo._yield_ui()
|
try: swisstopo._yield_ui()
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
# Shift falls aktiviert — Batch via System.List[Guid]
|
# Scale + Move via Rhinos eingebaute Commands auf
|
||||||
# damit Python.NET den richtigen Overload erwischt.
|
# Selektion — die batchen intern und sind bei 7000
|
||||||
if shift and kept:
|
# Objekten in Sekunden durch (statt Minuten mit
|
||||||
self._push_log("→ Shift {} Objekte zum Welt-Origin (Batch)...".format(len(kept)))
|
# einzeln-Transform-Loop).
|
||||||
xform = rg.Transform.Translation(-origin_shift_doc[0],
|
translate_doc = None
|
||||||
-origin_shift_doc[1],
|
if shift:
|
||||||
-origin_shift_doc[2])
|
translate_doc = (-origin_shift_doc[0],
|
||||||
try:
|
-origin_shift_doc[1],
|
||||||
from System.Collections.Generic import List as _List
|
-origin_shift_doc[2])
|
||||||
from System import Guid as _Guid
|
ops = []
|
||||||
ids = _List[_Guid]()
|
if abs(scale_correction - 1.0) > 1e-6:
|
||||||
for o in kept: ids.Add(o.Id)
|
ops.append("Scale {}×".format(scale_correction))
|
||||||
n_shifted = d.Objects.Transform(ids, xform, True)
|
if shift: ops.append("Shift→Origin")
|
||||||
self._push_log(" → {} Objekte verschoben".format(n_shifted))
|
if ops and kept:
|
||||||
except Exception as ex:
|
self._push_log("→ {} ({} Obj)...".format(
|
||||||
self._push_log(" Batch-Shift fehlgeschlagen, Loop-Fallback: {}".format(ex))
|
" + ".join(ops), len(kept)))
|
||||||
for o in kept:
|
self._apply_xform_fast(
|
||||||
try: d.Objects.Transform(o.Id, xform, True)
|
d, kept,
|
||||||
except Exception: pass
|
scale_factor=scale_correction,
|
||||||
# Layer-Move + Tag
|
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:
|
if z_id and kept:
|
||||||
self._push_log("→ Layer-Move auf 12_Gebäude...")
|
if variant == "solid":
|
||||||
sub_name = _find_ebene_sublayer_name(
|
self._push_log("→ Buildings auf '81_Swissbuildings' (solid)...")
|
||||||
d, ["gebaeude", "gebäude", "buildings"],
|
else:
|
||||||
"12", "Gebäude",
|
self._push_log("→ Layer konsolidieren (Build/Roof/Wall/Floor)...")
|
||||||
default_color="#888888", default_lw=0.25)
|
self._consolidate_buildings(d, kept, z_id,
|
||||||
self._move_to_sublayer(d, kept, z_id,
|
target_code="81", variant=variant)
|
||||||
sub_name.split("_", 1)[0], tag="buildings")
|
|
||||||
else:
|
else:
|
||||||
# Kein aktives Geschoss → nur Tag setzen
|
|
||||||
self._tag_objects(d, kept, "buildings")
|
self._tag_objects(d, kept, "buildings")
|
||||||
new_obj_ids.extend(o.Id for o in kept)
|
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(
|
xyz_paths = swisstopo.fetch_terrain_xyz(
|
||||||
bbox, resolution=res, progress=self._push_log)
|
bbox, resolution=res, progress=self._push_log)
|
||||||
mesh_objects = []
|
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:
|
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:
|
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(
|
grid = swisstopo.xyz_to_grid(
|
||||||
p,
|
p,
|
||||||
target_step=target_step,
|
target_step=target_step,
|
||||||
clip_bbox=bbox, # User-bbox in m!
|
clip_bbox=bbox,
|
||||||
progress=self._push_log)
|
progress=self._push_log)
|
||||||
if grid is None:
|
if grid is not None: grids.append(grid)
|
||||||
self._push_log("→ leeres Grid"); continue
|
except Exception as ex:
|
||||||
# Mesh in Doc-Units bauen: shift in m (LV95),
|
self._push_log("XYZ-Parse fail: {}".format(ex))
|
||||||
# dann beim Vertex-Add * m_to_unit
|
if grids:
|
||||||
mesh = swisstopo.mesh_from_grid(
|
try:
|
||||||
grid,
|
merged = swisstopo.merge_grids(grids)
|
||||||
origin_shift=origin_shift,
|
if merged is None:
|
||||||
unit_scale=m_to_unit)
|
self._push_log("Merge lieferte None")
|
||||||
self._push_log("→ Mesh: {} Vertices / {} Faces".format(
|
else:
|
||||||
mesh.Vertices.Count, mesh.Faces.Count))
|
self._push_log("Merge: {} Tiles → {} Punkte ({}×{} Raster)".format(
|
||||||
gid = d.Objects.AddMesh(mesh)
|
len(grids), len(merged["points"]),
|
||||||
obj = d.Objects.Find(gid)
|
len(merged["es"]), len(merged["ns"])))
|
||||||
if obj: mesh_objects.append((obj, grid["bbox"]))
|
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:
|
except Exception as ex:
|
||||||
self._push_log("Mesh-Bau fehlgeschlagen: {}".format(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:
|
if z_id and mesh_objects:
|
||||||
sub_name = _find_ebene_sublayer_name(
|
sub_name = _find_ebene_sublayer_name(
|
||||||
d, ["situation", "terrain", "gelaende"],
|
d, ["swisstopo", "gelaende_topo"],
|
||||||
"10", "Situation",
|
"80", "swisstopo",
|
||||||
default_color="#909090", default_lw=0.18)
|
default_color="#909090", default_lw=0.18)
|
||||||
objs = [m[0] for m in mesh_objects]
|
objs = [m[0] for m in mesh_objects]
|
||||||
self._move_to_sublayer(d, objs, z_id,
|
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:
|
elif mesh_objects:
|
||||||
objs = [m[0] for m in mesh_objects]
|
objs = [m[0] for m in mesh_objects]
|
||||||
self._tag_objects(d, objs, "terrain")
|
self._tag_objects(d, objs, "terrain")
|
||||||
@@ -6952,23 +7013,50 @@ class ElementeBridge(panel_base.BaseBridge):
|
|||||||
ortho_paths = swisstopo.fetch_orthophoto(
|
ortho_paths = swisstopo.fetch_orthophoto(
|
||||||
bbox, resolution="2.0", progress=self._push_log)
|
bbox, resolution="2.0", progress=self._push_log)
|
||||||
if ortho_paths:
|
if ortho_paths:
|
||||||
# Erstes Ortho-Tile auf alle Meshes (MVP — pro Mesh
|
# Max-Z des Terrains finden — Plane sitzt knapp darueber
|
||||||
# eigenes Mapping waere genauer, kommt spaeter)
|
# damit sie in Top-View ueber dem Terrain liegt.
|
||||||
for obj, mbbox in mesh_objects:
|
terr_max_z = 0.0
|
||||||
|
for tobj, _ in mesh_objects:
|
||||||
try:
|
try:
|
||||||
if shift:
|
bb = tobj.Geometry.GetBoundingBox(True)
|
||||||
# Mesh ist verschoben → bbox auch
|
if bb.IsValid and bb.Max.Z > terr_max_z:
|
||||||
mbbox_shifted = (
|
terr_max_z = bb.Max.Z
|
||||||
mbbox[0] - origin_shift[0],
|
except Exception: pass
|
||||||
mbbox[1] - origin_shift[1],
|
z_offset = max(0.001, terr_max_z * 1e-4) # winziges Epsilon
|
||||||
mbbox[2] - origin_shift[0],
|
plane_z = terr_max_z + z_offset
|
||||||
mbbox[3] - origin_shift[1])
|
self._push_log("→ {} Ortho-Tile(s), platziere Plane bei Z={:.3f}".format(
|
||||||
else:
|
len(ortho_paths), plane_z))
|
||||||
mbbox_shifted = mbbox
|
ortho_objs = []
|
||||||
swisstopo.apply_ortho_material(
|
for ortho_path in ortho_paths:
|
||||||
d, obj, ortho_paths[0], mbbox_shifted)
|
# 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:
|
except Exception as ex:
|
||||||
self._push_log("Ortho-Apply: {}".format(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)
|
new_obj_ids.extend(o.Id for o, _ in mesh_objects)
|
||||||
|
|
||||||
self._push_log("Import fertig: {} neue Objekte".format(len(new_obj_ids)))
|
self._push_log("Import fertig: {} neue Objekte".format(len(new_obj_ids)))
|
||||||
@@ -6999,6 +7087,57 @@ class ElementeBridge(panel_base.BaseBridge):
|
|||||||
finally:
|
finally:
|
||||||
sc.sticky["dossier_swisstopo_busy"] = False
|
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):
|
def _tag_objects(self, doc, objs, tag):
|
||||||
"""Setzt nur den dossier_swisstopo_kind UserString — fuer
|
"""Setzt nur den dossier_swisstopo_kind UserString — fuer
|
||||||
den Fall dass kein Geschoss aktiv ist und wir den Layer-Move
|
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)
|
doc.Objects.ModifyAttributes(o, attrs, True)
|
||||||
except Exception: pass
|
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
|
"""Verschiebt Liste von Rhino-Objekten auf den DOSSIER-Sublayer
|
||||||
<z_id>/<code>_*. Optional: Tag (UserString
|
<z_id>/<code>_*. Optional: Tag (UserString
|
||||||
dossier_swisstopo_kind) setzen — wird beim naechsten Import
|
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
|
if not objs: return
|
||||||
try:
|
try:
|
||||||
import layer_builder
|
import layer_builder
|
||||||
@@ -7023,6 +7352,15 @@ class ElementeBridge(panel_base.BaseBridge):
|
|||||||
if parent_idx < 0: return
|
if parent_idx < 0: return
|
||||||
parent_id = doc.Layers[parent_idx].Id
|
parent_id = doc.Layers[parent_idx].Id
|
||||||
sub_idx = layer_builder._find_sublayer_by_code(doc, parent_id, code)
|
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
|
if sub_idx < 0: return
|
||||||
n = 0
|
n = 0
|
||||||
for o in objs:
|
for o in objs:
|
||||||
|
|||||||
+127
-90
@@ -287,19 +287,84 @@ def _apply_section_style(doc, layer, section_cfg, layer_color):
|
|||||||
print(diag, "OK applied")
|
print(diag, "OK applied")
|
||||||
|
|
||||||
|
|
||||||
|
def walk_ebenen(ebenen, parent_path=()):
|
||||||
|
"""Iteriert Ebenen-Baum (flach + Children). Liefert Tuples
|
||||||
|
(path, ebene) wobei path ein Tuple der Codes von der Root bis zu dieser
|
||||||
|
Ebene ist (inkl. eigener Code). Beispiel:
|
||||||
|
walk_ebenen([{'code':'20','children':[{'code':'01'}]}])
|
||||||
|
→ [(('20',), e20), (('20','01'), e01)]"""
|
||||||
|
out = []
|
||||||
|
if not ebenen: return out
|
||||||
|
for e in ebenen:
|
||||||
|
if not isinstance(e, dict): continue
|
||||||
|
code = e.get("code")
|
||||||
|
if not code: continue
|
||||||
|
path = parent_path + (code,)
|
||||||
|
out.append((path, e))
|
||||||
|
children = e.get("children")
|
||||||
|
if isinstance(children, list) and children:
|
||||||
|
out.extend(walk_ebenen(children, path))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _build_ebene_layer(doc, parent_id, e, diag_prefix=""):
|
||||||
|
"""Findet/erstellt einen Sublayer fuer eine Ebene unter parent_id.
|
||||||
|
Liefert den layer_idx oder -1. Setzt Farbe/LW/Section-Style."""
|
||||||
|
code = e.get("code") or ""
|
||||||
|
name = e.get("name") or "Ebene"
|
||||||
|
sub_name = "{}_{}".format(code, name) if code else name
|
||||||
|
col = _color(e.get("color"))
|
||||||
|
lw = float(e.get("lw", 0.13))
|
||||||
|
sub_idx = _find_sublayer_by_code(doc, parent_id, code) if code else -1
|
||||||
|
if sub_idx < 0:
|
||||||
|
sub_idx = _add_layer(doc, sub_name, parent_id, col, lw)
|
||||||
|
if sub_idx >= 0 and code:
|
||||||
|
doc.Layers[sub_idx].SetUserString("dossier_code", code)
|
||||||
|
else:
|
||||||
|
sub = doc.Layers[sub_idx]
|
||||||
|
if sub.Name != sub_name: sub.Name = sub_name
|
||||||
|
sub.Color = col
|
||||||
|
try:
|
||||||
|
import massstab as _ms
|
||||||
|
_ms.write_plotweight(doc, sub, float(lw))
|
||||||
|
except Exception:
|
||||||
|
sub.PlotWeight = lw
|
||||||
|
if code: sub.SetUserString("dossier_code", code)
|
||||||
|
# Section Style anwenden (Py3-only — IPy 2.7 no-op)
|
||||||
|
try:
|
||||||
|
_apply_section_style(doc, doc.Layers[sub_idx],
|
||||||
|
e.get("section"), e.get("color"))
|
||||||
|
except Exception as ex:
|
||||||
|
print("[EBENEN] section-style apply ({}{}): {}".format(
|
||||||
|
diag_prefix, sub_name, ex))
|
||||||
|
return sub_idx
|
||||||
|
|
||||||
|
|
||||||
|
def _build_ebenen_recursive(doc, parent_id, ebenen, diag_prefix=""):
|
||||||
|
"""Rekursive Ebenen-Erstellung: jeder Eintrag wird als Sublayer angelegt,
|
||||||
|
seine 'children' werden unter dem neu erstellten Sublayer angelegt."""
|
||||||
|
if not ebenen: return
|
||||||
|
for e in ebenen:
|
||||||
|
if not isinstance(e, dict): continue
|
||||||
|
sub_idx = _build_ebene_layer(doc, parent_id, e, diag_prefix=diag_prefix)
|
||||||
|
if sub_idx < 0: continue
|
||||||
|
children = e.get("children")
|
||||||
|
if isinstance(children, list) and children:
|
||||||
|
child_parent_id = doc.Layers[sub_idx].Id
|
||||||
|
_build_ebenen_recursive(doc, child_parent_id, children,
|
||||||
|
diag_prefix=diag_prefix + e.get("name", "") + "/")
|
||||||
|
|
||||||
|
|
||||||
def build_layers(doc, zeichnungsebenen, ebenen):
|
def build_layers(doc, zeichnungsebenen, ebenen):
|
||||||
"""
|
"""Stellt sicher dass fuer jede Zeichnungsebene ein Parent-Layer existiert
|
||||||
Stellt sicher dass fuer jede Zeichnungsebene ein Parent-Layer existiert
|
und unter jedem alle Ebenen (rekursiv inkl. children) als Sublayer angelegt
|
||||||
und unter jedem alle Ebenen als Sublayer angelegt/aktualisiert sind.
|
/ aktualisiert sind."""
|
||||||
"""
|
|
||||||
for z in zeichnungsebenen:
|
for z in zeichnungsebenen:
|
||||||
z_id = z["id"]
|
z_id = z["id"]
|
||||||
z_name = z["name"]
|
z_name = z["name"]
|
||||||
|
|
||||||
# Parent finden oder anlegen
|
# Parent finden oder anlegen
|
||||||
idx = _find_top_by_id(doc, z_id)
|
idx = _find_top_by_id(doc, z_id)
|
||||||
if idx < 0:
|
if idx < 0: idx = _find_top_by_name(doc, z_name)
|
||||||
idx = _find_top_by_name(doc, z_name)
|
|
||||||
if idx < 0:
|
if idx < 0:
|
||||||
idx = _add_layer(doc, z_name)
|
idx = _add_layer(doc, z_name)
|
||||||
doc.Layers[idx].SetUserString("dossier_id", z_id)
|
doc.Layers[idx].SetUserString("dossier_id", z_id)
|
||||||
@@ -308,78 +373,53 @@ def build_layers(doc, zeichnungsebenen, ebenen):
|
|||||||
if parent.Name != z_name:
|
if parent.Name != z_name:
|
||||||
parent.Name = z_name
|
parent.Name = z_name
|
||||||
parent.SetUserString("dossier_id", z_id)
|
parent.SetUserString("dossier_id", z_id)
|
||||||
|
|
||||||
parent_id = doc.Layers[idx].Id
|
parent_id = doc.Layers[idx].Id
|
||||||
|
_build_ebenen_recursive(doc, parent_id, ebenen,
|
||||||
# Sublayer pro Ebene
|
diag_prefix=z_name + "/")
|
||||||
for e in ebenen:
|
|
||||||
sub_name = "{}_{}".format(e["code"], e["name"])
|
|
||||||
col = _color(e.get("color"))
|
|
||||||
lw = float(e.get("lw", 0.13))
|
|
||||||
sub_idx = _find_sublayer_by_code(doc, parent_id, e["code"])
|
|
||||||
if sub_idx < 0:
|
|
||||||
sub_idx = _add_layer(doc, sub_name, parent_id, col, lw)
|
|
||||||
doc.Layers[sub_idx].SetUserString("dossier_code", e["code"])
|
|
||||||
else:
|
|
||||||
sub = doc.Layers[sub_idx]
|
|
||||||
if sub.Name != sub_name:
|
|
||||||
sub.Name = sub_name
|
|
||||||
sub.Color = col
|
|
||||||
try:
|
|
||||||
import massstab as _ms
|
|
||||||
_ms.write_plotweight(doc, sub, float(lw))
|
|
||||||
except Exception:
|
|
||||||
sub.PlotWeight = lw
|
|
||||||
sub.SetUserString("dossier_code", e["code"])
|
|
||||||
|
|
||||||
# Section Style anwenden (Py3-only — IPy 2.7 no-op)
|
|
||||||
try:
|
|
||||||
_apply_section_style(doc, doc.Layers[sub_idx],
|
|
||||||
e.get("section"), e.get("color"))
|
|
||||||
except Exception as ex:
|
|
||||||
print("[EBENEN] section-style apply ({}): {}".format(sub_name, ex))
|
|
||||||
|
|
||||||
doc.Views.Redraw()
|
doc.Views.Redraw()
|
||||||
print("[EBENEN] {} Zeichnungsebenen x {} Ebenen aktualisiert".format(
|
n_total = len(walk_ebenen(ebenen))
|
||||||
len(zeichnungsebenen), len(ebenen)))
|
print("[EBENEN] {} Zeichnungsebenen x {} Ebenen aktualisiert (inkl. {} Sub)".format(
|
||||||
|
len(zeichnungsebenen), len(ebenen), max(0, n_total - len(ebenen))))
|
||||||
|
|
||||||
|
|
||||||
|
def _layer_matches_code(layer, code):
|
||||||
|
"""True wenn der Layer zu der Ebene mit `code` gehoert. Akzeptiert
|
||||||
|
sowohl Top-Sub-Layer (Geschoss/CODE_Name) als auch Sub-Sub-Layer
|
||||||
|
(Geschoss/Parent/CODE_Name) — Match via Name-Prefix `code_`."""
|
||||||
|
if _is_top_level(layer): return False
|
||||||
|
return layer.Name.startswith(code + "_")
|
||||||
|
|
||||||
|
|
||||||
def update_layer_style(doc, code, color_hex=None, lw=None):
|
def update_layer_style(doc, code, color_hex=None, lw=None):
|
||||||
"""Aendert Farbe und/oder Stiftdicke fuer alle Sublayer mit dem gegebenen Code."""
|
"""Aendert Farbe und/oder Stiftdicke fuer alle Sublayer mit dem gegebenen
|
||||||
|
Code — auch tief verschachtelte (Sub-Sub-Layer mit gleichem Code-Prefix)."""
|
||||||
col = _color(color_hex) if color_hex else None
|
col = _color(color_hex) if color_hex else None
|
||||||
try:
|
try:
|
||||||
import massstab as _ms
|
import massstab as _ms
|
||||||
except Exception:
|
except Exception:
|
||||||
_ms = None
|
_ms = None
|
||||||
for i, layer in enumerate(doc.Layers):
|
for layer in doc.Layers:
|
||||||
if _is_top_level(layer):
|
if not _layer_matches_code(layer, code): continue
|
||||||
continue
|
if col is not None: layer.Color = col
|
||||||
if layer.Name.startswith(code + "_"):
|
if lw is not None:
|
||||||
if col is not None:
|
if _ms is not None:
|
||||||
layer.Color = col
|
_ms.write_plotweight(doc, layer, float(lw))
|
||||||
if lw is not None:
|
else:
|
||||||
if _ms is not None:
|
layer.PlotWeight = float(lw)
|
||||||
_ms.write_plotweight(doc, layer, float(lw))
|
|
||||||
else:
|
|
||||||
layer.PlotWeight = float(lw)
|
|
||||||
doc.Views.Redraw()
|
doc.Views.Redraw()
|
||||||
|
|
||||||
|
|
||||||
def set_ebene_visible(doc, code, visible):
|
def set_ebene_visible(doc, code, visible):
|
||||||
"""Schaltet alle Sublayer mit Code in/aus Zeichnungsebenen."""
|
"""Schaltet alle Sublayer mit Code in/aus (auch tief verschachtelte)."""
|
||||||
for i, layer in enumerate(doc.Layers):
|
for layer in doc.Layers:
|
||||||
if _is_top_level(layer):
|
if _layer_matches_code(layer, code):
|
||||||
continue
|
|
||||||
if layer.Name.startswith(code + "_"):
|
|
||||||
layer.IsVisible = visible
|
layer.IsVisible = visible
|
||||||
doc.Views.Redraw()
|
doc.Views.Redraw()
|
||||||
|
|
||||||
|
|
||||||
def set_ebene_locked(doc, code, locked):
|
def set_ebene_locked(doc, code, locked):
|
||||||
for i, layer in enumerate(doc.Layers):
|
for layer in doc.Layers:
|
||||||
if _is_top_level(layer):
|
if _layer_matches_code(layer, code):
|
||||||
continue
|
|
||||||
if layer.Name.startswith(code + "_"):
|
|
||||||
layer.IsLocked = locked
|
layer.IsLocked = locked
|
||||||
doc.Views.Redraw()
|
doc.Views.Redraw()
|
||||||
|
|
||||||
@@ -631,10 +671,16 @@ def apply_visibility(doc, zeichnungsebenen, ebenen, active_z_id, active_code, z_
|
|||||||
"""
|
"""
|
||||||
Kombinierte Sichtbarkeit aus Z-Mode (Zeichnungsebenen) und E-Mode (Ebenen).
|
Kombinierte Sichtbarkeit aus Z-Mode (Zeichnungsebenen) und E-Mode (Ebenen).
|
||||||
Beide Modi: 'all' | 'active' | 'grey' | 'grey_locked'
|
Beide Modi: 'all' | 'active' | 'grey' | 'grey_locked'
|
||||||
|
|
||||||
|
Versteht den hierarchischen Ebenen-Baum: Children erben ParentLayerId vom
|
||||||
|
Sub-Layer (nicht vom Geschoss). Sub-Sub-Layer werden rekursiv mitgepflegt.
|
||||||
"""
|
"""
|
||||||
canonical = {e["code"]: _color(e.get("color")) for e in ebenen}
|
# Flat walk durch Ebenen-Tree (top + children) — alle Codes mit ihren
|
||||||
e_eye_vis = {e["code"]: e.get("visible", True) for e in ebenen}
|
# Eye/Lock-Flags.
|
||||||
e_eye_locked = {e["code"]: e.get("locked", False) for e in ebenen}
|
flat_ebenen = [e for _path, e in walk_ebenen(ebenen)]
|
||||||
|
canonical = {e["code"]: _color(e.get("color")) for e in flat_ebenen}
|
||||||
|
e_eye_vis = {e["code"]: e.get("visible", True) for e in flat_ebenen}
|
||||||
|
e_eye_locked = {e["code"]: e.get("locked", False) for e in flat_ebenen}
|
||||||
|
|
||||||
id_to_top, name_to_top, children_by_parent = {}, {}, {}
|
id_to_top, name_to_top, children_by_parent = {}, {}, {}
|
||||||
for layer in doc.Layers:
|
for layer in doc.Layers:
|
||||||
@@ -693,17 +739,15 @@ def apply_visibility(doc, zeichnungsebenen, ebenen, active_z_id, active_code, z_
|
|||||||
if not p_vis:
|
if not p_vis:
|
||||||
continue # Children erben Parent-Hidden
|
continue # Children erben Parent-Hidden
|
||||||
|
|
||||||
# E-Mode -> Sublayer-Zustand
|
# E-Mode → Sub-Layer (rekursiv durch Tree; Sub-Sub-Layer haben Parent
|
||||||
for child in children:
|
# = Sub-Layer, nicht das Geschoss — also iterativ in die Tiefe).
|
||||||
if "_" not in child.Name:
|
def _apply_to_sublayer(child, p_grey_eff):
|
||||||
continue
|
if "_" not in child.Name: return
|
||||||
code = child.Name.split("_", 1)[0]
|
code = child.Name.split("_", 1)[0]
|
||||||
if code not in canonical:
|
if code not in canonical: return
|
||||||
continue
|
|
||||||
is_active_e = (code == active_code)
|
is_active_e = (code == active_code)
|
||||||
eye_v = e_eye_vis.get(code, True)
|
eye_v = e_eye_vis.get(code, True)
|
||||||
eye_l = e_eye_locked.get(code, False)
|
eye_l = e_eye_locked.get(code, False)
|
||||||
|
|
||||||
if is_active_e:
|
if is_active_e:
|
||||||
e_vis, e_grey, e_lock = True, False, False
|
e_vis, e_grey, e_lock = True, False, False
|
||||||
elif e_mode == "active":
|
elif e_mode == "active":
|
||||||
@@ -716,35 +760,28 @@ def apply_visibility(doc, zeichnungsebenen, ebenen, active_z_id, active_code, z_
|
|||||||
e_vis, e_grey, e_lock = True, True, True
|
e_vis, e_grey, e_lock = True, True, True
|
||||||
else: # grey
|
else: # grey
|
||||||
e_vis, e_grey, e_lock = True, True, False
|
e_vis, e_grey, e_lock = True, True, False
|
||||||
|
|
||||||
# Kombination
|
|
||||||
child_vis = e_vis
|
child_vis = e_vis
|
||||||
child_grey = p_grey or e_grey
|
child_grey = p_grey_eff or e_grey
|
||||||
child_lock = e_lock or eye_l
|
child_lock = e_lock or eye_l
|
||||||
|
|
||||||
changed = False
|
changed = False
|
||||||
if child.IsVisible != child_vis:
|
if child.IsVisible != child_vis:
|
||||||
child.IsVisible = child_vis
|
child.IsVisible = child_vis; changed = True
|
||||||
changed = True
|
|
||||||
if child.IsLocked != child_lock:
|
if child.IsLocked != child_lock:
|
||||||
child.IsLocked = child_lock
|
child.IsLocked = child_lock; changed = True
|
||||||
changed = True
|
|
||||||
if child_grey:
|
if child_grey:
|
||||||
if child.Color != GREY:
|
if child.Color != GREY:
|
||||||
child.Color = GREY
|
child.Color = GREY; changed = True
|
||||||
changed = True
|
|
||||||
else:
|
else:
|
||||||
canon = canonical.get(code)
|
canon = canonical.get(code)
|
||||||
if canon is not None and child.Color != canon:
|
if canon is not None and child.Color != canon:
|
||||||
child.Color = canon
|
child.Color = canon; changed = True
|
||||||
changed = True
|
|
||||||
# In neueren Rhino-Versionen committed der Property-Setter direkt,
|
|
||||||
# in manchen Faellen (besonders auf Mac) wird IsLocked nicht
|
|
||||||
# persistiert ohne explizites Modify. Defensiv:
|
|
||||||
if changed:
|
if changed:
|
||||||
try:
|
try: doc.Layers.Modify(child, child.LayerIndex, True)
|
||||||
doc.Layers.Modify(child, child.LayerIndex, True)
|
except Exception: pass
|
||||||
except Exception:
|
# Sub-Sub-Layer rekursiv (Children dieses Sub-Layers).
|
||||||
pass
|
# Sub-Sub-Layer erben den 'grey'-Zustand des Parents.
|
||||||
|
for grand in children_by_parent.get(child.Id, []):
|
||||||
|
_apply_to_sublayer(grand, child_grey)
|
||||||
|
for child in children:
|
||||||
|
_apply_to_sublayer(child, p_grey)
|
||||||
doc.Views.Redraw()
|
doc.Views.Redraw()
|
||||||
|
|||||||
+83
-21
@@ -87,9 +87,15 @@ def _broadcast_state(doc=None, hatch_patterns=None):
|
|||||||
try:
|
try:
|
||||||
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
|
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
|
||||||
e_raw = doc.Strings.GetValue("dossier_ebenen")
|
e_raw = doc.Strings.GetValue("dossier_ebenen")
|
||||||
|
# Projekt-Nullpunkt in m.ü.M — wird beim Swisstopo-Import als
|
||||||
|
# Z-Offset angewandt (Real-Welt-Höhen → Doc-Z relativ zu OKFF=0).
|
||||||
|
zero_raw = doc.Strings.GetValue("dossier_project_zero_mum")
|
||||||
|
try: zero_mum = float(zero_raw) if zero_raw else 0.0
|
||||||
|
except Exception: zero_mum = 0.0
|
||||||
payload = {
|
payload = {
|
||||||
"zeichnungsebenen": json.loads(z_raw) if z_raw else None,
|
"zeichnungsebenen": json.loads(z_raw) if z_raw else None,
|
||||||
"ebenen": json.loads(e_raw) if e_raw else None,
|
"ebenen": json.loads(e_raw) if e_raw else None,
|
||||||
|
"projectZeroMum": zero_mum,
|
||||||
"hatchPatterns": hatch_patterns if hatch_patterns is not None
|
"hatchPatterns": hatch_patterns if hatch_patterns is not None
|
||||||
else _hatch_pattern_names(doc),
|
else _hatch_pattern_names(doc),
|
||||||
}
|
}
|
||||||
@@ -367,9 +373,13 @@ class EbenenBridge(panel_base.BaseBridge):
|
|||||||
layer_builder.build_layers(doc, z, e)
|
layer_builder.build_layers(doc, z, e)
|
||||||
layer_builder.cleanup_default_layers(doc)
|
layer_builder.cleanup_default_layers(doc)
|
||||||
self._ensure_active_sublayer()
|
self._ensure_active_sublayer()
|
||||||
|
zero_raw = doc.Strings.GetValue("dossier_project_zero_mum")
|
||||||
|
try: zero_mum = float(zero_raw) if zero_raw else 0.0
|
||||||
|
except Exception: zero_mum = 0.0
|
||||||
self.send("STATE_SYNC", {
|
self.send("STATE_SYNC", {
|
||||||
"zeichnungsebenen": z,
|
"zeichnungsebenen": z,
|
||||||
"ebenen": e,
|
"ebenen": e,
|
||||||
|
"projectZeroMum": zero_mum,
|
||||||
"hatchPatterns": _hatch_pattern_names(doc),
|
"hatchPatterns": _hatch_pattern_names(doc),
|
||||||
})
|
})
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
@@ -471,9 +481,28 @@ class EbenenBridge(panel_base.BaseBridge):
|
|||||||
print("[EBENEN] open_geschoss_settings: kein Geschoss-Payload")
|
print("[EBENEN] open_geschoss_settings: kein Geschoss-Payload")
|
||||||
return
|
return
|
||||||
gid = geschoss["id"]
|
gid = geschoss["id"]
|
||||||
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
# Projekt-Nullpunkt (m.ü.M) mit ins Param-Bundle — als projektweite
|
||||||
|
# Settings auch im Geschoss-Dialog editierbar.
|
||||||
|
try:
|
||||||
|
z_mum_raw = doc.Strings.GetValue("dossier_project_zero_mum") if doc else None
|
||||||
|
project_zero_mum = float(z_mum_raw) if z_mum_raw else 0.0
|
||||||
|
except Exception:
|
||||||
|
project_zero_mum = 0.0
|
||||||
|
params = dict(geschoss)
|
||||||
|
params["projectZeroMum"] = project_zero_mum
|
||||||
def on_save(updated):
|
def on_save(updated):
|
||||||
doc = Rhino.RhinoDoc.ActiveDoc
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
if doc is None: return
|
if doc is None: return
|
||||||
|
# Projekt-Nullpunkt extrahieren (project-weit, nicht pro Geschoss)
|
||||||
|
try:
|
||||||
|
if "projectZeroMum" in updated:
|
||||||
|
val = updated.pop("projectZeroMum")
|
||||||
|
val = float(val) if val is not None else 0.0
|
||||||
|
doc.Strings.SetString("dossier_project_zero_mum", str(val))
|
||||||
|
print("[EBENEN] project_zero_mum = {} m.ü.M".format(val))
|
||||||
|
except Exception as ex:
|
||||||
|
print("[EBENEN] project_zero_mum save:", ex)
|
||||||
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
|
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
|
||||||
if not z_raw:
|
if not z_raw:
|
||||||
print("[EBENEN] save_geschoss: kein z-Store"); return
|
print("[EBENEN] save_geschoss: kein z-Store"); return
|
||||||
@@ -497,9 +526,9 @@ class EbenenBridge(panel_base.BaseBridge):
|
|||||||
self._apply(z_list, e_list, save_z=True, save_e=False)
|
self._apply(z_list, e_list, save_z=True, save_e=False)
|
||||||
panel_base.open_satellite_window(
|
panel_base.open_satellite_window(
|
||||||
"geschoss_settings",
|
"geschoss_settings",
|
||||||
params=geschoss,
|
params=params,
|
||||||
title="Zeichnungsebene: {}".format(geschoss.get("name", "")),
|
title="Zeichnungsebene: {}".format(geschoss.get("name", "")),
|
||||||
size=(380, 540),
|
size=(380, 580),
|
||||||
on_save=on_save)
|
on_save=on_save)
|
||||||
|
|
||||||
def _open_ebenen_settings(self, ebene, hatch_patterns):
|
def _open_ebenen_settings(self, ebene, hatch_patterns):
|
||||||
@@ -733,22 +762,36 @@ class EbenenBridge(panel_base.BaseBridge):
|
|||||||
return
|
return
|
||||||
payload_z = p.get("zeichnungsebenen") or []
|
payload_z = p.get("zeichnungsebenen") or []
|
||||||
payload_e = p.get("ebenen") or []
|
payload_e = p.get("ebenen") or []
|
||||||
|
# Hilfsfunktion: alle Codes (inkl. Children) als flat dict {code: ebene}
|
||||||
|
def _walk_codes(lst):
|
||||||
|
out = {}
|
||||||
|
if not isinstance(lst, list): return out
|
||||||
|
for x in lst:
|
||||||
|
if not isinstance(x, dict): continue
|
||||||
|
c = x.get("code")
|
||||||
|
if c: out[c] = x
|
||||||
|
kids = x.get("children")
|
||||||
|
if isinstance(kids, list):
|
||||||
|
out.update(_walk_codes(kids))
|
||||||
|
return out
|
||||||
# Strukturelle Aenderung pending? Wenn React-Payload IDs/Codes enthaelt
|
# Strukturelle Aenderung pending? Wenn React-Payload IDs/Codes enthaelt
|
||||||
# die noch nicht in doc.Strings sind (= User hat gerade neue Ebene
|
# die noch nicht in doc.Strings sind (= User hat gerade neue Ebene
|
||||||
# angelegt aber der strukturelle APPLY ist noch in der 200ms-Debounce),
|
# angelegt aber der strukturelle APPLY ist noch in der 200ms-Debounce),
|
||||||
# NICHT speichern. Sonst ueberschreibt die schnellere SET_VISIBILITY
|
# NICHT speichern. Sonst ueberschreibt die schnellere SET_VISIBILITY
|
||||||
# den geplanten APPLY-Save und die neue Ebene geht in der Race
|
# den geplanten APPLY-Save und die neue Ebene geht in der Race
|
||||||
# verloren.
|
# verloren.
|
||||||
payload_z_ids = {z.get("id") for z in payload_z if isinstance(z, dict)}
|
payload_z_ids = {z.get("id") for z in payload_z if isinstance(z, dict)}
|
||||||
payload_e_codes = {e.get("code") for e in payload_e if isinstance(e, dict)}
|
payload_e_codes = set(_walk_codes(payload_e).keys())
|
||||||
existing_z_ids = {z.get("id") for z in z_full if isinstance(z, dict)}
|
existing_z_ids = {z.get("id") for z in z_full if isinstance(z, dict)}
|
||||||
existing_e_codes = {e.get("code") for e in e_full if isinstance(e, dict)}
|
existing_e_codes = set(_walk_codes(e_full).keys())
|
||||||
has_new_structural = (
|
has_new_structural = (
|
||||||
bool(payload_z_ids - existing_z_ids - {None}) or
|
bool(payload_z_ids - existing_z_ids - {None}) or
|
||||||
bool(payload_e_codes - existing_e_codes - {None})
|
bool(payload_e_codes - existing_e_codes - {None})
|
||||||
)
|
)
|
||||||
z_state = {z["id"]: z for z in payload_z if isinstance(z, dict) and z.get("id")}
|
z_state = {z["id"]: z for z in payload_z if isinstance(z, dict) and z.get("id")}
|
||||||
e_state = {e["code"]: e for e in payload_e if isinstance(e, dict) and e.get("code")}
|
# e_state ist flach (Code → Ebene) ueber den ganzen Tree des Payloads,
|
||||||
|
# damit auch Child-Visibility-Toggles ankommen.
|
||||||
|
e_state = _walk_codes(payload_e)
|
||||||
merged_z = []
|
merged_z = []
|
||||||
for z in z_full:
|
for z in z_full:
|
||||||
if not isinstance(z, dict): continue
|
if not isinstance(z, dict): continue
|
||||||
@@ -758,23 +801,40 @@ class EbenenBridge(panel_base.BaseBridge):
|
|||||||
m["visible"] = s.get("visible", True)
|
m["visible"] = s.get("visible", True)
|
||||||
m["locked"] = s.get("locked", False)
|
m["locked"] = s.get("locked", False)
|
||||||
merged_z.append(m)
|
merged_z.append(m)
|
||||||
merged_e = []
|
# Merge fuer Ebenen rekursiv: jedes Element behaelt seine Position +
|
||||||
for e in e_full:
|
# children-Struktur, nur visible/locked werden ueberschrieben falls
|
||||||
if not isinstance(e, dict): continue
|
# im Payload anwesend.
|
||||||
m = dict(e)
|
def _merge_ebenen_tree(orig_list):
|
||||||
s = e_state.get(e.get("code"))
|
out = []
|
||||||
if s is not None:
|
for e in orig_list:
|
||||||
m["visible"] = s.get("visible", True)
|
if not isinstance(e, dict): continue
|
||||||
m["locked"] = s.get("locked", False)
|
m = dict(e)
|
||||||
merged_e.append(m)
|
s = e_state.get(e.get("code"))
|
||||||
|
if s is not None:
|
||||||
|
m["visible"] = s.get("visible", True)
|
||||||
|
m["locked"] = s.get("locked", False)
|
||||||
|
kids = e.get("children")
|
||||||
|
if isinstance(kids, list):
|
||||||
|
m["children"] = _merge_ebenen_tree(kids)
|
||||||
|
out.append(m)
|
||||||
|
return out
|
||||||
|
merged_e = _merge_ebenen_tree(e_full)
|
||||||
# Detect whether the merge actually changed any visible/locked values.
|
# Detect whether the merge actually changed any visible/locked values.
|
||||||
# Wenn nicht: das ist nur der Echo-Roundtrip eines apply_layer_preset
|
# Wenn nicht: das ist nur der Echo-Roundtrip eines apply_layer_preset
|
||||||
# (React-State == doc.Strings → kein User-Click) und wir wollen das
|
# (React-State == doc.Strings → kein User-Click) und wir wollen das
|
||||||
# aktive Preset NICHT clearen.
|
# aktive Preset NICHT clearen. Bei Ebenen rekursiv durch Children.
|
||||||
|
def _flatten(lst):
|
||||||
|
out = []
|
||||||
|
for x in (lst or []):
|
||||||
|
if not isinstance(x, dict): continue
|
||||||
|
out.append(x)
|
||||||
|
kids = x.get("children")
|
||||||
|
if isinstance(kids, list):
|
||||||
|
out.extend(_flatten(kids))
|
||||||
|
return out
|
||||||
def _vis_lock_changed(old, new):
|
def _vis_lock_changed(old, new):
|
||||||
old_by = {x.get("id") or x.get("code"): x for x in old if isinstance(x, dict)}
|
old_by = {x.get("id") or x.get("code"): x for x in _flatten(old)}
|
||||||
for nx in new:
|
for nx in _flatten(new):
|
||||||
if not isinstance(nx, dict): continue
|
|
||||||
key = nx.get("id") or nx.get("code")
|
key = nx.get("id") or nx.get("code")
|
||||||
if key is None: continue
|
if key is None: continue
|
||||||
ox = old_by.get(key)
|
ox = old_by.get(key)
|
||||||
@@ -815,10 +875,12 @@ class EbenenBridge(panel_base.BaseBridge):
|
|||||||
bool(z.get("visible", True)),
|
bool(z.get("visible", True)),
|
||||||
bool(z.get("locked", False)))
|
bool(z.get("locked", False)))
|
||||||
for z in zlist if isinstance(z, dict))
|
for z in zlist if isinstance(z, dict))
|
||||||
|
# Ebenen flat ueber Children — sonst dedupt der Cache auch nach
|
||||||
|
# einem Child-Toggle, weil die Top-Level-Liste identisch aussieht.
|
||||||
es = tuple((e.get("code"),
|
es = tuple((e.get("code"),
|
||||||
bool(e.get("visible", True)),
|
bool(e.get("visible", True)),
|
||||||
bool(e.get("locked", False)))
|
bool(e.get("locked", False)))
|
||||||
for e in elist if isinstance(e, dict))
|
for e in _flatten(elist))
|
||||||
return (active_z_id, active_code, z_mode, e_mode, zs, es)
|
return (active_z_id, active_code, z_mode, e_mode, zs, es)
|
||||||
cur_sig = _sig(merged_z, merged_e)
|
cur_sig = _sig(merged_z, merged_e)
|
||||||
if sc.sticky.get("_vis_last_sig") == cur_sig and not any_changed:
|
if sc.sticky.get("_vis_last_sig") == cur_sig and not any_changed:
|
||||||
|
|||||||
+236
-72
@@ -323,25 +323,38 @@ def _extract_zip_to_dir(zip_path, dest_dir):
|
|||||||
|
|
||||||
# --- Buildings: 3D-Gebaeude DWG --------------------------------------------
|
# --- Buildings: 3D-Gebaeude DWG --------------------------------------------
|
||||||
|
|
||||||
# swissBUILDINGS3D 3.0 ist Cesium-3D-Tiles (kein DWG). Fuer DWG-Import nutzen
|
# swissBUILDINGS3D 3.0: liefert mehrere Formate (DXF/DWG/OBJ/IFC/3DTiles) und
|
||||||
# wir die 2.0-Variante.
|
# variant-Filter (solid/separated). In Staedten sind die 3.0-Tiles aber riesig
|
||||||
_BUILDINGS_COLLECTION = "ch.swisstopo.swissbuildings3d_2"
|
# (>700 MB), weil nicht 1km-strukturiert — dann fallen wir auf 2.0 zurueck
|
||||||
|
# (verlaesslich 1km-Tiles, ~50 MB).
|
||||||
|
_BUILDINGS_COLLECTION_V3 = "ch.swisstopo.swissbuildings3d_3_0"
|
||||||
|
_BUILDINGS_COLLECTION_V2 = "ch.swisstopo.swissbuildings3d_2"
|
||||||
|
|
||||||
|
|
||||||
def fetch_buildings_dwg(bbox_lv95, progress=None):
|
def _fetch_buildings_from_collection(collection_id, bbox_wgs, variant,
|
||||||
"""Holt swissBUILDINGS3D 2.0 Tile-DXF/DWG-Files fuer eine LV95-bbox.
|
progress=None):
|
||||||
Wichtig: filtert NUR per-Tile-Assets (Pattern `_NNNN-NN_`). National-
|
"""Holt Tile-CAD-Files aus EINER STAC-Collection. Liefert Liste Pfade
|
||||||
Geodatabase-Assets (>1 GB) werden NICHT gematcht — sonst laedt das Plugin
|
oder [] wenn nichts brauchbar geladen werden konnte (z.B. alle ueber
|
||||||
versehentlich den gesamt-CH-Datensatz."""
|
Size-Limit)."""
|
||||||
bbox_wgs = lv95_bbox_to_wgs84_bbox(*bbox_lv95)
|
if progress: progress("STAC-Query {} (variant={})...".format(
|
||||||
if progress: progress("STAC-Query " + _BUILDINGS_COLLECTION + "...")
|
collection_id, variant))
|
||||||
items = stac_query(_BUILDINGS_COLLECTION, bbox_wgs,
|
items = stac_query(collection_id, bbox_wgs,
|
||||||
asset_extensions=[".dwg", ".dxf",
|
asset_extensions=[".dwg", ".dxf", ".obj", ".ifc",
|
||||||
".dwg.zip", ".dxf.zip"])
|
".dwg.zip", ".dxf.zip",
|
||||||
|
".obj.zip", ".ifc.zip"])
|
||||||
items = _dedupe_latest(items)
|
items = _dedupe_latest(items)
|
||||||
if not items:
|
if not items:
|
||||||
if progress: progress("Keine Tiles in der Region (collection={})".format(_BUILDINGS_COLLECTION))
|
if progress: progress(" Keine Tiles in der Region")
|
||||||
return []
|
return []
|
||||||
|
variant_marker = "_{}_".format(variant.lower())
|
||||||
|
items_v = [it for it in items
|
||||||
|
if variant_marker in (it.get("id") or "").lower()
|
||||||
|
or any(variant_marker in (a.get("href") or "").lower()
|
||||||
|
for a in it.get("assets", {}).values())]
|
||||||
|
if items_v and len(items_v) < len(items):
|
||||||
|
if progress: progress(" Item-Filter: {}/{} matchen variant '{}'".format(
|
||||||
|
len(items_v), len(items), variant))
|
||||||
|
items = items_v
|
||||||
paths = []
|
paths = []
|
||||||
for i, item in enumerate(items):
|
for i, item in enumerate(items):
|
||||||
if progress: progress("Lade Tile {}/{}: {}".format(
|
if progress: progress("Lade Tile {}/{}: {}".format(
|
||||||
@@ -353,31 +366,69 @@ def fetch_buildings_dwg(bbox_lv95, progress=None):
|
|||||||
if not per_tile:
|
if not per_tile:
|
||||||
if progress: progress("→ kein Per-Tile-Asset, skip")
|
if progress: progress("→ kein Per-Tile-Asset, skip")
|
||||||
continue
|
continue
|
||||||
# Priorisierung: direkt-DXF/DWG > ZIP-DXF/DWG
|
# Varianten-Filter: `_solid_` vs `_separated_` im Filename. Default
|
||||||
|
# ist 'separated'. Falls keine Asset mit dem Marker matcht (alte
|
||||||
|
# Collection-Version o.ae.), fallen wir auf alle per-tile zurueck.
|
||||||
|
variant_marker = "_{}_".format(variant.lower())
|
||||||
|
per_tile_v = [(k, a) for k, a in per_tile
|
||||||
|
if variant_marker in a["href"].lower()]
|
||||||
|
if per_tile_v:
|
||||||
|
per_tile = per_tile_v
|
||||||
|
if progress: progress(" → {} Asset(s) matchen variant '{}'".format(
|
||||||
|
len(per_tile), variant))
|
||||||
|
else:
|
||||||
|
if progress:
|
||||||
|
hrefs_short = ", ".join(os.path.basename(a["href"])
|
||||||
|
for _, a in per_tile[:3])
|
||||||
|
progress(" → kein '{}' Marker, nehme erstes Asset (verfuegbar: {})".format(
|
||||||
|
variant, hrefs_short))
|
||||||
|
# Priorisierung: DXF/DWG (am stabilsten in Rhino) > OBJ > IFC
|
||||||
chosen = None
|
chosen = None
|
||||||
for k, a in per_tile:
|
for prio_ext in [(".dxf", ".dwg"), (".obj",), (".ifc",),
|
||||||
low = a["href"].lower()
|
(".dxf.zip", ".dwg.zip"), (".obj.zip",), (".ifc.zip",)]:
|
||||||
if low.endswith((".dxf", ".dwg")):
|
|
||||||
chosen = a["href"]; break
|
|
||||||
if chosen is None:
|
|
||||||
for k, a in per_tile:
|
for k, a in per_tile:
|
||||||
low = a["href"].lower()
|
low = a["href"].lower()
|
||||||
if low.endswith((".dxf.zip", ".dwg.zip")):
|
if low.endswith(prio_ext):
|
||||||
chosen = a["href"]; break
|
chosen = a["href"]; break
|
||||||
|
if chosen is not None: break
|
||||||
if chosen is None:
|
if chosen is None:
|
||||||
chosen = per_tile[0][1]["href"]
|
chosen = per_tile[0][1]["href"]
|
||||||
|
|
||||||
p = download_asset(chosen, subdir="buildings3d_dwg", status=progress)
|
p = download_asset(chosen, subdir="buildings3d_dwg", status=progress)
|
||||||
if not p: continue
|
if not p: continue
|
||||||
|
|
||||||
# ZIP-Wrapper aufloesen
|
# ZIP-Wrapper aufloesen + Variant-Filter (ZIP kann beide DWGs enthalten)
|
||||||
if p.lower().endswith(".zip"):
|
if p.lower().endswith(".zip"):
|
||||||
extracted = _extract_zip_to_dir(
|
extracted = _extract_zip_to_dir(
|
||||||
p, os.path.join(CACHE_DIR, "buildings3d_dwg", "_unzipped"))
|
p, os.path.join(CACHE_DIR, "buildings3d_dwg", "_unzipped"))
|
||||||
dwgs = [e for e in extracted if e.lower().endswith((".dwg", ".dxf"))]
|
cads_all = [e for e in extracted
|
||||||
paths.extend(dwgs)
|
if e.lower().endswith((".dwg", ".dxf", ".obj", ".ifc"))]
|
||||||
|
cads_v = [e for e in cads_all
|
||||||
|
if variant_marker in os.path.basename(e).lower()]
|
||||||
|
cads = cads_v if cads_v else cads_all
|
||||||
|
if cads_v and len(cads_v) < len(cads_all):
|
||||||
|
if progress: progress(" ZIP-Filter: {}/{} Files matchen '{}'".format(
|
||||||
|
len(cads_v), len(cads_all), variant))
|
||||||
|
paths.extend(cads)
|
||||||
else:
|
else:
|
||||||
paths.append(p)
|
paths.append(p)
|
||||||
|
return paths
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_buildings_dwg(bbox_lv95, progress=None, variant="separated"):
|
||||||
|
"""Holt swissBUILDINGS3D Tile-CAD-Files. Versucht erst v3.0 (separated/
|
||||||
|
solid Varianten), faellt automatisch auf v2.0 zurueck wenn v3.0 in der
|
||||||
|
Region keine brauchbaren Files liefert (typisch in Staedten — die 3.0-
|
||||||
|
Tiles sind dort >700 MB pro Stueck und werden vom Size-Limit geblockt)."""
|
||||||
|
bbox_wgs = lv95_bbox_to_wgs84_bbox(*bbox_lv95)
|
||||||
|
paths = _fetch_buildings_from_collection(
|
||||||
|
_BUILDINGS_COLLECTION_V3, bbox_wgs, variant, progress=progress)
|
||||||
|
if not paths:
|
||||||
|
if progress: progress("v3.0 lieferte keine Tiles — fallback auf v2.0 (1km-Tiles)...")
|
||||||
|
# v2.0 hat keine variant-Marker im Filename, ist immer "separated"-
|
||||||
|
# artig (Kategorien auf eigenen DXF-Layern innerhalb einer DWG).
|
||||||
|
paths = _fetch_buildings_from_collection(
|
||||||
|
_BUILDINGS_COLLECTION_V2, bbox_wgs, variant, progress=progress)
|
||||||
if progress: progress("{} CAD-Datei(en) bereit".format(len(paths)))
|
if progress: progress("{} CAD-Datei(en) bereit".format(len(paths)))
|
||||||
return paths
|
return paths
|
||||||
|
|
||||||
@@ -520,6 +571,41 @@ def xyz_to_grid(path, target_step=2.0, clip_bbox=None, progress=None):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def merge_grids(grids):
|
||||||
|
"""Vereint mehrere Grid-Dicts (eines pro XYZ-Tile) zu einem zusammen-
|
||||||
|
haengenden Grid. swissALTI3D-Tiles liefern jeweils 1km×1km Punkte —
|
||||||
|
benachbarte Tiles teilen KEINE Rand-Punkte (Tile A endet z.B. bei
|
||||||
|
e=2700999.5, Tile B startet bei e=2701000.0). Beim getrennten Meshen
|
||||||
|
entsteht dadurch ein 1m-Streifen ohne Faces. Hier mergen wir die
|
||||||
|
Punkte VORHER zu einem unified Grid, dann verbindet mesh_from_grid
|
||||||
|
die Tile-Grenze automatisch (benachbarte Spalten = ein step Abstand
|
||||||
|
nach Sub-Sampling).
|
||||||
|
|
||||||
|
Annahme: alle Grids teilen sich denselben step (gleicher target_step
|
||||||
|
+ Quell-Resolution). Origin-Alignment ist gegeben, weil swissALTI3D
|
||||||
|
auf einem globalen 0.5m-Raster liegt."""
|
||||||
|
if not grids: return None
|
||||||
|
grids = [g for g in grids if g is not None]
|
||||||
|
if not grids: return None
|
||||||
|
if len(grids) == 1: return grids[0]
|
||||||
|
step = grids[0]["step"]
|
||||||
|
all_points = {}
|
||||||
|
all_es = set(); all_ns = set()
|
||||||
|
for g in grids:
|
||||||
|
for (e, n), z in g["points"].items():
|
||||||
|
all_points[(e, n)] = z
|
||||||
|
all_es.add(e); all_ns.add(n)
|
||||||
|
if not all_points: return None
|
||||||
|
es_sorted = sorted(all_es); ns_sorted = sorted(all_ns)
|
||||||
|
return {
|
||||||
|
"bbox": (es_sorted[0], ns_sorted[0], es_sorted[-1], ns_sorted[-1]),
|
||||||
|
"step": step,
|
||||||
|
"es": es_sorted,
|
||||||
|
"ns": ns_sorted,
|
||||||
|
"points": all_points,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def mesh_from_grid(grid, origin_shift=(0, 0, 0), unit_scale=1.0):
|
def mesh_from_grid(grid, origin_shift=(0, 0, 0), unit_scale=1.0):
|
||||||
"""Baut ein Rhino-Mesh aus dem XYZ-Grid. origin_shift wird auf jeden
|
"""Baut ein Rhino-Mesh aus dem XYZ-Grid. origin_shift wird auf jeden
|
||||||
Vertex angewendet (typisch: bbox-Center zu Welt-0/0/0 schieben).
|
Vertex angewendet (typisch: bbox-Center zu Welt-0/0/0 schieben).
|
||||||
@@ -584,53 +670,131 @@ def fetch_orthophoto(bbox_lv95, resolution="2.0", progress=None):
|
|||||||
return paths
|
return paths
|
||||||
|
|
||||||
|
|
||||||
def apply_ortho_material(doc, mesh_obj, ortho_path, mesh_bbox_lv95):
|
def _geotiff_to_png(tif_path, max_dim=2048):
|
||||||
"""Erzeugt Rhino-Material mit dem SWISSIMAGE-GeoTIFF als Bitmap-Texture,
|
"""SWISSIMAGE kommt als GeoTIFF — Rhinos Material-Bitmap kann GeoTIFF nicht
|
||||||
weist es dem mesh_obj zu. UV-Mapping kommt aus den XY-Koords (linear auf
|
direkt lesen. Konvertiere zu PNG. Zwei Wege:
|
||||||
der bbox)."""
|
1) Pillow (wenn in Rhinos CPython verfuegbar) — universell + downsample
|
||||||
if not (ortho_path and os.path.isfile(ortho_path)): return
|
2) Eto.Drawing.Bitmap (Mac: NSImage liest TIFF nativ) — Fallback
|
||||||
|
Liefert PNG-Pfad oder None bei Fehler."""
|
||||||
|
if not tif_path: return None
|
||||||
|
base, _ = os.path.splitext(tif_path)
|
||||||
|
png_path = base + "_2k.png"
|
||||||
|
if os.path.isfile(png_path) and os.path.getsize(png_path) > 0:
|
||||||
|
print("[SWISSTOPO] PNG-Cache:", os.path.basename(png_path))
|
||||||
|
return png_path
|
||||||
|
# --- Variante 1: Pillow
|
||||||
try:
|
try:
|
||||||
rdoc = doc.RenderMaterials
|
from PIL import Image
|
||||||
from Rhino.Render import RenderMaterial, RenderContent
|
img = Image.open(tif_path)
|
||||||
try:
|
if max(img.width, img.height) > max_dim:
|
||||||
mat = RenderMaterial.CreateBasicMaterial(
|
scale = max_dim / float(max(img.width, img.height))
|
||||||
Rhino.DocObjects.Material(), doc)
|
new_w = max(1, int(img.width * scale))
|
||||||
except Exception:
|
new_h = max(1, int(img.height * scale))
|
||||||
mat = RenderMaterial.CreateBasicMaterial(
|
img = img.resize((new_w, new_h), Image.LANCZOS)
|
||||||
Rhino.DocObjects.Material())
|
if img.mode not in ("RGB", "RGBA"):
|
||||||
try: mat.Name = "swisstopo_ortho_" + os.path.basename(ortho_path)
|
img = img.convert("RGB")
|
||||||
except Exception: pass
|
img.save(png_path, "PNG", optimize=False)
|
||||||
# Bitmap zuweisen — Property-Name variiert mit Rhino-Version
|
print("[SWISSTOPO] Pillow: {} → {} ({}x{}px)".format(
|
||||||
try:
|
os.path.basename(tif_path), os.path.basename(png_path),
|
||||||
mat.SetParameter("diffuse-bitmap-filename", ortho_path)
|
img.width, img.height))
|
||||||
except Exception as ex:
|
return png_path
|
||||||
print("[SWISSTOPO] material bitmap:", ex)
|
except ImportError:
|
||||||
try:
|
print("[SWISSTOPO] Pillow nicht verfuegbar — versuche Eto.Drawing")
|
||||||
mid = rdoc.Add(mat)
|
|
||||||
except Exception:
|
|
||||||
mid = doc.Materials.Add()
|
|
||||||
# UV-Mapping: planar in XY-bbox
|
|
||||||
e_min, n_min, e_max, n_max = mesh_bbox_lv95
|
|
||||||
try:
|
|
||||||
plane = rg.Plane(rg.Point3d((e_min + e_max) / 2.0,
|
|
||||||
(n_min + n_max) / 2.0, 0),
|
|
||||||
rg.Vector3d.ZAxis)
|
|
||||||
dx = abs(e_max - e_min)
|
|
||||||
dy = abs(n_max - n_min)
|
|
||||||
mapping = Rhino.Render.TextureMapping.CreatePlaneMapping(
|
|
||||||
plane, rg.Interval(-dx/2.0, dx/2.0),
|
|
||||||
rg.Interval(-dy/2.0, dy/2.0),
|
|
||||||
rg.Interval(-1, 1))
|
|
||||||
doc.Objects.ModifyTextureMapping(mesh_obj, 1, mapping)
|
|
||||||
except Exception as ex:
|
|
||||||
print("[SWISSTOPO] uv-mapping:", ex)
|
|
||||||
# Material aufs Object setzen
|
|
||||||
try:
|
|
||||||
attrs = mesh_obj.Attributes.Duplicate()
|
|
||||||
attrs.MaterialSource = Rhino.DocObjects.ObjectMaterialSource.MaterialFromObject
|
|
||||||
attrs.RenderMaterial = mat
|
|
||||||
doc.Objects.ModifyAttributes(mesh_obj, attrs, True)
|
|
||||||
except Exception as ex:
|
|
||||||
print("[SWISSTOPO] material assign:", ex)
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[SWISSTOPO] apply_ortho_material:", ex)
|
print("[SWISSTOPO] Pillow-convert fail:", ex)
|
||||||
|
# --- Variante 2: Eto.Drawing (Mac NSImage liest TIFF)
|
||||||
|
try:
|
||||||
|
import Eto.Drawing as _ed
|
||||||
|
bmp_src = _ed.Bitmap(tif_path)
|
||||||
|
if bmp_src is None:
|
||||||
|
print("[SWISSTOPO] Eto konnte TIFF nicht laden")
|
||||||
|
return None
|
||||||
|
# Downsample falls > max_dim
|
||||||
|
w, h = bmp_src.Width, bmp_src.Height
|
||||||
|
if max(w, h) > max_dim:
|
||||||
|
scale = max_dim / float(max(w, h))
|
||||||
|
new_w = max(1, int(w * scale))
|
||||||
|
new_h = max(1, int(h * scale))
|
||||||
|
target = _ed.Bitmap(new_w, new_h, _ed.PixelFormat.Format32bppRgba)
|
||||||
|
g = _ed.Graphics(target)
|
||||||
|
try:
|
||||||
|
try: g.AntiAlias = True
|
||||||
|
except Exception: pass
|
||||||
|
g.DrawImage(bmp_src, 0, 0, new_w, new_h)
|
||||||
|
finally: g.Dispose()
|
||||||
|
bmp_src = target
|
||||||
|
w, h = new_w, new_h
|
||||||
|
try: bmp_src.Save(png_path, _ed.ImageFormat.Png)
|
||||||
|
except Exception:
|
||||||
|
# Eto.ImageFormat-Variante kann je nach Eto-Version variieren
|
||||||
|
bmp_src.Save(png_path)
|
||||||
|
print("[SWISSTOPO] Eto: {} → {} ({}x{}px)".format(
|
||||||
|
os.path.basename(tif_path), os.path.basename(png_path), w, h))
|
||||||
|
return png_path
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SWISSTOPO] Eto-convert fail:", ex)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def add_ortho_plane(doc, ortho_path, tile_bbox_lv95, shift_lv95, m_to_unit,
|
||||||
|
z_doc=0.0):
|
||||||
|
"""Erzeugt eine planare Brep-Flaeche mit dem SWISSIMAGE-Foto als Material,
|
||||||
|
direkt sichtbar in Top/Shaded/Rendered Display-Mode.
|
||||||
|
|
||||||
|
tile_bbox_lv95: (e_min, n_min, e_max, n_max) in LV95-Metern der Tile-Region
|
||||||
|
shift_lv95: (sx, sy, sz) — Origin-Shift in LV95-Metern (typisch eC,nC)
|
||||||
|
m_to_unit: Skalierung m → doc-units (z.B. 0.001 fuer km-Doc)
|
||||||
|
z_doc: Z-Hoehe der Plane in Doc-Units (typisch max-Terrain-Z + Epsilon)
|
||||||
|
|
||||||
|
Liefert den RhinoObject der erzeugten Plane (oder None)."""
|
||||||
|
if not (ortho_path and os.path.isfile(ortho_path)): return None
|
||||||
|
# GeoTIFF → PNG damit Rhino's Material-Bitmap es als Diffuse nehmen kann
|
||||||
|
if ortho_path.lower().endswith((".tif", ".tiff")):
|
||||||
|
png = _geotiff_to_png(ortho_path)
|
||||||
|
if not png: return None
|
||||||
|
ortho_path = png
|
||||||
|
# bbox in Doc-Units (nach Shift + Scale)
|
||||||
|
e_min, n_min, e_max, n_max = tile_bbox_lv95
|
||||||
|
sx, sy, sz = shift_lv95
|
||||||
|
x_min = (e_min - sx) * m_to_unit
|
||||||
|
x_max = (e_max - sx) * m_to_unit
|
||||||
|
y_min = (n_min - sy) * m_to_unit
|
||||||
|
y_max = (n_max - sy) * m_to_unit
|
||||||
|
# Mesh-Quad mit expliziten Per-Vertex-UV-Koordinaten — bombensicher
|
||||||
|
# fuer Cycles/Raytraced. Eine Brep-Plane braucht erst Render-Mesh-
|
||||||
|
# Erzeugung + TextureMapping, was diverse Fallstricke hat.
|
||||||
|
mesh = rg.Mesh()
|
||||||
|
mesh.Vertices.Add(x_min, y_min, z_doc) # 0 → UV (0,0)
|
||||||
|
mesh.Vertices.Add(x_max, y_min, z_doc) # 1 → UV (1,0)
|
||||||
|
mesh.Vertices.Add(x_max, y_max, z_doc) # 2 → UV (1,1)
|
||||||
|
mesh.Vertices.Add(x_min, y_max, z_doc) # 3 → UV (0,1)
|
||||||
|
mesh.Faces.AddFace(0, 1, 2, 3)
|
||||||
|
mesh.TextureCoordinates.Add(0.0, 0.0)
|
||||||
|
mesh.TextureCoordinates.Add(1.0, 0.0)
|
||||||
|
mesh.TextureCoordinates.Add(1.0, 1.0)
|
||||||
|
mesh.TextureCoordinates.Add(0.0, 1.0)
|
||||||
|
mesh.Normals.ComputeNormals()
|
||||||
|
mesh.Compact()
|
||||||
|
gid = doc.Objects.AddMesh(mesh)
|
||||||
|
obj = doc.Objects.Find(gid)
|
||||||
|
if obj is None: return None
|
||||||
|
# Material: Legacy + ToPhysicallyBased + PBR_BaseColor-Texture.
|
||||||
|
# Bekannt instabil unter Mac Rhino 8 für Raytraced (Cycles greift den
|
||||||
|
# Shim nicht zuverlaessig); zumindest Shaded zeigt die Textur.
|
||||||
|
try:
|
||||||
|
mat = Rhino.DocObjects.Material()
|
||||||
|
mat.Name = "swisstopo_ortho"
|
||||||
|
mat.SetBitmapTexture(ortho_path)
|
||||||
|
mat.ToPhysicallyBased()
|
||||||
|
tex = Rhino.DocObjects.Texture()
|
||||||
|
tex.FileName = ortho_path
|
||||||
|
tex.Enabled = True
|
||||||
|
mat.SetTexture(tex, Rhino.DocObjects.TextureType.PBR_BaseColor)
|
||||||
|
midx = doc.Materials.Add(mat)
|
||||||
|
attrs = obj.Attributes.Duplicate()
|
||||||
|
attrs.MaterialSource = Rhino.DocObjects.ObjectMaterialSource.MaterialFromObject
|
||||||
|
attrs.MaterialIndex = midx
|
||||||
|
doc.Objects.ModifyAttributes(obj, attrs, True)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SWISSTOPO] ortho-material:", ex)
|
||||||
|
return obj
|
||||||
|
|||||||
+20
-4
@@ -62,9 +62,18 @@ export default function App() {
|
|||||||
|
|
||||||
// Sichtbarkeit live anwenden bei Layer-Aenderungen. Zeichnungsebenen-Slice
|
// Sichtbarkeit live anwenden bei Layer-Aenderungen. Zeichnungsebenen-Slice
|
||||||
// bleibt leer — Backend mergt mit doc.Strings.
|
// bleibt leer — Backend mergt mit doc.Strings.
|
||||||
|
// Rekursiv durch Children — sonst feuert das useEffect nicht wenn nur die
|
||||||
|
// Visibility/Lock einer Sub-Ebene geaendert wurde.
|
||||||
|
const visKeyFor = (e) => {
|
||||||
|
const own = `${e.code}:${e.visible !== false ? 1 : 0}:${e.locked === true ? 1 : 0}`
|
||||||
|
const kids = Array.isArray(e.children) && e.children.length
|
||||||
|
? '(' + e.children.map(visKeyFor).join(',') + ')'
|
||||||
|
: ''
|
||||||
|
return own + kids
|
||||||
|
}
|
||||||
const visibilityKey = useMemo(() => (
|
const visibilityKey = useMemo(() => (
|
||||||
activeCode + '|' + eMode + '|' +
|
activeCode + '|' + eMode + '|' +
|
||||||
ebenen.map(e => `${e.code}:${e.visible !== false ? 1 : 0}:${e.locked === true ? 1 : 0}`).join(',')
|
ebenen.map(visKeyFor).join(',')
|
||||||
), [activeCode, eMode, ebenen])
|
), [activeCode, eMode, ebenen])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -73,17 +82,24 @@ export default function App() {
|
|||||||
}, [visibilityKey])
|
}, [visibilityKey])
|
||||||
|
|
||||||
// Auto-Apply bei strukturellen Aenderungen (name, fill) — wieder nur unsere
|
// Auto-Apply bei strukturellen Aenderungen (name, fill) — wieder nur unsere
|
||||||
// Slice, Backend mergt.
|
// Slice, Backend mergt. Rekursiv durch Children.
|
||||||
const fillSig = (e) => {
|
const fillSig = (e) => {
|
||||||
const f = e.fill
|
const f = e.fill
|
||||||
if (!f || !f.pattern || f.pattern === 'None') return ''
|
if (!f || !f.pattern || f.pattern === 'None') return ''
|
||||||
return [f.pattern, f.source || 'layer', f.color || '', f.scale ?? 1, f.rotation ?? 0].join('|')
|
return [f.pattern, f.source || 'layer', f.color || '', f.scale ?? 1, f.rotation ?? 0].join('|')
|
||||||
}
|
}
|
||||||
|
const structKeyFor = (e) => {
|
||||||
|
const own = `${e.code}:${e.name}:${fillSig(e)}`
|
||||||
|
const kids = Array.isArray(e.children) && e.children.length
|
||||||
|
? '(' + e.children.map(structKeyFor).join(',') + ')'
|
||||||
|
: ''
|
||||||
|
return own + kids
|
||||||
|
}
|
||||||
const structureKey = useMemo(() => (
|
const structureKey = useMemo(() => (
|
||||||
ebenen.map(e => `${e.code}:${e.name}:${fillSig(e)}`).join(',')
|
ebenen.map(structKeyFor).join(',')
|
||||||
), [ebenen])
|
), [ebenen])
|
||||||
const appliedStructureKey = useMemo(() => (
|
const appliedStructureKey = useMemo(() => (
|
||||||
appliedE.map(e => `${e.code}:${e.name}:${fillSig(e)}`).join(',')
|
appliedE.map(structKeyFor).join(',')
|
||||||
), [appliedE])
|
), [appliedE])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
+16
-1
@@ -57,6 +57,7 @@ export default function SwisstopoApp() {
|
|||||||
// Optionen
|
// Optionen
|
||||||
const [radius, setRadius] = useState(100)
|
const [radius, setRadius] = useState(100)
|
||||||
const [getBuild, setGetBuild] = useState(true)
|
const [getBuild, setGetBuild] = useState(true)
|
||||||
|
const [buildVariant, setBuildVariant] = useState('separated')
|
||||||
const [getTerrain, setGetTerrain] = useState(false)
|
const [getTerrain, setGetTerrain] = useState(false)
|
||||||
const [getOrtho, setGetOrtho] = useState(false)
|
const [getOrtho, setGetOrtho] = useState(false)
|
||||||
const [shift, setShift] = useState(true)
|
const [shift, setShift] = useState(true)
|
||||||
@@ -141,6 +142,7 @@ export default function SwisstopoApp() {
|
|||||||
replaceExisting,
|
replaceExisting,
|
||||||
clipToBbox,
|
clipToBbox,
|
||||||
terrainResolution: terrainRes,
|
terrainResolution: terrainRes,
|
||||||
|
buildVariant,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,9 +237,22 @@ export default function SwisstopoApp() {
|
|||||||
<label style={{ display: 'flex', alignItems: 'center', gap: 6,
|
<label style={{ display: 'flex', alignItems: 'center', gap: 6,
|
||||||
fontSize: 11, cursor: 'pointer' }}>
|
fontSize: 11, cursor: 'pointer' }}>
|
||||||
<input type="checkbox" checked={getBuild} onChange={(e) => setGetBuild(e.target.checked)} />
|
<input type="checkbox" checked={getBuild} onChange={(e) => setGetBuild(e.target.checked)} />
|
||||||
<Icon name="location_city" size={13} /> Bestand-Gebäude (swissBUILDINGS3D, DWG)
|
<Icon name="location_city" size={13} /> Bestand-Gebäude (swissBUILDINGS3D 3.0, DWG)
|
||||||
</label>
|
</label>
|
||||||
</Field>
|
</Field>
|
||||||
|
{getBuild && (
|
||||||
|
<Field label="GEBÄUDE-VARIANTE"
|
||||||
|
hint="Solid: ein geschlossenes Solid pro Gebäude (klein, schnell). Separated: Dach/Fassade/Wand als separate Objekte (mehr Detail, ermoeglicht z.B. Dach auszublenden).">
|
||||||
|
<Radio
|
||||||
|
value={buildVariant}
|
||||||
|
options={[
|
||||||
|
{ value: 'separated', label: 'Separated (Dach/Fassade getrennt)' },
|
||||||
|
{ value: 'solid', label: 'Solid (ein Volumen pro Gebäude)' },
|
||||||
|
]}
|
||||||
|
onChange={setBuildVariant}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
<Field label="">
|
<Field label="">
|
||||||
<label style={{ display: 'flex', alignItems: 'center', gap: 6,
|
<label style={{ display: 'flex', alignItems: 'center', gap: 6,
|
||||||
fontSize: 11, cursor: 'pointer' }}>
|
fontSize: 11, cursor: 'pointer' }}>
|
||||||
|
|||||||
@@ -145,7 +145,69 @@ function EditableText({ value, onCommit, style, fontWeight, fontSize, autoEditTr
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function EbeneRow({ e, active, mode, onClick, onContextMenu, onToggleVisible, onToggleLock, onColorChange, onLwChange, onNameChange, onCodeChange, onDelete, autoEditCode, autoEditName, rowRef }) {
|
// --- Tree-Helper -----------------------------------------------------------
|
||||||
|
// Rekursive Updates: code ist global eindeutig (Children duerfen keinen
|
||||||
|
// bestehenden Top-Level Code haben). Helper finden/aendern den passenden
|
||||||
|
// Eintrag irgendwo im Tree.
|
||||||
|
function _updateInTree(ebenen, code, patch) {
|
||||||
|
return ebenen.map(e => {
|
||||||
|
if (e.code === code) return { ...e, ...patch }
|
||||||
|
if (Array.isArray(e.children) && e.children.length) {
|
||||||
|
return { ...e, children: _updateInTree(e.children, code, patch) }
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function _removeFromTree(ebenen, code) {
|
||||||
|
const out = []
|
||||||
|
for (const e of ebenen) {
|
||||||
|
if (e.code === code) continue
|
||||||
|
if (Array.isArray(e.children) && e.children.length) {
|
||||||
|
out.push({ ...e, children: _removeFromTree(e.children, code) })
|
||||||
|
} else {
|
||||||
|
out.push(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
function _addChildInTree(ebenen, parentCode, child) {
|
||||||
|
return ebenen.map(e => {
|
||||||
|
if (e.code === parentCode) {
|
||||||
|
const kids = Array.isArray(e.children) ? e.children : []
|
||||||
|
return { ...e, children: [...kids, child] }
|
||||||
|
}
|
||||||
|
if (Array.isArray(e.children) && e.children.length) {
|
||||||
|
return { ...e, children: _addChildInTree(e.children, parentCode, child) }
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function _findInTree(ebenen, code) {
|
||||||
|
for (const e of ebenen) {
|
||||||
|
if (e.code === code) return e
|
||||||
|
if (Array.isArray(e.children) && e.children.length) {
|
||||||
|
const f = _findInTree(e.children, code)
|
||||||
|
if (f) return f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function _allCodes(ebenen) {
|
||||||
|
const out = []
|
||||||
|
for (const e of ebenen) {
|
||||||
|
out.push(e.code)
|
||||||
|
if (Array.isArray(e.children) && e.children.length) {
|
||||||
|
out.push(..._allCodes(e.children))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
function EbeneRow({ e, depth, hasChildren, expanded, onToggleExpand, active, mode, onClick, onContextMenu, onToggleVisible, onToggleLock, onColorChange, onLwChange, onNameChange, onCodeChange, onDelete, autoEditCode, autoEditName, rowRef }) {
|
||||||
// Auge zeigt den Eye-State (User-Intention) — auch fuer die aktive Ebene.
|
// Auge zeigt den Eye-State (User-Intention) — auch fuer die aktive Ebene.
|
||||||
// So sieht man auf einen Blick ob sie "normalerweise" sichtbar waere.
|
// So sieht man auf einen Blick ob sie "normalerweise" sichtbar waere.
|
||||||
// Aktive Ebene rendert Rhino zwar immer sichtbar, das visible-Flag bleibt
|
// Aktive Ebene rendert Rhino zwar immer sichtbar, das visible-Flag bleibt
|
||||||
@@ -160,6 +222,7 @@ function EbeneRow({ e, active, mode, onClick, onContextMenu, onToggleVisible, on
|
|||||||
style={{
|
style={{
|
||||||
display: 'flex', alignItems: 'center', gap: 5,
|
display: 'flex', alignItems: 'center', gap: 5,
|
||||||
padding: '3px 12px',
|
padding: '3px 12px',
|
||||||
|
paddingLeft: 12 + (depth || 0) * 14,
|
||||||
margin: active ? '1px 6px' : '0',
|
margin: active ? '1px 6px' : '0',
|
||||||
background: active ? 'var(--active-dim)'
|
background: active ? 'var(--active-dim)'
|
||||||
: (e.visible !== false) ? 'var(--bg-item)'
|
: (e.visible !== false) ? 'var(--bg-item)'
|
||||||
@@ -174,6 +237,16 @@ function EbeneRow({ e, active, mode, onClick, onContextMenu, onToggleVisible, on
|
|||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{hasChildren ? (
|
||||||
|
<button
|
||||||
|
className="btn-icon-xs"
|
||||||
|
onClick={(ev) => { ev.stopPropagation(); onToggleExpand() }}
|
||||||
|
title={expanded ? 'Einklappen' : 'Aufklappen'}
|
||||||
|
style={{ width: 14, height: 14 }}
|
||||||
|
><Icon name={expanded ? 'expand_more' : 'chevron_right'} size={12} /></button>
|
||||||
|
) : (
|
||||||
|
<span style={{ width: 14, flexShrink: 0 }} />
|
||||||
|
)}
|
||||||
{eyeShown ? (
|
{eyeShown ? (
|
||||||
<button
|
<button
|
||||||
className={`btn-icon-sm ${e.visible !== false ? 'is-on' : ''}`}
|
className={`btn-icon-sm ${e.visible !== false ? 'is-on' : ''}`}
|
||||||
@@ -262,6 +335,7 @@ export default function EbenenManager({
|
|||||||
const [ctxMenu, setCtxMenu] = useState(null) // { x, y, code }
|
const [ctxMenu, setCtxMenu] = useState(null) // { x, y, code }
|
||||||
const [clipboard, setClipboard] = useState(null) // { color, lw }
|
const [clipboard, setClipboard] = useState(null) // { color, lw }
|
||||||
const [autoEdit, setAutoEdit] = useState(null) // { code, field, token }
|
const [autoEdit, setAutoEdit] = useState(null) // { code, field, token }
|
||||||
|
const [expanded, setExpanded] = useState({}) // { code: true }
|
||||||
// Settings-Dialog laeuft jetzt in einem echten Rhino-Fenster (Satellite-
|
// Settings-Dialog laeuft jetzt in einem echten Rhino-Fenster (Satellite-
|
||||||
// Window via Eto.Form + WebView). State hier nicht mehr noetig.
|
// Window via Eto.Form + WebView). State hier nicht mehr noetig.
|
||||||
|
|
||||||
@@ -279,9 +353,9 @@ export default function EbenenManager({
|
|||||||
else { setSortBy(key); setSortDir('asc') }
|
else { setSortBy(key); setSortDir('asc') }
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortedEbenen = useMemo(() => {
|
const sortByCurrent = (arr) => {
|
||||||
const arr = [...ebenen]
|
const sorted = [...arr]
|
||||||
arr.sort((a, b) => {
|
sorted.sort((a, b) => {
|
||||||
let cmp = 0
|
let cmp = 0
|
||||||
if (sortBy === 'code') {
|
if (sortBy === 'code') {
|
||||||
const ca = parseInt(a.code, 10), cb = parseInt(b.code, 10)
|
const ca = parseInt(a.code, 10), cb = parseInt(b.code, 10)
|
||||||
@@ -293,19 +367,24 @@ export default function EbenenManager({
|
|||||||
}
|
}
|
||||||
return sortDir === 'desc' ? -cmp : cmp
|
return sortDir === 'desc' ? -cmp : cmp
|
||||||
})
|
})
|
||||||
return arr
|
return sorted
|
||||||
}, [ebenen, sortBy, sortDir])
|
}
|
||||||
|
|
||||||
|
// Sort wirkt innerhalb jeder Ebene des Baums — Children behalten ihre
|
||||||
|
// Beziehung zum Parent, werden aber unter sich sortiert.
|
||||||
|
const sortedEbenen = useMemo(() => sortByCurrent(ebenen),
|
||||||
|
[ebenen, sortBy, sortDir])
|
||||||
|
|
||||||
const updateByCode = (code, patch) => {
|
const updateByCode = (code, patch) => {
|
||||||
onChange(ebenen.map(e => e.code === code ? { ...e, ...patch } : e))
|
onChange(_updateInTree(ebenen, code, patch))
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleToggleVisible = (code) => {
|
const handleToggleVisible = (code) => {
|
||||||
const cur = ebenen.find(e => e.code === code)
|
const cur = _findInTree(ebenen, code)
|
||||||
if (cur) updateByCode(code, { visible: !(cur.visible !== false) })
|
if (cur) updateByCode(code, { visible: !(cur.visible !== false) })
|
||||||
}
|
}
|
||||||
const handleToggleLock = (code) => {
|
const handleToggleLock = (code) => {
|
||||||
const cur = ebenen.find(e => e.code === code)
|
const cur = _findInTree(ebenen, code)
|
||||||
if (cur) updateByCode(code, { locked: !cur.locked })
|
if (cur) updateByCode(code, { locked: !cur.locked })
|
||||||
}
|
}
|
||||||
const handleColorChange = (code, color) => {
|
const handleColorChange = (code, color) => {
|
||||||
@@ -324,8 +403,9 @@ export default function EbenenManager({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const handleCodeChange = (oldCode, newCode) => {
|
const handleCodeChange = (oldCode, newCode) => {
|
||||||
if (ebenen.some(e => e.code === newCode && e.code !== oldCode)) return
|
// Code muss global eindeutig sein (sonst gibt es mehrdeutige Layer-Matches)
|
||||||
onChange(ebenen.map(e => e.code === oldCode ? { ...e, code: newCode } : e))
|
if (_allCodes(ebenen).some(c => c === newCode && c !== oldCode)) return
|
||||||
|
onChange(_updateInTree(ebenen, oldCode, { code: newCode }))
|
||||||
// Phase weiterschalten: Code -> Name
|
// Phase weiterschalten: Code -> Name
|
||||||
if (autoEdit && autoEdit.code === oldCode && autoEdit.field === 'code') {
|
if (autoEdit && autoEdit.code === oldCode && autoEdit.field === 'code') {
|
||||||
setAutoEdit({ code: newCode, field: 'name', token: Date.now() })
|
setAutoEdit({ code: newCode, field: 'name', token: Date.now() })
|
||||||
@@ -339,25 +419,27 @@ export default function EbenenManager({
|
|||||||
const confirmDelete = (moveToCode) => {
|
const confirmDelete = (moveToCode) => {
|
||||||
const code = deleteTarget
|
const code = deleteTarget
|
||||||
deleteEbene(code, moveToCode)
|
deleteEbene(code, moveToCode)
|
||||||
onChange(ebenen.filter(e => e.code !== code))
|
onChange(_removeFromTree(ebenen, code))
|
||||||
if (activeCode === code) {
|
if (activeCode === code) {
|
||||||
const next = ebenen.find(e => e.code !== code)
|
const flat = ebenen.flatMap(e =>
|
||||||
|
[e, ...(Array.isArray(e.children) ? e.children : [])])
|
||||||
|
const next = flat.find(e => e.code !== code)
|
||||||
if (next) onActiveChange(next.code)
|
if (next) onActiveChange(next.code)
|
||||||
}
|
}
|
||||||
setDeleteTarget(null)
|
setDeleteTarget(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextFreeAfter = (afterCode) => {
|
const nextFreeAfter = (afterCode) => {
|
||||||
// Naechste freie Nummer NACH afterCode (= activeCode). Wenn afterCode
|
// Naechste freie Nummer NACH afterCode. Codes sind global eindeutig
|
||||||
// = "20", probiert "21", dann "22", etc. Fallback: max+1.
|
// (auch ueber Children) — also alle Codes als Konfliktraum.
|
||||||
const existing = new Set(ebenen.map(e => e.code))
|
const existing = new Set(_allCodes(ebenen))
|
||||||
let n = parseInt(afterCode, 10)
|
let n = parseInt(afterCode, 10)
|
||||||
if (isNaN(n)) n = 49
|
if (isNaN(n)) n = 49
|
||||||
for (let i = 1; i < 100; i++) {
|
for (let i = 1; i < 1000; i++) {
|
||||||
const c = String(n + i).padStart(2, '0')
|
const c = String(n + i).padStart(2, '0')
|
||||||
if (!existing.has(c)) return c
|
if (!existing.has(c)) return c
|
||||||
}
|
}
|
||||||
const codes = ebenen.map(e => parseInt(e.code, 10)).filter(x => !isNaN(x))
|
const codes = _allCodes(ebenen).map(c => parseInt(c, 10)).filter(x => !isNaN(x))
|
||||||
return String((codes.length ? Math.max(...codes) : 49) + 1).padStart(2, '0')
|
return String((codes.length ? Math.max(...codes) : 49) + 1).padStart(2, '0')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,11 +461,28 @@ export default function EbenenManager({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const duplicateEbene = (code) => {
|
const duplicateEbene = (code) => {
|
||||||
const src = ebenen.find(e => e.code === code)
|
const src = _findInTree(ebenen, code)
|
||||||
if (!src) return
|
if (!src) return
|
||||||
onChange([...ebenen, {
|
const dupCode = nextFreeAfter(code)
|
||||||
...src, code: nextFreeCode(), name: src.name + ' KOPIE',
|
const dup = { ...src, code: dupCode, name: src.name + ' KOPIE' }
|
||||||
}])
|
// Top-Level Eintrag — wir haengen Duplikat einfach hinten an
|
||||||
|
onChange([...ebenen, dup])
|
||||||
|
}
|
||||||
|
|
||||||
|
const addChild = (parentCode) => {
|
||||||
|
const code = nextFreeAfter(parentCode)
|
||||||
|
const child = {
|
||||||
|
code, name: 'NEU',
|
||||||
|
color: '#888888', lw: 0.18, visible: true, locked: false,
|
||||||
|
}
|
||||||
|
onChange(_addChildInTree(ebenen, parentCode, child))
|
||||||
|
// Parent expanden damit der neue Eintrag sichtbar ist
|
||||||
|
setExpanded(s => ({ ...s, [parentCode]: true }))
|
||||||
|
setAutoEdit({ code, field: 'code', token: Date.now() })
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleExpand = (code) => {
|
||||||
|
setExpanded(s => ({ ...s, [code]: !s[code] }))
|
||||||
}
|
}
|
||||||
|
|
||||||
const copyProps = (code) => {
|
const copyProps = (code) => {
|
||||||
@@ -405,10 +504,11 @@ export default function EbenenManager({
|
|||||||
|
|
||||||
const ctxItems = (code) => [
|
const ctxItems = (code) => [
|
||||||
{ label: 'Ebeneneinstellungen…', icon: 'settings', onClick: () => {
|
{ label: 'Ebeneneinstellungen…', icon: 'settings', onClick: () => {
|
||||||
const target = ebenen.find(e => e.code === code)
|
const target = _findInTree(ebenen, code)
|
||||||
if (target) openEbenenSettings(target, hatchPatterns)
|
if (target) openEbenenSettings(target, hatchPatterns)
|
||||||
} },
|
} },
|
||||||
{ divider: true },
|
{ divider: true },
|
||||||
|
{ label: 'Sub-Ebene hinzufügen…', icon: 'add', onClick: () => addChild(code) },
|
||||||
{ label: 'Selektion hierher übertragen', icon: 'move_down', onClick: () => moveSelectionToEbene(code) },
|
{ label: 'Selektion hierher übertragen', icon: 'move_down', onClick: () => moveSelectionToEbene(code) },
|
||||||
{ divider: true },
|
{ divider: true },
|
||||||
{ label: 'Duplizieren', icon: 'content_copy', onClick: () => duplicateEbene(code) },
|
{ label: 'Duplizieren', icon: 'content_copy', onClick: () => duplicateEbene(code) },
|
||||||
@@ -490,25 +590,44 @@ export default function EbenenManager({
|
|||||||
<div style={{ width: 18 }} />
|
<div style={{ width: 18 }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{sortedEbenen.map(e => (
|
{(() => {
|
||||||
<EbeneRow
|
// Rekursives Rendern: jede Ebene + sortierte Children (falls expanded)
|
||||||
key={e.code}
|
const renderRow = (e, depth) => {
|
||||||
e={e}
|
const kids = Array.isArray(e.children) ? e.children : []
|
||||||
active={e.code === activeCode}
|
const hasChildren = kids.length > 0
|
||||||
mode={mode}
|
const isExpanded = !!expanded[e.code]
|
||||||
onClick={() => onActiveChange(e.code)}
|
const rows = [
|
||||||
onContextMenu={(ev) => openContextMenu(ev, e.code)}
|
<EbeneRow
|
||||||
onToggleVisible={() => handleToggleVisible(e.code)}
|
key={e.code}
|
||||||
onToggleLock={() => handleToggleLock(e.code)}
|
e={e}
|
||||||
onColorChange={(c) => handleColorChange(e.code, c)}
|
depth={depth}
|
||||||
onLwChange={(lw) => handleLwChange(e.code, lw)}
|
hasChildren={hasChildren}
|
||||||
onNameChange={(n) => handleNameChange(e.code, n)}
|
expanded={isExpanded}
|
||||||
onCodeChange={(c) => handleCodeChange(e.code, c)}
|
onToggleExpand={() => toggleExpand(e.code)}
|
||||||
onDelete={() => handleDelete(e.code)}
|
active={e.code === activeCode}
|
||||||
autoEditCode={autoEdit && autoEdit.code === e.code && autoEdit.field === 'code' ? autoEdit.token : null}
|
mode={mode}
|
||||||
autoEditName={autoEdit && autoEdit.code === e.code && autoEdit.field === 'name' ? autoEdit.token : null}
|
onClick={() => onActiveChange(e.code)}
|
||||||
/>
|
onContextMenu={(ev) => openContextMenu(ev, e.code)}
|
||||||
))}
|
onToggleVisible={() => handleToggleVisible(e.code)}
|
||||||
|
onToggleLock={() => handleToggleLock(e.code)}
|
||||||
|
onColorChange={(c) => handleColorChange(e.code, c)}
|
||||||
|
onLwChange={(lw) => handleLwChange(e.code, lw)}
|
||||||
|
onNameChange={(n) => handleNameChange(e.code, n)}
|
||||||
|
onCodeChange={(c) => handleCodeChange(e.code, c)}
|
||||||
|
onDelete={() => handleDelete(e.code)}
|
||||||
|
autoEditCode={autoEdit && autoEdit.code === e.code && autoEdit.field === 'code' ? autoEdit.token : null}
|
||||||
|
autoEditName={autoEdit && autoEdit.code === e.code && autoEdit.field === 'name' ? autoEdit.token : null}
|
||||||
|
/>
|
||||||
|
]
|
||||||
|
if (hasChildren && isExpanded) {
|
||||||
|
for (const child of sortByCurrent(kids)) {
|
||||||
|
rows.push(...renderRow(child, depth + 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
return sortedEbenen.flatMap(e => renderRow(e, 0))
|
||||||
|
})()}
|
||||||
|
|
||||||
{ctxMenu && (
|
{ctxMenu && (
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
|
|||||||
@@ -245,6 +245,7 @@ export default function GeschossManager({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{/* Master-Row: Master-Eye links + Master-Lock rechts (analog
|
{/* Master-Row: Master-Eye links + Master-Lock rechts (analog
|
||||||
EbenenManager). */}
|
EbenenManager). */}
|
||||||
<div style={{
|
<div style={{
|
||||||
|
|||||||
@@ -146,6 +146,20 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div style={{ height: 1, background: 'var(--border-light)', margin: '6px 0' }} />
|
||||||
|
|
||||||
|
<Field
|
||||||
|
label="0-KOTE m.ü.M (PROJEKTWEIT)"
|
||||||
|
hint="Höhe ü. Meer am OKFF=0. Wird beim Swisstopo-Import als Z-Offset benutzt — alle Real-Welt-Höhen werden um diesen Wert runtergeschoben. Gilt projektweit (nicht nur dieses Geschoss).">
|
||||||
|
<input
|
||||||
|
type="number" step="0.01"
|
||||||
|
value={draft.projectZeroMum ?? 0}
|
||||||
|
onChange={(ev) => set({ projectZeroMum: parseFloat(ev.target.value) || 0 })}
|
||||||
|
style={{ flex: 1, fontSize: 11, textAlign: 'right', minWidth: 0,
|
||||||
|
fontFamily: 'var(--font-mono)' }}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
|
|||||||
+14
-3
@@ -318,9 +318,20 @@ export function applyVisibility(activeZ, zeichnungsebenen, activeCode, ebenen, z
|
|||||||
visible: z.visible !== false,
|
visible: z.visible !== false,
|
||||||
locked: z.locked === true,
|
locked: z.locked === true,
|
||||||
}))
|
}))
|
||||||
const slimE = eList.map(e => ({
|
// Rekursiv durch Children — sonst landen Sub-Ebenen-Toggles nicht beim
|
||||||
code: e.code, visible: e.visible !== false, locked: e.locked === true,
|
// Backend.
|
||||||
}))
|
const slimEbene = (e) => {
|
||||||
|
const out = {
|
||||||
|
code: e.code,
|
||||||
|
visible: e.visible !== false,
|
||||||
|
locked: e.locked === true,
|
||||||
|
}
|
||||||
|
if (Array.isArray(e.children) && e.children.length) {
|
||||||
|
out.children = e.children.map(slimEbene)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
const slimE = eList.map(slimEbene)
|
||||||
send('SET_VISIBILITY', {
|
send('SET_VISIBILITY', {
|
||||||
activeZ: a.activeZ ? { id: a.activeZ.id } : null,
|
activeZ: a.activeZ ? { id: a.activeZ.id } : null,
|
||||||
activeCode: a.activeCode,
|
activeCode: a.activeCode,
|
||||||
|
|||||||
Reference in New Issue
Block a user