T-Junction Phase 2 + Layer-Smart-Join: Backbone-Drill bis Stahl-Layer-Far-Face

Phase-2-Fixes:
- Backbone-Ext berechnet bis MATCHING-Layer (Stahl) far-face, nicht bis
  Body-far-face. Beton drillt durch Stahl-Band, nicht durch Daemm/Putz.
- Case A/B detection via dot(out_dir, RhinoPerp). Rhino Curve.Offset
  benutzt (tan × +z) = (b_tan.Y, -b_tan.X), nicht (-b_tan.Y, b_tan.X).
- Backbone-axis-ext separat von _my_axis_ext: nur Backbone-Column
  extended, non-backbone columns stoppen am Snap.
- Through-only Mats (z.B. Daemm wo T-stem keinen hat) werden auch
  durchlaufen damit hoehere-prio backbone diese carven kann.
- Post-Carve Union: gleiche-Material-Pieces mergen (Backbone-Beton-Col
  + Through-Stahl-Band → T-Shape).
- BBox-overlap strict-Filter vor BoolDiff: touching coplanar faces
  ueberspringen, vermeidet Rhino BoolDiff "punch-through" Artefakte.
- _has_my_cols guard: KeyError beim consume von T-stem-Layern fuer
  through-only mats verhindert.

Layer-Smart-Join (smart_join.py):
- Neue _layer_join_attempt: 2 selektierte wand_volume Breps gleicher
  Material via BoolUnion mergen. Manueller Override fuer Edge-Cases
  wo auto Phase 2 nicht reicht.

Cluster-Volume Select-Handler (elemente.py):
- Alt-Click bypassed Cluster-Swap → User kann einzelne Layer-Breps
  direkt anwaehlen (= fuer Layer-Smart-Join).
