Files
DOSSIER/rhino/elemente.py
T
karim 9dc191be4f Initial commit — Dossier Rhino 8 Plugin
OpenStudio-Suite Architektur-Plugin fuer Rhino 8 (Mac):
- Smart-Elemente: Wand, Decke, Dach (Pult/Sattel/Walm/Mansarde),
  Oeffnungen (Fenster/Tueren mit Rahmen + Sims + Glas + Fluegel),
  Treppen (gerade · L · Wendel mit Schrittmass-Validierung)
- Live-Previews mit Step-Lines + Soll-Range-Clamping
- Bidirektionale Selection-Sync zwischen Source-Linie und Volume
- Geschoss-/Ebenen-Verwaltung mit OKFF-Persistenz
- Layouts mit PDF-Export
- Ausschnitte / Massstab / Override-Regeln
- Petrol-Gruen Theme (Rapport-konform)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 04:27:41 +02:00

4776 lines
209 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ! 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"))