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:
2026-05-19 23:21:45 +02:00
parent 4111f12f32
commit afb59b6626
10 changed files with 1103 additions and 326 deletions
+83 -21
View File
@@ -87,9 +87,15 @@ def _broadcast_state(doc=None, hatch_patterns=None):
try:
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
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 = {
"zeichnungsebenen": json.loads(z_raw) if z_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
else _hatch_pattern_names(doc),
}
@@ -367,9 +373,13 @@ class EbenenBridge(panel_base.BaseBridge):
layer_builder.build_layers(doc, z, e)
layer_builder.cleanup_default_layers(doc)
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", {
"zeichnungsebenen": z,
"ebenen": e,
"projectZeroMum": zero_mum,
"hatchPatterns": _hatch_pattern_names(doc),
})
except Exception as ex:
@@ -471,9 +481,28 @@ class EbenenBridge(panel_base.BaseBridge):
print("[EBENEN] open_geschoss_settings: kein Geschoss-Payload")
return
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):
doc = Rhino.RhinoDoc.ActiveDoc
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")
if not z_raw:
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)
panel_base.open_satellite_window(
"geschoss_settings",
params=geschoss,
params=params,
title="Zeichnungsebene: {}".format(geschoss.get("name", "")),
size=(380, 540),
size=(380, 580),
on_save=on_save)
def _open_ebenen_settings(self, ebene, hatch_patterns):
@@ -733,22 +762,36 @@ class EbenenBridge(panel_base.BaseBridge):
return
payload_z = p.get("zeichnungsebenen") 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
# die noch nicht in doc.Strings sind (= User hat gerade neue Ebene
# angelegt aber der strukturelle APPLY ist noch in der 200ms-Debounce),
# NICHT speichern. Sonst ueberschreibt die schnellere SET_VISIBILITY
# den geplanten APPLY-Save und die neue Ebene geht in der Race
# verloren.
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)}
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)}
payload_z_ids = {z.get("id") for z in payload_z if isinstance(z, 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_e_codes = set(_walk_codes(e_full).keys())
has_new_structural = (
bool(payload_z_ids - existing_z_ids - {None}) or
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")}
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 = []
for z in z_full:
if not isinstance(z, dict): continue
@@ -758,23 +801,40 @@ class EbenenBridge(panel_base.BaseBridge):
m["visible"] = s.get("visible", True)
m["locked"] = s.get("locked", False)
merged_z.append(m)
merged_e = []
for e in e_full:
if not isinstance(e, dict): continue
m = dict(e)
s = e_state.get(e.get("code"))
if s is not None:
m["visible"] = s.get("visible", True)
m["locked"] = s.get("locked", False)
merged_e.append(m)
# Merge fuer Ebenen rekursiv: jedes Element behaelt seine Position +
# children-Struktur, nur visible/locked werden ueberschrieben falls
# im Payload anwesend.
def _merge_ebenen_tree(orig_list):
out = []
for e in orig_list:
if not isinstance(e, dict): continue
m = dict(e)
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.
# Wenn nicht: das ist nur der Echo-Roundtrip eines apply_layer_preset
# (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):
old_by = {x.get("id") or x.get("code"): x for x in old if isinstance(x, dict)}
for nx in new:
if not isinstance(nx, dict): continue
old_by = {x.get("id") or x.get("code"): x for x in _flatten(old)}
for nx in _flatten(new):
key = nx.get("id") or nx.get("code")
if key is None: continue
ox = old_by.get(key)
@@ -815,10 +875,12 @@ class EbenenBridge(panel_base.BaseBridge):
bool(z.get("visible", True)),
bool(z.get("locked", False)))
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"),
bool(e.get("visible", True)),
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)
cur_sig = _sig(merged_z, merged_e)
if sc.sticky.get("_vis_last_sig") == cur_sig and not any_changed: