8f5084b085
elemente.py: - Idle-Pfad Brüstungs-Mitnahme entfernt — war Duplikat zur CommandEnd-Logik und konnte je nach Reihenfolge entweder doppelt anwenden oder gar nicht (race condition mit `_elemente_wand_z_delta` Sticky-Reset). - `float(z_delta)` mit try/except für ValueError/TypeError gewrapped — vorher konnte ein korruptes Sticky-Tuple den Idle/CommandEnd-Pass crashen. - `_elemente_replace_selected_ids` wird nach Migrate consumiert (auf None gesetzt). Sonst blieb eine stale Liste hängen und beeinflusste spätere unverwandte Migrations. - Einrückung im CommandEnd-Brüstungs-Block normalisiert. Dead Files: - `rhino/startup.py3` entfernt — veraltetes Backup ohne Marker-Code für den Launcher-Splash. `rhino/startup.py` ist die aktuelle Version. - `rhino/__pycache__` aufgeräumt (war eh in .gitignore). Kein funktionales Verhalten geändert. Audit-Findings HIGH/MEDIUM bereinigt. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
7922 lines
353 KiB
Python
7922 lines
353 KiB
Python
#! python 3
|
||
# -*- 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_WAND_LAYERED = "dossier_wand_layered" # "1" = mehrschichtig, sonst solid
|
||
_KEY_WAND_LAYERS = "dossier_wand_layers" # JSON-Liste [{name, dicke, color}]
|
||
_KEY_WAND_LAYER_IDX = "dossier_wand_layer_idx" # Layer-Index am Volume-Brep
|
||
_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"
|
||
# Decken-Aussparungen — Source = geschlossene Outline-Curve, Parent = Decke
|
||
_KEY_AUSSP_PARENT = "dossier_aussp_parent" # decke_id der Eltern-Decke
|
||
|
||
# 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]}
|
||
|
||
# Tragwerk: Stuetze / Traeger / Unterzug — gemeinsames Querschnitts-System
|
||
_KEY_TRAG_KIND = "dossier_trag_kind" # "stuetze" | "traeger" | "unterzug"
|
||
_KEY_TRAG_PROFIL = "dossier_trag_profil" # "quadrat"|"rechteck"|"rund"|"i_profil"|"rohr"
|
||
_KEY_TRAG_B = "dossier_trag_b" # Breite/Hauptdim (m)
|
||
_KEY_TRAG_H = "dossier_trag_h" # Hoehe (Rechteck/I) (m)
|
||
_KEY_TRAG_D = "dossier_trag_d" # Durchmesser (Rund/Rohr) (m)
|
||
_KEY_TRAG_T = "dossier_trag_t" # Wanddicke (Rohr/I-tweb-tflange) (m)
|
||
_KEY_TRAG_ANGLE = "dossier_trag_angle" # Rotation um Z (Grad)
|
||
_KEY_TRAG_Z_OVER = "dossier_trag_z_over" # Z-Override (m) — leer = automatisch
|
||
|
||
_TRAG_PROFILE = ("quadrat", "rechteck", "rund", "i_profil", "rohr")
|
||
_TRAG_KINDS = ("stuetze", "traeger")
|
||
|
||
# Raum (Raumstempel) — Source = geschlossene Outline-Curve, Volume = TextEntity
|
||
_KEY_RAUM_NAME = "dossier_raum_name"
|
||
_KEY_RAUM_NUMMER = "dossier_raum_nummer"
|
||
_KEY_RAUM_FUNKTION = "dossier_raum_funktion"
|
||
_KEY_RAUM_RUNDUNG = "dossier_raum_rundung" # "exakt"|"0.01"|"0.1"|"0.5"|"1"
|
||
_KEY_RAUM_TXT_H = "dossier_raum_txt_h" # Texthoehe in m
|
||
_KEY_RAUM_ALIGN = "dossier_raum_align" # "links"|"mid"|"rechts"
|
||
_KEY_RAUM_SIA = "dossier_raum_sia" # "" | "hnf" | "nnf" | "vf" | "ff"
|
||
_KEY_RAUM_FUELL = "dossier_raum_fuellung" # "" (keine) | "Solid" | Pattern-Name | "ByLayer"
|
||
|
||
_RAUM_RUNDUNGEN = ("exakt", "0.01", "0.1", "0.5", "1")
|
||
_RAUM_ALIGN = ("links", "mid", "rechts")
|
||
_RAUM_SIA_KINDS = ("", "hnf", "nnf", "vf", "ff")
|
||
_RAUM_FUNKTIONEN = (
|
||
"wohnen", "schlafen", "bad", "kueche", "essen", "flur", "diele",
|
||
"buero", "atelier", "lager", "technik", "balkon", "terrasse",
|
||
"sonstiges",
|
||
)
|
||
# SIA-416 Farbpalette nach CH-Buero-Konvention (helle, kraeftige Pastelltoene).
|
||
_SIA_COLORS_HEX = {
|
||
"hnf": "#e8a8a8", # Hauptnutzflaeche — Rot
|
||
"nnf": "#e8c498", # Nebennutzflaeche — Orange
|
||
"vf": "#e8d878", # Verkehrsflaeche — Gelb
|
||
"ff": "#a8c8e0", # Funktionsflaeche — Hellblau
|
||
}
|
||
_SIA_LABELS = {
|
||
"": "—",
|
||
"hnf": "HNF",
|
||
"nnf": "NNF",
|
||
"vf": "VF",
|
||
"ff": "FF",
|
||
}
|
||
|
||
# Cross-Doc Preset-Name fuer Override-Engine. Steuert auch das siaFillMode-
|
||
# Flag im UI: aktives Preset == diesem Namen ⇒ SIA-Modus ist an.
|
||
_SIA_PRESET_NAME = "SIA-Raeume"
|
||
|
||
|
||
def _build_sia_preset_rules():
|
||
"""Erzeugt die 4 Override-Regeln fuer SIA-416-Klassifikation. Matcht
|
||
auf den UserString `dossier_raum_sia` und setzt Outline-Farbe (+ Hatch
|
||
Pattern Solid falls ein Fuell-Hatch via Gestaltung am Raum haengt)."""
|
||
rules = []
|
||
order = [("hnf", "HNF Hauptnutz"), ("nnf", "NNF Nebennutz"),
|
||
("vf", "VF Verkehr"), ("ff", "FF Funktion")]
|
||
for code, name in order:
|
||
rules.append({
|
||
"id": "sia_" + code,
|
||
"name": name,
|
||
"enabled": True,
|
||
"condition": {
|
||
"type": "user_string",
|
||
"operator": "equals",
|
||
"key": _KEY_RAUM_SIA,
|
||
"value": code,
|
||
},
|
||
"actions": {
|
||
"color": _SIA_COLORS_HEX[code],
|
||
"hatchPattern": "Solid",
|
||
},
|
||
})
|
||
return rules
|
||
|
||
|
||
def _ensure_sia_preset(force=False):
|
||
"""Stellt sicher dass das SIA-Preset im cross-doc Presets-File existiert.
|
||
force=True: ueberschreibt bestehendes Preset (Single-Source-of-Truth aus
|
||
elemente.py). force=False: nur anlegen wenn noch nicht vorhanden — so
|
||
bleiben User-Anpassungen aus dem Overrides-Panel erhalten."""
|
||
try:
|
||
import overrides as _ov
|
||
if force or _ov.load_preset(_SIA_PRESET_NAME) is None:
|
||
_ov.save_preset(_SIA_PRESET_NAME, _build_sia_preset_rules())
|
||
except Exception as ex:
|
||
print("[ELEMENTE] ensure_sia_preset:", ex)
|
||
|
||
|
||
def _sia_fill_enabled(doc):
|
||
"""SIA-Modus aktiv? Wahr nur wenn Override-Engine global enabled IST
|
||
UND das SIA-Preset als active markiert ist. Falls der User die Engine
|
||
via Overrides-Panel ausschaltet, muss force_solid sofort entfallen."""
|
||
try:
|
||
import overrides as _ov
|
||
cfg = _ov.load_config(doc)
|
||
return (bool(cfg.get("enabled"))
|
||
and cfg.get("activePreset") == _SIA_PRESET_NAME)
|
||
except Exception:
|
||
return False
|
||
|
||
|
||
def _list_hatch_patterns(doc):
|
||
"""Liefert alle nicht-geloeschten Hatch-Pattern-Namen aus dem Doc."""
|
||
out = []
|
||
try:
|
||
for i in range(doc.HatchPatterns.Count):
|
||
hp = doc.HatchPatterns[i]
|
||
if hp is None or hp.IsDeleted: continue
|
||
name = hp.Name
|
||
if name and name not in out:
|
||
out.append(name)
|
||
except Exception as ex:
|
||
print("[ELEMENTE] list_hatch_patterns:", ex)
|
||
return out
|
||
|
||
_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):
|
||
# `_reset_panels.py` cleart sticky via `= None` (statt del), daher kann
|
||
# sc.sticky.get() den default ueberlesen und ein None zurueckgeben.
|
||
# Hier defensive Fallback → default wenn der Wert None ist.
|
||
v = sc.sticky.get("elemente_last_" + key, default)
|
||
return default if v is None else v
|
||
|
||
|
||
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 _layer_path_tragwerk(doc, geschoss_name):
|
||
"""Tragwerk (Stuetze/Traeger/Unterzug) — Sublayer 'TRAGWERK' (Code 50)."""
|
||
sub = _find_ebene_sublayer_name(doc, ["trag", "stütz", "stuetz"],
|
||
"50", "TRAGWERK",
|
||
default_color="#2f5d54", default_lw=0.50)
|
||
return "{}::{}".format(geschoss_name, sub)
|
||
|
||
|
||
def _layer_path_raum(doc, geschoss_name):
|
||
"""Raeume (Outline + Stempel) — Sublayer 'RAEUME' (Code 60)."""
|
||
sub = _find_ebene_sublayer_name(doc, ["raum", "räum", "raeum"],
|
||
"60", "RAEUME",
|
||
default_color="#7a8a9a", default_lw=0.13)
|
||
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, 95, 200, 180)
|
||
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, 95, 200, 180)
|
||
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, 95, 200, 180)
|
||
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, 95, 200, 180)
|
||
color_close = SD.Color.FromArgb(180, 150, 230, 205)
|
||
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, 95, 200, 180)
|
||
color_edge = SD.Color.FromArgb(180, 140, 215, 200)
|
||
color_step = SD.Color.FromArgb(200, 180, 240, 220)
|
||
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, 95, 200, 180)
|
||
color_edge = SD.Color.FromArgb(180, 140, 215, 200)
|
||
color_step = SD.Color.FromArgb(200, 180, 240, 220)
|
||
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, 95, 200, 180)
|
||
color_edge = SD.Color.FromArgb(180, 140, 215, 200)
|
||
color_step = SD.Color.FromArgb(200, 180, 240, 220)
|
||
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, 95, 200, 180)
|
||
color_edge = SD.Color.FromArgb(180, 140, 215, 200)
|
||
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, 95, 200, 180)
|
||
color_edge = SD.Color.FromArgb(180, 140, 215, 200)
|
||
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, 95, 200, 180)
|
||
color_edge = SD.Color.FromArgb(180, 140, 215, 200)
|
||
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_stuetze_preview(profil, B, H, D, t, angle):
|
||
"""Preview-Handler fuer Stuetze: zeichnet die Profil-Kontur am Cursor."""
|
||
import System.Drawing as SD
|
||
color_main = SD.Color.FromArgb(255, 95, 200, 180)
|
||
color_axis = SD.Color.FromArgb(180, 140, 215, 200)
|
||
def handler(sender, e):
|
||
try:
|
||
cur = rg.Point3d(e.CurrentPoint.X, e.CurrentPoint.Y, 0)
|
||
crv = _trag_profile_curve(profil, B, H, D, t, cur, angle)
|
||
if crv is not None:
|
||
try: e.Display.DrawCurve(crv, color_main, 2)
|
||
except Exception: pass
|
||
# Bei Rohr zusaetzlich inneren Kreis
|
||
if profil == "rohr":
|
||
wall_t = max(0.002, float(t))
|
||
inner_d = max(0.01, float(D) - 2.0 * wall_t)
|
||
inner = _trag_profile_curve("rund", 0, 0, inner_d, 0,
|
||
cur, 0)
|
||
if inner is not None:
|
||
try: e.Display.DrawCurve(inner, color_axis, 1)
|
||
except Exception: pass
|
||
try: e.Display.DrawPoint(cur, color_main)
|
||
except Exception: pass
|
||
except Exception: pass
|
||
return handler
|
||
|
||
|
||
def _make_traeger_preview(first_pt, profil, B, H, D, t, angle):
|
||
"""Preview-Handler fuer Traeger/Unterzug: Rubberband-Linie + Profil-
|
||
Kontur am Anfang und Ende der Achse."""
|
||
import System.Drawing as SD
|
||
color_main = SD.Color.FromArgb(255, 95, 200, 180)
|
||
color_axis = SD.Color.FromArgb(180, 140, 215, 200)
|
||
def handler(sender, e):
|
||
try:
|
||
p0 = rg.Point3d(first_pt.X, first_pt.Y, 0)
|
||
p1 = rg.Point3d(e.CurrentPoint.X, e.CurrentPoint.Y, 0)
|
||
# Rubberband-Achse
|
||
try: e.Display.DrawLine(rg.Line(p0, p1), color_main, 2)
|
||
except Exception: pass
|
||
# Profil an Anfangs- + Endpunkt
|
||
for ctr in (p0, p1):
|
||
crv = _trag_profile_curve(profil, B, H, D, t, ctr, angle)
|
||
if crv is not None:
|
||
try: e.Display.DrawCurve(crv, color_axis, 1)
|
||
except Exception: pass
|
||
try: e.Display.DrawPoint(p0, color_main)
|
||
except Exception: pass
|
||
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, 95, 200, 180)
|
||
color_edge = SD.Color.FromArgb(180, 140, 215, 200)
|
||
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 _wall_out_dirs(axis_curve):
|
||
"""Liefert (out_start, out_end) — XY-Einheitsvektoren die AUSSERHALB
|
||
der Wand zeigen am Start- bzw. Endpunkt der Achse. Bei Fehler: (None, None)."""
|
||
try:
|
||
t_s = axis_curve.TangentAtStart
|
||
t_e = axis_curve.TangentAtEnd
|
||
except Exception:
|
||
return None, None
|
||
out_s = rg.Vector3d(-t_s.X, -t_s.Y, 0)
|
||
out_e = rg.Vector3d(t_e.X, t_e.Y, 0)
|
||
try: out_s.Unitize()
|
||
except Exception: pass
|
||
try: out_e.Unitize()
|
||
except Exception: pass
|
||
return out_s, out_e
|
||
|
||
|
||
def _pt_key(p, decimals=4):
|
||
"""Hash-Key fuer Punkt-Matching mit ~0.1mm Genauigkeit."""
|
||
return (round(p.X, decimals), round(p.Y, decimals))
|
||
|
||
|
||
# --- Performance: Joint-Cache + Timing -------------------------------------
|
||
|
||
_JOINTS_CACHE_KEY = "_dossier_joints_cache"
|
||
_TIMING_KEY = "_dossier_timing_enabled"
|
||
|
||
|
||
def _invalidate_joints_cache(geschoss_id=None):
|
||
"""Invalidiert den Wand-Joint-Cache. None = alle Geschosse leeren."""
|
||
cache = sc.sticky.get(_JOINTS_CACHE_KEY)
|
||
if not isinstance(cache, dict): return
|
||
if geschoss_id is None: cache.clear()
|
||
else: cache.pop(geschoss_id, None)
|
||
|
||
|
||
class _TimedBlock(object):
|
||
"""Context-Manager fuer Performance-Messung. Aktivierbar via
|
||
sc.sticky['_dossier_timing_enabled'] = True — sonst no-op."""
|
||
def __init__(self, label):
|
||
self.label = label
|
||
self.t0 = None
|
||
def __enter__(self):
|
||
if sc.sticky.get(_TIMING_KEY):
|
||
import time
|
||
self.t0 = time.perf_counter()
|
||
return self
|
||
def __exit__(self, *args):
|
||
if self.t0 is not None:
|
||
import time
|
||
ms = (time.perf_counter() - self.t0) * 1000
|
||
print("[TIMING] {}: {:.1f}ms".format(self.label, ms))
|
||
|
||
|
||
def _collect_wall_joints(doc, geschoss_id):
|
||
"""Sammelt alle Wand-Endpunkte im Geschoss. Liefert:
|
||
{point_key: [(wall_id, "start"|"end", out_dir_vector), ...]}.
|
||
GECACHT pro geschoss_id in sc.sticky — Cache wird von den Listenern
|
||
invalidiert wenn wand_axis-Objekte hinzukommen/weichen/sich aendern.
|
||
Spart bei mehreren Walls im Doc viel Zeit (O(n) statt O(n²))."""
|
||
cache = sc.sticky.get(_JOINTS_CACHE_KEY)
|
||
if not isinstance(cache, dict):
|
||
cache = {}
|
||
sc.sticky[_JOINTS_CACHE_KEY] = cache
|
||
cached = cache.get(geschoss_id)
|
||
if cached is not None:
|
||
return cached
|
||
joints = {}
|
||
for obj in doc.Objects:
|
||
meta = _read_meta(obj)
|
||
if not meta or meta["type"] != "wand_axis": continue
|
||
if meta["geschoss"] != geschoss_id: continue
|
||
geom = obj.Geometry
|
||
if not isinstance(geom, rg.Curve): continue
|
||
p_s = geom.PointAtStart
|
||
p_e = geom.PointAtEnd
|
||
out_s, out_e = _wall_out_dirs(geom)
|
||
if out_s is None or out_e is None: continue
|
||
joints.setdefault(_pt_key(p_s), []).append(
|
||
(meta["id"], "start", out_s))
|
||
joints.setdefault(_pt_key(p_e), []).append(
|
||
(meta["id"], "end", out_e))
|
||
cache[geschoss_id] = joints
|
||
return joints
|
||
|
||
|
||
def _miter_dir(out_a, out_b):
|
||
"""Miter-Linien-Richtung (Vector3d in XY) am Joint zwischen zwei Waenden.
|
||
out_a/out_b sind die Einheitsvektoren die AUSSERHALB der Wand zeigen
|
||
am gemeinsamen Punkt. Die Miter-Linie verlaeuft entlang der Winkel-
|
||
halbierenden dieser beiden Vektoren — also direkt unit(out_a + out_b).
|
||
None bei colinearen Walls (180°, gerade Fortsetzung — kein Miter noetig)."""
|
||
bx = out_a.X + out_b.X
|
||
by = out_a.Y + out_b.Y
|
||
length = (bx*bx + by*by) ** 0.5
|
||
if length < 1e-6: return None
|
||
return rg.Vector3d(bx/length, by/length, 0)
|
||
|
||
|
||
def _detect_t_junction(doc, geschoss_id, wall_id, endpoint,
|
||
pos_tol=0.01, end_tol=0.05):
|
||
"""Sucht ob `endpoint` auf der INNEREN Achse einer anderen Wand liegt
|
||
(T-Stoss). Endpunkte der anderen Wand (Eckverbindung) werden bewusst
|
||
ausgeschlossen — die werden bereits durch die Corner-Logik abgedeckt.
|
||
Liefert (other_wall_id, b_tangent_vec3, b_dicke) oder None."""
|
||
for obj in doc.Objects:
|
||
meta = _read_meta(obj)
|
||
if not meta or meta["type"] != "wand_axis": continue
|
||
if meta["geschoss"] != geschoss_id: continue
|
||
if meta["id"] == wall_id: continue
|
||
geom = obj.Geometry
|
||
if not isinstance(geom, rg.Curve): continue
|
||
try:
|
||
ok, t = geom.ClosestPoint(endpoint)
|
||
if not ok: continue
|
||
cp = geom.PointAt(t)
|
||
dx = cp.X - endpoint.X; dy = cp.Y - endpoint.Y
|
||
if (dx*dx + dy*dy) ** 0.5 > pos_tol: continue
|
||
# Nicht in der Naehe der Endpunkte (sonst Corner statt T)
|
||
ps = geom.PointAtStart; pe = geom.PointAtEnd
|
||
d_s = ((ps.X-endpoint.X)**2 + (ps.Y-endpoint.Y)**2) ** 0.5
|
||
d_e = ((pe.X-endpoint.X)**2 + (pe.Y-endpoint.Y)**2) ** 0.5
|
||
if d_s < end_tol or d_e < end_tol: continue
|
||
tan = geom.TangentAt(t)
|
||
return (meta["id"],
|
||
rg.Vector3d(tan.X, tan.Y, 0),
|
||
float(meta["dicke"]))
|
||
except Exception: continue
|
||
return None
|
||
|
||
|
||
def _t_junction_miter(endpoint, out_dir, b_tan, b_dicke):
|
||
"""Berechnet (miter_pt, miter_dir) fuer einen T-Stoss.
|
||
miter_dir = Tangente der Durchgangs-Wand (Linie laeuft parallel zu B's Achse).
|
||
miter_pt = endpoint verschoben um d_B/2 in Approach-Richtung — also auf
|
||
der NAHEN Aussenflaeche von B (der Seite an der A ankommt)."""
|
||
perp_b = rg.Vector3d(-b_tan.Y, b_tan.X, 0)
|
||
try: perp_b.Unitize()
|
||
except Exception: return None
|
||
# A's Body liegt auf der Seite -out_dir. Approach-Seite (perp_b
|
||
# ausgerichtet zur Approach) = sign(dot(-out_dir, perp_b)).
|
||
s = -(out_dir.X * perp_b.X + out_dir.Y * perp_b.Y)
|
||
if abs(s) < 1e-6:
|
||
# A parallel zu B — kein sauberer T-Stoss
|
||
return None
|
||
side = 1.0 if s > 0 else -1.0
|
||
off = float(b_dicke) * 0.5 * side
|
||
mpt = rg.Point3d(endpoint.X + perp_b.X * off,
|
||
endpoint.Y + perp_b.Y * off, 0)
|
||
mdir = rg.Vector3d(b_tan.X, b_tan.Y, 0)
|
||
try: mdir.Unitize()
|
||
except Exception: pass
|
||
return (mpt, mdir)
|
||
|
||
|
||
def _find_dependent_walls(doc, geschoss_id, moving_wall_id, old_curve, new_curve,
|
||
pos_tol=0.01):
|
||
"""Findet alle Waende deren Geometrie sich aendert wenn moving_wall sich
|
||
aendert: jede Wand deren ENDPUNKT auf der alten oder neuen Achse von
|
||
moving_wall liegt. Deckt sowohl Corner (Endpunkt-Endpunkt-Match) als
|
||
auch T-Stoss (Endpunkt auf Achse) ab."""
|
||
deps = set()
|
||
for obj in doc.Objects:
|
||
m = _read_meta(obj)
|
||
if not m or m["type"] != "wand_axis": continue
|
||
if m["geschoss"] != geschoss_id: continue
|
||
if m["id"] == moving_wall_id: continue
|
||
geom = obj.Geometry
|
||
if not isinstance(geom, rg.Curve): continue
|
||
hit = False
|
||
for ep in (geom.PointAtStart, geom.PointAtEnd):
|
||
for ag in (old_curve, new_curve):
|
||
if not isinstance(ag, rg.Curve): continue
|
||
try:
|
||
ok, t = ag.ClosestPoint(ep)
|
||
if not ok: continue
|
||
cp = ag.PointAt(t)
|
||
if ((cp.X-ep.X)**2 + (cp.Y-ep.Y)**2) ** 0.5 < pos_tol:
|
||
hit = True; break
|
||
except Exception: continue
|
||
if hit: break
|
||
if hit: deps.add(m["id"])
|
||
return deps
|
||
|
||
|
||
def _line_line_xy(p1, d1, p2, d2):
|
||
"""Schnittpunkt zweier Geraden in XY. None bei parallel."""
|
||
cross = d1.X * d2.Y - d1.Y * d2.X
|
||
if abs(cross) < 1e-9: return None
|
||
dx = p2.X - p1.X
|
||
dy = p2.Y - p1.Y
|
||
t = (dx * d2.Y - dy * d2.X) / cross
|
||
return rg.Point3d(p1.X + t * d1.X, p1.Y + t * d1.Y, 0)
|
||
|
||
|
||
def _set_curve_endpoint(crv, which, new_pt):
|
||
"""Ersetzt Start- oder Endpunkt einer Curve. Funktioniert fuer
|
||
LineCurve + PolylineCurve. Bei anderen Typen None (Miter wird dann
|
||
uebersprungen → Fallback perpendicular cap)."""
|
||
if isinstance(crv, rg.LineCurve):
|
||
if which == "start":
|
||
return rg.LineCurve(new_pt, crv.PointAtEnd)
|
||
return rg.LineCurve(crv.PointAtStart, new_pt)
|
||
if isinstance(crv, rg.PolylineCurve):
|
||
pts = [crv.Point(i) for i in range(crv.PointCount)]
|
||
if which == "start": pts[0] = new_pt
|
||
else: pts[-1] = new_pt
|
||
return rg.PolylineCurve(rg.Polyline(pts))
|
||
return None
|
||
|
||
|
||
def _apply_miter(curve, which, miter_pt, miter_dir, max_extend):
|
||
"""Trimmt/erweitert eine Offset-Curve so dass ihr 'which'-Endpunkt auf
|
||
der Miter-Linie liegt. max_extend = Sicherheitsgrenze (Miter-Limit) —
|
||
bei sehr spitzen Winkeln wird sonst der Schnittpunkt nach unendlich
|
||
fliegen. Falls limit ueberschritten oder Schnitt nicht moeglich,
|
||
Originalkurve zurueckgeben."""
|
||
if curve is None: return curve
|
||
try:
|
||
if which == "start":
|
||
tan = curve.TangentAtStart
|
||
base = curve.PointAtStart
|
||
else:
|
||
tan = curve.TangentAtEnd
|
||
base = curve.PointAtEnd
|
||
tan = rg.Vector3d(tan.X, tan.Y, 0)
|
||
ipt = _line_line_xy(base, tan, miter_pt, miter_dir)
|
||
if ipt is None: return curve
|
||
# Miter-Limit: verschiebung darf max_extend nicht uebersteigen
|
||
dx = ipt.X - base.X; dy = ipt.Y - base.Y
|
||
if (dx*dx + dy*dy) ** 0.5 > max_extend:
|
||
return curve
|
||
modified = _set_curve_endpoint(curve, which, ipt)
|
||
return modified if modified is not None else curve
|
||
except Exception:
|
||
return curve
|
||
|
||
|
||
def _make_wall_layer_brep(axis_curve, d_left, d_right, uk, ok,
|
||
miter_start=None, miter_end=None,
|
||
max_miter_extend=None):
|
||
"""Baut einen einzelnen Schicht-Brep zwischen den Offsets d_left und
|
||
d_right von der Achse. d_left > d_right; positive Werte zeigen auf die
|
||
+perp Seite. Wird sowohl fuer solide Waende als auch einzelne Schichten
|
||
eines mehrlagigen Aufbaus verwendet."""
|
||
if not isinstance(axis_curve, rg.Curve): return None
|
||
thickness = float(d_left) - float(d_right)
|
||
if abs(thickness) < 1e-9: return None
|
||
height = float(ok) - float(uk)
|
||
if height <= 0: return None
|
||
plane = rg.Plane.WorldXY
|
||
tol = 0.001
|
||
left = _offset_curve(axis_curve, plane, float(d_left), tol)
|
||
right = _offset_curve(axis_curve, plane, float(d_right), tol)
|
||
if not left or not right: return None
|
||
L = left[0]; R = right[0]
|
||
if max_miter_extend is None:
|
||
max_miter_extend = abs(thickness) * 5.0
|
||
if miter_start is not None:
|
||
m_pt, m_dir = miter_start
|
||
L = _apply_miter(L, "start", m_pt, m_dir, max_miter_extend)
|
||
R = _apply_miter(R, "start", m_pt, m_dir, max_miter_extend)
|
||
if miter_end is not None:
|
||
m_pt, m_dir = miter_end
|
||
L = _apply_miter(L, "end", m_pt, m_dir, max_miter_extend)
|
||
R = _apply_miter(R, "end", m_pt, m_dir, max_miter_extend)
|
||
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 None
|
||
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 _wall_offsets_from_referenz(dicke, referenz):
|
||
"""Liefert (start_offset, d_total) — start_offset ist der Wert von 'links'
|
||
relativ zur Achse, d_total ist die Summe der Wand-Dicke (immer positiv)."""
|
||
dicke = float(dicke)
|
||
half = dicke / 2.0
|
||
if referenz == "left": return (0.0, dicke) # Achse auf linker Aussenkante
|
||
if referenz == "right": return (+dicke, dicke) # Achse auf rechter Aussenkante
|
||
return (+half, dicke) # mid
|
||
|
||
|
||
def _make_volume_geometry(axis_curve, dicke, uk, ok, referenz="mid",
|
||
miter_start=None, miter_end=None):
|
||
"""Solide Wand — duenner Wrapper um _make_wall_layer_brep mit Offsets
|
||
abgeleitet aus referenz."""
|
||
start_off, d_total = _wall_offsets_from_referenz(dicke, referenz)
|
||
d_left = start_off
|
||
d_right = start_off - d_total
|
||
brep = _make_wall_layer_brep(axis_curve, d_left, d_right, uk, ok,
|
||
miter_start=miter_start,
|
||
miter_end=miter_end)
|
||
if brep is None:
|
||
return _make_volume_from_line(axis_curve.PointAtStart,
|
||
axis_curve.PointAtEnd, dicke, uk, ok)
|
||
return brep
|
||
|
||
|
||
# Material-Bibliothek fuer Wand-Schichten (entwurfs-/fruephasen-orientiert).
|
||
# Jedes Material hat:
|
||
# - color: Hex-Farbe (Surface + Layer-Color)
|
||
# - hatch: Hatch-Pattern-Name (Section-Hatch in Rhino's Layer-Properties)
|
||
# - scale: Hatch-Skalierung
|
||
# Beim Regen wird pro Material eine Sub-Ebene unter 20_WAENDE erzeugt
|
||
# (z.B. `EG::20_WAENDE::Beton`) und die Section-Hatch der Sub-Ebene
|
||
# konfiguriert — sobald der User eine Clipping Plane setzt, zeigt Rhino
|
||
# automatisch die korrekte Schnitt-Symbolik pro Schicht.
|
||
_MATERIAL_LIBRARY = {
|
||
"Beton": {"color": "#9a9a9a", "hatch": "Hatch3", "scale": 1.0},
|
||
"Stahlbeton": {"color": "#888888", "hatch": "Hatch3", "scale": 0.5},
|
||
"Mauerwerk": {"color": "#b67860", "hatch": "Hatch1", "scale": 1.0},
|
||
"Dämmung": {"color": "#f4e4a0", "hatch": "Hatch2", "scale": 0.5},
|
||
"Holz": {"color": "#c89a5a", "hatch": "HatchDash", "scale": 1.0},
|
||
"Stahl": {"color": "#7a7a7a", "hatch": "Solid", "scale": 1.0},
|
||
"Putz": {"color": "#ede4d6", "hatch": "Solid", "scale": 1.0},
|
||
"Glas": {"color": "#bcd4e0", "hatch": "Solid", "scale": 1.0},
|
||
}
|
||
|
||
|
||
def _set_layer_section_hatch(doc, layer_idx, hatch_name, scale=1.0,
|
||
rotation=0.0):
|
||
"""Konfiguriert Rhinos native Section-Hatch-Properties am Layer.
|
||
Sobald eine Clipping Plane Objekte auf diesem Layer schneidet, wird
|
||
die Schnittflaeche on-the-fly mit dem konfigurierten Hatch gefuellt.
|
||
Defensiv geschrieben — falls die API-Properties in einer Rhino-Version
|
||
fehlen, geht die Funktion still durch."""
|
||
if layer_idx < 0 or layer_idx >= doc.Layers.Count: return False
|
||
try:
|
||
layer = doc.Layers[layer_idx]
|
||
hp_idx = doc.HatchPatterns.Find(hatch_name or "Solid", True)
|
||
if hp_idx < 0:
|
||
hp_idx = doc.HatchPatterns.Find("Solid", True)
|
||
if hp_idx < 0: return False
|
||
changed = False
|
||
try: layer.SectionHatchIndex = hp_idx; changed = True
|
||
except Exception: pass
|
||
try: layer.SectionHatchScale = float(scale)
|
||
except Exception: pass
|
||
try: layer.SectionHatchRotation = float(rotation)
|
||
except Exception: pass
|
||
if changed:
|
||
try: doc.Layers.Modify(layer, layer_idx, True)
|
||
except Exception: pass
|
||
return changed
|
||
except Exception as ex:
|
||
print("[ELEMENTE] _set_layer_section_hatch:", ex)
|
||
return False
|
||
|
||
|
||
def _ensure_material_sublayer(doc, geschoss_name, material_name):
|
||
"""Stellt sicher dass `<geschoss>::20_WAENDE::<material>` existiert,
|
||
mit Material-Farbe + Section-Hatch konfiguriert. Liefert Layer-Index.
|
||
Bei leerem oder unbekanntem Material: Fallback auf das normale
|
||
Wand-Volume-Layer (= Standard fuer Solid-Waende)."""
|
||
if not material_name or material_name not in _MATERIAL_LIBRARY:
|
||
return _ensure_layer(doc, _layer_path_volume(doc, geschoss_name))
|
||
mat = _MATERIAL_LIBRARY[material_name]
|
||
parent_path = _layer_path_volume(doc, geschoss_name)
|
||
full_path = "{}::{}".format(parent_path, material_name)
|
||
idx = _ensure_layer(doc, full_path)
|
||
if idx < 0: return idx
|
||
try:
|
||
import System.Drawing as SD
|
||
layer = doc.Layers[idx]
|
||
hex_str = mat["color"].lstrip("#")
|
||
r = int(hex_str[0:2], 16); g = int(hex_str[2:4], 16); b = int(hex_str[4:6], 16)
|
||
new_col = SD.Color.FromArgb(255, r, g, b)
|
||
# Nur aendern wenn die Farbe abweicht (vermeidet unnoetige Doc-Dirty)
|
||
try:
|
||
if int(layer.Color.ToArgb()) != int(new_col.ToArgb()):
|
||
layer.Color = new_col
|
||
doc.Layers.Modify(layer, idx, True)
|
||
except Exception: pass
|
||
_set_layer_section_hatch(doc, idx, mat["hatch"],
|
||
mat.get("scale", 1.0))
|
||
except Exception as ex:
|
||
print("[ELEMENTE] _ensure_material_sublayer:", ex)
|
||
return idx
|
||
|
||
|
||
def _ensure_material(doc, hex_color):
|
||
"""Findet oder erstellt ein Material mit der gegebenen Hex-Diffuse-Farbe.
|
||
Cached pro hex → Index in sc.sticky, dedupliziert geteilte Farben.
|
||
Liefert Material-Index oder -1."""
|
||
if not hex_color or not isinstance(hex_color, str): return -1
|
||
s = hex_color.strip()
|
||
if not s.startswith("#") or len(s) < 7: return -1
|
||
key = s.lower()
|
||
cache = sc.sticky.get("_dossier_material_cache")
|
||
if not isinstance(cache, dict):
|
||
cache = {}
|
||
sc.sticky["_dossier_material_cache"] = cache
|
||
# Validiere gecachte Eintraege
|
||
cached = cache.get(key)
|
||
if cached is not None:
|
||
try:
|
||
if 0 <= cached < doc.Materials.Count:
|
||
m = doc.Materials[cached]
|
||
if m is not None and not m.IsDeleted:
|
||
return cached
|
||
except Exception: pass
|
||
# stale → neu anlegen
|
||
del cache[key]
|
||
try:
|
||
import System.Drawing as SD
|
||
h = key.lstrip("#")
|
||
r = int(h[0:2], 16); g = int(h[2:4], 16); b = int(h[4:6], 16)
|
||
mat = Rhino.DocObjects.Material()
|
||
mat.Name = "Dossier_Schicht_" + h
|
||
mat.DiffuseColor = SD.Color.FromArgb(255, r, g, b)
|
||
idx = doc.Materials.Add(mat)
|
||
if idx >= 0:
|
||
cache[key] = idx
|
||
return idx
|
||
except Exception as ex:
|
||
print("[ELEMENTE] _ensure_material:", ex)
|
||
return -1
|
||
|
||
|
||
def _make_wand_layer_breps(axis_curve, layers, dicke, referenz, uk, ok,
|
||
miter_start=None, miter_end=None):
|
||
"""Baut eine Liste (brep, color_hex, name) pro Schicht. Schicht-Reihen-
|
||
folge: von der +perp-Seite zur -perp-Seite (links→rechts entlang der
|
||
Wand-Achse). layers = Liste von dicts mit Keys 'dicke', 'color', 'name'."""
|
||
out = []
|
||
if not layers:
|
||
return out
|
||
start_off, d_total = _wall_offsets_from_referenz(dicke, referenz)
|
||
cur = start_off
|
||
max_ext = float(d_total) * 5.0
|
||
for layer in layers:
|
||
try: d = float(layer.get("dicke", 0))
|
||
except Exception: d = 0.0
|
||
if d <= 0: continue
|
||
d_left = cur
|
||
d_right = cur - d
|
||
brep = _make_wall_layer_brep(axis_curve, d_left, d_right, uk, ok,
|
||
miter_start=miter_start,
|
||
miter_end=miter_end,
|
||
max_miter_extend=max_ext)
|
||
out.append((brep, layer.get("color", ""), layer.get("name", "")))
|
||
cur = d_right
|
||
return out
|
||
|
||
|
||
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,
|
||
trag_kind=None, trag_profil=None, trag_b=None,
|
||
trag_h=None, trag_d=None, trag_t=None,
|
||
trag_angle=None, trag_z_over=None,
|
||
raum_name=None, raum_nummer=None, raum_funktion=None,
|
||
raum_rundung=None, raum_txt_h=None,
|
||
raum_align=None, raum_sia=None, raum_fuellung=None,
|
||
wand_layered=None, wand_layers=None, wand_layer_idx=None,
|
||
aussp_parent=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
|
||
# Tragwerk-Felder
|
||
if trag_kind is not None and trag_kind in _TRAG_KINDS:
|
||
obj_attrs.SetUserString(_KEY_TRAG_KIND, trag_kind)
|
||
if trag_profil is not None and trag_profil in _TRAG_PROFILE:
|
||
obj_attrs.SetUserString(_KEY_TRAG_PROFIL, trag_profil)
|
||
if trag_b is not None:
|
||
obj_attrs.SetUserString(_KEY_TRAG_B, "{:.4f}".format(float(trag_b)))
|
||
if trag_h is not None:
|
||
obj_attrs.SetUserString(_KEY_TRAG_H, "{:.4f}".format(float(trag_h)))
|
||
if trag_d is not None:
|
||
obj_attrs.SetUserString(_KEY_TRAG_D, "{:.4f}".format(float(trag_d)))
|
||
if trag_t is not None:
|
||
obj_attrs.SetUserString(_KEY_TRAG_T, "{:.4f}".format(float(trag_t)))
|
||
if trag_angle is not None:
|
||
obj_attrs.SetUserString(_KEY_TRAG_ANGLE, "{:.4f}".format(float(trag_angle)))
|
||
if trag_z_over is not None:
|
||
if trag_z_over == "" or trag_z_over is None:
|
||
obj_attrs.SetUserString(_KEY_TRAG_Z_OVER, "")
|
||
else:
|
||
try: obj_attrs.SetUserString(_KEY_TRAG_Z_OVER,
|
||
"{:.4f}".format(float(trag_z_over)))
|
||
except Exception: pass
|
||
# Raum-Felder
|
||
if raum_name is not None:
|
||
obj_attrs.SetUserString(_KEY_RAUM_NAME, str(raum_name))
|
||
if raum_nummer is not None:
|
||
obj_attrs.SetUserString(_KEY_RAUM_NUMMER, str(raum_nummer))
|
||
if raum_funktion is not None:
|
||
obj_attrs.SetUserString(_KEY_RAUM_FUNKTION, str(raum_funktion))
|
||
if raum_rundung is not None and raum_rundung in _RAUM_RUNDUNGEN:
|
||
obj_attrs.SetUserString(_KEY_RAUM_RUNDUNG, raum_rundung)
|
||
if raum_txt_h is not None:
|
||
try: obj_attrs.SetUserString(_KEY_RAUM_TXT_H,
|
||
"{:.4f}".format(float(raum_txt_h)))
|
||
except Exception: pass
|
||
if raum_align is not None and raum_align in _RAUM_ALIGN:
|
||
obj_attrs.SetUserString(_KEY_RAUM_ALIGN, raum_align)
|
||
if raum_sia is not None and raum_sia in _RAUM_SIA_KINDS:
|
||
obj_attrs.SetUserString(_KEY_RAUM_SIA, raum_sia)
|
||
if raum_fuellung is not None:
|
||
# Akzeptiere Bool (legacy) oder Pattern-Name. Bool True -> "Solid".
|
||
if isinstance(raum_fuellung, bool):
|
||
v = "Solid" if raum_fuellung else ""
|
||
else:
|
||
v = str(raum_fuellung)
|
||
obj_attrs.SetUserString(_KEY_RAUM_FUELL, v)
|
||
# Wand-Schichten
|
||
if wand_layered is not None:
|
||
obj_attrs.SetUserString(_KEY_WAND_LAYERED,
|
||
"1" if bool(wand_layered) else "")
|
||
if wand_layers is not None:
|
||
try:
|
||
import json as _json
|
||
if isinstance(wand_layers, str):
|
||
# akzeptiere bereits-JSON-String
|
||
obj_attrs.SetUserString(_KEY_WAND_LAYERS, wand_layers)
|
||
else:
|
||
obj_attrs.SetUserString(_KEY_WAND_LAYERS,
|
||
_json.dumps(wand_layers,
|
||
ensure_ascii=False))
|
||
except Exception: pass
|
||
if wand_layer_idx is not None:
|
||
try: obj_attrs.SetUserString(_KEY_WAND_LAYER_IDX,
|
||
"{}".format(int(wand_layer_idx)))
|
||
except Exception: pass
|
||
# Decken-Aussparung
|
||
if aussp_parent is not None:
|
||
obj_attrs.SetUserString(_KEY_AUSSP_PARENT, str(aussp_parent))
|
||
|
||
|
||
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
|
||
# Tragwerk-Felder
|
||
tk_raw = a.GetUserString(_KEY_TRAG_KIND) or ""
|
||
if tk_raw not in _TRAG_KINDS: tk_raw = ""
|
||
tp_raw = a.GetUserString(_KEY_TRAG_PROFIL) or "quadrat"
|
||
if tp_raw not in _TRAG_PROFILE: tp_raw = "quadrat"
|
||
try: t_b = float(a.GetUserString(_KEY_TRAG_B) or "0.25")
|
||
except Exception: t_b = 0.25
|
||
try: t_h = float(a.GetUserString(_KEY_TRAG_H) or "0.25")
|
||
except Exception: t_h = 0.25
|
||
try: t_d = float(a.GetUserString(_KEY_TRAG_D) or "0.25")
|
||
except Exception: t_d = 0.25
|
||
try: t_t = float(a.GetUserString(_KEY_TRAG_T) or "0.01")
|
||
except Exception: t_t = 0.01
|
||
try: t_ang = float(a.GetUserString(_KEY_TRAG_ANGLE) or "0")
|
||
except Exception: t_ang = 0.0
|
||
t_zov = a.GetUserString(_KEY_TRAG_Z_OVER) or ""
|
||
# Raum-Felder
|
||
r_name = a.GetUserString(_KEY_RAUM_NAME) or ""
|
||
r_num = a.GetUserString(_KEY_RAUM_NUMMER) or ""
|
||
r_fkt = a.GetUserString(_KEY_RAUM_FUNKTION) or ""
|
||
r_rnd = a.GetUserString(_KEY_RAUM_RUNDUNG) or "0.1"
|
||
if r_rnd not in _RAUM_RUNDUNGEN: r_rnd = "0.1"
|
||
try: r_th = float(a.GetUserString(_KEY_RAUM_TXT_H) or "0.20")
|
||
except Exception: r_th = 0.20
|
||
r_align = a.GetUserString(_KEY_RAUM_ALIGN) or "mid"
|
||
if r_align not in _RAUM_ALIGN: r_align = "mid"
|
||
r_sia = a.GetUserString(_KEY_RAUM_SIA) or ""
|
||
if r_sia not in _RAUM_SIA_KINDS: r_sia = ""
|
||
# Default: Fuellung AUS ("" = kein Hatch). User kann Pattern-Namen
|
||
# waehlen (z.B. "Solid", "Hatch1", ...) oder "ByLayer". Legacy-
|
||
# Migration: alter Bool-Wert "1" wird zu "Solid", "0" zu "".
|
||
r_fuell_raw = a.GetUserString(_KEY_RAUM_FUELL)
|
||
if r_fuell_raw == "1": r_fuell = "Solid"
|
||
elif r_fuell_raw == "0": r_fuell = ""
|
||
else: r_fuell = r_fuell_raw or ""
|
||
# Wand-Schichten
|
||
w_layered = (a.GetUserString(_KEY_WAND_LAYERED) == "1")
|
||
w_layers_raw = a.GetUserString(_KEY_WAND_LAYERS) or ""
|
||
w_layers = []
|
||
if w_layers_raw:
|
||
try:
|
||
import json as _json
|
||
parsed = _json.loads(w_layers_raw)
|
||
if isinstance(parsed, list):
|
||
for ly in parsed:
|
||
if not isinstance(ly, dict): continue
|
||
try: d = float(ly.get("dicke", 0))
|
||
except Exception: d = 0.0
|
||
if d <= 0: continue
|
||
w_layers.append({
|
||
"name": str(ly.get("name", "")),
|
||
"dicke": d,
|
||
"color": str(ly.get("color", "")),
|
||
"material": str(ly.get("material", "")),
|
||
})
|
||
except Exception: pass
|
||
try: w_layer_idx = int(a.GetUserString(_KEY_WAND_LAYER_IDX) or "-1")
|
||
except Exception: w_layer_idx = -1
|
||
aussp_parent_raw = a.GetUserString(_KEY_AUSSP_PARENT) or ""
|
||
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,
|
||
"trag_kind": tk_raw,
|
||
"trag_profil": tp_raw,
|
||
"trag_b": t_b,
|
||
"trag_h": t_h,
|
||
"trag_d": t_d,
|
||
"trag_t": t_t,
|
||
"trag_angle": t_ang,
|
||
"trag_z_over": t_zov,
|
||
"raum_name": r_name,
|
||
"raum_nummer": r_num,
|
||
"raum_funktion": r_fkt,
|
||
"raum_rundung": r_rnd,
|
||
"raum_txt_h": r_th,
|
||
"raum_align": r_align,
|
||
"raum_sia": r_sia,
|
||
"raum_fuellung": r_fuell,
|
||
"wand_layered": w_layered,
|
||
"wand_layers": w_layers,
|
||
"wand_layer_idx": w_layer_idx,
|
||
"aussp_parent": aussp_parent_raw,
|
||
}
|
||
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",
|
||
"stuetze_point", "traeger_axis",
|
||
"raum_outline", "decke_aussparung_outline")
|
||
VOLUME_TYPES = ("wand_volume", "decke_volume", "dach_volume",
|
||
"oeffnung_volume", "treppe_volume",
|
||
"stuetze_volume", "traeger_volume",
|
||
"raum_stamp", "raum_fill")
|
||
# 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 _trag_profile_curve(profil_typ, B, H, D, t, center_pt, angle_deg=0.0):
|
||
"""Liefert eine geschlossene PolylineCurve (oder Curve) im Querschnitt
|
||
eines Tragwerk-Elements, am center_pt in der XY-Ebene, mit Rotation
|
||
angle_deg um die Z-Achse.
|
||
|
||
profil_typ:
|
||
'quadrat' — B × B
|
||
'rechteck' — B × H
|
||
'rund' — Kreis mit Durchmesser D
|
||
'i_profil' — I-Querschnitt (HEB-Style): Flanschbreite B, Hoehe H,
|
||
Flanschdicke + Stegdicke = t (vereinfacht)
|
||
'rohr' — Hohl-Rund: Aussen-D, Wand-t (gibt einen geschlossenen
|
||
Ring zurueck — fuer Brep direkt verwendbar)"""
|
||
import math
|
||
cx, cy = center_pt.X, center_pt.Y
|
||
cz = center_pt.Z
|
||
a = math.radians(float(angle_deg))
|
||
cos_a, sin_a = math.cos(a), math.sin(a)
|
||
|
||
def _xy(lx, ly):
|
||
# Lokale Koords (lx, ly) am center_pt rotieren + verschieben
|
||
wx = cx + lx * cos_a - ly * sin_a
|
||
wy = cy + lx * sin_a + ly * cos_a
|
||
return rg.Point3d(wx, wy, cz)
|
||
|
||
if profil_typ == "quadrat":
|
||
hb = float(B) * 0.5
|
||
pts = [_xy(-hb, -hb), _xy(+hb, -hb),
|
||
_xy(+hb, +hb), _xy(-hb, +hb), _xy(-hb, -hb)]
|
||
return rg.PolylineCurve(rg.Polyline(pts))
|
||
|
||
if profil_typ == "rechteck":
|
||
hb = float(B) * 0.5
|
||
hh = float(H) * 0.5
|
||
pts = [_xy(-hb, -hh), _xy(+hb, -hh),
|
||
_xy(+hb, +hh), _xy(-hb, +hh), _xy(-hb, -hh)]
|
||
return rg.PolylineCurve(rg.Polyline(pts))
|
||
|
||
if profil_typ == "rund":
|
||
r = float(D) * 0.5
|
||
plane = rg.Plane(center_pt, rg.Vector3d.ZAxis)
|
||
return rg.NurbsCurve.CreateFromCircle(rg.Circle(plane, r))
|
||
|
||
if profil_typ == "i_profil":
|
||
# HEB-Stil: Flansch breite B, Gesamthoehe H, Flansch/Steg-Dicke = t
|
||
bf = float(B) * 0.5
|
||
hf = float(H) * 0.5
|
||
tf = max(0.005, float(t)) # Flanschdicke
|
||
tw = max(0.005, float(t) * 0.6) # Stegdicke etwas duenner
|
||
wf = tw * 0.5 # halbe Stegbreite
|
||
# 12-Punkt-Polygon (I-Form, CCW)
|
||
local = [
|
||
(-bf, -hf), (+bf, -hf),
|
||
(+bf, -hf + tf), (+wf, -hf + tf),
|
||
(+wf, +hf - tf), (+bf, +hf - tf),
|
||
(+bf, +hf), (-bf, +hf),
|
||
(-bf, +hf - tf), (-wf, +hf - tf),
|
||
(-wf, -hf + tf), (-bf, -hf + tf),
|
||
]
|
||
pts = [_xy(lx, ly) for (lx, ly) in local]
|
||
pts.append(pts[0]) # close
|
||
return rg.PolylineCurve(rg.Polyline(pts))
|
||
|
||
if profil_typ == "rohr":
|
||
# Wird im Brep-Builder als Aussen-Curve und Innen-Curve gehandhabt.
|
||
# Hier nur die Aussenkante als Kreis zurueckgeben — der Builder
|
||
# macht die Subtraktion separat.
|
||
r = float(D) * 0.5
|
||
plane = rg.Plane(center_pt, rg.Vector3d.ZAxis)
|
||
return rg.NurbsCurve.CreateFromCircle(rg.Circle(plane, r))
|
||
|
||
return None
|
||
|
||
|
||
def _make_stuetze_volume(point, profil_typ, B, H, D, t, angle, uk, ok):
|
||
"""Vertikale Extrusion eines Querschnitts von uk bis ok an `point`."""
|
||
height = float(ok) - float(uk)
|
||
if height <= 1e-6: return None
|
||
base_pt = rg.Point3d(point.X, point.Y, float(uk))
|
||
crv = _trag_profile_curve(profil_typ, B, H, D, t, base_pt, angle)
|
||
if crv is None: return None
|
||
try:
|
||
ext = rg.Extrusion.Create(crv, height, True)
|
||
if ext is None: return None
|
||
outer = ext.ToBrep()
|
||
# Bei Rohr: zusaetzlich Innen-Kreis ausstanzen
|
||
if profil_typ == "rohr" and outer is not None:
|
||
wall_t = max(0.002, float(t))
|
||
inner_d = max(0.01, float(D) - 2.0 * wall_t)
|
||
inner_crv = _trag_profile_curve("rund", 0, 0, inner_d, 0,
|
||
base_pt, 0)
|
||
if inner_crv is not None:
|
||
inner_ext = rg.Extrusion.Create(inner_crv, height, True)
|
||
if inner_ext is not None:
|
||
inner_brep = inner_ext.ToBrep()
|
||
diff = rg.Brep.CreateBooleanDifference(
|
||
[outer], [inner_brep], 0.001)
|
||
if diff and len(diff) > 0:
|
||
return diff[0]
|
||
return outer
|
||
except Exception as ex:
|
||
print("[ELEMENTE] Stuetze extrusion:", ex)
|
||
return None
|
||
|
||
|
||
def _trag_profile_in_plane(profil_typ, B, H, D, t, plane, angle_deg=0.0):
|
||
"""Variante von _trag_profile_curve, die das Profil in einer
|
||
beliebigen Ebene (statt XY) aufbaut. Das Profil wird zunaechst
|
||
flach in WorldXY am Origin gebaut und dann via PlaneToPlane in
|
||
die Zielebene transformiert."""
|
||
flat = _trag_profile_curve(profil_typ, B, H, D, t,
|
||
rg.Point3d(0, 0, 0), angle_deg)
|
||
if flat is None: return None
|
||
try:
|
||
xform = rg.Transform.PlaneToPlane(rg.Plane.WorldXY, plane)
|
||
flat.Transform(xform)
|
||
except Exception: pass
|
||
return flat
|
||
|
||
|
||
def _make_traeger_volume(axis_curve, profil_typ, B, H, D, t, angle, z_top):
|
||
"""Horizontaler Traeger entlang einer Achse. Profil sitzt in einer
|
||
Ebene senkrecht zur Achse — wird zur kompletten Extrusion entlang
|
||
der Achse. `z_top` ist die OBERKANTE des Traegers."""
|
||
if not isinstance(axis_curve, rg.Curve): return None
|
||
try:
|
||
# Profil-Hoehe ermitteln (fuer Z-Versatz nach unten)
|
||
if profil_typ == "quadrat": prof_h = float(B)
|
||
elif profil_typ == "rechteck": prof_h = float(H)
|
||
elif profil_typ in ("rund", "rohr"): prof_h = float(D)
|
||
elif profil_typ == "i_profil": prof_h = float(H)
|
||
else: prof_h = 0.3
|
||
z_center = float(z_top) - prof_h * 0.5
|
||
|
||
# Achse-Richtung (XY-Ebene)
|
||
p0 = axis_curve.PointAtStart
|
||
p1 = axis_curve.PointAtEnd
|
||
tan = rg.Vector3d(p1.X - p0.X, p1.Y - p0.Y, 0)
|
||
length = tan.Length
|
||
if length < 1e-6: return None
|
||
tan.Unitize()
|
||
# Profil-Ebene: Origin am Centerline-Anfang, X = horizontal-perp,
|
||
# Y = WeltZ (hoch), Normal (Z der Ebene) = Achsenrichtung
|
||
perp = rg.Vector3d(-tan.Y, tan.X, 0)
|
||
origin = rg.Point3d(p0.X, p0.Y, z_center)
|
||
try:
|
||
plane = rg.Plane(origin, perp, rg.Vector3d.ZAxis)
|
||
except Exception:
|
||
plane = rg.Plane.WorldXY
|
||
crv = _trag_profile_in_plane(profil_typ, B, H, D, t, plane, angle)
|
||
if crv is None: return None
|
||
|
||
# Extrusion in Richtung der Achse, Laenge = length
|
||
try:
|
||
ext = rg.Extrusion.CreateExtrusion(crv, tan * length)
|
||
if ext is None: return None
|
||
outer = ext.ToBrep()
|
||
if outer is None: return None
|
||
outer = outer.CapPlanarHoles(0.001) or outer
|
||
except Exception as ex:
|
||
print("[ELEMENTE] Traeger extrusion:", ex)
|
||
return None
|
||
|
||
# Bei Rohr: Innen-Kreis ausstanzen
|
||
if profil_typ == "rohr":
|
||
wall_t = max(0.002, float(t))
|
||
inner_d = max(0.01, float(D) - 2.0 * wall_t)
|
||
inner_crv = _trag_profile_in_plane("rund", 0, 0, inner_d, 0,
|
||
plane, 0)
|
||
if inner_crv is not None:
|
||
try:
|
||
inner_ext = rg.Extrusion.CreateExtrusion(inner_crv,
|
||
tan * length)
|
||
if inner_ext is not None:
|
||
inner_brep = inner_ext.ToBrep()
|
||
if inner_brep is not None:
|
||
inner_brep = inner_brep.CapPlanarHoles(0.001) or inner_brep
|
||
diff = rg.Brep.CreateBooleanDifference(
|
||
[outer], [inner_brep], 0.001)
|
||
if diff and len(diff) > 0:
|
||
outer = diff[0]
|
||
except Exception as ex:
|
||
print("[ELEMENTE] Traeger rohr-diff:", ex)
|
||
return outer
|
||
except Exception as ex:
|
||
print("[ELEMENTE] Traeger:", ex)
|
||
return None
|
||
|
||
|
||
def _make_decke_volume(outline_curve, dicke, uk, ok, hole_curves=None):
|
||
"""Decke als Extrusion zwischen UK und OK. Optional mit Loechern
|
||
(Aussparungen) — wird ueber `Brep.CreatePlanarBreps([outer, holes...])`
|
||
+ `BrepFace.CreateExtrusion` gebaut, kein BoolDiff. Das ist deutlich
|
||
robuster fuer duenne Slabs."""
|
||
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
|
||
|
||
# Kein Loch → klassischer Extrusion-Pfad
|
||
if not hole_curves:
|
||
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()
|
||
|
||
# Mit Loechern: Curves auf z=uk bringen, planar-brep mit Loechern bauen,
|
||
# diese Face entlang Z extrudieren — fertig.
|
||
outer = outline_curve.DuplicateCurve()
|
||
holes = []
|
||
for h in hole_curves:
|
||
if not isinstance(h, rg.Curve): continue
|
||
if not h.IsClosed: continue
|
||
holes.append(h.DuplicateCurve())
|
||
# Alle auf z=uk normalisieren (BoundingBox Min.Z)
|
||
def _to_z(c, target_z):
|
||
try:
|
||
bb = c.GetBoundingBox(True)
|
||
cur = bb.Min.Z
|
||
dz = float(target_z) - cur
|
||
if abs(dz) > 1e-9:
|
||
c.Transform(rg.Transform.Translation(0, 0, dz))
|
||
except Exception: pass
|
||
_to_z(outer, uk)
|
||
for h in holes: _to_z(h, uk)
|
||
|
||
try:
|
||
tol = 0.001
|
||
planar = rg.Brep.CreatePlanarBreps([outer] + holes, tol)
|
||
if not planar or len(planar) == 0:
|
||
print("[ELEMENTE] Decke planar w/ holes — CreatePlanarBreps "
|
||
"lieferte nichts. Fallback ohne Loch.")
|
||
return _make_decke_volume(outline_curve, dicke, uk, ok, None)
|
||
base = planar[0]
|
||
if base.Faces.Count == 0:
|
||
return _make_decke_volume(outline_curve, dicke, uk, ok, None)
|
||
face = base.Faces[0]
|
||
path = rg.LineCurve(rg.Point3d(0, 0, uk),
|
||
rg.Point3d(0, 0, ok))
|
||
result = face.CreateExtrusion(path, True)
|
||
if result is None or not result.IsValid:
|
||
print("[ELEMENTE] BrepFace.CreateExtrusion fehlgeschlagen — "
|
||
"Fallback ohne Loch.")
|
||
return _make_decke_volume(outline_curve, dicke, uk, ok, None)
|
||
return result
|
||
except Exception as ex:
|
||
print("[ELEMENTE] Decke mit Loch:", ex)
|
||
return _make_decke_volume(outline_curve, dicke, uk, ok, None)
|
||
|
||
|
||
def _find_aussparungen_for_decke(doc, decke_id):
|
||
"""Alle decke_aussparung_outline Source-Curves deren aussp_parent ==
|
||
decke_id. Liefert Liste von (obj, meta)."""
|
||
out = []
|
||
for obj in doc.Objects:
|
||
meta = _read_meta(obj)
|
||
if meta is None: continue
|
||
if meta["type"] != "decke_aussparung_outline": continue
|
||
if meta.get("aussp_parent") != decke_id: continue
|
||
out.append((obj, meta))
|
||
return out
|
||
|
||
|
||
def _point_in_curve_xy(curve, pt, tol):
|
||
"""Liefert True wenn pt INNERHALB der geschlossenen 2D-Curve liegt
|
||
(XY-Projektion). Robust gegen Enum-int-Konvertierungs-Probleme in
|
||
IronPython3 — vergleicht direkt mit dem PointContainment-Enum,
|
||
Fallback auf int-Vergleich."""
|
||
try:
|
||
test = curve.Contains(pt, rg.Plane.WorldXY, tol)
|
||
except Exception:
|
||
try: test = curve.Contains(pt)
|
||
except Exception: return False
|
||
# Direkter Enum-Vergleich
|
||
try:
|
||
if test == rg.PointContainment.Inside: return True
|
||
if test == rg.PointContainment.Coincident: return True
|
||
return False
|
||
except Exception: pass
|
||
# Fallback: int-Vergleich (Inside=2, Coincident=3)
|
||
try: return int(test) in (2, 3)
|
||
except Exception: return False
|
||
|
||
|
||
def _find_decke_containing_point(doc, geschoss_id, point_xy):
|
||
"""Sucht die Decke deren Outline (in XY-Projektion) den Punkt
|
||
umschliesst. Bevorzugt Decken im aktiven Geschoss, faellt sonst auf
|
||
Decken in ANDEREN Geschossen zurueck (User kann z.B. in 1OG ein Loch
|
||
in die EG-Decke setzen). Bei mehreren Treffern: kleinste Flaeche."""
|
||
active = []
|
||
any_ = []
|
||
tol = max(doc.ModelAbsoluteTolerance, 1e-4)
|
||
for obj in doc.Objects:
|
||
meta = _read_meta(obj)
|
||
if meta is None: continue
|
||
if meta["type"] != "decke_outline": continue
|
||
geom = obj.Geometry
|
||
if not isinstance(geom, rg.Curve): continue
|
||
if not geom.IsClosed: continue
|
||
if not _point_in_curve_xy(geom, point_xy, tol): continue
|
||
try:
|
||
amp = rg.AreaMassProperties.Compute(geom)
|
||
area = abs(amp.Area) if amp is not None else 0.0
|
||
except Exception: area = 0.0
|
||
bucket = active if meta["geschoss"] == geschoss_id else any_
|
||
bucket.append((area, meta["id"]))
|
||
pool = active if active else any_
|
||
if not pool: return None
|
||
pool.sort(key=lambda x: x[0])
|
||
return pool[0][1]
|
||
|
||
|
||
# --- Raum (Raumstempel) -----------------------------------------------------
|
||
|
||
def _raum_amp(outline_curve):
|
||
"""Liefert (area, perimeter, centroid) fuer eine geschlossene Outline.
|
||
Bei Fehler oder offener Kurve: (0, 0, Point3d(0,0,0))."""
|
||
if not isinstance(outline_curve, rg.Curve):
|
||
return 0.0, 0.0, rg.Point3d(0, 0, 0)
|
||
if not outline_curve.IsClosed:
|
||
return 0.0, 0.0, rg.Point3d(0, 0, 0)
|
||
try:
|
||
amp = rg.AreaMassProperties.Compute(outline_curve)
|
||
if amp is None:
|
||
return 0.0, 0.0, rg.Point3d(0, 0, 0)
|
||
area = float(amp.Area)
|
||
ctr = amp.Centroid
|
||
try: perim = float(outline_curve.GetLength())
|
||
except Exception: perim = 0.0
|
||
return abs(area), perim, ctr
|
||
except Exception:
|
||
return 0.0, 0.0, rg.Point3d(0, 0, 0)
|
||
|
||
|
||
def _format_area(area, rundung):
|
||
"""Formatiert eine Flaeche in m^2 gemaess Rundungs-Regel.
|
||
'exakt' -> 2 NK; '0.01' -> 2 NK; '0.1' -> 1 NK; '0.5' -> 1 NK,
|
||
halbiert; '1' -> ganze m2."""
|
||
try: a = float(area)
|
||
except Exception: a = 0.0
|
||
if rundung == "1":
|
||
return "{:.0f}".format(round(a))
|
||
if rundung == "0.5":
|
||
return "{:.1f}".format(round(a * 2.0) / 2.0)
|
||
if rundung == "0.1":
|
||
return "{:.1f}".format(round(a, 1))
|
||
if rundung == "0.01":
|
||
return "{:.2f}".format(round(a, 2))
|
||
return "{:.2f}".format(a) # exakt
|
||
|
||
|
||
def _make_raum_stamp_text(centroid, name, nummer, funktion, area, rundung,
|
||
text_height, z=0.0, align="mid"):
|
||
"""Baut eine TextEntity am Centroid: 'Nummer Name\nA m^2'.
|
||
align: 'links' | 'mid' | 'rechts' — wirkt auf die Justification."""
|
||
try:
|
||
plane = rg.Plane(rg.Point3d(centroid.X, centroid.Y, float(z)),
|
||
rg.Vector3d.ZAxis)
|
||
te = rg.TextEntity()
|
||
# Zeile 1: Nummer + Name (falls vorhanden), sonst nur Name
|
||
line1 = (name or "Raum").strip()
|
||
if nummer and str(nummer).strip():
|
||
line1 = "{} {}".format(str(nummer).strip(), line1)
|
||
# Zeile 2: Flaeche
|
||
area_line = "{} m²".format(_format_area(area, rundung))
|
||
te.Text = "{}\n{}".format(line1, area_line)
|
||
te.Plane = plane
|
||
try: te.TextHeight = float(text_height)
|
||
except Exception: te.TextHeight = 0.20
|
||
try:
|
||
if align == "links": te.Justification = rg.TextJustification.MiddleLeft
|
||
elif align == "rechts": te.Justification = rg.TextJustification.MiddleRight
|
||
else: te.Justification = rg.TextJustification.MiddleCenter
|
||
except Exception: pass
|
||
return te
|
||
except Exception as ex:
|
||
print("[ELEMENTE] Raum Stamp:", ex)
|
||
return None
|
||
|
||
|
||
def _make_raum_hatch(outline_curve, z_uk, doc, pattern_name="Solid"):
|
||
"""Erzeugt einen Hatch unter der Raum-Outline am Z=z_uk + 1 mm mit
|
||
gegebenem Pattern. Color = ByObject (default hell). Override-System
|
||
kann den Hatch via Pattern + Color umfaerben."""
|
||
if not isinstance(outline_curve, rg.Curve): return None
|
||
if not outline_curve.IsClosed: return None
|
||
try:
|
||
# Pattern aufloesen — Fallback: Solid, dann Current
|
||
pattern_idx = doc.HatchPatterns.Find(pattern_name or "Solid", True)
|
||
if pattern_idx < 0:
|
||
pattern_idx = doc.HatchPatterns.Find("Solid", True)
|
||
if pattern_idx < 0:
|
||
pattern_idx = doc.HatchPatterns.CurrentHatchPatternIndex
|
||
if pattern_idx < 0: return None
|
||
crv = outline_curve.DuplicateCurve()
|
||
# Minimal nach oben — vermeidet Z-Fighting mit Decken
|
||
z_off = float(z_uk) + 0.001
|
||
crv.Transform(rg.Transform.Translation(0, 0, z_off))
|
||
hatches = rg.Hatch.Create(crv, pattern_idx, 0.0, 1.0, 0.001)
|
||
if not hatches or len(hatches) == 0: return None
|
||
return hatches[0]
|
||
except Exception as ex:
|
||
print("[ELEMENTE] Raum Hatch:", ex)
|
||
return None
|
||
|
||
|
||
# --- 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
|
||
# Decken-Aussparung hat kein eigenes Volumen → Eltern-Decke regen
|
||
if meta["type"] == "decke_aussparung_outline":
|
||
parent_id = meta.get("aussp_parent") or ""
|
||
if parent_id:
|
||
return _regenerate_element(doc, parent_id)
|
||
return False
|
||
geom = src_obj.Geometry
|
||
# Stuetze hat Point-Geometrie, alle anderen Source-Typen sind Curves
|
||
if meta["type"] == "stuetze_point":
|
||
if not (isinstance(geom, rg.Point) or isinstance(geom, rg.Point3d)):
|
||
return False
|
||
else:
|
||
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"])
|
||
# Wand-Verbindungen: Miter-Linien aus Nachbarwand-Joints (Corner + T).
|
||
miter_start = None
|
||
miter_end = None
|
||
try:
|
||
joints = _collect_wall_joints(doc, meta["geschoss"])
|
||
out_s, out_e = _wall_out_dirs(geom)
|
||
p_s = geom.PointAtStart
|
||
p_e = geom.PointAtEnd
|
||
if out_s is not None:
|
||
key_s = _pt_key(p_s)
|
||
partners_s = [(wid, end, od)
|
||
for (wid, end, od) in joints.get(key_s, [])
|
||
if wid != element_id]
|
||
if len(partners_s) == 1:
|
||
_wid, _end, other_out = partners_s[0]
|
||
mdir = _miter_dir(out_s, other_out)
|
||
if mdir is not None:
|
||
miter_start = (p_s, mdir)
|
||
elif len(partners_s) == 0:
|
||
tj = _detect_t_junction(doc, meta["geschoss"],
|
||
element_id, p_s)
|
||
if tj is not None:
|
||
_oid, b_tan, b_dicke = tj
|
||
tm = _t_junction_miter(p_s, out_s, b_tan, b_dicke)
|
||
if tm is not None: miter_start = tm
|
||
if out_e is not None:
|
||
key_e = _pt_key(p_e)
|
||
partners_e = [(wid, end, od)
|
||
for (wid, end, od) in joints.get(key_e, [])
|
||
if wid != element_id]
|
||
if len(partners_e) == 1:
|
||
_wid, _end, other_out = partners_e[0]
|
||
mdir = _miter_dir(out_e, other_out)
|
||
if mdir is not None:
|
||
miter_end = (p_e, mdir)
|
||
elif len(partners_e) == 0:
|
||
tj = _detect_t_junction(doc, meta["geschoss"],
|
||
element_id, p_e)
|
||
if tj is not None:
|
||
_oid, b_tan, b_dicke = tj
|
||
tm = _t_junction_miter(p_e, out_e, b_tan, b_dicke)
|
||
if tm is not None: miter_end = tm
|
||
except Exception as ex:
|
||
print("[ELEMENTE] wall joints:", ex)
|
||
|
||
# Schichten ermitteln. Layered + nicht-leere Liste → mehrere Breps,
|
||
# sonst ein einzelnes (solid).
|
||
layers_def = meta.get("wand_layers") or []
|
||
is_layered = bool(meta.get("wand_layered")) and len(layers_def) > 0
|
||
if is_layered:
|
||
layer_breps = _make_wand_layer_breps(
|
||
geom, layers_def, meta["dicke"],
|
||
meta.get("referenz", "mid"), uk, ok,
|
||
miter_start=miter_start, miter_end=miter_end)
|
||
else:
|
||
single_brep = _make_volume_geometry(
|
||
geom, meta["dicke"], uk, ok,
|
||
meta.get("referenz", "mid"),
|
||
miter_start=miter_start, miter_end=miter_end)
|
||
layer_breps = [(single_brep, "", "")] if single_brep else []
|
||
|
||
# Oeffnungen einsammeln + Cutouts pro Schicht anwenden.
|
||
opening_jobs = []
|
||
cutouts = []
|
||
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
|
||
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
|
||
cutouts.append(cutout)
|
||
opening_jobs.append((op_meta, eff_pt, uk))
|
||
if cutouts:
|
||
new_layer_breps = []
|
||
for (brep, color, lname) in layer_breps:
|
||
if brep is None:
|
||
new_layer_breps.append((None, color, lname)); continue
|
||
for cut in cutouts:
|
||
try:
|
||
diff = rg.Brep.CreateBooleanDifference(
|
||
[brep], [cut], 0.001)
|
||
if diff and len(diff) > 0:
|
||
brep = diff[0]
|
||
except Exception as ex:
|
||
print("[ELEMENTE] BoolDiff layer:", ex)
|
||
new_layer_breps.append((brep, color, lname))
|
||
layer_breps = new_layer_breps
|
||
|
||
# Oeffnungs-Sub-Volumina (Rahmen+Sims+Glas) erzeugen.
|
||
# Nicht-destruktiv wenn moeglich: wenn die Anzahl der Sub-Volumen gleich
|
||
# bleibt (z.B. bei Bruestung-/Hoehe-/XY-Aenderung), nutzen wir
|
||
# `doc.Objects.Replace` auf die existierenden IDs statt Delete+AddBrep.
|
||
# Damit kollidiert ein laufender `_Move`-Command nicht mehr mit dem
|
||
# Wand-Regen → kein „Unable to transform"-Fehler mehr. Bei Anzahl-
|
||
# Aenderung (z.B. Fluegel-Wechsel) Fallback auf Delete+Add.
|
||
op_layer = _ensure_layer(doc, _layer_path_volume(doc, geschoss_name))
|
||
for op_meta, pt_loc, op_uk in opening_jobs:
|
||
old_objs = list(_find_objects_by_wall_id(doc, op_meta["id"],
|
||
"oeffnung_volume"))
|
||
pieces = _make_oeffnung_pieces(geom, pt_loc, meta["dicke"],
|
||
op_meta, op_uk)
|
||
if len(old_objs) == len(pieces) and len(pieces) > 0:
|
||
for (old_obj, _old_meta), pbrep in zip(old_objs, pieces):
|
||
try: doc.Objects.Replace(old_obj.Id, pbrep)
|
||
except Exception as ex:
|
||
print("[ELEMENTE] replace oeff vol:", ex)
|
||
continue
|
||
# Fallback: Anzahl hat sich geaendert → alte loeschen + neue adden.
|
||
for o, _m in old_objs:
|
||
try: doc.Objects.Delete(o.Id, True)
|
||
except Exception as ex: print("[ELEMENTE] del old oeff vol:", ex)
|
||
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)
|
||
|
||
# Source-Layer migrieren + Volumen-Layer-Index ermitteln
|
||
layer = _ensure_layer(doc, _layer_path_volume(doc, geschoss_name))
|
||
src_layer = _ensure_layer(doc, _layer_path_axis(doc, geschoss_name))
|
||
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)
|
||
|
||
# Alle alten wand_volume-Objekte loeschen, neue (1..N) hinzufuegen.
|
||
for o, _m in _find_objects_by_wall_id(doc, element_id, "wand_volume"):
|
||
try: doc.Objects.Delete(o.Id, True)
|
||
except Exception: pass
|
||
import json as _json
|
||
layers_json = (_json.dumps(layers_def, ensure_ascii=False)
|
||
if is_layered else "")
|
||
# Per-Schicht Material-Lookup: wenn ein Material-Name aus der
|
||
# Library gesetzt ist, nimm dessen Farbe/Hatch + leg den Brep auf
|
||
# die Material-Sub-Ebene (Section-Hatch greift dann bei Clipping
|
||
# Planes). Sonst Fallback auf inline color + Standard-Wand-Layer.
|
||
for idx, (lbrep, color, lname) in enumerate(layer_breps):
|
||
if lbrep is None: continue
|
||
# Material-Name aus dem layers_def-Eintrag (gleicher Index)
|
||
lay_def = layers_def[idx] if idx < len(layers_def) else {}
|
||
mat_name = lay_def.get("material", "") if is_layered else ""
|
||
effective_color = color
|
||
target_layer = layer
|
||
if mat_name and mat_name in _MATERIAL_LIBRARY:
|
||
effective_color = _MATERIAL_LIBRARY[mat_name]["color"]
|
||
target_layer = _ensure_material_sublayer(doc, geschoss_name,
|
||
mat_name)
|
||
if target_layer < 0: target_layer = layer
|
||
|
||
attrs = Rhino.DocObjects.ObjectAttributes()
|
||
attrs.LayerIndex = target_layer
|
||
# Edges immer SCHWARZ — entkoppelt von Sublayer-Farbe und
|
||
# Material-Diffuse. Sonst werden die Outlines material-faerbig.
|
||
try:
|
||
import System.Drawing as SD
|
||
attrs.ColorSource = (
|
||
Rhino.DocObjects.ObjectColorSource.ColorFromObject)
|
||
attrs.ObjectColor = SD.Color.FromArgb(255, 0, 0, 0)
|
||
except Exception: pass
|
||
# Faces via Material (DiffuseColor) — getrennt vom ObjectColor.
|
||
if effective_color:
|
||
mat_idx = _ensure_material(doc, effective_color)
|
||
if mat_idx >= 0:
|
||
attrs.MaterialIndex = mat_idx
|
||
attrs.MaterialSource = (
|
||
Rhino.DocObjects.ObjectMaterialSource.MaterialFromObject)
|
||
_attach_meta(attrs, element_id, "wand_volume",
|
||
meta["geschoss"], meta["dicke"],
|
||
meta["uk_override"], meta["ok_override"],
|
||
meta.get("referenz", "mid"),
|
||
wand_layered=is_layered,
|
||
wand_layers=layers_json,
|
||
wand_layer_idx=idx)
|
||
try: doc.Objects.AddBrep(lbrep, attrs)
|
||
except Exception as ex: print("[ELEMENTE] AddBrep wand layer:", ex)
|
||
return True
|
||
elif meta["type"] == "decke_outline":
|
||
uk, ok = _resolve_decke_z(doc, meta["geschoss"], meta["dicke"],
|
||
meta["uk_override"], meta["ok_override"])
|
||
# Aussparungs-Outline-Curves einsammeln und direkt an
|
||
# _make_decke_volume durchreichen — der baut den Slab mit den
|
||
# Loechern via planar Brep + Extrusion (kein BoolDiff).
|
||
aps = _find_aussparungen_for_decke(doc, element_id)
|
||
hole_curves = []
|
||
for ap_obj, ap_meta in aps:
|
||
ap_geom = ap_obj.Geometry
|
||
if isinstance(ap_geom, rg.Curve) and ap_geom.IsClosed:
|
||
hole_curves.append(ap_geom)
|
||
else:
|
||
print("[ELEMENTE] Aussparung", ap_meta["id"],
|
||
"Source ist keine geschlossene Curve — uebersprungen")
|
||
brep = _make_decke_volume(geom, meta["dicke"], uk, ok, hole_curves)
|
||
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
|
||
elif meta["type"] == "stuetze_point":
|
||
# Punkt-Geometrie: aus dem geom (Point oder Point3d) Location holen
|
||
if isinstance(geom, rg.Point):
|
||
pt = geom.Location
|
||
else:
|
||
pt = geom # Point3d
|
||
g_start = _geschoss_by_id(doc, meta["geschoss"])
|
||
uk = float(g_start.get("okff", 0.0)) if g_start else 0.0
|
||
z_over = meta.get("trag_z_over", "")
|
||
if z_over:
|
||
try: ok = uk + float(z_over)
|
||
except Exception:
|
||
ok = uk + float(g_start.get("hoehe", 3.0)) if g_start else uk + 3.0
|
||
else:
|
||
ok = uk + float(g_start.get("hoehe", 3.0)) if g_start else uk + 3.0
|
||
brep = _make_stuetze_volume(pt,
|
||
meta.get("trag_profil", "quadrat"),
|
||
meta.get("trag_b", 0.25),
|
||
meta.get("trag_h", 0.25),
|
||
meta.get("trag_d", 0.25),
|
||
meta.get("trag_t", 0.01),
|
||
meta.get("trag_angle", 0.0),
|
||
uk, ok)
|
||
vol_type = "stuetze_volume"
|
||
layer = _ensure_layer(doc, _layer_path_tragwerk(doc, geschoss_name))
|
||
src_layer = layer
|
||
elif meta["type"] == "traeger_axis":
|
||
# Achse + z_top aus Geschoss + Override
|
||
g_start = _geschoss_by_id(doc, meta["geschoss"])
|
||
uk = float(g_start.get("okff", 0.0)) if g_start else 0.0
|
||
h = float(g_start.get("hoehe", 3.0)) if g_start else 3.0
|
||
z_over = meta.get("trag_z_over", "")
|
||
kind = meta.get("trag_kind", "traeger")
|
||
if z_over:
|
||
try: z_top = uk + float(z_over)
|
||
except Exception:
|
||
z_top = uk + h # an OK Geschoss
|
||
else:
|
||
# Unterzug haengt unter Deckenoberkante (uk + h),
|
||
# Traeger sitzt mit Oberkante an OK Geschoss
|
||
z_top = uk + h
|
||
brep = _make_traeger_volume(geom,
|
||
meta.get("trag_profil", "rechteck"),
|
||
meta.get("trag_b", 0.20),
|
||
meta.get("trag_h", 0.40),
|
||
meta.get("trag_d", 0.25),
|
||
meta.get("trag_t", 0.01),
|
||
meta.get("trag_angle", 0.0),
|
||
z_top)
|
||
vol_type = "traeger_volume"
|
||
layer = _ensure_layer(doc, _layer_path_tragwerk(doc, geschoss_name))
|
||
src_layer = layer
|
||
elif meta["type"] == "raum_outline":
|
||
# Raum: Source = geschlossene Outline; Volumes = TextEntity (Stempel)
|
||
# + optional Brep-Fuellung (SIA-Modus). Geht NICHT durch den
|
||
# Brep-Replace-Pfad — wird direkt hier emittiert.
|
||
layer = _ensure_layer(doc, _layer_path_raum(doc, geschoss_name))
|
||
src_layer = layer
|
||
# Source-Outline ggf. auf richtigen Layer migrieren
|
||
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: pass
|
||
area, perim, ctr = _raum_amp(geom)
|
||
# Z-Lage: auf Geschoss-OKFF
|
||
g_start = _geschoss_by_id(doc, meta["geschoss"])
|
||
z_uk = float(g_start.get("okff", 0.0)) if g_start else 0.0
|
||
|
||
# Alte Stempel + Fills loeschen
|
||
for o, _m in _find_objects_by_wall_id(doc, element_id, "raum_stamp"):
|
||
try: doc.Objects.Delete(o.Id, True)
|
||
except Exception: pass
|
||
for o, _m in _find_objects_by_wall_id(doc, element_id, "raum_fill"):
|
||
try: doc.Objects.Delete(o.Id, True)
|
||
except Exception: pass
|
||
# Defensiv: auch die Hatch loeschen die via curve.ebenen_fill_hatch_id
|
||
# verlinkt ist — falls die Metadaten-UserStrings aus irgendeinem Grund
|
||
# verloren gingen (z.B. nach Override-Replace), wuerde der Loop oben
|
||
# sie nicht finden und es bliebe eine Geister-Hatch zurueck.
|
||
try:
|
||
hid_s = src_obj.Attributes.GetUserString("ebenen_fill_hatch_id")
|
||
if hid_s:
|
||
old_h = doc.Objects.FindId(System.Guid(hid_s))
|
||
if old_h is not None and not old_h.IsDeleted:
|
||
doc.Objects.Delete(old_h.Id, True)
|
||
except Exception: pass
|
||
|
||
# Fuell-Hatch: Pattern aus raum_fuellung (string). Sonderfaelle:
|
||
# "" = kein Hatch (User will leeren Raum)
|
||
# "ByLayer" = Hatch mit ColorSource=ByLayer, Pattern=Solid
|
||
# sonst = Pattern-Name (Solid, Hatch1, Beton, …)
|
||
# SIA-Modus: wenn aktiv UND Raum klassifiziert, IMMER Solid erzwingen
|
||
# damit der Override eine Flaeche zum Einfaerben hat.
|
||
fuell_raw = meta.get("raum_fuellung", "")
|
||
sia_code = meta.get("raum_sia") or ""
|
||
force_solid = (_sia_fill_enabled(doc) and sia_code in _SIA_COLORS_HEX)
|
||
by_layer = (fuell_raw == "ByLayer")
|
||
pattern_for_hatch = ""
|
||
if force_solid:
|
||
pattern_for_hatch = "Solid"
|
||
elif fuell_raw:
|
||
pattern_for_hatch = "Solid" if by_layer else fuell_raw
|
||
new_hatch_id = None
|
||
hatch_geom = (_make_raum_hatch(geom, z_uk, doc, pattern_for_hatch)
|
||
if pattern_for_hatch else None)
|
||
if hatch_geom is not None:
|
||
try:
|
||
import System.Drawing as SD
|
||
h_attrs = Rhino.DocObjects.ObjectAttributes()
|
||
h_attrs.LayerIndex = layer
|
||
# Default-Farbe = sehr helles Grau (wirkt fast transparent)
|
||
default_col = SD.Color.FromArgb(255, 245, 245, 245)
|
||
if by_layer and not force_solid:
|
||
# Hatch folgt Layerfarbe (Standard-Rhino-Verhalten)
|
||
h_attrs.ColorSource = (
|
||
Rhino.DocObjects.ObjectColorSource.ColorFromLayer)
|
||
elif force_solid:
|
||
# SIA-Modus aktiv + Raum klassifiziert → DIREKT mit SIA-
|
||
# Farbe erstellen. Zusaetzlich Override-Backup-UserStrings
|
||
# setzen, damit restore_all beim Toggle-Off auf Default
|
||
# zurueckkommt.
|
||
hex_str = _SIA_COLORS_HEX[sia_code]
|
||
h = hex_str.lstrip("#")
|
||
r = int(h[0:2], 16); g = int(h[2:4], 16); bl = int(h[4:6], 16)
|
||
h_attrs.ColorSource = (
|
||
Rhino.DocObjects.ObjectColorSource.ColorFromObject)
|
||
h_attrs.ObjectColor = SD.Color.FromArgb(255, r, g, bl)
|
||
# Override-Backup setzen: bei Restore zurueck auf
|
||
# ColorFromObject + #f5f5f5 (Standard-Rhino-Verhalten).
|
||
h_attrs.SetUserString("dossier_or_hatch_csrc", "1")
|
||
h_attrs.SetUserString("dossier_or_hatch_color", "#f5f5f5")
|
||
h_attrs.SetUserString("dossier_or_hatch_color_done", "1")
|
||
else:
|
||
# User hat Pattern gewaehlt (z.B. "Solid"), kein SIA →
|
||
# heller Default damit's dezent wirkt.
|
||
h_attrs.ColorSource = (
|
||
Rhino.DocObjects.ObjectColorSource.ColorFromObject)
|
||
h_attrs.ObjectColor = default_col
|
||
# WICHTIG: keine raum_* UserStrings auf die Hatch setzen.
|
||
# Sonst matcht der Override-Engine die Hatch DIREKT zusaetzlich
|
||
# zum Curve-Pfad → doppelte Backup-Strings, korruptes Restore.
|
||
_attach_meta(h_attrs, element_id, "raum_fill", meta["geschoss"],
|
||
0.0, "", "", "mid")
|
||
new_hatch_id = doc.Objects.AddHatch(hatch_geom, h_attrs)
|
||
# Backup-UserStrings im SIA-Fall nochmal POST-AddHatch
|
||
# setzen — manche IronPython3/Rhino-Versionen verlieren
|
||
# einzelne UserStrings ueber den AddHatch-Roundtrip.
|
||
if (force_solid and new_hatch_id is not None
|
||
and new_hatch_id != System.Guid.Empty):
|
||
new_h = doc.Objects.FindId(new_hatch_id)
|
||
if new_h is not None:
|
||
fresh = new_h.Attributes.Duplicate()
|
||
fresh.SetUserString("dossier_or_hatch_csrc", "1")
|
||
fresh.SetUserString("dossier_or_hatch_color",
|
||
"#f5f5f5")
|
||
fresh.SetUserString("dossier_or_hatch_color_done",
|
||
"1")
|
||
doc.Objects.ModifyAttributes(new_h, fresh, True)
|
||
except Exception as ex:
|
||
print("[ELEMENTE] Raum AddHatch:", ex)
|
||
|
||
# Hatch via Gestaltung-Konvention an die Curve linken (UserString
|
||
# `ebenen_fill_hatch_id` auf der Source) — damit das Override-
|
||
# System ihn ueber hatchPattern/-Color-Actions modifizieren kann.
|
||
if new_hatch_id is not None and new_hatch_id != System.Guid.Empty:
|
||
try:
|
||
src_attrs = src_obj.Attributes.Duplicate()
|
||
src_attrs.SetUserString("ebenen_fill_hatch_id",
|
||
str(new_hatch_id))
|
||
doc.Objects.ModifyAttributes(src_obj, src_attrs, True)
|
||
except Exception as ex:
|
||
print("[ELEMENTE] Raum link hatch:", ex)
|
||
# Override-Engine zusaetzlich anstossen (Safety-Net + Pattern-
|
||
# Override). Der ModifyAttributes-Aufruf oben loest KEIN
|
||
# ReplaceRhinoObject-Event aus, also wuerde der Override-Listener
|
||
# die neue Hatch sonst nicht erreichen. Bei force_solid haben wir
|
||
# die Farbe schon direkt gesetzt — das hier wirkt idempotent.
|
||
try:
|
||
import overrides as _ov
|
||
cfg = _ov.load_config(doc)
|
||
if cfg.get("enabled") and cfg.get("rules"):
|
||
cur = doc.Objects.FindId(src_obj.Id)
|
||
if cur is not None:
|
||
was = sc.sticky.get("overrides_applying", False)
|
||
sc.sticky["overrides_applying"] = True
|
||
try: _ov._apply_to_single_object(doc, cur)
|
||
finally: sc.sticky["overrides_applying"] = was
|
||
except Exception as ex:
|
||
print("[ELEMENTE] override re-apply:", ex)
|
||
|
||
# Stempel
|
||
te = _make_raum_stamp_text(
|
||
ctr,
|
||
meta.get("raum_name", "Raum"),
|
||
meta.get("raum_nummer", ""),
|
||
meta.get("raum_funktion", ""),
|
||
area,
|
||
meta.get("raum_rundung", "0.1"),
|
||
meta.get("raum_txt_h", 0.20),
|
||
z=z_uk,
|
||
align=meta.get("raum_align", "mid"))
|
||
if te is None:
|
||
return True # Outline evtl. offen — Source behalten
|
||
attrs = Rhino.DocObjects.ObjectAttributes()
|
||
attrs.LayerIndex = layer
|
||
_attach_meta(attrs, element_id, "raum_stamp", meta["geschoss"],
|
||
0.0, "", "", "mid",
|
||
raum_name=meta.get("raum_name"),
|
||
raum_nummer=meta.get("raum_nummer"),
|
||
raum_funktion=meta.get("raum_funktion"),
|
||
raum_rundung=meta.get("raum_rundung"),
|
||
raum_txt_h=meta.get("raum_txt_h"),
|
||
raum_align=meta.get("raum_align"),
|
||
raum_sia=meta.get("raum_sia"),
|
||
raum_fuellung=meta.get("raum_fuellung"))
|
||
try: doc.Objects.AddText(te, attrs)
|
||
except Exception as ex: print("[ELEMENTE] Raum AddText:", ex)
|
||
return True
|
||
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"),
|
||
trag_kind=meta.get("trag_kind"),
|
||
trag_profil=meta.get("trag_profil"),
|
||
trag_b=meta.get("trag_b"),
|
||
trag_h=meta.get("trag_h"),
|
||
trag_d=meta.get("trag_d"),
|
||
trag_t=meta.get("trag_t"),
|
||
trag_angle=meta.get("trag_angle"),
|
||
trag_z_over=meta.get("trag_z_over"),
|
||
raum_name=meta.get("raum_name"),
|
||
raum_nummer=meta.get("raum_nummer"),
|
||
raum_funktion=meta.get("raum_funktion"),
|
||
raum_rundung=meta.get("raum_rundung"),
|
||
raum_txt_h=meta.get("raum_txt_h"),
|
||
raum_align=meta.get("raum_align"),
|
||
raum_sia=meta.get("raum_sia"),
|
||
raum_fuellung=meta.get("raum_fuellung"))
|
||
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 = ()
|
||
# SIA-Praeset einmalig in den cross-doc Presets-Store legen, damit es
|
||
# im Overrides-Panel direkt sichtbar ist (force=False ⇒ User-Anpassungen
|
||
# bleiben erhalten).
|
||
try: _ensure_sia_preset(force=False)
|
||
except Exception: pass
|
||
|
||
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_AUSSPARUNG": self._cmd_create_aussparung(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 == "CREATE_STUETZE": self._cmd_create_stuetze(p)
|
||
elif t == "CREATE_TRAEGER": self._cmd_create_traeger(p)
|
||
elif t == "CREATE_RAUM": self._cmd_create_raum(p)
|
||
elif t == "EXPORT_RAEUME": self._cmd_export_raeume(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,
|
||
"layered": bool(meta.get("wand_layered", False)),
|
||
"layers": meta.get("wand_layers", []),
|
||
})
|
||
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"] == "decke_aussparung_outline":
|
||
try:
|
||
area, perim, _ctr = _raum_amp(obj.Geometry)
|
||
except Exception:
|
||
area, perim = 0.0, 0.0
|
||
base.update({
|
||
"kind": "aussparung",
|
||
"parentId": meta.get("aussp_parent", ""),
|
||
"area": area,
|
||
"umfang": perim,
|
||
})
|
||
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"),
|
||
})
|
||
elif meta["type"] == "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),
|
||
})
|
||
elif meta["type"] == "raum_outline":
|
||
# Raum: Flaeche + Umfang aus der Outline-Curve
|
||
try:
|
||
g_obj = obj.Geometry
|
||
area, perim, _ctr = _raum_amp(g_obj)
|
||
except Exception:
|
||
area, perim = 0.0, 0.0
|
||
rnd = meta.get("raum_rundung", "0.1")
|
||
base.update({
|
||
"kind": "raum",
|
||
"name": meta.get("raum_name", "Raum"),
|
||
"nummer": meta.get("raum_nummer", ""),
|
||
"funktion": meta.get("raum_funktion", ""),
|
||
"rundung": rnd,
|
||
"txtH": meta.get("raum_txt_h", 0.20),
|
||
"align": meta.get("raum_align", "mid"),
|
||
"sia": meta.get("raum_sia", ""),
|
||
"fuellung": bool(meta.get("raum_fuellung", False)),
|
||
"area": area,
|
||
"areaFmt": _format_area(area, rnd),
|
||
"umfang": perim,
|
||
})
|
||
elif meta["type"] in ("stuetze_point", "traeger_axis"):
|
||
# Tragwerk: Stuetze (Punkt) oder Traeger/Unterzug (Achse)
|
||
gs = _geschoss_by_id(doc, meta["geschoss"])
|
||
try: uk = float(gs.get("okff", 0.0)) if gs else 0.0
|
||
except Exception: uk = 0.0
|
||
try: h_g = float(gs.get("hoehe", 3.0)) if gs else 3.0
|
||
except Exception: h_g = 3.0
|
||
zov = meta.get("trag_z_over", "")
|
||
if zov:
|
||
try: ok_val = uk + float(zov)
|
||
except Exception: ok_val = uk + h_g
|
||
else:
|
||
ok_val = uk + h_g
|
||
kind_str = meta.get("trag_kind", "")
|
||
# Fallback: ohne Kind aus type ableiten
|
||
if not kind_str:
|
||
kind_str = ("stuetze" if meta["type"] == "stuetze_point"
|
||
else "traeger")
|
||
axis_len = 0.0
|
||
if meta["type"] == "traeger_axis":
|
||
try: axis_len = float(obj.Geometry.GetLength())
|
||
except Exception: axis_len = 0.0
|
||
base.update({
|
||
"kind": kind_str, # "stuetze" | "traeger" | "unterzug"
|
||
"profil": meta.get("trag_profil", "quadrat"),
|
||
"b": meta.get("trag_b", 0.25),
|
||
"h": meta.get("trag_h", 0.25),
|
||
"d": meta.get("trag_d", 0.25),
|
||
"t": meta.get("trag_t", 0.01),
|
||
"angle": meta.get("trag_angle", 0.0),
|
||
"zOver": meta.get("trag_z_over", ""),
|
||
"uk": uk,
|
||
"ok": ok_val,
|
||
"axisLen": axis_len,
|
||
})
|
||
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),
|
||
"siaFillMode": _sia_fill_enabled(doc),
|
||
"hatchPatterns": _list_hatch_patterns(doc),
|
||
"materials": [
|
||
{"name": n, "color": m["color"],
|
||
"hatch": m.get("hatch", ""), "scale": m.get("scale", 1.0)}
|
||
for n, m in _MATERIAL_LIBRARY.items()],
|
||
})
|
||
|
||
# --- 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. Performance-Wrap:
|
||
- BeginUndoRecord → eine Undo-Aktion fuer die ganze Wand-Erstellung
|
||
(statt mehreren kleinen fuer AddCurve + jedes Layer-Brep)
|
||
- Views.RedrawEnabled=False waehrend der Regen-Phase →
|
||
ein einziger Redraw am Ende statt einer pro Add/Delete-Op"""
|
||
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)
|
||
|
||
undo_serial = doc.BeginUndoRecord("Wand erstellen")
|
||
prev_redraw = doc.Views.RedrawEnabled
|
||
doc.Views.RedrawEnabled = False
|
||
try:
|
||
with _TimedBlock("Wand AddCurve + Regen"):
|
||
if doc.Objects.AddCurve(axis, attrs) == System.Guid.Empty:
|
||
print("[ELEMENTE] Wand AddCurve fehlgeschlagen"); return
|
||
# Joint-Cache invalidieren — neue Wand-Achse ist im Doc.
|
||
_invalidate_joints_cache(geschoss_id)
|
||
_regenerate_element(doc, wall_id)
|
||
# Eckverbindungen/T-Stoesse: abhaengige Nachbarn regen.
|
||
try:
|
||
deps = _find_dependent_walls(doc, geschoss_id, wall_id,
|
||
None, axis)
|
||
for wid in deps:
|
||
try: _regenerate_element(doc, wid)
|
||
except Exception as ex:
|
||
print("[ELEMENTE] dep regen:", ex)
|
||
except Exception as ex:
|
||
print("[ELEMENTE] deps:", ex)
|
||
finally:
|
||
doc.Views.RedrawEnabled = prev_redraw
|
||
try: doc.EndUndoRecord(undo_serial)
|
||
except Exception: pass
|
||
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_aussparung(self, p):
|
||
"""Decken-Aussparung: geschlossene Outline auf einer Decke. Outline
|
||
wird als Cutout aus der Decke abgezogen — fuer Treppenloecher,
|
||
Atrien, Schaechte. Eltern-Decke wird automatisch detektiert
|
||
ueber den Centroid der gezeichneten Outline."""
|
||
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
|
||
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("aussp_modus", "Rechteck")
|
||
if modus not in modi: modus = "Rechteck"
|
||
|
||
def _prompt(base):
|
||
return "{} [Modus={}]".format(base, modus)
|
||
|
||
first_pt = None
|
||
try:
|
||
while True:
|
||
gp = ric.GetPoint()
|
||
gp.SetCommandPrompt(_prompt("Aussparung: Startpunkt"))
|
||
opt_modus = gp.AddOptionList("Modus", modi, modi.index(modus))
|
||
res = gp.Get()
|
||
if res == GetResult.Option:
|
||
if gp.OptionIndex() == opt_modus:
|
||
try: modus = modi[gp.Option().CurrentListOptionIndex]
|
||
except Exception: pass
|
||
continue
|
||
if res != GetResult.Point: return
|
||
first_pt = gp.Point()
|
||
break
|
||
except Exception as ex:
|
||
print("[ELEMENTE] aussp first-pt:", 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] aussp collect:", ex); return
|
||
|
||
if outline_curve is None or not outline_curve.IsClosed:
|
||
print("[ELEMENTE] keine gueltige Outline"); return
|
||
|
||
# Outline auf Z=0 normalisieren
|
||
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
|
||
|
||
# Eltern-Decke via Centroid + Point-in-Curve-Test finden.
|
||
# Aktives Geschoss wird bevorzugt, faellt aber sonst auf jede
|
||
# andere Decke zurueck (User koennte z.B. in 1OG ein Loch in der
|
||
# EG-Decke setzen wollen).
|
||
try:
|
||
amp = rg.AreaMassProperties.Compute(outline)
|
||
ctr = amp.Centroid if amp is not None else outline.PointAtStart
|
||
except Exception:
|
||
ctr = outline.PointAtStart
|
||
ctr_xy = rg.Point3d(ctr.X, ctr.Y, 0)
|
||
decke_id = _find_decke_containing_point(doc, geschoss, ctr_xy)
|
||
if decke_id is None:
|
||
# Diagnose: ueberhaupt eine Decke im Doc?
|
||
n_decken = 0
|
||
for obj in doc.Objects:
|
||
m = _read_meta(obj)
|
||
if m and m["type"] == "decke_outline": n_decken += 1
|
||
if n_decken == 0:
|
||
print("[ELEMENTE] Aussparung: keine Decke im Dokument — "
|
||
"erst eine Decke zeichnen.")
|
||
else:
|
||
print(("[ELEMENTE] Aussparung: keine der {} Decken enthaelt "
|
||
"den Centroid ({:.3f}, {:.3f}). Outline muss "
|
||
"geometrisch INNERHALB einer Decken-Outline "
|
||
"liegen.").format(n_decken, ctr_xy.X, ctr_xy.Y))
|
||
return
|
||
|
||
# Element anlegen
|
||
element_id = "aussp_" + uuid.uuid4().hex[:10]
|
||
g = _geschoss_by_id(doc, geschoss)
|
||
geschoss_name = g.get("name", "EG") if g else "EG"
|
||
layer = _ensure_layer(doc, _layer_path_decke(doc, geschoss_name))
|
||
attrs = Rhino.DocObjects.ObjectAttributes()
|
||
attrs.LayerIndex = layer
|
||
_attach_meta(attrs, element_id, "decke_aussparung_outline", geschoss,
|
||
0.0, "", "", "mid", aussp_parent=decke_id)
|
||
new_id = doc.Objects.AddCurve(outline, attrs)
|
||
if new_id == System.Guid.Empty:
|
||
print("[ELEMENTE] Aussparung AddCurve fehlgeschlagen"); return
|
||
_save_last(aussp_modus=modus)
|
||
# Eltern-Decke regenerieren — das Loch wird abgezogen
|
||
_regenerate_element(doc, decke_id)
|
||
doc.Views.Redraw()
|
||
print("[ELEMENTE] Aussparung erzeugt: {}".format(element_id))
|
||
self._send_state()
|
||
|
||
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 _cmd_create_stuetze(self, p):
|
||
"""Stuetzen-Erzeugung: 1 Klick (Punkt) + Optionen. Erzeugt ein
|
||
Source-Point + vertikales Tragwerk-Volume zwischen Geschoss-UK
|
||
und Geschoss-OK (oder Z-Override).
|
||
Profile: quadrat, rechteck, rund, i_profil, rohr."""
|
||
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
|
||
try:
|
||
import Rhino.Input.Custom as ric
|
||
from Rhino.Input import GetResult
|
||
except Exception as ex:
|
||
print("[ELEMENTE] Imports:", ex); return
|
||
|
||
profil = p.get("profil") or _last("stuetze_profil", "quadrat")
|
||
if profil not in _TRAG_PROFILE: profil = "quadrat"
|
||
try: B = float(p.get("b") or _last("stuetze_b", 0.30))
|
||
except Exception: B = 0.30
|
||
try: H = float(p.get("h") or _last("stuetze_h", 0.30))
|
||
except Exception: H = 0.30
|
||
try: D = float(p.get("d") or _last("stuetze_d", 0.25))
|
||
except Exception: D = 0.25
|
||
try: t_wall = float(p.get("t") or _last("stuetze_t", 0.01))
|
||
except Exception: t_wall = 0.01
|
||
try: angle = float(p.get("angle") or _last("stuetze_angle", 0.0))
|
||
except Exception: angle = 0.0
|
||
z_over = p.get("zOver", "")
|
||
|
||
prof_labels = ["Quadrat", "Rechteck", "Rund", "I-Profil", "Rohr"]
|
||
|
||
def _prompt(base):
|
||
return "{} [Profil={}, B={:.2f}{}{}, Drehung={:.0f}°]".format(
|
||
base,
|
||
prof_labels[_TRAG_PROFILE.index(profil)],
|
||
B,
|
||
", H={:.2f}".format(H) if profil in ("rechteck", "i_profil") else "",
|
||
", D={:.2f}".format(D) if profil in ("rund", "rohr") else "",
|
||
angle)
|
||
|
||
pt = None
|
||
try:
|
||
while True:
|
||
gp = ric.GetPoint()
|
||
gp.SetCommandPrompt(_prompt("Stuetze: Position"))
|
||
try:
|
||
gp.DynamicDraw += _make_stuetze_preview(
|
||
profil, B, H, D, t_wall, angle)
|
||
except Exception: pass
|
||
opt_pr = gp.AddOptionList("Profil", prof_labels,
|
||
_TRAG_PROFILE.index(profil))
|
||
opt_b = gp.AddOption("Breite")
|
||
opt_h = -1; opt_d = -1; opt_t = -1
|
||
if profil in ("rechteck", "i_profil"):
|
||
opt_h = gp.AddOption("Hoehe")
|
||
if profil in ("rund", "rohr"):
|
||
opt_d = gp.AddOption("Durchmesser")
|
||
if profil in ("i_profil", "rohr"):
|
||
opt_t = gp.AddOption("Wanddicke")
|
||
opt_a = gp.AddOption("Drehung")
|
||
res = gp.Get()
|
||
if res == GetResult.Option:
|
||
idx = gp.OptionIndex()
|
||
if idx == opt_pr:
|
||
try: profil = _TRAG_PROFILE[gp.Option().CurrentListOptionIndex]
|
||
except Exception: pass
|
||
elif idx == opt_b:
|
||
gn = ric.GetNumber(); gn.SetCommandPrompt("Breite (m)")
|
||
gn.SetDefaultNumber(B); gn.SetLowerLimit(0.02, False)
|
||
if gn.Get() == GetResult.Number: B = float(gn.Number())
|
||
elif opt_h >= 0 and idx == opt_h:
|
||
gn = ric.GetNumber(); gn.SetCommandPrompt("Hoehe (m)")
|
||
gn.SetDefaultNumber(H); gn.SetLowerLimit(0.02, False)
|
||
if gn.Get() == GetResult.Number: H = float(gn.Number())
|
||
elif opt_d >= 0 and idx == opt_d:
|
||
gn = ric.GetNumber(); gn.SetCommandPrompt("Durchmesser (m)")
|
||
gn.SetDefaultNumber(D); gn.SetLowerLimit(0.02, False)
|
||
if gn.Get() == GetResult.Number: D = float(gn.Number())
|
||
elif opt_t >= 0 and idx == opt_t:
|
||
gn = ric.GetNumber(); gn.SetCommandPrompt("Wanddicke (m)")
|
||
gn.SetDefaultNumber(t_wall); gn.SetLowerLimit(0.002, False)
|
||
if gn.Get() == GetResult.Number: t_wall = float(gn.Number())
|
||
elif idx == opt_a:
|
||
gn = ric.GetNumber(); gn.SetCommandPrompt("Drehung (Grad)")
|
||
gn.SetDefaultNumber(angle)
|
||
if gn.Get() == GetResult.Number: angle = float(gn.Number())
|
||
continue
|
||
if res != GetResult.Point: return
|
||
pt = gp.Point()
|
||
break
|
||
except Exception as ex:
|
||
print("[ELEMENTE] stuetze point:", ex); return
|
||
|
||
# Source-Point auf Z=0 setzen
|
||
pt3 = rg.Point3d(pt.X, pt.Y, 0)
|
||
element_id = "trag_" + uuid.uuid4().hex[:10]
|
||
g = _geschoss_by_id(doc, geschoss)
|
||
geschoss_name = g.get("name", "EG") if g else "EG"
|
||
layer = _ensure_layer(doc, _layer_path_tragwerk(doc, geschoss_name))
|
||
attrs = Rhino.DocObjects.ObjectAttributes()
|
||
attrs.LayerIndex = layer
|
||
_attach_meta(attrs, element_id, "stuetze_point", geschoss,
|
||
0.0, "", "", "mid",
|
||
trag_kind="stuetze", trag_profil=profil,
|
||
trag_b=B, trag_h=H, trag_d=D, trag_t=t_wall,
|
||
trag_angle=angle, trag_z_over=z_over)
|
||
new_id = doc.Objects.AddPoint(pt3, attrs)
|
||
if new_id == System.Guid.Empty:
|
||
print("[ELEMENTE] Stuetze AddPoint fehlgeschlagen"); return
|
||
_save_last(stuetze_profil=profil, stuetze_b=B, stuetze_h=H,
|
||
stuetze_d=D, stuetze_t=t_wall, stuetze_angle=angle)
|
||
_regenerate_element(doc, element_id)
|
||
doc.Views.Redraw()
|
||
print("[ELEMENTE] Stuetze erzeugt: {}".format(element_id))
|
||
self._send_state()
|
||
|
||
def _cmd_create_traeger(self, p):
|
||
"""Traeger-Erzeugung: 2 Klicks (Achse) + Optionen.
|
||
Profile: quadrat, rechteck, rund, i_profil, rohr."""
|
||
doc = Rhino.RhinoDoc.ActiveDoc
|
||
if doc is None: return
|
||
kind = "traeger"
|
||
geschoss = p.get("geschoss") or _active_geschoss_id(doc)
|
||
if not geschoss:
|
||
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
|
||
|
||
profil = p.get("profil") or _last("traeger_profil", "rechteck")
|
||
if profil not in _TRAG_PROFILE: profil = "rechteck"
|
||
try: B = float(p.get("b") or _last("traeger_b", 0.20))
|
||
except Exception: B = 0.20
|
||
try: H = float(p.get("h") or _last("traeger_h", 0.30))
|
||
except Exception: H = 0.30
|
||
try: D = float(p.get("d") or _last("traeger_d", 0.25))
|
||
except Exception: D = 0.25
|
||
try: t_wall = float(p.get("t") or _last("traeger_t", 0.01))
|
||
except Exception: t_wall = 0.01
|
||
try: angle = float(p.get("angle") or _last("traeger_angle", 0.0))
|
||
except Exception: angle = 0.0
|
||
z_over = p.get("zOver", "")
|
||
|
||
prof_labels = ["Quadrat", "Rechteck", "Rund", "I-Profil", "Rohr"]
|
||
|
||
def _prompt(base):
|
||
return "Traeger: {} [Profil={}, B={:.2f}{}{}]".format(
|
||
base,
|
||
prof_labels[_TRAG_PROFILE.index(profil)],
|
||
B,
|
||
", H={:.2f}".format(H) if profil in ("rechteck", "i_profil") else "",
|
||
", D={:.2f}".format(D) if profil in ("rund", "rohr") else "")
|
||
|
||
first_pt = None
|
||
try:
|
||
while True:
|
||
gp = ric.GetPoint()
|
||
gp.SetCommandPrompt(_prompt("Anfangspunkt"))
|
||
try:
|
||
gp.DynamicDraw += _make_stuetze_preview(
|
||
profil, B, H, D, t_wall, angle)
|
||
except Exception: pass
|
||
opt_pr = gp.AddOptionList("Profil", prof_labels,
|
||
_TRAG_PROFILE.index(profil))
|
||
opt_b = gp.AddOption("Breite")
|
||
opt_h = -1; opt_d = -1; opt_t = -1
|
||
if profil in ("rechteck", "i_profil"):
|
||
opt_h = gp.AddOption("Hoehe")
|
||
if profil in ("rund", "rohr"):
|
||
opt_d = gp.AddOption("Durchmesser")
|
||
if profil in ("i_profil", "rohr"):
|
||
opt_t = gp.AddOption("Wanddicke")
|
||
opt_a = gp.AddOption("Drehung")
|
||
res = gp.Get()
|
||
if res == GetResult.Option:
|
||
idx = gp.OptionIndex()
|
||
if idx == opt_pr:
|
||
try: profil = _TRAG_PROFILE[gp.Option().CurrentListOptionIndex]
|
||
except Exception: pass
|
||
elif idx == opt_b:
|
||
gn = ric.GetNumber(); gn.SetCommandPrompt("Breite (m)")
|
||
gn.SetDefaultNumber(B); gn.SetLowerLimit(0.02, False)
|
||
if gn.Get() == GetResult.Number: B = float(gn.Number())
|
||
elif opt_h >= 0 and idx == opt_h:
|
||
gn = ric.GetNumber(); gn.SetCommandPrompt("Hoehe (m)")
|
||
gn.SetDefaultNumber(H); gn.SetLowerLimit(0.02, False)
|
||
if gn.Get() == GetResult.Number: H = float(gn.Number())
|
||
elif opt_d >= 0 and idx == opt_d:
|
||
gn = ric.GetNumber(); gn.SetCommandPrompt("Durchmesser (m)")
|
||
gn.SetDefaultNumber(D); gn.SetLowerLimit(0.02, False)
|
||
if gn.Get() == GetResult.Number: D = float(gn.Number())
|
||
elif opt_t >= 0 and idx == opt_t:
|
||
gn = ric.GetNumber(); gn.SetCommandPrompt("Wanddicke (m)")
|
||
gn.SetDefaultNumber(t_wall); gn.SetLowerLimit(0.002, False)
|
||
if gn.Get() == GetResult.Number: t_wall = float(gn.Number())
|
||
elif idx == opt_a:
|
||
gn = ric.GetNumber(); gn.SetCommandPrompt("Drehung (Grad)")
|
||
gn.SetDefaultNumber(angle)
|
||
if gn.Get() == GetResult.Number: angle = float(gn.Number())
|
||
continue
|
||
if res != GetResult.Point: return
|
||
first_pt = gp.Point()
|
||
break
|
||
except Exception as ex:
|
||
print("[ELEMENTE] traeger first-pt:", ex); return
|
||
|
||
gp2 = ric.GetPoint()
|
||
gp2.SetCommandPrompt(_prompt("Endpunkt"))
|
||
try: gp2.SetBasePoint(first_pt, True)
|
||
except Exception: pass
|
||
try:
|
||
gp2.DynamicDraw += _make_traeger_preview(
|
||
first_pt, profil, B, H, D, t_wall, angle)
|
||
except Exception: pass
|
||
res = gp2.Get()
|
||
if res != GetResult.Point: return
|
||
end_pt = gp2.Point()
|
||
|
||
p0 = rg.Point3d(first_pt.X, first_pt.Y, 0)
|
||
p1 = rg.Point3d(end_pt.X, end_pt.Y, 0)
|
||
if p0.DistanceTo(p1) < 1e-6:
|
||
print("[ELEMENTE] Achse zu kurz"); return
|
||
line = rg.LineCurve(p0, p1)
|
||
|
||
element_id = "trag_" + uuid.uuid4().hex[:10]
|
||
g = _geschoss_by_id(doc, geschoss)
|
||
geschoss_name = g.get("name", "EG") if g else "EG"
|
||
layer = _ensure_layer(doc, _layer_path_tragwerk(doc, geschoss_name))
|
||
attrs = Rhino.DocObjects.ObjectAttributes()
|
||
attrs.LayerIndex = layer
|
||
_attach_meta(attrs, element_id, "traeger_axis", geschoss,
|
||
0.0, "", "", "mid",
|
||
trag_kind=kind, trag_profil=profil,
|
||
trag_b=B, trag_h=H, trag_d=D, trag_t=t_wall,
|
||
trag_angle=angle, trag_z_over=z_over)
|
||
new_id = doc.Objects.AddCurve(line, attrs)
|
||
if new_id == System.Guid.Empty:
|
||
print("[ELEMENTE] Traeger AddCurve fehlgeschlagen"); return
|
||
_save_last(traeger_profil=profil, traeger_b=B, traeger_h=H,
|
||
traeger_d=D, traeger_t=t_wall, traeger_angle=angle)
|
||
_regenerate_element(doc, element_id)
|
||
doc.Views.Redraw()
|
||
print("[ELEMENTE] Traeger erzeugt: {}".format(element_id))
|
||
self._send_state()
|
||
|
||
def _cmd_create_raum(self, p):
|
||
"""Raum-Erzeugung: geschlossene Outline + Stempel mit Name +
|
||
Flaeche. Modi: Polylinie | Rechteck | Rechteck3Punkte | Kreis."""
|
||
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
|
||
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("raum_modus", "Polylinie")
|
||
if modus not in modi: modus = "Polylinie"
|
||
name = p.get("name") or _last("raum_name_last", "Raum")
|
||
rundung = p.get("rundung") or _last("raum_rundung", "0.1")
|
||
if rundung not in _RAUM_RUNDUNGEN: rundung = "0.1"
|
||
funktion = p.get("funktion") or _last("raum_funktion", "")
|
||
try: txt_h = float(p.get("txtH") or _last("raum_txt_h", 0.20))
|
||
except Exception: txt_h = 0.20
|
||
|
||
def _build_prompt(base):
|
||
return "{} [Modus={}, Name='{}', Rundung={}]".format(
|
||
base, modus, name, rundung)
|
||
|
||
first_pt = None
|
||
try:
|
||
while True:
|
||
gp = ric.GetPoint()
|
||
gp.SetCommandPrompt(_build_prompt("Raum: Startpunkt"))
|
||
opt_modus = gp.AddOptionList("Modus", modi, modi.index(modus))
|
||
opt_rund = gp.AddOptionList("Rundung", list(_RAUM_RUNDUNGEN),
|
||
_RAUM_RUNDUNGEN.index(rundung))
|
||
opt_name = gp.AddOption("Name")
|
||
res = gp.Get()
|
||
if res == GetResult.Option:
|
||
idx = gp.OptionIndex()
|
||
if idx == opt_modus:
|
||
try: modus = modi[gp.Option().CurrentListOptionIndex]
|
||
except Exception: pass
|
||
elif idx == opt_rund:
|
||
try: rundung = _RAUM_RUNDUNGEN[
|
||
gp.Option().CurrentListOptionIndex]
|
||
except Exception: pass
|
||
elif idx == opt_name:
|
||
try:
|
||
gs = ric.GetString()
|
||
gs.SetCommandPrompt("Raum-Name")
|
||
gs.SetDefaultString(name)
|
||
if gs.Get() == GetResult.String:
|
||
name = (gs.StringResult() or "Raum").strip()
|
||
except Exception as ex:
|
||
print("[ELEMENTE] GetString:", ex)
|
||
continue
|
||
if res != GetResult.Point: return
|
||
first_pt = gp.Point()
|
||
break
|
||
except Exception as ex:
|
||
print("[ELEMENTE] raum first-pt:", 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] raum collect:", ex); return
|
||
|
||
if outline_curve is None or not outline_curve.IsClosed:
|
||
print("[ELEMENTE] keine gueltige Outline"); return
|
||
|
||
# Element anlegen
|
||
raum_id = "raum_" + uuid.uuid4().hex[:10]
|
||
g = _geschoss_by_id(doc, geschoss)
|
||
geschoss_name = g.get("name", "EG") if g else "EG"
|
||
layer = _ensure_layer(doc, _layer_path_raum(doc, geschoss_name))
|
||
attrs = Rhino.DocObjects.ObjectAttributes()
|
||
attrs.LayerIndex = layer
|
||
_attach_meta(attrs, raum_id, "raum_outline", geschoss,
|
||
0.0, "", "", "mid",
|
||
raum_name=name,
|
||
raum_nummer="",
|
||
raum_funktion=funktion,
|
||
raum_rundung=rundung,
|
||
raum_txt_h=txt_h)
|
||
new_id = doc.Objects.AddCurve(outline_curve, attrs)
|
||
if new_id == System.Guid.Empty:
|
||
print("[ELEMENTE] Raum AddCurve fehlgeschlagen"); return
|
||
_save_last(raum_modus=modus, raum_name_last=name,
|
||
raum_rundung=rundung, raum_funktion=funktion,
|
||
raum_txt_h=txt_h)
|
||
_regenerate_element(doc, raum_id)
|
||
doc.Views.Redraw()
|
||
print("[ELEMENTE] Raum erzeugt: {} ({})".format(name, raum_id))
|
||
self._send_state()
|
||
|
||
def _cmd_export_raeume(self, p):
|
||
"""Schreibt CSV mit allen Raeumen: Nummer, Name, Geschoss,
|
||
Funktion, SIA, Flaeche, Umfang. Datei via SaveFileDialog."""
|
||
doc = Rhino.RhinoDoc.ActiveDoc
|
||
if doc is None: return
|
||
try:
|
||
from Rhino.UI import SaveFileDialog
|
||
sfd = SaveFileDialog()
|
||
sfd.DefaultExt = "csv"
|
||
sfd.Filter = "CSV (*.csv)|*.csv"
|
||
sfd.FileName = "raeume.csv"
|
||
ok = False
|
||
try: ok = sfd.ShowSaveDialog()
|
||
except Exception:
|
||
try: ok = sfd.ShowDialog()
|
||
except Exception: ok = False
|
||
if not ok:
|
||
print("[ELEMENTE] Export abgebrochen"); return
|
||
path = sfd.FileName
|
||
except Exception as ex:
|
||
print("[ELEMENTE] SaveFileDialog:", ex); return
|
||
|
||
rows = []
|
||
for obj in doc.Objects:
|
||
m = _read_meta(obj)
|
||
if not m or m["type"] != "raum_outline": continue
|
||
try:
|
||
g = _geschoss_by_id(doc, m["geschoss"])
|
||
gname = g.get("name", "?") if g else "?"
|
||
area, perim, _c = _raum_amp(obj.Geometry)
|
||
rnd = m.get("raum_rundung", "0.1")
|
||
rows.append({
|
||
"nummer": m.get("raum_nummer", ""),
|
||
"name": m.get("raum_name", "Raum"),
|
||
"geschoss": gname,
|
||
"funktion": m.get("raum_funktion", ""),
|
||
"sia": _SIA_LABELS.get(m.get("raum_sia", ""), ""),
|
||
"area": "{:.4f}".format(area),
|
||
"area_fmt": _format_area(area, rnd),
|
||
"umfang": "{:.4f}".format(perim),
|
||
})
|
||
except Exception as ex:
|
||
print("[ELEMENTE] export row:", ex)
|
||
|
||
# Stabil sortieren: Geschoss, Nummer, Name
|
||
rows.sort(key=lambda r: (r["geschoss"], r["nummer"], r["name"]))
|
||
|
||
# CSV schreiben — Semikolon (DE/CH Excel) + UTF-8 BOM
|
||
try:
|
||
import io
|
||
with io.open(path, "w", encoding="utf-8-sig", newline="") as f:
|
||
f.write("Nummer;Name;Geschoss;Funktion;SIA;"
|
||
"Flaeche (m2);Flaeche gerundet (m2);Umfang (m)\n")
|
||
for r in rows:
|
||
def esc(s):
|
||
s = str(s)
|
||
if ";" in s or '"' in s or "\n" in s:
|
||
return '"' + s.replace('"', '""') + '"'
|
||
return s
|
||
f.write(";".join([
|
||
esc(r["nummer"]), esc(r["name"]), esc(r["geschoss"]),
|
||
esc(r["funktion"]), esc(r["sia"]),
|
||
r["area"].replace(".", ","),
|
||
r["area_fmt"].replace(".", ","),
|
||
r["umfang"].replace(".", ","),
|
||
]) + "\n")
|
||
print("[ELEMENTE] Export Raeume: {} ({} Zeilen)".format(
|
||
path, len(rows)))
|
||
except Exception as ex:
|
||
print("[ELEMENTE] CSV schreiben:", ex)
|
||
|
||
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
|
||
# Tragwerk: Stuetze (Punkt) oder Traeger/Unterzug (Achse)
|
||
if old_meta["type"] in ("stuetze_point", "traeger_axis"):
|
||
kind = p.get("kind", old_meta.get("trag_kind", "stuetze"))
|
||
if kind not in _TRAG_KINDS:
|
||
kind = old_meta.get("trag_kind", "stuetze")
|
||
profil = p.get("profil", old_meta.get("trag_profil", "quadrat"))
|
||
if profil not in _TRAG_PROFILE:
|
||
profil = old_meta.get("trag_profil", "quadrat")
|
||
try: B = float(p.get("b", old_meta.get("trag_b", 0.25)))
|
||
except Exception: B = old_meta.get("trag_b", 0.25)
|
||
try: H = float(p.get("h", old_meta.get("trag_h", 0.25)))
|
||
except Exception: H = old_meta.get("trag_h", 0.25)
|
||
try: D = float(p.get("d", old_meta.get("trag_d", 0.25)))
|
||
except Exception: D = old_meta.get("trag_d", 0.25)
|
||
try: t_wall = float(p.get("t", old_meta.get("trag_t", 0.01)))
|
||
except Exception: t_wall = old_meta.get("trag_t", 0.01)
|
||
try: angle = float(p.get("angle", old_meta.get("trag_angle", 0.0)))
|
||
except Exception: angle = old_meta.get("trag_angle", 0.0)
|
||
z_over = p.get("zOver", old_meta.get("trag_z_over", ""))
|
||
if z_over is None: z_over = ""
|
||
if isinstance(z_over, (int, float)):
|
||
z_over = "{:.4f}".format(float(z_over))
|
||
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_tragwerk(doc, gn))
|
||
_attach_meta(attrs, wall_id, old_meta["type"],
|
||
gstart, 0.0, "", "", "mid",
|
||
trag_kind=kind, trag_profil=profil,
|
||
trag_b=B, trag_h=H, trag_d=D, trag_t=t_wall,
|
||
trag_angle=angle, trag_z_over=z_over)
|
||
axis_obj.Attributes = attrs
|
||
axis_obj.CommitChanges()
|
||
_regenerate_volume(doc, wall_id)
|
||
doc.Views.Redraw()
|
||
self._send_state()
|
||
return
|
||
# Raum: Name/Nummer/Funktion/Rundung/Texthoehe/Align/SIA
|
||
if old_meta["type"] == "raum_outline":
|
||
r_name = p.get("name", old_meta.get("raum_name", "Raum"))
|
||
r_num = p.get("nummer", old_meta.get("raum_nummer", ""))
|
||
r_fkt = p.get("funktion", old_meta.get("raum_funktion", ""))
|
||
r_rnd = p.get("rundung", old_meta.get("raum_rundung", "0.1"))
|
||
if r_rnd not in _RAUM_RUNDUNGEN: r_rnd = "0.1"
|
||
try: r_th = float(p.get("txtH", old_meta.get("raum_txt_h", 0.20)))
|
||
except Exception: r_th = 0.20
|
||
r_align = p.get("align", old_meta.get("raum_align", "mid"))
|
||
if r_align not in _RAUM_ALIGN: r_align = "mid"
|
||
r_sia = p.get("sia", old_meta.get("raum_sia", ""))
|
||
if r_sia not in _RAUM_SIA_KINDS: r_sia = ""
|
||
r_fuell = p.get("fuellung",
|
||
old_meta.get("raum_fuellung", ""))
|
||
# Akzeptiert String (Pattern-Name) ODER Legacy-Bool. Niemals
|
||
# nach Bool casten — sonst kollabieren alle Pattern-Strings auf "Solid".
|
||
if isinstance(r_fuell, bool):
|
||
r_fuell = "Solid" if r_fuell else ""
|
||
elif r_fuell is None:
|
||
r_fuell = ""
|
||
else:
|
||
r_fuell = str(r_fuell)
|
||
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_raum(doc, gn))
|
||
_attach_meta(attrs, wall_id, "raum_outline",
|
||
gstart, 0.0, "", "", "mid",
|
||
raum_name=r_name,
|
||
raum_nummer=r_num,
|
||
raum_funktion=r_fkt,
|
||
raum_rundung=r_rnd,
|
||
raum_txt_h=r_th,
|
||
raum_align=r_align,
|
||
raum_sia=r_sia,
|
||
raum_fuellung=r_fuell)
|
||
axis_obj.Attributes = attrs
|
||
axis_obj.CommitChanges()
|
||
_save_last(raum_name_last=r_name, raum_rundung=r_rnd,
|
||
raum_funktion=r_fkt, raum_txt_h=r_th,
|
||
raum_align=r_align, raum_sia=r_sia,
|
||
raum_fuellung=r_fuell)
|
||
_regenerate_volume(doc, wall_id)
|
||
doc.Views.Redraw()
|
||
self._send_state()
|
||
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")
|
||
# Bruestung gilt fuer ALLE Oeffnungstypen — Tueren koennen via
|
||
# Z-Drag oder Panel auch hochgehoben werden (Schwelle, Stufe).
|
||
# Default je nach Typ: Fenster 0.9, Tuer 0.0.
|
||
bruest_default = 0.9 if otyp == "fenster" else 0.0
|
||
try: brueest = float(p.get("brueest",
|
||
old_meta.get("oeff_brueest", bruest_default)))
|
||
except Exception: brueest = old_meta.get("oeff_brueest", bruest_default)
|
||
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"
|
||
# Wand-Schichten
|
||
if "layered" in p:
|
||
wand_layered = bool(p.get("layered"))
|
||
else:
|
||
wand_layered = bool(old_meta.get("wand_layered", False))
|
||
wand_layers = p.get("layers", None)
|
||
if wand_layers is None:
|
||
wand_layers = old_meta.get("wand_layers", [])
|
||
# Wenn layered an aber Liste leer → eine Default-Schicht anlegen
|
||
if wand_layered and not wand_layers:
|
||
try: total_d = float(dicke)
|
||
except Exception: total_d = 0.25
|
||
wand_layers = [{"name": "Schicht 1", "dicke": total_d,
|
||
"color": "#cccccc"}]
|
||
# 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))
|
||
# Wenn layered + Layers gegeben: dicke aus Summe nachfuehren
|
||
if wand_layered and wand_layers:
|
||
try:
|
||
dicke = sum(float(l.get("dicke", 0)) for l in wand_layers)
|
||
except Exception: pass
|
||
_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,
|
||
wand_layered=wand_layered,
|
||
wand_layers=wand_layers if wand_layered else [])
|
||
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 _apply_wand_z_drag_constraint(new_obj, meta):
|
||
"""Wand-Achse Z-Drag → unbound mode.
|
||
|
||
Der User zieht einen End-Grip der wand_axis in Z. Default-Verhalten von
|
||
Rhino waere: schraege Linie im Raum. Wir wollen stattdessen: Wand bleibt
|
||
XY-planar, der Z-Drag wird als VERSCHIEBUNG der ganzen Wand interpretiert
|
||
(uk + ok beide um delta), und die Wand entkoppelt sich vom Geschoss
|
||
(Override-UK/OK in `_KEY_UK_OVER` / `_KEY_OK_OVER` geschrieben).
|
||
|
||
Delta-Logik: max-magnitude der zwei Endpunkt-Z's gewinnt — entspricht
|
||
'letzter Drag gewinnt' wenn nur ein End-Grip gezogen wurde.
|
||
Geometry-Z wird auf 0 zurueckgesetzt.
|
||
"""
|
||
geom = new_obj.Geometry
|
||
# Sticky reset — bei JEDEM Replace, damit kein alter Delta haengt
|
||
sc.sticky["_elemente_wand_z_delta"] = None
|
||
if not isinstance(geom, rg.Curve):
|
||
print("[ELEMENTE] wand z-drag skip: geom is {}".format(type(geom).__name__))
|
||
return False
|
||
# Z aus den Endpunkten der Curve (funktioniert fuer Line, Polyline, Spline).
|
||
z0 = geom.PointAtStart.Z
|
||
z1 = geom.PointAtEnd.Z
|
||
if abs(z0) < 1e-6 and abs(z1) < 1e-6:
|
||
return False
|
||
delta = z1 if abs(z1) > abs(z0) else z0
|
||
print("[ELEMENTE] wand z-drag triggered: z0={:.3f} z1={:.3f} delta={:.3f}".format(z0, z1, delta))
|
||
doc = Rhino.RhinoDoc.ActiveDoc
|
||
if doc is None:
|
||
return False
|
||
uk_cur, ok_cur = _resolve_uk_ok(doc, meta["geschoss"],
|
||
meta["uk_override"], meta["ok_override"])
|
||
new_uk = uk_cur + delta
|
||
new_ok = ok_cur + delta
|
||
attrs = new_obj.Attributes.Duplicate()
|
||
attrs.SetUserString(_KEY_UK_OVER, "{:.6f}".format(new_uk))
|
||
attrs.SetUserString(_KEY_OK_OVER, "{:.6f}".format(new_ok))
|
||
doc.Objects.ModifyAttributes(new_obj.Id, attrs, True)
|
||
# Curve auf Z=0 fixen. LineCurve: explizit beide Endpunkte (auch bei
|
||
# einzelnem End-Grip-Drag). Andere Curves: ueber Translation (akzeptiert
|
||
# leichten Schraeg bei End-Grip-Drag, gleicht sich beim naechsten
|
||
# Replace aus).
|
||
if isinstance(geom, rg.LineCurve):
|
||
line = geom.Line
|
||
flat = rg.LineCurve(
|
||
rg.Point3d(line.From.X, line.From.Y, 0.0),
|
||
rg.Point3d(line.To.X, line.To.Y, 0.0))
|
||
else:
|
||
flat = geom.DuplicateCurve()
|
||
flat.Translate(rg.Vector3d(0, 0, -delta))
|
||
doc.Objects.Replace(new_obj.Id, flat)
|
||
# Bruestungs-Mitnahme bei Wand-Z-Drag: ausgelagert in `_migrate_openings_to_new_axis`,
|
||
# damit's in EINEM Schritt mit der XY-Migration passiert (sonst kollidiert
|
||
# ein zusaetzliches Replace hier mit dem Migrate-Replace dort und der
|
||
# Wand-Regen verliert die Volumen). Delta in sticky stellen, Migrate liest.
|
||
if abs(delta) >= 1e-6:
|
||
sc.sticky["_elemente_wand_z_delta"] = (meta["id"], delta)
|
||
# KEIN synchroner Regen hier: der Replace-Handler ruft danach noch
|
||
# `_migrate_openings_to_new_axis` + `_queue_regen` auf. Ein hier-jetzt-
|
||
# Regen wuerde die Wand mit den ALTEN Oeffnungs-Positionen neu generieren
|
||
# — Cutouts an falscher Stelle, Oeffnungs-Volumen verschoben („Symbole
|
||
# zerschossen"). Der debounced Idle-Regen sieht migrierte Oeffnungen
|
||
# und macht's konsistent.
|
||
return True
|
||
|
||
|
||
def _apply_oeffnung_constraint(new_obj, meta, old_obj=None):
|
||
"""Oeffnungs-Point Constraints:
|
||
|
||
- **XY-Drag** → direktionale Projektion: Drag-Vektor wird auf die
|
||
Wand-Tangente projiziert, dann arc-length entlang Wand verschoben.
|
||
Damit folgt der Punkt dem Drag *im Mass* und *in der Wand-Richtung*
|
||
(nicht orthogonal zur Wand wie bei ClosestPoint).
|
||
Bei fehlender Alt-Position (z.B. First-Placement): Fallback auf
|
||
nearest-point.
|
||
- **Z-Drag** → Bruestungshoehe (`_KEY_OEFF_BRUEST`) wird um delta-Z
|
||
angepasst. Oeffnungs-Hoehe selbst bleibt gleich (= Drag verschiebt,
|
||
nicht streckt). Geometry-Z wird auf 0 fixiert.
|
||
|
||
Synchroner Regen am Ende — sonst sieht der User die alten Volumen noch
|
||
~50 ms an der User-Drag-Position bis der debounced Idle-Regen sie
|
||
zurueckspringen laesst (sichtbares Flickern).
|
||
"""
|
||
geom_new = new_obj.Geometry
|
||
if not isinstance(geom_new, rg.Point):
|
||
return False
|
||
pt_new = geom_new.Location
|
||
|
||
pt_old = None
|
||
if old_obj is not None:
|
||
try:
|
||
og = old_obj.Geometry
|
||
if isinstance(og, rg.Point):
|
||
pt_old = og.Location
|
||
except Exception:
|
||
pass
|
||
|
||
parent_id = meta.get("oeff_parent")
|
||
parent_curve = None
|
||
doc = Rhino.RhinoDoc.ActiveDoc
|
||
if doc is not None and parent_id:
|
||
for obj in doc.Objects:
|
||
m = _read_meta(obj)
|
||
if m and m.get("id") == parent_id and m.get("type") == "wand_axis":
|
||
cg = obj.Geometry
|
||
if isinstance(cg, rg.Curve):
|
||
parent_curve = cg
|
||
break
|
||
|
||
target_x, target_y = pt_new.X, pt_new.Y
|
||
if parent_curve is not None:
|
||
if pt_old is not None:
|
||
try:
|
||
rc, t_old = parent_curve.ClosestPoint(
|
||
rg.Point3d(pt_old.X, pt_old.Y, 0.0))
|
||
if rc:
|
||
tangent = parent_curve.TangentAt(t_old)
|
||
tangent.Z = 0.0
|
||
if tangent.Unitize():
|
||
drag = rg.Vector3d(
|
||
pt_new.X - pt_old.X, pt_new.Y - pt_old.Y, 0.0)
|
||
step = drag * tangent # Dot-Produkt = Komponente entlang Tangente
|
||
arc_old = parent_curve.GetLength(
|
||
rg.Interval(parent_curve.Domain.T0, t_old))
|
||
full_len = parent_curve.GetLength()
|
||
arc_new = max(0.0, min(full_len, arc_old + step))
|
||
rc2, t_new = parent_curve.LengthParameter(arc_new)
|
||
if rc2:
|
||
p_on = parent_curve.PointAt(t_new)
|
||
target_x, target_y = p_on.X, p_on.Y
|
||
except Exception as ex:
|
||
print("[ELEMENTE] tangential project:", ex)
|
||
else:
|
||
try:
|
||
rc, t = parent_curve.ClosestPoint(
|
||
rg.Point3d(pt_new.X, pt_new.Y, 0.0))
|
||
if rc:
|
||
p_on = parent_curve.PointAt(t)
|
||
target_x, target_y = p_on.X, p_on.Y
|
||
except Exception:
|
||
pass
|
||
|
||
# Aktuelle Bruestung lesen — Default je nach Oeffnungs-Typ
|
||
cur_bruest = meta.get("oeff_brueest")
|
||
try:
|
||
cur_bruest_val = float(cur_bruest) if cur_bruest not in (None, "") else 0.9
|
||
except (ValueError, TypeError):
|
||
cur_bruest_val = 0.9
|
||
# Z-Delta = Drag in Z. Der Punkt SITZT visuell auf Bruestung-Hoehe
|
||
# (siehe Geometry-Schreibung unten), daher pt_old.Z ~= alte Bruestung.
|
||
delta_z = (pt_new.Z - pt_old.Z) if pt_old is not None else (pt_new.Z - cur_bruest_val)
|
||
new_bruest = cur_bruest_val
|
||
if abs(delta_z) >= 1e-6:
|
||
new_bruest = max(0.0, cur_bruest_val + delta_z)
|
||
|
||
# Punkt visuell auf Bruestungs-Hoehe (= Unterkante Oeffnung), nicht auf 0.
|
||
# So sieht der User wo die Oeffnung beginnt + Z-Drag-Delta entspricht
|
||
# direkt der Bruestungsaenderung.
|
||
target_z = new_bruest
|
||
geom_changed = not (
|
||
abs(target_x - pt_new.X) < 1e-9
|
||
and abs(target_y - pt_new.Y) < 1e-9
|
||
and abs(pt_new.Z - target_z) < 1e-6
|
||
)
|
||
# WICHTIG Reihenfolge: erst Replace (Geometry), dann ModifyAttributes auf
|
||
# das jetzt frische Object. Anders herum verliert der Replace die vorher
|
||
# geupdateten Attributes — der Wand-Regen liest dann die alte Bruestung
|
||
# und Cutout/Sub-Volumen rendern inkonsistent.
|
||
if geom_changed and doc is not None:
|
||
doc.Objects.Replace(new_obj.Id,
|
||
rg.Point(rg.Point3d(target_x, target_y, target_z)))
|
||
if abs(delta_z) >= 1e-6 and doc is not None:
|
||
current = doc.Objects.FindId(new_obj.Id)
|
||
if current is not None:
|
||
attrs = current.Attributes.Duplicate()
|
||
attrs.SetUserString(_KEY_OEFF_BRUEST, "{:.6f}".format(new_bruest))
|
||
doc.Objects.ModifyAttributes(current.Id, attrs, True)
|
||
if doc is not None and parent_id:
|
||
# Skip Sync-Regen wenn wir gerade in einer Batch-Verarbeitung sind
|
||
# (Command-End): dort macht der Caller EINEN Sync-Regen pro Wand
|
||
# am Schluss → spart Mehrfach-Regen bei mehreren Öffnungen pro Wand.
|
||
if not sc.sticky.get("_dossier_skip_sync_regen"):
|
||
try: _regenerate_element(doc, parent_id)
|
||
except Exception as ex:
|
||
print("[ELEMENTE] sync regen oeffnung:", ex)
|
||
return geom_changed
|
||
|
||
|
||
def _on_object_replaced(sender, e):
|
||
"""Wenn eine Source (Wand-Achse/Decke-Outline/etc.) veraendert wird
|
||
→ Regeneration queuen (debounct ueber Idle, 50 ms Ruhe).
|
||
|
||
Wrappt den ganzen Handler in einem Undo-Record. Sonst sind die
|
||
nachgeschalteten Delete/Create-Operationen vom Regen ausserhalb des
|
||
Rhino-User-Undo-Steps → Cmd-Z setzt nur den Drag zurueck, laesst aber
|
||
die regenerierten Volumen liegen.
|
||
"""
|
||
if sc.sticky.get(_REGEN_BUSY): return
|
||
# Wenn ein User-Transform-Command (_Move/_Rotate/etc.) aktiv ist: GAR
|
||
# NICHTS hier tun. Rhinos Move soll konfliktfrei durchlaufen. Nach
|
||
# CommandEnd vergleichen wir Snapshot vs. aktuellen State + machen den
|
||
# ganzen Update in einem konfliktfreien Batch.
|
||
if sc.sticky.get(_UT_ACTIVE_KEY): return
|
||
doc = Rhino.RhinoDoc.ActiveDoc
|
||
# Snapshot der aktuell selektierten IDs — damit Migrate die Objekte
|
||
# skippen kann die Rhinos Move/Rotate gerade transformiert (sonst
|
||
# kollidiert mein Replace mit Rhinos Transform → „Unable to transform").
|
||
# Wichtig: hier nehmen, nicht spaeter — sobald ein User-Move erstes
|
||
# Replace feuert, kann obj.IsSelected() unzuverlaessig werden.
|
||
try:
|
||
if doc is not None:
|
||
sel_ids = set()
|
||
for so in doc.Objects.GetSelectedObjects(False, False):
|
||
sel_ids.add(str(so.Id))
|
||
sc.sticky["_elemente_replace_selected_ids"] = sel_ids
|
||
except Exception: pass
|
||
undo_serial = None
|
||
if doc is not None:
|
||
try: undo_serial = doc.BeginUndoRecord("Dossier: Element Update")
|
||
except Exception: undo_serial = None
|
||
try:
|
||
_on_object_replaced_body(sender, e)
|
||
finally:
|
||
if undo_serial is not None and doc is not None:
|
||
try: doc.EndUndoRecord(undo_serial)
|
||
except Exception: pass
|
||
|
||
|
||
def _on_object_replaced_body(sender, e):
|
||
try:
|
||
import time
|
||
sc.sticky["_elemente_last_replace_time"] = time.time()
|
||
except Exception: pass
|
||
try:
|
||
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"),
|
||
trag_kind=meta.get("trag_kind") or None,
|
||
trag_profil=meta.get("trag_profil") or None,
|
||
trag_b=meta.get("trag_b"),
|
||
trag_h=meta.get("trag_h"),
|
||
trag_d=meta.get("trag_d"),
|
||
trag_t=meta.get("trag_t"),
|
||
trag_angle=meta.get("trag_angle"),
|
||
trag_z_over=meta.get("trag_z_over"),
|
||
raum_name=meta.get("raum_name") or None,
|
||
raum_nummer=meta.get("raum_nummer") or None,
|
||
raum_funktion=meta.get("raum_funktion") or None,
|
||
raum_rundung=meta.get("raum_rundung") or None,
|
||
raum_txt_h=meta.get("raum_txt_h"),
|
||
raum_align=meta.get("raum_align") or None,
|
||
raum_sia=meta.get("raum_sia") or None,
|
||
raum_fuellung=meta.get("raum_fuellung"))
|
||
new_obj.Attributes = attrs
|
||
new_obj.CommitChanges()
|
||
except Exception: pass
|
||
# Grip-Constraints: Z-Drag bei Wand → unbound mode (Override-UK/OK,
|
||
# Geometry-Z fix); XY-Drag bei Oeffnung → snap auf Eltern-Wand-Achse.
|
||
# WICHTIG _REGEN_BUSY waehrend der Korrektur, sonst loest CommitChanges
|
||
# einen rekursiven Replace-Event aus.
|
||
_was_busy = sc.sticky.get(_REGEN_BUSY, False)
|
||
sc.sticky[_REGEN_BUSY] = True
|
||
try:
|
||
if meta.get("type") == "wand_axis":
|
||
_apply_wand_z_drag_constraint(e.NewRhinoObject, meta)
|
||
elif meta.get("type") == "oeffnung_point":
|
||
_apply_oeffnung_constraint(e.NewRhinoObject, meta, e.OldRhinoObject)
|
||
except Exception as ex:
|
||
print("[ELEMENTE] grip constraint:", ex)
|
||
finally:
|
||
sc.sticky[_REGEN_BUSY] = _was_busy
|
||
|
||
# Oeffnungen entlang der neuen Achse migrieren + Regen einreihen.
|
||
if meta.get("type") == "wand_axis":
|
||
# Joint-Cache invalidieren — Wand hat sich geaendert
|
||
_invalidate_joints_cache(meta.get("geschoss"))
|
||
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)
|
||
# Wand-Verbindungen: alle ABHAENGIGEN Waende mit re-regenerieren.
|
||
# Das umfasst sowohl Corner-Partner (Endpunkte teilen) als auch
|
||
# T-Stoss-Wande (Endpunkt liegt auf der bewegten Achse). Wir
|
||
# checken gegen ALTE und NEUE Geometrie damit auch sich-loesende
|
||
# Verbindungen erkannt werden.
|
||
try:
|
||
doc2 = Rhino.RhinoDoc.ActiveDoc
|
||
if doc2 is not None:
|
||
deps = _find_dependent_walls(doc2, meta["geschoss"],
|
||
meta["id"],
|
||
old_geom, new_geom)
|
||
for wid in deps:
|
||
_queue_regen(wid)
|
||
except Exception as ex:
|
||
print("[ELEMENTE] dep regen:", 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
|
||
|
||
# Selected-Snapshot vom Replace-Handler — nicht live IsSelected, weil
|
||
# op_obj im laufenden Move-Event evtl. schon stale ist.
|
||
# Snapshot der vom User selektierten IDs vom Replace-Handler ziehen UND
|
||
# gleich consumen — sonst bleibt eine stale Liste im sticky und wirkt sich
|
||
# auf spaetere unverwandte Migrations aus.
|
||
skip_ids = sc.sticky.get("_elemente_replace_selected_ids") or set()
|
||
sc.sticky["_elemente_replace_selected_ids"] = None
|
||
_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:
|
||
# Skip Oeffnungs-Punkte die der User gerade selbst im Multi-
|
||
# Select transformiert — Rhinos Move kuemmert sich um sie,
|
||
# ein zusaetzliches `doc.Objects.Replace` hier kollidiert mit
|
||
# Rhinos parallel-laufender Move-Operation → „Unable to
|
||
# transform" + ganzer Regen-Undo-Record wird rollbacked.
|
||
if str(op_obj.Id) in skip_ids:
|
||
continue
|
||
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
|
||
# XY-only ClosestPoint — sonst zieht eine non-zero Z-Komponente
|
||
# (Bruestungs-Hoehe) den Parameter bei kurvigen Wand-Achsen
|
||
# leicht weg von der „echten" Position.
|
||
ok_old, t_old = old_geom.ClosestPoint(
|
||
rg.Point3d(cur_pos.X, cur_pos.Y, 0.0))
|
||
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)
|
||
# Z aus Bruestung des Oeffnungs-UserStrings — sonst rutscht
|
||
# der Punkt auf Wand-Achsen-Z=0 zurueck und verliert seine
|
||
# visuelle Lage unter der Oeffnung. Bruestungs-MITNAHME bei
|
||
# Wand-Z-Drag passiert im IDLE-Pfad (siehe `_on_idle_selection`,
|
||
# liest sticky `_elemente_wand_z_delta`) — NICHT hier, sonst
|
||
# kollidiert die zusaetzliche ModifyAttributes/Replace-Sequenz
|
||
# mit Rhinos Move/Rotate-Operation („Unable to transform").
|
||
bruest = op_meta.get("oeff_brueest")
|
||
try:
|
||
bruest_z = float(bruest) if bruest not in (None, "") else 0.0
|
||
except (ValueError, TypeError):
|
||
bruest_z = 0.0
|
||
new_pos = rg.Point3d(new_pos.X, new_pos.Y, bruest_z)
|
||
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_):
|
||
"""Zaehlt Objekte mit derselben element_id + type.
|
||
Skipt deleted Objekte — sonst wird beim Move-Transform (Delete+Add) das
|
||
alte Object kurz mitgezaehlt → False-positive „Duplikat" → der
|
||
`_on_object_added`-Handler vergibt eine neue ID an die Tuer/das Fenster
|
||
→ Bezug zur Eltern-Wand geht verloren → Sub-Volumen werden nie
|
||
regeneriert (Rahmen bleibt stehen)."""
|
||
n = 0
|
||
for obj in doc.Objects:
|
||
try:
|
||
if obj.IsDeleted: continue
|
||
except Exception: pass
|
||
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
|
||
# Wenn dieselbe wand_axis-ID gerade in der Cascade-Queue ist und jetzt
|
||
# zurueckkommt → war ein Transform-Delete (Rotate/Move/Mirror), keine
|
||
# echte Loeschung. Queue entfernen, sonst killen wir gleich alle
|
||
# Oeffnungen obwohl die Wand noch lebt.
|
||
if meta.get("type") == "wand_axis":
|
||
pending = sc.sticky.get("_elemente_pending_wand_cascade")
|
||
if isinstance(pending, dict) and meta["id"] in pending:
|
||
pending.pop(meta["id"], None)
|
||
# Source-Cascade canceln wenn die Source mit gleicher ID
|
||
# zurueckkommt (= war Transform, kein User-Delete).
|
||
if meta.get("type") in SOURCE_TYPES:
|
||
pending_src = sc.sticky.get("_elemente_pending_source_cascade")
|
||
if isinstance(pending_src, dict) and meta["id"] in pending_src:
|
||
pending_src.pop(meta["id"], None)
|
||
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:
|
||
# Joint-Cache invalidieren bei Wand-Duplikat
|
||
if meta["type"] == "wand_axis":
|
||
_invalidate_joints_cache(meta.get("geschoss"))
|
||
# 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_"
|
||
elif meta["type"] in ("stuetze_point", "traeger_axis"): prefix = "trag_"
|
||
elif meta["type"] == "raum_outline": prefix = "raum_"
|
||
elif meta["type"] == "decke_aussparung_outline": prefix = "aussp_"
|
||
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"),
|
||
trag_kind=meta.get("trag_kind") or None,
|
||
trag_profil=meta.get("trag_profil") or None,
|
||
trag_b=meta.get("trag_b"),
|
||
trag_h=meta.get("trag_h"),
|
||
trag_d=meta.get("trag_d"),
|
||
trag_t=meta.get("trag_t"),
|
||
trag_angle=meta.get("trag_angle"),
|
||
trag_z_over=meta.get("trag_z_over"),
|
||
raum_name=meta.get("raum_name") or None,
|
||
raum_nummer=meta.get("raum_nummer") or None,
|
||
raum_funktion=meta.get("raum_funktion") or None,
|
||
raum_rundung=meta.get("raum_rundung") or None,
|
||
raum_txt_h=meta.get("raum_txt_h"))
|
||
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.
|
||
|
||
WICHTIG: Volume-Delete wird in eine Queue gelegt + im Idle prozessiert.
|
||
Sofortiges Loeschen kollidiert mit _Move/_Rotate/_Mirror, die das
|
||
Source-Object via Delete+Re-Add transformieren — der naechste Move-
|
||
Schritt fuer das Volume bekommt dann „Unable to transform" weil das
|
||
Volume schon weg ist. Die Queue wird in `_on_object_added` gecancelt
|
||
wenn die Source mit gleicher ID zurueckkommt (= Transform, kein User-
|
||
Delete).
|
||
"""
|
||
try:
|
||
obj = e.TheObject
|
||
meta = _read_meta(obj)
|
||
if meta and meta.get("type") in SOURCE_TYPES:
|
||
doc = Rhino.RhinoDoc.ActiveDoc
|
||
# Source-Cascade in die Queue (alle Sub-Volumen sammeln —
|
||
# Tueren/Fenster haben mehrere: Rahmen, Sims, Glas, Fluegel).
|
||
try:
|
||
import time
|
||
vol_ids = [v.Id for v in _find_all_volumes(doc, meta["id"])]
|
||
if vol_ids:
|
||
pending = sc.sticky.get("_elemente_pending_source_cascade")
|
||
if not isinstance(pending, dict):
|
||
pending = {}
|
||
sc.sticky["_elemente_pending_source_cascade"] = pending
|
||
parent_id = (meta.get("oeff_parent")
|
||
or meta.get("aussp_parent") or "")
|
||
pending[meta["id"]] = {
|
||
"ts": time.time(), "volumes": vol_ids,
|
||
"type": meta["type"], "parent": parent_id,
|
||
}
|
||
except Exception as ex:
|
||
print("[ELEMENTE] queue source cascade:", ex)
|
||
if meta["type"] == "oeffnung_point":
|
||
parent_id = meta.get("oeff_parent")
|
||
if parent_id:
|
||
_queue_regen(parent_id)
|
||
if meta["type"] == "decke_aussparung_outline":
|
||
parent_id = meta.get("aussp_parent")
|
||
if parent_id:
|
||
_queue_regen(parent_id)
|
||
# Wand-Verbindung: alle abhaengigen Waende (Corner + T-Stoss)
|
||
# neu generieren — ihre Miter loest sich auf da die Partnerwand
|
||
# weg ist. Zusaetzlich: alle in der Wand sitzenden Oeffnungen
|
||
# (Tueren/Fenster via oeff_parent) zur Loesch-Queue. Die wird im
|
||
# Idle erst abgearbeitet (mit Check ob wand_axis re-added wurde),
|
||
# weil _Rotate/_Move/_Mirror eine Wand intern via Delete+Re-Add
|
||
# transformieren — sofortiges Loeschen wuerde alle Oeffnungen
|
||
# vernichten obwohl die Wand gleich zurueck kommt.
|
||
if meta["type"] == "wand_axis":
|
||
_invalidate_joints_cache(meta.get("geschoss"))
|
||
try:
|
||
import time
|
||
op_ids = [op_meta["id"] for _op_obj, op_meta
|
||
in _find_openings_for_wall(doc, meta["id"])]
|
||
if op_ids:
|
||
pending = sc.sticky.get("_elemente_pending_wand_cascade")
|
||
if not isinstance(pending, dict):
|
||
pending = {}
|
||
sc.sticky["_elemente_pending_wand_cascade"] = pending
|
||
pending[meta["id"]] = {"ts": time.time(),
|
||
"openings": op_ids}
|
||
except Exception as ex:
|
||
print("[ELEMENTE] queue cascade:", ex)
|
||
try:
|
||
geom = obj.Geometry if obj is not None else None
|
||
if isinstance(geom, rg.Curve):
|
||
deps = _find_dependent_walls(doc, meta["geschoss"],
|
||
meta["id"], geom, None)
|
||
for wid in deps:
|
||
_queue_regen(wid)
|
||
except Exception as ex:
|
||
print("[ELEMENTE] del dep regen:", ex)
|
||
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.
|
||
# Klick auf eine Surface zieht alle Schichten/Sub-Volumen desselben Bauteils
|
||
# mit (Rahmen+Sims+Fluegel bei Oeffnungen, Stufen bei Treppen, Schichten bei
|
||
# Wand/Decke). Source-Achse/Punkt kriegt zusaetzlich Grips zum Editieren.
|
||
_PAIRED_VOLUME_TYPES = (
|
||
"wand_volume", "decke_volume", "dach_volume",
|
||
"oeffnung_volume", "treppe_volume",
|
||
"stuetze_volume", "traeger_volume",
|
||
)
|
||
_PAIRED_SOURCE_TYPES = (
|
||
"wand_axis", "decke_outline", "dach_outline",
|
||
"oeffnung_point", "treppe_axis",
|
||
"stuetze_point", "traeger_axis",
|
||
)
|
||
|
||
|
||
def _find_all_volumes(doc, element_id, type_filter=None):
|
||
"""Liefert ALLE Volume-Objekte zu element_id (z.B. alle Schichten einer
|
||
mehrlagigen Wand)."""
|
||
out = []
|
||
for obj in doc.Objects:
|
||
m = _read_meta(obj)
|
||
if m and m["id"] == element_id:
|
||
t = m["type"]
|
||
if t not in VOLUME_TYPES: continue
|
||
if type_filter is None or t == type_filter:
|
||
out.append(obj)
|
||
return out
|
||
|
||
|
||
def _collect_partners(doc, rhino_objects):
|
||
"""Sammelt Partner-Objekte fuer Selection-Sync und die Source-Objekte
|
||
die Grips brauchen. Bei mehrschichtigen Waenden werden ALLE Schicht-
|
||
Volumen als Partner gesammelt — die Wand verhaelt sich dann als ein
|
||
zusammenhaengender Bauteil-Verbund. Liefert (partners, sources)."""
|
||
partners = []
|
||
sources = []
|
||
seen_partner_ids = set()
|
||
seen_source_ids = set()
|
||
def _add_partner(o):
|
||
if o is None: return
|
||
sid = str(o.Id)
|
||
if sid in seen_partner_ids: return
|
||
partners.append(o); seen_partner_ids.add(sid)
|
||
def _add_source(o):
|
||
if o is None: return
|
||
sid = str(o.Id)
|
||
if sid in seen_source_ids: return
|
||
sources.append(o); seen_source_ids.add(sid)
|
||
for obj in rhino_objects:
|
||
meta = _read_meta(obj)
|
||
if meta is None: continue
|
||
t = meta.get("type", "")
|
||
if t in _PAIRED_VOLUME_TYPES:
|
||
# Klick auf Volume (oder eine Schicht) → Source + alle Geschwister-
|
||
# Volumen (andere Schichten derselben Wand) mitsammeln.
|
||
src, _ = _find_source(doc, meta["id"])
|
||
if src is not None:
|
||
_add_partner(src); _add_source(src)
|
||
for v in _find_all_volumes(doc, meta["id"]):
|
||
if str(v.Id) != str(obj.Id):
|
||
_add_partner(v)
|
||
elif t in _PAIRED_SOURCE_TYPES:
|
||
# Klick auf Source → ALLE Volumen (alle Schichten) mitsammeln.
|
||
for v in _find_all_volumes(doc, meta["id"]):
|
||
_add_partner(v)
|
||
_add_source(obj)
|
||
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 _sync_orphan_grips(doc):
|
||
"""Cleanup: alle Source-Objekte mit GripsOn=True die NICHT mehr
|
||
selektiert sind → Grips abschalten. Verhindert dass Grips nach
|
||
Deselect-Events haengen bleiben."""
|
||
if sc.sticky.get(_SELECT_BUSY): return
|
||
if sc.sticky.get(_REGEN_BUSY): return
|
||
sc.sticky[_SELECT_BUSY] = True
|
||
try:
|
||
for obj in doc.Objects:
|
||
try:
|
||
if not obj.GripsOn: continue
|
||
meta = _read_meta(obj)
|
||
if meta is None: continue
|
||
if meta.get("type") not in _PAIRED_SOURCE_TYPES: continue
|
||
if obj.IsSelected(False) == 0:
|
||
obj.GripsOn = False
|
||
obj.CommitChanges()
|
||
except Exception: pass
|
||
finally:
|
||
sc.sticky[_SELECT_BUSY] = False
|
||
|
||
|
||
def _on_idle_selection(sender, e):
|
||
"""Pollt periodisch die Selektion + verarbeitet Pending-Regenerate-Queue.
|
||
|
||
Debouncing: Pending-Regens werden erst nach 80 ms Ruhe (kein neues
|
||
Replace-Event) ausgefuehrt. So vermeiden wir Volume-Flicker waehrend
|
||
fortlaufenden Gumball-/Move-Operationen — der finale Regen rendert
|
||
nach Drag-Ende, bis dahin uebernimmt Rhinos Transform die Geometrie."""
|
||
b = sc.sticky.get("elemente_bridge")
|
||
if b is None: return
|
||
doc = Rhino.RhinoDoc.ActiveDoc
|
||
if doc is None: return
|
||
|
||
# 1) Pending Regenerations abarbeiten — debounct (50 ms Ruhe).
|
||
# Kein EnableDrawing-Suspend mehr (das hat User-Feedback langsamer
|
||
# gemacht und konnte das Volumen "verschwinden" lassen wenn der
|
||
# Toggle nicht sauber zurueck-flippt).
|
||
pending = _pending_set()
|
||
if pending:
|
||
try:
|
||
import time
|
||
last_replace = sc.sticky.get("_elemente_last_replace_time", 0.0)
|
||
now = time.time()
|
||
quiet_for = now - last_replace
|
||
except Exception:
|
||
quiet_for = 1.0
|
||
if quiet_for >= 0.05:
|
||
ids = list(pending)
|
||
pending.clear()
|
||
sc.sticky[_REGEN_BUSY] = True
|
||
# Bulk-Performance: ein einziger Undo-Record fuer alle queued
|
||
# Regens + Redraw nur am Ende (statt einem pro AddBrep/Delete).
|
||
undo_serial = doc.BeginUndoRecord(
|
||
"Elemente regenerieren ({})".format(len(ids)))
|
||
prev_redraw = doc.Views.RedrawEnabled
|
||
doc.Views.RedrawEnabled = False
|
||
try:
|
||
with _TimedBlock("Idle-Regen-Batch x{}".format(len(ids))):
|
||
for wid in ids:
|
||
try: _regenerate_volume(doc, wid)
|
||
except Exception as ex:
|
||
print("[ELEMENTE] regen", wid, ex)
|
||
finally:
|
||
doc.Views.RedrawEnabled = prev_redraw
|
||
try: doc.EndUndoRecord(undo_serial)
|
||
except Exception: pass
|
||
sc.sticky[_REGEN_BUSY] = False
|
||
try: doc.Views.Redraw()
|
||
except Exception: pass
|
||
try: b._send_state()
|
||
except Exception: pass
|
||
|
||
# 1a) Pending Wand-Cascade-Deletes verarbeiten.
|
||
# Bei Wand-Delete queuen wir die zugehoerigen Oeffnungs-IDs (statt sofort
|
||
# zu loeschen), weil _Rotate/_Move/_Mirror die Wand intern via Delete+
|
||
# Re-Add transformieren. Im Re-Add wird der Eintrag entfernt (kein
|
||
# echter Delete). Bleibt der Eintrag nach 500 ms uebrig → echter User-
|
||
# Delete → Oeffnungen kaskaden.
|
||
pending_cascade = sc.sticky.get("_elemente_pending_wand_cascade")
|
||
if isinstance(pending_cascade, dict) and pending_cascade:
|
||
try:
|
||
import time
|
||
now = time.time()
|
||
to_run = []
|
||
for wall_id, info in list(pending_cascade.items()):
|
||
if now - info.get("ts", 0) >= 0.5:
|
||
to_run.append((wall_id, info.get("openings", [])))
|
||
del pending_cascade[wall_id]
|
||
for wall_id, op_ids in to_run:
|
||
# Doppelt-Check: lebt die Wand noch wirklich nicht mehr?
|
||
still_there = False
|
||
for obj in doc.Objects:
|
||
m = _read_meta(obj)
|
||
if m and m.get("id") == wall_id and m.get("type") == "wand_axis":
|
||
still_there = True
|
||
break
|
||
if still_there:
|
||
continue
|
||
_was = sc.sticky.get(_REGEN_BUSY, False)
|
||
sc.sticky[_REGEN_BUSY] = True
|
||
try:
|
||
for op_id in op_ids:
|
||
for vol_obj in _find_all_volumes(doc, op_id):
|
||
try: doc.Objects.Delete(vol_obj.Id, True)
|
||
except Exception: pass
|
||
# Source-Point auch loeschen
|
||
for obj, _m in _find_objects_by_wall_id(doc, op_id):
|
||
if _m.get("type") == "oeffnung_point":
|
||
try: doc.Objects.Delete(obj.Id, True)
|
||
except Exception: pass
|
||
finally:
|
||
sc.sticky[_REGEN_BUSY] = _was
|
||
if to_run:
|
||
try: doc.Views.Redraw()
|
||
except Exception: pass
|
||
try: b._send_state()
|
||
except Exception: pass
|
||
except Exception as ex:
|
||
print("[ELEMENTE] pending cascade:", ex)
|
||
|
||
# 1a2) Pending Source-Cascade verarbeiten (Tueren/Fenster/Aussparungen).
|
||
# Analog Wand-Cascade: Source-Delete koennte Transform sein (Move/Rotate),
|
||
# daher 500 ms warten + Check ob Source mit gleicher ID re-added wurde.
|
||
pending_src = sc.sticky.get("_elemente_pending_source_cascade")
|
||
if isinstance(pending_src, dict) and pending_src:
|
||
try:
|
||
import time
|
||
now = time.time()
|
||
to_run_src = []
|
||
for src_id, info in list(pending_src.items()):
|
||
if now - info.get("ts", 0) >= 0.5:
|
||
to_run_src.append((src_id, info))
|
||
del pending_src[src_id]
|
||
for src_id, info in to_run_src:
|
||
still_there = False
|
||
for obj in doc.Objects:
|
||
m = _read_meta(obj)
|
||
if m and m.get("id") == src_id and m.get("type") == info.get("type"):
|
||
still_there = True
|
||
break
|
||
if still_there:
|
||
continue
|
||
_was = sc.sticky.get(_REGEN_BUSY, False)
|
||
sc.sticky[_REGEN_BUSY] = True
|
||
try:
|
||
for vol_id in info.get("volumes", []):
|
||
try: doc.Objects.Delete(vol_id, True)
|
||
except Exception: pass
|
||
finally:
|
||
sc.sticky[_REGEN_BUSY] = _was
|
||
parent_id = info.get("parent")
|
||
if parent_id:
|
||
_queue_regen(parent_id)
|
||
if to_run_src:
|
||
try: doc.Views.Redraw()
|
||
except Exception: pass
|
||
try: b._send_state()
|
||
except Exception: pass
|
||
except Exception as ex:
|
||
print("[ELEMENTE] pending source cascade:", ex)
|
||
|
||
# 1b) SIA-State-Change-Detection — wenn der User den Override-Modus
|
||
# ausserhalb meines SIA-Buttons aendert (z.B. via Overrides-Panel-
|
||
# Master-Toggle oder via Preset-Dropdown), regen wir alle Raeume.
|
||
try:
|
||
cur_sia = _sia_fill_enabled(doc)
|
||
last_sia = getattr(b, "_last_sia_state", None)
|
||
if last_sia is None:
|
||
b._last_sia_state = cur_sia
|
||
elif last_sia != cur_sia:
|
||
b._last_sia_state = cur_sia
|
||
ids = []
|
||
for obj in doc.Objects:
|
||
mm = _read_meta(obj)
|
||
if mm and mm["type"] == "raum_outline":
|
||
ids.append(mm["id"])
|
||
if ids:
|
||
sc.sticky[_REGEN_BUSY] = True
|
||
undo_serial = doc.BeginUndoRecord(
|
||
"SIA-Modus Regen ({})".format(len(ids)))
|
||
prev_redraw = doc.Views.RedrawEnabled
|
||
doc.Views.RedrawEnabled = False
|
||
try:
|
||
with _TimedBlock("SIA-Regen x{}".format(len(ids))):
|
||
for rid in ids:
|
||
try: _regenerate_element(doc, rid)
|
||
except Exception: pass
|
||
finally:
|
||
doc.Views.RedrawEnabled = prev_redraw
|
||
try: doc.EndUndoRecord(undo_serial)
|
||
except Exception: pass
|
||
sc.sticky[_REGEN_BUSY] = False
|
||
try: doc.Views.Redraw()
|
||
except Exception: pass
|
||
try: b._send_state()
|
||
except Exception: pass
|
||
except Exception: pass
|
||
|
||
# 2) Grips-Sync — sicherstellen dass keine "orphan" Grips visible bleiben
|
||
try: _sync_orphan_grips(doc)
|
||
except Exception: pass
|
||
|
||
# 3) 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
|
||
|
||
|
||
# Welche Rhino-Commands transformieren mehrere Objekte gleichzeitig — bei
|
||
# diesen lassen wir Rhinos Move/Rotate KOMPLETT durchlaufen und feuern den
|
||
# Wand-Regen erst NACH CommandEnd. So gibt's keine „Unable to transform"-
|
||
# Kollision mehr zwischen meinem Sync-Regen und Rhinos pending Transforms.
|
||
_USER_TRANSFORM_CMDS = frozenset((
|
||
"Move", "Rotate", "Rotate3D", "Mirror", "Scale", "Scale1D", "Scale2D",
|
||
"Drag", "Gumball", "Orient", "Orient3Pt", "RemapCPlane", "Transform",
|
||
))
|
||
|
||
_UT_ACTIVE_KEY = "_dossier_user_transform_active"
|
||
_UT_SNAPSHOT_KEY = "_dossier_pre_transform_snapshot"
|
||
|
||
|
||
def _snapshot_source_positions(doc):
|
||
"""Schnappschuss aller Source-Geometrien — gerade vor einem User-Transform.
|
||
Wird in _on_command_end gegen aktuelle Positionen verglichen, um Brüstung-
|
||
Mitnahme + Migration zu rechnen ohne mit Rhinos noch laufender Move-
|
||
Operation zu kollidieren."""
|
||
snap = {}
|
||
if doc is None: return snap
|
||
for obj in doc.Objects:
|
||
try:
|
||
m = _read_meta(obj)
|
||
if not m: continue
|
||
t = m.get("type")
|
||
if t not in SOURCE_TYPES: continue
|
||
geom = obj.Geometry
|
||
if hasattr(geom, "Location"):
|
||
p = geom.Location
|
||
snap[m["id"]] = {"type": t, "pos": (p.X, p.Y, p.Z)}
|
||
elif isinstance(geom, rg.Curve):
|
||
s = geom.PointAtStart; e = geom.PointAtEnd
|
||
snap[m["id"]] = {"type": t,
|
||
"start": (s.X, s.Y, s.Z),
|
||
"end": (e.X, e.Y, e.Z)}
|
||
except Exception: pass
|
||
return snap
|
||
|
||
|
||
def _on_command_begin(sender, e):
|
||
try:
|
||
name = getattr(e, "CommandEnglishName", "") or ""
|
||
except Exception: name = ""
|
||
if name not in _USER_TRANSFORM_CMDS: return
|
||
doc = Rhino.RhinoDoc.ActiveDoc
|
||
if doc is None: return
|
||
sc.sticky[_UT_SNAPSHOT_KEY] = _snapshot_source_positions(doc)
|
||
sc.sticky[_UT_ACTIVE_KEY] = name
|
||
|
||
|
||
def _on_command_end(sender, e):
|
||
name = sc.sticky.get(_UT_ACTIVE_KEY)
|
||
if not name: return
|
||
sc.sticky[_UT_ACTIVE_KEY] = None
|
||
snapshot = sc.sticky.get(_UT_SNAPSHOT_KEY) or {}
|
||
sc.sticky[_UT_SNAPSHOT_KEY] = None
|
||
doc = Rhino.RhinoDoc.ActiveDoc
|
||
if doc is None: return
|
||
|
||
# Pseudo-Object Wrapper damit _apply_oeffnung_constraint pt_old.Location
|
||
# lesen kann ohne den echten alten RhinoObject zu kennen.
|
||
class _PseudoOld(object):
|
||
def __init__(self, pt): self.Geometry = rg.Point(pt)
|
||
|
||
affected_walls = set()
|
||
_was_busy = sc.sticky.get(_REGEN_BUSY, False)
|
||
sc.sticky[_REGEN_BUSY] = True
|
||
# Skip-Flag: in der Schleife wuerde jeder Constraint einen eigenen Sync-
|
||
# Regen ausloesen → mehrere Regens pro Wand. Wir machen am Schluss EINEN
|
||
# Regen pro affected_wall — viel schneller bei mehreren Oeffnungen.
|
||
sc.sticky["_dossier_skip_sync_regen"] = True
|
||
# Display-Updates komplett suppressen waehrend der Batch — Rhino zeichnet
|
||
# sonst nach jedem Brep-Add/Replace neu, was bei mehreren Sub-Volumen
|
||
# sichtbares „Aufbauen" verursacht. Ein einziger Redraw am Ende reicht.
|
||
prev_redraw = doc.Views.RedrawEnabled
|
||
doc.Views.RedrawEnabled = False
|
||
try:
|
||
for obj in list(doc.Objects):
|
||
try:
|
||
m = _read_meta(obj)
|
||
if not m: continue
|
||
t = m.get("type")
|
||
if t not in SOURCE_TYPES: continue
|
||
old = snapshot.get(m["id"])
|
||
if old is None: continue
|
||
if t == "wand_axis":
|
||
geom = obj.Geometry
|
||
if not isinstance(geom, rg.Curve): continue
|
||
os = old.get("start"); oe = old.get("end")
|
||
# Migrate NUR wenn XY tatsaechlich geaendert. Bei reinem
|
||
# Z-Drag (XY identisch) waere Migrate ein no-op-Loop ueber
|
||
# alle Oeffnungen mit Replace-Ops je Punkt — spart die
|
||
# ganze Pass + die nachfolgenden Replace-Events.
|
||
if os and oe:
|
||
xy_changed = (
|
||
abs(geom.PointAtStart.X - os[0]) > 1e-6 or
|
||
abs(geom.PointAtStart.Y - os[1]) > 1e-6 or
|
||
abs(geom.PointAtEnd.X - oe[0]) > 1e-6 or
|
||
abs(geom.PointAtEnd.Y - oe[1]) > 1e-6
|
||
)
|
||
if xy_changed:
|
||
try:
|
||
old_line = rg.LineCurve(
|
||
rg.Point3d(os[0], os[1], os[2]),
|
||
rg.Point3d(oe[0], oe[1], oe[2]))
|
||
_migrate_openings_to_new_axis(m["id"], old_line, geom)
|
||
except Exception as ex:
|
||
print("[ELEMENTE] post-cmd migrate:", ex)
|
||
# Z-Drag detect + Brüstungs-Mitnahme. Constraint setzt
|
||
# sticky-delta wenn Z geaendert; wir consumen es direkt.
|
||
_apply_wand_z_drag_constraint(obj, m)
|
||
z_entry = sc.sticky.get("_elemente_wand_z_delta")
|
||
z_delta = 0.0
|
||
if isinstance(z_entry, tuple) and len(z_entry) == 2 \
|
||
and z_entry[0] == m["id"]:
|
||
try: z_delta = float(z_entry[1])
|
||
except (ValueError, TypeError): z_delta = 0.0
|
||
sc.sticky["_elemente_wand_z_delta"] = None
|
||
if abs(z_delta) >= 1e-6:
|
||
# Brüstungen aller Öffnungen der Wand um delta mitnehmen
|
||
for op_obj, op_meta in _find_openings_for_wall(doc, m["id"]):
|
||
cur_b = op_meta.get("oeff_brueest")
|
||
try:
|
||
cur_b_val = float(cur_b) if cur_b not in (None, "") else 0.0
|
||
except (ValueError, TypeError):
|
||
cur_b_val = 0.0
|
||
new_b = max(0.0, cur_b_val + z_delta)
|
||
try:
|
||
attrs = op_obj.Attributes.Duplicate()
|
||
attrs.SetUserString(_KEY_OEFF_BRUEST,
|
||
"{:.6f}".format(new_b))
|
||
doc.Objects.ModifyAttributes(op_obj.Id, attrs, True)
|
||
pt_geom = op_obj.Geometry
|
||
if hasattr(pt_geom, "Location"):
|
||
pt = pt_geom.Location
|
||
doc.Objects.Replace(op_obj.Id,
|
||
rg.Point(rg.Point3d(pt.X, pt.Y, new_b)))
|
||
except Exception as ex:
|
||
print("[ELEMENTE] post-cmd brueest:", ex)
|
||
affected_walls.add(m["id"])
|
||
elif t == "oeffnung_point":
|
||
op_pos = old.get("pos")
|
||
if op_pos is None: continue
|
||
pseudo = _PseudoOld(rg.Point3d(op_pos[0], op_pos[1], op_pos[2]))
|
||
_apply_oeffnung_constraint(obj, m, pseudo)
|
||
pid = m.get("oeff_parent")
|
||
if pid: affected_walls.add(pid)
|
||
except Exception as ex:
|
||
print("[ELEMENTE] post-cmd source:", ex)
|
||
finally:
|
||
sc.sticky[_REGEN_BUSY] = _was_busy
|
||
sc.sticky["_dossier_skip_sync_regen"] = None
|
||
|
||
# Sync-Regen aller betroffenen Wände — Move ist sauber abgeschlossen,
|
||
# kein Konflikt mehr moeglich. EIN Regen pro Wand (nicht pro Oeffnung).
|
||
# Display bleibt suppressed bis ALLE Wände durch sind → kein „Aufbauen".
|
||
try:
|
||
for wid in affected_walls:
|
||
try: _regenerate_element(doc, wid)
|
||
except Exception as ex:
|
||
print("[ELEMENTE] post-cmd regen:", ex)
|
||
finally:
|
||
doc.Views.RedrawEnabled = prev_redraw
|
||
try: doc.Views.Redraw()
|
||
except Exception: pass
|
||
b = sc.sticky.get("elemente_bridge")
|
||
if b is not None:
|
||
try: b._send_state()
|
||
except Exception: pass
|
||
|
||
|
||
def _install_listeners(bridge):
|
||
"""Listener-Registrierung mit Re-Reload-Schutz.
|
||
|
||
Problem: `_reset_panels.py` cleart sticky-Flags + lädt das Modul neu.
|
||
Die alten Listener-Function-Refs bleiben aber in Rhinos Event-Liste
|
||
(`-=` auf den Bool-Flag entfernt sie nicht). Resultat nach Reload:
|
||
Listener doppelt registriert → jedes Event feuert zweimal → Migrate
|
||
laeuft parallel → Race-Condition + Symbole hängen.
|
||
|
||
Fix: Function-Refs in sticky merken. Vor Re-Registrierung die alten
|
||
Refs explizit deregistrieren — `-=` funktioniert weil wir denselben
|
||
Pointer in sticky aufbewahrt haben.
|
||
"""
|
||
# Wichtig: refs_key darf KEINEN Modul-Namen enthalten (z.B. "elemente"),
|
||
# sonst cleart `_reset_panels.py` ihn auf None vor dem Re-Install → wir
|
||
# finden die alten Function-Refs nicht mehr → Listener werden nicht
|
||
# deregistriert → mehrfache Registrierung nach jedem Reload.
|
||
refs_key = "_dossier_runtime_event_refs"
|
||
sc.sticky["elemente_bridge"] = bridge
|
||
old_refs = sc.sticky.get(refs_key)
|
||
if isinstance(old_refs, dict):
|
||
try:
|
||
if old_refs.get("replace"): Rhino.RhinoDoc.ReplaceRhinoObject -= old_refs["replace"]
|
||
except Exception: pass
|
||
try:
|
||
if old_refs.get("add"): Rhino.RhinoDoc.AddRhinoObject -= old_refs["add"]
|
||
except Exception: pass
|
||
try:
|
||
if old_refs.get("delete"): Rhino.RhinoDoc.DeleteRhinoObject -= old_refs["delete"]
|
||
except Exception: pass
|
||
try:
|
||
if old_refs.get("select"): Rhino.RhinoDoc.SelectObjects -= old_refs["select"]
|
||
except Exception: pass
|
||
try:
|
||
if old_refs.get("deselect"): Rhino.RhinoDoc.DeselectObjects -= old_refs["deselect"]
|
||
except Exception: pass
|
||
try:
|
||
if old_refs.get("idle"): Rhino.RhinoApp.Idle -= old_refs["idle"]
|
||
except Exception: pass
|
||
try:
|
||
if old_refs.get("cmd_begin"): Rhino.Commands.Command.BeginCommand -= old_refs["cmd_begin"]
|
||
except Exception: pass
|
||
try:
|
||
if old_refs.get("cmd_end"): Rhino.Commands.Command.EndCommand -= old_refs["cmd_end"]
|
||
except Exception: pass
|
||
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
|
||
Rhino.Commands.Command.BeginCommand += _on_command_begin
|
||
Rhino.Commands.Command.EndCommand += _on_command_end
|
||
sc.sticky[refs_key] = {
|
||
"replace": _on_object_replaced,
|
||
"add": _on_object_added,
|
||
"delete": _on_object_deleted,
|
||
"select": _on_select_objects,
|
||
"deselect": _on_deselect_objects,
|
||
"idle": _on_idle_selection,
|
||
"cmd_begin": _on_command_begin,
|
||
"cmd_end": _on_command_end,
|
||
}
|
||
print("[ELEMENTE] Listener aktiv (Replace + Add + Delete + Select + Idle + Cmd)")
|
||
|
||
|
||
def _bridge_factory():
|
||
b = ElementeBridge()
|
||
_install_listeners(b)
|
||
return b
|
||
|
||
|
||
panel_base.register_and_open("elemente", "ELEMENTE", PANEL_GUID_STR,
|
||
_bridge_factory,
|
||
icon_spec=("foundation", "#5fa896"))
|