Perf: Pure-Translate-Skip — Wand+Tür+Fenster gemeinsam = instant

Im CommandEnd-Batch wird jetzt zuerst auf pure-translate gecheckt:
- Alle bewegten Source-Geometrien haben EXAKT denselben Delta-Vektor
- Beide Endpunkte einer Wand-Achse haben gleichen Vektor (= kein End-Grip-
  Drag, kein Rotate/Scale)
- Keine Z-Komponente in den Deltas (= kein Brüstungs-Property-Change)

Wenn alle Bedingungen erfüllt: REINE TRANSLATION aller Geometrien um den
Delta-Vektor. Sub-Volumen die schon von Rhinos Multi-Select-Move
transformed wurden (BBox-Center verschoben) werden geskippt; nur die
unbewegten kriegen Translate(vec). KEIN Wand-Regen, KEIN Boolean-Diff →
instant.

Snapshot um Volume-BBox-Centers erweitert für die Same-Position-
Detection. Bei Property-Änderung (Brüstung/Höhe/Breite/Rotate) fällt's
automatisch auf den vollen Regen-Pfad zurück.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 14:58:20 +02:00
parent 8a67b9f9d1
commit d3984ba501
+136 -9
View File
@@ -7693,27 +7693,37 @@ _UT_SNAPSHOT_KEY = "_dossier_pre_transform_snapshot"
def _snapshot_source_positions(doc): def _snapshot_source_positions(doc):
"""Schnappschuss aller Source-Geometrien — gerade vor einem User-Transform. """Schnappschuss vor einem User-Transform: Source-Geometrien + Volume-
Wird in _on_command_end gegen aktuelle Positionen verglichen, um Brüstung- BBox-Centers. Source-Map (key=element_id) füttert Constraint+Migrate.
Mitnahme + Migration zu rechnen ohne mit Rhinos noch laufender Move- Volume-Map (key=obj.Id-string) erlaubt im CommandEnd die Pure-Translate-
Operation zu kollidieren.""" Detection wir checken pro Volume ob es schon vom Rhinos Move
snap = {} transformed wurde, oder noch ge-translaten werden muss."""
snap = {"sources": {}, "volumes": {}}
if doc is None: return snap if doc is None: return snap
for obj in doc.Objects: for obj in doc.Objects:
try: try:
m = _read_meta(obj) m = _read_meta(obj)
if not m: continue if not m: continue
t = m.get("type") t = m.get("type")
if t not in SOURCE_TYPES: continue
geom = obj.Geometry geom = obj.Geometry
if t in SOURCE_TYPES:
if hasattr(geom, "Location"): if hasattr(geom, "Location"):
p = geom.Location p = geom.Location
snap[m["id"]] = {"type": t, "pos": (p.X, p.Y, p.Z)} snap["sources"][m["id"]] = {"type": t, "pos": (p.X, p.Y, p.Z)}
elif isinstance(geom, rg.Curve): elif isinstance(geom, rg.Curve):
s = geom.PointAtStart; e = geom.PointAtEnd s = geom.PointAtStart; e = geom.PointAtEnd
snap[m["id"]] = {"type": t, snap["sources"][m["id"]] = {"type": t,
"start": (s.X, s.Y, s.Z), "start": (s.X, s.Y, s.Z),
"end": (e.X, e.Y, e.Z)} "end": (e.X, e.Y, e.Z)}
elif t in VOLUME_TYPES:
try:
bb = geom.GetBoundingBox(True)
if bb.IsValid:
c = bb.Center
snap["volumes"][str(obj.Id)] = {
"element_id": m["id"], "type": t,
"center": (c.X, c.Y, c.Z)}
except Exception: pass
except Exception: pass except Exception: pass
return snap return snap
@@ -7738,6 +7748,123 @@ def _on_command_end(sender, e):
doc = Rhino.RhinoDoc.ActiveDoc doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return if doc is None: return
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):
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):
return None
return ds
return None
deltas = {}
abort_pure = False
for obj in doc.Objects:
try:
m = _read_meta(obj)
if not m: continue
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
abort_pure = True
break
deltas[m["id"]] = d
except Exception: pass
pure_delta = None
if not abort_pure:
moved = [d for d in deltas.values()
if abs(d[0]) > 1e-6 or abs(d[1]) > 1e-6 or abs(d[2]) > 1e-6]
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
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.
vec = rg.Vector3d(*pure_delta)
_was_busy = sc.sticky.get(_REGEN_BUSY, False)
sc.sticky[_REGEN_BUSY] = True
prev_redraw = doc.Views.RedrawEnabled
doc.Views.RedrawEnabled = False
try:
for obj in list(doc.Objects):
try:
m = _read_meta(obj)
if not m: continue
t = m.get("type")
# Sources die nicht in deltas waren (= unbewegt) auch translaten
if t in SOURCE_TYPES and m["id"] not in deltas:
new_geom = obj.Geometry.Duplicate()
new_geom.Translate(vec)
doc.Objects.Replace(obj.Id, new_geom)
continue
# Volumes: vergleiche Position vs. Snapshot. Wenn unbewegt,
# translaten. Wenn bereits transformed (durch Multi-Select-
# Move), skip.
if t in VOLUME_TYPES:
vol_snap = volumes_snap.get(str(obj.Id))
if vol_snap is None: continue
try:
bb = obj.Geometry.GetBoundingBox(True)
if not bb.IsValid: continue
c_now = bb.Center
c_old = vol_snap["center"]
dx = c_now.X - c_old[0]
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)
doc.Objects.Replace(obj.Id, new_geom)
except Exception: pass
except Exception as ex:
print("[ELEMENTE] pure-translate:", ex)
finally:
doc.Views.RedrawEnabled = prev_redraw
sc.sticky[_REGEN_BUSY] = _was_busy
try: doc.Views.Redraw()
except Exception: pass
b = sc.sticky.get("elemente_bridge")
if b is not None:
try: b._send_state()
except Exception: pass
return
# ─── Regulärer Pfad: Constraints + Migrate + Regen (existing flow) ──────
# Pseudo-Object Wrapper damit _apply_oeffnung_constraint pt_old.Location # Pseudo-Object Wrapper damit _apply_oeffnung_constraint pt_old.Location
# lesen kann ohne den echten alten RhinoObject zu kennen. # lesen kann ohne den echten alten RhinoObject zu kennen.
class _PseudoOld(object): class _PseudoOld(object):
@@ -7762,7 +7889,7 @@ def _on_command_end(sender, e):
if not m: continue if not m: continue
t = m.get("type") t = m.get("type")
if t not in SOURCE_TYPES: continue if t not in SOURCE_TYPES: continue
old = snapshot.get(m["id"]) old = sources_snap.get(m["id"])
if old is None: continue if old is None: continue
if t == "wand_axis": if t == "wand_axis":
geom = obj.Geometry geom = obj.Geometry