This commit is contained in:
2026-06-01 21:54:02 +02:00
parent df56a54b66
commit 118bc51cc5
2 changed files with 185 additions and 45 deletions
+117 -45
View File
@@ -2751,6 +2751,11 @@ class _ClusterVolumeSelectHandler(Rhino.UI.MouseCallback):
try:
if "Left" not in str(e.MouseButton): return
except Exception: pass
# Alt-Click bypassed Cluster-Swap → user kann Layer-Breps
# direkt anwaehlen (= fuer Layer-Smart-Join)
try:
if bool(e.AltKeyDown): return
except Exception: pass
view = e.View
if view is None: return
vp = view.ActiveViewport
@@ -3731,19 +3736,33 @@ def _t_junction_layer_overrides(doc, my_meta, through_meta, ep_pt, out_dir,
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-Extension: column drills durch through-body bis FAR FACE
# auf out_dir-Seite (= durchbricht non-backbone Layer auf Far-Seite,
# z.B. Daemm + Putz von Aussenwand wenn Beton-Innenwand anstoesst).
# Resultat: "durchgehendes T" beim Backbone-Material.
# Backbone column extends in out_dir-Richtung bis Far-Face der
# MATCHING-Layer (= Stahl-Band) im through-wall. NICHT bis Body-Far-Face.
# = Beton drillt nur durch Stahl-Schicht, nicht durch Daemm/Putz.
# Case A (T-stem-axis covers body schon naturally) → ext=0.
# Case B (OPPOSITE side) → ext = backbone-layer-thickness auf out_dir-Seite.
backbone_ext = 0.0
if backbone_ext is None:
backbone_ext = float(b_dicke) * 0.5
if backbone and backbone_dL is not None and backbone_dR is not None:
# Rhino Curve.Offset perp-Konvention: positive offset = (tan × +z)
perp_bx_e = rg.Vector3d(b_tan.Y, -b_tan.X, 0)
try: perp_bx_e.Unitize()
except Exception: pass
dot_opx_e = (out_dir.X * perp_bx_e.X
+ out_dir.Y * perp_bx_e.Y)
if dot_opx_e > 0:
backbone_ext = max(backbone_dL, backbone_dR, 0.0)
else:
backbone_ext = max(-backbone_dL, -backbone_dR, 0.0)
print(("[ELEMENTE] bb-ext-calc: bb_dL={:.3f} bb_dR={:.3f}"
" btan=({:.2f},{:.2f}) perp=({:.2f},{:.2f})"
" od=({:.2f},{:.2f}) dot_opx={:.2f} → ext={:.3f}").format(
backbone_dL, backbone_dR,
b_tan.X, b_tan.Y, perp_bx_e.X, perp_bx_e.Y,
out_dir.X, out_dir.Y, dot_opx_e, backbone_ext))
standard_miter = _t_junction_miter(ep_pt, out_dir, b_tan, b_dicke,
through_meta.get("referenz", "mid"))
@@ -8805,23 +8824,25 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
_ext_arr[_li_x] or 0)
break
if not _backbone_mat: continue
# T-Stem axis (extended for backbone-side)
# Backbone-axis = extended bis Through-Far-Face (drill).
# Non-backbone columns nutzen geom (= stoppen am Snap).
if _backbone_ext_val > 0:
if _is_end:
_my_axis_ext = _extend_axis_curve(
_backbone_axis_ext = _extend_axis_curve(
geom, 0, _backbone_ext_val)
else:
_my_axis_ext = _extend_axis_curve(
_backbone_axis_ext = _extend_axis_curve(
geom, _backbone_ext_val, 0)
else:
_my_axis_ext = geom
_backbone_axis_ext = geom
_my_axis_ext = geom
try:
_ps = _my_axis_ext.PointAtStart
_pe = _my_axis_ext.PointAtEnd
_ps = _backbone_axis_ext.PointAtStart
_pe = _backbone_axis_ext.PointAtEnd
_gs = geom.PointAtStart
_ge = geom.PointAtEnd
print(("[ELEMENTE] axis-ext: geom y=[{:.3f}{:.3f}]"
" → ext y=[{:.3f}{:.3f}] (ext_val={:.3f},"
"bb-ext y=[{:.3f}{:.3f}] (ext_val={:.3f},"
" is_end={})").format(
_gs.Y, _ge.Y, _ps.Y, _pe.Y,
_backbone_ext_val, _is_end))
@@ -8872,7 +8893,7 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
if _bbones:
_bd_l, _bd_r, _ = _bbones[0]
_backbone_col_rect = _layer_rect_2d(
_my_axis_ext, _bd_l, _bd_r)
_backbone_axis_ext, _bd_l, _bd_r)
# 3D Version fuer Carve
if _backbone_col_rect is not None:
try:
@@ -8918,8 +8939,11 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
# 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
# Auch Mats die NUR in through sind durchlaufen,
# damit hoehere-prio backbone diese carven kann
# (z.B. Aussenwand-Daemm wo Beton durchdrillt).
_is_backbone = (_mat == _backbone_mat)
_has_my_cols = _mat in _my_info_by_mat
# Cross-junction safe: nutze CURRENT through-Brep(s)
_layer_breps_3d = []
for _o in _th_objs_by_mat.get(_mat, []):
@@ -8933,8 +8957,12 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
# (= 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)
_ax_for_col = (_backbone_axis_ext if _is_backbone
else _my_axis_ext)
_my_cols_iter = (_my_info_by_mat[_mat]
if _has_my_cols else [])
for (_dl, _dr, _layer_i) in _my_cols_iter:
_r = _layer_rect_2d(_ax_for_col, _dl, _dr)
if _r is None: continue
try:
_profile = _r.DuplicateCurve()
@@ -8956,21 +8984,11 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
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
# Kein 3D Union — bei disjoint breps wuerde Union eine
# Mega-Brep mit combined bbox machen, was den BBox-
# overlap-filter unten kaputt macht. Jeder brep wird
# separat carved.
_result_breps = list(_layer_breps_3d)
# Carve mit backbone-col + hoehere-prio through-bands
if not _is_backbone:
_carve_breps = []
@@ -9000,10 +9018,50 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
_carve_breps.append(_ob_br)
except Exception: pass
if _carve_breps:
# Diagnostic: pre-carve breps + carve breps
for _di, _dbr in enumerate(_result_breps):
try:
_bbd = _dbr.GetBoundingBox(True)
print(("[ELEMENTE] {} PRE-CARVE br[{}]"
" x=[{:.3f},{:.3f}]"
" y=[{:.3f},{:.3f}]").format(
_mat, _di,
_bbd.Min.X, _bbd.Max.X,
_bbd.Min.Y, _bbd.Max.Y))
except Exception: pass
for _di, _dcb in enumerate(_carve_breps):
try:
_bbd = _dcb.GetBoundingBox(True)
print(("[ELEMENTE] {} CARVE cb[{}]"
" x=[{:.3f},{:.3f}]"
" y=[{:.3f},{:.3f}]").format(
_mat, _di,
_bbd.Min.X, _bbd.Max.X,
_bbd.Min.Y, _bbd.Max.Y))
except Exception: pass
_eps = 0.001
_current = list(_result_breps)
for _cb in _carve_breps:
for _ci, _cb in enumerate(_carve_breps):
_cb_bb = _cb.GetBoundingBox(True)
_next = []
for _br in _current:
for _bi, _br in enumerate(_current):
# Strict bbox overlap: touching faces
# triggern Rhino BoolDiff "punch-through"
# Artefakte → vorher rausfiltern.
_br_bb = _br.GetBoundingBox(True)
if (_br_bb.Min.X + _eps >= _cb_bb.Max.X
or _cb_bb.Min.X + _eps >= _br_bb.Max.X
or _br_bb.Min.Y + _eps >= _cb_bb.Max.Y
or _cb_bb.Min.Y + _eps >= _br_bb.Max.Y
or _br_bb.Min.Z + _eps >= _cb_bb.Max.Z
or _cb_bb.Min.Z + _eps >= _br_bb.Max.Z):
print(("[ELEMENTE] {} SKIP carve"
" cb[{}] vs br[{}]").format(
_mat, _ci, _bi))
_next.append(_br); continue
print(("[ELEMENTE] {} DO carve"
" cb[{}] vs br[{}]").format(
_mat, _ci, _bi))
try:
_diff = rg.Brep.CreateBooleanDifference(
_br, _cb, 0.001)
@@ -9021,6 +9079,18 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
_result_breps = _current
# (West-stub filter entfernt — war zu aggressiv,
# discarded legitime through-wall west pieces)
# Union NACH Carve: touchende gleiches-Material
# Pieces zusammenführen (z.B. Backbone-Beton-Säule
# + Aussenwand-Stahl-Band → T-Shape).
if len(_result_breps) > 1:
try:
_u3d_post = rg.Brep.CreateBooleanUnion(
list(_result_breps), 0.01)
if _u3d_post and len(_u3d_post) > 0:
_result_breps = list(_u3d_post)
except Exception as _ex:
print("[ELEMENTE] {} post-Union exc:"
.format(_mat), _ex)
# 4. Validity + MergeCoplanarFaces
_result_breps = [_br for _br in _result_breps
if _br is not None and _br.IsValid]
@@ -9061,11 +9131,13 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
_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])
# (nur wenn T-stem dieses Material ueberhaupt hat)
if _has_my_cols:
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,