Fenster/Tueren LoD + Stile + Phase-3-Ausschnitt-Darstellung + UI-Konsistenz

Fenster/Tueren:
- 3-stufige SIA-400-Darstellung pro Element: einfach (1:100, flache
  Scheibe ohne Tiefe in Wand-Mittelebene), standard (1:50, Rahmen +
  Glas + Sims), detail (1:20, Doppelverglasung).
- Aussenseite-Flag mit Auto-Detection aus der Click-Richtung beim
  Setzen — Sim sitzt automatisch aussen. Im Panel als Umkehren-Toggle.
- Tueren-Rahmen-Typ Zarge|Block — Blockrahmen ragt seitlich raus.
- Rahmen-Offset (m von Wand-Innenseite) ersetzt das 3-Preset Lage-
  Feld. Wirkt auch in der einfachen Darstellung (Pane sitzt auf der
  Rahmen-Mittelebene, nicht in Wand-Mitte).
- Sims nur AUSSEN. Innen entfaellt — der Sim ist gleichzeitig der
  visuelle Indikator fuer die Aussenseite.
- Oeffnungs-Stile: list/save/delete-API mit 6 Default-Presets
  (Fenster Standard/Gross/Bandlage, Tuer Innen/Eingang/Verglast).
  Style-ID per UserString am Objekt persistiert. Im Panel BarCombo
  mit "Aktuelle als Stil speichern…". Beim Rhino-Command "Stil"-
  Option zum Picken vor dem Klick.

Ausschnitt-Darstellung (Phase 3):
- Doc-Level Override dossier_aktive_darstellung gewinnt vor per-
  Object-Setting. Wechsel triggert Regen aller Oeffnungen via neuer
  regenerate_all_oeffnungen-API.
- Ausschnitt-Capture speichert die Darstellung mit, Restore wendet
  sie an und regeneriert.
- Oberleiste-Quick-Switch BarCombo mit 4 Optionen.
- AusschnittSettings-Dialog: Darstellungs-Dropdown.

Gestaltung (SectionStyle Phase 2):
- _set_section_style schreibt per-Object SectionHatchIndex/Scale/
  Rotation/Color mit Multi-Fallback (Property-Namen varieren je
  Rhino-Build). _selection_summary liest die selben zurueck.
- HatchEditor als shared Component fuer Fill + Section.
- geometryKind ignoriert DOSSIER-Source-Curves damit Wand-Selektion
  (Axis + Volume) als 3D klassifiziert wird.

UI-Konsistenz Panels:
- Ebenenkombi zurueck als eigene Section oben im Ebenen-Panel,
  Modelldarstellung-Dropdown an die freigewordene Position in der
  Oberleiste (Row 1 Col 2 im 2x2-Preset-Block).
- BarCombo erweitert: stretch-Prop (Pill waechst auf Container-
  Breite), onSecond/secondIcon/secondTitle fuer 2. Trailing-Button,
  gearIcon-Prop. Plus-Slot immer ganz aussen rechts, Settings-Slot
  direkt nach dem Caret.
- Ebenen + Zeichnungsebenen visuell kohaerent: identisches Padding
  (1px 12px 1px 0), Chevron/Spacer-Slot 12px, Master-Row mit Eye
  16x16 + Lock 14x14, gleiche Border + Borderfarbe. Eye-Icons in
  beiden Panels untereinander ausgerichtet.
- Properties-Container ohne Border (war zuvor accent-gruen, dann
  border — User wollte gar nichts mehr).
- ElementList raus aus dem Elemente-Panel (Uebersicht via Tree-
  Window erreichbar). NeuesElement bleibt voll sichtbar bei
  Selektion (kein Collapse), Properties oben.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-22 12:34:15 +02:00
