From f011e2ca944ba3af242d52a19b80e931daf77bf1 Mon Sep 17 00:00:00 2001 From: karim Date: Sun, 31 May 2026 00:03:10 +0200 Subject: [PATCH] Cascade-Cleanup: wand_centerline + wand_outline beim wand_axis-Delete mit-loeschen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug: nach _Delete einer Wand blieben die Hilfslinien (Centerline, Outline) als Orphan-Curves stehen. _find_all_volumes filtert nur VOLUME_TYPES, in denen Centerline/Outline nicht sind. Fix: bei wand_axis-Delete-Event zusaetzlich alle wand_centerline/wand_outline Curves mit derselben wall_id ID-Liste ergaenzen → nach 500ms cascade-delete raeumt sie mit weg. Layered Cluster + per-Layer BooleanUnion via _build_cluster_layered_breps + erweiterter _regen_cluster_anchor sind im selben commit drin (siehe diff vor diesem Bugfix). --- rhino/elemente.py | 276 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 214 insertions(+), 62 deletions(-) diff --git a/rhino/elemente.py b/rhino/elemente.py index 0f6156c..31d9f8c 100644 --- a/rhino/elemente.py +++ b/rhino/elemente.py @@ -2601,6 +2601,110 @@ def _build_cluster_union_brep(doc, cluster_ids, uk, ok): return result +def _build_cluster_layered_breps(doc, cluster_ids, layers_def, uk, ok): + """Per-Layer-Cluster: fuer jeden Layer-Index die per-Wand Schicht-Breps + sammeln + BooleanUnion. Liefert Liste [(brep, color, name)] — eine pro + Layer-Index, in derselben Reihenfolge wie layers_def. + + Voraussetzung: alle Cluster-Member haben kompatible Layer-Struktur + (= gleich viele Schichten, gleiche dicken; sichergestellt durch + _wand_chain_compat in der Cluster-BFS).""" + if not cluster_ids or not layers_def: return [] + walls = {} + for wid in cluster_ids: + ax = _find_axis(doc, wid) + if ax is None: continue + wm = _read_meta(ax) + if wm is None: continue + g = ax.Geometry + if not isinstance(g, rg.Curve): continue + walls[wid] = (ax, wm, g) + if not walls: return [] + eps = 0.001 + tol = doc.ModelAbsoluteTolerance + # Pre-compute Achse-Extensions pro Wand (gleich fuer alle Layer) + extensions = {} + for wid, (ax, wm, g) in walls.items(): + ext_start = 0.0; ext_end = 0.0 + my_start = g.PointAtStart; my_end = g.PointAtEnd + for nid, (n_ax, n_meta, n_g) in walls.items(): + if nid == wid: continue + n_half = float(n_meta.get("dicke", 0.0)) * 0.5 + n_start = n_g.PointAtStart; n_end = n_g.PointAtEnd + if (my_start.DistanceTo(n_start) < eps or + my_start.DistanceTo(n_end) < eps): + ext_start = max(ext_start, n_half) + if (my_end.DistanceTo(n_start) < eps or + my_end.DistanceTo(n_end) < eps): + ext_end = max(ext_end, n_half) + for my_ep, is_start in ((my_start, True), (my_end, False)): + rc, t = n_g.ClosestPoint(my_ep) + if not rc: continue + if n_g.PointAt(t).DistanceTo(my_ep) > eps: continue + if (n_start.DistanceTo(my_ep) < eps or + n_end.DistanceTo(my_ep) < eps): + continue + if is_start: ext_start = max(ext_start, n_half) + else: ext_end = max(ext_end, n_half) + extensions[wid] = (ext_start, ext_end) + + out = [] + for layer_idx, layer in enumerate(layers_def): + layer_breps = [] + for wid, (ax, wm, g) in walls.items(): + wand_layers = wm.get("wand_layers") or [] + if layer_idx >= len(wand_layers): continue + try: my_dicke = float(wm.get("dicke", 0)) + except Exception: my_dicke = 0 + referenz = wm.get("referenz", "mid") + start_off, _ = _wall_offsets_from_referenz(my_dicke, referenz) + cur = start_off + for prev_l in wand_layers[:layer_idx]: + try: cur -= float(prev_l.get("dicke", 0)) + except Exception: pass + d_left = cur + try: d_right = cur - float(wand_layers[layer_idx].get("dicke", 0)) + except Exception: d_right = cur + ext_start, ext_end = extensions.get(wid, (0.0, 0.0)) + ext_g = _extend_axis_curve(g, ext_start, ext_end) + b = _make_wall_layer_brep(ext_g, d_left, d_right, uk, ok) + if b is not None: + layer_breps.append(b) + color = layer.get("color", "") + name = layer.get("name", "") + if not layer_breps: + out.append((None, color, name)); continue + if len(layer_breps) == 1: + try: layer_breps[0].MergeCoplanarFaces(tol) + except Exception: pass + out.append((layer_breps[0], color, name)); continue + try: + unioned = rg.Brep.CreateBooleanUnion(layer_breps, tol) + except Exception as ex: + print("[ELEMENTE] cluster layered union exc layer={}:".format( + layer_idx), ex) + unioned = None + if not unioned or len(unioned) == 0: + print("[ELEMENTE] cluster layered union empty layer={}".format( + layer_idx)) + out.append((None, color, name)); continue + if len(unioned) == 1: + result = unioned[0] + else: + try: + joined = rg.Brep.JoinBreps(list(unioned), tol) + if joined and len(joined) > 0: + result = max(joined, key=lambda b: b.GetVolume() if b else 0) + else: + result = max(unioned, key=lambda b: b.GetVolume() if b else 0) + except Exception: + result = max(unioned, key=lambda b: b.GetVolume() if b else 0) + try: result.MergeCoplanarFaces(tol) + except Exception: pass + out.append((result, color, name)) + return out + + class _ClusterVolumeSelectHandler(Rhino.UI.MouseCallback): """Mouse-Down auf Wand-Volume: - Klick INNEN (nicht am Rand/Eck) → naechste Achse selektieren @@ -2804,22 +2908,37 @@ def install_cluster_select_handler(): def _regen_cluster_anchor(doc, anchor_id, cluster_ids, anchor_meta): - """Anchor-Build fuer komplexe Multi-Wand-Cluster (same material, verzweigt). - Baut BooleanUnion-Brep aller Cluster-Member-Volumina, subtrahiert - Oeffnungs-Cutouts. Opening-Sub-Pieces (Rahmen/Sims/Glas/Schwung) bleiben - pro-Wand zustaendig — die werden von ihrer eigenen Wand-Regen verwaltet. + """Anchor-Build fuer komplexe Multi-Wand-Cluster. + SOLID-Wand: ein Boolean-Union-Brep ueber alle Cluster-Member. + LAYERED-Wand: pro Layer-Index separater Union-Brep (alle Cluster-Member + haben kompatible Layer-Struktur, sichergestellt durch chain_compat). - Return True bei Erfolg, False bei Fallback-Bedarf (caller faellt dann auf - chain/solo zurueck).""" + Opening-Cutouts pro Member abziehen. Opening-Sub-Pieces (Rahmen/Sims/Glas) + bleiben pro-Wand zustaendig (separate Objekte mit oeff_parent). + + Return True bei Erfolg, False bei Fallback-Bedarf.""" cluster_list = sorted(cluster_ids) uk, ok = _resolve_uk_ok(doc, anchor_meta["geschoss"], anchor_meta["uk_override"], anchor_meta["ok_override"]) - cluster_brep = _build_cluster_union_brep(doc, cluster_list, uk, ok) - if cluster_brep is None: - return False + is_layered = bool(anchor_meta.get("wand_layered", False)) + layers_def = anchor_meta.get("wand_layers") or [] + if is_layered and not layers_def: + is_layered = False # Fallback wenn Layer-Def fehlt - # Openings: cutouts pro Member-Wand sammeln + abziehen + if is_layered: + # Per-Layer-Build via Boolean-Union der per-Wand Schicht-Breps + layer_results = _build_cluster_layered_breps( + doc, cluster_list, layers_def, uk, ok) + if not layer_results or all(b is None for (b, _c, _n) in layer_results): + return False + else: + single = _build_cluster_union_brep(doc, cluster_list, uk, ok) + if single is None: + return False + layer_results = [(single, "", "")] + + # Openings: cutouts pro Member-Wand sammeln (gleich fuer alle Layer) tol = 0.001 cutouts = [] for c_wid in cluster_list: @@ -2844,21 +2963,26 @@ def _regen_cluster_anchor(doc, anchor_id, cluster_ids, anchor_meta): if co: cutouts.append(co) except Exception as ex: print("[ELEMENTE] cluster cutout:", ex) + # Cutouts pro Layer-Result abziehen if cutouts: - try: - diff = rg.Brep.CreateBooleanDifference( - [cluster_brep], cutouts, tol) - if diff and len(diff) > 0: - cluster_brep = diff[0] - except Exception as ex: - print("[ELEMENTE] cluster bool-diff openings:", ex) + new_results = [] + for (b, col, nm) in layer_results: + if b is None: + new_results.append((None, col, nm)); continue + try: + diff = rg.Brep.CreateBooleanDifference([b], cutouts, tol) + if diff and len(diff) > 0: + b = diff[0] + except Exception as ex: + print("[ELEMENTE] cluster bool-diff openings:", ex) + new_results.append((b, col, nm)) + layer_results = new_results # Alle stale wand_volume-Objekte der Cluster-Members raeumen for c_wid in cluster_list: for o, _m in _find_objects_by_wall_id(doc, c_wid, "wand_volume"): try: doc.Objects.Delete(o.Id, True) except Exception: pass - # Auch alte Anchor-Volumes mit chain_members die unsere Member enthalten cluster_set = set(cluster_list) for _vobj in list(doc.Objects): _vm = _read_meta(_vobj) @@ -2868,49 +2992,65 @@ def _regen_cluster_anchor(doc, anchor_id, cluster_ids, anchor_meta): try: doc.Objects.Delete(_vobj.Id, True) except Exception: pass - # Layer + Material aus Anchor's Style + # AddBrep pro Layer (oder einmal bei solid) g = _geschoss_by_id(doc, anchor_meta["geschoss"]) geschoss_name = g.get("name", "EG") if g else "EG" - layer_idx = _ensure_layer(doc, _layer_path_volume(doc, geschoss_name)) - mat_color = "#9a9a9a" - try: - sm = _wand_solid_material(doc, anchor_meta) - if sm: - all_mats = _get_all_materials(doc) - if sm in all_mats: - mat_color = all_mats[sm].get("color", mat_color) - ms = _ensure_material_sublayer(doc, geschoss_name, sm) - if ms >= 0: layer_idx = ms - except Exception as ex: - print("[ELEMENTE] cluster material lookup:", ex) + base_layer_idx = _ensure_layer(doc, _layer_path_volume(doc, geschoss_name)) + all_mats = _get_all_materials(doc) + import json as _json + layers_json = (_json.dumps(layers_def, ensure_ascii=False) + if is_layered else "") + for idx, (lbrep, color, lname) in enumerate(layer_results): + if lbrep is None: continue + # Material-Lookup: bei layered pro Schicht via layers_def[idx].material, + # bei solid via Style. + mat_name = "" + effective_color = color + if is_layered and idx < len(layers_def): + mat_name = layers_def[idx].get("material", "") or "" + elif not is_layered: + try: mat_name = _wand_solid_material(doc, anchor_meta) or "" + except Exception: mat_name = "" + target_layer = base_layer_idx + full_mat_dict = None + if mat_name and mat_name in all_mats: + full_mat_dict = all_mats[mat_name] + effective_color = full_mat_dict.get("color", effective_color) + ms = _ensure_material_sublayer(doc, geschoss_name, mat_name) + if ms >= 0: target_layer = ms + if not effective_color: + effective_color = "#9a9a9a" - attrs = Rhino.DocObjects.ObjectAttributes() - attrs.LayerIndex = layer_idx - try: - import System.Drawing as SD - attrs.ColorSource = ( - Rhino.DocObjects.ObjectColorSource.ColorFromObject) - attrs.ObjectColor = SD.Color.FromArgb(255, 0, 0, 0) - except Exception: pass - mat_idx = _ensure_material(doc, mat_color) - if mat_idx >= 0: - attrs.MaterialIndex = mat_idx - attrs.MaterialSource = ( - Rhino.DocObjects.ObjectMaterialSource.MaterialFromObject) - _attach_meta(attrs, anchor_id, "wand_volume", - anchor_meta["geschoss"], anchor_meta["dicke"], - anchor_meta["uk_override"], anchor_meta["ok_override"], - anchor_meta.get("referenz", "mid"), - wand_chain_members=cluster_list) - # Cluster-Volume bewusst NICHT in Anchor-Group: sonst wuerde Click auf - # Anchor-Achse das ganze gemerged Brep mit-selektieren. Stattdessen: - # - Click auf Achse → nur die Achse - # - Click auf Volume → Select-Handler swappt auf naechste Cluster-Achse - try: - doc.Objects.AddBrep(cluster_brep, attrs) - except Exception as ex: - print("[ELEMENTE] AddBrep cluster:", ex) - return False + attrs = Rhino.DocObjects.ObjectAttributes() + attrs.LayerIndex = target_layer + try: + import System.Drawing as SD + attrs.ColorSource = ( + Rhino.DocObjects.ObjectColorSource.ColorFromObject) + attrs.ObjectColor = SD.Color.FromArgb(255, 0, 0, 0) + except Exception: pass + mat_idx_attr = -1 + if full_mat_dict is not None: + mat_idx_attr = _ensure_pbr_material(doc, full_mat_dict) + if mat_idx_attr < 0 and effective_color: + mat_idx_attr = _ensure_material(doc, effective_color) + if mat_idx_attr >= 0: + attrs.MaterialIndex = mat_idx_attr + attrs.MaterialSource = ( + Rhino.DocObjects.ObjectMaterialSource.MaterialFromObject) + _attach_meta(attrs, anchor_id, "wand_volume", + anchor_meta["geschoss"], anchor_meta["dicke"], + anchor_meta["uk_override"], anchor_meta["ok_override"], + anchor_meta.get("referenz", "mid"), + wand_layered=is_layered, + wand_layers=layers_json, + wand_layer_idx=(idx if is_layered else None), + wand_chain_members=cluster_list) + try: + doc.Objects.AddBrep(lbrep, attrs) + except Exception as ex: + print("[ELEMENTE] AddBrep cluster layer {}:".format(idx), ex) + return False # Stale Auto-Groups + Centerlines fuer alle Cluster-Member regen try: for _wid in cluster_list: @@ -2918,8 +3058,9 @@ def _regen_cluster_anchor(doc, anchor_id, cluster_ids, anchor_meta): _regen_wall_lines(doc, _wid, in_cluster=True) except Exception as ex: print("[ELEMENTE] strip/lines (cluster):", ex) - print("[ELEMENTE] cluster-union anchor={} members={} cutouts={}".format( - anchor_id, len(cluster_list), len(cutouts))) + print("[ELEMENTE] cluster-union anchor={} members={} layers={} cutouts={}".format( + anchor_id, len(cluster_list), + len(layer_results) if is_layered else 1, len(cutouts))) return True @@ -8084,8 +8225,7 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name cluster_ids = _find_wall_cluster(doc, element_id) except Exception as ex: print("[ELEMENTE] cluster detect:", ex) - is_layered_meta = bool(meta.get("wand_layered", False)) - if (len(cluster_ids) > 1 and not is_layered_meta + if (len(cluster_ids) > 1 and not _is_linear_chain(doc, cluster_ids)): anchor = sorted(cluster_ids)[0] if anchor != element_id: @@ -15285,6 +15425,18 @@ def _on_object_deleted(sender, e): try: import time vol_ids = [v.Id for v in _find_all_volumes(doc, meta["id"])] + # Bei wand_axis-Delete auch wand_centerline + wand_outline + # Curves mit-cascaden — sonst bleiben sie als Orphan-Linien + # nach dem Loeschen einer Wand stehen. + if meta.get("type") == "wand_axis": + for _o in doc.Objects: + try: + _t = _o.Attributes.GetUserString(_KEY_TYPE) or "" + _wid = _o.Attributes.GetUserString(_KEY_ID) or "" + if (_t in ("wand_centerline", "wand_outline") + and _wid == meta["id"]): + vol_ids.append(_o.Id) + except Exception: pass if vol_ids: pending = sc.sticky.get("_elemente_pending_source_cascade") if not isinstance(pending, dict):