diff --git a/rhino/aliases/cmd/smart_join.py b/rhino/aliases/cmd/smart_join.py index 0f5b027..80d5db1 100644 --- a/rhino/aliases/cmd/smart_join.py +++ b/rhino/aliases/cmd/smart_join.py @@ -296,12 +296,80 @@ def _l_join_attempt(doc, sel): 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(): doc = Rhino.RhinoDoc.ActiveDoc if doc is None: return sel = list(doc.Objects.GetSelectedObjects(False, False)) if not sel: 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) n_wand_axes = sum(1 for o in sel if (o.Attributes.GetUserString("dossier_element_type") diff --git a/rhino/elemente.py b/rhino/elemente.py index 390e9f4..6ac547c 100644 --- a/rhino/elemente.py +++ b/rhino/elemente.py @@ -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,