From d3984ba501f487aafede395cdcd00ea3ca782c21 Mon Sep 17 00:00:00 2001 From: karim Date: Mon, 18 May 2026 14:58:20 +0200 Subject: [PATCH] =?UTF-8?q?Perf:=20Pure-Translate-Skip=20=E2=80=94=20Wand+?= =?UTF-8?q?T=C3=BCr+Fenster=20gemeinsam=20=3D=20instant?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- rhino/elemente.py | 157 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 142 insertions(+), 15 deletions(-) diff --git a/rhino/elemente.py b/rhino/elemente.py index 1d1a1f3..4774ad3 100644 --- a/rhino/elemente.py +++ b/rhino/elemente.py @@ -7693,27 +7693,37 @@ _UT_SNAPSHOT_KEY = "_dossier_pre_transform_snapshot" def _snapshot_source_positions(doc): - """Schnappschuss aller Source-Geometrien — gerade vor einem User-Transform. - Wird in _on_command_end gegen aktuelle Positionen verglichen, um Brüstung- - Mitnahme + Migration zu rechnen ohne mit Rhinos noch laufender Move- - Operation zu kollidieren.""" - snap = {} + """Schnappschuss vor einem User-Transform: Source-Geometrien + Volume- + BBox-Centers. Source-Map (key=element_id) füttert Constraint+Migrate. + Volume-Map (key=obj.Id-string) erlaubt im CommandEnd die Pure-Translate- + Detection — wir checken pro Volume ob es schon vom Rhinos Move + transformed wurde, oder noch ge-translaten werden muss.""" + snap = {"sources": {}, "volumes": {}} if doc is None: return snap for obj in doc.Objects: try: m = _read_meta(obj) if not m: continue t = m.get("type") - if t not in SOURCE_TYPES: continue geom = obj.Geometry - if hasattr(geom, "Location"): - p = geom.Location - snap[m["id"]] = {"type": t, "pos": (p.X, p.Y, p.Z)} - elif isinstance(geom, rg.Curve): - s = geom.PointAtStart; e = geom.PointAtEnd - snap[m["id"]] = {"type": t, - "start": (s.X, s.Y, s.Z), - "end": (e.X, e.Y, e.Z)} + if t in SOURCE_TYPES: + if hasattr(geom, "Location"): + p = geom.Location + snap["sources"][m["id"]] = {"type": t, "pos": (p.X, p.Y, p.Z)} + elif isinstance(geom, rg.Curve): + s = geom.PointAtStart; e = geom.PointAtEnd + snap["sources"][m["id"]] = {"type": t, + "start": (s.X, s.Y, s.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 return snap @@ -7738,6 +7748,123 @@ def _on_command_end(sender, e): doc = Rhino.RhinoDoc.ActiveDoc 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 # lesen kann ohne den echten alten RhinoObject zu kennen. class _PseudoOld(object): @@ -7762,7 +7889,7 @@ def _on_command_end(sender, e): if not m: continue t = m.get("type") if t not in SOURCE_TYPES: continue - old = snapshot.get(m["id"]) + old = sources_snap.get(m["id"]) if old is None: continue if t == "wand_axis": geom = obj.Geometry