T-Junction Phase 2 mit 3D Brep Union + Material-Prio-Carve

Asymmetric L-merge fuer Schichtdurchdringung:
- Backbone (= hoechste Material-Prio in beiden Waenden) bildet T-form
- Non-backbone Layer werden gecarved mit backbone-Column + through-Bands
  hoeherer Prio
- ext=0 fuer T-Stem-Axis (= column endet am snap, kein "drueber")
- 3D Brep Union via Brep.CreateBooleanUnion mit cross-junction safety
  (= aktueller through-Brep statt von Meta neu zu bauen)
- Cleanup: MergeCoplanarFaces

Plus:
- Innenwand Beton 20cm Style (Putz + Beton + Putz, ref-mid)
- curve_vertex_dots.py: gruene Vertex-Punkte fuer Polylinen/Curves
- Cluster-Volume Select Handler: Shift-Modifier fuer Multi-Select
- startup.py: Top-View maximieren on Doc-Open

Known limitation: Putz-Schicht kann in bestimmten Konfigurationen visuell
suedlich des Daemm-Band-Top weiter sichtbar sein (= edge case fuer
asymmetric layers). Naechster Schritt: manuelle 2D-Polygon-Konstruktion
statt 3D Boolean.
This commit is contained in:
2026-06-01 14:32:55 +02:00
parent 9cce8199c3
commit df56a54b66
4 changed files with 756 additions and 51 deletions
+534 -51
View File
@@ -2214,7 +2214,8 @@ def _detect_t_junction(doc, geschoss_id, wall_id, endpoint,
tan = geom.TangentAt(t)
return (meta["id"],
rg.Vector3d(tan.X, tan.Y, 0),
float(meta["dicke"]))
float(meta["dicke"]),
meta.get("referenz", "mid"))
except Exception: continue
return None
@@ -2299,32 +2300,30 @@ def _detect_through_wall_at(doc, partners_at_joint, exclude_self_tan):
# Gefunden: ti/tj formen die Through-Wand
mi = _wand_meta_by_id(doc, wi)
b_dicke = float((mi or {}).get("dicke", 0.25))
return (rg.Vector3d(ti.X, ti.Y, 0), b_dicke)
b_ref = (mi or {}).get("referenz", "mid")
return (rg.Vector3d(ti.X, ti.Y, 0), b_dicke, b_ref)
return None
def _t_junction_miter(endpoint, out_dir, b_tan, b_dicke):
def _t_junction_miter(endpoint, out_dir, b_tan, b_dicke, b_referenz="mid"):
"""Berechnet (miter_pt, miter_dir) fuer einen T-Stoss.
miter_dir = Tangente der Durchgangs-Wand (Linie laeuft parallel zu B's Achse).
miter_pt = endpoint verschoben um d_B/2 in Approach-Richtung also auf
der NAHEN Aussenflaeche von B (der Seite an der A ankommt).
A (= T-Stem, das zweite Wand-Stueck) wird an dieser Stelle abgeschnitten;
B (= Durchgangswand) bleibt unveraendert. Das ist das BIM-Standard-
Verhalten: 'erste Wand bleibt, zweite haengt sich dran'."""
miter_pt = endpoint verschoben bis zur NAHEN Aussenflaeche von B.
Beruecksichtigt B's referenz: bei mid ist Axis Centerline (Aussen-
flaechen ±d/2), bei left/right ist Axis auf einer Aussenkante."""
perp_b = rg.Vector3d(-b_tan.Y, b_tan.X, 0)
try: perp_b.Unitize()
except Exception: return None
# A's Body liegt auf der Seite -out_dir. Approach-Seite (perp_b
# ausgerichtet zur Approach) = sign(dot(-out_dir, perp_b)).
s = -(out_dir.X * perp_b.X + out_dir.Y * perp_b.Y)
if abs(s) < 1e-6:
# A parallel zu B — kein sauberer T-Stoss
return None
if abs(s) < 1e-6: return None
side = 1.0 if s > 0 else -1.0
off = float(b_dicke) * 0.5 * side
mpt = rg.Point3d(endpoint.X + perp_b.X * off,
endpoint.Y + perp_b.Y * off, 0)
# B's Aussenflaechen-Offsets entlang perp_b
start_off, d_total = _wall_offsets_from_referenz(b_dicke, b_referenz)
off_a = start_off
off_b = start_off - d_total
# Nahe Aussenflaeche = die in approach-Richtung naehere (= side-Richtung)
near_off = max(off_a, off_b) if side > 0 else min(off_a, off_b)
mpt = rg.Point3d(endpoint.X + perp_b.X * near_off,
endpoint.Y + perp_b.Y * near_off, 0)
mdir = rg.Vector3d(b_tan.X, b_tan.Y, 0)
try: mdir.Unitize()
except Exception: pass
@@ -2867,11 +2866,17 @@ class _ClusterVolumeSelectHandler(Rhino.UI.MouseCallback):
best_d2 = d2; best_axis = c_ax
except Exception: continue
if best_axis is None: return
# Shift-Modifier: ADD zur Selektion (kein UnselectAll)
try:
_shift = bool(e.ShiftKeyDown)
except Exception:
_shift = False
try: e.Cancel = True
except Exception: pass
self._busy = True
try:
doc.Objects.UnselectAll()
if not _shift:
doc.Objects.UnselectAll()
doc.Objects.Select(best_axis.Id, True)
try: view.Redraw()
except Exception: pass
@@ -3367,6 +3372,53 @@ def _make_wall_layer_brep(axis_curve, d_left, d_right, uk, ok,
return extrusion.ToBrep()
def _layer_rect_2d(axis_curve, d_left, d_right):
"""Liefert geschlossene XY-Rect-Curve fuer eine Schicht (Achse +
perp Offsets). Genutzt fuer 2D-Polylinen-Union beim T-Junction.
Nutzt _offset_curve wie _make_wall_layer_brep wichtig fuer
Boolean-Compatibility (PolyCurve statt PolylineCurve).
Forced z=0 + CCW winding (= ClosedCurveOrientation check)."""
if not isinstance(axis_curve, rg.Curve): return None
d_l = float(d_left); d_r = float(d_right)
if abs(d_l - d_r) < 1e-9: return None
try:
# Force axis to z=0 plane
ax_xy = axis_curve.DuplicateCurve()
if abs(ax_xy.PointAtStart.Z) > 1e-9 or abs(ax_xy.PointAtEnd.Z) > 1e-9:
ax_xy.Transform(rg.Transform.PlaneToPlane(
rg.Plane(rg.Point3d(0,0,ax_xy.PointAtStart.Z),
rg.Vector3d.ZAxis),
rg.Plane.WorldXY))
plane = rg.Plane.WorldXY
tol = 0.001
left = _offset_curve(ax_xy, plane, d_l, tol)
right = _offset_curve(ax_xy, plane, d_r, tol)
if not left or not right: return None
L = left[0]; R = right[0]
R.Reverse()
cap_s = rg.LineCurve(L.PointAtEnd, R.PointAtStart)
cap_e = rg.LineCurve(R.PointAtEnd, L.PointAtStart)
joined = rg.Curve.JoinCurves([L, cap_s, R, cap_e], tol)
if not joined or len(joined) == 0 or not joined[0].IsClosed:
return None
out = joined[0]
# Force z=0 (offset_curve might preserve original z)
try:
if (abs(out.PointAtStart.Z) > 1e-9):
xform = rg.Transform.Translation(0, 0, -out.PointAtStart.Z)
out.Transform(xform)
except Exception: pass
# Force CCW orientation (= positive area in WorldXY)
try:
ori = out.ClosedCurveOrientation(rg.Plane.WorldXY)
if ori == rg.CurveOrientation.Clockwise:
out.Reverse()
except Exception: pass
return out
except Exception:
return None
def _wall_offsets_from_referenz(dicke, referenz):
"""Liefert (start_offset, d_total) — start_offset ist der Wert von 'links'
relativ zur Achse, d_total ist die Summe der Wand-Dicke (immer positiv)."""
@@ -3600,19 +3652,38 @@ def _wand_meta_prio(doc, meta):
except Exception: return 500
# Material-Prio fuer T-Junction Schichtdurchdringung: an einem T-Stoss
# zwischen zwei layered Waenden geht NUR die hoechste gemeinsame
# Material-Prio als 'Backbone' durch + uniont (= T-Form). Alle anderen
# T-Stem-Layer T-mitern am Near-Face → bei symmetrisch geschichteten
# Waenden ergibt das automatisch L-Stoesse mit gleichfarbigen Aussenlagen.
_MATERIAL_PRIO = {
"stahlbeton": 800,
"beton": 800,
"mauerwerk": 600,
"kalksandstein": 600,
"ziegel": 550,
"holzstaender": 400,
"holz": 400,
"daemmung": 200,
"putz": 100,
}
def _material_prio(mat):
if not mat: return 0
return _MATERIAL_PRIO.get(mat.strip().lower(), 300)
def _t_junction_layer_overrides(doc, my_meta, through_meta, ep_pt, out_dir,
b_tan, b_dicke):
"""Per-Layer Schichtdurchdringung bei T-Junction zwischen layered Waenden.
Pro T-Stem-Schicht wird in der Through-Wand nach einer Schicht mit
GLEICHEM MATERIAL gesucht:
- Match: T-Stem-Layer extends durch die Through-Wand (Axis-Extension um
b_dicke/2 = bis Far-Face) visuell verbunden
- No Match: T-Stem-Layer stoppt am Near-Face (Standard T-Miter)
NEU: nur das BACKBONE-Material extends + uniont. Backbone = das mit
hoechster Material-Prio das in BEIDEN Waenden vorkommt. Andere Layer
stoppen am Near-Face (Standard T-Miter).
Returns (per_layer_ext, per_layer_miter) Listen pro T-Stem-Layer.
Wenn my keine Layers hat: (None, None) (Caller faellt auf uniform Miter
zurueck)."""
Returns (per_layer_ext, per_layer_miter) Listen pro T-Stem-Layer."""
if not my_meta or not through_meta: return None, None
my_layers = my_meta.get("wand_layers") or []
if not my_layers: return None, None
@@ -3624,26 +3695,101 @@ def _t_junction_layer_overrides(doc, my_meta, through_meta, ep_pt, out_dir,
mat = (l.get("material") or "").strip()
if mat: th_materials.add(mat)
else:
# Through ist solid → 1 Material aus Style
try:
sm = _wand_solid_material(doc, through_meta) if doc else ""
if sm: th_materials.add(sm)
except Exception: pass
standard_miter = _t_junction_miter(ep_pt, out_dir, b_tan, b_dicke)
extension = float(b_dicke) * 0.5 # bis Far-Face
# Backbone = hoechste Material-Prio in BEIDEN Waenden
my_materials = set()
for layer in my_layers:
m = (layer.get("material") or "").strip()
if m: my_materials.add(m)
common = my_materials & th_materials
backbone = None
backbone_prio = -1
for m in common:
p = _material_prio(m)
if p > backbone_prio:
backbone = m
backbone_prio = p
# Backbone-Schicht-Position in der Through-Wand finden (perp-Offsets)
backbone_dL = None
backbone_dR = None
if backbone and through_meta.get("wand_layered") and th_layers:
th_dicke = float(through_meta.get("dicke", 0) or 0)
th_ref = through_meta.get("referenz", "mid")
start_off, _ = _wall_offsets_from_referenz(th_dicke, th_ref)
cur = start_off
for tl in th_layers:
d = float(tl.get("dicke", 0) or 0)
if d <= 0: continue
if (tl.get("material") or "").strip() == backbone:
backbone_dL = cur
backbone_dR = cur - d
break
cur -= d
# Backbone-Extension: IMMER 0 = column endet exakt am Through-axis,
# geht nie ueber outer face hinaus. Asymmetric Merge ist Resultat
# der natuerlichen Geometrie:
# - Wenn T-Stem-Body auf gleicher Seite wie Through-Wand-Body
# (= column passes durch wall body): alle Layer mergen via 3D
# Union + Carve (L-merge auf T-Stem-Layer-Seite, west_piece
# getrennt).
# - Wenn T-Stem-Body auf gegenueberliegender Seite (= column endet
# am Through-Aussenrand, betritt wall body nicht): kein Merge,
# visually 2 separate Walls die sich am axis touchieren.
backbone_ext = 0.0
if backbone_ext is None:
backbone_ext = float(b_dicke) * 0.5
standard_miter = _t_junction_miter(ep_pt, out_dir, b_tan, b_dicke,
through_meta.get("referenz", "mid"))
# Through-Layer pro Material indexieren (Liste von (d_l, d_r))
through_by_mat_pos = {}
if through_meta.get("wand_layered") and th_layers:
th_dicke2 = float(through_meta.get("dicke", 0) or 0)
th_ref2 = through_meta.get("referenz", "mid")
start_off2, _ = _wall_offsets_from_referenz(th_dicke2, th_ref2)
cur2 = start_off2
for tl in th_layers:
d2 = float(tl.get("dicke", 0) or 0)
if d2 <= 0: continue
m2 = (tl.get("material") or "").strip()
if m2:
through_by_mat_pos.setdefault(m2, []).append(
(cur2, cur2 - d2))
cur2 -= d2
# perp_b und Approach-Richtung
perp_bx = rg.Vector3d(-b_tan.Y, b_tan.X, 0)
try: perp_bx.Unitize()
except Exception: pass
dot_opx = out_dir.X * perp_bx.X + out_dir.Y * perp_bx.Y
b_tan_u = rg.Vector3d(b_tan.X, b_tan.Y, 0)
try: b_tan_u.Unitize()
except Exception: pass
per_ext = []
per_miter = []
match_log = []
for layer in my_layers:
mat = (layer.get("material") or "").strip()
if mat and mat in th_materials:
# Match → durchstossen
per_ext.append(extension)
if backbone and mat == backbone:
per_ext.append(backbone_ext)
per_miter.append(None)
match_log.append("{}=BACKBONE(+{:.3f})".format(mat, backbone_ext))
else:
# Kein Match → an Near-Face stoppen
# Non-backbone: Standard T-Miter an Through-Aussenkante.
# T-Stem-Layer + Through-Putz-Band touchieren am L-Korner
# mit gleichem Material (Same-color visual L, seam moeglich).
per_ext.append(0.0)
per_miter.append(standard_miter)
match_log.append("{}=stop".format(mat or "?"))
print("[ELEMENTE] T-Junction Schichtdurchdringung: backbone={} "
"(prio={}, ext={:.3f}), through-mats={}, layers: {}".format(
backbone or "none", backbone_prio, backbone_ext,
sorted(th_materials), ", ".join(match_log)))
return per_ext, per_miter
@@ -8507,24 +8653,21 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
element_id, p_s,
exclude_ids=chain_set)
if tj is not None:
_oid, b_tan, b_dicke = tj
_oid, b_tan, b_dicke, b_ref = tj
if _wand_should_apply_t_miter(doc, meta, _oid):
tm = _t_junction_miter(p_s, out_s, b_tan, b_dicke)
tm = _t_junction_miter(p_s, out_s, b_tan,
b_dicke, b_ref)
if tm is not None: miter_start = tm
# Through-Meta merken fuer per-Layer Schicht-
# durchdringung (s. unten bei layer_breps build)
t_junction_start = (_oid, b_tan, b_dicke, p_s, out_s)
else:
# 3+ Joint: ich bin T-Stem wenn meine Tangente NICHT
# collinear mit zwei collinearen Partner-Tangenten ist.
# Eigene Tangente (gerichtet aus Body raus) am Start = -TangentAtStart
_my_t = geom.TangentAtStart
_my_out_tan = rg.Vector3d(-_my_t.X, -_my_t.Y, 0)
through = _detect_through_wall_at(
doc, joints.get(key_s, []), _my_out_tan)
if through is not None:
b_tan, b_dicke = through
tm = _t_junction_miter(p_s, out_s, b_tan, b_dicke)
b_tan, b_dicke, b_ref = through
tm = _t_junction_miter(p_s, out_s, b_tan,
b_dicke, b_ref)
if tm is not None: miter_start = tm
if out_e is not None:
key_e = _pt_key(p_e)
@@ -8541,20 +8684,21 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
element_id, p_e,
exclude_ids=chain_set)
if tj is not None:
_oid, b_tan, b_dicke = tj
_oid, b_tan, b_dicke, b_ref = tj
if _wand_should_apply_t_miter(doc, meta, _oid):
tm = _t_junction_miter(p_e, out_e, b_tan, b_dicke)
tm = _t_junction_miter(p_e, out_e, b_tan,
b_dicke, b_ref)
if tm is not None: miter_end = tm
t_junction_end = (_oid, b_tan, b_dicke, p_e, out_e)
else:
# 3+ Joint: T-Stem-Erkennung (analog start)
_my_t = geom.TangentAtEnd
_my_out_tan = rg.Vector3d(_my_t.X, _my_t.Y, 0)
through = _detect_through_wall_at(
doc, joints.get(key_e, []), _my_out_tan)
if through is not None:
b_tan, b_dicke = through
tm = _t_junction_miter(p_e, out_e, b_tan, b_dicke)
b_tan, b_dicke, b_ref = through
tm = _t_junction_miter(p_e, out_e, b_tan,
b_dicke, b_ref)
if tm is not None: miter_end = tm
except Exception as ex:
print("[ELEMENTE] wall joints:", ex)
@@ -8589,14 +8733,346 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
per_layer_ext_start=pl_ext_s, per_layer_ext_end=pl_ext_e,
per_layer_miter_start=pl_miter_s,
per_layer_miter_end=pl_miter_e)
# Diagnostic: layer build status — User sieht oft "solid" obwohl
# walls layered sind, kontrollieren obs an Brep-Build oder Display liegt
# Diagnostic: layer build status
_n_ok = sum(1 for (b, _c, _n) in layer_breps if b is not None)
_n_fail = len(layer_breps) - _n_ok
print("[ELEMENTE] layered build {} (chain={}): {}/{} layers built"
" (def={} layers)".format(
element_id, len(chain_ids) if chain_ids else 1,
_n_ok, len(layer_breps), len(layers_def)))
# Phase 2 Schichtdurchdringung (2D-Polylinen-Approach):
# Pro Material 2D-Rechtecke (Through-Bands + T-Stem-Spalten)
# bauen, in 2D unionen, fuer non-backbone Material mit
# Backbone-Column subtrahieren (= 2D-Carve), dann extrudieren.
# Robuster als 3D-BoolUnion mit touching breps.
try:
import json as _j
for _tj_info, _is_end in ((t_junction_start, False),
(t_junction_end, True)):
if _tj_info is None: continue
_toid = _tj_info[0]
_mit_arr = pl_miter_e if _is_end else pl_miter_s
_ext_arr = pl_ext_e if _is_end else pl_ext_s
if _mit_arr is None: continue
# Through axis + meta
_th_axis = _find_axis(doc, _toid)
if _th_axis is None: continue
_th_meta = _read_meta(_th_axis)
if not _th_meta or not _th_meta.get("wand_layered"): continue
_th_geom = _th_axis.Geometry
if not isinstance(_th_geom, rg.Curve): continue
_th_layers = _th_meta.get("wand_layers") or []
_th_ref = _th_meta.get("referenz", "mid")
_th_dicke = float(_th_meta.get("dicke", 0) or 0)
# Through-Layer-Offsets per Material
_th_start, _ = _wall_offsets_from_referenz(
_th_dicke, _th_ref)
_th_off_by_mat = {}
_cur = _th_start
for _tl in _th_layers:
_d = float(_tl.get("dicke", 0) or 0)
if _d <= 0: continue
_m = (_tl.get("material") or "").strip()
if _m:
_th_off_by_mat.setdefault(_m, []).append(
(_cur, _cur - _d))
_cur -= _d
# T-Stem-Layer-Offsets + layer-idx per Material
_my_layers = meta.get("wand_layers") or []
_my_dicke = float(meta.get("dicke", 0) or 0)
_my_ref = meta.get("referenz", "mid")
_my_start, _ = _wall_offsets_from_referenz(
_my_dicke, _my_ref)
_my_info_by_mat = {}
_cur = _my_start
for _i, _ml in enumerate(_my_layers):
_d = float(_ml.get("dicke", 0) or 0)
if _d <= 0: continue
_m = (_ml.get("material") or "").strip()
if _m:
_my_info_by_mat.setdefault(_m, []).append(
(_cur, _cur - _d, _i))
_cur -= _d
# Backbone-Material + Ext-Wert
_backbone_mat = None
_backbone_ext_val = 0.0
for _li_x in range(len(layers_def)):
if (_li_x < len(_mit_arr)
and _mit_arr[_li_x] is None):
_backbone_mat = (layers_def[_li_x].get(
"material") or "").strip()
if _ext_arr and _li_x < len(_ext_arr):
_backbone_ext_val = float(
_ext_arr[_li_x] or 0)
break
if not _backbone_mat: continue
# T-Stem axis (extended for backbone-side)
if _backbone_ext_val > 0:
if _is_end:
_my_axis_ext = _extend_axis_curve(
geom, 0, _backbone_ext_val)
else:
_my_axis_ext = _extend_axis_curve(
geom, _backbone_ext_val, 0)
else:
_my_axis_ext = geom
try:
_ps = _my_axis_ext.PointAtStart
_pe = _my_axis_ext.PointAtEnd
_gs = geom.PointAtStart
_ge = geom.PointAtEnd
print(("[ELEMENTE] axis-ext: geom y=[{:.3f}{:.3f}]"
" → ext y=[{:.3f}{:.3f}] (ext_val={:.3f},"
" is_end={})").format(
_gs.Y, _ge.Y, _ps.Y, _pe.Y,
_backbone_ext_val, _is_end))
except Exception: pass
# Backbone-Near-Face in T-Stem-Axis-Richtung (=
# wo Backbone-Layer auf Approach-Seite anfaengt).
# Non-backbone matching columns sollen DORT stoppen
# (Backbone hat hoehere Prio, Putz/etc weicht).
_btan_v = _tj_info[1]
_od_v = _tj_info[4]
_perp_b = rg.Vector3d(-_btan_v.Y, _btan_v.X, 0)
try: _perp_b.Unitize()
except Exception: pass
_dot_op = _od_v.X * _perp_b.X + _od_v.Y * _perp_b.Y
_backbone_near_dist = None
if _backbone_mat in _th_off_by_mat:
_bb_through = _th_off_by_mat[_backbone_mat]
if _bb_through:
_bb_dl, _bb_dr = _bb_through[0]
if _dot_op > 0:
_bn_perp = min(_bb_dl, _bb_dr)
else:
_bn_perp = max(_bb_dl, _bb_dr)
_backbone_near_dist = _bn_perp * (
1 if _dot_op > 0 else -1)
# Helper: T-Stem axis clipped an gegebener axial-distance
# vom Endpoint (negativ = retract, positiv = extend)
def _ax_clipped(_dist):
try:
_p1 = geom.PointAtStart
_p2 = geom.PointAtEnd
_tan = _p2 - _p1
if _tan.Length < 1e-9: return geom
_tan.Unitize()
if _is_end:
_new_end = _p2 + _tan * _dist
return rg.LineCurve(_p1, _new_end)
else:
_new_start = _p1 - _tan * _dist
return rg.LineCurve(_new_start, _p2)
except Exception:
return geom
# Backbone-Column 2D Rect + 3D Brep (fuer Carve)
_backbone_col_rect = None
_backbone_col_brep_3d = None
if _backbone_mat in _my_info_by_mat:
_bbones = _my_info_by_mat[_backbone_mat]
if _bbones:
_bd_l, _bd_r, _ = _bbones[0]
_backbone_col_rect = _layer_rect_2d(
_my_axis_ext, _bd_l, _bd_r)
# 3D Version fuer Carve
if _backbone_col_rect is not None:
try:
_bbh = float(ok) - float(uk)
_bbp = _backbone_col_rect.DuplicateCurve()
if abs(uk) > 1e-9:
_bbp.Transform(rg.Transform.Translation(
0, 0, uk))
_bbe = rg.Extrusion.Create(
_bbp, _bbh, True)
if _bbe is not None:
_bbb = _bbe.ToBrep()
if _bbb is not None and _bbb.IsValid:
_backbone_col_brep_3d = _bbb
except Exception: pass
# Through wand_volume objects per material
_th_objs_by_mat = {}
for _o in doc.Objects:
try:
if (_o.Attributes.GetUserString(_KEY_TYPE)
!= "wand_volume"): continue
if (_o.Attributes.GetUserString(_KEY_ID)
!= _toid): continue
_idx_s = _o.Attributes.GetUserString(
_KEY_WAND_LAYER_IDX) or ""
_lj = _o.Attributes.GetUserString(
_KEY_WAND_LAYERS) or ""
if not _idx_s or not _lj: continue
_tl = _j.loads(_lj)
_i = int(_idx_s)
if 0 <= _i < len(_tl):
_m = (_tl[_i].get("material") or "").strip()
if _m:
_th_objs_by_mat.setdefault(
_m, []).append(_o)
except Exception: pass
# 2D-Polylinen Union per Material:
# ALLE matching T-Stem-Columns nutzen die GLEICHE
# _my_axis_ext (= backbone-extended). Damit reichen
# ALLE Schichten gleich hoch ins Through (nicht nur
# Backbone) — Daemm + Putz visuell durchgehend.
# Carve verhindert Material-Konflikte: non-backbone
# Polygone werden gegen Backbone-Column geschnitten.
_height = float(ok) - float(uk)
for _mat in list(_th_off_by_mat.keys()):
if _mat not in _my_info_by_mat: continue
_is_backbone = (_mat == _backbone_mat)
# Cross-junction safe: nutze CURRENT through-Brep(s)
_layer_breps_3d = []
for _o in _th_objs_by_mat.get(_mat, []):
try:
_br_cur = _o.Geometry
if isinstance(_br_cur, rg.Brep) and _br_cur.IsValid:
_layer_breps_3d.append(
_br_cur.DuplicateBrep())
except Exception: pass
# Sammle T-Stem-Column Rects + Column-X-Range
# (= fuer west-stub-filter spaeter)
_column_breps = []
_column_x_ranges = []
for (_dl, _dr, _layer_i) in _my_info_by_mat[_mat]:
_r = _layer_rect_2d(_my_axis_ext, _dl, _dr)
if _r is None: continue
try:
_profile = _r.DuplicateCurve()
if abs(uk) > 1e-9:
_profile.Transform(
rg.Transform.Translation(0, 0, uk))
_extr = rg.Extrusion.Create(
_profile, _height, True)
if _extr is None: continue
_br = _extr.ToBrep()
if _br is None or not _br.IsValid: continue
_column_breps.append(_br)
# Column x range fuer filter
_cbb = _br.GetBoundingBox(True)
_column_x_ranges.append(
(_cbb.Min.X, _cbb.Max.X))
_layer_breps_3d.append(_br)
except Exception as _ex:
print("[ELEMENTE] Column-extr exc ({}):"
.format(_mat), _ex)
if not _layer_breps_3d: continue
# 3D Union all
if len(_layer_breps_3d) == 1:
_result_breps = _layer_breps_3d
else:
try:
_u3d = rg.Brep.CreateBooleanUnion(
_layer_breps_3d, 0.01)
if _u3d and len(_u3d) > 0:
_result_breps = list(_u3d)
else:
_result_breps = _layer_breps_3d
except Exception as _ex:
print("[ELEMENTE] {} Union exc:"
.format(_mat), _ex)
_result_breps = _layer_breps_3d
# Carve mit backbone-col + hoehere-prio through-bands
if not _is_backbone:
_carve_breps = []
if _backbone_col_brep_3d is not None:
_carve_breps.append(_backbone_col_brep_3d)
_my_prio = _material_prio(_mat)
for _other_mat, _bands in _th_off_by_mat.items():
if _other_mat == _mat: continue
_other_prio = _material_prio(_other_mat)
if _other_prio <= _my_prio: continue
for (_obdl, _obdr) in _bands:
_ob_rect = _layer_rect_2d(
_th_geom, _obdl, _obdr)
if _ob_rect is None: continue
try:
_ob_profile = _ob_rect.DuplicateCurve()
if abs(uk) > 1e-9:
_ob_profile.Transform(
rg.Transform.Translation(
0, 0, uk))
_ob_extr = rg.Extrusion.Create(
_ob_profile, _height, True)
if _ob_extr is None: continue
_ob_br = _ob_extr.ToBrep()
if (_ob_br is not None
and _ob_br.IsValid):
_carve_breps.append(_ob_br)
except Exception: pass
if _carve_breps:
_current = list(_result_breps)
for _cb in _carve_breps:
_next = []
for _br in _current:
try:
_diff = rg.Brep.CreateBooleanDifference(
_br, _cb, 0.001)
except Exception:
_next.append(_br); continue
if (_diff is not None
and len(_diff) > 0):
for _db in _diff:
if (_db is not None
and _db.IsValid):
_next.append(_db)
else:
_next.append(_br)
_current = _next
_result_breps = _current
# (West-stub filter entfernt — war zu aggressiv,
# discarded legitime through-wall west pieces)
# 4. Validity + MergeCoplanarFaces
_result_breps = [_br for _br in _result_breps
if _br is not None and _br.IsValid]
for _br in _result_breps:
try: _br.MergeCoplanarFaces(0.01)
except Exception: pass
# Re-Extrude entfernt — war Bug-Quelle wo carve-
# holes verloren gingen. Direkter carved Brep ist
# OK (= 2 disconnected pieces nach BoolDifference).
if not _result_breps: continue
# Diagnostic: BBox y-range per result brep
for _bri, _br_diag in enumerate(_result_breps):
try:
_bb_d = _br_diag.GetBoundingBox(True)
print("[ELEMENTE] {} result[{}] y=[{:.3f}"
",{:.3f}]".format(
_mat, _bri, _bb_d.Min.Y, _bb_d.Max.Y))
except Exception: pass
# Replace through objects mit Extrude-Resultaten
_existing = _th_objs_by_mat.get(_mat, [])
for _ri in range(len(_result_breps)):
if _ri < len(_existing):
try:
doc.Objects.Replace(
_existing[_ri].Id, _result_breps[_ri])
except Exception as _ex:
print("[ELEMENTE] 2D Replace:", _ex)
else:
if _existing:
try:
_attrs = _existing[0].Attributes.Duplicate()
doc.Objects.AddBrep(
_result_breps[_ri], _attrs)
except Exception as _ex:
print("[ELEMENTE] 2D Add:", _ex)
for _ei in range(len(_result_breps), len(_existing)):
try: doc.Objects.Delete(
_existing[_ei].Id, True)
except Exception: pass
# T-Stem-Layer fuer dieses Material consumen
for (_dl, _dr, _layer_i) in _my_info_by_mat[_mat]:
if _layer_i < len(layer_breps):
_lb_old = layer_breps[_layer_i]
layer_breps[_layer_i] = (
None, _lb_old[1], _lb_old[2])
print("[ELEMENTE] 2D-Phase2: {} ({}) → {} brep(s)"
" aus {} cols".format(
_mat,
"backbone" if _is_backbone else "L-merge",
len(_result_breps), len(_column_breps)))
except Exception as _ex:
print("[ELEMENTE] T-Junction Phase2 2D:", _ex)
else:
single_brep = _make_volume_geometry(
geom, meta["dicke"], uk, ok,
@@ -17843,6 +18319,13 @@ def _install_listeners(bridge):
install_cluster_select_handler()
except Exception as ex:
print("[ELEMENTE] cluster-select install:", ex)
# Generic Vertex-Dots fuer Curves (Polylinen, Rectangles etc) —
# display-only, hilft beim visuellen Finden von Grip-Positionen.
try:
import curve_vertex_dots
curve_vertex_dots.install_curve_vertex_dots()
except Exception as ex:
print("[ELEMENTE] curve_vertex_dots install:", ex)
# Pre-Warm: native OpenNURBS-Libraries beim Plugin-Start laden um den
# First-Call-Lag zu vermeiden (User-Meldung: beim ersten Wand-Verbinden
# haengt das UI kurz, danach nicht mehr → native code wird lazy-loaded).