diff --git a/rhino/elemente.py b/rhino/elemente.py index ead9569..409c0be 100644 --- a/rhino/elemente.py +++ b/rhino/elemente.py @@ -4021,14 +4021,28 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name layer_breps = new_layer_breps # Oeffnungs-Sub-Volumina (Rahmen+Sims+Glas) erzeugen. + # Nicht-destruktiv wenn moeglich: wenn die Anzahl der Sub-Volumen gleich + # bleibt (z.B. bei Bruestung-/Hoehe-/XY-Aenderung), nutzen wir + # `doc.Objects.Replace` auf die existierenden IDs statt Delete+AddBrep. + # Damit kollidiert ein laufender `_Move`-Command nicht mehr mit dem + # Wand-Regen → kein „Unable to transform"-Fehler mehr. Bei Anzahl- + # Aenderung (z.B. Fluegel-Wechsel) Fallback auf Delete+Add. op_layer = _ensure_layer(doc, _layer_path_volume(doc, geschoss_name)) for op_meta, pt_loc, op_uk in opening_jobs: - for o, _m in _find_objects_by_wall_id(doc, op_meta["id"], - "oeffnung_volume"): - try: doc.Objects.Delete(o.Id, True) - except Exception as ex: print("[ELEMENTE] del old oeff vol:", ex) + old_objs = list(_find_objects_by_wall_id(doc, op_meta["id"], + "oeffnung_volume")) pieces = _make_oeffnung_pieces(geom, pt_loc, meta["dicke"], op_meta, op_uk) + if len(old_objs) == len(pieces) and len(pieces) > 0: + for (old_obj, _old_meta), pbrep in zip(old_objs, pieces): + try: doc.Objects.Replace(old_obj.Id, pbrep) + except Exception as ex: + print("[ELEMENTE] replace oeff vol:", ex) + continue + # Fallback: Anzahl hat sich geaendert → alte loeschen + neue adden. + for o, _m in old_objs: + try: doc.Objects.Delete(o.Id, True) + except Exception as ex: print("[ELEMENTE] del old oeff vol:", ex) for pbrep in pieces: op_attrs = Rhino.DocObjects.ObjectAttributes() op_attrs.LayerIndex = op_layer @@ -6860,16 +6874,10 @@ def _apply_oeffnung_constraint(new_obj, meta, old_obj=None): attrs.SetUserString(_KEY_OEFF_BRUEST, "{:.6f}".format(new_bruest)) doc.Objects.ModifyAttributes(current.Id, attrs, True) if doc is not None and parent_id: - # Defer-Regel: NUR Tueren im User-`_Move` brauchen Defer (Sync kollidiert - # mit Rhinos Move-Operation → „Unable to transform"). Fenster und Grip- - # Drag laufen sync — bei diesen tritt der Konflikt nicht auf und der - # Sync-Regen gibt sofortiges Feedback ohne sichtbares Aufladen. - skip_ids = sc.sticky.get("_elemente_replace_selected_ids") or set() - is_user_move = str(new_obj.Id) in skip_ids - is_tuer = (meta.get("oeff_typ") == "tuer") - if is_user_move and is_tuer: - _queue_regen(parent_id) - else: + # Skip Sync-Regen wenn wir gerade in einer Batch-Verarbeitung sind + # (Command-End): dort macht der Caller EINEN Sync-Regen pro Wand + # am Schluss → spart Mehrfach-Regen bei mehreren Öffnungen pro Wand. + if not sc.sticky.get("_dossier_skip_sync_regen"): try: _regenerate_element(doc, parent_id) except Exception as ex: print("[ELEMENTE] sync regen oeffnung:", ex) @@ -6886,6 +6894,11 @@ def _on_object_replaced(sender, e): die regenerierten Volumen liegen. """ if sc.sticky.get(_REGEN_BUSY): return + # Wenn ein User-Transform-Command (_Move/_Rotate/etc.) aktiv ist: GAR + # NICHTS hier tun. Rhinos Move soll konfliktfrei durchlaufen. Nach + # CommandEnd vergleichen wir Snapshot vs. aktuellen State + machen den + # ganzen Update in einem konfliktfreien Batch. + if sc.sticky.get(_UT_ACTIVE_KEY): return doc = Rhino.RhinoDoc.ActiveDoc # Snapshot der aktuell selektierten IDs — damit Migrate die Objekte # skippen kann die Rhinos Move/Rotate gerade transformiert (sonst @@ -7683,6 +7696,175 @@ def _on_idle_selection(sender, e): pass +# Welche Rhino-Commands transformieren mehrere Objekte gleichzeitig — bei +# diesen lassen wir Rhinos Move/Rotate KOMPLETT durchlaufen und feuern den +# Wand-Regen erst NACH CommandEnd. So gibt's keine „Unable to transform"- +# Kollision mehr zwischen meinem Sync-Regen und Rhinos pending Transforms. +_USER_TRANSFORM_CMDS = frozenset(( + "Move", "Rotate", "Rotate3D", "Mirror", "Scale", "Scale1D", "Scale2D", + "Drag", "Gumball", "Orient", "Orient3Pt", "RemapCPlane", "Transform", +)) + +_UT_ACTIVE_KEY = "_dossier_user_transform_active" +_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 = {} + 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)} + except Exception: pass + return snap + + +def _on_command_begin(sender, e): + try: + name = getattr(e, "CommandEnglishName", "") or "" + except Exception: name = "" + if name not in _USER_TRANSFORM_CMDS: return + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: return + sc.sticky[_UT_SNAPSHOT_KEY] = _snapshot_source_positions(doc) + sc.sticky[_UT_ACTIVE_KEY] = name + + +def _on_command_end(sender, e): + name = sc.sticky.get(_UT_ACTIVE_KEY) + if not name: return + sc.sticky[_UT_ACTIVE_KEY] = None + snapshot = sc.sticky.get(_UT_SNAPSHOT_KEY) or {} + sc.sticky[_UT_SNAPSHOT_KEY] = None + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: return + + # Pseudo-Object Wrapper damit _apply_oeffnung_constraint pt_old.Location + # lesen kann ohne den echten alten RhinoObject zu kennen. + class _PseudoOld(object): + def __init__(self, pt): self.Geometry = rg.Point(pt) + + affected_walls = set() + _was_busy = sc.sticky.get(_REGEN_BUSY, False) + sc.sticky[_REGEN_BUSY] = True + # Skip-Flag: in der Schleife wuerde jeder Constraint einen eigenen Sync- + # Regen ausloesen → mehrere Regens pro Wand. Wir machen am Schluss EINEN + # Regen pro affected_wall — viel schneller bei mehreren Oeffnungen. + sc.sticky["_dossier_skip_sync_regen"] = True + # Display-Updates komplett suppressen waehrend der Batch — Rhino zeichnet + # sonst nach jedem Brep-Add/Replace neu, was bei mehreren Sub-Volumen + # sichtbares „Aufbauen" verursacht. Ein einziger Redraw am Ende reicht. + 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") + if t not in SOURCE_TYPES: continue + old = snapshot.get(m["id"]) + if old is None: continue + if t == "wand_axis": + geom = obj.Geometry + if not isinstance(geom, rg.Curve): continue + os = old.get("start"); oe = old.get("end") + # Migrate NUR wenn XY tatsaechlich geaendert. Bei reinem + # Z-Drag (XY identisch) waere Migrate ein no-op-Loop ueber + # alle Oeffnungen mit Replace-Ops je Punkt — spart die + # ganze Pass + die nachfolgenden Replace-Events. + if os and oe: + xy_changed = ( + abs(geom.PointAtStart.X - os[0]) > 1e-6 or + abs(geom.PointAtStart.Y - os[1]) > 1e-6 or + abs(geom.PointAtEnd.X - oe[0]) > 1e-6 or + abs(geom.PointAtEnd.Y - oe[1]) > 1e-6 + ) + if xy_changed: + try: + 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) + except Exception as ex: + print("[ELEMENTE] post-cmd migrate:", ex) + # Z-Drag detect + Brüstungs-Mitnahme (= setzt sticky-delta + # den der Idle-Pfad spaeter applied — aber wir koennen + # gleich hier syncen) + _apply_wand_z_drag_constraint(obj, m) + z_entry = sc.sticky.get("_elemente_wand_z_delta") + if isinstance(z_entry, tuple) and len(z_entry) == 2 \ + and z_entry[0] == m["id"]: + z_delta = float(z_entry[1]) + sc.sticky["_elemente_wand_z_delta"] = None + # Brüstungen aller Öffnungen mit anpassen + if abs(z_delta) >= 1e-6: + for op_obj, op_meta in _find_openings_for_wall(doc, m["id"]): + cur_b = op_meta.get("oeff_brueest") + try: + cur_b_val = float(cur_b) if cur_b not in (None, "") else 0.0 + except (ValueError, TypeError): cur_b_val = 0.0 + new_b = max(0.0, cur_b_val + z_delta) + try: + attrs = op_obj.Attributes.Duplicate() + attrs.SetUserString(_KEY_OEFF_BRUEST, + "{:.6f}".format(new_b)) + doc.Objects.ModifyAttributes(op_obj.Id, attrs, True) + pt_geom = op_obj.Geometry + if hasattr(pt_geom, "Location"): + pt = pt_geom.Location + doc.Objects.Replace(op_obj.Id, + rg.Point(rg.Point3d(pt.X, pt.Y, new_b))) + except Exception as ex: + print("[ELEMENTE] post-cmd brueest:", ex) + affected_walls.add(m["id"]) + elif t == "oeffnung_point": + op_pos = old.get("pos") + if op_pos is None: continue + pseudo = _PseudoOld(rg.Point3d(op_pos[0], op_pos[1], op_pos[2])) + _apply_oeffnung_constraint(obj, m, pseudo) + pid = m.get("oeff_parent") + if pid: affected_walls.add(pid) + except Exception as ex: + print("[ELEMENTE] post-cmd source:", ex) + finally: + sc.sticky[_REGEN_BUSY] = _was_busy + sc.sticky["_dossier_skip_sync_regen"] = None + + # Sync-Regen aller betroffenen Wände — Move ist sauber abgeschlossen, + # kein Konflikt mehr moeglich. EIN Regen pro Wand (nicht pro Oeffnung). + # Display bleibt suppressed bis ALLE Wände durch sind → kein „Aufbauen". + try: + for wid in affected_walls: + try: _regenerate_element(doc, wid) + except Exception as ex: + print("[ELEMENTE] post-cmd regen:", ex) + finally: + doc.Views.RedrawEnabled = prev_redraw + 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 + + def _install_listeners(bridge): """Listener-Registrierung mit Re-Reload-Schutz. @@ -7722,21 +7904,31 @@ def _install_listeners(bridge): try: if old_refs.get("idle"): Rhino.RhinoApp.Idle -= old_refs["idle"] except Exception: pass + try: + if old_refs.get("cmd_begin"): Rhino.Commands.Command.BeginCommand -= old_refs["cmd_begin"] + except Exception: pass + try: + if old_refs.get("cmd_end"): Rhino.Commands.Command.EndCommand -= old_refs["cmd_end"] + except Exception: pass Rhino.RhinoDoc.ReplaceRhinoObject += _on_object_replaced Rhino.RhinoDoc.AddRhinoObject += _on_object_added Rhino.RhinoDoc.DeleteRhinoObject += _on_object_deleted Rhino.RhinoDoc.SelectObjects += _on_select_objects Rhino.RhinoDoc.DeselectObjects += _on_deselect_objects Rhino.RhinoApp.Idle += _on_idle_selection + Rhino.Commands.Command.BeginCommand += _on_command_begin + Rhino.Commands.Command.EndCommand += _on_command_end sc.sticky[refs_key] = { - "replace": _on_object_replaced, - "add": _on_object_added, - "delete": _on_object_deleted, - "select": _on_select_objects, - "deselect": _on_deselect_objects, - "idle": _on_idle_selection, + "replace": _on_object_replaced, + "add": _on_object_added, + "delete": _on_object_deleted, + "select": _on_select_objects, + "deselect": _on_deselect_objects, + "idle": _on_idle_selection, + "cmd_begin": _on_command_begin, + "cmd_end": _on_command_end, } - print("[ELEMENTE] Listener aktiv (Replace + Add + Delete + Select + Idle)") + print("[ELEMENTE] Listener aktiv (Replace + Add + Delete + Select + Idle + Cmd)") def _bridge_factory(): diff --git a/rhino/gestaltung.py b/rhino/gestaltung.py index 6216e9b..e9cbdef 100644 --- a/rhino/gestaltung.py +++ b/rhino/gestaltung.py @@ -1374,11 +1374,47 @@ def _install_selection_listener(bridge): except Exception: pass def on_replace(sender, args): - """Hatch der zugehoerigen Curve mitziehen wenn Curve veraendert wird.""" + """Sync Curve↔Hatch bei Move/Replace: + - Curve hat _FILL_KEY (= hatch_id) → Hatch via Hatch.Create neu auf die + aktuelle Curve aufsetzen (existierender Pfad). + - Hatch hat _FILL_OWNER_KEY (= curve_id) → Curve um den gleichen + Vektor mit-translaten (User hat Hatch alleine verschoben). + """ new_obj = args.NewRhinoObject if new_obj is None or new_obj.Id in _processing: return a = new_obj.Attributes + # Reverse-Direction: Hatch verschoben/rotiert/skaliert → Curve mitnehmen. + # Wir nehmen die Outer-Boundary direkt aus der (bereits transformed) + # Hatch — funktioniert fuer Move, Rotate, Scale, beliebige Transforms. + if isinstance(new_obj.Geometry, rg.Hatch): + owner_id_str = a.GetUserString(_FILL_OWNER_KEY) + if not owner_id_str: + return + try: + owner_id = System.Guid(owner_id_str) + except Exception: + return + doc2 = Rhino.RhinoDoc.ActiveDoc + owner_obj = doc2.Objects.FindId(owner_id) + if owner_obj is None or owner_obj.IsDeleted: + return + try: + new_curves = new_obj.Geometry.Get3dCurves(True) + except Exception as ex: + print("[GESTALTUNG] hatch.Get3dCurves:", ex) + return + if not new_curves or len(new_curves) == 0: + return + new_curve = new_curves[0] + _processing.add(owner_id) + try: + doc2.Objects.Replace(owner_id, new_curve) + except Exception as ex: + print("[GESTALTUNG] hatch→curve replace:", ex) + finally: + _processing.discard(owner_id) + return hatch_id_str = a.GetUserString(_FILL_KEY) if not hatch_id_str: return