# ! python3 # -*- coding: utf-8 -*- """ elemente.py ELEMENTE-Panel: Smart Architektur-Elemente. Phase 1: Waende — Achsen-Linie (editierbar) + Volumen (auto-generiert). Achse ist die Quelle der Wahrheit, Volumen wird bei jeder Achsen-Aenderung oder Geschoss-Aenderung neu gebaut. """ import os import sys import json import uuid import Rhino import Rhino.Geometry as rg import System import scriptcontext as sc _HERE = os.path.dirname(os.path.abspath(__file__)) if _HERE not in sys.path: sys.path.insert(0, _HERE) import panel_base PANEL_GUID_STR = "5a6b7c8d-9e0f-4a1b-c2d3-e4f5061728e0" # UserString-Keys auf Elementen _KEY_ID = "dossier_element_id" # gemeinsame UUID Achse+Volumen _KEY_TYPE = "dossier_element_type" # "wand_axis" | "wand_volume" _KEY_GESCHOSS = "dossier_geschoss" _KEY_DICKE = "dossier_dicke" # in doc-units _KEY_UK_OVER = "dossier_uk_override" # "" = auto, sonst float _KEY_OK_OVER = "dossier_ok_override" _KEY_REFERENZ = "dossier_referenz" # "mid" | "left" | "right" _KEY_DACH_NEIGUNG = "dossier_dach_neigung" # Grad als string ("30") _KEY_DACH_EAVE = "dossier_dach_eave" # Index der Traufkante (string) _KEY_DACH_TYP = "dossier_dach_typ" # "pult"|"sattel"|"walm"|"mansarde" _KEY_DACH_NEIG_UNTEN = "dossier_dach_neigung_unten" # Mansarde: untere Neigung _KEY_DACH_KNICK_H = "dossier_dach_knick_h" # Mansarde: Hoehe des Knicks _KEY_DACH_VARIANTE = "dossier_dach_variante" # Mansarde: "walm" | "giebel" | "walm_giebel" # Oeffnungen (Fenster/Tueren) — Source = Point auf Wand-Achse _KEY_OEFF_TYP = "dossier_oeff_typ" # "fenster" | "tuer" _KEY_OEFF_PARENT = "dossier_oeff_parent" # parent wand element_id _KEY_OEFF_BREITE = "dossier_oeff_breite" _KEY_OEFF_HOEHE = "dossier_oeff_hoehe" _KEY_OEFF_BRUEST = "dossier_oeff_brueest" # Bruestungshoehe (nur Fenster, sonst 0) _KEY_OEFF_RAHMEN_B = "dossier_oeff_rahmen_b" # Rahmen-Riegel-Breite (Profilbreite, in der Wandflaeche) _KEY_OEFF_RAHMEN_TIEFE = "dossier_oeff_rahmen_tiefe" # Rahmen-Tiefe (entlang Wandnormale) _KEY_OEFF_RAHMEN_POS = "dossier_oeff_rahmen_pos" # "aussen" | "mid" | "innen" — Lage im Wandquerschnitt _KEY_OEFF_FLUEGEL = "dossier_oeff_fluegel" # Anzahl Fluegel (1,2,3,4) _KEY_OEFF_SIMS_AUS = "dossier_oeff_sims_aus" # Style: "ohne"|"schmal"|"standard"|"breit" _KEY_OEFF_SIMS_IN = "dossier_oeff_sims_in" # Style: "ohne"|"schmal"|"standard"|"breit" _KEY_OEFF_GLAS = "dossier_oeff_glas" # "1"|"0" — sichtbare Glas-Scheibe _KEY_OEFF_REFERENZ = "dossier_oeff_referenz" # "mid" | "links" | "rechts" — Lage des Klick-Punkts in der Oeffnung _OEFF_REFERENZ_OPTIONS = ("mid", "links", "rechts") # Treppen-spezifische Keys _KEY_GESCHOSS_END = "dossier_geschoss_end" # Zielgeschoss-ID (Treppe) _KEY_TREPPE_BREITE = "dossier_treppe_breite" _KEY_TREPPE_N = "dossier_treppe_n" # Anzahl Stufen (Steigungen) _KEY_TREPPE_REFERENZ = "dossier_treppe_referenz" # "mid"|"links"|"rechts" — Lage der Lauflinie zur Treppe _KEY_TREPPE_MODUS = "dossier_treppe_modus" # "massiv"|"flach"|"plattenrand" _KEY_TREPPE_LAUF_D = "dossier_treppe_lauf_d" # Lauf-Plattendicke (m) _KEY_TREPPE_ART = "dossier_treppe_art" # "gerade"|"l"|"wendel" _KEY_TREPPE_H_OVER = "dossier_treppe_h_over" # eigene Hoehe (m); leer = Geschoss _KEY_TREPPE_SOLL = "dossier_treppe_soll" # JSON {s:[lo,hi,on], a:[lo,hi,on], sa:[lo,hi,on]} _TREPPE_SOLL_DEFAULT = { "s": [0.15, 0.20, True], "a": [0.21, 0.35, True], "sa": [0.60, 0.65, True], } _TREPPE_MODI = ("massiv", "flach", "plattenrand") _TREPPE_ARTEN = ("gerade", "l", "wendel") # Sims-Stile (Aussen/Innen) — Dicke (Z), Auskragung (perp), Ueberhang seitlich _OEFF_SIMS_STYLES = { "ohne": None, "schmal": {"dicke": 0.03, "aus": 0.08, "ueberhang": 0.03}, "standard": {"dicke": 0.04, "aus": 0.14, "ueberhang": 0.05}, "breit": {"dicke": 0.05, "aus": 0.22, "ueberhang": 0.06}, } _OEFF_RAHMEN_POS_OPTIONS = ("aussen", "mid", "innen") # --- Last-Used-Defaults (sticky, session-life) ------------------------------ # Speichert die letzten Werte (Dicke, Referenz, Modus, Neigung), damit der # naechste Create-Befehl mit denselben Defaults startet. Sticky ueberlebt # Doc-Wechsel, aber NICHT Rhino-Restart — was passt: "ich hab gerade 0.30 # fuer eine Wand benutzt, neue Wand soll auch 0.30 sein". def _last(key, default): return sc.sticky.get("elemente_last_" + key, default) def _save_last(**kwargs): for k, v in kwargs.items(): sc.sticky["elemente_last_" + k] = v # --- Geschoss-Lookup -------------------------------------------------------- def _load_geschosse(doc): """Liest die Geschoss/Ebenen-Liste aus doc.Strings (vom Ebenen-Manager).""" raw = doc.Strings.GetValue("dossier_zeichnungsebenen") or \ doc.Strings.GetValue("dossier_ebenen") if not raw: return [] try: data = json.loads(raw) return data if isinstance(data, list) else [] except Exception: return [] def _geschoss_by_id(doc, gid): if not gid: return None for e in _load_geschosse(doc): if isinstance(e, dict) and e.get("id") == gid: return e return None def _active_geschoss_id(doc): """Liefert die ID des aktuell aktiven Geschosses (= im Ebenen-Manager blau hervorgehoben). Falls keins gesetzt oder das aktive keine Geschoss-Ebene ist (z.B. Schnitt/Ansicht), wird das erste echte Geschoss zurueckgegeben.""" try: active = doc.Strings.GetValue("dossier_active_id") or "" except Exception: active = "" geschosse = [g for g in _load_geschosse(doc) if isinstance(g, dict) and g.get("isGeschoss")] if active and any(g.get("id") == active for g in geschosse): return active return geschosse[0].get("id") if geschosse else "" def _active_geschoss_name(doc): """Name des aktiven Geschosses fuer UI-Anzeige.""" gid = _active_geschoss_id(doc) g = _geschoss_by_id(doc, gid) return g.get("name", "") if g else "" def _resolve_uk_ok(doc, gid, uk_over, ok_over): """Wand: UK = OKFF, OK = OKFF + Hoehe (Standard fuer Geschoss-volle Wand).""" g = _geschoss_by_id(doc, gid) if g is None: uk = float(uk_over) if uk_over not in (None, "") else 0.0 ok = float(ok_over) if ok_over not in (None, "") else 3.0 return uk, ok okff = float(g.get("okff", 0.0)) hoehe = float(g.get("hoehe", 3.0)) auto_uk = okff auto_ok = okff + hoehe uk = float(uk_over) if uk_over not in (None, "") else auto_uk ok = float(ok_over) if ok_over not in (None, "") else auto_ok return uk, ok def _resolve_decke_z(doc, gid, dicke, uk_over, ok_over): """Decke: OK = OKFF des verknuepften Geschosses (= Bodenkante = 0.00 relativ zum Geschoss). UK = OK - dicke (Decke geht NACH UNTEN). OK ist der natuerliche Fixpunkt: aendert sich die Dicke, wandert UK mit. Override-Logik: - Nur OK_override gesetzt → OK = override, UK = OK - dicke - Nur UK_override gesetzt → UK = override, OK = UK + dicke - Beide gesetzt → beide literal""" g = _geschoss_by_id(doc, gid) okff = float(g.get("okff", 0.0)) if g else 0.0 auto_ok = okff has_ok = ok_over not in (None, "") has_uk = uk_over not in (None, "") if has_ok and has_uk: return float(uk_over), float(ok_over) if has_ok: ok = float(ok_over) return ok - float(dicke), ok if has_uk: uk = float(uk_over) return uk, uk + float(dicke) # Beide auto return auto_ok - float(dicke), auto_ok # --- Layer-Pfade ------------------------------------------------------------ def _find_ebene_sublayer_name(doc, keywords, default_code, default_name, default_color="#888888", default_lw=0.35): """Findet aus der Ebenen-Liste den ersten Sublayer der einem der Keywords entspricht. Wenn nicht gefunden, wird der Sublayer mit den Default-Werten AUTOMATISCH in die Ebenen-Liste eingetragen (damit er auch im Ebenen- Manager-UI erscheint) und der Rhinopanel-Bridge ein State-Refresh getriggert. Ergebnis: 'CODE_NAME' wie 'WAENDE'.""" raw = doc.Strings.GetValue("dossier_ebenen") ebenen = [] if raw: try: data = json.loads(raw) if isinstance(data, list): ebenen = data except Exception as ex: print("[ELEMENTE] sublayer-lookup:", ex) # 1) Per Keyword in der Liste suchen for e in ebenen: if not isinstance(e, dict): continue name = (e.get("name") or "") low = name.lower() for kw in keywords: if kw in low: return "{}_{}".format(e.get("code", default_code), name or default_name) # 2) Auto-Add: weder Keyword noch Code vorhanden → Eintrag anlegen if ebenen and not any(isinstance(e, dict) and e.get("code") == default_code for e in ebenen): ebenen.append({ "code": default_code, "name": default_name, "color": default_color, "lw": default_lw, "visible": True, "locked": False, }) try: doc.Strings.SetString("dossier_ebenen", json.dumps(ebenen, ensure_ascii=False)) print("[ELEMENTE] Ebene '{}_{}' automatisch hinzugefuegt".format( default_code, default_name)) # Ebenen-Manager UI mit-informieren b = sc.sticky.get("ebenen_bridge_ref") \ or sc.sticky.get("ebenen_bridge") \ or sc.sticky.get("rhinopanel_bridge") if b is not None and hasattr(b, "_send_state"): try: b._send_state() except Exception: pass except Exception as ex: print("[ELEMENTE] Auto-Add fehler:", ex) return "{}_{}".format(default_code, default_name) def _layer_path_axis(doc, geschoss_name): """Wand-Achse + Volumen — Sublayer 'WÄNDE' (Code 20).""" sub = _find_ebene_sublayer_name(doc, ["wand", "wände", "waende"], "20", "WÄNDE", default_color="#0a0a0a", default_lw=0.50) return "{}::{}".format(geschoss_name, sub) def _layer_path_volume(doc, geschoss_name): return _layer_path_axis(doc, geschoss_name) def _layer_path_decke(doc, geschoss_name): """Decken-Outline + Volumen — Sublayer 'DECKEN' (Code 30).""" sub = _find_ebene_sublayer_name(doc, ["decke"], "30", "DECKEN", default_color="#605850", default_lw=0.35) return "{}::{}".format(geschoss_name, sub) def _layer_path_dach(doc, geschoss_name): """Dach-Outline + Volumen — Sublayer 'DÄCHER' (Code 31).""" sub = _find_ebene_sublayer_name(doc, ["dach", "däch", "daech"], "31", "DÄCHER", default_color="#7a4a3a", default_lw=0.35) return "{}::{}".format(geschoss_name, sub) def _layer_path_treppe(doc, geschoss_name): """Treppen-Lauflinie + Volumen — Sublayer 'TREPPEN' (Code 40).""" sub = _find_ebene_sublayer_name(doc, ["trepp"], "40", "TREPPEN", default_color="#a08040", default_lw=0.35) return "{}::{}".format(geschoss_name, sub) def _ensure_layer(doc, path): """Stellt sicher, dass ein Layer-Pfad existiert. Liefert Layer-Index.""" idx = doc.Layers.FindByFullPath(path, -1) if idx >= 0: return idx # Schrittweise anlegen parts = path.split("::") parent_id = System.Guid.Empty cur_path = "" for part in parts: cur_path = part if not cur_path else (cur_path + "::" + part) idx = doc.Layers.FindByFullPath(cur_path, -1) if idx < 0: from Rhino.DocObjects import Layer layer = Layer() layer.Name = part if parent_id != System.Guid.Empty: layer.ParentLayerId = parent_id idx = doc.Layers.Add(layer) parent_id = doc.Layers[idx].Id return idx # --- Wall-Konstruktion ------------------------------------------------------ def _make_rectangle_preview(c1): """Preview: 4 gruene Kanten des Rechtecks waehrend des Ziehens.""" import System.Drawing as SD color = SD.Color.FromArgb(255, 90, 200, 90) def handler(sender, e): try: cx, cy = e.CurrentPoint.X, e.CurrentPoint.Y p1 = rg.Point3d(c1.X, c1.Y, 0) p2 = rg.Point3d(cx, c1.Y, 0) p3 = rg.Point3d(cx, cy, 0) p4 = rg.Point3d(c1.X, cy, 0) for a, b in ((p1, p2), (p2, p3), (p3, p4), (p4, p1)): e.Display.DrawLine(a, b, color, 2) except Exception: pass return handler def _make_rect3pt_preview(c1, c2): """Preview fuer 3-Punkt-Rechteck. c2=None waehrend Sammlung der zweiten Ecke (zeige Linie c1→Maus), sonst zeige rotiertes Rechteck.""" import System.Drawing as SD color = SD.Color.FromArgb(255, 90, 200, 90) p1 = rg.Point3d(c1.X, c1.Y, 0) def handler(sender, e): try: cur = rg.Point3d(e.CurrentPoint.X, e.CurrentPoint.Y, 0) if c2 is None: e.Display.DrawLine(p1, cur, color, 2) return p2 = rg.Point3d(c2.X, c2.Y, 0) ex = p2.X - p1.X; ey = p2.Y - p1.Y edge_len = (ex * ex + ey * ey) ** 0.5 if edge_len < 1e-9: e.Display.DrawLine(p1, p2, color, 2); return px = -ey / edge_len; py = ex / edge_len d = (cur.X - p2.X) * px + (cur.Y - p2.Y) * py p3 = rg.Point3d(p2.X + d * px, p2.Y + d * py, 0) p4 = rg.Point3d(p1.X + d * px, p1.Y + d * py, 0) for a, b in ((p1, p2), (p2, p3), (p3, p4), (p4, p1)): e.Display.DrawLine(a, b, color, 2) except Exception: pass return handler def _make_circle_preview(center): """Preview: Kreis vom Mittelpunkt zum Mauspunkt.""" import System.Drawing as SD color = SD.Color.FromArgb(255, 90, 200, 90) cen = rg.Point3d(center.X, center.Y, 0) def handler(sender, e): try: cur = rg.Point3d(e.CurrentPoint.X, e.CurrentPoint.Y, 0) r = cen.DistanceTo(cur) if r <= 1e-9: return try: e.Display.DrawCircle(rg.Circle(rg.Plane.WorldXY, cen, r), color, 2) except Exception: # Fallback: NurbsCurve zeichnen e.Display.DrawCurve(rg.Circle(rg.Plane.WorldXY, cen, r).ToNurbsCurve(), color, 2) except Exception: pass return handler def _collect_rectangle(doc, c1): """Achsen-aligned Rechteck aus 2 diagonalen Ecken. Liefert geschlossene PolylineCurve in XY-Ebene auf Z=0.""" try: import Rhino.Input.Custom as ric from Rhino.Input import GetResult except Exception: return None gp = ric.GetPoint() gp.SetCommandPrompt("Gegenueberliegende Ecke") try: gp.SetBasePoint(c1, True) except Exception: pass try: gp.DynamicDraw += _make_rectangle_preview(c1) except Exception: pass res = gp.Get() if res != GetResult.Point: return None c2 = gp.Point() pts = [ rg.Point3d(c1.X, c1.Y, 0), rg.Point3d(c2.X, c1.Y, 0), rg.Point3d(c2.X, c2.Y, 0), rg.Point3d(c1.X, c2.Y, 0), rg.Point3d(c1.X, c1.Y, 0), ] return rg.PolylineCurve(rg.Polyline(pts)) def _collect_rectangle_3pt(doc, c1): """3-Punkt-Rechteck: c1 = erste Ecke, c2 = Ende der ersten Kante (definiert Richtung), c3 = Punkt auf der gegenueberliegenden Seite (definiert Hoehe). Erzeugt rotiertes Rechteck.""" try: import Rhino.Input.Custom as ric from Rhino.Input import GetResult except Exception: return None gp = ric.GetPoint() gp.SetCommandPrompt("Ende der ersten Kante") try: gp.SetBasePoint(c1, True) except Exception: pass try: gp.DynamicDraw += _make_rect3pt_preview(c1, None) except Exception: pass res = gp.Get() if res != GetResult.Point: return None c2 = gp.Point() gp = ric.GetPoint() gp.SetCommandPrompt("Hoehe (Punkt auf gegenueberliegender Seite)") try: gp.SetBasePoint(c2, True) except Exception: pass try: gp.DynamicDraw += _make_rect3pt_preview(c1, c2) except Exception: pass res = gp.Get() if res != GetResult.Point: return None c3 = gp.Point() ex = c2.X - c1.X ey = c2.Y - c1.Y edge_len = (ex * ex + ey * ey) ** 0.5 if edge_len < 1e-9: return None # Perpendikular in XY (links der edge-Richtung) px = -ey / edge_len py = ex / edge_len # Signierte Distanz von c3 zur Edge (c1-c2) d = (c3.X - c2.X) * px + (c3.Y - c2.Y) * py p1 = rg.Point3d(c1.X, c1.Y, 0) p2 = rg.Point3d(c2.X, c2.Y, 0) p3 = rg.Point3d(c2.X + d * px, c2.Y + d * py, 0) p4 = rg.Point3d(c1.X + d * px, c1.Y + d * py, 0) return rg.PolylineCurve(rg.Polyline([p1, p2, p3, p4, p1])) def _collect_circle(doc, center): """Kreis aus Mittelpunkt + Radiuspunkt. Liefert NurbsCurve.""" try: import Rhino.Input.Custom as ric from Rhino.Input import GetResult except Exception: return None gp = ric.GetPoint() gp.SetCommandPrompt("Radiuspunkt") try: gp.SetBasePoint(center, True) except Exception: pass try: gp.DynamicDraw += _make_circle_preview(center) except Exception: pass res = gp.Get() if res != GetResult.Point: return None rp = gp.Point() cen = rg.Point3d(center.X, center.Y, 0) rad_pt = rg.Point3d(rp.X, rp.Y, 0) radius = cen.DistanceTo(rad_pt) if radius <= 1e-9: return None return rg.Circle(rg.Plane.WorldXY, cen, radius).ToNurbsCurve() def _make_decke_preview_handler(committed_points): """Live-Preview waehrend Decken-Outline gezeichnet wird: gesetzte Segmente + Rubberband + gestrichelte Schliessungs-Linie zurueck zum Startpunkt.""" import System.Drawing as SD color_line = SD.Color.FromArgb(255, 90, 200, 90) color_close = SD.Color.FromArgb(180, 150, 230, 150) color_node = SD.Color.FromArgb(255, 255, 255, 255) def handler(sender, e): try: cur = e.CurrentPoint cur_xy = rg.Point3d(cur.X, cur.Y, 0) pts = list(committed_points) + [cur_xy] for i in range(len(pts) - 1): e.Display.DrawLine(pts[i], pts[i + 1], color_line, 2) # Schliessungs-Hinweis: gestrichelte Linie zurueck zum Startpunkt if len(committed_points) >= 2: try: e.Display.DrawDottedLine(cur_xy, committed_points[0], color_close) except Exception: e.Display.DrawLine(cur_xy, committed_points[0], color_close, 1) for pp in committed_points: try: e.Display.DrawPoint(pp, color_node) except Exception: pass except Exception: pass return handler def _draw_axis_with_offsets(display, axis_curve, dicke, referenz, color_axis, color_edge): """Zeichnet eine Wand-Achse + ihre Offset-Kanten (Aussenkanten der Wand). Wird von allen Wand-Preview-Handlern wiederverwendet.""" try: display.DrawCurve(axis_curve, color_axis, 2) except Exception: pass plane = rg.Plane.WorldXY tol = 0.001 half = float(dicke) / 2.0 if referenz == "left": offsets = [0.0, -float(dicke)] elif referenz == "right": offsets = [+float(dicke), 0.0] else: offsets = [+half, -half] for d in offsets: try: if abs(d) < 1e-9: display.DrawCurve(axis_curve, color_edge, 1) else: result = axis_curve.Offset(plane, d, tol, rg.CurveOffsetCornerStyle.Sharp) if result: for c in result: display.DrawCurve(c, color_edge, 1) except Exception: pass def _make_treppe_preview_handler(p0, breite, referenz, n_stufen, fixed_length=None, min_length=None, max_length=None): """Live-Preview fuer die gerade Treppe waehrend der Lauflinien-Wahl. Zeichnet: Lauflinie (Mitte), die zwei Aussenkanten (je nach Referenz) sowie kurze Querstriche an jeder Setzstufen-Position. Laengen-Steuerung (von hoechster zu niedrigster Prio): - `fixed_length`: Mausvektor wird genau auf diese Laenge reskaliert - `min_length` / `max_length`: Mausvektor wird in dieser Range geclampt (frei innerhalb, Stop bei den Grenzen) - sonst: Mausvektor wird unveraendert benutzt""" import System.Drawing as SD color_axis = SD.Color.FromArgb(255, 90, 200, 90) color_edge = SD.Color.FromArgb(180, 120, 220, 120) color_step = SD.Color.FromArgb(200, 200, 240, 120) p0_xy = rg.Point3d(p0.X, p0.Y, 0) N = max(2, int(n_stufen)) b = float(breite) if referenz == "links": perp_lo, perp_hi = 0.0, -b elif referenz == "rechts": perp_lo, perp_hi = 0.0, +b else: perp_lo, perp_hi = -b * 0.5, +b * 0.5 def handler(sender, e): try: mouse = rg.Point3d(e.CurrentPoint.X, e.CurrentPoint.Y, 0) tan_vec = rg.Vector3d(mouse.X - p0_xy.X, mouse.Y - p0_xy.Y, 0) mouse_dist = tan_vec.Length if mouse_dist < 1e-4: return tan_vec.Unitize() # Bei Regel-Modus: Endpunkt entweder fix oder in einer Range. if fixed_length is not None and fixed_length > 1e-4: L = float(fixed_length) cur = rg.Point3d(p0_xy.X + tan_vec.X * L, p0_xy.Y + tan_vec.Y * L, 0) elif min_length is not None or max_length is not None: lo = float(min_length) if min_length is not None else 1e-4 hi = float(max_length) if max_length is not None else 1e9 L = mouse_dist if L < lo: L = lo if L > hi: L = hi cur = rg.Point3d(p0_xy.X + tan_vec.X * L, p0_xy.Y + tan_vec.Y * L, 0) else: L = mouse_dist cur = mouse perp = rg.Vector3d(-tan_vec.Y, tan_vec.X, 0) # Lauflinie (Mittel-Achse) gruen try: e.Display.DrawLine(p0_xy, cur, color_axis, 2) except Exception: pass # Aussenkanten der Treppe (zwei Parallelen je nach Referenz) def edge_at(perp_off): ax = rg.Point3d(p0_xy.X + perp.X * perp_off, p0_xy.Y + perp.Y * perp_off, 0) bx = rg.Point3d(cur.X + perp.X * perp_off, cur.Y + perp.Y * perp_off, 0) try: e.Display.DrawLine(ax, bx, color_edge, 1) except Exception: pass edge_at(perp_lo); edge_at(perp_hi) # Querstriche an jeder Setzstufen-Position (N Auftritte) A = L / max(1, N) for k in range(1, N + 1): x = k * A if x > L + 1e-6: break mid = rg.Point3d(p0_xy.X + tan_vec.X * x, p0_xy.Y + tan_vec.Y * x, 0) a = rg.Point3d(mid.X + perp.X * perp_lo, mid.Y + perp.Y * perp_lo, 0) bp = rg.Point3d(mid.X + perp.X * perp_hi, mid.Y + perp.Y * perp_hi, 0) try: e.Display.DrawLine(a, bp, color_step, 1) except Exception: pass except Exception: pass return handler def _make_treppe_wendel_preview(center, start, breite, referenz, n_stufen, total_h=None, soll=None, regel_mode="frei"): """Live-Preview fuer den 3. Klick einer Wendeltreppe. Zeichnet: Mittelpunkt-Lauflinie + alle N Keile fuer die aktuelle End-Position. Bei `regel_mode == "regel"` wird der Sweep auf einen gueltigen Bereich geclampt — die Richtung kommt aus der Maus, die Drehung wird auf den Soll-A-Wert beschraenkt. So bleiben Auftritt + 2S+A im Soll.""" import System.Drawing as SD import math color_axis = SD.Color.FromArgb(255, 90, 200, 90) color_edge = SD.Color.FromArgb(180, 120, 220, 120) color_step = SD.Color.FromArgb(200, 200, 240, 120) cx, cy = center.X, center.Y sx, sy = start.X, start.Y r_click = math.sqrt((sx - cx) ** 2 + (sy - cy) ** 2) if r_click < 0.05: r_click = 0.05 r_inner, r_outer = _wendel_radii(r_click, breite, referenz) a_start_fixed = math.atan2(sy - cy, sx - cx) N = max(2, int(n_stufen)) def handler(sender, e): try: mouse = e.CurrentPoint cross_z = ((sx - cx) * (mouse.Y - cy) - (sy - cy) * (mouse.X - cx)) sweep_sign = 1.0 if cross_z >= 0 else -1.0 a_end_raw = math.atan2(mouse.Y - cy, mouse.X - cx) delta = a_end_raw - a_start_fixed if sweep_sign > 0: while delta < 0: delta += 2.0 * math.pi else: while delta > 0: delta -= 2.0 * math.pi if abs(delta) < 0.02: return # Clamp Sweep im Regel-Modus — Auftritt-Soll wird ueber die # GANZE Trittbreite (innen + aussen) erzwungen. if regel_mode == "regel" and total_h is not None and soll is not None: try: s_lo, s_hi = _wendel_sweep_range( r_click, breite, referenz, N, total_h, soll) raw = abs(delta) if raw < s_lo: clamped = s_lo elif raw > s_hi: clamped = s_hi else: clamped = raw delta = clamped * (1.0 if delta >= 0 else -1.0) except Exception: pass da = delta / N # Lauflinie center→mouse try: e.Display.DrawLine(rg.Point3d(cx, cy, 0), rg.Point3d(mouse.X, mouse.Y, 0), color_axis, 1) except Exception: pass # Alle N Keile als Quad-Linien for k in range(N): a0 = a_start_fixed + k * da a1 = a_start_fixed + (k + 1) * da pi0 = rg.Point3d(cx + r_inner * math.cos(a0), cy + r_inner * math.sin(a0), 0) po0 = rg.Point3d(cx + r_outer * math.cos(a0), cy + r_outer * math.sin(a0), 0) pi1 = rg.Point3d(cx + r_inner * math.cos(a1), cy + r_inner * math.sin(a1), 0) po1 = rg.Point3d(cx + r_outer * math.cos(a1), cy + r_outer * math.sin(a1), 0) # Riser bei a0 (radial-Linie) try: e.Display.DrawLine(pi0, po0, color_step, 1) except Exception: pass # Inner & outer "Bogen" (linear approximiert) try: e.Display.DrawLine(pi0, pi1, color_edge, 1) except Exception: pass try: e.Display.DrawLine(po0, po1, color_edge, 1) except Exception: pass # Letzter Riser bei alpha_final a_f = a_start_fixed + delta pif = rg.Point3d(cx + r_inner * math.cos(a_f), cy + r_inner * math.sin(a_f), 0) pof = rg.Point3d(cx + r_outer * math.cos(a_f), cy + r_outer * math.sin(a_f), 0) try: e.Display.DrawLine(pif, pof, color_step, 1) except Exception: pass # Live-Label: Stufen, Sweep, Auftritt an Innen/Lauf/Aussen try: deg = abs(delta) * 180.0 / math.pi A_in = abs(da) * r_inner A_lauf = abs(da) * r_click A_out = abs(da) * r_outer lbl = "St {} | {:.0f}° | A i/l/a: {:.2f}/{:.2f}/{:.2f}".format( N, deg, A_in, A_lauf, A_out) if regel_mode == "regel": lbl += " (Regel)" e.Display.DrawDot(rg.Point3d(mouse.X, mouse.Y, 0), lbl) except Exception: pass except Exception: pass return handler def _make_treppe_l_corner_preview(p0, breite, referenz, total_n, total_h): """Preview fuer den 2. Klick einer L-Treppe (Podest-Eck). Zeigt: - Lauflinie + Aussenkanten - Step-Lines an A_opt-Abstaenden (zeigt wo jeder Tritt landet) - Live-Label mit N1 (Stufen vor Podest) und N2 (nach Podest) """ import System.Drawing as SD color_axis = SD.Color.FromArgb(255, 90, 200, 90) color_edge = SD.Color.FromArgb(180, 120, 220, 120) color_step = SD.Color.FromArgb(200, 200, 240, 120) p0_xy = rg.Point3d(p0.X, p0.Y, 0) half_b = float(breite) * 0.5 if referenz == "links": perp_lo, perp_hi = 0.0, -float(breite) elif referenz == "rechts": perp_lo, perp_hi = 0.0, +float(breite) else: perp_lo, perp_hi = -half_b, +half_b # A_opt aus Soll-Schrittmass 0.63 - 2*S, geclampt auf erlaubten Bereich S = float(total_h) / max(1, int(total_n)) A_opt = 0.63 - 2.0 * S if A_opt < 0.21: A_opt = 0.21 if A_opt > 0.35: A_opt = 0.35 def handler(sender, e): try: mouse = rg.Point3d(e.CurrentPoint.X, e.CurrentPoint.Y, 0) tan_vec = rg.Vector3d(mouse.X - p0_xy.X, mouse.Y - p0_xy.Y, 0) L = tan_vec.Length if L < 1e-4: return tan_vec.Unitize() perp = rg.Vector3d(-tan_vec.Y, tan_vec.X, 0) try: e.Display.DrawLine(p0_xy, mouse, color_axis, 2) except Exception: pass def edge_at(perp_off): a = rg.Point3d(p0_xy.X + perp.X * perp_off, p0_xy.Y + perp.Y * perp_off, 0) b = rg.Point3d(mouse.X + perp.X * perp_off, mouse.Y + perp.Y * perp_off, 0) try: e.Display.DrawLine(a, b, color_edge, 1) except Exception: pass edge_at(perp_lo); edge_at(perp_hi) # N1 = Stufen die in Run 1 passen (effektive Laenge = L - half_b # weil Podest die Haelfte einnimmt). N2 = restliche Stufen. eff_L1 = max(0.0, L - half_b) N1 = max(0, int(round(eff_L1 / A_opt))) N1 = min(N1, int(total_n) - 1) N2 = max(0, int(total_n) - N1) # Step-Lines an N1 Positionen for k in range(1, N1 + 1): x = k * A_opt if x > L + 1e-6: break mid = rg.Point3d(p0_xy.X + tan_vec.X * x, p0_xy.Y + tan_vec.Y * x, 0) a = rg.Point3d(mid.X + perp.X * perp_lo, mid.Y + perp.Y * perp_lo, 0) bp = rg.Point3d(mid.X + perp.X * perp_hi, mid.Y + perp.Y * perp_hi, 0) try: e.Display.DrawLine(a, bp, color_step, 1) except Exception: pass # Live-Label am Mauspunkt try: e.Display.DrawDot(mouse, "Vor Podest: {} | Nach: {}".format(N1, N2)) except Exception: pass except Exception: pass return handler def _make_spline_preview_handler(committed_points, dicke, referenz): """Preview fuer Spline-Wand: interpolierter NURBS durch committed + Maus.""" import System.Drawing as SD color_axis = SD.Color.FromArgb(255, 90, 200, 90) color_edge = SD.Color.FromArgb(180, 120, 220, 120) color_node = SD.Color.FromArgb(255, 255, 255, 255) def handler(sender, e): try: cur_xy = rg.Point3d(e.CurrentPoint.X, e.CurrentPoint.Y, 0) pts = list(committed_points) + [cur_xy] if len(pts) < 2: return axis = None if len(pts) == 2: axis = rg.LineCurve(pts[0], pts[1]) else: try: axis = rg.Curve.CreateInterpolatedCurve(pts, 3) except Exception: axis = rg.PolylineCurve(rg.Polyline(pts)) if axis is None: return _draw_axis_with_offsets(e.Display, axis, dicke, referenz, color_axis, color_edge) for pp in committed_points: try: e.Display.DrawPoint(pp, color_node) except Exception: pass except Exception: pass return handler def _make_arc_preview_handler(p0, p_mid, dicke, referenz): """Preview fuer Bogen-Wand. p_mid=None waehrend Sammlung des Mittelpunkts (nur Linie zeigen), sonst echter Bogen p0→p_mid→Maus.""" import System.Drawing as SD color_axis = SD.Color.FromArgb(255, 90, 200, 90) color_edge = SD.Color.FromArgb(180, 120, 220, 120) color_node = SD.Color.FromArgb(255, 255, 255, 255) p0_xy = rg.Point3d(p0.X, p0.Y, 0) def handler(sender, e): try: cur_xy = rg.Point3d(e.CurrentPoint.X, e.CurrentPoint.Y, 0) try: e.Display.DrawPoint(p0_xy, color_node) except Exception: pass if p_mid is None: # Phase 1: Mittelpunkt — Rubberband-Linie e.Display.DrawLine(p0_xy, cur_xy, color_axis, 2) return mid_xy = rg.Point3d(p_mid.X, p_mid.Y, 0) try: e.Display.DrawPoint(mid_xy, color_node) except Exception: pass # Phase 2: Endpunkt — Bogen + Offsets try: arc = rg.Arc(p0_xy, mid_xy, cur_xy) except Exception: return if not arc.IsValid: return axis = rg.ArcCurve(arc) _draw_axis_with_offsets(e.Display, axis, dicke, referenz, color_axis, color_edge) except Exception: pass return handler def _make_rectangle_wall_preview_handler(c1, dicke, referenz): """Preview fuer Wand-Rechteck: 4 Linien-Achsen + ihre Offsets.""" import System.Drawing as SD color_axis = SD.Color.FromArgb(255, 90, 200, 90) color_edge = SD.Color.FromArgb(180, 120, 220, 120) c1_xy = rg.Point3d(c1.X, c1.Y, 0) def handler(sender, e): try: cx = e.CurrentPoint.X cy = e.CurrentPoint.Y p1 = c1_xy p2 = rg.Point3d(cx, c1_xy.Y, 0) p3 = rg.Point3d(cx, cy, 0) p4 = rg.Point3d(c1_xy.X, cy, 0) corners = [p1, p2, p3, p4, p1] for i in range(4): axis = rg.LineCurve(corners[i], corners[i + 1]) _draw_axis_with_offsets(e.Display, axis, dicke, referenz, color_axis, color_edge) except Exception: pass return handler def _make_preview_handler(committed_points, dicke, referenz): """Preview fuer Polylinie-Wand: gesetzte Punkte + Rubberband + Wand-Kanten.""" import System.Drawing as SD color_axis = SD.Color.FromArgb(255, 90, 200, 90) color_edge = SD.Color.FromArgb(180, 120, 220, 120) color_node = SD.Color.FromArgb(255, 255, 255, 255) def handler(sender, e): try: cur_xy = rg.Point3d(e.CurrentPoint.X, e.CurrentPoint.Y, 0) pts = list(committed_points) + [cur_xy] if len(pts) < 2: return axis = (rg.LineCurve(pts[0], pts[1]) if len(pts) == 2 else rg.PolylineCurve(rg.Polyline(pts))) _draw_axis_with_offsets(e.Display, axis, dicke, referenz, color_axis, color_edge) for pp in committed_points: try: e.Display.DrawPoint(pp, color_node) except Exception: pass except Exception: pass return handler def _make_axis_geometry(p0, p1): """2D-Wandlinie: Linie auf der UK-Hoehe. Aber damit der User die Achse frei editieren kann, behalten wir die Linie in der XY-Ebene auf Z=0 — UK/OK kommen aus dem Geschoss-Lookup, nicht aus der Linie. """ return rg.LineCurve(rg.Point3d(p0.X, p0.Y, 0), rg.Point3d(p1.X, p1.Y, 0)) def _make_volume_from_line(p0_xy, p1_xy, dicke, uk, ok): """Fallback: gerade Wand aus zwei XY-Punkten.""" p0 = rg.Point3d(p0_xy.X, p0_xy.Y, uk) p1 = rg.Point3d(p1_xy.X, p1_xy.Y, uk) direction = rg.Vector3d(p1 - p0) if direction.Length < 1e-9: return None direction.Unitize() normal = rg.Vector3d(-direction.Y, direction.X, 0) half = dicke / 2.0 a = p0 + normal * half b = p0 - normal * half c = p1 - normal * half d = p1 + normal * half poly = rg.Polyline([a, b, c, d, a]) profile = rg.PolylineCurve(poly) height = ok - uk if height <= 0: return None extrusion = rg.Extrusion.Create(profile, height, True) if extrusion is None: return None return extrusion.ToBrep() def _offset_curve(curve, plane, distance, tol): """Curve.Offset-Wrapper der distance=0 als reine Kopie behandelt.""" if abs(distance) < 1e-9: return [curve.DuplicateCurve()] try: result = curve.Offset(plane, distance, tol, rg.CurveOffsetCornerStyle.Sharp) if result is None or len(result) == 0: return None return list(result) except Exception: return None def _make_volume_geometry(axis_curve, dicke, uk, ok, referenz="mid"): """Baut die Wand-Brep aus einer beliebigen Achsen-Kurve. referenz: 'mid' = Achse mittig, 'left'/'right' = Achse auf einer Aussen- kante (Walking-Richtung der Kurve).""" if not isinstance(axis_curve, rg.Curve): return None dicke = float(dicke) if dicke <= 0: return None height = float(ok) - float(uk) if height <= 0: return None # Offsets bestimmen — Summe muss IMMER dicke sein half = dicke / 2.0 if referenz == "left": d_left, d_right = 0.0, -dicke elif referenz == "right": d_left, d_right = +dicke, 0.0 else: # mid d_left, d_right = +half, -half plane = rg.Plane.WorldXY tol = 0.001 left = _offset_curve(axis_curve, plane, d_left, tol) right = _offset_curve(axis_curve, plane, d_right, tol) if not left or not right: return _make_volume_from_line(axis_curve.PointAtStart, axis_curve.PointAtEnd, dicke, uk, ok) L = left[0] R = right[0] try: R.Reverse() cap_start = rg.LineCurve(L.PointAtEnd, R.PointAtStart) cap_end = rg.LineCurve(R.PointAtEnd, L.PointAtStart) joined = rg.Curve.JoinCurves([L, cap_start, R, cap_end], tol) except Exception: joined = None if not joined or len(joined) == 0 or not joined[0].IsClosed: return _make_volume_from_line(axis_curve.PointAtStart, axis_curve.PointAtEnd, dicke, uk, ok) profile = joined[0].DuplicateCurve() if abs(uk) > 1e-9: profile.Transform(rg.Transform.Translation(0, 0, uk)) extrusion = rg.Extrusion.Create(profile, height, True) if extrusion is None: return None return extrusion.ToBrep() def _attach_meta(obj_attrs, wall_id, type_, geschoss, dicke, uk_over, ok_over, referenz="mid", neigung=None, eave_idx=None, dach_typ=None, neigung_unten=None, knick_h=None, dach_variante=None, oeff_typ=None, oeff_parent=None, oeff_breite=None, oeff_hoehe=None, oeff_brueest=None, oeff_rahmen_b=None, oeff_rahmen_tiefe=None, oeff_rahmen_pos=None, oeff_fluegel=None, oeff_sims_aus=None, oeff_sims_in=None, oeff_glas=None, oeff_referenz=None, geschoss_end=None, treppe_breite=None, treppe_n=None, treppe_referenz=None, treppe_modus=None, treppe_lauf_d=None, treppe_art=None, treppe_h_over=None, treppe_soll=None): """User-Strings auf die Object-Attributes setzen.""" obj_attrs.SetUserString(_KEY_ID, wall_id) obj_attrs.SetUserString(_KEY_TYPE, type_) obj_attrs.SetUserString(_KEY_GESCHOSS, geschoss or "") obj_attrs.SetUserString(_KEY_DICKE, "{:.6f}".format(float(dicke))) obj_attrs.SetUserString(_KEY_UK_OVER, "" if uk_over in (None, "") else "{:.6f}".format(float(uk_over))) obj_attrs.SetUserString(_KEY_OK_OVER, "" if ok_over in (None, "") else "{:.6f}".format(float(ok_over))) obj_attrs.SetUserString(_KEY_REFERENZ, referenz if referenz in ("mid", "left", "right") else "mid") if neigung is not None: obj_attrs.SetUserString(_KEY_DACH_NEIGUNG, "{:.4f}".format(float(neigung))) if eave_idx is not None: obj_attrs.SetUserString(_KEY_DACH_EAVE, "{}".format(int(eave_idx))) if dach_typ is not None and dach_typ in ("pult", "sattel", "walm", "mansarde"): obj_attrs.SetUserString(_KEY_DACH_TYP, dach_typ) if neigung_unten is not None: obj_attrs.SetUserString(_KEY_DACH_NEIG_UNTEN, "{:.4f}".format(float(neigung_unten))) if knick_h is not None: obj_attrs.SetUserString(_KEY_DACH_KNICK_H, "{:.6f}".format(float(knick_h))) if dach_variante is not None and dach_variante in ("walm", "giebel", "walm_giebel"): obj_attrs.SetUserString(_KEY_DACH_VARIANTE, dach_variante) if oeff_typ is not None and oeff_typ in ("fenster", "tuer"): obj_attrs.SetUserString(_KEY_OEFF_TYP, oeff_typ) if oeff_parent is not None: obj_attrs.SetUserString(_KEY_OEFF_PARENT, str(oeff_parent)) if oeff_breite is not None: obj_attrs.SetUserString(_KEY_OEFF_BREITE, "{:.4f}".format(float(oeff_breite))) if oeff_hoehe is not None: obj_attrs.SetUserString(_KEY_OEFF_HOEHE, "{:.4f}".format(float(oeff_hoehe))) if oeff_brueest is not None: obj_attrs.SetUserString(_KEY_OEFF_BRUEST, "{:.4f}".format(float(oeff_brueest))) if oeff_rahmen_b is not None: obj_attrs.SetUserString(_KEY_OEFF_RAHMEN_B, "{:.4f}".format(float(oeff_rahmen_b))) if oeff_rahmen_tiefe is not None: obj_attrs.SetUserString(_KEY_OEFF_RAHMEN_TIEFE, "{:.4f}".format(float(oeff_rahmen_tiefe))) if oeff_rahmen_pos is not None and oeff_rahmen_pos in _OEFF_RAHMEN_POS_OPTIONS: obj_attrs.SetUserString(_KEY_OEFF_RAHMEN_POS, oeff_rahmen_pos) if oeff_fluegel is not None: obj_attrs.SetUserString(_KEY_OEFF_FLUEGEL, "{}".format(int(oeff_fluegel))) if oeff_sims_aus is not None: # akzeptiere Bool (legacy) oder Style-String if isinstance(oeff_sims_aus, bool): v = "standard" if oeff_sims_aus else "ohne" else: v = str(oeff_sims_aus) if v not in _OEFF_SIMS_STYLES: v = "ohne" obj_attrs.SetUserString(_KEY_OEFF_SIMS_AUS, v) if oeff_sims_in is not None: if isinstance(oeff_sims_in, bool): v = "standard" if oeff_sims_in else "ohne" else: v = str(oeff_sims_in) if v not in _OEFF_SIMS_STYLES: v = "ohne" obj_attrs.SetUserString(_KEY_OEFF_SIMS_IN, v) if oeff_glas is not None: obj_attrs.SetUserString(_KEY_OEFF_GLAS, "1" if bool(oeff_glas) else "0") if oeff_referenz is not None and oeff_referenz in _OEFF_REFERENZ_OPTIONS: obj_attrs.SetUserString(_KEY_OEFF_REFERENZ, oeff_referenz) # --- Treppen-Felder --- if geschoss_end is not None: obj_attrs.SetUserString(_KEY_GESCHOSS_END, geschoss_end or "") if treppe_breite is not None: obj_attrs.SetUserString(_KEY_TREPPE_BREITE, "{:.4f}".format(float(treppe_breite))) if treppe_n is not None: obj_attrs.SetUserString(_KEY_TREPPE_N, "{}".format(int(treppe_n))) if treppe_referenz is not None and treppe_referenz in ("mid", "links", "rechts"): obj_attrs.SetUserString(_KEY_TREPPE_REFERENZ, treppe_referenz) if treppe_modus is not None and treppe_modus in _TREPPE_MODI: obj_attrs.SetUserString(_KEY_TREPPE_MODUS, treppe_modus) if treppe_lauf_d is not None: obj_attrs.SetUserString(_KEY_TREPPE_LAUF_D, "{:.4f}".format(float(treppe_lauf_d))) if treppe_art is not None and treppe_art in _TREPPE_ARTEN: obj_attrs.SetUserString(_KEY_TREPPE_ART, treppe_art) if treppe_h_over is not None: if treppe_h_over == "" or treppe_h_over is None: obj_attrs.SetUserString(_KEY_TREPPE_H_OVER, "") else: try: obj_attrs.SetUserString(_KEY_TREPPE_H_OVER, "{:.4f}".format(float(treppe_h_over))) except Exception: pass if treppe_soll is not None: try: import json obj_attrs.SetUserString(_KEY_TREPPE_SOLL, json.dumps(treppe_soll)) except Exception: pass def _read_meta(obj): """Liest Element-Metadaten von einem Rhino-Objekt. Liefert dict oder None.""" try: a = obj.Attributes type_ = a.GetUserString(_KEY_TYPE) if not type_: return None ref = a.GetUserString(_KEY_REFERENZ) or "mid" if ref not in ("mid", "left", "right"): ref = "mid" try: neigung = float(a.GetUserString(_KEY_DACH_NEIGUNG) or "30") except Exception: neigung = 30.0 try: eave = int(a.GetUserString(_KEY_DACH_EAVE) or "0") except Exception: eave = 0 dt = a.GetUserString(_KEY_DACH_TYP) or "pult" if dt not in ("pult", "sattel", "walm", "mansarde"): dt = "pult" try: nu = float(a.GetUserString(_KEY_DACH_NEIG_UNTEN) or "60") except Exception: nu = 60.0 try: kh = float(a.GetUserString(_KEY_DACH_KNICK_H) or "2.0") except Exception: kh = 2.0 dv = a.GetUserString(_KEY_DACH_VARIANTE) or "walm" if dv not in ("walm", "giebel", "walm_giebel"): dv = "walm" ot = a.GetUserString(_KEY_OEFF_TYP) or "" if ot not in ("fenster", "tuer"): ot = "" try: ob = float(a.GetUserString(_KEY_OEFF_BREITE) or "1.0") except Exception: ob = 1.0 try: oh = float(a.GetUserString(_KEY_OEFF_HOEHE) or "1.4") except Exception: oh = 1.4 try: obr = float(a.GetUserString(_KEY_OEFF_BRUEST) or "0.9") except Exception: obr = 0.9 try: or_b = float(a.GetUserString(_KEY_OEFF_RAHMEN_B) or "0.06") except Exception: or_b = 0.06 try: or_t = float(a.GetUserString(_KEY_OEFF_RAHMEN_TIEFE) or "0.08") except Exception: or_t = 0.08 or_p = a.GetUserString(_KEY_OEFF_RAHMEN_POS) or "mid" if or_p not in _OEFF_RAHMEN_POS_OPTIONS: or_p = "mid" try: ofl = int(a.GetUserString(_KEY_OEFF_FLUEGEL) or "1") except Exception: ofl = 1 if ofl < 1: ofl = 1 # Sims-Stile + Glas-Default is_fenster = (ot == "fenster") def _sims_style(raw, default_fenster): if raw in _OEFF_SIMS_STYLES: return raw if raw == "1": return "standard" # Legacy bool true if raw == "0": return "ohne" # Legacy bool false return ("standard" if is_fenster else "ohne") if default_fenster else "ohne" sa_raw = a.GetUserString(_KEY_OEFF_SIMS_AUS) or "" si_raw = a.GetUserString(_KEY_OEFF_SIMS_IN) or "" osa = _sims_style(sa_raw, True) osi = _sims_style(si_raw, True) og_raw = a.GetUserString(_KEY_OEFF_GLAS) ogl = (og_raw == "1") if og_raw in ("0", "1") else is_fenster oref = a.GetUserString(_KEY_OEFF_REFERENZ) or "mid" if oref not in _OEFF_REFERENZ_OPTIONS: oref = "mid" # Treppen-Felder gend = a.GetUserString(_KEY_GESCHOSS_END) or "" try: tb = float(a.GetUserString(_KEY_TREPPE_BREITE) or "1.0") except Exception: tb = 1.0 try: tn = int(a.GetUserString(_KEY_TREPPE_N) or "15") except Exception: tn = 15 if tn < 2: tn = 2 tref = a.GetUserString(_KEY_TREPPE_REFERENZ) or "mid" if tref not in ("mid", "links", "rechts"): tref = "mid" tmod = a.GetUserString(_KEY_TREPPE_MODUS) or "flach" if tmod not in _TREPPE_MODI: tmod = "flach" try: tld = float(a.GetUserString(_KEY_TREPPE_LAUF_D) or "0.18") except Exception: tld = 0.18 tart = a.GetUserString(_KEY_TREPPE_ART) or "gerade" if tart not in _TREPPE_ARTEN: tart = "gerade" thov = a.GetUserString(_KEY_TREPPE_H_OVER) or "" # Soll-Werte JSON, mit Defaults wenn nicht gesetzt import json tsoll = dict(_TREPPE_SOLL_DEFAULT) soll_raw = a.GetUserString(_KEY_TREPPE_SOLL) if soll_raw: try: parsed = json.loads(soll_raw) if isinstance(parsed, dict): for k in ("s", "a", "sa"): if k in parsed and isinstance(parsed[k], list) and len(parsed[k]) >= 3: tsoll[k] = [float(parsed[k][0]), float(parsed[k][1]), bool(parsed[k][2])] except Exception: pass return { "id": a.GetUserString(_KEY_ID) or "", "type": type_, "geschoss": a.GetUserString(_KEY_GESCHOSS) or "", "dicke": float(a.GetUserString(_KEY_DICKE) or "0.25"), "uk_override": a.GetUserString(_KEY_UK_OVER) or "", "ok_override": a.GetUserString(_KEY_OK_OVER) or "", "referenz": ref, "neigung": neigung, "eave_idx": eave, "dach_typ": dt, "neigung_unten": nu, "knick_h": kh, "dach_variante": dv, "oeff_typ": ot, "oeff_parent": a.GetUserString(_KEY_OEFF_PARENT) or "", "oeff_breite": ob, "oeff_hoehe": oh, "oeff_brueest": obr, "oeff_rahmen_b": or_b, "oeff_rahmen_tiefe": or_t, "oeff_rahmen_pos": or_p, "oeff_fluegel": ofl, "oeff_sims_aus": osa, "oeff_sims_in": osi, "oeff_glas": ogl, "oeff_referenz": oref, "geschoss_end": gend, "treppe_breite": tb, "treppe_n": tn, "treppe_referenz": tref, "treppe_modus": tmod, "treppe_lauf_d": tld, "treppe_art": tart, "treppe_h_over": thov, "treppe_soll": tsoll, } except Exception: return None def _find_objects_by_wall_id(doc, wall_id, type_filter=None): """Findet alle Rhino-Objekte mit der gegebenen wall_id.""" out = [] for obj in doc.Objects: meta = _read_meta(obj) if meta and meta["id"] == wall_id: if type_filter is None or meta["type"] == type_filter: out.append((obj, meta)) return out def _find_axis(doc, wall_id): for obj, meta in _find_objects_by_wall_id(doc, wall_id, "wand_axis"): return obj return None def _find_volume(doc, wall_id): for obj, meta in _find_objects_by_wall_id(doc, wall_id, "wand_volume"): return obj return None def _find_openings_for_wall(doc, wall_id): """Alle Oeffnungs-Points (oeffnung_point) deren oeff_parent == wall_id.""" out = [] for obj in doc.Objects: meta = _read_meta(obj) if meta is None: continue if meta["type"] != "oeffnung_point": continue if meta.get("oeff_parent") != wall_id: continue out.append((obj, meta)) return out def _oeff_effective_axis_point(axis_curve, point_on_axis, breite, referenz): """Berechnet den effektiven Zentrums-Punkt der Oeffnung auf der Wand- Achse — abhaengig davon ob der Klick-Punkt am linken/rechten Rand oder in der Mitte der Oeffnung liegen soll. referenz="mid" → Punkt liegt mittig: Zentrum = Klick-Punkt referenz="links" → Klick-Punkt am linken Rand: Zentrum = pt + tan*half referenz="rechts" → Klick-Punkt am rechten Rand: Zentrum = pt - tan*half "links"/"rechts" beziehen sich auf die +tan/-tan Richtung der Wand- Achse am Klick-Punkt. Mathematisch geht der Walk entlang der echten Bogenlaenge auf der Kurve — funktioniert auch fuer gebogene Achsen.""" if referenz not in ("links", "rechts"): return point_on_axis if not isinstance(axis_curve, rg.Curve): return point_on_axis half = float(breite) * 0.5 try: ok, t = axis_curve.ClosestPoint(point_on_axis) if not ok: return point_on_axis # Aktuelle Bogenlaenge vom Kurvenanfang bis t sub = rg.Interval(axis_curve.Domain.Min, t) try: arc_cur = axis_curve.GetLength(sub) except Exception: dom = axis_curve.Domain arc_cur = ((t - dom.Min) / dom.Length) * axis_curve.GetLength() if referenz == "links": arc_new = arc_cur + half # Zentrum +tan/2 vom Klick else: # rechts arc_new = arc_cur - half # Zentrum -tan/2 total = axis_curve.GetLength() if arc_new < 0: arc_new = 0 if arc_new > total: arc_new = total lp = axis_curve.LengthParameter(arc_new) t_new = None if isinstance(lp, tuple) and len(lp) >= 2 and lp[0]: t_new = lp[1] if t_new is None: # Fallback: lineare Parameter-Interpolation dom = axis_curve.Domain t_new = dom.Min + (arc_new / total) * dom.Length return axis_curve.PointAt(t_new) except Exception: return point_on_axis def _make_oeffnung_cutout(axis_curve, point_on_axis, wall_dicke, breite, hoehe, brueest_h, base_z): """Baut eine Cutout-Box die bei einer Wand-Regen via Boolean-Difference abgezogen wird. Box ist zentriert am Point, entlang der Wand-Tangente ausgerichtet, in Z von base_z+brueest bis base_z+brueest+hoehe, und in Wand-Querrichtung leicht ueberdimensioniert (1.5x dicke) damit der Schnitt sauber durch die Wand geht.""" if not isinstance(axis_curve, rg.Curve): return None try: ok, t = axis_curve.ClosestPoint(point_on_axis) if not ok: return None pt = axis_curve.PointAt(t) tan = axis_curve.TangentAt(t) tan = rg.Vector3d(tan.X, tan.Y, 0) if tan.Length < 1e-9: return None tan.Unitize() perp = rg.Vector3d(-tan.Y, tan.X, 0) half_b = float(breite) * 0.5 half_d = float(wall_dicke) * 1.5 # ueberdimensioniert quer zur Wand z_low = float(base_z) + float(brueest_h) z_high = z_low + float(hoehe) if z_high <= z_low + 1e-9: return None c0 = rg.Point3d(pt.X - tan.X * half_b - perp.X * half_d, pt.Y - tan.Y * half_b - perp.Y * half_d, z_low) c1 = rg.Point3d(pt.X + tan.X * half_b - perp.X * half_d, pt.Y + tan.Y * half_b - perp.Y * half_d, z_low) c2 = rg.Point3d(pt.X + tan.X * half_b + perp.X * half_d, pt.Y + tan.Y * half_b + perp.Y * half_d, z_low) c3 = rg.Point3d(pt.X - tan.X * half_b + perp.X * half_d, pt.Y - tan.Y * half_b + perp.Y * half_d, z_low) poly = rg.Polyline([c0, c1, c2, c3, c0]) base_curve = rg.PolylineCurve(poly) extrusion = rg.Extrusion.Create(base_curve, z_high - z_low, True) if extrusion is None: return None return extrusion.ToBrep() except Exception as ex: print("[ELEMENTE] _make_oeffnung_cutout:", ex) return None def _oeff_axis_frame(axis_curve, point_on_axis): """Liefert (pt, tan, perp) — pt projiziert auf Achse, tan = Wandrichtung (XY-Projektion), perp = 90° CCW von tan (in XY). Return None bei Fehler.""" if not isinstance(axis_curve, rg.Curve): return None try: ok, t = axis_curve.ClosestPoint(point_on_axis) if not ok: return None pt = axis_curve.PointAt(t) tan = axis_curve.TangentAt(t) tan = rg.Vector3d(tan.X, tan.Y, 0) if tan.Length < 1e-9: return None tan.Unitize() perp = rg.Vector3d(-tan.Y, tan.X, 0) return pt, tan, perp except Exception: return None def _make_oeff_box(pt, tan, tan_lo, tan_hi, z_lo, z_hi, perp_lo, perp_hi): """Baut eine achsen-orientierte Box-Brep im lokalen tan/Z/perp-System relativ zur Wand-Achse am Punkt `pt`. Liefert Brep oder None.""" try: plane = rg.Plane(rg.Point3d(pt.X, pt.Y, 0), tan, rg.Vector3d(0, 0, 1)) if tan_hi <= tan_lo + 1e-9 or z_hi <= z_lo + 1e-9 or perp_hi <= perp_lo + 1e-9: return None box = rg.Box(plane, rg.Interval(tan_lo, tan_hi), rg.Interval(z_lo, z_hi), rg.Interval(perp_lo, perp_hi)) return box.ToBrep() except Exception as ex: print("[ELEMENTE] _make_oeff_box:", ex) return None def _resolve_rahmen_perp_range(half_d, rahmen_tiefe, rahmen_pos): """Berechnet (perp_lo, perp_hi) entlang Plane.ZAxis fuer den Rahmen je nach Position-Praeset. half_d = halbe Wandtiefe, rahmen_tiefe = Profil-Tiefe entlang Wandnormale. Konvention: Plane.ZAxis fuer unsere Box-Konstruktion ist tan x (0,0,1) — d.h. eine Seite der Wand. "aussen" mappt empirisch auf +Plane.ZAxis-Seite (kann je nach Wand-Achsrichtung andersrum sein — User kann Wand-Achse mit Rhino-Befehl Dir umdrehen).""" rt = max(0.01, float(rahmen_tiefe)) # Wenn Tiefe groesser als Wand → klammern (Inset 1mm damit kein Z-Fight) rt = min(rt, 2.0 * half_d - 0.002) if rt <= 0: rt = max(0.01, 2.0 * half_d - 0.002) inset = 0.001 if rahmen_pos == "aussen": hi = +half_d - inset lo = hi - rt elif rahmen_pos == "innen": lo = -half_d + inset hi = lo + rt else: # mid lo = -rt * 0.5 hi = +rt * 0.5 return lo, hi def _make_oeffnung_pieces(axis_curve, point_on_axis, wall_dicke, oeff_meta, base_z): """Baut die einzelnen Brep-Pieces der Oeffnung — Rahmen (single Brep via Boolean-Differenz), Mittelpfosten (pro Fluegel), Glas, Sims aussen, Sims innen. Liefert eine Liste von Breps. Caller persistiert jedes als eigenes 'oeffnung_volume' Object mit der gleichen Oeffnungs-ID.""" frame = _oeff_axis_frame(axis_curve, point_on_axis) if frame is None: return [] pt, tan, perp = frame breite = float(oeff_meta.get("oeff_breite", 1.0)) hoehe = float(oeff_meta.get("oeff_hoehe", 1.4)) brueest = float(oeff_meta.get("oeff_brueest", 0.9)) rahmen_b = float(oeff_meta.get("oeff_rahmen_b", 0.06)) rahmen_t = float(oeff_meta.get("oeff_rahmen_tiefe", 0.08)) rahmen_pos = oeff_meta.get("oeff_rahmen_pos", "mid") fluegel = max(1, int(oeff_meta.get("oeff_fluegel", 1))) sims_aus_style = oeff_meta.get("oeff_sims_aus", "ohne") sims_in_style = oeff_meta.get("oeff_sims_in", "ohne") has_glas = bool(oeff_meta.get("oeff_glas", False)) is_tuer = (oeff_meta.get("oeff_typ") == "tuer") half_b = breite * 0.5 half_d = float(wall_dicke) * 0.5 z_lo = float(base_z) + brueest z_hi = z_lo + hoehe inner_l = -half_b + rahmen_b inner_r = +half_b - rahmen_b # Bei Tueren: KEIN unterer Riegel (Zarge ist 3-seitig). Das Innen- # Loch geht bis unter den Outer-Box-Boden, sodass der Boolean-Diff # den unteren Riegel wegschneidet. inner_z_lo_frame = (z_lo - 0.01) if is_tuer else (z_lo + rahmen_b) inner_z_hi_frame = z_hi - rahmen_b # Fuer Mittelpfosten/Glas: bei Tueren beginnen die bei z_lo (Boden), # bei Fenstern oberhalb des unteren Rahmens. payload_z_lo = z_lo if is_tuer else (z_lo + rahmen_b) payload_z_hi = z_hi - rahmen_b if inner_l >= inner_r - 1e-6 or payload_z_lo >= payload_z_hi - 1e-6: return [] # Rahmen-Profil zu dick fuer Oeffnung frame_perp_lo, frame_perp_hi = _resolve_rahmen_perp_range( half_d, rahmen_t, rahmen_pos) pieces = [] # --- RAHMEN: outer box - inner box, sauberer single-Brep try: outer_box = _make_oeff_box(pt, tan, -half_b, +half_b, z_lo, z_hi, frame_perp_lo, frame_perp_hi) # Inner box leicht laenger in perp Richtung damit der Diff sauber # durchschneidet (keine Hauchschicht uebrig). inner_box = _make_oeff_box(pt, tan, inner_l, inner_r, inner_z_lo_frame, inner_z_hi_frame, frame_perp_lo - 0.01, frame_perp_hi + 0.01) if outer_box is not None and inner_box is not None: diff = rg.Brep.CreateBooleanDifference( [outer_box], [inner_box], 0.001) if diff and len(diff) > 0: pieces.append(diff[0]) else: pieces.append(outer_box) except Exception as ex: print("[ELEMENTE] Rahmen BoolDiff:", ex) # --- Mittelpfosten (Fluegel > 1): kleine Stege im inneren Bereich if fluegel > 1: span = inner_r - inner_l for i in range(1, fluegel): x_mid = inner_l + span * (float(i) / fluegel) x_lo = x_mid - rahmen_b * 0.5 x_hi = x_mid + rahmen_b * 0.5 mp = _make_oeff_box(pt, tan, x_lo, x_hi, payload_z_lo, payload_z_hi, frame_perp_lo, frame_perp_hi) if mp is not None: pieces.append(mp) # --- INNERE FUELLUNG: Glas (Fenster oder verglaste Tuer) ODER # Tuerblatt (massive Tuere ohne Glas). Beides als Box-Brep pro Fluegel. if is_tuer and not has_glas: # Tuerblatt — 40 mm massive Platte, mittig in Rahmen-Tiefe fill_t = 0.04 elif has_glas: fill_t = 0.012 # 12 mm Glas else: fill_t = 0 # nichts (z.B. Fenster ohne Glas) if fill_t > 0: fill_mid = (frame_perp_lo + frame_perp_hi) * 0.5 fill_lo = fill_mid - fill_t * 0.5 fill_hi = fill_mid + fill_t * 0.5 if fluegel > 1: span = inner_r - inner_l for i in range(fluegel): fx_lo = inner_l + span * (float(i) / fluegel) fx_hi = inner_l + span * (float(i + 1) / fluegel) if i > 0: fx_lo += rahmen_b * 0.5 if i < fluegel - 1: fx_hi -= rahmen_b * 0.5 fp = _make_oeff_box(pt, tan, fx_lo, fx_hi, payload_z_lo, payload_z_hi, fill_lo, fill_hi) if fp is not None: pieces.append(fp) else: fp = _make_oeff_box(pt, tan, inner_l, inner_r, payload_z_lo, payload_z_hi, fill_lo, fill_hi) if fp is not None: pieces.append(fp) # --- SIMS AUSSEN (+Plane.ZAxis-Seite) — Platte unter der Oeffnung sa = _OEFF_SIMS_STYLES.get(sims_aus_style) if sa is not None: s_t = sa["dicke"]; s_pr = sa["aus"]; s_oh = sa["ueberhang"] s_lo = z_lo - s_t sb = _make_oeff_box(pt, tan, -half_b - s_oh, +half_b + s_oh, s_lo, z_lo, +half_d, +half_d + s_pr) if sb is not None: pieces.append(sb) # --- SIMS INNEN (-Plane.ZAxis-Seite) — Platte unter der Oeffnung si = _OEFF_SIMS_STYLES.get(sims_in_style) if si is not None: s_t = si["dicke"]; s_pr = si["aus"]; s_oh = si["ueberhang"] s_lo = z_lo - s_t sb = _make_oeff_box(pt, tan, -half_b - s_oh, +half_b + s_oh, s_lo, z_lo, -half_d - s_pr, -half_d) if sb is not None: pieces.append(sb) return pieces SOURCE_TYPES = ("wand_axis", "decke_outline", "dach_outline", "oeffnung_point", "treppe_axis") VOLUME_TYPES = ("wand_volume", "decke_volume", "dach_volume", "oeffnung_volume", "treppe_volume") # Oeffnungs-Cutout: Boolean-Difference aus Wand. Zusaetzlich kriegt die # Oeffnung ihr eigenes Volumen (Rahmen + Sims + Glas) als Sub-Element. def _find_source(doc, element_id): """Source-Objekt — Achse (Wand) bzw. Outline (Decke/Dach).""" for obj, meta in _find_objects_by_wall_id(doc, element_id): if meta["type"] in SOURCE_TYPES: return obj, meta return None, None def _find_target_volume(doc, element_id): """Volumen-Objekt (Brep).""" for obj, meta in _find_objects_by_wall_id(doc, element_id): if meta["type"] in VOLUME_TYPES: return obj return None # --- Dach-Helpers (Pultdach) ------------------------------------------------- def _resolve_dach_base(doc, gid, uk_over): """Basis-Hoehe des Dachs an der Traufe (= Eave) = OKFF + Hoehe des Geschosses (Oberkante der Waende). uk_override kann das ueberschreiben.""" g = _geschoss_by_id(doc, gid) if g is None: return float(uk_over) if uk_over not in (None, "") else 3.0 okff = float(g.get("okff", 0.0)) hoehe = float(g.get("hoehe", 3.0)) auto = okff + hoehe return float(uk_over) if uk_over not in (None, "") else auto def _outline_points_xy(outline_curve): """Extrahiert XY-Vertices einer geschlossenen Polyline (ohne Schluss- Duplikat). Liefert Liste von Point3d mit Z=0.""" if not isinstance(outline_curve, rg.Curve): return [] ok, poly = outline_curve.TryGetPolyline() if not ok or poly is None: return [] pts = [poly[i] for i in range(poly.Count)] if len(pts) > 1 and pts[0].DistanceTo(pts[-1]) < 1e-6: pts = pts[:-1] return [rg.Point3d(p.X, p.Y, 0) for p in pts] def _thicken_roof_inward(top_brep, dicke, tol=0.001): """Verdickt eine offene Brep-Schale (oberes Dachshell) entlang der Flaechen-Normalen um `dicke` nach innen — d.h. senkrecht zur jeweiligen Dachflaeche, nicht nur vertikal. Liefert geschlossenen Festkoerper-Brep. Die Normalen-Richtung der gejointen Schale haengt von der Zeichen- richtung der Outline ab (CW vs CCW von oben). Statt blind ein Vorzeichen zu raten probiert die Funktion beide Richtungen und waehlt das Resultat das tatsaechlich nach UNTEN extrudiert (d.h. die Unterseite des Daches unter der originalen Aussenflaeche).""" if top_brep is None: return None d = float(dicke) if d <= 1e-9: return top_brep orig_bbox = top_brep.GetBoundingBox(True) orig_min_z = orig_bbox.Min.Z def _try(distance, extend): try: result = rg.Brep.CreateOffsetBrep(top_brep, distance, True, extend, tol) if isinstance(result, tuple): arr = result[0] elif hasattr(result, '__len__'): arr = result else: arr = None if arr and len(arr) > 0: return arr[0] except Exception as ex: print("[ELEMENTE] CreateOffsetBrep ({}, extend={}):".format(distance, extend), ex) return None # Probiere beide Vorzeichen (+/-d), beide extend-Varianten. candidates = [] for distance in (-d, d): r = _try(distance, False) if r is None: r = _try(distance, True) if r is not None: candidates.append(r) if not candidates: return None # Bevorzuge das Resultat das nach UNTEN extrudiert — die Unterseite des # Daches muss unter dem original Top-Brep-Min-Z liegen. Sonst geht die # Verdickung nach aussen (nach oben), was wir nicht wollen. threshold = max(1e-4, d * 0.05) for c in candidates: bb = c.GetBoundingBox(True) if bb.Min.Z < orig_min_z - threshold: return c return candidates[0] def _join_open_shell(faces, tol=0.001): """Joined eine Liste planarer Brep-Faces zu einer offenen Brep-Schale.""" valid = [f for f in faces if f is not None] if not valid: return None try: joined = rg.Brep.JoinBreps(valid, tol) if joined and len(joined) > 0: return joined[0] except Exception as ex: print("[ELEMENTE] _join_open_shell:", ex) return None def _make_pultdach_volume(outline_curve, dicke, base_height, slope_deg, eave_idx): """Pultdach: obere Dachflaeche liegt geneigt auf der Eckpunkt-Hoehe je Punkt (Eave bei base_height, opposite Seite ansteigend). Die Dicke wird senkrecht zur Dachflaeche nach innen extrudiert (via CreateOffsetBrep). Liefert geschlossenen Festkoerper-Brep oder None.""" import math pts_xy = _outline_points_xy(outline_curve) n = len(pts_xy) if n < 3: return None if eave_idx < 0 or eave_idx >= n: eave_idx = 0 a = pts_xy[eave_idx] b = pts_xy[(eave_idx + 1) % n] eave_vec = rg.Vector3d(b.X - a.X, b.Y - a.Y, 0) if eave_vec.Length < 1e-9: return None eave_vec.Unitize() perp = rg.Vector3d(-eave_vec.Y, eave_vec.X, 0) sample_idx = (eave_idx + 2) % n sv = rg.Vector3d(pts_xy[sample_idx].X - a.X, pts_xy[sample_idx].Y - a.Y, 0) d_sample = sv.X * perp.X + sv.Y * perp.Y if d_sample < 0: perp = -perp tan_s = math.tan(math.radians(float(slope_deg))) top_pts = [] for p in pts_xy: dv = rg.Vector3d(p.X - a.X, p.Y - a.Y, 0) d = dv.X * perp.X + dv.Y * perp.Y if d < 0: d = 0.0 z_top = base_height + d * tan_s top_pts.append(rg.Point3d(p.X, p.Y, z_top)) top_pts.append(top_pts[0]) tol = 0.001 try: top_curve = rg.PolylineCurve(rg.Polyline(top_pts)) top_faces = rg.Brep.CreatePlanarBreps([top_curve], tol) if not top_faces or len(top_faces) == 0: return None top_brep = top_faces[0] return _thicken_roof_inward(top_brep, dicke, tol) except Exception as ex: print("[ELEMENTE] Pultdach Brep:", ex) return None def _make_satteldach_brep(outline_curve, dicke, base_height, slope_deg, ridge_along='long'): """Satteldach: zwei geneigte Trapeze treffen sich am First, mit senkrechter Dicke nach innen extrudiert via CreateOffsetBrep. Erfordert eine 4-Punkt-Outline (Rechteck). ridge_along='long' → First entlang der laengeren Achse, 'short' → entlang der kuerzeren.""" import math pts = _outline_points_xy(outline_curve) if len(pts) != 4: return None e01 = pts[0].DistanceTo(pts[1]) e12 = pts[1].DistanceTo(pts[2]) long_axis_is_01 = e01 >= e12 use_01_as_long = long_axis_is_01 if ridge_along == 'long' else (not long_axis_is_01) short_len = min(e01, e12) if use_01_as_long else max(e01, e12) # short_len = die Spannweite quer zur First-Achse if use_01_as_long: span = e12 # quer zur First-Achse (= zu Edges 0-1, 2-3) else: span = e01 half_span = span * 0.5 ridge_z = base_height + half_span * math.tan(math.radians(float(slope_deg))) c0 = rg.Point3d(pts[0].X, pts[0].Y, base_height) c1 = rg.Point3d(pts[1].X, pts[1].Y, base_height) c2 = rg.Point3d(pts[2].X, pts[2].Y, base_height) c3 = rg.Point3d(pts[3].X, pts[3].Y, base_height) if use_01_as_long: # First parallel zu 0-1 und 2-3 → Endpunkte mid(1-2) und mid(3-0) ridge_a = rg.Point3d((pts[3].X + pts[0].X) * 0.5, (pts[3].Y + pts[0].Y) * 0.5, ridge_z) ridge_b = rg.Point3d((pts[1].X + pts[2].X) * 0.5, (pts[1].Y + pts[2].Y) * 0.5, ridge_z) # Trapeze (outer-facing, CCW von aussen) face_a_pts = [c0, c1, ridge_b, ridge_a, c0] # Seite 0-1 face_b_pts = [c2, c3, ridge_a, ridge_b, c2] # Seite 2-3 else: ridge_a = rg.Point3d((pts[0].X + pts[1].X) * 0.5, (pts[0].Y + pts[1].Y) * 0.5, ridge_z) ridge_b = rg.Point3d((pts[2].X + pts[3].X) * 0.5, (pts[2].Y + pts[3].Y) * 0.5, ridge_z) face_a_pts = [c1, c2, ridge_b, ridge_a, c1] # Seite 1-2 face_b_pts = [c3, c0, ridge_a, ridge_b, c3] # Seite 3-0 tol = 0.001 faces = [_planar_face_from_pts(face_a_pts, tol), _planar_face_from_pts(face_b_pts, tol)] top_brep = _join_open_shell(faces, tol) return _thicken_roof_inward(top_brep, dicke, tol) def _make_walmdach_brep(outline_curve, dicke, base_height, slope_deg): """Walmdach fuer Rechteck-Outline: 4 geneigte Flaechen, die am First zusammenlaufen. Bei einem Quadrat → Zeltdach (= Pyramidendach).""" import math pts = _outline_points_xy(outline_curve) if len(pts) != 4: return None e01 = pts[0].DistanceTo(pts[1]) e12 = pts[1].DistanceTo(pts[2]) long_axis_is_01 = e01 >= e12 # First entlang langer Achse; Laenge des Firsts = laengere Seite minus # kuerzere Seite (= 2x hip-inset) long_len = max(e01, e12) short_len = min(e01, e12) half_short = short_len * 0.5 tan_s = math.tan(math.radians(float(slope_deg))) ridge_height = half_short * tan_s # Firstpunkte = mittlere Punkte der kurzen Kanten, jeweils nach innen # verschoben um half_short (= hip-inset, damit alle 4 Walmflaechen # denselben Neigungswinkel haben) if long_axis_is_01: # Lange Kanten: 0-1 und 2-3. Kurze Kanten: 1-2 und 3-0. mid_12 = rg.Point3d((pts[1].X + pts[2].X) * 0.5, (pts[1].Y + pts[2].Y) * 0.5, 0) mid_30 = rg.Point3d((pts[3].X + pts[0].X) * 0.5, (pts[3].Y + pts[0].Y) * 0.5, 0) long_dir = pts[1] - pts[0] long_dir.Z = 0 long_unit = rg.Vector3d(long_dir); long_unit.Unitize() # ridge points sind die mids nach innen entlang -long_unit / +long_unit ridge_a = rg.Point3d(mid_30.X + long_unit.X * half_short, mid_30.Y + long_unit.Y * half_short, base_height + ridge_height) ridge_b = rg.Point3d(mid_12.X - long_unit.X * half_short, mid_12.Y - long_unit.Y * half_short, base_height + ridge_height) else: mid_01 = rg.Point3d((pts[0].X + pts[1].X) * 0.5, (pts[0].Y + pts[1].Y) * 0.5, 0) mid_23 = rg.Point3d((pts[2].X + pts[3].X) * 0.5, (pts[2].Y + pts[3].Y) * 0.5, 0) long_dir = pts[2] - pts[1] long_dir.Z = 0 long_unit = rg.Vector3d(long_dir); long_unit.Unitize() ridge_a = rg.Point3d(mid_01.X + long_unit.X * half_short, mid_01.Y + long_unit.Y * half_short, base_height + ridge_height) ridge_b = rg.Point3d(mid_23.X - long_unit.X * half_short, mid_23.Y - long_unit.Y * half_short, base_height + ridge_height) # Outer (oben sichtbar) Eckpunkte auf base_height c0 = rg.Point3d(pts[0].X, pts[0].Y, base_height) c1 = rg.Point3d(pts[1].X, pts[1].Y, base_height) c2 = rg.Point3d(pts[2].X, pts[2].Y, base_height) c3 = rg.Point3d(pts[3].X, pts[3].Y, base_height) is_square = abs(long_len - short_len) < 1e-6 tol = 0.001 faces = [] def add_face(pts): f = _planar_face_from_pts(pts, tol) if f: faces.append(f) # NUR die Outer-Schraegflaechen — die Dicke wird per CreateOffsetBrep # senkrecht zur jeweiligen Flaeche nach innen extrudiert. if long_axis_is_01: if is_square: apex_o = ridge_a add_face([c0, c1, apex_o, c0]) add_face([c1, c2, apex_o, c1]) add_face([c2, c3, apex_o, c2]) add_face([c3, c0, apex_o, c3]) else: add_face([c0, c1, ridge_b, ridge_a, c0]) add_face([c2, c3, ridge_a, ridge_b, c2]) add_face([c1, c2, ridge_b, c1]) add_face([c3, c0, ridge_a, c3]) else: if is_square: apex_o = ridge_a add_face([c0, c1, apex_o, c0]) add_face([c1, c2, apex_o, c1]) add_face([c2, c3, apex_o, c2]) add_face([c3, c0, apex_o, c3]) else: add_face([c1, c2, ridge_b, ridge_a, c1]) add_face([c3, c0, ridge_a, ridge_b, c3]) add_face([c0, c1, ridge_a, c0]) add_face([c2, c3, ridge_b, c2]) top_brep = _join_open_shell(faces, tol) return _thicken_roof_inward(top_brep, dicke, tol) def _make_mansardendach_brep(outline_curve, dicke, base_height, slope_upper_deg, slope_lower_deg, knick_h, variante="walm"): """Dispatcher: 'walm' = 4-seitig mit Knick rundum (Mansarden-Walm). 'giebel' = klassisches Mansardendach mit vertikalen Giebel-Pentagonen an den Schmalseiten (DACH-Region-Standard). 'walm_giebel' = unten Walm (Knick rundum), oben Giebel (First ueber voller Laenge, vertikale Dreieck-Giebel an den Schmalseiten).""" if variante == "giebel": return _make_mansardendach_giebel(outline_curve, dicke, base_height, slope_upper_deg, slope_lower_deg, knick_h) if variante == "walm_giebel": return _make_mansardendach_walm_giebel(outline_curve, dicke, base_height, slope_upper_deg, slope_lower_deg, knick_h) return _make_mansardendach_walm(outline_curve, dicke, base_height, slope_upper_deg, slope_lower_deg, knick_h) def _make_mansardendach_giebel(outline_curve, dicke, base_height, slope_upper_deg, slope_lower_deg, knick_h): """Klassisches Mansardendach: Knick nur auf den 2 langen Seiten, an den Schmalseiten vertikale Giebelwand-Pentagone bis hoch zum First.""" import math pts = _outline_points_xy(outline_curve) if len(pts) != 4: return None e01 = pts[0].DistanceTo(pts[1]) e12 = pts[1].DistanceTo(pts[2]) # Corners so umsortieren dass corners[0]→corners[1] die erste lange Kante ist if e01 >= e12: corners = [pts[0], pts[1], pts[2], pts[3]] short_len = e12 else: corners = [pts[1], pts[2], pts[3], pts[0]] short_len = e01 half_short = short_len * 0.5 # Long-Edge-Richtung + Inward-Perpendicular (zeigt vom 1. langen Edge weg) lv = rg.Vector3d(corners[1].X - corners[0].X, corners[1].Y - corners[0].Y, 0) if lv.Length < 1e-9: return None lv.Unitize() perp = rg.Vector3d(-lv.Y, lv.X, 0) mid_first = rg.Point3d((corners[0].X + corners[1].X) * 0.5, (corners[0].Y + corners[1].Y) * 0.5, 0) mid_opp = rg.Point3d((corners[2].X + corners[3].X) * 0.5, (corners[2].Y + corners[3].Y) * 0.5, 0) if (mid_opp.X - mid_first.X) * perp.X + (mid_opp.Y - mid_first.Y) * perp.Y < 0: perp = -perp tan_lower = math.tan(math.radians(float(slope_lower_deg))) tan_upper = math.tan(math.radians(float(slope_upper_deg))) if tan_lower <= 1e-9: return _make_walmdach_brep(outline_curve, dicke, base_height, slope_upper_deg) knick_inset = float(knick_h) / tan_lower if knick_inset >= half_short - 1e-6: return _make_walmdach_brep(outline_curve, dicke, base_height, slope_lower_deg) remaining = half_short - knick_inset ridge_above_knick = remaining * tan_upper total_h = float(knick_h) + ridge_above_knick # Eave-Ecken c = [rg.Point3d(p.X, p.Y, base_height) for p in corners] # Knick-Ecken: corners[0,1] auf 1. langer Kante → Knick in +perp; # corners[2,3] auf gegenueberliegender Kante → Knick in -perp. # WICHTIG: in der GIEBEL-Variante bleiben die Knick-Ecken auf demselben # X (entlang langer Achse) wie die zugehoerige Eave-Ecke — KEIN diagonaler # Inset wie bei Walm. So liegen sie in der Vertikal-Ebene der Gable. k = [ rg.Point3d(c[0].X + perp.X * knick_inset, c[0].Y + perp.Y * knick_inset, base_height + knick_h), rg.Point3d(c[1].X + perp.X * knick_inset, c[1].Y + perp.Y * knick_inset, base_height + knick_h), rg.Point3d(c[2].X - perp.X * knick_inset, c[2].Y - perp.Y * knick_inset, base_height + knick_h), rg.Point3d(c[3].X - perp.X * knick_inset, c[3].Y - perp.Y * knick_inset, base_height + knick_h), ] # First-Endpunkte: Mittelpunkte der Gable-Kanten — diese liegen # geometrisch BEREITS auf der Centerline der Langachse. Kein zusaetzlicher # Inset noetig (der war im alten Code falsch — verschob den First auf # einen Eckpunkt und stauchte die Gable-Pentagone). ridge_w = rg.Point3d((c[3].X + c[0].X) * 0.5, (c[3].Y + c[0].Y) * 0.5, base_height + total_h) ridge_e = rg.Point3d((c[1].X + c[2].X) * 0.5, (c[1].Y + c[2].Y) * 0.5, base_height + total_h) tol = 0.001 faces = [] def add(pl): f = _planar_face_from_pts(pl, tol) if f: faces.append(f) # NUR Outer-Schale (4 Dachflaechen + 2 vertikale Giebel-Pentagone). # Dicke wird per CreateOffsetBrep senkrecht zur jeweiligen Flaeche # nach innen extrudiert. add([c[0], c[1], k[1], k[0], c[0]]) add([c[2], c[3], k[3], k[2], c[2]]) add([k[0], k[1], ridge_e, ridge_w, k[0]]) add([k[2], k[3], ridge_w, ridge_e, k[2]]) add([c[0], c[3], k[3], ridge_w, k[0], c[0]]) # West-Giebel add([c[1], c[2], k[2], ridge_e, k[1], c[1]]) # Ost-Giebel top_brep = _join_open_shell(faces, tol) return _thicken_roof_inward(top_brep, dicke, tol) def _make_mansardendach_walm(outline_curve, dicke, base_height, slope_upper_deg, slope_lower_deg, knick_h): """Mansardendach (4-seitig, Walm-Mansarde): 4 Eaves mit Knick rundum, unten steile Flaeche, oben flachere Walm-Kappe. Erfordert 4-Punkt-Outline.""" import math pts = _outline_points_xy(outline_curve) if len(pts) != 3 + 1 and len(pts) != 4: return None if len(pts) != 4: return None e01 = pts[0].DistanceTo(pts[1]) e12 = pts[1].DistanceTo(pts[2]) long_axis_is_01 = e01 >= e12 short_len = min(e01, e12) half_short = short_len * 0.5 tan_lower = math.tan(math.radians(float(slope_lower_deg))) tan_upper = math.tan(math.radians(float(slope_upper_deg))) knick_h = float(knick_h) if tan_lower <= 1e-9: return _make_walmdach_brep(outline_curve, dicke, base_height, slope_upper_deg) knick_inset = knick_h / tan_lower if knick_inset >= half_short - 1e-6: # Knick zu hoch → degeneriert zu reinem Walm return _make_walmdach_brep(outline_curve, dicke, base_height, slope_lower_deg) remaining = half_short - knick_inset ridge_above_knick = remaining * tan_upper total_height = knick_h + ridge_above_knick # Eave-Ecken auf base_height c0 = rg.Point3d(pts[0].X, pts[0].Y, base_height) c1 = rg.Point3d(pts[1].X, pts[1].Y, base_height) c2 = rg.Point3d(pts[2].X, pts[2].Y, base_height) c3 = rg.Point3d(pts[3].X, pts[3].Y, base_height) # Knick-Ecken: nach innen verschoben (Diagonal-Approximation fuer Rechteck) cx = (pts[0].X + pts[1].X + pts[2].X + pts[3].X) * 0.25 cy = (pts[0].Y + pts[1].Y + pts[2].Y + pts[3].Y) * 0.25 def inset_corner(corner): dx = cx - corner.X; dy = cy - corner.Y L = (dx * dx + dy * dy) ** 0.5 if L < 1e-9: return rg.Point3d(corner.X, corner.Y, base_height + knick_h) # Diagonale Verschiebung = knick_inset / cos(45°) ≈ knick_inset * sqrt(2) # fuer ein achsenaligned Rechteck; sonst Approximation. f = (knick_inset * 1.41421356) / L return rg.Point3d(corner.X + dx * f, corner.Y + dy * f, base_height + knick_h) k0 = inset_corner(pts[0]) k1 = inset_corner(pts[1]) k2 = inset_corner(pts[2]) k3 = inset_corner(pts[3]) # Ridge entlang langer Achse — MIT Hip-Inset, damit oben drauf ein # echtes Walmdach steht (nicht ein Sattel mit degenerierten # Walm-Dreiecken). Inset-Distanz = `remaining` = halbe kurze Seite # des Knick-Polygons. So treffen sich die Walm-Hipflaechen unter dem # gleichen Neigungswinkel wie die Trapezflaechen. if long_axis_is_01: long_dir = rg.Vector3d(pts[1].X - pts[0].X, pts[1].Y - pts[0].Y, 0) else: long_dir = rg.Vector3d(pts[2].X - pts[1].X, pts[2].Y - pts[1].Y, 0) long_dir.Z = 0 long_dir.Unitize() if long_axis_is_01: mid_west = rg.Point3d((k3.X + k0.X) * 0.5, (k3.Y + k0.Y) * 0.5, 0) mid_east = rg.Point3d((k1.X + k2.X) * 0.5, (k1.Y + k2.Y) * 0.5, 0) ra = rg.Point3d(mid_west.X + long_dir.X * remaining, mid_west.Y + long_dir.Y * remaining, base_height + total_height) rb = rg.Point3d(mid_east.X - long_dir.X * remaining, mid_east.Y - long_dir.Y * remaining, base_height + total_height) else: mid_south = rg.Point3d((k0.X + k1.X) * 0.5, (k0.Y + k1.Y) * 0.5, 0) mid_north = rg.Point3d((k2.X + k3.X) * 0.5, (k2.Y + k3.Y) * 0.5, 0) ra = rg.Point3d(mid_south.X + long_dir.X * remaining, mid_south.Y + long_dir.Y * remaining, base_height + total_height) rb = rg.Point3d(mid_north.X - long_dir.X * remaining, mid_north.Y - long_dir.Y * remaining, base_height + total_height) is_square = abs(ra.DistanceTo(rb)) < 1e-6 # → Zelt-Mansarde tol = 0.001 faces = [] def add(pts_list): f = _planar_face_from_pts(pts_list, tol) if f: faces.append(f) # NUR Outer-Schale — Dicke wird via CreateOffsetBrep senkrecht zur # jeweiligen Flaeche nach innen extrudiert. # 1) Untere steile Mansarde-Flaechen (4) add([c0, c1, k1, k0, c0]) add([c1, c2, k2, k1, c1]) add([c2, c3, k3, k2, c2]) add([c3, c0, k0, k3, c3]) # 2) Obere flachere Walm-Kappe if long_axis_is_01: if is_square: apex_o = ra for tri in ((k0, k1, apex_o), (k1, k2, apex_o), (k2, k3, apex_o), (k3, k0, apex_o)): add([tri[0], tri[1], tri[2], tri[0]]) else: add([k0, k1, rb, ra, k0]) add([k2, k3, ra, rb, k2]) add([k1, k2, rb, k1]) add([k3, k0, ra, k3]) else: if is_square: apex_o = ra for tri in ((k0, k1, apex_o), (k1, k2, apex_o), (k2, k3, apex_o), (k3, k0, apex_o)): add([tri[0], tri[1], tri[2], tri[0]]) else: add([k1, k2, rb, ra, k1]) add([k3, k0, ra, rb, k3]) add([k0, k1, ra, k0]) add([k2, k3, rb, k2]) top_brep = _join_open_shell(faces, tol) return _thicken_roof_inward(top_brep, dicke, tol) def _make_mansardendach_walm_giebel(outline_curve, dicke, base_height, slope_upper_deg, slope_lower_deg, knick_h): """Mansardendach Walm-Giebel: unten Walm (Knick rundum, 4 steile Mansarde-Flaechen), oben Giebel (First ueber voller Laenge, 2 obere Dachflaechen entlang der langen Seiten, vertikale Dreieck-Giebel an den Schmalseiten). Erfordert Rechteck-Outline.""" import math pts = _outline_points_xy(outline_curve) if len(pts) != 4: return None e01 = pts[0].DistanceTo(pts[1]) e12 = pts[1].DistanceTo(pts[2]) long_axis_is_01 = e01 >= e12 short_len = min(e01, e12) half_short = short_len * 0.5 tan_lower = math.tan(math.radians(float(slope_lower_deg))) tan_upper = math.tan(math.radians(float(slope_upper_deg))) knick_h = float(knick_h) if tan_lower <= 1e-9: # Untere Mansarde degeneriert → reines Satteldach mit slope_upper return _make_satteldach_brep(outline_curve, dicke, base_height, slope_upper_deg) knick_inset = knick_h / tan_lower if knick_inset >= half_short - 1e-6: # Knick zu hoch → unten ginge bis zur Spitze, oben kein Platz mehr return _make_walmdach_brep(outline_curve, dicke, base_height, slope_lower_deg) remaining = half_short - knick_inset ridge_above_knick = remaining * tan_upper total_height = knick_h + ridge_above_knick # Eave-Ecken c0 = rg.Point3d(pts[0].X, pts[0].Y, base_height) c1 = rg.Point3d(pts[1].X, pts[1].Y, base_height) c2 = rg.Point3d(pts[2].X, pts[2].Y, base_height) c3 = rg.Point3d(pts[3].X, pts[3].Y, base_height) cx = (pts[0].X + pts[1].X + pts[2].X + pts[3].X) * 0.25 cy = (pts[0].Y + pts[1].Y + pts[2].Y + pts[3].Y) * 0.25 def inset_corner(corner): dx = cx - corner.X; dy = cy - corner.Y L = (dx * dx + dy * dy) ** 0.5 if L < 1e-9: return rg.Point3d(corner.X, corner.Y, base_height + knick_h) f = (knick_inset * 1.41421356) / L return rg.Point3d(corner.X + dx * f, corner.Y + dy * f, base_height + knick_h) k0 = inset_corner(pts[0]) k1 = inset_corner(pts[1]) k2 = inset_corner(pts[2]) k3 = inset_corner(pts[3]) # First: ueber voller Laenge des Knick-Polygons (KEIN Hip-Inset → Giebel oben) if long_axis_is_01: # Lange Seiten: k0-k1 und k2-k3 ; Schmalseiten: k1-k2 (Ost), k3-k0 (West) ra = rg.Point3d((k3.X + k0.X) * 0.5, (k3.Y + k0.Y) * 0.5, base_height + total_height) # West rb = rg.Point3d((k1.X + k2.X) * 0.5, (k1.Y + k2.Y) * 0.5, base_height + total_height) # Ost else: # Lange Seiten: k1-k2 und k3-k0 ; Schmalseiten: k0-k1 (Sued), k2-k3 (Nord) ra = rg.Point3d((k0.X + k1.X) * 0.5, (k0.Y + k1.Y) * 0.5, base_height + total_height) # Sued rb = rg.Point3d((k2.X + k3.X) * 0.5, (k2.Y + k3.Y) * 0.5, base_height + total_height) # Nord tol = 0.001 faces = [] def add(pts_list): f = _planar_face_from_pts(pts_list, tol) if f: faces.append(f) # NUR Outer-Schale — Dicke wird via CreateOffsetBrep senkrecht zur # jeweiligen Flaeche nach innen extrudiert. # 1) Untere steile Mansarde-Flaechen (4) add([c0, c1, k1, k0, c0]) add([c1, c2, k2, k1, c1]) add([c2, c3, k3, k2, c2]) add([c3, c0, k0, k3, c3]) # 2) Obere Giebel-Section: 2 Sattel-Flaechen + 2 vertikale Dreieck-Giebel if long_axis_is_01: add([k0, k1, rb, ra, k0]) add([k2, k3, ra, rb, k2]) add([k3, k0, ra, k3]) add([k1, k2, rb, k1]) else: add([k1, k2, rb, ra, k1]) add([k3, k0, ra, rb, k3]) add([k0, k1, ra, k0]) add([k2, k3, rb, k2]) top_brep = _join_open_shell(faces, tol) return _thicken_roof_inward(top_brep, dicke, tol) def _planar_face_from_pts(pts, tol): """Erzeugt eine planare Brep-Flaeche aus einer Liste von Eckpunkten.""" try: curve = rg.PolylineCurve(rg.Polyline(pts)) result = rg.Brep.CreatePlanarBreps([curve], tol) if result and len(result) > 0: return result[0] except Exception: pass return None # --- Decken-Volumen --------------------------------------------------------- def _make_decke_volume(outline_curve, dicke, uk, ok): """Decke = Extrusion einer geschlossenen planaren Curve von UK bis OK.""" if not isinstance(outline_curve, rg.Curve): return None if not outline_curve.IsClosed: return None height = float(ok) - float(uk) if height <= 0: return None profile = outline_curve.DuplicateCurve() if abs(uk) > 1e-9: profile.Transform(rg.Transform.Translation(0, 0, uk)) extrusion = rg.Extrusion.Create(profile, height, True) if extrusion is None: return None return extrusion.ToBrep() # --- Treppen-Volumen -------------------------------------------------------- def _treppe_profile_2d(N, S, A, uk, modus, lauf_d): """Liefert das 2D-Seitenprofil der Treppe als Liste von (x, z)-Tupeln. Konvention: N Steigungen + N Auftritte (oberster Tritt gehoert zur Treppe — fuegt sich sauber in die obere Decke ein, ohne freistehende Setzstufe oben). Lauflaenge = N*A, Hoehe = N*S. Modi: 'massiv' — Block bis zum Boden (uk) 'flach' — schraege Plattenunterseite parallel zur Steigungslinie, Plattendicke lauf_d vertikal gemessen 'plattenrand' — Faltwerk-Treppe mit echten Vertikalen unter den Setzstufen (Konvex-Ecken truncated, Konkav-Ecken haben zusaetzliche L-Verlaengerung — Slab folgt dem oberen Profil parallel) """ Z0 = float(uk) d = float(lauf_d) # Oberes Profil: N Risers + N Treads (der letzte Tritt ist enthalten) top = [(0.0, Z0)] for k in range(N): top.append((k * A, Z0 + (k + 1) * S)) # top of riser k top.append(((k + 1) * A, Z0 + (k + 1) * S)) # end of tread k # top endet bei (N*A, Z0 + N*S) x_end = N * A z_top = Z0 + N * S if modus == "massiv": return top + [(x_end, Z0), (0.0, Z0)] if modus == "flach": # Soffit parallel zur Steigungslinie (von (0,Z0) bis (NA, NS+Z0)), # vertikal um lauf_d nach unten versetzt. return top + [(x_end, z_top - d), (0.0, Z0 - d), (0.0, Z0)] # plattenrand: Faltwerk-Treppe. Innere (Soffit-) Punkte mit Konvex/ # Konkav-Handling, sodass die Vertikalen unter den Setzstufen ihre # eigene D-Tiefe haben (kein Klotz-Look mit floatenden Tritten). inner = [] inner.append((d, Z0)) # Start: rechts vom Riser-Anfang (Konvex-Ecke (0,Z0)) for k in range(N): # Konvex-Ecke (k*A, Z0+(k+1)*S): Top-Eck rechts oben des Risers # Innere Eck-Punkt bei (k*A + D, (k+1)S - D) inner.append((k * A + d, Z0 + (k + 1) * S - d)) # Konkav-Ecke ((k+1)*A, Z0+(k+1)*S): Tread-Ende, naechster Riser # geht hoch. Extra L-Verlaengerung — ausser beim allerletzten Schritt, # wo der Tritt zur Decke laeuft. if k < N - 1: inner.append(((k + 1) * A, Z0 + (k + 1) * S - d)) inner.append(((k + 1) * A + d, Z0 + (k + 1) * S - d)) else: inner.append((x_end, Z0 + N * S - d)) # Schliessen: Top-right Drop + Inner reversed + zurueck zum Start # inner letzter Punkt ist (x_end, z_top - d). Top-right ist (x_end, z_top). inner_rev = list(reversed(inner)) # endet bei (d, Z0) return top + inner_rev + [(0.0, Z0)] def _wendel_radii(r_click, breite, referenz): """Berechnet (r_inner, r_outer) der Wendeltreppe basierend auf dem Klick-Radius (= Lauflinien-Position) und der Referenz. Konvention: 'links' → Lauflinie auf AUSSEN-Kante (body extends inward) 'mid' → Lauflinie mittig 'rechts' → Lauflinie auf INNEN-Kante (body extends outward) r_inner wird absolut auf >= 0.01m geclampt. Bei r_inner < 0.05m schaltet die Geometrie auf Cone-Wedge um (Innenkante kollabiert zur Mittelachse — Spindeltreppe-Style).""" half_b = float(breite) * 0.5 MIN_R = 0.01 if referenz == "links": return (max(MIN_R, r_click - float(breite)), r_click) if referenz == "rechts": return (max(MIN_R, r_click), r_click + float(breite)) return (max(MIN_R, r_click - half_b), r_click + half_b) def _wendel_sweep(center, p_start, p_end): """Liefert (alpha_start, delta) — Startwinkel und signed Sweep-Winkel in Rad. Sweep-Richtung kommt aus dem Cross-Product start vs. end.""" import math sx, sy = p_start.X - center.X, p_start.Y - center.Y ex, ey = p_end.X - center.X, p_end.Y - center.Y a_start = math.atan2(sy, sx) a_end_raw = math.atan2(ey, ex) cross_z = sx * ey - sy * ex sweep_sign = 1.0 if cross_z >= 0 else -1.0 delta = a_end_raw - a_start if sweep_sign > 0: while delta < 0: delta += 2.0 * math.pi else: while delta > 0: delta -= 2.0 * math.pi return a_start, delta def _wendel_wedge_cone_brep(center, r_out, a0, a1, top_z, bot_z_a0, bot_z_a1, tol=0.001): """Cone-Wedge fuer Spindeltreppen: innere Kante kollabiert zur Mittelachse. 5 Vertices (t_c, t_o0, t_o1, b_c, b_o0, b_o1) und 5-6 Faces (Top-Dreieck, Bottom-Dreieck, Aussen-Quad, 2 Radial- Quads). Vermeidet degenerierte Innen-Faces bei r_inner ≈ 0.""" import math cx, cy = center.X, center.Y t_c = rg.Point3d(cx, cy, top_z) t_o0 = rg.Point3d(cx + r_out * math.cos(a0), cy + r_out * math.sin(a0), top_z) t_o1 = rg.Point3d(cx + r_out * math.cos(a1), cy + r_out * math.sin(a1), top_z) b_o0 = rg.Point3d(cx + r_out * math.cos(a0), cy + r_out * math.sin(a0), bot_z_a0) b_o1 = rg.Point3d(cx + r_out * math.cos(a1), cy + r_out * math.sin(a1), bot_z_a1) # Bottom-Center: bei flach-Modus = Mittel der beiden bot_z, sonst gleich. b_c_z = (bot_z_a0 + bot_z_a1) * 0.5 b_c = rg.Point3d(cx, cy, b_c_z) da = a1 - a0 flat_bot = abs(bot_z_a0 - bot_z_a1) < 1e-6 faces = [] # Top Dreieck — CCW von oben if da > 0: faces.append([t_c, t_o0, t_o1, t_c]) else: faces.append([t_c, t_o1, t_o0, t_c]) # Bottom Dreieck — reversed if da > 0: faces.append([b_c, b_o1, b_o0, b_c]) else: faces.append([b_c, b_o0, b_o1, b_c]) # Aussen-Seite (planar bei flat_bot, sonst trianguliert) if flat_bot: faces.append([t_o0, b_o0, b_o1, t_o1, t_o0]) else: faces.append([t_o0, b_o0, b_o1, t_o0]) faces.append([t_o0, b_o1, t_o1, t_o0]) # Radiale Seiten (immer planar — alle in radial Ebene) faces.append([t_c, t_o0, b_o0, b_c, t_c]) faces.append([t_c, b_c, b_o1, t_o1, t_c]) breps = [] for face_pts in faces: try: f = _planar_face_from_pts(face_pts, tol) if f: breps.append(f) except Exception: pass if not breps: return None try: joined = rg.Brep.JoinBreps(breps, tol) if joined and len(joined) > 0: return joined[0] except Exception: pass return breps[0] if breps else None def _wendel_wedge_brep(center, r_in, r_out, a0, a1, top_z, bot_z_a0, bot_z_a1, tol=0.001): """Bauet einen Wendel-Tritt als 8-Vertex-Polyeder: - Flat top bei top_z - Bottom Z-Werte koennen pro Winkel-Seite differieren (flach-Modus: schraeg parallel zur Steigungslinie; plattenrand-Modus: flat). - 4 Seitenflaechen (innen, aussen, radial a0, radial a1). Bei r_in < 0.05m → Cone-Wedge (Spindeltreppe-Style) — verhindert degenerierte Geometrie an der Mittelachse.""" import math if r_in < 0.05: return _wendel_wedge_cone_brep(center, r_out, a0, a1, top_z, bot_z_a0, bot_z_a1, tol) cx, cy = center.X, center.Y t_i0 = rg.Point3d(cx + r_in * math.cos(a0), cy + r_in * math.sin(a0), top_z) t_o0 = rg.Point3d(cx + r_out * math.cos(a0), cy + r_out * math.sin(a0), top_z) t_i1 = rg.Point3d(cx + r_in * math.cos(a1), cy + r_in * math.sin(a1), top_z) t_o1 = rg.Point3d(cx + r_out * math.cos(a1), cy + r_out * math.sin(a1), top_z) b_i0 = rg.Point3d(cx + r_in * math.cos(a0), cy + r_in * math.sin(a0), bot_z_a0) b_o0 = rg.Point3d(cx + r_out * math.cos(a0), cy + r_out * math.sin(a0), bot_z_a0) b_i1 = rg.Point3d(cx + r_in * math.cos(a1), cy + r_in * math.sin(a1), bot_z_a1) b_o1 = rg.Point3d(cx + r_out * math.cos(a1), cy + r_out * math.sin(a1), bot_z_a1) da = a1 - a0 flat_bot = abs(bot_z_a0 - bot_z_a1) < 1e-6 faces = [] # Top — planar if da > 0: faces.append([t_i0, t_o0, t_o1, t_i1, t_i0]) else: faces.append([t_i0, t_i1, t_o1, t_o0, t_i0]) # Bottom — planar wenn flat, sonst in 2 Dreiecke teilen if da > 0: bot_order = [b_i0, b_i1, b_o1, b_o0] else: bot_order = [b_i0, b_o0, b_o1, b_i1] if flat_bot: faces.append(bot_order + [bot_order[0]]) else: faces.append([bot_order[0], bot_order[1], bot_order[2], bot_order[0]]) faces.append([bot_order[0], bot_order[2], bot_order[3], bot_order[0]]) # Inner side: planar wenn flat, sonst Dreiecke if flat_bot: faces.append([t_i0, t_i1, b_i1, b_i0, t_i0]) else: faces.append([t_i0, t_i1, b_i1, t_i0]) faces.append([t_i0, b_i1, b_i0, t_i0]) # Outer side: planar wenn flat, sonst Dreiecke if flat_bot: faces.append([t_o0, b_o0, b_o1, t_o1, t_o0]) else: faces.append([t_o0, b_o0, b_o1, t_o0]) faces.append([t_o0, b_o1, t_o1, t_o0]) # Radiale Seiten — immer planar (alle Punkte auf derselben Radial-Ebene) faces.append([t_i0, t_o0, b_o0, b_i0, t_i0]) faces.append([t_i1, b_i1, b_o1, t_o1, t_i1]) breps = [] for face_pts in faces: try: f = _planar_face_from_pts(face_pts, tol) if f: breps.append(f) except Exception: pass if not breps: return None try: joined = rg.Brep.JoinBreps(breps, tol) if joined and len(joined) > 0: return joined[0] except Exception as ex: print("[ELEMENTE] wendel wedge join:", ex) return breps[0] if breps else None def _wendel_sweep_range(r_lauf, breite, referenz, n_stufen, H, soll): """Liefert (sweep_min, sweep_max) in Radian — gueltiger Drehwinkel. Erzwingt Soll-Auftritt-Range UEBER DIE GANZE TRITTBREITE (von r_inner bis r_outer), nicht nur an der Lauflinie: A = r * (sweep/N) A_inner = r_inner * (sweep/N) >= a_lo → sweep >= N*a_lo/r_inner A_outer = r_outer * (sweep/N) <= a_hi → sweep <= N*a_hi/r_outer So liegen beide Enden eines Tritts im Soll. Wenn der Bereich widerspruechlich ist (zu breite Treppe fuer kleinen Radius), fallback auf Lauflinie-basiertes Clamping.""" r_inner, r_outer = _wendel_radii(r_lauf, breite, referenz) n = max(1, int(n_stufen)) s = float(H) / n a_lo, a_hi = 0.05, 2.0 if soll.get("sa", [0, 0, False])[2]: a_lo = max(a_lo, float(soll["sa"][0]) - 2.0 * s) a_hi = min(a_hi, float(soll["sa"][1]) - 2.0 * s) if soll.get("a", [0, 0, False])[2]: a_lo = max(a_lo, float(soll["a"][0])) a_hi = min(a_hi, float(soll["a"][1])) if a_lo > a_hi: mid = (a_lo + a_hi) * 0.5 a_lo = a_hi = mid ri = max(0.01, float(r_inner)) ro = max(0.01, float(r_outer)) sweep_lo = n * a_lo / ri # tightest lower (kleinster Radius) sweep_hi = n * a_hi / ro # tightest upper (groesster Radius) if sweep_lo > sweep_hi: # Range nicht erfuellbar (zu breite Treppe oder zu enger Radius) # → Fallback: nur an der Lauflinie clampen, der User sieht im Label # dass A_inner/A_outer ausserhalb Soll sind. rl = max(0.01, float(r_lauf)) sweep_lo = n * a_lo / rl sweep_hi = n * a_hi / rl return (sweep_lo, sweep_hi) def _make_treppe_wendel_volume(axis_polyline, breite, referenz, n_stufen, uk, ok, modus="flach", lauf_d=0.18): """Wendeltreppe aus 3-Punkt-Polylinie [center, start, end]. Bauet N Stufen als gestapelte trapezfoermige Keile um die Mittelachse. Lauflinien-Radius = Distanz center→start. Sweep-Winkel und -Richtung werden aus der End-Position abgeleitet (Cross-Product gibt Drehsinn, Winkel ist die natuerliche Strecke in dieser Richtung). Modus: 'massiv' — jeder Keil reicht von UK bis Step-Top (Wedding-Cake) 'flach' / 'plattenrand' — jeder Keil ist nur lauf_d dick (floating steps); bei Wendel keine echte Helix-Soffit fuer Entwurfs-Niveau.""" import math if not isinstance(axis_polyline, rg.Curve): return None try: ok_pl, poly = axis_polyline.TryGetPolyline() except Exception: return None if not ok_pl or poly is None or poly.Count != 3: return None center = rg.Point3d(poly[0].X, poly[0].Y, 0) p_start = rg.Point3d(poly[1].X, poly[1].Y, 0) p_end = rg.Point3d(poly[2].X, poly[2].Y, 0) r_click = math.sqrt((p_start.X - center.X) ** 2 + (p_start.Y - center.Y) ** 2) if r_click < 0.2: return None r_inner, r_outer = _wendel_radii(r_click, breite, referenz) if r_outer - r_inner < 0.05: return None a_start, delta = _wendel_sweep(center, p_start, p_end) if abs(delta) < 0.05: return None H = float(ok) - float(uk) if H <= 1e-6: return None N = max(2, int(n_stufen)) S = H / N da = delta / N parts = [] for k in range(N): a0 = a_start + k * da a1 = a_start + (k + 1) * da z_top = float(uk) + (k + 1) * S if modus == "massiv": # Wedding-Cake: Block bis zum Boden, einfache Extrusion z_bot = float(uk) c0i = (center.X + r_inner * math.cos(a0), center.Y + r_inner * math.sin(a0)) c0o = (center.X + r_outer * math.cos(a0), center.Y + r_outer * math.sin(a0)) c1i = (center.X + r_inner * math.cos(a1), center.Y + r_inner * math.sin(a1)) c1o = (center.X + r_outer * math.cos(a1), center.Y + r_outer * math.sin(a1)) if da > 0: corners = [c0i, c0o, c1o, c1i] else: corners = [c0i, c1i, c1o, c0o] pts = [rg.Point3d(x, y, z_bot) for (x, y) in corners] pts.append(pts[0]) try: crv = rg.PolylineCurve(rg.Polyline(pts)) ext = rg.Extrusion.Create(crv, z_top - z_bot, True) if ext is not None: parts.append(ext.ToBrep()) except Exception as ex: print("[ELEMENTE] Wendel massiv step:", ex) elif modus == "plattenrand": # Gestuft: flat bottom bei z_top - D unter jedem Tritt. # Adjacent Wedges haben unterschiedliche Bottom-Z → visible # "Schritt" auf der Unterseite (= Faltwerk-Look). bot = z_top - float(lauf_d) wedge = _wendel_wedge_brep(center, r_inner, r_outer, a0, a1, z_top, bot, bot) if wedge is not None: parts.append(wedge) else: # flach # Helikoide Unterseite: Bottom verlaeuft schraeg parallel zur # Steigungslinie. Bei a0 bei (uk + k*S - D), bei a1 bei # (uk + (k+1)*S - D). Adjacent Wedges meet seamlessly an der # gemeinsamen Kante → kontinuierlicher Spiral-Slab. bot_a0 = float(uk) + k * S - float(lauf_d) bot_a1 = float(uk) + (k + 1) * S - float(lauf_d) wedge = _wendel_wedge_brep(center, r_inner, r_outer, a0, a1, z_top, bot_a0, bot_a1) if wedge is not None: parts.append(wedge) if not parts: return None if len(parts) == 1: return parts[0] try: merged = rg.Brep.MergeBreps(parts, 0.001) if merged is not None: return merged except Exception: pass try: joined = rg.Brep.JoinBreps(parts, 0.001) if joined and len(joined) > 0: return joined[0] except Exception: pass return parts[0] def _line_intersect_xy(p1, dir1, p2, dir2): """2D-Linien-Schnittpunkt in der XY-Ebene. dir1, dir2 = Richtungs- vektoren (muessen nicht normalisiert sein). Liefert Point3d (Z=0) oder None bei Parallelitaet.""" det = dir1.X * (-dir2.Y) - dir1.Y * (-dir2.X) if abs(det) < 1e-9: return None dx = p2.X - p1.X dy = p2.Y - p1.Y t1 = (dx * (-dir2.Y) - dy * (-dir2.X)) / det return rg.Point3d(p1.X + dir1.X * t1, p1.Y + dir1.Y * t1, 0) def _make_treppe_l_volume(axis_polyline, breite, referenz, n_stufen, uk, ok, modus="flach", lauf_d=0.18): """L-Treppe aus 3-Punkt-Polylinie (Start, Eck-Punkt, End). Bauet 2 gerade Laufe + 1 Podest am Eck zusammen. Hoehe wird proportional zu den Lauflinien-Laengen auf die beiden Laufe verteilt.""" if not isinstance(axis_polyline, rg.Curve): return None try: ok_pl, poly = axis_polyline.TryGetPolyline() except Exception: return None if not ok_pl or poly is None or poly.Count != 3: return None p0 = rg.Point3d(poly[0].X, poly[0].Y, 0) p1 = rg.Point3d(poly[1].X, poly[1].Y, 0) p2 = rg.Point3d(poly[2].X, poly[2].Y, 0) v1 = rg.Vector3d(p1.X - p0.X, p1.Y - p0.Y, 0) v2 = rg.Vector3d(p2.X - p1.X, p2.Y - p1.Y, 0) L1 = v1.Length L2 = v2.Length half_b = float(breite) * 0.5 if L1 < half_b + 0.05 or L2 < half_b + 0.05: print("[ELEMENTE] L-Treppe: Lauflinien zu kurz fuer Podest") return None H = float(ok) - float(uk) if H <= 1e-6: return None N = max(2, int(n_stufen)) S = H / N # Stufen-Verteilung: N1 wird aus L1 mit dem optimalen A bestimmt, # damit die Klick-Position des Users direkt N1 (Stufen vor Podest) # entspricht — genauso wie's der Live-Preview anzeigt. eff_L1 = L1 - half_b eff_L2 = L2 - half_b if eff_L1 + eff_L2 <= 0: return None A_opt = 0.63 - 2.0 * S if A_opt < 0.21: A_opt = 0.21 if A_opt > 0.35: A_opt = 0.35 N1 = int(round(eff_L1 / A_opt)) if N1 < 1: N1 = 1 if N1 > N - 1: N1 = N - 1 N2 = N - N1 v1u = rg.Vector3d(v1); v1u.Unitize() v2u = rg.Vector3d(v2); v2u.Unitize() # Run 1: von p0 bis p1 - v1u*half_b run1_end = rg.Point3d(p1.X - v1u.X * half_b, p1.Y - v1u.Y * half_b, 0) line1 = rg.LineCurve(p0, run1_end) z_podest = float(uk) + N1 * S brep1 = _make_treppe_volume(line1, breite, referenz, N1, float(uk), z_podest, modus, lauf_d) # Run 2: von p1 + v2u*half_b bis p2 run2_start = rg.Point3d(p1.X + v2u.X * half_b, p1.Y + v2u.Y * half_b, 0) line2 = rg.LineCurve(run2_start, p2) brep2 = _make_treppe_volume(line2, breite, referenz, N2, z_podest, float(ok), modus, lauf_d) # Podest am Eck p1 — adaptives Hexagon das die zwei Lauf-Querschnitte # an ihren tatsaechlichen Richtungen verbindet. Bei 90° L kollabiert # zu Quadrat, bei flacheren/spitzeren Winkeln wird's ein 5/6-Eck mit # den Schnittpunkten der ausserhalbliegenden Seitenwaende. perp1 = rg.Vector3d(-v1u.Y, v1u.X, 0) perp2 = rg.Vector3d(-v2u.Y, v2u.X, 0) b = float(breite) if referenz == "links": perp_lo, perp_hi = 0.0, +b elif referenz == "rechts": perp_lo, perp_hi = -b, 0.0 else: # mid perp_lo, perp_hi = -half_b, +half_b # End-Querschnitt von Run 1 + Start-Querschnitt von Run 2 end_lo = rg.Point3d(run1_end.X + perp1.X * perp_lo, run1_end.Y + perp1.Y * perp_lo, 0) end_hi = rg.Point3d(run1_end.X + perp1.X * perp_hi, run1_end.Y + perp1.Y * perp_hi, 0) start_lo = rg.Point3d(run2_start.X + perp2.X * perp_lo, run2_start.Y + perp2.Y * perp_lo, 0) start_hi = rg.Point3d(run2_start.X + perp2.X * perp_hi, run2_start.Y + perp2.Y * perp_hi, 0) # Eckpunkte = Schnitt der Seitenwand-Linien (Run 1 weiter entlang v1, # Run 2 zurueck entlang -v2) minus_v2 = rg.Vector3d(-v2u.X, -v2u.Y, 0) corner_lo = _line_intersect_xy(end_lo, v1u, start_lo, minus_v2) corner_hi = _line_intersect_xy(end_hi, v1u, start_hi, minus_v2) if modus == "massiv": z_lo = float(uk) else: z_lo = z_podest - float(lauf_d) z_hi = z_podest podest_brep = None try: # Hexagon-Vertices in CCW-Order: # end_lo → corner_lo → start_lo → start_hi → corner_hi → end_hi def _add_unique(arr, p, tol=1e-5): if p is None: return if not arr: arr.append(p); return last = arr[-1] if (last.X - p.X) ** 2 + (last.Y - p.Y) ** 2 < tol * tol: return arr.append(p) verts = [] _add_unique(verts, end_lo) _add_unique(verts, corner_lo) _add_unique(verts, start_lo) _add_unique(verts, start_hi) _add_unique(verts, corner_hi) _add_unique(verts, end_hi) if len(verts) >= 3: # CCW-Check via Shoelace — sonst umdrehen damit Extrusion in +Z geht area2 = 0.0 n_v = len(verts) for i in range(n_v): pa = verts[i] pb = verts[(i + 1) % n_v] area2 += pa.X * pb.Y - pa.Y * pb.X if area2 < 0: verts = list(reversed(verts)) pts_bot = [rg.Point3d(p.X, p.Y, z_lo) for p in verts] pts_bot.append(pts_bot[0]) bot_curve = rg.PolylineCurve(rg.Polyline(pts_bot)) ext = rg.Extrusion.Create(bot_curve, z_hi - z_lo, True) if ext is not None: podest_brep = ext.ToBrep() except Exception as ex: print("[ELEMENTE] Podest hexagon:", ex) parts = [b for b in (brep1, podest_brep, brep2) if b is not None] if not parts: return None if len(parts) == 1: return parts[0] # MergeBreps versucht alle Brep-Teile zu einem einzelnen Brep # (mit ggf. mehreren disjunkten Shells) zu kombinieren. try: merged = rg.Brep.MergeBreps(parts, 0.001) if merged is not None: return merged except Exception: pass try: joined = rg.Brep.JoinBreps(parts, 0.001) if joined and len(joined) > 0: return joined[0] except Exception: pass return parts[0] def _make_treppe_volume(axis_curve, breite, referenz, n_stufen, uk, ok, modus="flach", lauf_d=0.18): """Gerade Treppe: bauet ein Seitenprofil (Step-Polygon) entlang der Lauflinie und extrudiert es senkrecht um `breite`. Einzelnes sauberes Brep-Volumen. `modus` bestimmt die Form der Unterseite.""" if not isinstance(axis_curve, rg.Curve): return None try: P0 = axis_curve.PointAtStart P1 = axis_curve.PointAtEnd tan_vec = rg.Vector3d(P1.X - P0.X, P1.Y - P0.Y, 0) L = tan_vec.Length if L < 1e-6: return None tan_vec.Unitize() perp = rg.Vector3d(-tan_vec.Y, tan_vec.X, 0) H = float(ok) - float(uk) if H <= 1e-6: return None N = max(2, int(n_stufen)) S = H / N A = L / N # N Auftritte (oberster Tritt inkl.) if modus not in _TREPPE_MODI: modus = "flach" profile_2d = _treppe_profile_2d(N, S, A, uk, modus, lauf_d) b = float(breite) if referenz == "links": perp_start, perp_end = 0.0, -b elif referenz == "rechts": perp_start, perp_end = 0.0, +b else: perp_start, perp_end = -b * 0.5, +b * 0.5 shift0 = rg.Vector3d(perp.X * perp_start, perp.Y * perp_start, 0) world_pts = [] for (px, pz) in profile_2d: wp = rg.Point3d(P0.X + tan_vec.X * px + shift0.X, P0.Y + tan_vec.Y * px + shift0.Y, pz) world_pts.append(wp) poly = rg.Polyline(world_pts) profile_curve = rg.PolylineCurve(poly) if not profile_curve.IsClosed: return None extrude_len = perp_end - perp_start try: ext = rg.Extrusion.Create(profile_curve, extrude_len, True) if ext is not None: return ext.ToBrep() except Exception as ex: print("[ELEMENTE] Treppe Extrusion:", ex) return None except Exception as ex: print("[ELEMENTE] _make_treppe_volume:", ex) return None def _regenerate_element(doc, element_id): """Regeneriert das Volumen eines Elements (Wand oder Decke) anhand seines Source-Objekts (Achse bzw. Outline).""" src_obj, meta = _find_source(doc, element_id) if src_obj is None or meta is None: return False # Oeffnung selbst hat kein Volumen — stattdessen die Elternwand regen if meta["type"] == "oeffnung_point": parent_id = meta.get("oeff_parent") or "" if parent_id: return _regenerate_element(doc, parent_id) return False geom = src_obj.Geometry if not isinstance(geom, rg.Curve): return False g = _geschoss_by_id(doc, meta["geschoss"]) geschoss_name = g.get("name", "EG") if g else "EG" # _REGEN_BUSY waehrend Regen setzen — verhindert dass die Listener # (Add/Replace/Delete) waehrend dem Erstellen/Loeschen von Volume- # Objekten Dedup oder Cascade-Logik ausfuehren. Wichtig fuer # Oeffnungen, wo mehrere Brep-Pieces mit gleicher ID hinzugefuegt # werden. _was_busy = sc.sticky.get(_REGEN_BUSY, False) sc.sticky[_REGEN_BUSY] = True try: return _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name) finally: sc.sticky[_REGEN_BUSY] = _was_busy def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name): """Eigentliche Implementierung des Regen — der aeussere Wrapper `_regenerate_element` setzt _REGEN_BUSY und dispatcht oeffnung_point.""" if meta["type"] == "wand_axis": uk, ok = _resolve_uk_ok(doc, meta["geschoss"], meta["uk_override"], meta["ok_override"]) brep = _make_volume_geometry(geom, meta["dicke"], uk, ok, meta.get("referenz", "mid")) # Oeffnungen (Fenster/Tueren) abziehen + jeweils das Oeffnungs- # Volumen (Rahmen+Sims+Glas) erstellen oder aktualisieren. opening_jobs = [] if brep is not None: for op_obj, op_meta in _find_openings_for_wall(doc, element_id): pt_geom = op_obj.Geometry pt_loc = None if hasattr(pt_geom, 'Location'): pt_loc = pt_geom.Location elif isinstance(pt_geom, rg.Point3d): pt_loc = pt_geom if pt_loc is None: continue # Effektives Oeffnungs-Zentrum auf der Achse je nach # Referenz-Lage (mid/links/rechts) berechnen. eff_pt = _oeff_effective_axis_point( geom, pt_loc, op_meta["oeff_breite"], op_meta.get("oeff_referenz", "mid")) cutout = _make_oeffnung_cutout( geom, eff_pt, meta["dicke"], op_meta["oeff_breite"], op_meta["oeff_hoehe"], op_meta["oeff_brueest"], uk) if cutout is None: continue try: diff = rg.Brep.CreateBooleanDifference( [brep], [cutout], 0.001) if diff and len(diff) > 0: brep = diff[0] except Exception as ex: print("[ELEMENTE] BoolDiff Oeffnung:", ex) # Job fuer das Oeffnungs-Volumen merken — mit effektivem # Zentrum, sodass der Rahmen am selben Ort entsteht. opening_jobs.append((op_meta, eff_pt, uk)) # Oeffnungs-Volumina aktualisieren (Rahmen + Mittelpfosten + Sims + Glas). # Mehrere Brep-Pieces pro Oeffnung — alle bekommen die gleiche # Oeffnungs-ID und werden bei jedem Regen komplett neu aufgebaut. op_layer = _ensure_layer(doc, _layer_path_volume(doc, geschoss_name)) for op_meta, pt_loc, op_uk in opening_jobs: # Alte Volume-Objekte dieser Oeffnung loeschen for o, _m in _find_objects_by_wall_id(doc, op_meta["id"], "oeffnung_volume"): try: doc.Objects.Delete(o.Id, True) except Exception as ex: print("[ELEMENTE] del old oeff vol:", ex) # Neue Pieces bauen pieces = _make_oeffnung_pieces(geom, pt_loc, meta["dicke"], op_meta, op_uk) for pbrep in pieces: op_attrs = Rhino.DocObjects.ObjectAttributes() op_attrs.LayerIndex = op_layer _attach_meta(op_attrs, op_meta["id"], "oeffnung_volume", op_meta["geschoss"], meta["dicke"], "", "", oeff_typ=op_meta.get("oeff_typ"), oeff_parent=op_meta.get("oeff_parent"), oeff_breite=op_meta.get("oeff_breite"), oeff_hoehe=op_meta.get("oeff_hoehe"), oeff_brueest=op_meta.get("oeff_brueest"), oeff_rahmen_b=op_meta.get("oeff_rahmen_b"), oeff_rahmen_tiefe=op_meta.get("oeff_rahmen_tiefe"), oeff_rahmen_pos=op_meta.get("oeff_rahmen_pos"), oeff_fluegel=op_meta.get("oeff_fluegel"), oeff_sims_aus=op_meta.get("oeff_sims_aus"), oeff_sims_in=op_meta.get("oeff_sims_in"), oeff_glas=op_meta.get("oeff_glas"), oeff_referenz=op_meta.get("oeff_referenz")) doc.Objects.AddBrep(pbrep, op_attrs) vol_type = "wand_volume" layer = _ensure_layer(doc, _layer_path_volume(doc, geschoss_name)) src_layer = _ensure_layer(doc, _layer_path_axis(doc, geschoss_name)) elif meta["type"] == "decke_outline": uk, ok = _resolve_decke_z(doc, meta["geschoss"], meta["dicke"], meta["uk_override"], meta["ok_override"]) brep = _make_decke_volume(geom, meta["dicke"], uk, ok) vol_type = "decke_volume" layer = _ensure_layer(doc, _layer_path_decke(doc, geschoss_name)) src_layer = layer elif meta["type"] == "dach_outline": base = _resolve_dach_base(doc, meta["geschoss"], meta["uk_override"]) dt = meta.get("dach_typ", "pult") if dt == "sattel": brep = _make_satteldach_brep(geom, meta["dicke"], base, meta.get("neigung", 30.0)) elif dt == "walm": brep = _make_walmdach_brep(geom, meta["dicke"], base, meta.get("neigung", 30.0)) elif dt == "mansarde": brep = _make_mansardendach_brep( geom, meta["dicke"], base, meta.get("neigung", 30.0), meta.get("neigung_unten", 60.0), meta.get("knick_h", 2.0), variante=meta.get("dach_variante", "walm")) else: # pult brep = _make_pultdach_volume(geom, meta["dicke"], base, meta.get("neigung", 30.0), meta.get("eave_idx", 0)) vol_type = "dach_volume" layer = _ensure_layer(doc, _layer_path_dach(doc, geschoss_name)) src_layer = layer elif meta["type"] == "treppe_axis": # Start- und Zielgeschoss → uk/ok aus OKFF-Differenz. # H-Override hat Vorrang vor Zielgeschoss. g_start = _geschoss_by_id(doc, meta["geschoss"]) g_end = _geschoss_by_id(doc, meta.get("geschoss_end", "")) if g_start is None: return False uk = float(g_start.get("okff", 0.0)) h_over = meta.get("treppe_h_over", "") if h_over: try: ok = uk + float(h_over) except Exception: ok = uk + float(g_start.get("hoehe", 3.0)) elif g_end is not None: ok = float(g_end.get("okff", uk + 3.0)) else: ok = uk + float(g_start.get("hoehe", 3.0)) art = meta.get("treppe_art", "gerade") if art == "l": brep = _make_treppe_l_volume(geom, meta.get("treppe_breite", 1.0), meta.get("treppe_referenz", "mid"), meta.get("treppe_n", 15), uk, ok, modus=meta.get("treppe_modus", "flach"), lauf_d=meta.get("treppe_lauf_d", 0.18)) elif art == "wendel": brep = _make_treppe_wendel_volume( geom, meta.get("treppe_breite", 1.0), meta.get("treppe_referenz", "mid"), meta.get("treppe_n", 15), uk, ok, modus=meta.get("treppe_modus", "flach"), lauf_d=meta.get("treppe_lauf_d", 0.18)) else: brep = _make_treppe_volume(geom, meta.get("treppe_breite", 1.0), meta.get("treppe_referenz", "mid"), meta.get("treppe_n", 15), uk, ok, modus=meta.get("treppe_modus", "flach"), lauf_d=meta.get("treppe_lauf_d", 0.18)) vol_type = "treppe_volume" layer = _ensure_layer(doc, _layer_path_treppe(doc, geschoss_name)) src_layer = layer else: return False # Migration: Source-Objekt (Achse/Outline) auf den aktuellen Layer # schieben, falls es noch auf einem alten Layer steht (z.B. von einer # frueheren Bug-Version auf "01_WAND" / "06_3D_VOLUMEN") try: if src_layer >= 0 and src_obj.Attributes.LayerIndex != src_layer: new_attrs = src_obj.Attributes.Duplicate() new_attrs.LayerIndex = src_layer doc.Objects.ModifyAttributes(src_obj, new_attrs, True) except Exception as ex: print("[ELEMENTE] migrate src-layer:", ex) if brep is None: return False vol_obj = _find_target_volume(doc, element_id) attrs = Rhino.DocObjects.ObjectAttributes() attrs.LayerIndex = layer _attach_meta(attrs, element_id, vol_type, meta["geschoss"], meta["dicke"], meta["uk_override"], meta["ok_override"], meta.get("referenz", "mid"), neigung=meta.get("neigung"), eave_idx=meta.get("eave_idx"), dach_typ=meta.get("dach_typ"), neigung_unten=meta.get("neigung_unten"), knick_h=meta.get("knick_h"), dach_variante=meta.get("dach_variante"), geschoss_end=meta.get("geschoss_end"), treppe_breite=meta.get("treppe_breite"), treppe_n=meta.get("treppe_n"), treppe_referenz=meta.get("treppe_referenz"), treppe_modus=meta.get("treppe_modus"), treppe_lauf_d=meta.get("treppe_lauf_d"), treppe_art=meta.get("treppe_art"), treppe_h_over=meta.get("treppe_h_over"), treppe_soll=meta.get("treppe_soll")) if vol_obj is not None: doc.Objects.Replace(vol_obj.Id, brep) vol_obj_new = doc.Objects.Find(vol_obj.Id) if vol_obj_new is not None: vol_obj_new.Attributes = attrs vol_obj_new.CommitChanges() else: doc.Objects.AddBrep(brep, attrs) return True # Alias fuer Backwards-Compat / interne Aufrufer _regenerate_volume = _regenerate_element # --- Bridge ----------------------------------------------------------------- class ElementeBridge(panel_base.BaseBridge): def __init__(self): panel_base.BaseBridge.__init__(self, "elemente") self._last_selection_ids = () def _on_ready(self): self._send_state() def handle(self, data): if not isinstance(data, dict): return t = data.get("type", "") p = data.get("payload") or {} if not isinstance(p, dict): p = {} if t == "READY": self._on_ready() elif t == "LIST": self._send_state() elif t == "CREATE_WALL": self._cmd_create_wall(p) elif t == "CREATE_DECKE": self._cmd_create_decke(p) elif t == "CREATE_DACH": self._cmd_create_dach(p) elif t == "CREATE_FENSTER": self._cmd_create_oeffnung(p, "fenster") elif t == "CREATE_TUER": self._cmd_create_oeffnung(p, "tuer") elif t == "CREATE_TREPPE": self._cmd_create_treppe(p) elif t == "UPDATE_WALL": self._update_wall(p) elif t == "UPDATE_ELEMENT": self._update_wall(p) # gleiche Logik fuer alle elif t == "DELETE_WALL": self._delete_wall(p.get("id")) elif t == "DELETE_ELEMENT": self._delete_wall(p.get("id")) elif t == "REGENERATE_ALL": self._regenerate_all() def _send_state(self): doc = Rhino.RhinoDoc.ActiveDoc if doc is None: self.send("STATE", {"elements": [], "geschosse": [], "selection": None}) return geschosse = _load_geschosse(doc) # Alle Source-Objekte (Achsen + Outlines) durchgehen elements = [] seen_ids = set() for obj in doc.Objects: meta = _read_meta(obj) if meta is None: continue if meta["type"] not in SOURCE_TYPES: continue if meta["id"] in seen_ids: continue seen_ids.add(meta["id"]) g = _geschoss_by_id(doc, meta["geschoss"]) geschoss_name = g.get("name", "?") if g else "?" selected = obj.IsSelected(False) > 0 base = { "id": meta["id"], "objectId": str(obj.Id), "geschoss": meta["geschoss"], "geschossName": geschoss_name, "dicke": meta["dicke"], "ukOverride": meta["uk_override"], "okOverride": meta["ok_override"], "selected": selected, } if meta["type"] == "wand_axis": uk, ok = _resolve_uk_ok(doc, meta["geschoss"], meta["uk_override"], meta["ok_override"]) base.update({ "kind": "wand", "referenz": meta.get("referenz", "mid"), "uk": uk, "ok": ok, }) elif meta["type"] == "decke_outline": uk, ok = _resolve_decke_z(doc, meta["geschoss"], meta["dicke"], meta["uk_override"], meta["ok_override"]) base.update({ "kind": "decke", "uk": uk, "ok": ok, }) elif meta["type"] == "dach_outline": base_h = _resolve_dach_base(doc, meta["geschoss"], meta["uk_override"]) base.update({ "kind": "dach", "uk": base_h, "ok": base_h, "neigung": meta.get("neigung", 30.0), "eaveIdx": meta.get("eave_idx", 0), "dachTyp": meta.get("dach_typ", "pult"), "neigungUnten": meta.get("neigung_unten", 60.0), "knickH": meta.get("knick_h", 2.0), "dachVariante": meta.get("dach_variante", "walm"), }) elif meta["type"] == "oeffnung_point": base.update({ "kind": meta.get("oeff_typ", "fenster"), "parentId": meta.get("oeff_parent", ""), "breite": meta.get("oeff_breite", 1.0), "hoehe": meta.get("oeff_hoehe", 1.4), "brueest": meta.get("oeff_brueest", 0.9), "rahmenB": meta.get("oeff_rahmen_b", 0.06), "rahmenTiefe": meta.get("oeff_rahmen_tiefe", 0.08), "rahmenPos": meta.get("oeff_rahmen_pos", "mid"), "fluegel": meta.get("oeff_fluegel", 1), "simsAus": meta.get("oeff_sims_aus", "ohne"), "simsIn": meta.get("oeff_sims_in", "ohne"), "glas": bool(meta.get("oeff_glas", False)), "oeffReferenz": meta.get("oeff_referenz", "mid"), }) else: # treppe_axis gs = _geschoss_by_id(doc, meta["geschoss"]) ge = _geschoss_by_id(doc, meta.get("geschoss_end", "")) try: uk = float(gs.get("okff", 0.0)) if gs else 0.0 except Exception: uk = 0.0 hov = meta.get("treppe_h_over", "") if hov: try: ok = uk + float(hov) except Exception: ok = uk + 3.0 elif ge is not None: try: ok = float(ge.get("okff", uk + 3.0)) except Exception: ok = uk + 3.0 else: try: ok = uk + float(gs.get("hoehe", 3.0)) if gs else uk + 3.0 except Exception: ok = uk + 3.0 # Lauflinien-Laenge aus dem Source-Curve try: lauf_len = float(obj.Geometry.GetLength()) except Exception: lauf_len = 0.0 base.update({ "kind": "treppe", "geschossEnd": meta.get("geschoss_end", ""), "geschossEndName": (ge.get("name") if ge else ""), "breite": meta.get("treppe_breite", 1.0), "nStufen": meta.get("treppe_n", 15), "treppeReferenz": meta.get("treppe_referenz", "mid"), "treppeModus": meta.get("treppe_modus", "flach"), "treppeArt": meta.get("treppe_art", "gerade"), "laufD": meta.get("treppe_lauf_d", 0.18), "laufLen": lauf_len, "uk": uk, "ok": ok, "hOver": meta.get("treppe_h_over", ""), "soll": meta.get("treppe_soll", _TREPPE_SOLL_DEFAULT), }) elements.append(base) sel_id = next((e["id"] for e in elements if e["selected"]), None) self.send("STATE", { "elements": elements, "geschosse": [{"id": g.get("id"), "name": g.get("name")} for g in geschosse if isinstance(g, dict)], "selection": sel_id, "activeGeschoss": _active_geschoss_id(doc), "activeGeschossName": _active_geschoss_name(doc), }) # --- Wand-Befehle ------------------------------------------------------- def _cmd_create_wall(self, p): """Interaktive Wand-Erzeugung mit Modus-Auswahl. Modi: - Polylinie: mehrere Punkte, Enter / Klick auf letzten = fertig - Linie: 2 Punkte, danach automatisch fertig - Spline: mehrere Kontrollpunkte, Enter = fertig, Achse wird ein interpolierter NURBS - Bogen: 3-Punkt-Bogen (Anfang, Mittelpunkt, Ende) Plus Optionen: Referenz, Dicke.""" doc = Rhino.RhinoDoc.ActiveDoc if doc is None: return geschoss = p.get("geschoss") or _active_geschoss_id(doc) if not geschoss: print("[ELEMENTE] Kein Geschoss aktiv"); return # Last-Used: Werte ueberleben den Befehl. Frontend kann ueberschreiben, # sonst nehmen wir den letzten Wert aus der Session. d_in = p.get("dicke") try: dicke = float(d_in) if d_in else _last("wand_dicke", 0.25) except Exception: dicke = _last("wand_dicke", 0.25) uk_over = p.get("ukOverride", "") ok_over = p.get("okOverride", "") referenz = p.get("referenz") or _last("wand_referenz", "mid") try: import Rhino.Input.Custom as ric from Rhino.Input import GetResult except Exception as ex: print("[ELEMENTE] Imports:", ex); return modi = ["Polylinie", "Linie", "Rechteck", "Spline", "Bogen"] modus = p.get("modus") or _last("wand_modus", "Polylinie") if modus not in modi: modus = "Polylinie" ref_codes = ["mid", "left", "right"] ref_labels = ["Mittig", "Links", "Rechts"] try: ref_idx = ref_codes.index(referenz) except ValueError: ref_idx = 0 def _build_prompt(base): return "{} [Modus={}, Referenz={}, Dicke={:.3f}]".format( base, modus, ref_labels[ref_idx], dicke) first_pt = None try: while True: gp = ric.GetPoint() gp.SetCommandPrompt(_build_prompt("Wand: Startpunkt")) opt_modus = gp.AddOptionList("Modus", modi, modi.index(modus)) opt_ref = gp.AddOptionList("Referenz", ref_labels, ref_idx) opt_dicke = gp.AddOption("Dicke") res = gp.Get() if res == GetResult.Option: if gp.OptionIndex() == opt_modus: try: modus = modi[gp.Option().CurrentListOptionIndex] except Exception: pass elif gp.OptionIndex() == opt_ref: try: ref_idx = gp.Option().CurrentListOptionIndex except Exception: pass elif gp.OptionIndex() == opt_dicke: try: gn = ric.GetNumber() gn.SetCommandPrompt("Wand-Dicke") gn.SetDefaultNumber(dicke) gn.SetLowerLimit(0.01, False) if gn.Get() == GetResult.Number: dicke = float(gn.Number()) except Exception as ex: print("[ELEMENTE] GetNumber:", ex) continue if res != GetResult.Point: return first_pt = gp.Point() break except Exception as ex: print("[ELEMENTE] wand first-point:", ex); return referenz = ref_codes[ref_idx] try: if modus == "Rechteck": axes = self._collect_wall_rectangle(doc, first_pt, dicke, referenz) if not axes: print("[ELEMENTE] Rechteck abgebrochen"); return for ac in axes: self._make_wall_from_axis(doc, ac, geschoss, dicke, uk_over, ok_over, referenz) _save_last(wand_dicke=dicke, wand_referenz=referenz, wand_modus=modus) self._send_state() return if modus == "Polylinie": axis_curve = self._collect_wall_polyline(doc, first_pt, dicke, referenz, ends_after=None) elif modus == "Linie": axis_curve = self._collect_wall_polyline(doc, first_pt, dicke, referenz, ends_after=1) elif modus == "Spline": axis_curve = self._collect_wall_spline(doc, first_pt, dicke, referenz) elif modus == "Bogen": axis_curve = self._collect_wall_arc(doc, first_pt, dicke, referenz) else: axis_curve = None except Exception as ex: print("[ELEMENTE] wand collect:", ex); return if axis_curve is None: print("[ELEMENTE] keine gueltige Achse"); return self._make_wall_from_axis(doc, axis_curve, geschoss, dicke, uk_over, ok_over, referenz) _save_last(wand_dicke=dicke, wand_referenz=referenz, wand_modus=modus) self._send_state() def _collect_wall_polyline(self, doc, first_pt, dicke, referenz, ends_after=None): """Sammelt eine Polyline-Achse. ends_after=N: nach N weiteren Punkten automatisch beenden (1 = klassische Linie aus 2 Punkten).""" try: import Rhino.Input.Custom as ric from Rhino.Input import GetResult except Exception: return None points = [first_pt] tol = max(doc.ModelAbsoluteTolerance, 1e-4) while True: if ends_after is not None and len(points) > ends_after: break gp = ric.GetPoint() label = "Endpunkt" if ends_after == 1 else \ "Naechster Punkt (Enter = fertig)" gp.SetCommandPrompt(label) if ends_after != 1: gp.AcceptNothing(True) try: gp.SetBasePoint(points[-1], True) except Exception: pass try: preview = _make_preview_handler(list(points), dicke, referenz) gp.DynamicDraw += preview except Exception: pass res = gp.Get() if res == GetResult.Nothing: break if res != GetResult.Point: break pt = gp.Point() if pt.DistanceTo(points[-1]) < tol and ends_after != 1: break points.append(pt) if len(points) < 2: return None pts3d = [rg.Point3d(p.X, p.Y, 0) for p in points] if len(pts3d) == 2: return rg.LineCurve(pts3d[0], pts3d[1]) return rg.PolylineCurve(rg.Polyline(pts3d)) def _collect_wall_spline(self, doc, first_pt, dicke, referenz): """Spline-Wand: interpolierter NURBS durch Kontrollpunkte. Live-Preview zeigt Kurve + Wand-Kanten.""" try: import Rhino.Input.Custom as ric from Rhino.Input import GetResult except Exception: return None points = [first_pt] tol = max(doc.ModelAbsoluteTolerance, 1e-4) while True: gp = ric.GetPoint() gp.SetCommandPrompt("Spline-Kontrollpunkt (Enter = fertig)") gp.AcceptNothing(True) try: gp.SetBasePoint(points[-1], True) except Exception: pass try: preview = _make_spline_preview_handler(list(points), dicke, referenz) gp.DynamicDraw += preview except Exception: pass res = gp.Get() if res == GetResult.Nothing: break if res != GetResult.Point: break pt = gp.Point() if pt.DistanceTo(points[-1]) < tol: break points.append(pt) if len(points) < 2: return None pts3d = [rg.Point3d(p.X, p.Y, 0) for p in points] if len(pts3d) == 2: return rg.LineCurve(pts3d[0], pts3d[1]) try: return rg.Curve.CreateInterpolatedCurve(pts3d, 3) except Exception: return rg.PolylineCurve(rg.Polyline(pts3d)) def _collect_wall_arc(self, doc, first_pt, dicke, referenz): """3-Punkt-Bogen mit Live-Preview ueber 2 Phasen (Mittel- und Endpunkt).""" try: import Rhino.Input.Custom as ric from Rhino.Input import GetResult except Exception: return None # Phase 1: Punkt auf dem Bogen gp = ric.GetPoint() gp.SetCommandPrompt("Punkt auf dem Bogen") try: gp.SetBasePoint(first_pt, True) except Exception: pass try: preview = _make_arc_preview_handler(first_pt, None, dicke, referenz) gp.DynamicDraw += preview except Exception: pass res = gp.Get() if res != GetResult.Point: return None mid = gp.Point() # Phase 2: Endpunkt — mit Bogen-Preview gp = ric.GetPoint() gp.SetCommandPrompt("Endpunkt") try: gp.SetBasePoint(mid, True) except Exception: pass try: preview = _make_arc_preview_handler(first_pt, mid, dicke, referenz) gp.DynamicDraw += preview except Exception: pass res = gp.Get() if res != GetResult.Point: return None end = gp.Point() p0 = rg.Point3d(first_pt.X, first_pt.Y, 0) p1 = rg.Point3d(mid.X, mid.Y, 0) p2 = rg.Point3d(end.X, end.Y, 0) arc = rg.Arc(p0, p1, p2) if not arc.IsValid: return None return rg.ArcCurve(arc) def _collect_wall_rectangle(self, doc, c1, dicke, referenz): """Rechteck-Wand: zweite (diagonale) Ecke. Liefert Liste von 4 Line-Curves (eine pro Seite) — vier eigenstaendige Waende.""" try: import Rhino.Input.Custom as ric from Rhino.Input import GetResult except Exception: return None gp = ric.GetPoint() gp.SetCommandPrompt("Gegenueberliegende Ecke") try: gp.SetBasePoint(c1, True) except Exception: pass try: preview = _make_rectangle_wall_preview_handler(c1, dicke, referenz) gp.DynamicDraw += preview except Exception: pass res = gp.Get() if res != GetResult.Point: return None c2 = gp.Point() p1 = rg.Point3d(c1.X, c1.Y, 0) p2 = rg.Point3d(c2.X, c1.Y, 0) p3 = rg.Point3d(c2.X, c2.Y, 0) p4 = rg.Point3d(c1.X, c2.Y, 0) # Im Uhrzeigersinn — Referenz "links" landet damit innen, "rechts" aussen return [ rg.LineCurve(p1, p2), rg.LineCurve(p2, p3), rg.LineCurve(p3, p4), rg.LineCurve(p4, p1), ] def _make_wall_from_axis(self, doc, axis_curve, geschoss_id, dicke, uk_over, ok_over, referenz): """Erzeugt Wand aus beliebiger Achsen-Curve (Line, Polyline, NURBS, Arc).""" wall_id = "wall_" + uuid.uuid4().hex[:10] g = _geschoss_by_id(doc, geschoss_id) geschoss_name = g.get("name", "EG") if g else "EG" axis_layer = _ensure_layer(doc, _layer_path_axis(doc, geschoss_name)) axis = axis_curve.DuplicateCurve() try: z0 = axis.PointAtStart.Z if abs(z0) > 1e-6: axis.Transform(rg.Transform.Translation(0, 0, -z0)) except Exception: pass attrs = Rhino.DocObjects.ObjectAttributes() attrs.LayerIndex = axis_layer _attach_meta(attrs, wall_id, "wand_axis", geschoss_id, dicke, uk_over, ok_over, referenz) if doc.Objects.AddCurve(axis, attrs) == System.Guid.Empty: print("[ELEMENTE] Wand AddCurve fehlgeschlagen"); return _regenerate_element(doc, wall_id) doc.Views.Redraw() print("[ELEMENTE] Wand erzeugt: {}".format(wall_id)) def _cmd_create_decke(self, p): """Decken-Erzeugung mit Modus-Auswahl: Polylinie, Rechteck, Rechteck-3-Punkte oder Kreis. Modus + Dicke per Command-Option.""" doc = Rhino.RhinoDoc.ActiveDoc if doc is None: return geschoss = p.get("geschoss") or _active_geschoss_id(doc) if not geschoss: print("[ELEMENTE] Kein Geschoss aktiv"); return d_in = p.get("dicke") try: dicke = float(d_in) if d_in else _last("decke_dicke", 0.20) except Exception: dicke = _last("decke_dicke", 0.20) uk_over = p.get("ukOverride", "") ok_over = p.get("okOverride", "") try: import Rhino.Input.Custom as ric from Rhino.Input import GetResult except Exception as ex: print("[ELEMENTE] Imports:", ex); return modi = ["Polylinie", "Rechteck", "Rechteck3Punkte", "Kreis"] modus = p.get("modus") or _last("decke_modus", "Polylinie") if modus not in modi: modus = "Polylinie" def _build_prompt(base): return "{} [Modus={}, Dicke={:.3f}]".format(base, modus, dicke) first_pt = None try: # Erster Punkt + Optionen while True: gp = ric.GetPoint() gp.SetCommandPrompt(_build_prompt("Decke: Startpunkt")) opt_modus = gp.AddOptionList("Modus", modi, modi.index(modus)) opt_dicke = gp.AddOption("Dicke") res = gp.Get() if res == GetResult.Option: if gp.OptionIndex() == opt_modus: try: modus = modi[gp.Option().CurrentListOptionIndex] except Exception: pass elif gp.OptionIndex() == opt_dicke: try: gn = ric.GetNumber() gn.SetCommandPrompt("Decken-Dicke") gn.SetDefaultNumber(dicke) gn.SetLowerLimit(0.01, False) if gn.Get() == GetResult.Number: dicke = float(gn.Number()) except Exception as ex: print("[ELEMENTE] GetNumber:", ex) continue if res != GetResult.Point: return first_pt = gp.Point() break except Exception as ex: print("[ELEMENTE] decke first-point:", ex); return outline_curve = None try: if modus == "Polylinie": outline_curve = self._collect_polyline_outline(doc, first_pt) elif modus == "Rechteck": outline_curve = _collect_rectangle(doc, first_pt) elif modus == "Rechteck3Punkte": outline_curve = _collect_rectangle_3pt(doc, first_pt) elif modus == "Kreis": outline_curve = _collect_circle(doc, first_pt) except Exception as ex: print("[ELEMENTE] decke collect:", ex) return if outline_curve is None or not outline_curve.IsClosed: print("[ELEMENTE] keine gueltige Outline") return self._make_decke_from_outline(doc, outline_curve, geschoss, dicke, uk_over, ok_over) _save_last(decke_dicke=dicke, decke_modus=modus) self._send_state() def _collect_polyline_outline(self, doc, first_pt): """Sammelt eine geschlossene Polyline via aufeinanderfolgendes GetPoint mit Live-Preview. Enter / Klick auf Startpunkt schliesst.""" try: import Rhino.Input.Custom as ric from Rhino.Input import GetResult except Exception: return None points = [first_pt] tol = max(doc.ModelAbsoluteTolerance, 1e-4) while True: gp = ric.GetPoint() gp.SetCommandPrompt("Naechster Punkt (Enter / Klick auf Start = schliessen)") gp.AcceptNothing(True) try: gp.SetBasePoint(points[-1], True) except Exception: pass try: preview = _make_decke_preview_handler(list(points)) gp.DynamicDraw += preview except Exception: pass res = gp.Get() if res == GetResult.Nothing: break if res != GetResult.Point: break pt = gp.Point() if pt.DistanceTo(points[0]) < tol: break if pt.DistanceTo(points[-1]) < tol: break points.append(pt) if len(points) < 3: return None pts3d = [rg.Point3d(p.X, p.Y, 0) for p in points] if pts3d[0].DistanceTo(pts3d[-1]) > 1e-6: pts3d.append(pts3d[0]) return rg.PolylineCurve(rg.Polyline(pts3d)) def _make_decke_from_outline(self, doc, outline_curve, geschoss_id, dicke, uk_over, ok_over): """Decke aus beliebiger geschlossener Outline-Curve (Polyline, Rechteck, Kreis, ...). Curve wird so wie sie ist gespeichert; das Volumen wird per Extrusion erzeugt.""" element_id = "decke_" + uuid.uuid4().hex[:10] g = _geschoss_by_id(doc, geschoss_id) geschoss_name = g.get("name", "EG") if g else "EG" layer = _ensure_layer(doc, _layer_path_decke(doc, geschoss_name)) # Sicherstellen dass die Curve auf Z=0 liegt outline = outline_curve.DuplicateCurve() try: z0 = outline.PointAtStart.Z if abs(z0) > 1e-6: outline.Transform(rg.Transform.Translation(0, 0, -z0)) except Exception: pass attrs = Rhino.DocObjects.ObjectAttributes() attrs.LayerIndex = layer _attach_meta(attrs, element_id, "decke_outline", geschoss_id, dicke, uk_over, ok_over, "mid") outline_id = doc.Objects.AddCurve(outline, attrs) if outline_id == System.Guid.Empty: print("[ELEMENTE] Decke AddCurve fehlgeschlagen"); return _regenerate_element(doc, element_id) doc.Views.Redraw() print("[ELEMENTE] Decke erzeugt: {}".format(element_id)) def _cmd_create_dach(self, p): """Pultdach-Erzeugung mit Modus-Auswahl: Polylinie / Rechteck / Rechteck-3-Punkte. Die ERSTE Kante (Punkt 1 → Punkt 2) ist die Traufkante. Neigung + Dicke per Command-Option.""" doc = Rhino.RhinoDoc.ActiveDoc if doc is None: return geschoss = p.get("geschoss") or _active_geschoss_id(doc) if not geschoss: print("[ELEMENTE] Kein Geschoss aktiv"); return d_in = p.get("dicke") try: dicke = float(d_in) if d_in else _last("dach_dicke", 0.20) except Exception: dicke = _last("dach_dicke", 0.20) n_in = p.get("neigung") try: neigung = float(n_in) if n_in else _last("dach_neigung", 30.0) except Exception: neigung = _last("dach_neigung", 30.0) uk_over = p.get("ukOverride", "") try: import Rhino.Input.Custom as ric from Rhino.Input import GetResult except Exception as ex: print("[ELEMENTE] Imports:", ex); return modi = ["Polylinie", "Rechteck", "Rechteck3Punkte"] modus = p.get("modus") or _last("dach_modus", "Polylinie") if modus not in modi: modus = "Polylinie" typen = ["Pult", "Sattel", "Walm", "Mansarde"] dach_typ = p.get("dachTyp") or _last("dach_typ", "Pult") if dach_typ not in typen: dach_typ = "Pult" # Mansarde-spezifische Defaults try: neigung_unten = float(p.get("neigungUnten") or _last("dach_neigung_unten", 60.0)) except Exception: neigung_unten = 60.0 try: knick_h = float(p.get("knickH") or _last("dach_knick_h", 2.0)) except Exception: knick_h = 2.0 varianten = ["Walm", "Giebel", "Walm-Giebel"] variante_code_map = {"Walm": "walm", "Giebel": "giebel", "Walm-Giebel": "walm_giebel"} dach_variante = p.get("dachVariante") or _last("dach_variante", "Walm") if dach_variante not in varianten: dach_variante = "Walm" def _build_prompt(base): extra = "" if dach_typ == "Mansarde": extra = ", Variante={}".format(dach_variante) return "{} [Typ={}{}, Modus={}, Neigung={:.1f}°, Dicke={:.3f}]".format( base, dach_typ, extra, modus, neigung, dicke) first_pt = None try: while True: gp = ric.GetPoint() gp.SetCommandPrompt(_build_prompt("Dach: Startpunkt (1. Kante = Traufe)")) opt_typ = gp.AddOptionList("Typ", typen, typen.index(dach_typ)) opt_var = None if dach_typ == "Mansarde": opt_var = gp.AddOptionList("Variante", varianten, varianten.index(dach_variante)) opt_modus = gp.AddOptionList("Modus", modi, modi.index(modus)) opt_n = gp.AddOption("Neigung") opt_d = gp.AddOption("Dicke") res = gp.Get() if res == GetResult.Option: if gp.OptionIndex() == opt_typ: try: dach_typ = typen[gp.Option().CurrentListOptionIndex] except Exception: pass elif opt_var is not None and gp.OptionIndex() == opt_var: try: dach_variante = varianten[gp.Option().CurrentListOptionIndex] except Exception: pass elif gp.OptionIndex() == opt_modus: try: modus = modi[gp.Option().CurrentListOptionIndex] except Exception: pass elif gp.OptionIndex() == opt_n: try: gn = ric.GetNumber() gn.SetCommandPrompt("Dach-Neigung in Grad") gn.SetDefaultNumber(neigung) gn.SetLowerLimit(0.0, False) gn.SetUpperLimit(89.0, False) if gn.Get() == GetResult.Number: neigung = float(gn.Number()) except Exception as ex: print("[ELEMENTE] GetNumber:", ex) elif gp.OptionIndex() == opt_d: try: gn = ric.GetNumber() gn.SetCommandPrompt("Dach-Dicke") gn.SetDefaultNumber(dicke) gn.SetLowerLimit(0.01, False) if gn.Get() == GetResult.Number: dicke = float(gn.Number()) except Exception as ex: print("[ELEMENTE] GetNumber:", ex) continue if res != GetResult.Point: return first_pt = gp.Point() break except Exception as ex: print("[ELEMENTE] dach first-point:", ex); return outline_curve = None try: if modus == "Polylinie": outline_curve = self._collect_polyline_outline(doc, first_pt) elif modus == "Rechteck": outline_curve = _collect_rectangle(doc, first_pt) elif modus == "Rechteck3Punkte": outline_curve = _collect_rectangle_3pt(doc, first_pt) except Exception as ex: print("[ELEMENTE] dach collect:", ex); return if outline_curve is None or not outline_curve.IsClosed: print("[ELEMENTE] keine gueltige Outline"); return # Sattel/Walm/Mansarde brauchen Rechteck-Outline. Sonst Fallback Pult. dt_code = dach_typ.lower() if dt_code in ("sattel", "walm", "mansarde"): try: ok, poly = outline_curve.TryGetPolyline() if not ok or poly is None or poly.Count != 5: print("[ELEMENTE] {} braucht Rechteck-Outline — Fallback Pult".format(dach_typ)) dt_code = "pult" except Exception: dt_code = "pult" self._make_dach_from_outline(doc, outline_curve, geschoss, dicke, neigung, 0, uk_over, dt_code, neigung_unten=neigung_unten, knick_h=knick_h, dach_variante=variante_code_map.get( dach_variante, "walm")) _save_last(dach_dicke=dicke, dach_neigung=neigung, dach_modus=modus, dach_typ=dach_typ, dach_neigung_unten=neigung_unten, dach_knick_h=knick_h, dach_variante=dach_variante) self._send_state() def _make_dach_from_outline(self, doc, outline_curve, geschoss_id, dicke, neigung, eave_idx, uk_over, dach_typ="pult", neigung_unten=60.0, knick_h=2.0, dach_variante="walm"): """Dach aus geschlossener Outline-PolylineCurve.""" element_id = "dach_" + uuid.uuid4().hex[:10] g = _geschoss_by_id(doc, geschoss_id) geschoss_name = g.get("name", "EG") if g else "EG" layer = _ensure_layer(doc, _layer_path_dach(doc, geschoss_name)) outline = outline_curve.DuplicateCurve() try: z0 = outline.PointAtStart.Z if abs(z0) > 1e-6: outline.Transform(rg.Transform.Translation(0, 0, -z0)) except Exception: pass attrs = Rhino.DocObjects.ObjectAttributes() attrs.LayerIndex = layer _attach_meta(attrs, element_id, "dach_outline", geschoss_id, dicke, uk_over, "", "mid", neigung=neigung, eave_idx=eave_idx, dach_typ=dach_typ, neigung_unten=neigung_unten, knick_h=knick_h, dach_variante=dach_variante) outline_id = doc.Objects.AddCurve(outline, attrs) if outline_id == System.Guid.Empty: print("[ELEMENTE] Dach AddCurve fehlgeschlagen"); return _regenerate_element(doc, element_id) doc.Views.Redraw() print("[ELEMENTE] Dach erzeugt: {} ({}, neigung={}°)".format( element_id, dach_typ, neigung)) def _cmd_create_oeffnung(self, p, typ): """Fenster/Tuer-Erzeugung: User klickt auf eine Wand-Achse, dann einen Punkt darauf. Punkt-Position wird auf die Achse projiziert. Optionen: Breite, Hoehe, (Bruestung nur fuer Fenster).""" if typ not in ("fenster", "tuer"): return doc = Rhino.RhinoDoc.ActiveDoc if doc is None: return try: import Rhino.Input.Custom as ric from Rhino.Input import GetResult except Exception as ex: print("[ELEMENTE] Imports:", ex); return # Defaults if typ == "fenster": b_def = _last("fenster_breite", 1.20) h_def = _last("fenster_hoehe", 1.40) br_def = _last("fenster_brueest", 0.90) else: b_def = _last("tuer_breite", 0.90) h_def = _last("tuer_hoehe", 2.10) br_def = 0.0 try: breite = float(p.get("breite") or b_def) except Exception: breite = b_def try: hoehe = float(p.get("hoehe") or h_def) except Exception: hoehe = h_def if typ == "fenster": try: brueest = float(p.get("brueest") or br_def) except Exception: brueest = br_def else: brueest = 0.0 # 1) Wand-Achse waehlen try: gw = ric.GetObject() gw.SetCommandPrompt("Wand-Achse fuer {} waehlen".format( "Fenster" if typ == "fenster" else "Tuer")) gw.GeometryFilter = Rhino.DocObjects.ObjectType.Curve def _filter_wand(rhObj, geom, ci): m = _read_meta(rhObj) return m is not None and m.get("type") == "wand_axis" gw.SetCustomGeometryFilter(_filter_wand) res = gw.Get() if res != GetResult.Object: return wall_obj = gw.Object(0).Object() except Exception as ex: print("[ELEMENTE] Wand-Auswahl:", ex); return wall_meta = _read_meta(wall_obj) if wall_meta is None: return axis_curve = wall_obj.Geometry if not isinstance(axis_curve, rg.Curve): return # 2) Punkt auf der Achse — constrained an die Wand-Achse try: while True: gp = ric.GetPoint() prompt = "Position fuer {} [B={:.2f}, H={:.2f}".format( "Fenster" if typ == "fenster" else "Tuer", breite, hoehe) if typ == "fenster": prompt += ", Br={:.2f}".format(brueest) prompt += "]" gp.SetCommandPrompt(prompt) try: gp.Constrain(axis_curve, False) except Exception: pass opt_b = gp.AddOption("Breite") opt_h = gp.AddOption("Hoehe") opt_br = gp.AddOption("Bruestung") if typ == "fenster" else None rp = gp.Get() if rp == GetResult.Option: idx = gp.OptionIndex() if idx == opt_b: gn = ric.GetNumber() gn.SetCommandPrompt("Breite") gn.SetDefaultNumber(breite) gn.SetLowerLimit(0.05, False) if gn.Get() == GetResult.Number: breite = float(gn.Number()) elif idx == opt_h: gn = ric.GetNumber() gn.SetCommandPrompt("Hoehe") gn.SetDefaultNumber(hoehe) gn.SetLowerLimit(0.05, False) if gn.Get() == GetResult.Number: hoehe = float(gn.Number()) elif opt_br is not None and idx == opt_br: gn = ric.GetNumber() gn.SetCommandPrompt("Bruestungshoehe") gn.SetDefaultNumber(brueest) gn.SetLowerLimit(0.0, True) if gn.Get() == GetResult.Number: brueest = float(gn.Number()) continue if rp != GetResult.Point: return click_pt = gp.Point() break except Exception as ex: print("[ELEMENTE] Oeffnung point:", ex); return # Auf Achse projizieren (Constrain garantiert das eigentlich schon) try: ok, t = axis_curve.ClosestPoint(click_pt) if not ok: return on_axis = axis_curve.PointAt(t) except Exception as ex: print("[ELEMENTE] ClosestPoint:", ex); return # Point-Objekt mit Metadaten anlegen prefix = "fenster_" if typ == "fenster" else "tuer_" oeff_id = prefix + uuid.uuid4().hex[:10] geschoss = wall_meta["geschoss"] g = _geschoss_by_id(doc, geschoss) geschoss_name = g.get("name", "EG") if g else "EG" layer = _ensure_layer(doc, _layer_path_axis(doc, geschoss_name)) # Entwurfs-Defaults pro Typ is_fenster = (typ == "fenster") rahmen_b_def = _last("oeff_rahmen_b", 0.06) rahmen_t_def = _last("oeff_rahmen_tiefe", 0.08) rahmen_p_def = _last("oeff_rahmen_pos", "mid") fluegel_def = _last("{}_fluegel".format(typ), 2 if is_fenster else 1) simsa_def = "standard" if is_fenster else "ohne" simsi_def = "standard" if is_fenster else "ohne" glas_def = is_fenster referenz_def = _last("oeff_referenz", "mid") attrs = Rhino.DocObjects.ObjectAttributes() attrs.LayerIndex = layer _attach_meta(attrs, oeff_id, "oeffnung_point", geschoss, wall_meta["dicke"], "", "", oeff_typ=typ, oeff_parent=wall_meta["id"], oeff_breite=breite, oeff_hoehe=hoehe, oeff_brueest=brueest, oeff_rahmen_b=rahmen_b_def, oeff_rahmen_tiefe=rahmen_t_def, oeff_rahmen_pos=rahmen_p_def, oeff_fluegel=fluegel_def, oeff_sims_aus=simsa_def, oeff_sims_in=simsi_def, oeff_glas=glas_def, oeff_referenz=referenz_def) new_id = doc.Objects.AddPoint(on_axis, attrs) if new_id == System.Guid.Empty: print("[ELEMENTE] AddPoint fehlgeschlagen"); return # Last-used if typ == "fenster": _save_last(fenster_breite=breite, fenster_hoehe=hoehe, fenster_brueest=brueest) else: _save_last(tuer_breite=breite, tuer_hoehe=hoehe) # Eltern-Wand regen _regenerate_element(doc, wall_meta["id"]) doc.Views.Redraw() print("[ELEMENTE] {} erzeugt: {} an Wand {}".format( "Fenster" if typ == "fenster" else "Tuer", oeff_id, wall_meta["id"])) self._send_state() def _cmd_create_treppe(self, p): """Treppen-Erzeugung. Hoehe automatisch aus Geschoss-OKFF-Differenz. treppeArt aus Payload: 'gerade' (2 Punkte) | 'l' (3 Punkte mit Eck).""" doc = Rhino.RhinoDoc.ActiveDoc if doc is None: return treppe_art = p.get("treppeArt") or "gerade" if treppe_art not in _TREPPE_ARTEN: treppe_art = "gerade" geschoss_start = p.get("geschoss") or _active_geschoss_id(doc) if not geschoss_start: print("[ELEMENTE] Kein Geschoss aktiv"); return try: import Rhino.Input.Custom as ric from Rhino.Input import GetResult except Exception as ex: print("[ELEMENTE] Imports:", ex); return # Zielgeschoss-Default: das naechste Geschoss vom gleichen Typ in der # Liste, oder None falls keins existiert. geschosse = _load_geschosse(doc) gs = _geschoss_by_id(doc, geschoss_start) if gs is None: print("[ELEMENTE] Startgeschoss nicht gefunden"); return geschoss_end = p.get("geschossEnd") or "" if not geschoss_end: # naechstes Geschoss > start_okff try: start_okff = float(gs.get("okff", 0.0)) candidates = [] for g in geschosse: if not isinstance(g, dict): continue if g.get("type") != "grundriss": continue if g.get("id") == geschoss_start: continue try: o = float(g.get("okff", 0.0)) except Exception: continue if o > start_okff + 1e-6: candidates.append((o, g.get("id", ""))) candidates.sort() if candidates: geschoss_end = candidates[0][1] except Exception: pass g_end = _geschoss_by_id(doc, geschoss_end) if geschoss_end else None # Defaults try: breite = float(p.get("breite") or _last("treppe_breite", 1.0)) except Exception: breite = 1.0 referenz = p.get("referenz") or _last("treppe_referenz", "mid") if referenz not in ("mid", "links", "rechts"): referenz = "mid" # N: bei bekannter Hoehe ~S=0.18 berechnen try: uk = float(gs.get("okff", 0.0)) except Exception: uk = 0.0 if g_end is not None: try: ok = float(g_end.get("okff", uk + 3.0)) except Exception: ok = uk + 3.0 else: try: ok = uk + float(gs.get("hoehe", 3.0)) except Exception: ok = uk + 3.0 H = max(0.001, ok - uk) n_default = int(round(H / 0.18)) if n_default < 2: n_default = 2 try: n_stufen = int(p.get("nStufen") or _last("treppe_n", n_default)) except Exception: n_stufen = n_default if n_stufen < 2: n_stufen = 2 # Schrittmass-Regel: Default "regel" (Lauflinie wird auf optimale # Laenge gezwungen). User kann auf "frei" stellen. regel_mode = _last("treppe_regel", "regel") if regel_mode not in ("frei", "regel"): regel_mode = "regel" # Soll-Werte (Editable in der Treppe-Property-Card) aus sticky laden. # Default falls noch nichts gesetzt: 0.15-0.20 / 0.21-0.35 / 0.60-0.65. soll_last = _last("treppe_soll", None) soll = dict(_TREPPE_SOLL_DEFAULT) if soll_last: try: import json parsed = json.loads(soll_last) if isinstance(soll_last, str) else soll_last if isinstance(parsed, dict): for k in ("s", "a", "sa"): v = parsed.get(k) if isinstance(v, list) and len(v) >= 3: soll[k] = [float(v[0]), float(v[1]), bool(v[2])] except Exception: pass # Lauflaengen-Range fuer aktuelle N & H aus enabled Soll-Werten: # - 2S+A in [sa_lo, sa_hi] (enabled) → A in [sa_lo-2S, sa_hi-2S] # - A in [a_lo, a_hi] (enabled) # → A_range = Schnitt aller Constraints, L_range = N*A_range # Optimaler Mittelwert: L_opt = 0.63*N - 2*H def _l_range(n, h): n = max(1, int(n)) s = float(h) / n a_lo, a_hi = 0.05, 2.0 # weit-offene Defaults if soll["sa"][2]: # 2S+A enabled a_lo = max(a_lo, soll["sa"][0] - 2 * s) a_hi = min(a_hi, soll["sa"][1] - 2 * s) if soll["a"][2]: # A enabled a_lo = max(a_lo, soll["a"][0]) a_hi = min(a_hi, soll["a"][1]) if a_lo > a_hi: # widerspruechliche Constraints — gib Range zurueck zentriert mid = (a_lo + a_hi) * 0.5 a_lo = a_hi = mid return (n * a_lo, n * a_hi) def _l_optimal(n, h): lo, hi = _l_range(n, h) return max(0.3, (lo + hi) * 0.5) # Zwei Punkte fuer die Lauflinie einsammeln first_pt = None try: while True: gp = ric.GetPoint() end_name = (g_end.get("name") if g_end else "(naechste Ebene)") L_opt_show = _l_optimal(n_stufen, H) gp.SetCommandPrompt( "Treppe: Startpunkt [Hoehe {:.2f}, Stufen={}, Breite={:.2f}, Ref={}, Modus={}{}, Ziel={}]".format( H, n_stufen, breite, referenz, regel_mode, " (L_opt={:.2f})".format(L_opt_show) if regel_mode == "regel" else "", end_name)) opt_n = gp.AddOption("Stufen") opt_b = gp.AddOption("Breite") opt_ref = gp.AddOptionList("Referenz", ["Links", "Mittig", "Rechts"], {"links":0, "mid":1, "rechts":2}.get(referenz, 1)) # Regel-Option nur fuer gerade Treppen (bei L sind 2 Segmente # mit Podest dazwischen — die Regel laesst sich nicht trivial # auf 2 Klicks aufteilen). opt_reg = -1 if treppe_art != "l": opt_reg = gp.AddOptionList("Regel", ["frei", "Schrittmass"], 1 if regel_mode == "regel" else 0) res = gp.Get() if res == GetResult.Option: idx = gp.OptionIndex() if idx == opt_n: gn = ric.GetInteger() gn.SetCommandPrompt("Anzahl Stufen (Steigungen)") gn.SetDefaultInteger(n_stufen) gn.SetLowerLimit(2, False) gn.SetUpperLimit(40, False) if gn.Get() == GetResult.Number: n_stufen = int(gn.Number()) elif idx == opt_b: gn = ric.GetNumber() gn.SetCommandPrompt("Treppen-Breite") gn.SetDefaultNumber(breite) gn.SetLowerLimit(0.3, False) if gn.Get() == GetResult.Number: breite = float(gn.Number()) elif idx == opt_ref: try: v = ["links", "mid", "rechts"][gp.Option().CurrentListOptionIndex] referenz = v except Exception: pass elif opt_reg >= 0 and idx == opt_reg: try: v = ["frei", "regel"][gp.Option().CurrentListOptionIndex] regel_mode = v except Exception: pass continue if res != GetResult.Point: return first_pt = gp.Point() break except Exception as ex: print("[ELEMENTE] treppe first-pt:", ex); return # Zweiter Punkt mit DynamicDraw. Bei Regel-Modus: Maus gibt nur # die RICHTUNG vor — die Lauflinien-Laenge wird auf den optimalen # Wert fixiert (wie Rhinos Rotate-Befehl). Kein Kreis-Constraint # — der wuerde blockieren wenn die Maus weit weg ist. gp2 = ric.GetPoint() L_opt = _l_optimal(n_stufen, H) if treppe_art == "l": # L-Treppe: 2. Klick ist der Podest-Eck. Live-Preview zeigt # N1/N2 fuer die Mausposition. Regel-Modus aus (zu komplex). gp2.SetCommandPrompt( "L-Treppe: Eck-Punkt (Podest-Mitte) [Stufen={}, Breite={:.2f}]".format( n_stufen, breite)) gp2.SetBasePoint(first_pt, True) gp2.DynamicDraw += _make_treppe_l_corner_preview( first_pt, breite, referenz, n_stufen, H) elif treppe_art == "wendel": # Wendel: 1. Klick = Mittelpunkt, 2. Klick = Start (Radius # + Startwinkel). Preview: Linie center→Maus + Kreis. gp2.SetCommandPrompt( "Wendeltreppe: Start der Lauflinie (definiert Radius) [Stufen={}, Breite={:.2f}]".format( n_stufen, breite)) gp2.SetBasePoint(first_pt, True) gp2.DrawLineFromPoint(first_pt, True) elif regel_mode == "regel": L_min, L_max = _l_range(n_stufen, H) same = abs(L_max - L_min) < 1e-4 if same: gp2.SetCommandPrompt( "Treppe: Richtung (Lauflaenge {:.2f} m, Schrittmass-Regel)".format(L_min)) else: gp2.SetCommandPrompt( "Treppe: Endpunkt (Lauflaenge {:.2f}–{:.2f} m, Schrittmass-Regel)".format( L_min, L_max)) gp2.SetBasePoint(first_pt, True) if same: gp2.DynamicDraw += _make_treppe_preview_handler( first_pt, breite, referenz, n_stufen, fixed_length=L_min) else: gp2.DynamicDraw += _make_treppe_preview_handler( first_pt, breite, referenz, n_stufen, min_length=L_min, max_length=L_max) else: gp2.SetCommandPrompt( "Treppe: Endpunkt der Lauflinie (frei) [Stufen={}, Breite={:.2f}, Ref={}]".format( n_stufen, breite, referenz)) gp2.SetBasePoint(first_pt, True) gp2.DynamicDraw += _make_treppe_preview_handler( first_pt, breite, referenz, n_stufen) if gp2.Get() != GetResult.Point: return clicked = gp2.Point() if regel_mode == "regel" and treppe_art == "gerade": dx = clicked.X - first_pt.X dy = clicked.Y - first_pt.Y dist = (dx * dx + dy * dy) ** 0.5 if dist < 1e-4: print("[ELEMENTE] Keine Richtung gewaehlt"); return L_min2, L_max2 = _l_range(n_stufen, H) # Clamp Mauspos-Distanz in die Range (oder reskaliere auf fix # wenn Range gleich null). if abs(L_max2 - L_min2) < 1e-4: final_L = L_min2 else: final_L = max(L_min2, min(L_max2, dist)) second_pt = rg.Point3d(first_pt.X + dx / dist * final_L, first_pt.Y + dy / dist * final_L, first_pt.Z) else: second_pt = clicked # L-Treppe: dritter Punkt einsammeln (Endpunkt nach dem Eck) if treppe_art == "l": gp3 = ric.GetPoint() gp3.SetCommandPrompt( "L-Treppe: Endpunkt nach dem Podest [Stufen={}, Breite={:.2f}]".format( n_stufen, breite)) gp3.SetBasePoint(second_pt, True) gp3.DynamicDraw += _make_treppe_preview_handler( second_pt, breite, referenz, max(1, n_stufen // 2)) if gp3.Get() != GetResult.Point: return third_pt = gp3.Point() p_first = rg.Point3d(first_pt.X, first_pt.Y, 0) p_corner = rg.Point3d(second_pt.X, second_pt.Y, 0) p_end = rg.Point3d(third_pt.X, third_pt.Y, 0) pl = rg.Polyline([p_first, p_corner, p_end]) line = rg.PolylineCurve(pl) if line.GetLength() < 0.2: print("[ELEMENTE] L-Lauflinie zu kurz"); return elif treppe_art == "wendel": # Wendel: 3. Klick = Endpunkt (Sweep-Winkel + Drehrichtung). # Im Regel-Modus wird der Sweep auf den durch r_lauf + Soll # zulaessigen Bereich geclampt. gp3 = ric.GetPoint() gp3.SetCommandPrompt( "Wendeltreppe: Endpunkt der Lauflinie (definiert Drehwinkel) [Stufen={}, Modus={}]".format( n_stufen, regel_mode)) gp3.SetBasePoint(first_pt, True) gp3.DynamicDraw += _make_treppe_wendel_preview( first_pt, second_pt, breite, referenz, n_stufen, total_h=H, soll=soll, regel_mode=regel_mode) if gp3.Get() != GetResult.Point: return third_pt = gp3.Point() p_center = rg.Point3d(first_pt.X, first_pt.Y, 0) p_start_w = rg.Point3d(second_pt.X, second_pt.Y, 0) p_end_w = rg.Point3d(third_pt.X, third_pt.Y, 0) # Sweep + (im Regel-Modus) clampen auf gueltigen Bereich. # Dann den Endpunkt entsprechend reskalieren. import math a_s_w, dlt_w = _wendel_sweep(p_center, p_start_w, p_end_w) if regel_mode == "regel": r_lauf = math.sqrt( (p_start_w.X - p_center.X) ** 2 + (p_start_w.Y - p_center.Y) ** 2) try: s_lo, s_hi = _wendel_sweep_range( r_lauf, breite, referenz, n_stufen, H, soll) except Exception: s_lo, s_hi = 0.05, 2.0 * math.pi raw = abs(dlt_w) if raw < s_lo: clamped = s_lo elif raw > s_hi: clamped = s_hi else: clamped = raw dlt_clamped = clamped * (1.0 if dlt_w >= 0 else -1.0) # Neuen Endpunkt auf Kreis r_lauf bei Winkel a_s_w + dlt_clamped a_final = a_s_w + dlt_clamped p_end_w = rg.Point3d( p_center.X + r_lauf * math.cos(a_final), p_center.Y + r_lauf * math.sin(a_final), 0) # Wichtig: dlt_w fuer den nachfolgenden < 0.05 Check aktualisieren dlt_w = dlt_clamped if abs(dlt_w) < 0.05: print("[ELEMENTE] Wendel-Sweep zu klein"); return pl = rg.Polyline([p_center, p_start_w, p_end_w]) line = rg.PolylineCurve(pl) else: line = rg.LineCurve(rg.Point3d(first_pt.X, first_pt.Y, 0), rg.Point3d(second_pt.X, second_pt.Y, 0)) if line.GetLength() < 0.1: print("[ELEMENTE] Lauflinie zu kurz"); return # Element anlegen treppe_id = "treppe_" + uuid.uuid4().hex[:10] geschoss_name = gs.get("name", "EG") layer = _ensure_layer(doc, _layer_path_treppe(doc, geschoss_name)) attrs = Rhino.DocObjects.ObjectAttributes() attrs.LayerIndex = layer modus_def = _last("treppe_modus", "flach") if modus_def not in _TREPPE_MODI: modus_def = "flach" try: lauf_d_def = float(_last("treppe_lauf_d", 0.18)) except Exception: lauf_d_def = 0.18 _attach_meta(attrs, treppe_id, "treppe_axis", geschoss_start, breite, "", "", "mid", geschoss_end=geschoss_end, treppe_breite=breite, treppe_n=n_stufen, treppe_referenz=referenz, treppe_modus=modus_def, treppe_lauf_d=lauf_d_def, treppe_art=treppe_art) new_id = doc.Objects.AddCurve(line, attrs) if new_id == System.Guid.Empty: print("[ELEMENTE] AddCurve fehlgeschlagen"); return save_kwargs = dict(treppe_breite=breite, treppe_n=n_stufen, treppe_referenz=referenz, treppe_modus=modus_def, treppe_lauf_d=lauf_d_def, treppe_art=treppe_art) # regel_mode fuer gerade + wendel speichern (L hat keinen # sinnvollen Regel-Modus — wuerde sonst die User-Praeferenz # auf "frei" zuruecksetzen). if treppe_art in ("gerade", "wendel"): save_kwargs["treppe_regel"] = regel_mode _save_last(**save_kwargs) _regenerate_element(doc, treppe_id) doc.Views.Redraw() print("[ELEMENTE] Treppe erzeugt: {}".format(treppe_id)) self._send_state() def _update_wall(self, p): """Properties eines Elements aendern (Wand/Decke/Dach/Oeffnung). Volumen wird anschliessend regeneriert.""" doc = Rhino.RhinoDoc.ActiveDoc wall_id = p.get("id") if not wall_id: return axis_obj, old_meta = _find_source(doc, wall_id) if axis_obj is None or old_meta is None: return # Treppe: Breite/Anzahl Stufen/Referenz/Zielgeschoss if old_meta["type"] == "treppe_axis": try: tb = float(p.get("breite", old_meta.get("treppe_breite", 1.0))) except Exception: tb = old_meta.get("treppe_breite", 1.0) try: tn = int(p.get("nStufen", old_meta.get("treppe_n", 15))) except Exception: tn = old_meta.get("treppe_n", 15) if tn < 2: tn = 2 tref = p.get("treppeReferenz", old_meta.get("treppe_referenz", "mid")) if tref not in ("mid", "links", "rechts"): tref = "mid" tmod = p.get("treppeModus", old_meta.get("treppe_modus", "flach")) if tmod not in _TREPPE_MODI: tmod = "flach" try: tld = float(p.get("laufD", old_meta.get("treppe_lauf_d", 0.18))) except Exception: tld = old_meta.get("treppe_lauf_d", 0.18) gend = p.get("geschossEnd", old_meta.get("geschoss_end", "")) gstart = p.get("geschoss", old_meta["geschoss"]) attrs = axis_obj.Attributes if gstart != old_meta["geschoss"]: gs = _geschoss_by_id(doc, gstart) gn = gs.get("name", "EG") if gs else "EG" attrs.LayerIndex = _ensure_layer(doc, _layer_path_treppe(doc, gn)) # Custom H + Soll-Werte h_over = p.get("hOver", old_meta.get("treppe_h_over", "")) if h_over is None: h_over = "" if isinstance(h_over, (int, float)): h_over = "{:.4f}".format(float(h_over)) soll_in = p.get("soll", old_meta.get("treppe_soll", _TREPPE_SOLL_DEFAULT)) # Auf Form normieren if not isinstance(soll_in, dict): soll_in = dict(_TREPPE_SOLL_DEFAULT) soll_norm = {} for k, dv in _TREPPE_SOLL_DEFAULT.items(): v = soll_in.get(k, dv) if isinstance(v, list) and len(v) >= 3: try: soll_norm[k] = [float(v[0]), float(v[1]), bool(v[2])] except Exception: soll_norm[k] = list(dv) else: soll_norm[k] = list(dv) _attach_meta(attrs, wall_id, "treppe_axis", gstart, tb, "", "", "mid", geschoss_end=gend, treppe_breite=tb, treppe_n=tn, treppe_referenz=tref, treppe_modus=tmod, treppe_lauf_d=tld, treppe_art=old_meta.get("treppe_art", "gerade"), treppe_h_over=h_over, treppe_soll=soll_norm) # Persistenz fuer Creation Default try: import json _save_last(treppe_soll=json.dumps(soll_norm)) except Exception: pass axis_obj.Attributes = attrs axis_obj.CommitChanges() _regenerate_volume(doc, wall_id) doc.Views.Redraw() self._send_state() return # Oeffnung: Breite/Hoehe/Bruestung + Rahmen/Fluegel/Sims/Glas if old_meta["type"] == "oeffnung_point": try: breite = float(p.get("breite", old_meta.get("oeff_breite", 1.0))) except Exception: breite = old_meta.get("oeff_breite", 1.0) try: hoehe = float(p.get("hoehe", old_meta.get("oeff_hoehe", 1.4))) except Exception: hoehe = old_meta.get("oeff_hoehe", 1.4) otyp = old_meta.get("oeff_typ", "fenster") if otyp == "fenster": try: brueest = float(p.get("brueest", old_meta.get("oeff_brueest", 0.9))) except Exception: brueest = old_meta.get("oeff_brueest", 0.9) else: brueest = 0.0 try: rahmen_b = float(p.get("rahmenB", old_meta.get("oeff_rahmen_b", 0.06))) except Exception: rahmen_b = old_meta.get("oeff_rahmen_b", 0.06) try: rahmen_t = float(p.get("rahmenTiefe", old_meta.get("oeff_rahmen_tiefe", 0.08))) except Exception: rahmen_t = old_meta.get("oeff_rahmen_tiefe", 0.08) rahmen_p = p.get("rahmenPos", old_meta.get("oeff_rahmen_pos", "mid")) if rahmen_p not in _OEFF_RAHMEN_POS_OPTIONS: rahmen_p = "mid" try: fluegel = int(p.get("fluegel", old_meta.get("oeff_fluegel", 1))) except Exception: fluegel = old_meta.get("oeff_fluegel", 1) if fluegel < 1: fluegel = 1 simsa = p.get("simsAus", old_meta.get("oeff_sims_aus", "standard" if otyp == "fenster" else "ohne")) simsi = p.get("simsIn", old_meta.get("oeff_sims_in", "standard" if otyp == "fenster" else "ohne")) # Legacy: bool von alter UI - in String konvertieren if isinstance(simsa, bool): simsa = "standard" if simsa else "ohne" if isinstance(simsi, bool): simsi = "standard" if simsi else "ohne" if simsa not in _OEFF_SIMS_STYLES: simsa = "ohne" if simsi not in _OEFF_SIMS_STYLES: simsi = "ohne" glas = bool(p.get("glas", old_meta.get("oeff_glas", otyp == "fenster"))) oref = p.get("oeffReferenz", old_meta.get("oeff_referenz", "mid")) if oref not in _OEFF_REFERENZ_OPTIONS: oref = "mid" attrs = axis_obj.Attributes _attach_meta(attrs, wall_id, "oeffnung_point", old_meta["geschoss"], old_meta["dicke"], "", "", "mid", oeff_typ=otyp, oeff_parent=old_meta.get("oeff_parent", ""), oeff_breite=breite, oeff_hoehe=hoehe, oeff_brueest=brueest, oeff_rahmen_b=rahmen_b, oeff_rahmen_tiefe=rahmen_t, oeff_rahmen_pos=rahmen_p, oeff_fluegel=fluegel, oeff_sims_aus=simsa, oeff_sims_in=simsi, oeff_glas=glas, oeff_referenz=oref) axis_obj.Attributes = attrs axis_obj.CommitChanges() parent_id = old_meta.get("oeff_parent", "") if parent_id: _regenerate_element(doc, parent_id) doc.Views.Redraw() self._send_state() return # Neue Werte mergen geschoss = p.get("geschoss", old_meta["geschoss"]) try: dicke = float(p.get("dicke", old_meta["dicke"])) except Exception: dicke = old_meta["dicke"] uk_over = p.get("ukOverride", old_meta["uk_override"]) ok_over = p.get("okOverride", old_meta["ok_override"]) referenz = p.get("referenz", old_meta.get("referenz", "mid")) if referenz not in ("mid", "left", "right"): referenz = "mid" # Dach-spezifische Felder try: neigung = float(p.get("neigung", old_meta.get("neigung", 30.0))) except Exception: neigung = old_meta.get("neigung", 30.0) try: eave_idx = int(p.get("eaveIdx", old_meta.get("eave_idx", 0))) except Exception: eave_idx = old_meta.get("eave_idx", 0) dach_typ = p.get("dachTyp", old_meta.get("dach_typ", "pult")) if dach_typ not in ("pult", "sattel", "walm", "mansarde"): dach_typ = "pult" try: neigung_unten = float(p.get("neigungUnten", old_meta.get("neigung_unten", 60.0))) except Exception: neigung_unten = old_meta.get("neigung_unten", 60.0) try: knick_h = float(p.get("knickH", old_meta.get("knick_h", 2.0))) except Exception: knick_h = old_meta.get("knick_h", 2.0) dach_variante = p.get("dachVariante", old_meta.get("dach_variante", "walm")) if dach_variante not in ("walm", "giebel", "walm_giebel"): dach_variante = "walm" # Source-Attributes updaten attrs = axis_obj.Attributes # Bei Geschoss-Wechsel: Layer wechseln (passend zum Element-Typ) if geschoss != old_meta["geschoss"]: g = _geschoss_by_id(doc, geschoss) geschoss_name = g.get("name", "EG") if g else "EG" if old_meta["type"] == "wand_axis": attrs.LayerIndex = _ensure_layer(doc, _layer_path_axis(doc, geschoss_name)) elif old_meta["type"] == "decke_outline": attrs.LayerIndex = _ensure_layer(doc, _layer_path_decke(doc, geschoss_name)) elif old_meta["type"] == "dach_outline": attrs.LayerIndex = _ensure_layer(doc, _layer_path_dach(doc, geschoss_name)) _attach_meta(attrs, wall_id, old_meta["type"], geschoss, dicke, uk_over, ok_over, referenz, neigung=neigung, eave_idx=eave_idx, dach_typ=dach_typ, neigung_unten=neigung_unten, knick_h=knick_h, dach_variante=dach_variante) axis_obj.Attributes = attrs axis_obj.CommitChanges() # Volumen regenerieren (Layer ggf. anpassen) _regenerate_volume(doc, wall_id) doc.Views.Redraw() self._send_state() def _delete_wall(self, wall_id): """Achse + Volumen + Children loeschen. Bei Oeffnung wird die Elternwand nach dem Loeschen regeneriert (Loch verschwindet).""" doc = Rhino.RhinoDoc.ActiveDoc if not wall_id: return # Check ob es eine Oeffnung ist src_obj, src_meta = _find_source(doc, wall_id) parent_id = None if src_meta and src_meta["type"] == "oeffnung_point": parent_id = src_meta.get("oeff_parent") or None # Wenn eine Wand geloescht wird: zugehoerige Oeffnungen kaskadieren cascade_ids = [] if src_meta and src_meta["type"] == "wand_axis": for op_obj, op_meta in _find_openings_for_wall(doc, wall_id): cascade_ids.append(op_meta["id"]) for cid in cascade_ids: for obj, _m in _find_objects_by_wall_id(doc, cid): try: doc.Objects.Delete(obj.Id, True) except Exception: pass # Haupt-Element loeschen for obj, meta in _find_objects_by_wall_id(doc, wall_id): try: doc.Objects.Delete(obj.Id, True) except Exception as ex: print("[ELEMENTE] delete:", ex) # Bei Oeffnung-Delete: Elternwand regen if parent_id: _regenerate_element(doc, parent_id) doc.Views.Redraw() self._send_state() def _regenerate_all(self): """Alle Elemente (Waende + Decken) neu generieren — nuetzlich nach Geschoss-Aenderung.""" doc = Rhino.RhinoDoc.ActiveDoc if doc is None: return seen = set() for obj in list(doc.Objects): meta = _read_meta(obj) if meta is None: continue if meta["type"] not in SOURCE_TYPES: continue if meta["id"] in seen: continue seen.add(meta["id"]) _regenerate_element(doc, meta["id"]) doc.Views.Redraw() self._send_state() # --- Event-Listener --------------------------------------------------------- # Re-Entry-Guard: wenn _regenerate_volume die Brep ersetzt, feuert das # Rhino-Event nochmal — wir wollen nicht in eine Schleife geraten. _REGEN_BUSY = "_elemente_regen_busy" # Pending-Regenerate-Queue: alle wall_ids die beim naechsten Idle-Tick # regeneriert werden sollen. Debounct mehrfache Replace-Events waehrend # eines Gumball-Drags. def _pending_set(): s = sc.sticky.get("elemente_pending_regen") if s is None: s = set() sc.sticky["elemente_pending_regen"] = s return s def _queue_regen(wall_id): _pending_set().add(wall_id) def _on_object_replaced(sender, e): """Wenn eine Wand-Achse verschoben/skaliert/sub-object-editiert wird → Regeneration queuen (wird beim naechsten Idle-Tick ausgefuehrt).""" if sc.sticky.get(_REGEN_BUSY): return try: # Beide Seiten probieren — manchmal verliert e.NewRhinoObject die # UserStrings beim Replace-Roundtrip. meta = None try: meta = _read_meta(e.NewRhinoObject) except Exception: pass if meta is None: try: meta = _read_meta(e.OldRhinoObject) except Exception: pass if meta is None or meta.get("type") not in SOURCE_TYPES: return try: new_obj = e.NewRhinoObject if new_obj and not _read_meta(new_obj): attrs = new_obj.Attributes _attach_meta(attrs, meta["id"], meta["type"], meta["geschoss"], meta["dicke"], meta["uk_override"], meta["ok_override"], meta.get("referenz", "mid"), neigung=meta.get("neigung"), eave_idx=meta.get("eave_idx"), dach_typ=meta.get("dach_typ"), neigung_unten=meta.get("neigung_unten"), knick_h=meta.get("knick_h"), dach_variante=meta.get("dach_variante"), oeff_typ=meta.get("oeff_typ") or None, oeff_parent=meta.get("oeff_parent") or None, oeff_breite=meta.get("oeff_breite"), oeff_hoehe=meta.get("oeff_hoehe"), oeff_brueest=meta.get("oeff_brueest"), oeff_rahmen_b=meta.get("oeff_rahmen_b"), oeff_rahmen_tiefe=meta.get("oeff_rahmen_tiefe"), oeff_rahmen_pos=meta.get("oeff_rahmen_pos"), oeff_fluegel=meta.get("oeff_fluegel"), oeff_sims_aus=meta.get("oeff_sims_aus"), oeff_sims_in=meta.get("oeff_sims_in"), oeff_glas=meta.get("oeff_glas"), oeff_referenz=meta.get("oeff_referenz"), geschoss_end=meta.get("geschoss_end"), treppe_breite=meta.get("treppe_breite"), treppe_n=meta.get("treppe_n"), treppe_referenz=meta.get("treppe_referenz"), treppe_modus=meta.get("treppe_modus"), treppe_lauf_d=meta.get("treppe_lauf_d"), treppe_art=meta.get("treppe_art"), treppe_h_over=meta.get("treppe_h_over"), treppe_soll=meta.get("treppe_soll")) new_obj.Attributes = attrs new_obj.CommitChanges() except Exception: pass # Wenn eine Wand-Achse veraendert wurde: alle daran haengenden # Oeffnungs-Points entlang der neuen Achse migrieren (sticky). if meta.get("type") == "wand_axis": try: old_geom = e.OldRhinoObject.Geometry if e.OldRhinoObject else None new_geom = e.NewRhinoObject.Geometry if e.NewRhinoObject else None _migrate_openings_to_new_axis(meta["id"], old_geom, new_geom) except Exception as ex: print("[ELEMENTE] migrate openings:", ex) _queue_regen(meta["id"]) except Exception as ex: print("[ELEMENTE] on_object_replaced:", ex) def _migrate_openings_to_new_axis(wall_id, old_geom, new_geom): """Verschiebt alle Oeffnungs-Points einer Wand mit, wenn deren Achse veraendert wird. Mapping ueber relative Bogenlaenge: ein Oeffnungs- Punkt bei 30 % der alten Kurve sitzt nachher bei 30 % der neuen. So bleiben die Oeffnungen 'sticky' an der Wand bei Verschieben, Drehen, Skalieren oder Reshape der Achse.""" if not isinstance(old_geom, rg.Curve) or not isinstance(new_geom, rg.Curve): return doc = Rhino.RhinoDoc.ActiveDoc if doc is None: return try: old_len = old_geom.GetLength() new_len = new_geom.GetLength() except Exception: return if old_len < 1e-9 or new_len < 1e-9: return _was_busy = sc.sticky.get(_REGEN_BUSY, False) sc.sticky[_REGEN_BUSY] = True try: for op_obj, op_meta in _find_openings_for_wall(doc, wall_id): try: pt_geom = op_obj.Geometry if hasattr(pt_geom, 'Location'): cur_pos = pt_geom.Location elif isinstance(pt_geom, rg.Point3d): cur_pos = pt_geom else: continue ok_old, t_old = old_geom.ClosestPoint(cur_pos) if not ok_old: continue # Bogenlaenge auf alter Kurve bis t_old → relative Position sub = rg.Interval(old_geom.Domain.Min, t_old) try: arc_old = old_geom.GetLength(sub) except Exception: # Fallback: lineare Parameter-Interpolation dom_len = old_geom.Domain.Length arc_old = ((t_old - old_geom.Domain.Min) / dom_len) * old_len relative = arc_old / old_len if old_len > 1e-9 else 0.0 if relative < 0: relative = 0 if relative > 1: relative = 1 arc_new = relative * new_len # Parameter auf neuer Kurve bei dieser Bogenlaenge lp = new_geom.LengthParameter(arc_new) # LengthParameter Rueckgabe ist (bool, double) Tuple in IronPython3 t_new = None if isinstance(lp, tuple) and len(lp) >= 2 and lp[0]: t_new = lp[1] if t_new is None: # Fallback: lineare Parameter-Interpolation new_dom = new_geom.Domain t_new = new_dom.Min + relative * new_dom.Length new_pos = new_geom.PointAt(t_new) doc.Objects.Replace(op_obj.Id, rg.Point(new_pos)) except Exception as ex: print("[ELEMENTE] migrate one opening:", ex) finally: sc.sticky[_REGEN_BUSY] = _was_busy def _count_same_id_type(doc, element_id, type_): n = 0 for obj in doc.Objects: m = _read_meta(obj) if m and m["id"] == element_id and m["type"] == type_: n += 1 if n > 1: return n return n def _on_object_added(sender, e): """Faengt Duplikate ab (Copy/Mirror/Rotate-Copy): Rhino kopiert die UserStrings auf das neue Objekt mit. Source-Duplikate kriegen eine neue UUID, Volume-Duplikate werden geloescht (Regen baut das neue Volumen am richtigen Ort).""" if sc.sticky.get(_REGEN_BUSY): return try: new_obj = e.TheObject meta = _read_meta(new_obj) if meta is None: return doc = Rhino.RhinoDoc.ActiveDoc same_count = _count_same_id_type(doc, meta["id"], meta["type"]) if same_count <= 1: return # einziges Objekt mit dieser id, kein Duplikat if meta["type"] in SOURCE_TYPES: # Source-Duplikat: neue ID + Volumen regenerieren if meta["type"] == "wand_axis": prefix = "wall_" elif meta["type"] == "decke_outline": prefix = "decke_" elif meta["type"] == "dach_outline": prefix = "dach_" elif meta["type"] == "oeffnung_point": prefix = "fenster_" if meta.get("oeff_typ") == "fenster" else "tuer_" elif meta["type"] == "treppe_axis": prefix = "treppe_" else: prefix = "elem_" new_id = prefix + uuid.uuid4().hex[:10] attrs = new_obj.Attributes _attach_meta(attrs, new_id, meta["type"], meta["geschoss"], meta["dicke"], meta["uk_override"], meta["ok_override"], meta.get("referenz", "mid"), neigung=meta.get("neigung"), eave_idx=meta.get("eave_idx"), dach_typ=meta.get("dach_typ"), neigung_unten=meta.get("neigung_unten"), knick_h=meta.get("knick_h"), dach_variante=meta.get("dach_variante"), oeff_typ=meta.get("oeff_typ") or None, oeff_parent=meta.get("oeff_parent") or None, oeff_breite=meta.get("oeff_breite"), oeff_hoehe=meta.get("oeff_hoehe"), oeff_brueest=meta.get("oeff_brueest")) new_obj.Attributes = attrs new_obj.CommitChanges() print("[ELEMENTE] Source-Duplikat erkannt — neue ID {}".format(new_id)) _queue_regen(new_id) elif meta["type"] in VOLUME_TYPES: # Volume-Duplikat: das mit-kopierte Volumen ist verwaist, # weil das Source-Duplikat eine neue ID bekommt. Loeschen — # die Regen-Pipeline erstellt das richtige Volumen am # korrekten Ort fuer die neue ID. try: doc.Objects.Delete(new_obj.Id, True) except Exception as ex: print("[ELEMENTE] dup-volume delete:", ex) except Exception as ex: print("[ELEMENTE] on_object_added:", ex) def _on_object_deleted(sender, e): """Wenn das Source-Objekt (Achse/Outline/Oeffnungs-Point) manuell geloescht wird → verknuepftes Volumen entfernen. Bei Oeffnung: Elternwand regenerieren damit das Loch verschwindet.""" try: obj = e.TheObject meta = _read_meta(obj) if meta and meta.get("type") in SOURCE_TYPES: doc = Rhino.RhinoDoc.ActiveDoc vol = _find_target_volume(doc, meta["id"]) if vol is not None: doc.Objects.Delete(vol.Id, True) if meta["type"] == "oeffnung_point": parent_id = meta.get("oeff_parent") if parent_id: _queue_regen(parent_id) b = sc.sticky.get("elemente_bridge") if b is not None: b._send_state() except Exception as ex: print("[ELEMENTE] on_object_deleted:", ex) _SELECT_BUSY = "_elemente_select_busy" # Welche Typen werden gekoppelt? source ↔ volume bidirectional. # Aktuell Wand + Decke. Wenn's gut funktioniert auch fuer Dach, Treppe, Oeffnung. _PAIRED_VOLUME_TYPES = ("wand_volume", "decke_volume") _PAIRED_SOURCE_TYPES = ("wand_axis", "decke_outline") def _collect_partners(doc, rhino_objects): """Sammelt Partner-Objekte fuer Selection-Sync und die Source-Objekte die Grips brauchen. Liefert (partners_list, sources_with_grips_list).""" partners = [] sources = [] seen_partner_ids = set() seen_source_ids = set() for obj in rhino_objects: meta = _read_meta(obj) if meta is None: continue t = meta.get("type", "") if t in _PAIRED_VOLUME_TYPES: src, _ = _find_source(doc, meta["id"]) if src is not None: if str(src.Id) not in seen_partner_ids: partners.append(src) seen_partner_ids.add(str(src.Id)) if str(src.Id) not in seen_source_ids: sources.append(src) seen_source_ids.add(str(src.Id)) elif t in _PAIRED_SOURCE_TYPES: vol = _find_target_volume(doc, meta["id"]) if vol is not None: if str(vol.Id) not in seen_partner_ids: partners.append(vol) seen_partner_ids.add(str(vol.Id)) if str(obj.Id) not in seen_source_ids: sources.append(obj) seen_source_ids.add(str(obj.Id)) return partners, sources def _on_select_objects(sender, e): """ArchiCAD-Style bidirektionaler Selection-Sync: - Klick auf Volumen (Wand/Decke) → Source-Achse mitselektieren + Grips an - Klick auf Source-Achse → Volumen mitselektieren + Grips an So bewegen sich beide synchron bei Move/Gumball, und die Endpunkte der Lauflinie sind als Grips zum Drag verfuegbar.""" if sc.sticky.get(_SELECT_BUSY): return if sc.sticky.get(_REGEN_BUSY): return try: doc = Rhino.RhinoDoc.ActiveDoc if doc is None: return partners, sources = _collect_partners(doc, e.RhinoObjects) if not partners and not sources: return sc.sticky[_SELECT_BUSY] = True try: # Partner selektieren — idempotent for p in partners: try: if p.IsSelected(False) == 0: doc.Objects.Select(p.Id, True) except Exception as ex: print("[ELEMENTE] select partner:", ex) # Grips an Source — idempotent for s in sources: try: if not s.GripsOn: s.GripsOn = True s.CommitChanges() except Exception as ex: print("[ELEMENTE] grips on:", ex) finally: sc.sticky[_SELECT_BUSY] = False except Exception as ex: print("[ELEMENTE] on_select:", ex) def _on_deselect_objects(sender, e): """Bidirektional zu _on_select_objects: - Volume deselektiert → Source deselektieren + Grips aus - Source deselektiert → Volume deselektieren + Grips aus""" if sc.sticky.get(_SELECT_BUSY): return if sc.sticky.get(_REGEN_BUSY): return try: doc = Rhino.RhinoDoc.ActiveDoc if doc is None: return partners, sources = _collect_partners(doc, e.RhinoObjects) if not partners and not sources: return sc.sticky[_SELECT_BUSY] = True try: for p in partners: try: if p.IsSelected(False) > 0: doc.Objects.Select(p.Id, False) except Exception as ex: print("[ELEMENTE] deselect partner:", ex) for s in sources: try: if s.GripsOn: s.GripsOn = False s.CommitChanges() except Exception as ex: print("[ELEMENTE] grips off:", ex) finally: sc.sticky[_SELECT_BUSY] = False except Exception as ex: print("[ELEMENTE] on_deselect:", ex) def _on_idle_selection(sender, e): """Pollt periodisch die Selektion + verarbeitet Pending-Regenerate-Queue. Queue-Verarbeitung debounct mehrfache Replace-Events waehrend eines Gumball-Drags — pro Idle-Tick wird jede angefragte Wand einmal regeneriert.""" b = sc.sticky.get("elemente_bridge") if b is None: return doc = Rhino.RhinoDoc.ActiveDoc if doc is None: return # 1) Pending Regenerations abarbeiten (sofort, jeden Idle) pending = _pending_set() if pending: ids = list(pending) pending.clear() sc.sticky[_REGEN_BUSY] = True try: for wid in ids: try: _regenerate_volume(doc, wid) except Exception as ex: print("[ELEMENTE] regen", wid, ex) try: doc.Views.Redraw() except Exception: pass finally: sc.sticky[_REGEN_BUSY] = False # Bridge updaten — Volumen-Properties (UK/OK) koennten sich # geaendert haben durch die Edit-Aktion try: b._send_state() except Exception: pass # 2) Selektions-Poll (langsamer, ~5/s) try: b._idle_count = getattr(b, "_idle_count", 0) + 1 if b._idle_count < 10: return b._idle_count = 0 ids = tuple(sorted(str(o.Id) for o in doc.Objects.GetSelectedObjects(False, False))) if ids != getattr(b, "_last_selection_ids", ()): b._last_selection_ids = ids b._send_state() except Exception: pass def _install_listeners(bridge): flag = "elemente_listeners" sc.sticky["elemente_bridge"] = bridge if sc.sticky.get(flag): return Rhino.RhinoDoc.ReplaceRhinoObject += _on_object_replaced Rhino.RhinoDoc.AddRhinoObject += _on_object_added Rhino.RhinoDoc.DeleteRhinoObject += _on_object_deleted Rhino.RhinoDoc.SelectObjects += _on_select_objects Rhino.RhinoDoc.DeselectObjects += _on_deselect_objects Rhino.RhinoApp.Idle += _on_idle_selection sc.sticky[flag] = True print("[ELEMENTE] Listener aktiv (Replace + Add + Delete + Select + Idle)") def _bridge_factory(): b = ElementeBridge() _install_listeners(b) return b panel_base.register_and_open("elemente", "ELEMENTE", PANEL_GUID_STR, _bridge_factory, icon_spec=("E", "#5fa896"))