T-Junction angled: angle-aware Extension + Miter fuer flache T-Oberseite

Bei angled T-Junctions (nicht-90°) ragten Column-Ecken ueber Through-
Wand-Face raus (= "Stummel"). Fix in zwei Pfaden:

Cluster-Union (solid walls, _build_cluster_union_brep):
- Extension entlang my_tan = n_half / |cross(my_tan, n_tan)|. Fuer 90°
  bleibt = n_half, fuer angled wird laenger damit mitered End-Face auf
  Through-Body-Far-Face landet.
- Miter mit Nachbar-Tangent als miter_dir clippt L+R Curves an flache
  horizontale Linie aligned mit Through-Wand-Richtung.
- _make_volume_geometry erhaelt miter_start/_end.

Phase 2 (layered walls, _regenerate_element_body):
- Gleiches angle-aware Extension-Factor fuer _backbone_axis_ext.
- _layer_rect_2d erhaelt miter_start/_end Parameter.
- Backbone-col bekommt Miter am extended-end-Position.
- Non-backbone Spalten bekommen Std-Miter am Snap-Position.
- Beide aligned mit through-tan (b_tan) Richtung → T-Oberseite flach.

Diagnostic-Prints (bb-ext-calc, PRE-CARVE, CARVE cb, DO/SKIP carve)
bleiben drin fuer kommende Edge-Cases.
This commit is contained in:
2026-06-02 00:50:09 +02:00
parent 118bc51cc5
commit dcfafb18d1
+97 -12
View File
@@ -2563,7 +2563,14 @@ def _build_cluster_union_brep(doc, cluster_ids, uk, ok):
breps = [] breps = []
for wid, (ax, wm, g) in walls.items(): for wid, (ax, wm, g) in walls.items():
ext_start = 0.0; ext_end = 0.0 ext_start = 0.0; ext_end = 0.0
miter_start = None; miter_end = None
my_start = g.PointAtStart; my_end = g.PointAtEnd 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(): for nid, (n_ax, n_meta, n_g) in walls.items():
if nid == wid: continue if nid == wid: continue
# Extension um NACHBAR-dicke/2 (= bis zur Mitte bzw. Far-Face der # 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. # Seite als kleiner Stummel rausragen.
n_half = float(n_meta.get("dicke", 0.0)) * 0.5 n_half = float(n_meta.get("dicke", 0.0)) * 0.5
n_start = n_g.PointAtStart; n_end = n_g.PointAtEnd 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 # Endpunkt-Share: mein Endpunkt = ihr Endpunkt
if (my_start.DistanceTo(n_start) < eps or if (my_start.DistanceTo(n_start) < eps or
my_start.DistanceTo(n_end) < eps): 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 if (n_start.DistanceTo(my_ep) < eps or
n_end.DistanceTo(my_ep) < eps): n_end.DistanceTo(my_ep) < eps):
continue 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: 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: 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) ext_g = _extend_axis_curve(g, ext_start, ext_end)
b = _make_volume_geometry(ext_g, wm["dicke"], uk, ok, 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: if b is not None:
breps.append(b) breps.append(b)
if not breps: return None 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() 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 + """Liefert geschlossene XY-Rect-Curve fuer eine Schicht (Achse +
perp Offsets). Genutzt fuer 2D-Polylinen-Union beim T-Junction. perp Offsets). Genutzt fuer 2D-Polylinen-Union beim T-Junction.
Nutzt _offset_curve wie _make_wall_layer_brep wichtig fuer Nutzt _offset_curve wie _make_wall_layer_brep wichtig fuer
Boolean-Compatibility (PolyCurve statt PolylineCurve). 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 if not isinstance(axis_curve, rg.Curve): return None
d_l = float(d_left); d_r = float(d_right) d_l = float(d_left); d_r = float(d_right)
if abs(d_l - d_r) < 1e-9: return None 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) right = _offset_curve(ax_xy, plane, d_r, tol)
if not left or not right: return None if not left or not right: return None
L = left[0]; R = right[0] 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() R.Reverse()
cap_s = rg.LineCurve(L.PointAtEnd, R.PointAtStart) cap_s = rg.LineCurve(L.PointAtEnd, R.PointAtStart)
cap_e = rg.LineCurve(R.PointAtEnd, L.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) _ext_arr[_li_x] or 0)
break break
if not _backbone_mat: continue 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). # Backbone-axis = extended bis Through-Far-Face (drill).
# Non-backbone columns nutzen geom (= stoppen am Snap). # Angle-aware: ext entlang my_tan damit End-Face auf
if _backbone_ext_val > 0: # 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: if _is_end:
_backbone_axis_ext = _extend_axis_curve( _backbone_axis_ext = _extend_axis_curve(
geom, 0, _backbone_ext_val) geom, 0, _bb_ext_eff)
else: else:
_backbone_axis_ext = _extend_axis_curve( _backbone_axis_ext = _extend_axis_curve(
geom, _backbone_ext_val, 0) geom, _bb_ext_eff, 0)
else: else:
_backbone_axis_ext = geom _backbone_axis_ext = geom
_my_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: try:
_ps = _backbone_axis_ext.PointAtStart _ps = _backbone_axis_ext.PointAtStart
_pe = _backbone_axis_ext.PointAtEnd _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] _bbones = _my_info_by_mat[_backbone_mat]
if _bbones: if _bbones:
_bd_l, _bd_r, _ = _bbones[0] _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_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 # 3D Version fuer Carve
if _backbone_col_rect is not None: if _backbone_col_rect is not None:
try: try:
@@ -8959,10 +9037,17 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
_column_x_ranges = [] _column_x_ranges = []
_ax_for_col = (_backbone_axis_ext if _is_backbone _ax_for_col = (_backbone_axis_ext if _is_backbone
else _my_axis_ext) 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] _my_cols_iter = (_my_info_by_mat[_mat]
if _has_my_cols else []) if _has_my_cols else [])
for (_dl, _dr, _layer_i) in _my_cols_iter: 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 if _r is None: continue
try: try:
_profile = _r.DuplicateCurve() _profile = _r.DuplicateCurve()