Snapshot: Transform-Hierarchie + Brüstung-Konvention + Undo-Record

Funktionierender Stand der Move/Rotate-Pipeline mit Eltern-Kind-Cascade
und sauberer Brüstung-Semantik:

- Pure-Translate hierarchisch: nur Sources mit echtem Delta + ihre Kinder
  (Öffnungen → Wand) folgen mit. Wand folgt NICHT der Öffnung.
- Orphan-Detection: Öffnung ohne mitbewegter Eltern-Wand → Regen-Fallback
  (sonst bleibt Cutout am alten Ort im Wand-Brep).
- Brüstung = relativ zur Wand-UK (Archicad/Revit-Konvention). Bei Wand-
  Z-Drag wird UK_OVER angepasst, Brüstung bleibt; Öffnungs-Punkt wandert
  via Snapshot+Delta mit. Keine Doppel-Addition mehr.
- Opening-Punkt wird beim Erzeugen direkt auf UK+brüstung platziert
  (sonst Brüstung-Drop beim ersten Move).
- Undo-Record umschliesst Rhinos Move + unseren Regen in einem Cmd+Z-
  Schritt → keine doppelten Elemente nach Undo.
- RedrawEnabled-Suppression event-getriggert (erst beim ersten Replace-
  Event nach User-Klick) → Rubber-Band + Drag-Vorschau bleiben sichtbar.
- _Undo/_Redo: Event-Handler komplett aussetzen → kein Regen-Storm.
- Gestaltung-Listener während User-Transform + Regen stumm, danach
  einmaliger Selection-Refresh.

