diff --git a/rhino/elemente.py b/rhino/elemente.py index 6ac547c..b54e8ec 100644 --- a/rhino/elemente.py +++ b/rhino/elemente.py @@ -2563,7 +2563,14 @@ def _build_cluster_union_brep(doc, cluster_ids, uk, ok): breps = [] for wid, (ax, wm, g) in walls.items(): ext_start = 0.0; ext_end = 0.0 + miter_start = None; miter_end = None my_start = g.PointAtStart; my_end = g.PointAtEnd + try: + _myt = my_end - my_start + _ml = _myt.Length + my_tan = (_myt / _ml) if _ml > 1e-9 else rg.Vector3d(1, 0, 0) + except Exception: + my_tan = rg.Vector3d(1, 0, 0) for nid, (n_ax, n_meta, n_g) in walls.items(): if nid == wid: continue # Extension um NACHBAR-dicke/2 (= bis zur Mitte bzw. Far-Face der @@ -2571,6 +2578,17 @@ def _build_cluster_union_brep(doc, cluster_ids, uk, ok): # Seite als kleiner Stummel rausragen. n_half = float(n_meta.get("dicke", 0.0)) * 0.5 n_start = n_g.PointAtStart; n_end = n_g.PointAtEnd + try: + _nt = n_end - n_start + _nl = _nt.Length + n_tan = (_nt / _nl) if _nl > 1e-9 else rg.Vector3d(1, 0, 0) + except Exception: + n_tan = rg.Vector3d(1, 0, 0) + # Angle-aware Extension fuer T-Junction: ext entlang my_tan + # damit mitered End-Face exakt auf Nachbar-Body-Far-Face landet. + # ext = n_half / |my_tan · through_perp|. Fuer 90° = n_half. + _proj = abs(my_tan.X * n_tan.Y - my_tan.Y * n_tan.X) + n_ext = n_half / _proj if _proj > 1e-6 else n_half # Endpunkt-Share: mein Endpunkt = ihr Endpunkt if (my_start.DistanceTo(n_start) < eps or my_start.DistanceTo(n_end) < eps): @@ -2587,14 +2605,23 @@ def _build_cluster_union_brep(doc, cluster_ids, uk, ok): if (n_start.DistanceTo(my_ep) < eps or n_end.DistanceTo(my_ep) < eps): continue - # Verlaengere mein Brep bis zur Far-Face der Nachbar-Wand + # Angle-aware Extension + Miter aligned mit Nachbar-Tangent + # damit angled T-Junctions clean clippen (T-Oberseite flach). if is_start: - ext_start = max(ext_start, n_half) + ext_start = max(ext_start, n_ext) + _mpt = rg.Point3d(my_ep.X - my_tan.X * n_ext, + my_ep.Y - my_tan.Y * n_ext, 0) + miter_start = (_mpt, n_tan) else: - ext_end = max(ext_end, n_half) + ext_end = max(ext_end, n_ext) + _mpt = rg.Point3d(my_ep.X + my_tan.X * n_ext, + my_ep.Y + my_tan.Y * n_ext, 0) + miter_end = (_mpt, n_tan) ext_g = _extend_axis_curve(g, ext_start, ext_end) b = _make_volume_geometry(ext_g, wm["dicke"], uk, ok, - wm.get("referenz", "mid")) + wm.get("referenz", "mid"), + miter_start=miter_start, + miter_end=miter_end) if b is not None: breps.append(b) if not breps: return None @@ -3377,12 +3404,17 @@ 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): +def _layer_rect_2d(axis_curve, d_left, d_right, + miter_start=None, miter_end=None): """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).""" + Forced z=0 + CCW winding (= ClosedCurveOrientation check). + + miter_start/_end: optional (miter_pt, miter_dir) — clip L+R am Ende + auf die Miter-Linie. Fuer angled T-Junctions damit Column-Ecken + nicht ueber Through-Face rausragen.""" 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 @@ -3400,6 +3432,15 @@ def _layer_rect_2d(axis_curve, d_left, d_right): right = _offset_curve(ax_xy, plane, d_r, tol) if not left or not right: return None L = left[0]; R = right[0] + _max_ext = abs(d_l - d_r) * 5.0 + if miter_start is not None: + _mpt, _mdir = miter_start + L = _apply_miter(L, "start", _mpt, _mdir, _max_ext) + R = _apply_miter(R, "start", _mpt, _mdir, _max_ext) + if miter_end is not None: + _mpt, _mdir = miter_end + L = _apply_miter(L, "end", _mpt, _mdir, _max_ext) + R = _apply_miter(R, "end", _mpt, _mdir, _max_ext) R.Reverse() cap_s = rg.LineCurve(L.PointAtEnd, R.PointAtStart) cap_e = rg.LineCurve(R.PointAtEnd, L.PointAtStart) @@ -8824,18 +8865,50 @@ 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 tangent + n_tan fuer angle-aware extension + miter + try: + _gst = geom.PointAtStart + _get = geom.PointAtEnd + _vec = _get - _gst + _vl = _vec.Length + _mtan = ((_vec / _vl) if _vl > 1e-9 + else rg.Vector3d(0, 1, 0)) + except Exception: + _mtan = rg.Vector3d(0, 1, 0) + _ntan = _tj_info[1] # through-wall tangent + # Cross product magnitude = |sin(angle)| zwischen tan-Vektoren + # = cos(deviation from 90°). Fuer 90° T = 1. + _proj_ang = abs(_mtan.X * _ntan.Y - _mtan.Y * _ntan.X) + _ext_factor = 1.0 / _proj_ang if _proj_ang > 1e-6 else 1.0 # Backbone-axis = extended bis Through-Far-Face (drill). - # Non-backbone columns nutzen geom (= stoppen am Snap). - if _backbone_ext_val > 0: + # Angle-aware: ext entlang my_tan damit End-Face auf + # through-perp-Linie landet (= flach in n_tan-Richtung). + _bb_ext_eff = _backbone_ext_val * _ext_factor + if _bb_ext_eff > 1e-9: if _is_end: _backbone_axis_ext = _extend_axis_curve( - geom, 0, _backbone_ext_val) + geom, 0, _bb_ext_eff) else: _backbone_axis_ext = _extend_axis_curve( - geom, _backbone_ext_val, 0) + geom, _bb_ext_eff, 0) else: _backbone_axis_ext = geom _my_axis_ext = geom + # Miter fuer Phase 2 columns: aligned mit through-tan + # damit angled T-junction-Spalten flache End-Faces haben. + # Backbone-end miter am extended position. Non-backbone + # miter am Snap-Position (= geom-Ende). + _snap = _tj_info[3] + _bb_end_pt = rg.Point3d( + _snap.X + (_mtan.X * _bb_ext_eff + if _is_end + else -_mtan.X * _bb_ext_eff), + _snap.Y + (_mtan.Y * _bb_ext_eff + if _is_end + else -_mtan.Y * _bb_ext_eff), + 0) + _bb_miter = (_bb_end_pt, _ntan) + _std_miter = (rg.Point3d(_snap.X, _snap.Y, 0), _ntan) try: _ps = _backbone_axis_ext.PointAtStart _pe = _backbone_axis_ext.PointAtEnd @@ -8892,8 +8965,13 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name _bbones = _my_info_by_mat[_backbone_mat] if _bbones: _bd_l, _bd_r, _ = _bbones[0] + _bb_miter_args = ( + {"miter_end": _bb_miter} + if _is_end + else {"miter_start": _bb_miter}) _backbone_col_rect = _layer_rect_2d( - _backbone_axis_ext, _bd_l, _bd_r) + _backbone_axis_ext, _bd_l, _bd_r, + **_bb_miter_args) # 3D Version fuer Carve if _backbone_col_rect is not None: try: @@ -8959,10 +9037,17 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name _column_x_ranges = [] _ax_for_col = (_backbone_axis_ext if _is_backbone else _my_axis_ext) + _col_miter = (_bb_miter if _is_backbone + else _std_miter) + _col_miter_args = ( + {"miter_end": _col_miter} + if _is_end + else {"miter_start": _col_miter}) _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) + _r = _layer_rect_2d(_ax_for_col, _dl, _dr, + **_col_miter_args) if _r is None: continue try: _profile = _r.DuplicateCurve()