Schnitt/Ansicht-Feature + Terrain-Volumen + Geschoss-Add-Dialog

Schnitt-Feature V1+V2:
- Neues rhino/schnitte.py mit Pick-Workflow, Activation (Clipping-Planes +
  Parallel-View), 2D-Plan-Symbol auf 18_Schnittlinien-Sublayer
- Doppelklick auf Symbol aktiviert den Schnitt
- Schnitt-Settings (cutAtLine/Tiefe/Höhen/Blickrichtung) im GeschossSettingsDialog
- View-Snapshot + Restore beim Wechsel Schnitt → Geschoss
- Symbol-Cleanup bei Delete via normalem Ebenen-Menü

Terrain als Volumen:
- swisstopo.volumize_terrain_object: Skirt + Bottom-Cap auf Mesh/Brep
  damit Clipping-Planes gefuellte Querschnitte erzeugen
- UI im SwisstopoApp mit Nachbearbeitung-Section + Tiefen-Eingabe

Geschoss-Add mit Dialog:
- + im GeschossManager oeffnet 3-Optionen-Picker (Geschoss/Schnitt/Zeichnung)
- Geschoss-Dialog mit Anker-Dropdown, Position über/unter, Auto-Name,
  Höhen-Prefill aus Anker

Fix: _send_state fallback — Element gilt als selektiert wenn Source ODER
Volume in der Selection ist (robust gegen Layer-Visibility wenn Referenz-
linien-Layer im aktuellen Mode versteckt ist)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 18:28:59 +02:00
parent 3277f61ced
commit 059cbf8d4d
8 changed files with 1356 additions and 22 deletions
+132
View File
@@ -433,6 +433,40 @@ class EbenenBridge(panel_base.BaseBridge):
self._update_ebene_field(p["code"], "lw", p["lw"])
elif t == "SET_ACTIVE":
self._set_active_zeichnungsebene(p)
elif t == "CREATE_SCHNITT":
# Interaktiver Pick: 2 Punkte fuer Schnittlinie + Klick fuer
# Blickrichtung. Defaults aus payload (vom UI vorbelegt).
try:
import schnitte
sid = schnitte.pick_schnitt_interactive(doc, defaults={
"depthBack": float(p.get("depthBack", 8.0)),
"heightMin": float(p.get("heightMin", -1.0)),
"heightMax": float(p.get("heightMax", 12.0)),
"cutAtLine": bool(p.get("cutAtLine", True)),
"namePrefix": p.get("namePrefix", "S"),
})
if sid:
_broadcast_state(doc)
# Auto-aktivieren nach Erstellung
try:
zraw = doc.Strings.GetValue("dossier_zeichnungsebenen")
z_list = json.loads(zraw) if zraw else []
new_z = next((x for x in z_list
if isinstance(x, dict) and x.get("id") == sid),
None)
if new_z is not None:
self._set_active_zeichnungsebene(new_z)
except Exception as ex:
print("[SCHNITT] auto-activate:", ex)
except Exception as ex:
print("[SCHNITT] CREATE_SCHNITT:", ex)
elif t == "DELETE_SCHNITT":
try:
import schnitte
if schnitte.delete_schnitt_entry(doc, p.get("id") or ""):
_broadcast_state(doc)
except Exception as ex:
print("[SCHNITT] DELETE_SCHNITT:", ex)
elif t == "SET_ACTIVE_LAYER":
code = p.get("code", "")
if code:
@@ -551,6 +585,18 @@ class EbenenBridge(panel_base.BaseBridge):
try: e_list = json.loads(e_raw) if e_raw else []
except Exception: e_list = []
self._apply(z_list, e_list, save_z=True, save_e=False)
# Schnitt-Refresh: wenn der geaenderte Eintrag ein Schnitt ist
# UND aktuell aktiv ist, Clipping-Planes + View neu aufbauen
# damit die neuen Werte (depthBack, heightRange, cutAtLine etc.)
# sofort wirken.
try:
if updated.get("type") == "schnitt":
active_id = doc.Strings.GetValue("dossier_active_id") or ""
if active_id == updated.get("id"):
import schnitte
schnitte.activate_schnitt(doc, updated)
except Exception as ex:
print("[SCHNITT] post-save reactivate:", ex)
panel_base.open_satellite_window(
"geschoss_settings",
params=params,
@@ -709,6 +755,22 @@ class EbenenBridge(panel_base.BaseBridge):
new_sig = _fill_signature(ebenen)
fill_changed = (old_sig != new_sig)
# Schnitt-Cleanup-Detection: alt vs neu Schnitt-Ids vergleichen.
# Wenn ein Schnitt entfernt wurde (via normalem Delete-Menue), die
# 2D-Plan-Symbole + ggf. Clipping-Planes aufraeumen. Sonst bleiben
# Waisen im Doc.
schnitte_removed = set()
if save_z:
try:
import schnitte as _schn
old_z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
old_z = json.loads(old_z_raw) if old_z_raw else []
old_ids = _schn.schnitt_ids_in_list(old_z)
new_ids = _schn.schnitt_ids_in_list(zeichnungsebenen)
schnitte_removed = old_ids - new_ids
except Exception as ex:
print("[SCHNITT] cleanup detection:", ex)
_set_processing(True)
try:
print("[EBENEN] _apply: build_layers ...")
@@ -724,6 +786,21 @@ class EbenenBridge(panel_base.BaseBridge):
doc.Strings.SetString("dossier_zeichnungsebenen", z_json)
if save_e:
doc.Strings.SetString("dossier_ebenen", e_json)
# Cleanup geloeschter Schnitte: 2D-Symbole + ggf. Clipping-Planes.
# Muss NACH dem SetString passieren damit dossier_active_id-Check
# in cleanup_schnitt_artifacts den korrekten Stand sieht.
if schnitte_removed:
try:
import schnitte as _schn
active_id = doc.Strings.GetValue("dossier_active_id") or ""
n_total = 0
for sid in schnitte_removed:
n_total += _schn.cleanup_schnitt_artifacts(
doc, sid, active_id=active_id)
print("[SCHNITT] {} Schnitt(e) geloescht, {} Symbol-Curves entfernt".format(
len(schnitte_removed), n_total))
except Exception as ex:
print("[SCHNITT] artifact cleanup:", ex)
# Smart-Elemente (Waende) regenerieren — Geschoss-Hoehen/OKFF
# haben sich evtl. geaendert, gebundene Waende muessen neu
# extrudiert werden. Best-effort, faengt jeden Fehler ab.
@@ -943,6 +1020,61 @@ class EbenenBridge(panel_base.BaseBridge):
# Vorigen Stand merken um redundante teure Operationen zu sparen
prev_active_id = doc.Strings.GetValue("dossier_active_id") or ""
doc.Strings.SetString("dossier_active_id", z_id)
# Schnitt-Typ: Spezial-Pfad. Vertikale Clipping-Planes + Parallel-
# View statt der ueblichen horizontalen Geschoss-Clipping-Logik.
# Den vollen Record aus doc.Strings holen (z-Payload aus React ist
# minimal, hat type/linePts/etc nicht zwingend dabei).
z_full = z
try:
zraw = doc.Strings.GetValue("dossier_zeichnungsebenen")
if zraw:
for cand in json.loads(zraw):
if isinstance(cand, dict) and cand.get("id") == z_id:
z_full = cand; break
except Exception: pass
# Vorheriger Eintrag ein Schnitt? Brauchen wir fuer View-Snapshot-
# Logik: Geschoss → Schnitt snapshot, Schnitt → Geschoss restore.
prev_was_schnitt = False
try:
import schnitte as _schn_check
prev_was_schnitt = _schn_check.is_schnitt_id(doc, prev_active_id)
except Exception: pass
if isinstance(z_full, dict) and z_full.get("type") == "schnitt":
try:
import schnitte
# Pre-Schnitt-View snapshotten — aber NUR beim Wechsel von
# einem Nicht-Schnitt. Schnitt→Schnitt-Wechsel soll den
# urspruenglichen Plan-View nicht ueberschreiben.
if not prev_was_schnitt:
schnitte.save_pre_schnitt_view(doc)
# Horizontale Geschoss-Clipping aufraeumen falls aktiv —
# die existiert parallel zur Schnitt-Clipping und wuerde
# die Sicht doppelt schneiden.
try:
existing_geschoss = layer_builder._find_clipping_plane(doc)
if existing_geschoss is not None:
doc.Objects.Delete(existing_geschoss.Id, True)
except Exception: pass
schnitte.activate_schnitt(doc, z_full)
_broadcast_state(doc)
# Elemente-Panel auch informieren
try:
eb = sc.sticky.get("elemente_bridge")
if eb is not None: eb._notify_active_geschoss()
except Exception: pass
except Exception as ex:
print("[SCHNITT] activate fehler:", ex)
return
# Geschoss-Pfad (default): falls vorher ein Schnitt aktiv war,
# dessen Clipping-Planes aufraeumen + Pre-Schnitt-View restoren.
try:
import schnitte
schnitte.clear_schnitt_clipping(doc)
if prev_was_schnitt:
schnitte.restore_pre_schnitt_view(doc)
except Exception as ex:
print("[SCHNITT] cleanup beim Wechsel auf Geschoss:", ex)
# Aktiven Sublayer auf die GLEICHE Ebene unter dem neuen Geschoss
# umschalten — wenn User auf "20 Wände" steht und das Geschoss
# wechselt, soll Rhino's aktiver Layer "1OG::20_Wände" werden statt