Pure-Transform: Rotation läuft jetzt instant wie Translation
Erweitert die bisherige Pure-Translate-Optimierung auf beliebige Rigid-2D-Transforms (Translation + Z-Rotation). Statt nur einen Delta-Vektor zu detektieren, wird pro Source ein Rigid-Transform aus Snapshot-vs-aktueller-Geometrie berechnet: - Curve-Sources: aus Endpunkten Drehwinkel + Translation ableiten. - Length-Aenderung der Curve → Scale/End-Grip → abort_pure. - Z-Aenderung der Curve → Z-Drag → abort_pure (UK_OVER-Schreibung geht weiter ueber Regen-Pfad). - Point-Sources: nur Translation aus Position. Konsistenz-Check: alle Curve-Transforms muessen identisch sein, Point-Positionen muessen `canonical(old_pos) == new_pos` erfuellen. Sonst → Regen. Bei pure_transform != None: Transform auf alle Geometries der Cascade anwenden die nicht schon von Rhinos Move/Rotate transformed wurden. Volumes via bb-Snapshot-Check, Sources via identity-transform-check. Resultat: einzelne Wand + Oeffnungen rotieren → instant statt ~100-200ms Regen. Mirror-Limitation: Einzelne Wand-Spiegelung wird als 180°-Rotation interpretiert (matched die Endpunkte). Bei symmetrischen Volumen unsichtbar; bei asymmetrischen Fenstern visuell anders als ein echter Mirror. Mehrere Walls gleichzeitig spiegeln triggert all_consistent=False → Regen-Fallback (korrekt). Bekannte Einschraenkung, separater Fix nötig. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+139
-74
@@ -7947,33 +7947,70 @@ def _on_command_end(sender, e):
|
||||
sources_snap = snapshot.get("sources", {}) if isinstance(snapshot, dict) else {}
|
||||
volumes_snap = snapshot.get("volumes", {}) if isinstance(snapshot, dict) else {}
|
||||
|
||||
# ─── Pure-Translate-Detection ───────────────────────────────────────────
|
||||
# Wenn ALLE bewegten Sources um den exakt gleichen Vektor verschoben
|
||||
# wurden, KEIN Z-Drag (= Property-Aenderung), KEIN Rotate/Scale (= End-
|
||||
# Grip-Drag) — dann reicht eine reine Translation aller noch unbewegten
|
||||
# Volumen + Punkte. KEIN Wand-Regen, KEIN Boolean-Diff. Geht instant.
|
||||
def _source_delta(obj, old):
|
||||
# ─── Pure-Transform-Detection (Translation + Z-Rotation) ────────────────
|
||||
# Wenn ALLE bewegten Sources sich mit dem gleichen Rigid-2D-Transform
|
||||
# abbilden lassen (Translation und/oder Rotation um Z-Achse, KEIN Scale,
|
||||
# KEIN Z-Drag, KEIN End-Grip-Drag, KEIN Mirror), reicht eine Transform-
|
||||
# Anwendung auf alle noch unbewegten Volumen + Punkte. KEIN Wand-Regen,
|
||||
# KEIN Boolean-Diff. Geht instant.
|
||||
import math as _math
|
||||
|
||||
def _source_rigid_transform(obj, old):
|
||||
"""Berechnet den Rigid-2D-Transform (Translation + Z-Rotation) der
|
||||
alte Source-Geometrie auf die aktuelle abbildet. Returns None wenn
|
||||
Z-Drag/Scale/End-Grip/Mirror erkannt."""
|
||||
geom = obj.Geometry
|
||||
if hasattr(geom, "Location"):
|
||||
op = old.get("pos")
|
||||
if op is None: return None
|
||||
p = geom.Location
|
||||
return (p.X - op[0], p.Y - op[1], p.Z - op[2])
|
||||
if isinstance(geom, rg.Curve):
|
||||
os_pt = old.get("start"); oe_pt = old.get("end")
|
||||
if os_pt is None or oe_pt is None: return None
|
||||
ns = geom.PointAtStart; ne = geom.PointAtEnd
|
||||
ds = (ns.X - os_pt[0], ns.Y - os_pt[1], ns.Z - os_pt[2])
|
||||
de = (ne.X - oe_pt[0], ne.Y - oe_pt[1], ne.Z - oe_pt[2])
|
||||
# Beide Endpunkte muessen den gleichen Vektor haben (= Translate).
|
||||
# Sonst ist's Rotate/Scale/End-Grip-Drag.
|
||||
if (abs(ds[0]-de[0]) > 1e-6 or abs(ds[1]-de[1]) > 1e-6
|
||||
or abs(ds[2]-de[2]) > 1e-6):
|
||||
# Z-Aenderung verbietet Pure-Transform (= Z-Drag → UK_OVER muss
|
||||
# geschrieben werden → Regen-Pfad).
|
||||
if (abs(ns.Z - os_pt[2]) > 1e-6 or
|
||||
abs(ne.Z - oe_pt[2]) > 1e-6):
|
||||
return None
|
||||
return ds
|
||||
old_dx = oe_pt[0] - os_pt[0]; old_dy = oe_pt[1] - os_pt[1]
|
||||
new_dx = ne.X - ns.X; new_dy = ne.Y - ns.Y
|
||||
old_len = _math.hypot(old_dx, old_dy)
|
||||
new_len = _math.hypot(new_dx, new_dy)
|
||||
if old_len < 1e-9: return None
|
||||
# Laengenaenderung → Scale (oder einzelner Endpunkt-Drag)
|
||||
if abs(old_len - new_len) > 1e-6: return None
|
||||
# Drehwinkel um Z aus Richtungsvektoren
|
||||
old_angle = _math.atan2(old_dy, old_dx)
|
||||
new_angle = _math.atan2(new_dy, new_dx)
|
||||
angle = new_angle - old_angle
|
||||
# Transform: erst um old_start zentrieren, dann rotieren, dann
|
||||
# zu new_start translaten. So mappen sowohl old_start→new_start
|
||||
# als auch old_end→new_end korrekt.
|
||||
to_origin = rg.Transform.Translation(-os_pt[0], -os_pt[1], -os_pt[2])
|
||||
rotate = rg.Transform.Rotation(angle, rg.Vector3d.ZAxis, rg.Point3d.Origin)
|
||||
to_new = rg.Transform.Translation(ns.X, ns.Y, ns.Z)
|
||||
return to_new * rotate * to_origin
|
||||
if hasattr(geom, "Location"):
|
||||
op = old.get("pos")
|
||||
if op is None: return None
|
||||
p = geom.Location
|
||||
# Punkt: keine Orientierungs-Info → nur Translation ableitbar.
|
||||
# Konsistenz mit Curve-Transform wird in Phase 2 geprueft.
|
||||
return rg.Transform.Translation(p.X - op[0], p.Y - op[1], p.Z - op[2])
|
||||
return None
|
||||
|
||||
deltas = {}
|
||||
def _is_identity_transform(t, tol=1e-6):
|
||||
for i in range(4):
|
||||
for j in range(4):
|
||||
ref = 1.0 if i == j else 0.0
|
||||
if abs(t[i, j] - ref) > tol: return False
|
||||
return True
|
||||
|
||||
def _transforms_equal(t1, t2, tol=1e-6):
|
||||
for i in range(4):
|
||||
for j in range(4):
|
||||
if abs(t1[i, j] - t2[i, j]) > tol: return False
|
||||
return True
|
||||
|
||||
# Phase 1: Transform pro Source berechnen, abort bei non-rigid
|
||||
source_transforms = {}
|
||||
abort_pure = False
|
||||
for obj in doc.Objects:
|
||||
try:
|
||||
@@ -7982,25 +8019,63 @@ def _on_command_end(sender, e):
|
||||
if m.get("type") not in SOURCE_TYPES: continue
|
||||
old = sources_snap.get(m["id"])
|
||||
if old is None: continue
|
||||
d = _source_delta(obj, old)
|
||||
if d is None:
|
||||
# End-Grip-Drag o.ae. → kein Pure-Translate
|
||||
t = _source_rigid_transform(obj, old)
|
||||
if t is None:
|
||||
abort_pure = True
|
||||
break
|
||||
deltas[m["id"]] = d
|
||||
source_transforms[m["id"]] = t
|
||||
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}
|
||||
# Phase 2: moved_ids + canonical (bevorzugt Curve-Source fuer
|
||||
# Rotations-Info; Points haben nur Translation)
|
||||
moved_ids = {eid for eid, t in source_transforms.items()
|
||||
if not _is_identity_transform(t)}
|
||||
canonical = None
|
||||
for eid in moved_ids:
|
||||
old = sources_snap.get(eid)
|
||||
if old and "start" in old:
|
||||
canonical = source_transforms[eid]
|
||||
break
|
||||
if canonical is None and moved_ids:
|
||||
# Keine Curve bewegt → nimm irgendeinen Point-Transform
|
||||
for eid in moved_ids:
|
||||
canonical = source_transforms[eid]
|
||||
break
|
||||
|
||||
# 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.
|
||||
# Phase 3: alle bewegten Sources MUESSEN canonical erfuellen
|
||||
all_consistent = True
|
||||
if canonical is not None and not abort_pure:
|
||||
for eid in moved_ids:
|
||||
old = sources_snap.get(eid)
|
||||
if old is None: continue
|
||||
if "start" in old:
|
||||
# Curve: Transform muss canonical sein
|
||||
if not _transforms_equal(source_transforms[eid], canonical):
|
||||
all_consistent = False
|
||||
break
|
||||
else:
|
||||
# Point: canonical applied to old_pos muss aktuelle Position sein
|
||||
op = old.get("pos")
|
||||
if op is None: continue
|
||||
expected = rg.Point3d(op[0], op[1], op[2])
|
||||
expected.Transform(canonical)
|
||||
actual = None
|
||||
for obj in doc.Objects:
|
||||
mm = _read_meta(obj)
|
||||
if mm and mm.get("id") == eid and mm.get("type") == old.get("type"):
|
||||
gg = obj.Geometry
|
||||
if hasattr(gg, "Location"):
|
||||
actual = gg.Location
|
||||
break
|
||||
if actual is None: continue
|
||||
if (abs(actual.X - expected.X) > 1e-6 or
|
||||
abs(actual.Y - expected.Y) > 1e-6 or
|
||||
abs(actual.Z - expected.Z) > 1e-6):
|
||||
all_consistent = False
|
||||
break
|
||||
|
||||
# Orphan-Oeffnung erkennen: bewegte Oeffnung deren Eltern-Wand NICHT
|
||||
# mitbewegt wurde. Cutout muss regen.
|
||||
orphan_opening = False
|
||||
for eid in moved_ids:
|
||||
old = sources_snap.get(eid)
|
||||
@@ -8010,49 +8085,38 @@ def _on_command_end(sender, e):
|
||||
orphan_opening = True
|
||||
break
|
||||
|
||||
pure_delta = None
|
||||
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]
|
||||
same = all(abs(d[0]-first[0]) < 1e-6 and
|
||||
abs(d[1]-first[1]) < 1e-6 and
|
||||
abs(d[2]-first[2]) < 1e-6 for d in moved)
|
||||
# Z-Drag erkennen: wenn Wand-Achse Z aenderung hat → Brüstungs-
|
||||
# Mitnahme noetig → kein Pure-Translate.
|
||||
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")
|
||||
pure_transform = None
|
||||
if abort_pure:
|
||||
print("[ELEMENTE] no pure-transform: z-drag/scale/end-grip detected")
|
||||
elif orphan_opening:
|
||||
print("[ELEMENTE] no pure-translate: opening moved without parent wall (cutout muss regen)")
|
||||
print("[ELEMENTE] no pure-transform: opening moved without parent wall (cutout muss regen)")
|
||||
elif not all_consistent:
|
||||
print("[ELEMENTE] no pure-transform: sources moved with different transforms")
|
||||
elif canonical is not None:
|
||||
pure_transform = canonical
|
||||
|
||||
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)
|
||||
if pure_transform is not None:
|
||||
# PURE-TRANSFORM PFAD: Transform auf alle Geometries anwenden die
|
||||
# nicht schon vom User-Move transformed wurden. Funktioniert fuer
|
||||
# Translation UND Rotation. → instant feedback.
|
||||
tx = pure_transform[0, 3]
|
||||
ty = pure_transform[1, 3]
|
||||
tz = pure_transform[2, 3]
|
||||
# Rotations-Anteil aus m00/m01 (Z-Rotation der 2x2 oberen Submatrix)
|
||||
rot_deg = _math.degrees(_math.atan2(pure_transform[1, 0], pure_transform[0, 0]))
|
||||
print("[ELEMENTE] pure-transform: tx={:.3f} ty={:.3f} tz={:.3f} rot={:.1f}°".format(
|
||||
tx, ty, tz, rot_deg))
|
||||
|
||||
# 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.
|
||||
# Eltern→Kind-Cascade: nur bewegte Sources + deren Children folgen.
|
||||
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
|
||||
# RedrawEnabled wurde schon in _on_command_begin auf False gesetzt
|
||||
try:
|
||||
for obj in list(doc.Objects):
|
||||
try:
|
||||
@@ -8060,16 +8124,18 @@ def _on_command_end(sender, e):
|
||||
if not m: continue
|
||||
t = m.get("type")
|
||||
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:
|
||||
# Sources die nicht bewegt wurden (= identity transform)
|
||||
# transformen — nur via _should_follow erlaubt (Cascade).
|
||||
if t in SOURCE_TYPES:
|
||||
src_t = source_transforms.get(m["id"])
|
||||
if src_t is not None and not _is_identity_transform(src_t):
|
||||
continue # Rhino hat bereits transformed
|
||||
new_geom = obj.Geometry.Duplicate()
|
||||
new_geom.Translate(vec)
|
||||
new_geom.Transform(pure_transform)
|
||||
doc.Objects.Replace(obj.Id, new_geom)
|
||||
continue
|
||||
# Volumes: vergleiche Position vs. Snapshot. Wenn unbewegt,
|
||||
# translaten. Wenn bereits transformed (durch Multi-Select-
|
||||
# Move), skip.
|
||||
# Volumes: bb-Center gegen Snapshot vergleichen. Unbewegt
|
||||
# → transformen. Bereits transformed (Rhino) → skip.
|
||||
if t in VOLUME_TYPES:
|
||||
vol_snap = volumes_snap.get(str(obj.Id))
|
||||
if vol_snap is None: continue
|
||||
@@ -8082,13 +8148,12 @@ def _on_command_end(sender, e):
|
||||
dy = c_now.Y - c_old[1]
|
||||
dz = c_now.Z - c_old[2]
|
||||
if (abs(dx) < 1e-6 and abs(dy) < 1e-6 and abs(dz) < 1e-6):
|
||||
# noch nicht transformed → translaten
|
||||
new_geom = obj.Geometry.Duplicate()
|
||||
new_geom.Translate(vec)
|
||||
new_geom.Transform(pure_transform)
|
||||
doc.Objects.Replace(obj.Id, new_geom)
|
||||
except Exception: pass
|
||||
except Exception as ex:
|
||||
print("[ELEMENTE] pure-translate:", ex)
|
||||
print("[ELEMENTE] pure-transform:", ex)
|
||||
finally:
|
||||
sc.sticky[_REGEN_BUSY] = _was_busy
|
||||
doc.Views.RedrawEnabled = prev_redraw_enabled
|
||||
|
||||
Reference in New Issue
Block a user