Rotation: Snapshot-basierte Migrate für korrekte Bogenlängen-Position

Bug: Bei Rotation um einen externen Punkt liegt der Öffnungs-Punkt
nach Rhinos Transform NICHT mehr auf der alten Achse → migrate's
ClosestPoint(current_pos) snappte zum nächsten Endpunkt der alten
Achse → relative=1 → alle Öffnungen landeten am gleichen Ende der
neuen Achse (= „bei Referenzpunkt der Drehung").

Fix: Migrate nutzt jetzt die PRE-TRANSFORM Position aus dem Snapshot
(via `old_positions` Parameter). Aufrufer im CommandEnd-Regen-Pfad
sammelt die alten Positionen aus `sources_snap` und gibt sie weiter.

Migrate setzt opening_point.Z jetzt auch konsistent auf
`wall_uk + brüstung` statt nur `brüstung` — vermeidet Brüstung-Drop
beim nachfolgenden _apply_oeffnung_constraint.

Constraint überspringt XY-Projektion wenn Wand gerade migriert
wurde (`_dossier_migrated_walls` sticky-Set) — sonst würde
ClosestPoint(pt_old) auf neuer rotierter Achse die Position wieder
verschieben.

Debug-Logs in _apply_wand_z_drag_constraint + Wand-Regen bleiben
drin — haben bei der Eingrenzung des UK_OVER-Bugs geholfen, kosten
nichts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 22:47:09 +02:00
parent 2a75b1da93
commit 0978d9fc2e
+74 -8
View File
@@ -6839,7 +6839,16 @@ def _apply_oeffnung_constraint(new_obj, meta, old_obj=None):
break
target_x, target_y = pt_new.X, pt_new.Y
if parent_curve is not None:
# Wenn die Wand gerade migrate'd wurde (Rotation/Reshape/XY-Move) →
# XY-Projektion HIER UEBERSPRINGEN. Migrate hat den Punkt schon per
# Bogenlaengen-Mapping auf die neue Achse gesetzt. Eine zweite XY-
# Projektion mit ClosestPoint(pt_old) auf der NEUEN Achse wuerde die
# Position wieder verschieben (Rotation: pt_old liegt nicht mehr auf
# der neuen Achse → ClosestPoint+Tangent stimmen nicht zusammen).
migrated_walls = sc.sticky.get("_dossier_migrated_walls")
skip_xy_projection = (isinstance(migrated_walls, set)
and parent_id in migrated_walls)
if parent_curve is not None and not skip_xy_projection:
if pt_old is not None:
try:
rc, t_old = parent_curve.ClosestPoint(
@@ -7092,12 +7101,20 @@ def _on_object_replaced_body(sender, e):
print("[ELEMENTE] on_object_replaced:", ex)
def _migrate_openings_to_new_axis(wall_id, old_geom, new_geom):
def _migrate_openings_to_new_axis(wall_id, old_geom, new_geom, old_positions=None):
"""Verschiebt alle Oeffnungs-Points einer Wand mit, wenn deren Achse
veraendert wird. Mapping ueber relative Bogenlaenge: ein Oeffnungs-
Punkt bei 30 % der alten Kurve sitzt nachher bei 30 % der neuen.
So bleiben die Oeffnungen 'sticky' an der Wand bei Verschieben,
Drehen, Skalieren oder Reshape der Achse."""
Drehen, Skalieren oder Reshape der Achse.
`old_positions` (optional): {opening_id: (x, y, z)} Pre-Transform
Snapshot der Oeffnungs-Punkte. WICHTIG bei Rotation/Move: nach Rhinos
Transform liegen die Punkte schon NICHT MEHR auf der alten Axis
`ClosestPoint(current_pos)` an old_geom snappt zum naechsten Endpunkt
statt zur echten Bogenlaengen-Position alle Oeffnungen landen am
selben Ende. Bei Reshape-Operationen ohne Snapshot: Fallback auf
aktuelle Geometrie (Punkt liegt dort noch auf alter Axis)."""
if not isinstance(old_geom, rg.Curve) or not isinstance(new_geom, rg.Curve):
return
doc = Rhino.RhinoDoc.ActiveDoc
@@ -7107,6 +7124,29 @@ def _migrate_openings_to_new_axis(wall_id, old_geom, new_geom):
new_len = new_geom.GetLength()
except Exception: return
if old_len < 1e-9 or new_len < 1e-9: return
# Wand-UK aufloesen damit Oeffnungs-Punkte auf UK+Brueestung gesetzt
# werden (= visuell auf Unterkante Oeffnung). Sonst landen sie auf
# reiner Brueest-Hoehe und der nachfolgende Constraint interpretiert
# die Diskrepanz als User-Z-Drag → Brueest dropt.
wall_uk = 0.0
src = _find_axis(doc, wall_id)
if src is not None:
wm = _read_meta(src)
if wm:
try:
wall_uk, _ = _resolve_uk_ok(doc, wm["geschoss"],
wm["uk_override"],
wm["ok_override"])
except Exception:
wall_uk = 0.0
# Migrierten Wand registrieren — der Constraint soll fuer Oeffnungen
# dieser Wand die XY-Projektion ueberspringen (migrate hat XY bereits
# via Bogenlaengen-Mapping korrekt gesetzt).
migrated = sc.sticky.get("_dossier_migrated_walls")
if not isinstance(migrated, set):
migrated = set()
migrated.add(wall_id)
sc.sticky["_dossier_migrated_walls"] = migrated
# Selected-Snapshot vom Replace-Handler — nicht live IsSelected, weil
# op_obj im laufenden Move-Event evtl. schon stale ist.
@@ -7127,18 +7167,27 @@ def _migrate_openings_to_new_axis(wall_id, old_geom, new_geom):
# transform" + ganzer Regen-Undo-Record wird rollbacked.
if str(op_obj.Id) in skip_ids:
continue
# Pre-Transform Position bevorzugen — die liegt garantiert
# auf der alten Axis. Aktuelle (post-transform) Position kann
# bei Rotation weit weg liegen → ClosestPoint snappt zum
# falschen Endpunkt.
src_pos = None
if old_positions is not None:
src_pos = old_positions.get(op_meta["id"])
if src_pos is None:
pt_geom = op_obj.Geometry
if hasattr(pt_geom, 'Location'):
cur_pos = pt_geom.Location
loc = pt_geom.Location
src_pos = (loc.X, loc.Y, loc.Z)
elif isinstance(pt_geom, rg.Point3d):
cur_pos = pt_geom
src_pos = (pt_geom.X, pt_geom.Y, pt_geom.Z)
else:
continue
# XY-only ClosestPoint — sonst zieht eine non-zero Z-Komponente
# (Bruestungs-Hoehe) den Parameter bei kurvigen Wand-Achsen
# leicht weg von der „echten" Position.
ok_old, t_old = old_geom.ClosestPoint(
rg.Point3d(cur_pos.X, cur_pos.Y, 0.0))
rg.Point3d(src_pos[0], src_pos[1], 0.0))
if not ok_old: continue
# Bogenlaenge auf alter Kurve bis t_old → relative Position
sub = rg.Interval(old_geom.Domain.Min, t_old)
@@ -7174,7 +7223,9 @@ def _migrate_openings_to_new_axis(wall_id, old_geom, new_geom):
bruest_z = float(bruest) if bruest not in (None, "") else 0.0
except (ValueError, TypeError):
bruest_z = 0.0
new_pos = rg.Point3d(new_pos.X, new_pos.Y, bruest_z)
# Welt-Z = Wand-UK + Brueestung (Konvention: Punkt sitzt
# visuell auf Unterkante Oeffnung).
new_pos = rg.Point3d(new_pos.X, new_pos.Y, wall_uk + bruest_z)
doc.Objects.Replace(op_obj.Id, rg.Point(new_pos))
except Exception as ex:
print("[ELEMENTE] migrate one opening:", ex)
@@ -8053,6 +8104,7 @@ def _on_command_end(sender, e):
try: doc.EndUndoRecord(undo_serial)
except Exception: pass
sc.sticky["_dossier_undo_serial"] = None
sc.sticky["_dossier_migrated_walls"] = None
b = sc.sticky.get("elemente_bridge")
if b is not None:
try: b._send_state()
@@ -8111,7 +8163,20 @@ def _on_command_end(sender, e):
old_line = rg.LineCurve(
rg.Point3d(os[0], os[1], os[2]),
rg.Point3d(oe[0], oe[1], oe[2]))
_migrate_openings_to_new_axis(m["id"], old_line, geom)
# Pre-Transform Oeffnungs-Positionen aus dem
# Snapshot ziehen — Migrate braucht sie um die
# Bogenlaengen-Position auf der ALTEN Axis zu
# finden (sonst bei Rotation falscher Snap).
old_op_positions = {}
for snap_id, snap_data in sources_snap.items():
if snap_data.get("type") != "oeffnung_point":
continue
if snap_data.get("oeff_parent") != m["id"]:
continue
pos = snap_data.get("pos")
if pos: old_op_positions[snap_id] = pos
_migrate_openings_to_new_axis(
m["id"], old_line, geom, old_op_positions)
except Exception as ex:
print("[ELEMENTE] post-cmd migrate:", ex)
# Z-Drag detect + Brüstungs-Mitnahme. Constraint setzt
@@ -8183,6 +8248,7 @@ def _on_command_end(sender, e):
try: doc.EndUndoRecord(undo_serial)
except Exception: pass
sc.sticky["_dossier_undo_serial"] = None
sc.sticky["_dossier_migrated_walls"] = None
b = sc.sticky.get("elemente_bridge")
if b is not None:
try: b._send_state()