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 {}
|
sources_snap = snapshot.get("sources", {}) if isinstance(snapshot, dict) else {}
|
||||||
volumes_snap = snapshot.get("volumes", {}) if isinstance(snapshot, dict) else {}
|
volumes_snap = snapshot.get("volumes", {}) if isinstance(snapshot, dict) else {}
|
||||||
|
|
||||||
# ─── Pure-Translate-Detection ───────────────────────────────────────────
|
# ─── Pure-Transform-Detection (Translation + Z-Rotation) ────────────────
|
||||||
# Wenn ALLE bewegten Sources um den exakt gleichen Vektor verschoben
|
# Wenn ALLE bewegten Sources sich mit dem gleichen Rigid-2D-Transform
|
||||||
# wurden, KEIN Z-Drag (= Property-Aenderung), KEIN Rotate/Scale (= End-
|
# abbilden lassen (Translation und/oder Rotation um Z-Achse, KEIN Scale,
|
||||||
# Grip-Drag) — dann reicht eine reine Translation aller noch unbewegten
|
# KEIN Z-Drag, KEIN End-Grip-Drag, KEIN Mirror), reicht eine Transform-
|
||||||
# Volumen + Punkte. KEIN Wand-Regen, KEIN Boolean-Diff. Geht instant.
|
# Anwendung auf alle noch unbewegten Volumen + Punkte. KEIN Wand-Regen,
|
||||||
def _source_delta(obj, old):
|
# 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
|
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):
|
if isinstance(geom, rg.Curve):
|
||||||
os_pt = old.get("start"); oe_pt = old.get("end")
|
os_pt = old.get("start"); oe_pt = old.get("end")
|
||||||
if os_pt is None or oe_pt is None: return None
|
if os_pt is None or oe_pt is None: return None
|
||||||
ns = geom.PointAtStart; ne = geom.PointAtEnd
|
ns = geom.PointAtStart; ne = geom.PointAtEnd
|
||||||
ds = (ns.X - os_pt[0], ns.Y - os_pt[1], ns.Z - os_pt[2])
|
# Z-Aenderung verbietet Pure-Transform (= Z-Drag → UK_OVER muss
|
||||||
de = (ne.X - oe_pt[0], ne.Y - oe_pt[1], ne.Z - oe_pt[2])
|
# geschrieben werden → Regen-Pfad).
|
||||||
# Beide Endpunkte muessen den gleichen Vektor haben (= Translate).
|
if (abs(ns.Z - os_pt[2]) > 1e-6 or
|
||||||
# Sonst ist's Rotate/Scale/End-Grip-Drag.
|
abs(ne.Z - oe_pt[2]) > 1e-6):
|
||||||
if (abs(ds[0]-de[0]) > 1e-6 or abs(ds[1]-de[1]) > 1e-6
|
|
||||||
or abs(ds[2]-de[2]) > 1e-6):
|
|
||||||
return None
|
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
|
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
|
abort_pure = False
|
||||||
for obj in doc.Objects:
|
for obj in doc.Objects:
|
||||||
try:
|
try:
|
||||||
@@ -7982,25 +8019,63 @@ def _on_command_end(sender, e):
|
|||||||
if m.get("type") not in SOURCE_TYPES: continue
|
if m.get("type") not in SOURCE_TYPES: continue
|
||||||
old = sources_snap.get(m["id"])
|
old = sources_snap.get(m["id"])
|
||||||
if old is None: continue
|
if old is None: continue
|
||||||
d = _source_delta(obj, old)
|
t = _source_rigid_transform(obj, old)
|
||||||
if d is None:
|
if t is None:
|
||||||
# End-Grip-Drag o.ae. → kein Pure-Translate
|
|
||||||
abort_pure = True
|
abort_pure = True
|
||||||
break
|
break
|
||||||
deltas[m["id"]] = d
|
source_transforms[m["id"]] = t
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
|
|
||||||
# IDs der tatsaechlich bewegten Sources (Delta != 0). `deltas` enthaelt
|
# Phase 2: moved_ids + canonical (bevorzugt Curve-Source fuer
|
||||||
# ALLE Sources der Szene mit ihrem Snapshot-Delta — unbewegte haben
|
# Rotations-Info; Points haben nur Translation)
|
||||||
# Delta=(0,0,0). Wir brauchen aber die *bewegten*, um Hierarchie-
|
moved_ids = {eid for eid, t in source_transforms.items()
|
||||||
# Entscheidungen zu treffen (Eltern→Kind-Cascade).
|
if not _is_identity_transform(t)}
|
||||||
moved_ids = {eid for eid, d in deltas.items()
|
canonical = None
|
||||||
if abs(d[0]) > 1e-6 or abs(d[1]) > 1e-6 or abs(d[2]) > 1e-6}
|
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
|
# Phase 3: alle bewegten Sources MUESSEN canonical erfuellen
|
||||||
# mitbewegt wurde. In diesem Fall ist Pure-Translate verboten — die
|
all_consistent = True
|
||||||
# Wand-Aussparung (Boolean-Difference-Loch im Wand-Brep) bliebe am alten
|
if canonical is not None and not abort_pure:
|
||||||
# Ort. Nur ein voller Wand-Regen rebuildet das Loch an der neuen Pos.
|
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
|
orphan_opening = False
|
||||||
for eid in moved_ids:
|
for eid in moved_ids:
|
||||||
old = sources_snap.get(eid)
|
old = sources_snap.get(eid)
|
||||||
@@ -8010,49 +8085,38 @@ def _on_command_end(sender, e):
|
|||||||
orphan_opening = True
|
orphan_opening = True
|
||||||
break
|
break
|
||||||
|
|
||||||
pure_delta = None
|
pure_transform = None
|
||||||
if not abort_pure and not orphan_opening:
|
if abort_pure:
|
||||||
moved = [d for eid, d in deltas.items() if eid in moved_ids]
|
print("[ELEMENTE] no pure-transform: z-drag/scale/end-grip detected")
|
||||||
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")
|
|
||||||
elif orphan_opening:
|
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:
|
if pure_transform is not None:
|
||||||
# PURE-TRANSLATE PFAD: nur Geometries translaten die nicht schon vom
|
# PURE-TRANSFORM PFAD: Transform auf alle Geometries anwenden die
|
||||||
# User-Move transformed wurden. Keine Brep-Regeneration, kein
|
# nicht schon vom User-Move transformed wurden. Funktioniert fuer
|
||||||
# Boolean-Diff. → instant feedback.
|
# Translation UND Rotation. → instant feedback.
|
||||||
print("[ELEMENTE] pure-translate: delta=({:.3f}, {:.3f}, {:.3f})".format(*pure_delta))
|
tx = pure_transform[0, 3]
|
||||||
vec = rg.Vector3d(*pure_delta)
|
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
|
# Eltern→Kind-Cascade: nur bewegte Sources + deren Children folgen.
|
||||||
# Oeffnungen mit. Aber NICHT umgekehrt: wenn nur eine Oeffnung
|
|
||||||
# bewegt wird, bleibt die Wand stehen.
|
|
||||||
def _should_follow(m):
|
def _should_follow(m):
|
||||||
eid = m.get("id")
|
eid = m.get("id")
|
||||||
if eid in moved_ids: return True
|
if eid in moved_ids: return True
|
||||||
# Oeffnung ihrer Eltern-Wand folgen lassen
|
|
||||||
parent = m.get("oeff_parent")
|
parent = m.get("oeff_parent")
|
||||||
if parent and parent in moved_ids: return True
|
if parent and parent in moved_ids: return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
_was_busy = sc.sticky.get(_REGEN_BUSY, False)
|
_was_busy = sc.sticky.get(_REGEN_BUSY, False)
|
||||||
sc.sticky[_REGEN_BUSY] = True
|
sc.sticky[_REGEN_BUSY] = True
|
||||||
# RedrawEnabled wurde schon in _on_command_begin auf False gesetzt
|
|
||||||
try:
|
try:
|
||||||
for obj in list(doc.Objects):
|
for obj in list(doc.Objects):
|
||||||
try:
|
try:
|
||||||
@@ -8060,16 +8124,18 @@ def _on_command_end(sender, e):
|
|||||||
if not m: continue
|
if not m: continue
|
||||||
t = m.get("type")
|
t = m.get("type")
|
||||||
if not _should_follow(m): continue
|
if not _should_follow(m): continue
|
||||||
# Sources die nicht bewegt wurden (= Delta=0) translaten
|
# Sources die nicht bewegt wurden (= identity transform)
|
||||||
# — nur Oeffnungs-Sources via _should_follow erlaubt.
|
# transformen — nur via _should_follow erlaubt (Cascade).
|
||||||
if t in SOURCE_TYPES and m["id"] not in moved_ids:
|
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 = obj.Geometry.Duplicate()
|
||||||
new_geom.Translate(vec)
|
new_geom.Transform(pure_transform)
|
||||||
doc.Objects.Replace(obj.Id, new_geom)
|
doc.Objects.Replace(obj.Id, new_geom)
|
||||||
continue
|
continue
|
||||||
# Volumes: vergleiche Position vs. Snapshot. Wenn unbewegt,
|
# Volumes: bb-Center gegen Snapshot vergleichen. Unbewegt
|
||||||
# translaten. Wenn bereits transformed (durch Multi-Select-
|
# → transformen. Bereits transformed (Rhino) → skip.
|
||||||
# Move), skip.
|
|
||||||
if t in VOLUME_TYPES:
|
if t in VOLUME_TYPES:
|
||||||
vol_snap = volumes_snap.get(str(obj.Id))
|
vol_snap = volumes_snap.get(str(obj.Id))
|
||||||
if vol_snap is None: continue
|
if vol_snap is None: continue
|
||||||
@@ -8082,13 +8148,12 @@ def _on_command_end(sender, e):
|
|||||||
dy = c_now.Y - c_old[1]
|
dy = c_now.Y - c_old[1]
|
||||||
dz = c_now.Z - c_old[2]
|
dz = c_now.Z - c_old[2]
|
||||||
if (abs(dx) < 1e-6 and abs(dy) < 1e-6 and abs(dz) < 1e-6):
|
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 = obj.Geometry.Duplicate()
|
||||||
new_geom.Translate(vec)
|
new_geom.Transform(pure_transform)
|
||||||
doc.Objects.Replace(obj.Id, new_geom)
|
doc.Objects.Replace(obj.Id, new_geom)
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[ELEMENTE] pure-translate:", ex)
|
print("[ELEMENTE] pure-transform:", ex)
|
||||||
finally:
|
finally:
|
||||||
sc.sticky[_REGEN_BUSY] = _was_busy
|
sc.sticky[_REGEN_BUSY] = _was_busy
|
||||||
doc.Views.RedrawEnabled = prev_redraw_enabled
|
doc.Views.RedrawEnabled = prev_redraw_enabled
|
||||||
|
|||||||
Reference in New Issue
Block a user