Wand-Chain Cmd+Z-Fix: ReplaceObjectProxy als Grip-Drag-Cmd registrieren

Mac Rhino feuert beim Endpunkt-Grip-Drag intern den Command
'ReplaceObjectProxy'. Ohne den Eintrag in _USER_TRANSFORM_CMDS oeffnete
_on_command_begin keinen Undo-Record + nahm keinen Snapshot — unser
chain-Pre-Check + Regen liefen dann erst auf Idle in einem eigenen
Undo-Record ('Elemente regenerieren (N)'). Cmd+Z brauchte deshalb zwei
Schritte: erst Volume-Restore, dann Axis-Restore.

Fix:
- ReplaceObjectProxy in _USER_TRANSFORM_CMDS
- _REGEN_BUSY-Guard in _on_command_begin damit unsere eigenen internen
  Replace-Calls (Chain-Volume-Rebuild) keinen unerwuenschten Snapshot
  triggern

Plus: No-Op-Tolerance 1mm (Architektur-Praezision) im Replace-Handler
faengt CommitChanges-Microdrift ab — keine Chain-Break-Cascade beim
Selektieren mehr.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 16:14:57 +02:00
parent c050b9aeb6
commit f60d643bb7
+37 -11
View File
@@ -9874,6 +9874,28 @@ def _apply_wand_z_drag_constraint(new_obj, meta):
z1 = geom.PointAtEnd.Z
if abs(z0) < 1e-6 and abs(z1) < 1e-6:
return False
# In Top-View kann ein Z != 0 NICHT vom User stammen (er kann in Top gar
# nicht Z-draggen). Es ist ein Artefakt — typischerweise CommitChanges
# auf GripsOn oder Replace mit veraltetem Display-Z. Wand-Axis flatten
# ohne uk/ok zu modifizieren.
if _is_active_view_top_like():
print("[ELEMENTE] wand z-drag IGNORED (top view, z0={:.3f} z1={:.3f}) "
"— axis auf Z=0 flatten".format(z0, z1))
try:
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return False
if isinstance(geom, rg.LineCurve):
line = geom.Line
flat = rg.LineCurve(
rg.Point3d(line.From.X, line.From.Y, 0.0),
rg.Point3d(line.To.X, line.To.Y, 0.0))
else:
flat = geom.DuplicateCurve()
flat.Translate(rg.Vector3d(0, 0, -(z0 if abs(z0) > abs(z1) else z1)))
doc.Objects.Replace(new_obj.Id, flat)
except Exception as ex:
print("[ELEMENTE] flatten in top-view:", ex)
return False
delta = z1 if abs(z1) > abs(z0) else z0
print("[ELEMENTE] wand z-drag triggered: z0={:.3f} z1={:.3f} delta={:.3f}".format(z0, z1, delta))
doc = Rhino.RhinoDoc.ActiveDoc
@@ -10291,31 +10313,26 @@ def _on_object_replaced_body(sender, e):
if meta.get("type") == "wand_axis":
old_geom = e.OldRhinoObject.Geometry if e.OldRhinoObject else None
new_geom = e.NewRhinoObject.Geometry if e.NewRhinoObject else None
# No-Op-Check: GripsOn/CommitChanges feuert Replace ohne Geometrie-
# Aenderung. Triggert sonst eine ganze Regen-Kette die in eigenem
# Undo-Record laeuft (Cmd+Z muesste mehrfach gedrueckt werden).
# Endpunkt-Vergleich reicht fuer LineCurves; Polylines/Splines
# checken zusaetzlich Mid-Punkt + Laenge.
# No-Op-Check: GripsOn/CommitChanges feuert Replace ohne sinnvolle
# Geometrie-Aenderung. Toleranz auf 1mm (Architektur-Praezision).
unchanged = False
try:
if (isinstance(old_geom, rg.Curve)
and isinstance(new_geom, rg.Curve)):
p_o_s, p_o_e = old_geom.PointAtStart, old_geom.PointAtEnd
p_n_s, p_n_e = new_geom.PointAtStart, new_geom.PointAtEnd
tol = 1e-6
if (p_o_s.DistanceTo(p_n_s) < tol
and p_o_e.DistanceTo(p_n_e) < tol):
# Endpunkte gleich — Polyline/Spline-Mitten checken
tol_xy = 0.001 # 1 mm
if (p_o_s.DistanceTo(p_n_s) < tol_xy
and p_o_e.DistanceTo(p_n_e) < tol_xy):
try:
l_o = old_geom.GetLength()
l_n = new_geom.GetLength()
if abs(l_o - l_n) < tol * 100:
if abs(l_o - l_n) < tol_xy:
unchanged = True
except Exception:
unchanged = True
except Exception: unchanged = False
if unchanged:
# Nur Grips-Toggle / Attribut-Aenderung — kein Regen noetig.
return
# Joint-Cache invalidieren — Wand hat sich geaendert
_invalidate_joints_cache(meta.get("geschoss"))
@@ -11221,6 +11238,10 @@ def _on_idle_selection(sender, e):
_USER_TRANSFORM_CMDS = frozenset((
"Move", "Rotate", "Rotate3D", "Mirror", "Scale", "Scale1D", "Scale2D",
"Drag", "Gumball", "Orient", "Orient3Pt", "RemapCPlane", "Transform",
# Mac Rhino feuert "ReplaceObjectProxy" als Command-Name beim Grip-Drag.
# Ohne dieses Eintrag kommt unser Regen in eigenen Idle-Undo-Record →
# Cmd+Z muss zweimal gedrueckt werden.
"ReplaceObjectProxy",
))
# Bulk-Operations: User selektiert N Objekte + ausfuehrt die Operation
@@ -11371,6 +11392,11 @@ def _on_command_begin(sender, e):
except Exception: name = ""
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return
# ReplaceObjectProxy feuert auch fuer unsere eigenen internen Replace-
# Calls (chain-volume rebuild etc.). Wenn _REGEN_BUSY aktiv ist, kommt
# der Replace von uns — keinen Snapshot/Undo-Record oeffnen.
if name == "ReplaceObjectProxy" and sc.sticky.get(_REGEN_BUSY):
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-