9dc191be4f
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>
4776 lines
209 KiB
Python
4776 lines
209 KiB
Python
# ! 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"))
|