T-Junction Phase 2 mit 3D Brep Union + Material-Prio-Carve

Asymmetric L-merge fuer Schichtdurchdringung:
- Backbone (= hoechste Material-Prio in beiden Waenden) bildet T-form
- Non-backbone Layer werden gecarved mit backbone-Column + through-Bands
  hoeherer Prio
- ext=0 fuer T-Stem-Axis (= column endet am snap, kein "drueber")
- 3D Brep Union via Brep.CreateBooleanUnion mit cross-junction safety
  (= aktueller through-Brep statt von Meta neu zu bauen)
- Cleanup: MergeCoplanarFaces

Plus:
- Innenwand Beton 20cm Style (Putz + Beton + Putz, ref-mid)
- curve_vertex_dots.py: gruene Vertex-Punkte fuer Polylinen/Curves
- Cluster-Volume Select Handler: Shift-Modifier fuer Multi-Select
- startup.py: Top-View maximieren on Doc-Open

Known limitation: Putz-Schicht kann in bestimmten Konfigurationen visuell
suedlich des Daemm-Band-Top weiter sichtbar sein (= edge case fuer
asymmetric layers). Naechster Schritt: manuelle 2D-Polygon-Konstruktion
statt 3D Boolean.
This commit is contained in:
2026-06-01 14:32:55 +02:00
parent 9cce8199c3
commit df56a54b66
4 changed files with 756 additions and 51 deletions
+161
View File
@@ -0,0 +1,161 @@
#! python3
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
"""
curve_vertex_dots.py
Display-only Vertex-Dots fuer GENERISCHE Curves (Polylinen, Linien,
Rectangles, NurbsCurves etc). Zeigt gruene Punkte an allen Vertices
selektierter Curves — hilft beim Visuell-Finden von Grip-Positionen
wenn die Curve eine Fuellung (Hatch) hat und schwer per Klick auf
einen einzelnen Vertex zu treffen ist.
Display-only — kein eigener Drag-Handler. User editiert Vertices via
Rhino's native _Grips (Punkte sichtbar machen + Standard-Drag) oder
direktes Object-Snapping waehrend Drag.
Skipt dossier-managed Curves (wand_axis, treppe_axis, schnitt_axis,
wand_outline, wand_centerline, raum_polylinie etc) — die haben ihre
eigenen Conduits oder duerfen nicht via Vertex editiert werden.
"""
import Rhino
import Rhino.Display as rd
import Rhino.Geometry as rg
import scriptcontext as sc
import System.Drawing as SD
# --- Konstanten ------------------------------------------------------------
_MARKER_RADIUS_PX = 6
_MARKER_FILL = SD.Color.FromArgb(200, 95, 168, 150) # accent-gruen
_MARKER_BORDER = SD.Color.FromArgb(255, 47, 93, 84)
# Dossier-managed Element-Types die NICHT mit generic dots versehen werden
# (= haben eigene Conduits oder sind nicht editierbar via Vertex-Click).
_SKIP_TYPES = {
"wand_axis", "wand_centerline", "wand_outline", "wand_volume",
"treppe_axis", "treppe_outline", "treppe_volume",
"schnitt_axis", "schnitt_outline",
"raum_polylinie", "raum_stempel",
"ausschnitt_polylinie",
"decke_polylinie", "decke_volume",
"dach_polylinie", "dach_volume",
}
# --- Helpers --------------------------------------------------------------
def _is_dossier_managed(obj):
"""True wenn obj ein dossier-managed Element ist (= Skip)."""
if obj is None or obj.IsDeleted: return True
try:
t = obj.Attributes.GetUserString("dossier_element_type") or ""
return t in _SKIP_TYPES
except Exception:
return False
def _curve_vertices(curve):
"""Liefert Liste von rg.Point3d fuer alle relevanten Vertices der
Curve. Verschiedene Curve-Types haben verschiedene Vertices:
- LineCurve: 2 Endpunkte
- PolylineCurve: alle Polyline-Punkte (deduplizert wenn closed)
- PolyCurve: rekursiv Segmente
- NurbsCurve/sonst: Start + End (control points nicht — zu viele)"""
pts = []
if curve is None: return pts
try:
if isinstance(curve, rg.PolylineCurve):
ok, pline = curve.TryGetPolyline()
if ok and pline is not None:
n = pline.Count
# Deduplizieren wenn closed (letzter Punkt = erster)
last = n
try:
if (n >= 2
and pline[0].DistanceTo(pline[n - 1]) < 1e-6):
last = n - 1
except Exception: pass
for i in range(last):
pts.append(rg.Point3d(pline[i]))
return pts
if isinstance(curve, rg.LineCurve):
pts.append(curve.PointAtStart)
pts.append(curve.PointAtEnd)
return pts
if isinstance(curve, rg.PolyCurve):
for i in range(curve.SegmentCount):
seg = curve.SegmentCurve(i)
if seg is None: continue
# Nur Start jedes Segments (End ist Start des naechsten)
pts.append(seg.PointAtStart)
# Letztes Segment-End anhaengen
try:
pts.append(curve.PointAtEnd)
except Exception: pass
return pts
# Generic Curve: nur Start + End
try:
pts.append(curve.PointAtStart)
pts.append(curve.PointAtEnd)
except Exception: pass
except Exception:
pass
return pts
# --- Conduit -------------------------------------------------------------
class _VertexDotConduit(rd.DisplayConduit):
"""Zeichnet bei jeder selektierten generischen Curve gruene Punkte
an allen Vertices."""
def DrawForeground(self, e):
try:
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return
try:
sel = list(doc.Objects.GetSelectedObjects(False, False))
except Exception: return
seen_curve_ids = set()
for obj in sel:
if _is_dossier_managed(obj): continue
try:
cid = str(obj.Id)
except Exception: continue
if cid in seen_curve_ids: continue
seen_curve_ids.add(cid)
geom = obj.Geometry
if not isinstance(geom, rg.Curve): continue
for pt in _curve_vertices(geom):
try:
e.Display.DrawPoint(
pt, rd.PointStyle.RoundControlPoint,
_MARKER_RADIUS_PX, _MARKER_FILL)
except Exception:
try: e.Display.DrawDot(
pt, "·", _MARKER_FILL, _MARKER_BORDER)
except Exception: pass
except Exception as ex:
print("[CURVE_DOTS] DrawForeground:", ex)
# --- Install -------------------------------------------------------------
_STICKY_CONDUIT = "_dossier_curve_vertex_dots_conduit"
def install_curve_vertex_dots():
"""Idempotent: alten Conduit disable, neuen installieren."""
try:
old = sc.sticky.get(_STICKY_CONDUIT)
if old is not None:
try: old.Enabled = False
except Exception: pass
conduit = _VertexDotConduit()
conduit.Enabled = True
sc.sticky[_STICKY_CONDUIT] = conduit
print("[CURVE_DOTS] Vertex-Dot-Conduit aktiv")
except Exception as ex:
print("[CURVE_DOTS] install:", ex)
+534 -51
View File
@@ -2214,7 +2214,8 @@ def _detect_t_junction(doc, geschoss_id, wall_id, endpoint,
tan = geom.TangentAt(t)
return (meta["id"],
rg.Vector3d(tan.X, tan.Y, 0),
float(meta["dicke"]))
float(meta["dicke"]),
meta.get("referenz", "mid"))
except Exception: continue
return None
@@ -2299,32 +2300,30 @@ def _detect_through_wall_at(doc, partners_at_joint, exclude_self_tan):
# Gefunden: ti/tj formen die Through-Wand
mi = _wand_meta_by_id(doc, wi)
b_dicke = float((mi or {}).get("dicke", 0.25))
return (rg.Vector3d(ti.X, ti.Y, 0), b_dicke)
b_ref = (mi or {}).get("referenz", "mid")
return (rg.Vector3d(ti.X, ti.Y, 0), b_dicke, b_ref)
return None
def _t_junction_miter(endpoint, out_dir, b_tan, b_dicke):
def _t_junction_miter(endpoint, out_dir, b_tan, b_dicke, b_referenz="mid"):
"""Berechnet (miter_pt, miter_dir) fuer einen T-Stoss.
miter_dir = Tangente der Durchgangs-Wand (Linie laeuft parallel zu B's Achse).
miter_pt = endpoint verschoben um d_B/2 in Approach-Richtung also auf
der NAHEN Aussenflaeche von B (der Seite an der A ankommt).
A (= T-Stem, das zweite Wand-Stueck) wird an dieser Stelle abgeschnitten;
B (= Durchgangswand) bleibt unveraendert. Das ist das BIM-Standard-
Verhalten: 'erste Wand bleibt, zweite haengt sich dran'."""
miter_pt = endpoint verschoben bis zur NAHEN Aussenflaeche von B.
Beruecksichtigt B's referenz: bei mid ist Axis Centerline (Aussen-
flaechen ±d/2), bei left/right ist Axis auf einer Aussenkante."""
perp_b = rg.Vector3d(-b_tan.Y, b_tan.X, 0)
try: perp_b.Unitize()
except Exception: return None
# A's Body liegt auf der Seite -out_dir. Approach-Seite (perp_b
# ausgerichtet zur Approach) = sign(dot(-out_dir, perp_b)).
s = -(out_dir.X * perp_b.X + out_dir.Y * perp_b.Y)
if abs(s) < 1e-6:
# A parallel zu B — kein sauberer T-Stoss
return None
if abs(s) < 1e-6: return None
side = 1.0 if s > 0 else -1.0
off = float(b_dicke) * 0.5 * side
mpt = rg.Point3d(endpoint.X + perp_b.X * off,
endpoint.Y + perp_b.Y * off, 0)
# B's Aussenflaechen-Offsets entlang perp_b
start_off, d_total = _wall_offsets_from_referenz(b_dicke, b_referenz)
off_a = start_off
off_b = start_off - d_total
# Nahe Aussenflaeche = die in approach-Richtung naehere (= side-Richtung)
near_off = max(off_a, off_b) if side > 0 else min(off_a, off_b)
mpt = rg.Point3d(endpoint.X + perp_b.X * near_off,
endpoint.Y + perp_b.Y * near_off, 0)
mdir = rg.Vector3d(b_tan.X, b_tan.Y, 0)
try: mdir.Unitize()
except Exception: pass
@@ -2867,11 +2866,17 @@ class _ClusterVolumeSelectHandler(Rhino.UI.MouseCallback):
best_d2 = d2; best_axis = c_ax
except Exception: continue
if best_axis is None: return
# Shift-Modifier: ADD zur Selektion (kein UnselectAll)
try:
_shift = bool(e.ShiftKeyDown)
except Exception:
_shift = False
try: e.Cancel = True
except Exception: pass
self._busy = True
try:
doc.Objects.UnselectAll()
if not _shift:
doc.Objects.UnselectAll()
doc.Objects.Select(best_axis.Id, True)
try: view.Redraw()
except Exception: pass
@@ -3367,6 +3372,53 @@ 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):
"""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)."""
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
try:
# Force axis to z=0 plane
ax_xy = axis_curve.DuplicateCurve()
if abs(ax_xy.PointAtStart.Z) > 1e-9 or abs(ax_xy.PointAtEnd.Z) > 1e-9:
ax_xy.Transform(rg.Transform.PlaneToPlane(
rg.Plane(rg.Point3d(0,0,ax_xy.PointAtStart.Z),
rg.Vector3d.ZAxis),
rg.Plane.WorldXY))
plane = rg.Plane.WorldXY
tol = 0.001
left = _offset_curve(ax_xy, plane, d_l, tol)
right = _offset_curve(ax_xy, plane, d_r, tol)
if not left or not right: return None
L = left[0]; R = right[0]
R.Reverse()
cap_s = rg.LineCurve(L.PointAtEnd, R.PointAtStart)
cap_e = rg.LineCurve(R.PointAtEnd, L.PointAtStart)
joined = rg.Curve.JoinCurves([L, cap_s, R, cap_e], tol)
if not joined or len(joined) == 0 or not joined[0].IsClosed:
return None
out = joined[0]
# Force z=0 (offset_curve might preserve original z)
try:
if (abs(out.PointAtStart.Z) > 1e-9):
xform = rg.Transform.Translation(0, 0, -out.PointAtStart.Z)
out.Transform(xform)
except Exception: pass
# Force CCW orientation (= positive area in WorldXY)
try:
ori = out.ClosedCurveOrientation(rg.Plane.WorldXY)
if ori == rg.CurveOrientation.Clockwise:
out.Reverse()
except Exception: pass
return out
except Exception:
return None
def _wall_offsets_from_referenz(dicke, referenz):
"""Liefert (start_offset, d_total) — start_offset ist der Wert von 'links'
relativ zur Achse, d_total ist die Summe der Wand-Dicke (immer positiv)."""
@@ -3600,19 +3652,38 @@ def _wand_meta_prio(doc, meta):
except Exception: return 500
# Material-Prio fuer T-Junction Schichtdurchdringung: an einem T-Stoss
# zwischen zwei layered Waenden geht NUR die hoechste gemeinsame
# Material-Prio als 'Backbone' durch + uniont (= T-Form). Alle anderen
# T-Stem-Layer T-mitern am Near-Face → bei symmetrisch geschichteten
# Waenden ergibt das automatisch L-Stoesse mit gleichfarbigen Aussenlagen.
_MATERIAL_PRIO = {
"stahlbeton": 800,
"beton": 800,
"mauerwerk": 600,
"kalksandstein": 600,
"ziegel": 550,
"holzstaender": 400,
"holz": 400,
"daemmung": 200,
"putz": 100,
}
def _material_prio(mat):
if not mat: return 0
return _MATERIAL_PRIO.get(mat.strip().lower(), 300)
def _t_junction_layer_overrides(doc, my_meta, through_meta, ep_pt, out_dir,
b_tan, b_dicke):
"""Per-Layer Schichtdurchdringung bei T-Junction zwischen layered Waenden.
Pro T-Stem-Schicht wird in der Through-Wand nach einer Schicht mit
GLEICHEM MATERIAL gesucht:
- Match: T-Stem-Layer extends durch die Through-Wand (Axis-Extension um
b_dicke/2 = bis Far-Face) visuell verbunden
- No Match: T-Stem-Layer stoppt am Near-Face (Standard T-Miter)
NEU: nur das BACKBONE-Material extends + uniont. Backbone = das mit
hoechster Material-Prio das in BEIDEN Waenden vorkommt. Andere Layer
stoppen am Near-Face (Standard T-Miter).
Returns (per_layer_ext, per_layer_miter) Listen pro T-Stem-Layer.
Wenn my keine Layers hat: (None, None) (Caller faellt auf uniform Miter
zurueck)."""
Returns (per_layer_ext, per_layer_miter) Listen pro T-Stem-Layer."""
if not my_meta or not through_meta: return None, None
my_layers = my_meta.get("wand_layers") or []
if not my_layers: return None, None
@@ -3624,26 +3695,101 @@ def _t_junction_layer_overrides(doc, my_meta, through_meta, ep_pt, out_dir,
mat = (l.get("material") or "").strip()
if mat: th_materials.add(mat)
else:
# Through ist solid → 1 Material aus Style
try:
sm = _wand_solid_material(doc, through_meta) if doc else ""
if sm: th_materials.add(sm)
except Exception: pass
standard_miter = _t_junction_miter(ep_pt, out_dir, b_tan, b_dicke)
extension = float(b_dicke) * 0.5 # bis Far-Face
# Backbone = hoechste Material-Prio in BEIDEN Waenden
my_materials = set()
for layer in my_layers:
m = (layer.get("material") or "").strip()
if m: my_materials.add(m)
common = my_materials & th_materials
backbone = None
backbone_prio = -1
for m in common:
p = _material_prio(m)
if p > backbone_prio:
backbone = m
backbone_prio = p
# Backbone-Schicht-Position in der Through-Wand finden (perp-Offsets)
backbone_dL = None
backbone_dR = None
if backbone and through_meta.get("wand_layered") and th_layers:
th_dicke = float(through_meta.get("dicke", 0) or 0)
th_ref = through_meta.get("referenz", "mid")
start_off, _ = _wall_offsets_from_referenz(th_dicke, th_ref)
cur = start_off
for tl in th_layers:
d = float(tl.get("dicke", 0) or 0)
if d <= 0: continue
if (tl.get("material") or "").strip() == backbone:
backbone_dL = cur
backbone_dR = cur - d
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_ext = 0.0
if backbone_ext is None:
backbone_ext = float(b_dicke) * 0.5
standard_miter = _t_junction_miter(ep_pt, out_dir, b_tan, b_dicke,
through_meta.get("referenz", "mid"))
# Through-Layer pro Material indexieren (Liste von (d_l, d_r))
through_by_mat_pos = {}
if through_meta.get("wand_layered") and th_layers:
th_dicke2 = float(through_meta.get("dicke", 0) or 0)
th_ref2 = through_meta.get("referenz", "mid")
start_off2, _ = _wall_offsets_from_referenz(th_dicke2, th_ref2)
cur2 = start_off2
for tl in th_layers:
d2 = float(tl.get("dicke", 0) or 0)
if d2 <= 0: continue
m2 = (tl.get("material") or "").strip()
if m2:
through_by_mat_pos.setdefault(m2, []).append(
(cur2, cur2 - d2))
cur2 -= d2
# perp_b und Approach-Richtung
perp_bx = rg.Vector3d(-b_tan.Y, b_tan.X, 0)
try: perp_bx.Unitize()
except Exception: pass
dot_opx = out_dir.X * perp_bx.X + out_dir.Y * perp_bx.Y
b_tan_u = rg.Vector3d(b_tan.X, b_tan.Y, 0)
try: b_tan_u.Unitize()
except Exception: pass
per_ext = []
per_miter = []
match_log = []
for layer in my_layers:
mat = (layer.get("material") or "").strip()
if mat and mat in th_materials:
# Match → durchstossen
per_ext.append(extension)
if backbone and mat == backbone:
per_ext.append(backbone_ext)
per_miter.append(None)
match_log.append("{}=BACKBONE(+{:.3f})".format(mat, backbone_ext))
else:
# Kein Match → an Near-Face stoppen
# Non-backbone: Standard T-Miter an Through-Aussenkante.
# T-Stem-Layer + Through-Putz-Band touchieren am L-Korner
# mit gleichem Material (Same-color visual L, seam moeglich).
per_ext.append(0.0)
per_miter.append(standard_miter)
match_log.append("{}=stop".format(mat or "?"))
print("[ELEMENTE] T-Junction Schichtdurchdringung: backbone={} "
"(prio={}, ext={:.3f}), through-mats={}, layers: {}".format(
backbone or "none", backbone_prio, backbone_ext,
sorted(th_materials), ", ".join(match_log)))
return per_ext, per_miter
@@ -8507,24 +8653,21 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
element_id, p_s,
exclude_ids=chain_set)
if tj is not None:
_oid, b_tan, b_dicke = tj
_oid, b_tan, b_dicke, b_ref = tj
if _wand_should_apply_t_miter(doc, meta, _oid):
tm = _t_junction_miter(p_s, out_s, b_tan, b_dicke)
tm = _t_junction_miter(p_s, out_s, b_tan,
b_dicke, b_ref)
if tm is not None: miter_start = tm
# Through-Meta merken fuer per-Layer Schicht-
# durchdringung (s. unten bei layer_breps build)
t_junction_start = (_oid, b_tan, b_dicke, p_s, out_s)
else:
# 3+ Joint: ich bin T-Stem wenn meine Tangente NICHT
# collinear mit zwei collinearen Partner-Tangenten ist.
# Eigene Tangente (gerichtet aus Body raus) am Start = -TangentAtStart
_my_t = geom.TangentAtStart
_my_out_tan = rg.Vector3d(-_my_t.X, -_my_t.Y, 0)
through = _detect_through_wall_at(
doc, joints.get(key_s, []), _my_out_tan)
if through is not None:
b_tan, b_dicke = through
tm = _t_junction_miter(p_s, out_s, b_tan, b_dicke)
b_tan, b_dicke, b_ref = through
tm = _t_junction_miter(p_s, out_s, b_tan,
b_dicke, b_ref)
if tm is not None: miter_start = tm
if out_e is not None:
key_e = _pt_key(p_e)
@@ -8541,20 +8684,21 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
element_id, p_e,
exclude_ids=chain_set)
if tj is not None:
_oid, b_tan, b_dicke = tj
_oid, b_tan, b_dicke, b_ref = tj
if _wand_should_apply_t_miter(doc, meta, _oid):
tm = _t_junction_miter(p_e, out_e, b_tan, b_dicke)
tm = _t_junction_miter(p_e, out_e, b_tan,
b_dicke, b_ref)
if tm is not None: miter_end = tm
t_junction_end = (_oid, b_tan, b_dicke, p_e, out_e)
else:
# 3+ Joint: T-Stem-Erkennung (analog start)
_my_t = geom.TangentAtEnd
_my_out_tan = rg.Vector3d(_my_t.X, _my_t.Y, 0)
through = _detect_through_wall_at(
doc, joints.get(key_e, []), _my_out_tan)
if through is not None:
b_tan, b_dicke = through
tm = _t_junction_miter(p_e, out_e, b_tan, b_dicke)
b_tan, b_dicke, b_ref = through
tm = _t_junction_miter(p_e, out_e, b_tan,
b_dicke, b_ref)
if tm is not None: miter_end = tm
except Exception as ex:
print("[ELEMENTE] wall joints:", ex)
@@ -8589,14 +8733,346 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
per_layer_ext_start=pl_ext_s, per_layer_ext_end=pl_ext_e,
per_layer_miter_start=pl_miter_s,
per_layer_miter_end=pl_miter_e)
# Diagnostic: layer build status — User sieht oft "solid" obwohl
# walls layered sind, kontrollieren obs an Brep-Build oder Display liegt
# Diagnostic: layer build status
_n_ok = sum(1 for (b, _c, _n) in layer_breps if b is not None)
_n_fail = len(layer_breps) - _n_ok
print("[ELEMENTE] layered build {} (chain={}): {}/{} layers built"
" (def={} layers)".format(
element_id, len(chain_ids) if chain_ids else 1,
_n_ok, len(layer_breps), len(layers_def)))
# Phase 2 Schichtdurchdringung (2D-Polylinen-Approach):
# Pro Material 2D-Rechtecke (Through-Bands + T-Stem-Spalten)
# bauen, in 2D unionen, fuer non-backbone Material mit
# Backbone-Column subtrahieren (= 2D-Carve), dann extrudieren.
# Robuster als 3D-BoolUnion mit touching breps.
try:
import json as _j
for _tj_info, _is_end in ((t_junction_start, False),
(t_junction_end, True)):
if _tj_info is None: continue
_toid = _tj_info[0]
_mit_arr = pl_miter_e if _is_end else pl_miter_s
_ext_arr = pl_ext_e if _is_end else pl_ext_s
if _mit_arr is None: continue
# Through axis + meta
_th_axis = _find_axis(doc, _toid)
if _th_axis is None: continue
_th_meta = _read_meta(_th_axis)
if not _th_meta or not _th_meta.get("wand_layered"): continue
_th_geom = _th_axis.Geometry
if not isinstance(_th_geom, rg.Curve): continue
_th_layers = _th_meta.get("wand_layers") or []
_th_ref = _th_meta.get("referenz", "mid")
_th_dicke = float(_th_meta.get("dicke", 0) or 0)
# Through-Layer-Offsets per Material
_th_start, _ = _wall_offsets_from_referenz(
_th_dicke, _th_ref)
_th_off_by_mat = {}
_cur = _th_start
for _tl in _th_layers:
_d = float(_tl.get("dicke", 0) or 0)
if _d <= 0: continue
_m = (_tl.get("material") or "").strip()
if _m:
_th_off_by_mat.setdefault(_m, []).append(
(_cur, _cur - _d))
_cur -= _d
# T-Stem-Layer-Offsets + layer-idx per Material
_my_layers = meta.get("wand_layers") or []
_my_dicke = float(meta.get("dicke", 0) or 0)
_my_ref = meta.get("referenz", "mid")
_my_start, _ = _wall_offsets_from_referenz(
_my_dicke, _my_ref)
_my_info_by_mat = {}
_cur = _my_start
for _i, _ml in enumerate(_my_layers):
_d = float(_ml.get("dicke", 0) or 0)
if _d <= 0: continue
_m = (_ml.get("material") or "").strip()
if _m:
_my_info_by_mat.setdefault(_m, []).append(
(_cur, _cur - _d, _i))
_cur -= _d
# Backbone-Material + Ext-Wert
_backbone_mat = None
_backbone_ext_val = 0.0
for _li_x in range(len(layers_def)):
if (_li_x < len(_mit_arr)
and _mit_arr[_li_x] is None):
_backbone_mat = (layers_def[_li_x].get(
"material") or "").strip()
if _ext_arr and _li_x < len(_ext_arr):
_backbone_ext_val = float(
_ext_arr[_li_x] or 0)
break
if not _backbone_mat: continue
# T-Stem axis (extended for backbone-side)
if _backbone_ext_val > 0:
if _is_end:
_my_axis_ext = _extend_axis_curve(
geom, 0, _backbone_ext_val)
else:
_my_axis_ext = _extend_axis_curve(
geom, _backbone_ext_val, 0)
else:
_my_axis_ext = geom
try:
_ps = _my_axis_ext.PointAtStart
_pe = _my_axis_ext.PointAtEnd
_gs = geom.PointAtStart
_ge = geom.PointAtEnd
print(("[ELEMENTE] axis-ext: geom y=[{:.3f}{:.3f}]"
" → ext y=[{:.3f}{:.3f}] (ext_val={:.3f},"
" is_end={})").format(
_gs.Y, _ge.Y, _ps.Y, _pe.Y,
_backbone_ext_val, _is_end))
except Exception: pass
# Backbone-Near-Face in T-Stem-Axis-Richtung (=
# wo Backbone-Layer auf Approach-Seite anfaengt).
# Non-backbone matching columns sollen DORT stoppen
# (Backbone hat hoehere Prio, Putz/etc weicht).
_btan_v = _tj_info[1]
_od_v = _tj_info[4]
_perp_b = rg.Vector3d(-_btan_v.Y, _btan_v.X, 0)
try: _perp_b.Unitize()
except Exception: pass
_dot_op = _od_v.X * _perp_b.X + _od_v.Y * _perp_b.Y
_backbone_near_dist = None
if _backbone_mat in _th_off_by_mat:
_bb_through = _th_off_by_mat[_backbone_mat]
if _bb_through:
_bb_dl, _bb_dr = _bb_through[0]
if _dot_op > 0:
_bn_perp = min(_bb_dl, _bb_dr)
else:
_bn_perp = max(_bb_dl, _bb_dr)
_backbone_near_dist = _bn_perp * (
1 if _dot_op > 0 else -1)
# Helper: T-Stem axis clipped an gegebener axial-distance
# vom Endpoint (negativ = retract, positiv = extend)
def _ax_clipped(_dist):
try:
_p1 = geom.PointAtStart
_p2 = geom.PointAtEnd
_tan = _p2 - _p1
if _tan.Length < 1e-9: return geom
_tan.Unitize()
if _is_end:
_new_end = _p2 + _tan * _dist
return rg.LineCurve(_p1, _new_end)
else:
_new_start = _p1 - _tan * _dist
return rg.LineCurve(_new_start, _p2)
except Exception:
return geom
# Backbone-Column 2D Rect + 3D Brep (fuer Carve)
_backbone_col_rect = None
_backbone_col_brep_3d = None
if _backbone_mat in _my_info_by_mat:
_bbones = _my_info_by_mat[_backbone_mat]
if _bbones:
_bd_l, _bd_r, _ = _bbones[0]
_backbone_col_rect = _layer_rect_2d(
_my_axis_ext, _bd_l, _bd_r)
# 3D Version fuer Carve
if _backbone_col_rect is not None:
try:
_bbh = float(ok) - float(uk)
_bbp = _backbone_col_rect.DuplicateCurve()
if abs(uk) > 1e-9:
_bbp.Transform(rg.Transform.Translation(
0, 0, uk))
_bbe = rg.Extrusion.Create(
_bbp, _bbh, True)
if _bbe is not None:
_bbb = _bbe.ToBrep()
if _bbb is not None and _bbb.IsValid:
_backbone_col_brep_3d = _bbb
except Exception: pass
# Through wand_volume objects per material
_th_objs_by_mat = {}
for _o in doc.Objects:
try:
if (_o.Attributes.GetUserString(_KEY_TYPE)
!= "wand_volume"): continue
if (_o.Attributes.GetUserString(_KEY_ID)
!= _toid): continue
_idx_s = _o.Attributes.GetUserString(
_KEY_WAND_LAYER_IDX) or ""
_lj = _o.Attributes.GetUserString(
_KEY_WAND_LAYERS) or ""
if not _idx_s or not _lj: continue
_tl = _j.loads(_lj)
_i = int(_idx_s)
if 0 <= _i < len(_tl):
_m = (_tl[_i].get("material") or "").strip()
if _m:
_th_objs_by_mat.setdefault(
_m, []).append(_o)
except Exception: pass
# 2D-Polylinen Union per Material:
# ALLE matching T-Stem-Columns nutzen die GLEICHE
# _my_axis_ext (= backbone-extended). Damit reichen
# ALLE Schichten gleich hoch ins Through (nicht nur
# Backbone) — Daemm + Putz visuell durchgehend.
# Carve verhindert Material-Konflikte: non-backbone
# 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
_is_backbone = (_mat == _backbone_mat)
# Cross-junction safe: nutze CURRENT through-Brep(s)
_layer_breps_3d = []
for _o in _th_objs_by_mat.get(_mat, []):
try:
_br_cur = _o.Geometry
if isinstance(_br_cur, rg.Brep) and _br_cur.IsValid:
_layer_breps_3d.append(
_br_cur.DuplicateBrep())
except Exception: pass
# Sammle T-Stem-Column Rects + Column-X-Range
# (= 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)
if _r is None: continue
try:
_profile = _r.DuplicateCurve()
if abs(uk) > 1e-9:
_profile.Transform(
rg.Transform.Translation(0, 0, uk))
_extr = rg.Extrusion.Create(
_profile, _height, True)
if _extr is None: continue
_br = _extr.ToBrep()
if _br is None or not _br.IsValid: continue
_column_breps.append(_br)
# Column x range fuer filter
_cbb = _br.GetBoundingBox(True)
_column_x_ranges.append(
(_cbb.Min.X, _cbb.Max.X))
_layer_breps_3d.append(_br)
except Exception as _ex:
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
# Carve mit backbone-col + hoehere-prio through-bands
if not _is_backbone:
_carve_breps = []
if _backbone_col_brep_3d is not None:
_carve_breps.append(_backbone_col_brep_3d)
_my_prio = _material_prio(_mat)
for _other_mat, _bands in _th_off_by_mat.items():
if _other_mat == _mat: continue
_other_prio = _material_prio(_other_mat)
if _other_prio <= _my_prio: continue
for (_obdl, _obdr) in _bands:
_ob_rect = _layer_rect_2d(
_th_geom, _obdl, _obdr)
if _ob_rect is None: continue
try:
_ob_profile = _ob_rect.DuplicateCurve()
if abs(uk) > 1e-9:
_ob_profile.Transform(
rg.Transform.Translation(
0, 0, uk))
_ob_extr = rg.Extrusion.Create(
_ob_profile, _height, True)
if _ob_extr is None: continue
_ob_br = _ob_extr.ToBrep()
if (_ob_br is not None
and _ob_br.IsValid):
_carve_breps.append(_ob_br)
except Exception: pass
if _carve_breps:
_current = list(_result_breps)
for _cb in _carve_breps:
_next = []
for _br in _current:
try:
_diff = rg.Brep.CreateBooleanDifference(
_br, _cb, 0.001)
except Exception:
_next.append(_br); continue
if (_diff is not None
and len(_diff) > 0):
for _db in _diff:
if (_db is not None
and _db.IsValid):
_next.append(_db)
else:
_next.append(_br)
_current = _next
_result_breps = _current
# (West-stub filter entfernt — war zu aggressiv,
# discarded legitime through-wall west pieces)
# 4. Validity + MergeCoplanarFaces
_result_breps = [_br for _br in _result_breps
if _br is not None and _br.IsValid]
for _br in _result_breps:
try: _br.MergeCoplanarFaces(0.01)
except Exception: pass
# Re-Extrude entfernt — war Bug-Quelle wo carve-
# holes verloren gingen. Direkter carved Brep ist
# OK (= 2 disconnected pieces nach BoolDifference).
if not _result_breps: continue
# Diagnostic: BBox y-range per result brep
for _bri, _br_diag in enumerate(_result_breps):
try:
_bb_d = _br_diag.GetBoundingBox(True)
print("[ELEMENTE] {} result[{}] y=[{:.3f}"
",{:.3f}]".format(
_mat, _bri, _bb_d.Min.Y, _bb_d.Max.Y))
except Exception: pass
# Replace through objects mit Extrude-Resultaten
_existing = _th_objs_by_mat.get(_mat, [])
for _ri in range(len(_result_breps)):
if _ri < len(_existing):
try:
doc.Objects.Replace(
_existing[_ri].Id, _result_breps[_ri])
except Exception as _ex:
print("[ELEMENTE] 2D Replace:", _ex)
else:
if _existing:
try:
_attrs = _existing[0].Attributes.Duplicate()
doc.Objects.AddBrep(
_result_breps[_ri], _attrs)
except Exception as _ex:
print("[ELEMENTE] 2D Add:", _ex)
for _ei in range(len(_result_breps), len(_existing)):
try: doc.Objects.Delete(
_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])
print("[ELEMENTE] 2D-Phase2: {} ({}) → {} brep(s)"
" aus {} cols".format(
_mat,
"backbone" if _is_backbone else "L-merge",
len(_result_breps), len(_column_breps)))
except Exception as _ex:
print("[ELEMENTE] T-Junction Phase2 2D:", _ex)
else:
single_brep = _make_volume_geometry(
geom, meta["dicke"], uk, ok,
@@ -17843,6 +18319,13 @@ def _install_listeners(bridge):
install_cluster_select_handler()
except Exception as ex:
print("[ELEMENTE] cluster-select install:", ex)
# Generic Vertex-Dots fuer Curves (Polylinen, Rectangles etc) —
# display-only, hilft beim visuellen Finden von Grip-Positionen.
try:
import curve_vertex_dots
curve_vertex_dots.install_curve_vertex_dots()
except Exception as ex:
print("[ELEMENTE] curve_vertex_dots install:", ex)
# Pre-Warm: native OpenNURBS-Libraries beim Plugin-Start laden um den
# First-Call-Lag zu vermeiden (User-Meldung: beim ersten Wand-Verbinden
# haengt das UI kurz, danach nicht mehr → native code wird lazy-loaded).
+16
View File
@@ -349,6 +349,22 @@ _PROJECT_SETTINGS_DEFAULTS = {
{"material": "Daemmung", "dicke": 0.10},
{"material": "Putz", "dicke": 0.02},
]},
{"id": "style_innen_holz", "name": "Innenwand Holzstaender 14 cm",
"prio": 250, "dicke": 0.14, "referenz": "mid",
"layered": True, "material": "",
"layers": [
{"material": "Putz", "dicke": 0.015},
{"material": "Holzstaender", "dicke": 0.110},
{"material": "Putz", "dicke": 0.015},
]},
{"id": "style_innen_beton", "name": "Innenwand Beton 20 cm",
"prio": 700, "dicke": 0.23, "referenz": "mid",
"layered": True, "material": "",
"layers": [
{"material": "Putz", "dicke": 0.015},
{"material": "Stahlbeton", "dicke": 0.200},
{"material": "Putz", "dicke": 0.015},
]},
],
"project": {
"name": "",
+45
View File
@@ -151,6 +151,45 @@ def _assign_default_display_modes(doc):
print("[STARTUP] view-modes: {} Viewport(s) gesetzt".format(n_set))
_DOC_FLAG_VIEW_MAXIMIZED = "dossier_top_view_maximized"
def _maximize_top_view(doc):
"""Maximiert den Top-Viewport (= einzige aktive View statt 4-Viewport-
Default). Persistiert Flag in doc.Strings laeuft nur EINMAL pro Doc.
User-Overrides (manuelles Wechseln zu 4-View etc) bleiben erhalten."""
if doc is None: return
try:
if doc.Strings.GetValue(_DOC_FLAG_VIEW_MAXIMIZED) == "1":
return # schon initialisiert
except Exception: pass
try:
top_view = None
for view in doc.Views:
try:
vp = view.ActiveViewport
if vp is None: continue
if vp.Name == "Top":
top_view = view
break
except Exception: pass
if top_view is None:
print("[STARTUP] view-max: kein Top-Viewport gefunden")
return
try:
top_view.Maximized = True
doc.Views.ActiveView = top_view
doc.Views.Redraw()
print("[STARTUP] view-max: Top-Viewport maximiert")
except Exception as ex:
print("[STARTUP] view-max set:", ex); return
try:
doc.Strings.SetString(_DOC_FLAG_VIEW_MAXIMIZED, "1")
except Exception: pass
except Exception as ex:
print("[STARTUP] view-max:", ex)
_DOC_FLAG_UNIT_CHECKED = "dossier_unit_checked"
@@ -227,6 +266,7 @@ def _on_doc_opened(sender, e):
import panel_base
panel_base.migrate_to_dossier(doc)
_assign_default_display_modes(doc)
_maximize_top_view(doc)
_check_doc_unit(doc)
except Exception as ex:
print("[STARTUP] _on_doc_opened:", ex)
@@ -301,6 +341,11 @@ def _load_all(sender, e):
_pb._t_mark("post_init", "view_modes", _t_vm)
except Exception as ex:
print("[STARTUP] view-modes assign:", ex)
# Top-View maximieren (= einzige aktive View statt 4-View Default)
try:
_maximize_top_view(Rhino.RhinoDoc.ActiveDoc)
except Exception as ex:
print("[STARTUP] view-max:", ex)
# Unit-Check fuer das beim Start aktive Doc — fragt einmal pro Doc
# wenn doc.ModelUnitSystem != Project-Setting
_t_uc = _t.time()