parent 9ae8574ab0
commit 0c5f8055a5
13 changed files with 899 additions and 220 deletions
+504 -54
View File
@@ -59,6 +59,182 @@ _KEY_OEFF_SIMS_AUS = "dossier_oeff_sims_aus" # Style: "ohne"|"schmal"|"
_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
_KEY_OEFF_DARSTELLUNG = "dossier_oeff_darstellung" # "einfach" | "standard" | "detail" — SIA-400 LoD
_KEY_OEFF_AUSSENSEITE = "dossier_oeff_aussenseite" # "links" | "rechts" — welche Wand-Seite ist aussen
_KEY_OEFF_TUER_RAHMEN = "dossier_oeff_tuer_rahmen" # "zarge" | "block" — Tueren-Rahmen-Typ
_KEY_OEFF_RAHMEN_OFFSET = "dossier_oeff_rahmen_offset" # float (m): Abstand Rahmen-Innenkante von Wand-Innenseite
_KEY_AKTIVE_DARSTELLUNG = "dossier_aktive_darstellung" # doc-level global override: einfach|standard|detail|"" (= per-object)
_KEY_OEFF_STYLE_ID = "dossier_oeff_style_id" # per-Object: referenziert einen Style aus dossier_oeff_styles
_KEY_OEFF_STYLES = "dossier_oeff_styles" # JSON-Liste aller Fenster/Tueren-Styles
_KEY_OEFF_STYLE_ACTIVE = "dossier_oeff_style_active" # zuletzt benutzte Style-ID (pro typ)
_OEFF_DARSTELLUNGEN = ("einfach", "standard", "detail")
def get_aktive_darstellung(doc):
"""Liest die doc-level Darstellungs-Override. Leer → per-object."""
if doc is None: return ""
try:
v = doc.Strings.GetValue(_KEY_AKTIVE_DARSTELLUNG) or ""
except Exception:
v = ""
return v if v in _OEFF_DARSTELLUNGEN else ""
def set_aktive_darstellung(doc, value):
"""Setzt die doc-level Darstellungs-Override. Leer → clear."""
if doc is None: return
try:
if value and value in _OEFF_DARSTELLUNGEN:
doc.Strings.SetString(_KEY_AKTIVE_DARSTELLUNG, value)
else:
doc.Strings.Delete(_KEY_AKTIVE_DARSTELLUNG)
except Exception as ex:
print("[ELEMENTE] set_aktive_darstellung:", ex)
# ---------------------------------------------------------------------------
# Fenster/Tueren-Styles (Presets) — analog text_create.list_styles
_OEFF_STYLE_FIELDS = (
"typ", "breite", "hoehe", "brueest",
"rahmenB", "rahmenTiefe", "rahmenOffset",
"fluegel", "simsAus", "glas",
"darstellung", "tuerRahmen",
)
_OEFF_DEFAULT_STYLES = [
{"name": "Fenster Standard", "typ": "fenster",
"breite": 1.20, "hoehe": 1.40, "brueest": 0.90,
"rahmenB": 0.06, "rahmenTiefe": 0.08, "rahmenOffset": 0.05,
"fluegel": 2, "simsAus": "standard", "glas": True,
"darstellung": "standard"},
{"name": "Fenster Gross", "typ": "fenster",
"breite": 2.00, "hoehe": 1.80, "brueest": 0.40,
"rahmenB": 0.08, "rahmenTiefe": 0.10, "rahmenOffset": 0.05,
"fluegel": 3, "simsAus": "breit", "glas": True,
"darstellung": "standard"},
{"name": "Fenster Bandlage (boden)", "typ": "fenster",
"breite": 3.00, "hoehe": 0.60, "brueest": 0.00,
"rahmenB": 0.06, "rahmenTiefe": 0.08, "rahmenOffset": 0.05,
"fluegel": 3, "simsAus": "schmal", "glas": True,
"darstellung": "standard"},
{"name": "Tuer Innen", "typ": "tuer",
"breite": 0.90, "hoehe": 2.10, "brueest": 0.00,
"rahmenB": 0.05, "rahmenTiefe": 0.08, "rahmenOffset": 0.05,
"fluegel": 1, "simsAus": "ohne", "glas": False,
"darstellung": "standard", "tuerRahmen": "zarge"},
{"name": "Tuer Eingang", "typ": "tuer",
"breite": 1.00, "hoehe": 2.20, "brueest": 0.00,
"rahmenB": 0.08, "rahmenTiefe": 0.10, "rahmenOffset": 0.05,
"fluegel": 1, "simsAus": "ohne", "glas": False,
"darstellung": "standard", "tuerRahmen": "block"},
{"name": "Tuer Verglast", "typ": "tuer",
"breite": 0.90, "hoehe": 2.10, "brueest": 0.00,
"rahmenB": 0.06, "rahmenTiefe": 0.08, "rahmenOffset": 0.05,
"fluegel": 1, "simsAus": "ohne", "glas": True,
"darstellung": "standard", "tuerRahmen": "zarge"},
]
def _normalize_oeff_style(s):
"""Filtert auf erlaubte Felder + setzt sinnvolle Defaults."""
out = {}
for k in _OEFF_STYLE_FIELDS:
if k in s: out[k] = s[k]
return out
def list_oeff_styles(doc, typ=None):
"""Liste aller Window/Tuer-Styles. typ='fenster'|'tuer' filtert.
Seedet Defaults beim ersten Zugriff."""
if doc is None: return []
import json, uuid
try:
raw = doc.Strings.GetValue(_KEY_OEFF_STYLES)
if not raw:
seeded = []
for i, s in enumerate(_OEFF_DEFAULT_STYLES):
norm = _normalize_oeff_style(s)
norm["id"] = "os_default_" + str(i)
norm["name"] = s["name"]
seeded.append(norm)
try: doc.Strings.SetString(_KEY_OEFF_STYLES, json.dumps(seeded))
except Exception: pass
items = seeded
else:
parsed = json.loads(raw)
items = parsed if isinstance(parsed, list) else []
out = []
for it in items:
if not isinstance(it, dict): continue
n = _normalize_oeff_style(it)
n["id"] = it.get("id") or ("os_" + uuid.uuid4().hex[:8])
n["name"] = it.get("name") or "Stil"
out.append(n)
if typ in ("fenster", "tuer"):
out = [s for s in out if s.get("typ") == typ]
return out
except Exception as ex:
print("[ELEMENTE] list_oeff_styles:", ex)
return []
def save_oeff_style(doc, name, settings):
"""Speichert (oder updated) einen Style unter `name`. Returns ID."""
if doc is None or not name: return None
import json, uuid
items_all = []
try:
raw = doc.Strings.GetValue(_KEY_OEFF_STYLES)
if raw:
parsed = json.loads(raw)
if isinstance(parsed, list):
items_all = [it for it in parsed if isinstance(it, dict)]
except Exception: pass
sid = None
for it in items_all:
if it.get("name") == name:
sid = it.get("id"); break
norm = _normalize_oeff_style(settings or {})
norm["id"] = sid or ("os_" + uuid.uuid4().hex[:8])
norm["name"] = name
if sid:
items_all = [norm if it.get("id") == sid else it for it in items_all]
else:
items_all.append(norm)
try: doc.Strings.SetString(_KEY_OEFF_STYLES, json.dumps(items_all))
except Exception as ex:
print("[ELEMENTE] save_oeff_style:", ex)
return norm["id"]
def delete_oeff_style(doc, sid):
if doc is None or not sid: return
import json
try:
items = list_oeff_styles(doc)
items = [it for it in items if it.get("id") != sid]
doc.Strings.SetString(_KEY_OEFF_STYLES, json.dumps(items))
except Exception: pass
def get_active_oeff_style_id(doc, typ):
if doc is None: return None
try:
raw = doc.Strings.GetValue(_KEY_OEFF_STYLE_ACTIVE + "_" + typ)
return raw or None
except Exception:
return None
def set_active_oeff_style_id(doc, typ, sid):
if doc is None or typ not in ("fenster", "tuer"): return
try:
doc.Strings.SetString(_KEY_OEFF_STYLE_ACTIVE + "_" + typ, sid or "")
except Exception: pass
_OEFF_AUSSENSEITEN = ("links", "rechts")
_OEFF_TUER_RAHMEN = ("zarge", "block")
_OEFF_REFERENZ_OPTIONS = ("mid", "links", "rechts")
@@ -1705,7 +1881,9 @@ def _attach_meta(obj_attrs, wall_id, type_, geschoss, dicke, uk_over, ok_over,
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,
oeff_referenz=None, oeff_darstellung=None,
oeff_aussenseite=None, oeff_tuer_rahmen=None,
oeff_rahmen_offset=None, oeff_style_id=None,
geschoss_end=None, treppe_breite=None,
treppe_n=None, treppe_referenz=None,
treppe_modus=None, treppe_lauf_d=None, treppe_art=None,
@@ -1775,6 +1953,20 @@ def _attach_meta(obj_attrs, wall_id, type_, geschoss, dicke, uk_over, ok_over,
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)
if oeff_darstellung is not None and oeff_darstellung in _OEFF_DARSTELLUNGEN:
obj_attrs.SetUserString(_KEY_OEFF_DARSTELLUNG, oeff_darstellung)
if oeff_aussenseite is not None and oeff_aussenseite in _OEFF_AUSSENSEITEN:
obj_attrs.SetUserString(_KEY_OEFF_AUSSENSEITE, oeff_aussenseite)
if oeff_tuer_rahmen is not None and oeff_tuer_rahmen in _OEFF_TUER_RAHMEN:
obj_attrs.SetUserString(_KEY_OEFF_TUER_RAHMEN, oeff_tuer_rahmen)
if oeff_rahmen_offset is not None:
try:
v = max(0.0, float(oeff_rahmen_offset))
obj_attrs.SetUserString(_KEY_OEFF_RAHMEN_OFFSET, "{:.4f}".format(v))
except Exception: pass
if oeff_style_id is not None:
try: obj_attrs.SetUserString(_KEY_OEFF_STYLE_ID, str(oeff_style_id) or "")
except Exception: pass
# --- Treppen-Felder ---
if geschoss_end is not None:
obj_attrs.SetUserString(_KEY_GESCHOSS_END, geschoss_end or "")
@@ -1925,6 +2117,19 @@ def _read_meta(obj):
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"
odarst = a.GetUserString(_KEY_OEFF_DARSTELLUNG) or "standard"
if odarst not in _OEFF_DARSTELLUNGEN: odarst = "standard"
oauss = a.GetUserString(_KEY_OEFF_AUSSENSEITE) or "rechts"
if oauss not in _OEFF_AUSSENSEITEN: oauss = "rechts"
otrah = a.GetUserString(_KEY_OEFF_TUER_RAHMEN) or "zarge"
if otrah not in _OEFF_TUER_RAHMEN: otrah = "zarge"
# Rahmen-Offset (m, von Wand-Innenseite). Default 5cm. Wenn Legacy-
# Wert (rahmen_pos) gesetzt aber kein offset, benutzt build-Logik
# weiterhin den Preset.
try: oro = float(a.GetUserString(_KEY_OEFF_RAHMEN_OFFSET) or "0.05")
except Exception: oro = 0.05
if oro < 0: oro = 0.0
ostyle = a.GetUserString(_KEY_OEFF_STYLE_ID) or ""
# Treppen-Felder
gend = a.GetUserString(_KEY_GESCHOSS_END) or ""
try: tb = float(a.GetUserString(_KEY_TREPPE_BREITE) or "1.0")
@@ -2041,6 +2246,11 @@ def _read_meta(obj):
"oeff_sims_in": osi,
"oeff_glas": ogl,
"oeff_referenz": oref,
"oeff_darstellung": odarst,
"oeff_aussenseite": oauss,
"oeff_tuer_rahmen": otrah,
"oeff_rahmen_offset": oro,
"oeff_style_id": ostyle,
"geschoss_end": gend,
"treppe_breite": tb,
"treppe_n": tn,
@@ -2281,6 +2491,26 @@ def _make_oeffnung_pieces(axis_curve, point_on_axis, wall_dicke, oeff_meta, base
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")
# Doc-level Darstellungs-Override gewinnt vor per-Object-Setting —
# damit Ausschnitt-Wechsel / Oberleiste-Quick-Switch alle Oeffnungen
# konsistent zwingen koennen.
_doc = Rhino.RhinoDoc.ActiveDoc
global_darst = get_aktive_darstellung(_doc) if _doc is not None else ""
darstellung = global_darst or oeff_meta.get("oeff_darstellung", "standard")
if darstellung not in _OEFF_DARSTELLUNGEN:
darstellung = "standard"
# Aussenseite: +1 = aussen ist +Plane.ZAxis (default), -1 = aussen ist
# -Plane.ZAxis (Wand "umgedreht"). Wird verwendet um Sims aus/in
# konsistent zu platzieren unabhaengig von der Wand-Achsen-Richtung.
aussenseite = oeff_meta.get("oeff_aussenseite", "rechts")
aus_sign = +1 if aussenseite == "rechts" else -1
# Tueren-Rahmen-Typ: 'zarge' (klassisch, sitzt IM Wandquerschnitt) oder
# 'block' (Blockrahmen, sitzt UM die Oeffnung und ragt seitlich raus).
tuer_rahmen = oeff_meta.get("oeff_tuer_rahmen", "zarge")
# Rahmen-Offset (m): Abstand der Rahmen-Innenkante von der Wand-
# Innenseite. 0 = bündig innen, wand_dicke-rahmen_t = bündig aussen.
try: rahmen_offset = max(0.0, float(oeff_meta.get("oeff_rahmen_offset", 0.05)))
except Exception: rahmen_offset = 0.05
half_b = breite * 0.5
half_d = float(wall_dicke) * 0.5
@@ -2301,14 +2531,60 @@ def _make_oeffnung_pieces(axis_curve, point_on_axis, wall_dicke, oeff_meta, base
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)
# Rahmen-Position aus Offset bestimmen. aus_sign=+1 -> Innen ist
# -perp-Seite. Inside-Edge = -half_d + offset. Outside-Edge = inside
# + rahmen_t. Maximaler Offset = wand_dicke - rahmen_t (sonst ragt
# der Rahmen raus). Clamping mit 1mm Inset gegen Z-Fight.
rt = max(0.01, float(rahmen_t))
rt = min(rt, 2.0 * half_d - 0.002)
max_offset = 2.0 * half_d - rt - 0.001
eff_offset = min(max(0.0, rahmen_offset), max(0.0, max_offset))
if aus_sign > 0:
frame_perp_lo = -half_d + eff_offset
frame_perp_hi = frame_perp_lo + rt
else:
frame_perp_hi = +half_d - eff_offset
frame_perp_lo = frame_perp_hi - rt
# --- EINFACH (1:100): nur eine flache Scheibe OHNE Tiefe in der
# Rahmen-Mittelebene (= dort wo das Glas im Standard sitzt). Im
# 2D-Plan ergibt das eine einzelne Linie quer durch die Oeffnung,
# auf dem korrekten Tiefen-Offset.
if darstellung == "einfach":
try:
pane_perp = (frame_perp_lo + frame_perp_hi) * 0.5
# Plane.Origin ans Wand-Achsenpunkt verschoben um pane_perp
# entlang der Plane-Normale (=tan x Z = (0,0,1) cross tan).
normal = rg.Vector3d.CrossProduct(tan, rg.Vector3d(0, 0, 1))
try: normal.Unitize()
except Exception: pass
origin = rg.Point3d(pt.X + normal.X * pane_perp,
pt.Y + normal.Y * pane_perp,
0)
plane = rg.Plane(origin, tan, rg.Vector3d(0, 0, 1))
surf = rg.PlaneSurface(plane,
rg.Interval(-half_b, +half_b),
rg.Interval(z_lo, z_hi))
brep = surf.ToBrep()
return [brep] if brep is not None else []
except Exception as ex:
print("[ELEMENTE] einfach pane:", ex)
return []
pieces = []
# --- RAHMEN: outer box - inner box, sauberer single-Brep
# --- RAHMEN: outer box - inner box, sauberer single-Brep.
# Tueren mit 'block'-Rahmen: outer box ist breiter+hoeher als die
# Oeffnung (Blockrahmen sitzt UM den Wanddurchbruch). Sonst klassische
# Zarge: outer = Oeffnungsmasse.
is_block = (is_tuer and tuer_rahmen == "block")
block_overlap = 0.05 # 5cm seitlich + oben
out_l = (-half_b - block_overlap) if is_block else -half_b
out_r = (+half_b + block_overlap) if is_block else +half_b
out_z_hi = (z_hi + block_overlap) if is_block else z_hi
out_z_lo = z_lo # unten immer bei z_lo (Boden / Bruestung)
try:
outer_box = _make_oeff_box(pt, tan, -half_b, +half_b, z_lo, z_hi,
outer_box = _make_oeff_box(pt, tan, out_l, out_r, out_z_lo, out_z_hi,
frame_perp_lo, frame_perp_hi)
# Inner box leicht laenger in perp Richtung damit der Diff sauber
# durchschneidet (keine Hauchschicht uebrig).
@@ -2348,45 +2624,52 @@ def _make_oeffnung_pieces(axis_curve, point_on_axis, wall_dicke, oeff_meta, base
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)
# DETAIL (1:20): Doppelverglasung — 2 Scheiben a 6mm, 16mm SZR
is_double_glas = (darstellung == "detail" and has_glas and not is_tuer)
if is_double_glas:
single_t = 0.006
szr = 0.016
total = single_t * 2 + szr
outer_lo = fill_mid - total * 0.5
pane_specs = [
(outer_lo, outer_lo + single_t),
(outer_lo + single_t + szr,
outer_lo + single_t + szr + single_t),
]
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)
pane_specs = [(fill_mid - fill_t * 0.5, fill_mid + fill_t * 0.5)]
for (fl, fh) in pane_specs:
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,
fl, fh)
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,
fl, fh)
if fp is not None: pieces.append(fp)
# --- SIMS AUSSEN (+Plane.ZAxis-Seite) — Platte unter der Oeffnung
# --- SIMS — nur AUSSEN. aus_sign=+1 -> sims auf +perp, =-1 -> auf
# -perp. Drinnen nie. Sim ist gleichzeitig der visuelle Indikator
# fuer die Aussenseite (Test: aussenseite togglen → Sim wechselt).
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
if aus_sign > 0:
p_lo, p_hi = +half_d, +half_d + s_pr
else:
p_lo, p_hi = -half_d - s_pr, -half_d
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)
s_lo, z_lo, p_lo, p_hi)
if sb is not None: pieces.append(sb)
return pieces
@@ -4206,7 +4489,12 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
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"))
oeff_referenz=op_meta.get("oeff_referenz"),
oeff_darstellung=op_meta.get("oeff_darstellung"),
oeff_aussenseite=op_meta.get("oeff_aussenseite"),
oeff_tuer_rahmen=op_meta.get("oeff_tuer_rahmen"),
oeff_rahmen_offset=op_meta.get("oeff_rahmen_offset"),
oeff_style_id=op_meta.get("oeff_style_id"))
doc.Objects.AddBrep(pbrep, op_attrs)
# Source-Layer migrieren + Volumen-Layer-Index ermitteln
@@ -4684,6 +4972,18 @@ class ElementeBridge(panel_base.BaseBridge):
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()
elif t == "SAVE_OEFF_STYLE":
try:
doc = Rhino.RhinoDoc.ActiveDoc
save_oeff_style(doc, p.get("name") or "Stil", p.get("settings") or {})
except Exception as ex: print("[ELEMENTE] save oeff style:", ex)
self._send_state()
elif t == "DELETE_OEFF_STYLE":
try:
doc = Rhino.RhinoDoc.ActiveDoc
delete_oeff_style(doc, p.get("id"))
except Exception as ex: print("[ELEMENTE] del oeff style:", ex)
self._send_state()
elif t == "OPEN_ELEMENTE_UEBERSICHT":
try:
import elemente_uebersicht
@@ -4802,6 +5102,11 @@ class ElementeBridge(panel_base.BaseBridge):
"simsIn": meta.get("oeff_sims_in", "ohne"),
"glas": bool(meta.get("oeff_glas", False)),
"oeffReferenz": meta.get("oeff_referenz", "mid"),
"darstellung": meta.get("oeff_darstellung", "standard"),
"aussenseite": meta.get("oeff_aussenseite", "rechts"),
"tuerRahmen": meta.get("oeff_tuer_rahmen", "zarge"),
"rahmenOffset": meta.get("oeff_rahmen_offset", 0.05),
"styleId": meta.get("oeff_style_id", ""),
})
elif meta["type"] == "treppe_axis":
gs = _geschoss_by_id(doc, meta["geschoss"])
@@ -4911,6 +5216,7 @@ class ElementeBridge(panel_base.BaseBridge):
{"name": n, "color": m["color"],
"hatch": m.get("hatch", ""), "scale": m.get("scale", 1.0)}
for n, m in _MATERIAL_LIBRARY.items()],
"oeffStyles": list_oeff_styles(doc),
}
self.send("STATE", payload)
# An Properties-Satellite-Window forwarden falls offen
@@ -5691,6 +5997,37 @@ class ElementeBridge(panel_base.BaseBridge):
try: preview_base_z = float(axis_curve.PointAtStart.Z)
except Exception: preview_base_z = 0.0
# Entwurfs-Defaults pro Typ — VOR der Loop damit der Stil-Picker
# sie ueberschreiben kann.
is_fenster = (typ == "fenster")
rahmen_b_def = _last("oeff_rahmen_b", 0.06)
rahmen_t_def = _last("oeff_rahmen_tiefe", 0.08)
rahmen_offset_def = _last("oeff_rahmen_offset", 0.05)
fluegel_def = _last("{}_fluegel".format(typ), 2 if is_fenster else 1)
simsa_def = "standard" if is_fenster else "ohne"
glas_def = is_fenster
referenz_def = _last("oeff_referenz", "mid")
darst_def = "standard"
tuer_rahmen_def = "zarge"
# Pending-Style-ID aus sticky (von Stil-Picker gesetzt). Falls noch
# kein Style gepickt, nutzen wir den zuletzt-aktiven fuer diesen typ
# als Default-Source.
active_sid = get_active_oeff_style_id(doc, typ)
if active_sid:
for s in list_oeff_styles(doc, typ):
if s.get("id") == active_sid:
if "rahmenB" in s: rahmen_b_def = float(s["rahmenB"])
if "rahmenTiefe" in s: rahmen_t_def = float(s["rahmenTiefe"])
if "rahmenOffset" in s: rahmen_offset_def = float(s["rahmenOffset"])
if "fluegel" in s: fluegel_def = int(s["fluegel"])
if "simsAus" in s: simsa_def = s["simsAus"]
if "glas" in s: glas_def = bool(s["glas"])
if "darstellung" in s: darst_def = s["darstellung"]
if typ == "tuer" and "tuerRahmen" in s:
tuer_rahmen_def = s["tuerRahmen"]
break
pending_sid = active_sid or ""
# 2) Punkt auf der Achse — constrained an die Wand-Achse
try:
while True:
@@ -5701,8 +6038,12 @@ class ElementeBridge(panel_base.BaseBridge):
prompt += ", Br={:.2f}".format(brueest)
prompt += "]"
gp.SetCommandPrompt(prompt)
try: gp.Constrain(axis_curve, False)
except Exception: pass
# KEINE Constrain mehr — der User soll perpendicular zur
# Achse klicken koennen damit wir die Aussenseite ableiten
# koennen. Position wird via ClosestPoint auf die Achse
# projiziert. Das DynamicDraw-Preview projiziert intern
# auch, der Quader sitzt also weiterhin sauber auf der
# Achse — nur die Cursor-Position bleibt frei.
# Live-Preview: gruener Oeffnungs-Quader mit Glas-Diagonalen,
# Brueest-Marker und Mass-Label oberhalb des Sturzes
try:
@@ -5717,9 +6058,47 @@ class ElementeBridge(panel_base.BaseBridge):
opt_b = gp.AddOption("Breite")
opt_h = gp.AddOption("Hoehe")
opt_br = gp.AddOption("Bruestung") if typ == "fenster" else None
# Stil-Picker: zeigt verfuegbare Styles als Sub-Optionen
opt_st = gp.AddOption("Stil")
rp = gp.Get()
if rp == GetResult.Option:
idx = gp.OptionIndex()
if idx == opt_st:
styles = list_oeff_styles(doc, typ)
if not styles:
print("[ELEMENTE] Keine Styles fuer {}".format(typ))
else:
try:
go = ric.GetOption()
go.SetCommandPrompt("Stil waehlen")
opt_map = []
for s in styles:
safe = s["name"].replace(" ", "_")
opt_map.append((go.AddOption(safe), s))
if go.Get() == GetResult.Option:
oi = go.OptionIndex()
chosen = next((s for (i, s) in opt_map
if i == oi), None)
if chosen is not None:
if "breite" in chosen: breite = float(chosen["breite"])
if "hoehe" in chosen: hoehe = float(chosen["hoehe"])
if typ == "fenster" and "brueest" in chosen:
brueest = float(chosen["brueest"])
if "rahmenB" in chosen: rahmen_b_def = float(chosen["rahmenB"])
if "rahmenTiefe" in chosen: rahmen_t_def = float(chosen["rahmenTiefe"])
if "rahmenOffset" in chosen: rahmen_offset_def = float(chosen["rahmenOffset"])
if "fluegel" in chosen: fluegel_def = int(chosen["fluegel"])
if "simsAus" in chosen: simsa_def = chosen["simsAus"]
if "glas" in chosen: glas_def = bool(chosen["glas"])
if "darstellung" in chosen: darst_def = chosen["darstellung"]
if typ == "tuer" and "tuerRahmen" in chosen:
tuer_rahmen_def = chosen["tuerRahmen"]
pending_sid = chosen["id"]
set_active_oeff_style_id(doc, typ, chosen["id"])
print("[ELEMENTE] Stil '{}' geladen".format(chosen["name"]))
except Exception as ex:
print("[ELEMENTE] Stil-Picker:", ex)
continue
if idx == opt_b:
gn = ric.GetNumber()
gn.SetCommandPrompt("Breite")
@@ -5753,6 +6132,22 @@ class ElementeBridge(panel_base.BaseBridge):
except Exception as ex:
print("[ELEMENTE] ClosestPoint:", ex); return
# Aussenseite aus Click-Richtung ableiten: Vektor on_axis→click_pt
# mit der Perp-Richtung der Achse vergleichen. Vorzeichen entscheidet
# ob aussen=+perp (=rechts) oder aussen=-perp (=links). Bei Klick
# direkt auf der Achse: Default 'rechts'.
detected_aussen = "rechts"
try:
tan_at = axis_curve.TangentAt(t)
perp = rg.Vector3d.CrossProduct(tan_at, rg.Vector3d(0, 0, 1))
dx = click_pt.X - on_axis.X
dy = click_pt.Y - on_axis.Y
side = perp.X * dx + perp.Y * dy
if side < -1e-6: detected_aussen = "links"
elif side > 1e-6: detected_aussen = "rechts"
except Exception as ex:
print("[ELEMENTE] aussenseite detect:", ex)
# Point-Objekt mit Metadaten anlegen
prefix = "fenster_" if typ == "fenster" else "tuer_"
oeff_id = prefix + uuid.uuid4().hex[:10]
@@ -5761,17 +6156,6 @@ class ElementeBridge(panel_base.BaseBridge):
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,
@@ -5781,12 +6165,16 @@ class ElementeBridge(panel_base.BaseBridge):
oeff_brueest=brueest,
oeff_rahmen_b=rahmen_b_def,
oeff_rahmen_tiefe=rahmen_t_def,
oeff_rahmen_pos=rahmen_p_def,
oeff_rahmen_offset=rahmen_offset_def,
oeff_fluegel=fluegel_def,
oeff_sims_aus=simsa_def,
oeff_sims_in=simsi_def,
oeff_sims_in="ohne",
oeff_glas=glas_def,
oeff_referenz=referenz_def)
oeff_referenz=referenz_def,
oeff_aussenseite=detected_aussen,
oeff_darstellung=darst_def,
oeff_tuer_rahmen=tuer_rahmen_def,
oeff_style_id=pending_sid)
# Oeffnungs-Punkt auf UK+Brueestung-Hoehe platzieren (= visuell auf
# Unterkante Oeffnung). Constraint vergleicht spaeter pt.Z mit
# UK+brueest — wenn der Punkt am axis.Z=0 saesse, wuerde der erste
@@ -8210,6 +8598,41 @@ class ElementeBridge(panel_base.BaseBridge):
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"
odarst = p.get("darstellung", old_meta.get("oeff_darstellung", "standard"))
if odarst not in _OEFF_DARSTELLUNGEN: odarst = "standard"
oauss = p.get("aussenseite", old_meta.get("oeff_aussenseite", "rechts"))
if oauss not in _OEFF_AUSSENSEITEN: oauss = "rechts"
otrah = p.get("tuerRahmen", old_meta.get("oeff_tuer_rahmen", "zarge"))
if otrah not in _OEFF_TUER_RAHMEN: otrah = "zarge"
try: oro = float(p.get("rahmenOffset",
old_meta.get("oeff_rahmen_offset", 0.05)))
except Exception: oro = 0.05
if oro < 0: oro = 0.0
# Style-Apply: wenn ein styleId im Patch ist, alle Felder
# aus dem Style ueberschreiben (User-Patch in derselben
# Operation gewinnt aber ueber Style — sonst koennte ein
# Stil das eben gemachte Field-Edit wegmaecken).
o_style_id = p.get("styleId",
old_meta.get("oeff_style_id", "")) or ""
if p.get("styleId") and p.get("styleId") != old_meta.get("oeff_style_id"):
# Style wurde NEU gewaehlt → seine Werte applizieren
styles = list_oeff_styles(doc)
stl = next((s for s in styles if s.get("id") == p["styleId"]), None)
if stl is not None and stl.get("typ") == otyp:
if "breite" in stl: breite = float(stl["breite"])
if "hoehe" in stl: hoehe = float(stl["hoehe"])
if otyp == "fenster" and "brueest" in stl:
brueest = float(stl["brueest"])
if "rahmenB" in stl: rahmen_b = float(stl["rahmenB"])
if "rahmenTiefe" in stl: rahmen_t = float(stl["rahmenTiefe"])
if "rahmenOffset" in stl: oro = float(stl["rahmenOffset"])
if "fluegel" in stl: fluegel = int(stl["fluegel"])
if "simsAus" in stl: simsa = stl["simsAus"]
if "glas" in stl: glas = bool(stl["glas"])
if "darstellung" in stl: odarst = stl["darstellung"]
if otyp == "tuer" and "tuerRahmen" in stl:
otrah = stl["tuerRahmen"]
set_active_oeff_style_id(doc, otyp, p["styleId"])
attrs = axis_obj.Attributes
_attach_meta(attrs, wall_id, "oeffnung_point",
old_meta["geschoss"], old_meta["dicke"],
@@ -8224,7 +8647,12 @@ class ElementeBridge(panel_base.BaseBridge):
oeff_fluegel=fluegel,
oeff_sims_aus=simsa, oeff_sims_in=simsi,
oeff_glas=glas,
oeff_referenz=oref)
oeff_referenz=oref,
oeff_darstellung=odarst,
oeff_aussenseite=oauss,
oeff_tuer_rahmen=otrah,
oeff_rahmen_offset=oro,
oeff_style_id=o_style_id)
axis_obj.Attributes = attrs
axis_obj.CommitChanges()
parent_id = old_meta.get("oeff_parent", "")
@@ -8345,6 +8773,28 @@ class ElementeBridge(panel_base.BaseBridge):
self._send_state()
def regenerate_all_oeffnungen(doc):
"""Modul-API: regen aller Oeffnungen + ihrer Parent-Waende. Wird vom
Ausschnitt-Restore / Oberleiste-Darstellungs-Switch gerufen damit
der doc-level Darstellungs-Override sofort wirkt."""
if doc is None: return 0
seen_walls = set()
n = 0
for obj in list(doc.Objects):
meta = _read_meta(obj)
if meta is None: continue
if meta.get("type") != "oeffnung_point": continue
parent = meta.get("oeff_parent") or ""
if parent and parent not in seen_walls:
seen_walls.add(parent)
_regenerate_element(doc, parent)
n += 1
try: doc.Views.Redraw()
except Exception: pass
print("[ELEMENTE] regen all oeffnungen via {} Waende".format(n))
return n
# --- Event-Listener ---------------------------------------------------------
# Re-Entry-Guard: wenn _regenerate_volume die Brep ersetzt, feuert das