From 0978d9fc2e68f9eda30193cc9a6d949ab39bf66b Mon Sep 17 00:00:00 2001 From: karim Date: Mon, 18 May 2026 22:47:09 +0200 Subject: [PATCH] =?UTF-8?q?Rotation:=20Snapshot-basierte=20Migrate=20f?= =?UTF-8?q?=C3=BCr=20korrekte=20Bogenl=C3=A4ngen-Position?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- rhino/elemente.py | 92 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 79 insertions(+), 13 deletions(-) diff --git a/rhino/elemente.py b/rhino/elemente.py index ce5a6e8..ff758b4 100644 --- a/rhino/elemente.py +++ b/rhino/elemente.py @@ -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 - pt_geom = op_obj.Geometry - if hasattr(pt_geom, 'Location'): - cur_pos = pt_geom.Location - elif isinstance(pt_geom, rg.Point3d): - cur_pos = pt_geom - else: - 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'): + loc = pt_geom.Location + src_pos = (loc.X, loc.Y, loc.Z) + elif isinstance(pt_geom, rg.Point3d): + 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()