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:
+265
-47
@@ -3922,6 +3922,8 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
|
||||
if meta["type"] == "wand_axis":
|
||||
uk, ok = _resolve_uk_ok(doc, meta["geschoss"],
|
||||
meta["uk_override"], meta["ok_override"])
|
||||
print("[ELEMENTE] regen wand {}: uk={:.3f} ok={:.3f} (uk_over='{}' ok_over='{}')".format(
|
||||
element_id, uk, ok, meta.get("uk_override", ""), meta.get("ok_override", "")))
|
||||
# Wand-Verbindungen: Miter-Linien aus Nachbarwand-Joints (Corner + T).
|
||||
miter_start = None
|
||||
miter_end = None
|
||||
@@ -5580,7 +5582,19 @@ class ElementeBridge(panel_base.BaseBridge):
|
||||
oeff_sims_in=simsi_def,
|
||||
oeff_glas=glas_def,
|
||||
oeff_referenz=referenz_def)
|
||||
new_id = doc.Objects.AddPoint(on_axis, attrs)
|
||||
# Oeffnungs-Punkt auf UK+Brueestung-Hoehe platzieren (= visuell auf
|
||||
# Unterkante Oeffnung). Constraint vergleicht spaeter pt.Z mit
|
||||
# UK+brueest — wenn der Punkt am axis.Z=0 saesse, wuerde der erste
|
||||
# Move die Brueest auf 0 droppen. Hier UK auflösen (Geschoss-OKFF +
|
||||
# ggf. Override) und Punkt direkt auf richtige Welt-Z setzen.
|
||||
try:
|
||||
wall_uk, _ = _resolve_uk_ok(doc, geschoss,
|
||||
wall_meta.get("uk_override", ""),
|
||||
wall_meta.get("ok_override", ""))
|
||||
except Exception:
|
||||
wall_uk = 0.0
|
||||
pt_at_brueest = rg.Point3d(on_axis.X, on_axis.Y, wall_uk + float(brueest))
|
||||
new_id = doc.Objects.AddPoint(pt_at_brueest, attrs)
|
||||
if new_id == System.Guid.Empty:
|
||||
print("[ELEMENTE] AddPoint fehlgeschlagen"); return
|
||||
|
||||
@@ -6736,10 +6750,21 @@ def _apply_wand_z_drag_constraint(new_obj, meta):
|
||||
meta["uk_override"], meta["ok_override"])
|
||||
new_uk = uk_cur + delta
|
||||
new_ok = ok_cur + delta
|
||||
print("[ELEMENTE] wand z-drag: uk_cur={:.3f} ok_cur={:.3f} new_uk={:.3f} new_ok={:.3f} (meta uk_over='{}' ok_over='{}')".format(
|
||||
uk_cur, ok_cur, new_uk, new_ok, meta.get("uk_override", ""), meta.get("ok_override", "")))
|
||||
attrs = new_obj.Attributes.Duplicate()
|
||||
attrs.SetUserString(_KEY_UK_OVER, "{:.6f}".format(new_uk))
|
||||
attrs.SetUserString(_KEY_OK_OVER, "{:.6f}".format(new_ok))
|
||||
doc.Objects.ModifyAttributes(new_obj.Id, attrs, True)
|
||||
mod_ok = doc.Objects.ModifyAttributes(new_obj.Id, attrs, True)
|
||||
# Verifikation: UK_OVER wirklich in Doc geschrieben?
|
||||
verify = doc.Objects.FindId(new_obj.Id)
|
||||
if verify is not None:
|
||||
actual_uk = verify.Attributes.GetUserString(_KEY_UK_OVER) or "<empty>"
|
||||
actual_ok = verify.Attributes.GetUserString(_KEY_OK_OVER) or "<empty>"
|
||||
print("[ELEMENTE] wand z-drag ModifyAttributes returned={} → stored uk_over='{}' ok_over='{}'".format(
|
||||
mod_ok, actual_uk, actual_ok))
|
||||
else:
|
||||
print("[ELEMENTE] wand z-drag verify: FindId returned None!")
|
||||
# Curve auf Z=0 fixen. LineCurve: explizit beide Endpunkte (auch bei
|
||||
# einzelnem End-Grip-Drag). Andere Curves: ueber Translation (akzeptiert
|
||||
# leichten Schraeg bei End-Grip-Drag, gleicht sich beim naechsten
|
||||
@@ -6801,6 +6826,7 @@ def _apply_oeffnung_constraint(new_obj, meta, old_obj=None):
|
||||
|
||||
parent_id = meta.get("oeff_parent")
|
||||
parent_curve = None
|
||||
parent_meta = None
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
if doc is not None and parent_id:
|
||||
for obj in doc.Objects:
|
||||
@@ -6809,6 +6835,7 @@ def _apply_oeffnung_constraint(new_obj, meta, old_obj=None):
|
||||
cg = obj.Geometry
|
||||
if isinstance(cg, rg.Curve):
|
||||
parent_curve = cg
|
||||
parent_meta = m
|
||||
break
|
||||
|
||||
target_x, target_y = pt_new.X, pt_new.Y
|
||||
@@ -6850,17 +6877,33 @@ def _apply_oeffnung_constraint(new_obj, meta, old_obj=None):
|
||||
cur_bruest_val = float(cur_bruest) if cur_bruest not in (None, "") else 0.9
|
||||
except (ValueError, TypeError):
|
||||
cur_bruest_val = 0.9
|
||||
# Z-Delta = Drag in Z. Der Punkt SITZT visuell auf Bruestung-Hoehe
|
||||
# (siehe Geometry-Schreibung unten), daher pt_old.Z ~= alte Bruestung.
|
||||
delta_z = (pt_new.Z - pt_old.Z) if pt_old is not None else (pt_new.Z - cur_bruest_val)
|
||||
# Z-Delta gegen den ERWARTETEN Welt-Z des Punktes = Wand-UK + Brueest.
|
||||
# Bruestung ist relativ zur Wand-UK gespeichert. Wenn die Wand
|
||||
# hochgezogen wurde (UK_OVER += z_delta) und der Wand-Loop den
|
||||
# Oeffnungs-Punkt um z_delta translatet hat, sitzt der Punkt jetzt auf
|
||||
# `new_UK + cur_brueest` = `expected_pt_z`. delta_z = 0 → kein
|
||||
# Bruestungs-Update (gut so, sonst doppelt). Wenn der User nur den
|
||||
# Punkt allein vertikal gezogen hat (Brueestung-Drag), divergiert
|
||||
# pt_new.Z vom expected_pt_z → delta_z entspricht der echten User-
|
||||
# Eingabe → Bruestung wird angepasst.
|
||||
wall_uk = 0.0
|
||||
if parent_meta is not None:
|
||||
try:
|
||||
wall_uk, _ = _resolve_uk_ok(doc, parent_meta["geschoss"],
|
||||
parent_meta["uk_override"],
|
||||
parent_meta["ok_override"])
|
||||
except Exception:
|
||||
wall_uk = 0.0
|
||||
expected_pt_z = wall_uk + cur_bruest_val
|
||||
delta_z = pt_new.Z - expected_pt_z
|
||||
new_bruest = cur_bruest_val
|
||||
if abs(delta_z) >= 1e-6:
|
||||
new_bruest = max(0.0, cur_bruest_val + delta_z)
|
||||
|
||||
# Punkt visuell auf Bruestungs-Hoehe (= Unterkante Oeffnung), nicht auf 0.
|
||||
# So sieht der User wo die Oeffnung beginnt + Z-Drag-Delta entspricht
|
||||
# direkt der Bruestungsaenderung.
|
||||
target_z = new_bruest
|
||||
# Punkt visuell auf der Unterkante der Oeffnung in Welt-Z platzieren =
|
||||
# Wand-UK + Brueest. So sieht der User wo die Oeffnung beginnt, auch
|
||||
# wenn die Wand auf einem hoeheren Geschoss steht.
|
||||
target_z = wall_uk + new_bruest
|
||||
geom_changed = not (
|
||||
abs(target_x - pt_new.X) < 1e-9
|
||||
and abs(target_y - pt_new.Y) < 1e-9
|
||||
@@ -6901,10 +6944,15 @@ def _on_object_replaced(sender, e):
|
||||
"""
|
||||
if sc.sticky.get(_REGEN_BUSY): return
|
||||
# Wenn ein User-Transform-Command (_Move/_Rotate/etc.) aktiv ist: GAR
|
||||
# NICHTS hier tun. Rhinos Move soll konfliktfrei durchlaufen. Nach
|
||||
# CommandEnd vergleichen wir Snapshot vs. aktuellen State + machen den
|
||||
# ganzen Update in einem konfliktfreien Batch.
|
||||
if sc.sticky.get(_UT_ACTIVE_KEY): return
|
||||
# NICHTS hier tun (Rhinos Move soll konfliktfrei durchlaufen). Erstes
|
||||
# Event = User hat geklickt → Redraw ab jetzt suppressen, sonst Mismatch-
|
||||
# Frame zwischen Rhinos Auto-Redraw und unserem Regen.
|
||||
if sc.sticky.get(_UT_ACTIVE_KEY):
|
||||
_suppress_redraw_until_cmd_end()
|
||||
return
|
||||
# Undo/Redo: Rhino restored den Zustand → wir machen NICHTS, sonst
|
||||
# Regen-Storm fuer jedes restored Object.
|
||||
if sc.sticky.get(_UNDO_ACTIVE_KEY): return
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
# Snapshot der aktuell selektierten IDs — damit Migrate die Objekte
|
||||
# skippen kann die Rhinos Move/Rotate gerade transformiert (sonst
|
||||
@@ -7159,6 +7207,14 @@ def _on_object_added(sender, e):
|
||||
neue UUID, Volume-Duplikate werden geloescht (Regen baut das neue
|
||||
Volumen am richtigen Ort)."""
|
||||
if sc.sticky.get(_REGEN_BUSY): return
|
||||
# Waehrend Move/Rotate/Mirror/Scale: Rhino feuert intern Delete+Add fuer
|
||||
# jedes transformierte Objekt. CommandEnd uebernimmt die Re-Sync —
|
||||
# diese Events ignorieren, sonst laeuft die Regen-Pipeline trotz
|
||||
# Pure-Translate-Skip.
|
||||
if sc.sticky.get(_UT_ACTIVE_KEY):
|
||||
_suppress_redraw_until_cmd_end()
|
||||
return
|
||||
if sc.sticky.get(_UNDO_ACTIVE_KEY): return
|
||||
try:
|
||||
new_obj = e.TheObject
|
||||
meta = _read_meta(new_obj)
|
||||
@@ -7254,6 +7310,13 @@ def _on_object_deleted(sender, e):
|
||||
wenn die Source mit gleicher ID zurueckkommt (= Transform, kein User-
|
||||
Delete).
|
||||
"""
|
||||
# Waehrend Move/Rotate/Mirror/Scale: CommandEnd-Pfad uebernimmt das
|
||||
# Re-Sync. Sonst queued der Delete-Event ueberfluessige Regen-Calls die
|
||||
# den Pure-Translate-Skip wieder zunichtemachen.
|
||||
if sc.sticky.get(_UT_ACTIVE_KEY):
|
||||
_suppress_redraw_until_cmd_end()
|
||||
return
|
||||
if sc.sticky.get(_UNDO_ACTIVE_KEY): return
|
||||
try:
|
||||
obj = e.TheObject
|
||||
meta = _read_meta(obj)
|
||||
@@ -7688,8 +7751,15 @@ _USER_TRANSFORM_CMDS = frozenset((
|
||||
"Drag", "Gumball", "Orient", "Orient3Pt", "RemapCPlane", "Transform",
|
||||
))
|
||||
|
||||
# Undo/Redo: Rhino restored Objekte aus dem Undo-Stack → feuert Add/Delete-
|
||||
# Events fuer ALLE betroffenen Objekte. Unsere Handler wuerden fuer jedes
|
||||
# einen Regen queuen → Storm. Wir suppressen die Handler komplett; Undo hat
|
||||
# den Zustand schon konsistent wiederhergestellt, kein Regen noetig.
|
||||
_USER_UNDO_CMDS = frozenset(("Undo", "Redo"))
|
||||
|
||||
_UT_ACTIVE_KEY = "_dossier_user_transform_active"
|
||||
_UT_SNAPSHOT_KEY = "_dossier_pre_transform_snapshot"
|
||||
_UNDO_ACTIVE_KEY = "_dossier_undo_active"
|
||||
|
||||
|
||||
def _snapshot_source_positions(doc):
|
||||
@@ -7707,12 +7777,16 @@ def _snapshot_source_positions(doc):
|
||||
t = m.get("type")
|
||||
geom = obj.Geometry
|
||||
if t in SOURCE_TYPES:
|
||||
parent = m.get("oeff_parent") or ""
|
||||
if hasattr(geom, "Location"):
|
||||
p = geom.Location
|
||||
snap["sources"][m["id"]] = {"type": t, "pos": (p.X, p.Y, p.Z)}
|
||||
snap["sources"][m["id"]] = {"type": t,
|
||||
"oeff_parent": parent,
|
||||
"pos": (p.X, p.Y, p.Z)}
|
||||
elif isinstance(geom, rg.Curve):
|
||||
s = geom.PointAtStart; e = geom.PointAtEnd
|
||||
snap["sources"][m["id"]] = {"type": t,
|
||||
"oeff_parent": parent,
|
||||
"start": (s.X, s.Y, s.Z),
|
||||
"end": (e.X, e.Y, e.Z)}
|
||||
elif t in VOLUME_TYPES:
|
||||
@@ -7728,25 +7802,96 @@ def _snapshot_source_positions(doc):
|
||||
return snap
|
||||
|
||||
|
||||
def _suppress_redraw_until_cmd_end():
|
||||
"""Schaltet RedrawEnabled erst auf False sobald das ERSTE Object-Event
|
||||
waehrend eines User-Transform-Commands feuert. Damit bleiben Rubber-
|
||||
Band-Linie und Drag-Vorschau waehrend des Pickings sichtbar (Picking
|
||||
feuert keine Object-Events), aber Rhinos automatischer Post-Move-
|
||||
Redraw (kommt nach dem Klick, direkt nach den Replace-Events) wird
|
||||
unterdrueckt. Wird im selben Command nur einmal aktiv."""
|
||||
if sc.sticky.get("_dossier_cmd_redraw_suppressed"): return
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
if doc is None: return
|
||||
try:
|
||||
sc.sticky["_dossier_cmd_redraw_prev"] = bool(doc.Views.RedrawEnabled)
|
||||
doc.Views.RedrawEnabled = False
|
||||
sc.sticky["_dossier_cmd_redraw_suppressed"] = True
|
||||
except Exception as ex:
|
||||
print("[ELEMENTE] suppress redraw:", ex)
|
||||
|
||||
|
||||
def _on_command_begin(sender, e):
|
||||
try:
|
||||
name = getattr(e, "CommandEnglishName", "") or ""
|
||||
except Exception: name = ""
|
||||
if name not in _USER_TRANSFORM_CMDS: return
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
if doc is None: return
|
||||
# Undo/Redo: nur Flag setzen, KEIN Snapshot, KEIN Redraw-Suppress —
|
||||
# Rhinos Undo verwaltet RedrawEnabled selbst. Event-Handler ignorieren
|
||||
# waehrend dieser Phase alle Add/Delete/Replace-Events → kein Regen-
|
||||
# Storm.
|
||||
if name in _USER_UNDO_CMDS:
|
||||
sc.sticky[_UNDO_ACTIVE_KEY] = name
|
||||
return
|
||||
if name not in _USER_TRANSFORM_CMDS: return
|
||||
sc.sticky[_UT_SNAPSHOT_KEY] = _snapshot_source_positions(doc)
|
||||
sc.sticky[_UT_ACTIVE_KEY] = name
|
||||
# RedrawEnabled bleibt HIER auf True. Wird erst beim ersten Object-Event
|
||||
# (= nach dem Klick) via `_suppress_redraw_until_cmd_end` ausgeschaltet.
|
||||
# Rubber-Band-Linie + Drag-Vorschau bleiben dadurch wahrend Picking
|
||||
# sichtbar.
|
||||
# Undo-Record umschliesst Rhinos Move + unseren Regen in EINEM Undo-
|
||||
# Schritt. Sonst macht jedes Delete/AddBrep eine eigene Undo-Entry und
|
||||
# Cmd+Z bringt nur halbe Wand zurueck → Duplikate.
|
||||
try:
|
||||
serial = doc.BeginUndoRecord("Element-Transform")
|
||||
sc.sticky["_dossier_undo_serial"] = serial
|
||||
except Exception as ex:
|
||||
print("[ELEMENTE] cmd-begin undo record:", ex)
|
||||
sc.sticky["_dossier_undo_serial"] = None
|
||||
|
||||
|
||||
def _on_command_end(sender, e):
|
||||
# Undo/Redo abschliessen: nur Flag clearen, kein Regen + ein Selection-
|
||||
# Refresh fuers Gestaltung-Panel (Listener waren waehrend Undo aus).
|
||||
if sc.sticky.get(_UNDO_ACTIVE_KEY):
|
||||
sc.sticky[_UNDO_ACTIVE_KEY] = None
|
||||
gb = sc.sticky.get("gestaltung_bridge")
|
||||
if gb is not None:
|
||||
try: gb._send_selection()
|
||||
except Exception: pass
|
||||
b = sc.sticky.get("elemente_bridge")
|
||||
if b is not None:
|
||||
try: b._send_state()
|
||||
except Exception: pass
|
||||
return
|
||||
name = sc.sticky.get(_UT_ACTIVE_KEY)
|
||||
if not name: return
|
||||
sc.sticky[_UT_ACTIVE_KEY] = None
|
||||
# _UT_ACTIVE_KEY bleibt gesetzt bis am Ende der Funktion — sonst feuern
|
||||
# gestaltungs Listener auf die Replace-Events die wir hier selber
|
||||
# erzeugen (Pure-Translate translates Volumen via Replace; Regen-Pfad
|
||||
# ersetzt Sub-Volumen). Cleanup im finally-Block am Ende.
|
||||
snapshot = sc.sticky.get(_UT_SNAPSHOT_KEY) or {}
|
||||
sc.sticky[_UT_SNAPSHOT_KEY] = None
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
if doc is None: return
|
||||
if doc is None:
|
||||
sc.sticky[_UT_ACTIVE_KEY] = None
|
||||
sc.sticky["_dossier_cmd_redraw_suppressed"] = None
|
||||
sc.sticky["_dossier_cmd_redraw_prev"] = None
|
||||
sc.sticky["_dossier_undo_serial"] = None
|
||||
return
|
||||
|
||||
# RedrawEnabled wurde idR schon beim ersten Object-Event nach dem
|
||||
# User-Klick auf False gesetzt (`_suppress_redraw_until_cmd_end`). Den
|
||||
# gemerkten prev-Wert lesen. Falls kein Event gefeuert hat (z.B. Move
|
||||
# ohne tatsaechliche Aenderung), suppressen wir jetzt selber.
|
||||
if sc.sticky.get("_dossier_cmd_redraw_suppressed"):
|
||||
prev_redraw_enabled = sc.sticky.get("_dossier_cmd_redraw_prev", True)
|
||||
sc.sticky["_dossier_cmd_redraw_suppressed"] = None
|
||||
sc.sticky["_dossier_cmd_redraw_prev"] = None
|
||||
else:
|
||||
prev_redraw_enabled = doc.Views.RedrawEnabled
|
||||
doc.Views.RedrawEnabled = False
|
||||
|
||||
sources_snap = snapshot.get("sources", {}) if isinstance(snapshot, dict) else {}
|
||||
volumes_snap = snapshot.get("volumes", {}) if isinstance(snapshot, dict) else {}
|
||||
@@ -7794,10 +7939,29 @@ def _on_command_end(sender, e):
|
||||
deltas[m["id"]] = d
|
||||
except Exception: pass
|
||||
|
||||
# IDs der tatsaechlich bewegten Sources (Delta != 0). `deltas` enthaelt
|
||||
# ALLE Sources der Szene mit ihrem Snapshot-Delta — unbewegte haben
|
||||
# Delta=(0,0,0). Wir brauchen aber die *bewegten*, um Hierarchie-
|
||||
# Entscheidungen zu treffen (Eltern→Kind-Cascade).
|
||||
moved_ids = {eid for eid, d in deltas.items()
|
||||
if abs(d[0]) > 1e-6 or abs(d[1]) > 1e-6 or abs(d[2]) > 1e-6}
|
||||
|
||||
# Orphan-Oeffnung erkennen: bewegte Oeffnung, deren Eltern-Wand NICHT
|
||||
# mitbewegt wurde. In diesem Fall ist Pure-Translate verboten — die
|
||||
# Wand-Aussparung (Boolean-Difference-Loch im Wand-Brep) bliebe am alten
|
||||
# Ort. Nur ein voller Wand-Regen rebuildet das Loch an der neuen Pos.
|
||||
orphan_opening = False
|
||||
for eid in moved_ids:
|
||||
old = sources_snap.get(eid)
|
||||
if old and old.get("type") == "oeffnung_point":
|
||||
parent = old.get("oeff_parent")
|
||||
if parent and parent not in moved_ids:
|
||||
orphan_opening = True
|
||||
break
|
||||
|
||||
pure_delta = None
|
||||
if not abort_pure:
|
||||
moved = [d for d in deltas.values()
|
||||
if abs(d[0]) > 1e-6 or abs(d[1]) > 1e-6 or abs(d[2]) > 1e-6]
|
||||
if not abort_pure and not orphan_opening:
|
||||
moved = [d for eid, d in deltas.items() if eid in moved_ids]
|
||||
if moved:
|
||||
# Alle bewegten Sources muessen denselben Vektor haben
|
||||
first = moved[0]
|
||||
@@ -7809,24 +7973,45 @@ def _on_command_end(sender, e):
|
||||
has_z_drag = abs(first[2]) > 1e-6
|
||||
if same and not has_z_drag:
|
||||
pure_delta = first
|
||||
else:
|
||||
print("[ELEMENTE] no pure-translate: same={} has_z={} n_moved={}".format(
|
||||
same, has_z_drag, len(moved)))
|
||||
elif abort_pure:
|
||||
print("[ELEMENTE] no pure-translate: end-grip/rotate/scale detected")
|
||||
elif orphan_opening:
|
||||
print("[ELEMENTE] no pure-translate: opening moved without parent wall (cutout muss regen)")
|
||||
|
||||
if pure_delta is not None:
|
||||
# PURE-TRANSLATE PFAD: nur Geometries translaten die nicht schon vom
|
||||
# User-Move transformed wurden. Keine Brep-Regeneration, kein
|
||||
# Boolean-Diff. → instant feedback.
|
||||
print("[ELEMENTE] pure-translate: delta=({:.3f}, {:.3f}, {:.3f})".format(*pure_delta))
|
||||
vec = rg.Vector3d(*pure_delta)
|
||||
|
||||
# Eltern→Kind-Cascade: wenn eine Wand bewegt wurde, muessen ihre
|
||||
# Oeffnungen mit. Aber NICHT umgekehrt: wenn nur eine Oeffnung
|
||||
# bewegt wird, bleibt die Wand stehen.
|
||||
def _should_follow(m):
|
||||
eid = m.get("id")
|
||||
if eid in moved_ids: return True
|
||||
# Oeffnung ihrer Eltern-Wand folgen lassen
|
||||
parent = m.get("oeff_parent")
|
||||
if parent and parent in moved_ids: return True
|
||||
return False
|
||||
|
||||
_was_busy = sc.sticky.get(_REGEN_BUSY, False)
|
||||
sc.sticky[_REGEN_BUSY] = True
|
||||
prev_redraw = doc.Views.RedrawEnabled
|
||||
doc.Views.RedrawEnabled = False
|
||||
# RedrawEnabled wurde schon in _on_command_begin auf False gesetzt
|
||||
try:
|
||||
for obj in list(doc.Objects):
|
||||
try:
|
||||
m = _read_meta(obj)
|
||||
if not m: continue
|
||||
t = m.get("type")
|
||||
# Sources die nicht in deltas waren (= unbewegt) auch translaten
|
||||
if t in SOURCE_TYPES and m["id"] not in deltas:
|
||||
if not _should_follow(m): continue
|
||||
# Sources die nicht bewegt wurden (= Delta=0) translaten
|
||||
# — nur Oeffnungs-Sources via _should_follow erlaubt.
|
||||
if t in SOURCE_TYPES and m["id"] not in moved_ids:
|
||||
new_geom = obj.Geometry.Duplicate()
|
||||
new_geom.Translate(vec)
|
||||
doc.Objects.Replace(obj.Id, new_geom)
|
||||
@@ -7854,14 +8039,30 @@ def _on_command_end(sender, e):
|
||||
except Exception as ex:
|
||||
print("[ELEMENTE] pure-translate:", ex)
|
||||
finally:
|
||||
doc.Views.RedrawEnabled = prev_redraw
|
||||
sc.sticky[_REGEN_BUSY] = _was_busy
|
||||
doc.Views.RedrawEnabled = prev_redraw_enabled
|
||||
try: doc.Views.Redraw()
|
||||
except Exception: pass
|
||||
# Flag erst HIER cleren, nachdem alle Replace-Events durch sind —
|
||||
# sonst feuert gestaltung.on_replace pro Volume.
|
||||
sc.sticky[_UT_ACTIVE_KEY] = None
|
||||
# Undo-Record schliessen — alles seit BeginUndoRecord landet in
|
||||
# einem einzelnen Cmd+Z-Schritt.
|
||||
undo_serial = sc.sticky.get("_dossier_undo_serial")
|
||||
if undo_serial:
|
||||
try: doc.EndUndoRecord(undo_serial)
|
||||
except Exception: pass
|
||||
sc.sticky["_dossier_undo_serial"] = None
|
||||
b = sc.sticky.get("elemente_bridge")
|
||||
if b is not None:
|
||||
try: b._send_state()
|
||||
except Exception: pass
|
||||
# Gestaltung-Panel einmalig nachziehen — Listener waren waehrend
|
||||
# des User-Transform-Commands suspendiert.
|
||||
gb = sc.sticky.get("gestaltung_bridge")
|
||||
if gb is not None:
|
||||
try: gb._send_selection()
|
||||
except Exception: pass
|
||||
return
|
||||
|
||||
# ─── Regulärer Pfad: Constraints + Migrate + Regen (existing flow) ──────
|
||||
@@ -7877,11 +8078,10 @@ def _on_command_end(sender, e):
|
||||
# Regen ausloesen → mehrere Regens pro Wand. Wir machen am Schluss EINEN
|
||||
# Regen pro affected_wall — viel schneller bei mehreren Oeffnungen.
|
||||
sc.sticky["_dossier_skip_sync_regen"] = True
|
||||
# Display-Updates komplett suppressen waehrend der Batch — Rhino zeichnet
|
||||
# sonst nach jedem Brep-Add/Replace neu, was bei mehreren Sub-Volumen
|
||||
# sichtbares „Aufbauen" verursacht. Ein einziger Redraw am Ende reicht.
|
||||
prev_redraw = doc.Views.RedrawEnabled
|
||||
doc.Views.RedrawEnabled = False
|
||||
# RedrawEnabled wurde schon in _on_command_begin auf False gesetzt —
|
||||
# damit unterdruecken wir auch Rhinos automatischen Post-Move-Redraw
|
||||
# (sonst kurzer Mismatch-Frame: Oeffnung an neuer Pos, Wand-Loch noch
|
||||
# an alter Pos).
|
||||
try:
|
||||
for obj in list(doc.Objects):
|
||||
try:
|
||||
@@ -7925,26 +8125,30 @@ def _on_command_end(sender, e):
|
||||
except (ValueError, TypeError): z_delta = 0.0
|
||||
sc.sticky["_elemente_wand_z_delta"] = None
|
||||
if abs(z_delta) >= 1e-6:
|
||||
# Brüstungen aller Öffnungen der Wand um delta mitnehmen
|
||||
# OPTION A: Brueest ist RELATIV zur Wand-UK. Da UK
|
||||
# in `_apply_wand_z_drag_constraint` schon um z_delta
|
||||
# geaendert wurde, folgt die Oeffnung automatisch via
|
||||
# Regen (cutout = new_UK + brueest = old_world_Z +
|
||||
# z_delta). Wir muessen NICHT die brueest-UserString
|
||||
# aktualisieren — sonst gaebe es Doppel-Addition.
|
||||
# Den Oeffnungs-Punkt setzen wir auf Snapshot-Z +
|
||||
# z_delta. So funktioniert es egal ob Rhino die
|
||||
# Oeffnung schon mit-bewegt hat (User-Multi-Select)
|
||||
# oder nicht — das End-Z ist immer das richtige.
|
||||
for op_obj, op_meta in _find_openings_for_wall(doc, m["id"]):
|
||||
cur_b = op_meta.get("oeff_brueest")
|
||||
try:
|
||||
cur_b_val = float(cur_b) if cur_b not in (None, "") else 0.0
|
||||
except (ValueError, TypeError):
|
||||
cur_b_val = 0.0
|
||||
new_b = max(0.0, cur_b_val + z_delta)
|
||||
try:
|
||||
attrs = op_obj.Attributes.Duplicate()
|
||||
attrs.SetUserString(_KEY_OEFF_BRUEST,
|
||||
"{:.6f}".format(new_b))
|
||||
doc.Objects.ModifyAttributes(op_obj.Id, attrs, True)
|
||||
op_snap = sources_snap.get(op_meta["id"])
|
||||
if not op_snap: continue
|
||||
op_pos = op_snap.get("pos")
|
||||
if op_pos is None: continue
|
||||
pt_geom = op_obj.Geometry
|
||||
if hasattr(pt_geom, "Location"):
|
||||
if not hasattr(pt_geom, "Location"): continue
|
||||
pt = pt_geom.Location
|
||||
target_z = op_pos[2] + z_delta
|
||||
doc.Objects.Replace(op_obj.Id,
|
||||
rg.Point(rg.Point3d(pt.X, pt.Y, new_b)))
|
||||
rg.Point(rg.Point3d(pt.X, pt.Y, target_z)))
|
||||
except Exception as ex:
|
||||
print("[ELEMENTE] post-cmd brueest:", ex)
|
||||
print("[ELEMENTE] post-cmd brueest pt-shift:", ex)
|
||||
affected_walls.add(m["id"])
|
||||
elif t == "oeffnung_point":
|
||||
op_pos = old.get("pos")
|
||||
@@ -7962,19 +8166,33 @@ def _on_command_end(sender, e):
|
||||
# Sync-Regen aller betroffenen Wände — Move ist sauber abgeschlossen,
|
||||
# kein Konflikt mehr moeglich. EIN Regen pro Wand (nicht pro Oeffnung).
|
||||
# Display bleibt suppressed bis ALLE Wände durch sind → kein „Aufbauen".
|
||||
try:
|
||||
for wid in affected_walls:
|
||||
try: _regenerate_element(doc, wid)
|
||||
except Exception as ex:
|
||||
print("[ELEMENTE] post-cmd regen:", ex)
|
||||
finally:
|
||||
doc.Views.RedrawEnabled = prev_redraw
|
||||
doc.Views.RedrawEnabled = prev_redraw_enabled
|
||||
try: doc.Views.Redraw()
|
||||
except Exception: pass
|
||||
# Flag erst HIER cleren — nach dem Regen-Pfad, der via _regenerate_element
|
||||
# viele Replace-Events erzeugt die wir auch suppressen wollen.
|
||||
sc.sticky[_UT_ACTIVE_KEY] = None
|
||||
# Undo-Record schliessen — alles seit BeginUndoRecord landet in
|
||||
# einem einzelnen Cmd+Z-Schritt.
|
||||
undo_serial = sc.sticky.get("_dossier_undo_serial")
|
||||
if undo_serial:
|
||||
try: doc.EndUndoRecord(undo_serial)
|
||||
except Exception: pass
|
||||
sc.sticky["_dossier_undo_serial"] = None
|
||||
b = sc.sticky.get("elemente_bridge")
|
||||
if b is not None:
|
||||
try: b._send_state()
|
||||
except Exception: pass
|
||||
# Gestaltung-Panel einmalig nachziehen — Listener waren waehrend
|
||||
# des Commands + des Regens suspendiert.
|
||||
gb = sc.sticky.get("gestaltung_bridge")
|
||||
if gb is not None:
|
||||
try: gb._send_selection()
|
||||
except Exception: pass
|
||||
|
||||
|
||||
def _install_listeners(bridge):
|
||||
|
||||
+37
-18
@@ -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:
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user