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
+68
View File
@@ -296,12 +296,80 @@ def _l_join_attempt(doc, sel):
doc.EndUndoRecord(ur) doc.EndUndoRecord(ur)
def _layer_join_attempt(doc, wand_volumes):
"""Layer-Level Join: Versuch BoolUnion zwischen 2 wand_volume Breps.
Wenn beide gleiches Material haben + sich beruehren oder ueberlappen
→ mergen zu einem Brep. Behaelt UserStrings (= wand_id, material,
layer_idx) vom ersten Brep."""
if len(wand_volumes) != 2: return False
br0 = wand_volumes[0].Geometry
br1 = wand_volumes[1].Geometry
if not isinstance(br0, rg.Brep) or not isinstance(br1, rg.Brep):
return False
# Material-Check via UserStrings (= dossier_element_material)
mat0 = wand_volumes[0].Attributes.GetUserString(
"dossier_layer_material") or ""
mat1 = wand_volumes[1].Attributes.GetUserString(
"dossier_layer_material") or ""
if mat0 and mat1 and mat0 != mat1:
print("[SMART-JOIN] Layer-Join: Materialien unterschiedlich "
"({} vs {}), skip.".format(mat0, mat1))
return False
try:
union = rg.Brep.CreateBooleanUnion([br0, br1], 0.01)
except Exception as ex:
print("[SMART-JOIN] Layer-Join BoolUnion exc:", ex)
return False
if not union or len(union) == 0:
print("[SMART-JOIN] Layer-Join: Breps overlappen nicht "
"(BoolUnion returned None/empty)")
return False
if len(union) > 1:
print("[SMART-JOIN] Layer-Join: Breps overlappen nicht (BoolUnion"
" returned {} pieces, brauche 1)".format(len(union)))
return False
# 1 merged Brep
merged = union[0]
if not merged.IsValid:
print("[SMART-JOIN] Layer-Join: merged Brep invalid")
return False
try: merged.MergeCoplanarFaces(0.01)
except Exception: pass
ur = doc.BeginUndoRecord("DOSSIER Layer-Join")
try:
ok = doc.Objects.Replace(wand_volumes[0].Id, merged)
if ok:
doc.Objects.Delete(wand_volumes[1].Id, True)
return True
return False
finally:
doc.EndUndoRecord(ur)
def _run(): def _run():
doc = Rhino.RhinoDoc.ActiveDoc doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return if doc is None: return
sel = list(doc.Objects.GetSelectedObjects(False, False)) sel = list(doc.Objects.GetSelectedObjects(False, False))
if not sel: if not sel:
Rhino.RhinoApp.RunScript("_Join", False); return Rhino.RhinoApp.RunScript("_Join", False); return
# Layer-Level Join: wenn GENAU 2 wand_volume Breps selektiert + keine
# wand_axis → versuche Layer-Join (= BoolUnion der zwei Layer-Breps).
_wand_vols = [o for o in sel
if (o.Attributes.GetUserString("dossier_element_type")
or "") == "wand_volume"]
_wand_axes = [o for o in sel
if (o.Attributes.GetUserString("dossier_element_type")
or "") == "wand_axis"]
if (len(_wand_vols) == 2 and len(_wand_axes) == 0
and len(sel) == 2):
print("[SMART-JOIN] Layer-Join attempt: 2 wand_volume Breps")
try:
if _layer_join_attempt(doc, _wand_vols):
doc.Views.Redraw()
print("[SMART-JOIN] Layer-Join: Breps zu einem gemerged")
return
except Exception as ex:
print("[SMART-JOIN] Layer-Join exc:", ex)
# Info-Hint (T-Join unterstuetzt 1-Wand-Modus, L-Join braucht 2) # Info-Hint (T-Join unterstuetzt 1-Wand-Modus, L-Join braucht 2)
n_wand_axes = sum(1 for o in sel n_wand_axes = sum(1 for o in sel
if (o.Attributes.GetUserString("dossier_element_type") if (o.Attributes.GetUserString("dossier_element_type")
+117 -45
View File
@@ -2751,6 +2751,11 @@ class _ClusterVolumeSelectHandler(Rhino.UI.MouseCallback):
try: try:
if "Left" not in str(e.MouseButton): return if "Left" not in str(e.MouseButton): return
except Exception: pass 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 view = e.View
if view is None: return if view is None: return
vp = view.ActiveViewport vp = view.ActiveViewport
@@ -3731,19 +3736,33 @@ def _t_junction_layer_overrides(doc, my_meta, through_meta, ep_pt, out_dir,
break break
cur -= d cur -= d
# Backbone-Extension: IMMER 0 = column endet exakt am Through-axis, # Backbone-Extension: column drills durch through-body bis FAR FACE
# geht nie ueber outer face hinaus. Asymmetric Merge ist Resultat # auf out_dir-Seite (= durchbricht non-backbone Layer auf Far-Seite,
# der natuerlichen Geometrie: # z.B. Daemm + Putz von Aussenwand wenn Beton-Innenwand anstoesst).
# - Wenn T-Stem-Body auf gleicher Seite wie Through-Wand-Body # Resultat: "durchgehendes T" beim Backbone-Material.
# (= column passes durch wall body): alle Layer mergen via 3D # Backbone column extends in out_dir-Richtung bis Far-Face der
# Union + Carve (L-merge auf T-Stem-Layer-Seite, west_piece # MATCHING-Layer (= Stahl-Band) im through-wall. NICHT bis Body-Far-Face.
# getrennt). # = Beton drillt nur durch Stahl-Schicht, nicht durch Daemm/Putz.
# - Wenn T-Stem-Body auf gegenueberliegender Seite (= column endet # Case A (T-stem-axis covers body schon naturally) → ext=0.
# am Through-Aussenrand, betritt wall body nicht): kein Merge, # Case B (OPPOSITE side) → ext = backbone-layer-thickness auf out_dir-Seite.
# visually 2 separate Walls die sich am axis touchieren.
backbone_ext = 0.0 backbone_ext = 0.0
if backbone_ext is None: if backbone and backbone_dL is not None and backbone_dR is not None:
backbone_ext = float(b_dicke) * 0.5 # 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, standard_miter = _t_junction_miter(ep_pt, out_dir, b_tan, b_dicke,
through_meta.get("referenz", "mid")) 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) _ext_arr[_li_x] or 0)
break break
if not _backbone_mat: continue 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 _backbone_ext_val > 0:
if _is_end: if _is_end:
_my_axis_ext = _extend_axis_curve( _backbone_axis_ext = _extend_axis_curve(
geom, 0, _backbone_ext_val) geom, 0, _backbone_ext_val)
else: else:
_my_axis_ext = _extend_axis_curve( _backbone_axis_ext = _extend_axis_curve(
geom, _backbone_ext_val, 0) geom, _backbone_ext_val, 0)
else: else:
_my_axis_ext = geom _backbone_axis_ext = geom
_my_axis_ext = geom
try: try:
_ps = _my_axis_ext.PointAtStart _ps = _backbone_axis_ext.PointAtStart
_pe = _my_axis_ext.PointAtEnd _pe = _backbone_axis_ext.PointAtEnd
_gs = geom.PointAtStart _gs = geom.PointAtStart
_ge = geom.PointAtEnd _ge = geom.PointAtEnd
print(("[ELEMENTE] axis-ext: geom y=[{:.3f}{:.3f}]" 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( " is_end={})").format(
_gs.Y, _ge.Y, _ps.Y, _pe.Y, _gs.Y, _ge.Y, _ps.Y, _pe.Y,
_backbone_ext_val, _is_end)) _backbone_ext_val, _is_end))
@@ -8872,7 +8893,7 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
if _bbones: if _bbones:
_bd_l, _bd_r, _ = _bbones[0] _bd_l, _bd_r, _ = _bbones[0]
_backbone_col_rect = _layer_rect_2d( _backbone_col_rect = _layer_rect_2d(
_my_axis_ext, _bd_l, _bd_r) _backbone_axis_ext, _bd_l, _bd_r)
# 3D Version fuer Carve # 3D Version fuer Carve
if _backbone_col_rect is not None: if _backbone_col_rect is not None:
try: try:
@@ -8918,8 +8939,11 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
# Polygone werden gegen Backbone-Column geschnitten. # Polygone werden gegen Backbone-Column geschnitten.
_height = float(ok) - float(uk) _height = float(ok) - float(uk)
for _mat in list(_th_off_by_mat.keys()): 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) _is_backbone = (_mat == _backbone_mat)
_has_my_cols = _mat in _my_info_by_mat
# Cross-junction safe: nutze CURRENT through-Brep(s) # Cross-junction safe: nutze CURRENT through-Brep(s)
_layer_breps_3d = [] _layer_breps_3d = []
for _o in _th_objs_by_mat.get(_mat, []): 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) # (= fuer west-stub-filter spaeter)
_column_breps = [] _column_breps = []
_column_x_ranges = [] _column_x_ranges = []
for (_dl, _dr, _layer_i) in _my_info_by_mat[_mat]: _ax_for_col = (_backbone_axis_ext if _is_backbone
_r = _layer_rect_2d(_my_axis_ext, _dl, _dr) 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 if _r is None: continue
try: try:
_profile = _r.DuplicateCurve() _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 ({}):" print("[ELEMENTE] Column-extr exc ({}):"
.format(_mat), _ex) .format(_mat), _ex)
if not _layer_breps_3d: continue if not _layer_breps_3d: continue
# 3D Union all # Kein 3D Union — bei disjoint breps wuerde Union eine
if len(_layer_breps_3d) == 1: # Mega-Brep mit combined bbox machen, was den BBox-
_result_breps = _layer_breps_3d # overlap-filter unten kaputt macht. Jeder brep wird
else: # separat carved.
try: _result_breps = list(_layer_breps_3d)
_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 # Carve mit backbone-col + hoehere-prio through-bands
if not _is_backbone: if not _is_backbone:
_carve_breps = [] _carve_breps = []
@@ -9000,10 +9018,50 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
_carve_breps.append(_ob_br) _carve_breps.append(_ob_br)
except Exception: pass except Exception: pass
if _carve_breps: 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) _current = list(_result_breps)
for _cb in _carve_breps: for _ci, _cb in enumerate(_carve_breps):
_cb_bb = _cb.GetBoundingBox(True)
_next = [] _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: try:
_diff = rg.Brep.CreateBooleanDifference( _diff = rg.Brep.CreateBooleanDifference(
_br, _cb, 0.001) _br, _cb, 0.001)
@@ -9021,6 +9079,18 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
_result_breps = _current _result_breps = _current
# (West-stub filter entfernt — war zu aggressiv, # (West-stub filter entfernt — war zu aggressiv,
# discarded legitime through-wall west pieces) # 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 # 4. Validity + MergeCoplanarFaces
_result_breps = [_br for _br in _result_breps _result_breps = [_br for _br in _result_breps
if _br is not None and _br.IsValid] 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) _existing[_ei].Id, True)
except Exception: pass except Exception: pass
# T-Stem-Layer fuer dieses Material consumen # T-Stem-Layer fuer dieses Material consumen
for (_dl, _dr, _layer_i) in _my_info_by_mat[_mat]: # (nur wenn T-stem dieses Material ueberhaupt hat)
if _layer_i < len(layer_breps): if _has_my_cols:
_lb_old = layer_breps[_layer_i] for (_dl, _dr, _layer_i) in _my_info_by_mat[_mat]:
layer_breps[_layer_i] = ( if _layer_i < len(layer_breps):
None, _lb_old[1], _lb_old[2]) _lb_old = layer_breps[_layer_i]
layer_breps[_layer_i] = (
None, _lb_old[1], _lb_old[2])
print("[ELEMENTE] 2D-Phase2: {} ({}) → {} brep(s)" print("[ELEMENTE] 2D-Phase2: {} ({}) → {} brep(s)"
" aus {} cols".format( " aus {} cols".format(
_mat, _mat,