diff --git a/rhino/curve_vertex_dots.py b/rhino/curve_vertex_dots.py new file mode 100644 index 0000000..edb5e45 --- /dev/null +++ b/rhino/curve_vertex_dots.py @@ -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) diff --git a/rhino/elemente.py b/rhino/elemente.py index 071daf9..390e9f4 100644 --- a/rhino/elemente.py +++ b/rhino/elemente.py @@ -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). diff --git a/rhino/rhinopanel.py b/rhino/rhinopanel.py index 47dfeb7..89d9aa8 100644 --- a/rhino/rhinopanel.py +++ b/rhino/rhinopanel.py @@ -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": "", diff --git a/rhino/startup.py b/rhino/startup.py index d4ab6fc..d83619b 100644 --- a/rhino/startup.py +++ b/rhino/startup.py @@ -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()