Enthält Debug-Logs in _apply_wand_z_drag_constraint + Wand-Regen
für offenen Bug: bei gemeinsamer Z-Verschiebung (Wand+Fenster+Tür)
landen Öffnungen manchmal über der Wand — UK_OVER scheint nicht
durchzukommen. Logs sollen das eingrenzen.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 22:20:35 +02:00
parent d3984ba501
commit 2a75b1da93
2 changed files with 309 additions and 72 deletions
+38 -19
View File
@@ -1368,6 +1368,13 @@ def _install_selection_listener(bridge):
return
def refresh(*args):
# Waehrend Move/Rotate/Mirror/Scale schweigen — Rhino oszilliert die
# Selection pro transformiertem Object mehrfach (deselect→delete→add→
# reselect). Bei 7 Objekten sind das ~100 IPC-Sends in den WebView,
# was sich als „Regen" anfuehlt. elemente._on_command_end refresht
# nach dem Command einmalig.
if sc.sticky.get("_dossier_user_transform_active"): return
if sc.sticky.get("_dossier_undo_active"): return
b = sc.sticky.get("gestaltung_bridge")
if b is not None:
try: b._send_selection()
@@ -1380,6 +1387,12 @@ def _install_selection_listener(bridge):
- Hatch hat _FILL_OWNER_KEY (= curve_id) → Curve um den gleichen
Vektor mit-translaten (User hat Hatch alleine verschoben).
"""
# Waehrend User-Transform-Command: elemente uebernimmt die Geometrie-
# Synchronisation. Hatch-Re-Create laeuft hier sowieso ins Leere weil
# Rhino bei Move Delete+Add statt Replace feuert.
if sc.sticky.get("_dossier_user_transform_active"): return
if sc.sticky.get("_dossier_undo_active"): return
if sc.sticky.get("_elemente_regen_busy"): return
new_obj = args.NewRhinoObject
if new_obj is None or new_obj.Id in _processing:
return
@@ -1457,11 +1470,13 @@ def _install_selection_listener(bridge):
"""Wenn eine Curve geloescht wird, ihre gekoppelte Hatch mitloeschen.
Wenn umgekehrt eine Hatch direkt geloescht wird, den Verweis auf der
Curve aufraeumen damit beim naechsten Toggle keine Geister-Referenz steht."""
# Waehrend User-Transform-Command: Rhino feuert Delete+Add fuer
# transformierte Objekte. Curve→Hatch-Cascade hier wuerde die Hatch
# killen obwohl sie gleich wieder benoetigt wird.
if sc.sticky.get("_dossier_user_transform_active"): return
if sc.sticky.get("_dossier_undo_active"): return
if sc.sticky.get("_elemente_regen_busy"): return
obj = args.TheObject
try:
print("[GESTALTUNG] on_delete fired id={}".format(obj.Id if obj else None))
except Exception:
pass
if obj is None or obj.Id in _processing:
return
doc = Rhino.RhinoDoc.ActiveDoc
@@ -1470,16 +1485,24 @@ def _install_selection_listener(bridge):
except Exception:
return
# Pfad A: geloeschte Curve hatte eine Hatch -> Hatch mitloeschen
# Schneller Bail-out: ohne Hatch-UserString interessiert uns das
# Event nicht. Vermeidet Print-Spam fuer Wand-Sub-Volumen etc.
try:
hatch_id_str = attrs.GetUserString(_FILL_KEY)
except Exception:
hatch_id_str = None
# Fallback: Mapping in sc.sticky (UserStrings koennen nach Delete leer sein)
if not hatch_id_str:
try:
owner_id_str = attrs.GetUserString(_FILL_OWNER_KEY)
except Exception:
owner_id_str = None
if not hatch_id_str and not owner_id_str:
# UserStrings koennen nach Delete leer sein → Sticky-Fallback.
hatch_id_str = _lookup_hatch_for_curve(obj.Id)
if hatch_id_str:
print("[GESTALTUNG] on_delete: hatch via sticky map gefunden")
if not hatch_id_str:
return
print("[GESTALTUNG] on_delete: hatch via sticky map gefunden")
# Pfad A: geloeschte Curve hatte eine Hatch -> Hatch mitloeschen
if hatch_id_str:
try:
hatch_id = System.Guid(hatch_id_str)
@@ -1504,10 +1527,6 @@ def _install_selection_listener(bridge):
return # Curve-Fall fertig
# Pfad B: geloeschte Hatch hatte einen Owner-Verweis -> Curve aufraeumen
try:
owner_id_str = attrs.GetUserString(_FILL_OWNER_KEY)
except Exception:
owner_id_str = None
if owner_id_str:
try:
owner_id = System.Guid(owner_id_str)
@@ -1532,16 +1551,17 @@ def _install_selection_listener(bridge):
- Wenn das Objekt eben gerade als Teil eines Drag/Move geloescht wurde,
stellen wir die Hatch mit den gemerkten Metadaten wieder her.
- Sonst pruefen wir ob die Ebene ein Auto-Fill konfiguriert hat."""
# Waehrend User-Transform-Command: elemente uebernimmt Geometrie-Sync.
# Auto-Fill hier wuerde unnoetige Hatches erzeugen weil das Objekt
# bereits eine geerbte Fill-UserString hat (vom Delete+Add im Move).
if sc.sticky.get("_dossier_user_transform_active"): return
if sc.sticky.get("_dossier_undo_active"): return
if sc.sticky.get("_elemente_regen_busy"): return
obj = args.TheObject
if obj is None:
return
try:
geom_kind = type(obj.Geometry).__name__
except Exception:
geom_kind = "?"
if obj.Id in _processing:
return
print("[GESTALTUNG] on_add: id={} type={}".format(obj.Id, geom_kind))
doc = Rhino.RhinoDoc.ActiveDoc
# 1) Drag-Recovery: Hatch-Metadaten wurden gerade in on_delete gespeichert?
@@ -1566,7 +1586,6 @@ def _install_selection_listener(bridge):
except Exception as ex:
print("[GESTALTUNG] on_add Exception:", ex)
return
print("[GESTALTUNG] on_add ok={}".format(ok))
if ok:
b = sc.sticky.get("gestaltung_bridge")
if b is not None: