Cascade-Cleanup: wand_centerline + wand_outline beim wand_axis-Delete mit-loeschen

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).
This commit is contained in:
2026-05-31 00:03:10 +02:00
parent 5fdad504da
commit f011e2ca94
+192 -40
View File
@@ -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:
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(
[cluster_brep], cutouts, tol)
diff = rg.Brep.CreateBooleanDifference([b], cutouts, tol)
if diff and len(diff) > 0:
cluster_brep = 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,48 +2992,64 @@ 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:
base_layer_idx = _ensure_layer(doc, _layer_path_volume(doc, geschoss_name))
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)
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
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 = _ensure_material(doc, mat_color)
if mat_idx >= 0:
attrs.MaterialIndex = mat_idx
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)
# 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)
doc.Objects.AddBrep(lbrep, attrs)
except Exception as ex:
print("[ELEMENTE] AddBrep cluster:", ex)
print("[ELEMENTE] AddBrep cluster layer {}:".format(idx), ex)
return False
# Stale Auto-Groups + Centerlines fuer alle Cluster-Member regen
try:
@@ -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):