Oeffnungen-Sublayer + Sturzlinien + Referenz-Layer + Pill-Inputs + Anordnen-Pill

- Oeffnungen-Subtree (Rahmen/Glas/Tuerblatt/Sims/Pane/Schwung/Sturz) als
  nested Children unter WAENDE im dossier_ebenen-Tree registriert + per-Kind
  Material (Glas mit Transparenz)
- Sturzlinien bei 1:100 Tueren mit Innen/Aussen/Beide/Keine-Dropdown
- Referenzlinien-Layer (19) als eigene Ebene fuer wand_axis + oeffnung_point
- Swisstopo Patch-Terrain (Brep.CreatePatch) ersetzt das falsche Loft
- Pill-Style fuer alle Inputs zentral via index.css
- 2x2 Anordnen-Pill in der Oberleiste (BringToFront/Forward/Backward/SendToBack
  via Rhinos DisplayOrder, kein Z-Offset)
- Chevron-Verschiebung in Ebenen-Panel ohne dass Siblings shiften
- Fix: _update_ebene_field walked nur Top-Level, nested Sublayer-Style-
  Changes wurden nicht persistiert
- Fix: Sturz-Linetype wurde bei jedem Wand-Regen zurueckgesetzt

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 16:07:44 +02:00
parent 3dc6e31374
commit 3277f61ced
12 changed files with 803 additions and 80 deletions
+559 -58
View File
@@ -71,31 +71,38 @@ _KEY_OEFF_TUER_TYP = "dossier_oeff_tuer_typ" # "normal" | "wandoeffnu
_KEY_OEFF_HINGE_SIDE = "dossier_oeff_hinge_side" # "links" | "rechts" — Bandseite (welche Tuerflueg-Seite)
_KEY_OEFF_OPEN_ANGLE = "dossier_oeff_open_angle" # float Grad 0180 (Plan-Oeffnungswinkel)
_KEY_OEFF_SWING_INVERT = "dossier_oeff_swing_invert" # "1"/"0" — flippt die Schwung-Richtung
_KEY_OEFF_STURZ = "dossier_oeff_sturz" # "keine" | "innen" | "aussen" | "beide" — Sturzlinien-Anzeige bei 1:100 Tueren
_OEFF_TUER_TYPEN = ("normal", "wandoeffnung")
_OEFF_HINGE_SIDES = ("links", "rechts")
_OEFF_STURZ_OPTIONS = ("keine", "innen", "aussen", "beide")
_OEFF_DARSTELLUNGEN = ("einfach", "standard", "detail")
_OEFF_DARSTELLUNGEN = ("auto", "einfach", "standard", "detail")
# 'auto' = folgt der Doc-Level-Modelldarstellung (Oberleiste). Default
# fuer neue Elemente. Explizite Werte ueberstuern den Doc-Level-Wert.
_DARSTELLUNG_DEFAULT_GLOBAL = "einfach" # Doc-Level Default = 1:100
def get_aktive_darstellung(doc):
"""Liest die doc-level Darstellungs-Override. Leer → per-object."""
if doc is None: return ""
"""Liest die doc-level Modelldarstellung. Default = einfach (1:100).
'auto' wird hier nicht akzeptiert die Doc-Ebene hat IMMER einen
konkreten Wert, an dem sich auto-Elemente orientieren."""
if doc is None: return _DARSTELLUNG_DEFAULT_GLOBAL
try:
v = doc.Strings.GetValue(_KEY_AKTIVE_DARSTELLUNG) or ""
except Exception:
v = ""
return v if v in _OEFF_DARSTELLUNGEN else ""
if v in ("einfach", "standard", "detail"): return v
return _DARSTELLUNG_DEFAULT_GLOBAL
def set_aktive_darstellung(doc, value):
"""Setzt die doc-level Darstellungs-Override. Leer → clear."""
"""Setzt die doc-level Modelldarstellung. Akzeptiert nur konkrete
Werte (einfach/standard/detail). Leer/None/auto Default einfach."""
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)
v = value if value in ("einfach", "standard", "detail") else _DARSTELLUNG_DEFAULT_GLOBAL
doc.Strings.SetString(_KEY_AKTIVE_DARSTELLUNG, v)
except Exception as ex:
print("[ELEMENTE] set_aktive_darstellung:", ex)
@@ -414,6 +421,30 @@ _OEFF_SIMS_STYLES = {
}
_OEFF_RAHMEN_POS_OPTIONS = ("aussen", "mid", "innen")
# Pro Oeffnungs-Piece-Kind: eigener Sublayer unter `WAENDE::Öffnungen` +
# zugehoeriges Material (Hex-Diffuse + optional Transparenz). 'name' wird
# als Layer-Name benutzt. 'suffix' wird an den WAENDE-Code +"o" angehaengt
# (z.B. wand_code="20" -> Öffnungen-Ebene "20o" -> Rahmen "20o1") damit der
# Eintrag im dossier_ebenen-Tree eine eindeutige Code-ID bekommt und im
# Ebenen-Panel erscheint. transparency: 0.01.0.
_OEFF_PIECE_DEFS = {
"rahmen": {"layer": "Rahmen", "suffix": "1", "color": "#c89a5a",
"transparency": 0.0, "ior": 1.0},
"glas": {"layer": "Glas", "suffix": "2", "color": "#bcd4e0",
"transparency": 0.88, "ior": 1.5},
"fluegel": {"layer": "Türblatt", "suffix": "3", "color": "#8a6440",
"transparency": 0.0, "ior": 1.0},
"sims": {"layer": "Sims", "suffix": "4", "color": "#a8a8a8",
"transparency": 0.0, "ior": 1.0},
"pane": {"layer": "Pane", "suffix": "5", "color": "#404040",
"transparency": 0.0, "ior": 1.0},
"schwung": {"layer": "Schwung", "suffix": "6", "color": "#888888",
"transparency": 0.0, "ior": 1.0},
"sturz": {"layer": "Sturz", "suffix": "7", "color": "#6a6a6a",
"transparency": 0.0, "ior": 1.0,
"linetype": "Dashed"},
}
# --- Last-Used-Defaults (sticky, session-life) ------------------------------
# Speichert die letzten Werte (Dicke, Referenz, Modus, Neigung), damit der
@@ -614,15 +645,180 @@ def _layer_path_volume(doc, geschoss_name):
return _layer_path_axis(doc, geschoss_name)
def _layer_path_oeff_swing(doc, geschoss_name):
"""Türschwung-Linien — eigener Sublayer (Code 23) damit User die
Schwung-Bögen unabhängig von den Türen-Volumes ausblenden kann."""
sub = _find_ebene_sublayer_name(doc, ["schwung", "tuerschwung"],
"23", "Türschwung",
default_color="#888888", default_lw=0.13)
def _layer_path_referenz(doc, geschoss_name):
"""Sublayer 'Referenzlinien' (Code 19) — eigene Ebene fuer wand_axis +
oeffnung_point Source-Objekte. Getrennt vom Wand-Volumen-Layer (20)
damit der User die Konstruktions-Referenzen ein-/ausblenden kann ohne
die Volumen-Sichtbarkeit zu verlieren. Wird automatisch im Ebenen-
Panel sichtbar (auto-add via _find_ebene_sublayer_name)."""
sub = _find_ebene_sublayer_name(doc, ["referenz", "referenzlinie"],
"19", "Referenzlinien",
default_color="#a0a0a0", default_lw=0.13)
return "{}::{}".format(geschoss_name, sub)
def _ensure_oeff_ebenen_in_doc(doc):
"""Registriert die Oeffnungen-Ebene + alle Piece-Children im
`dossier_ebenen`-JSON-Tree (als Children der WAENDE-Ebene). Damit
erscheinen die Sublayer im Dossier-Ebenen-Panel und koennen vom User
visible/locked geschaltet werden.
Returnt den WAENDE-Code (z.B. "20") oder None wenn die WAENDE-Ebene
nicht im JSON-Tree liegt. Triggert build_layers + broadcast_state
wenn etwas geaendert wurde."""
raw = doc.Strings.GetValue("dossier_ebenen")
try: ebenen = json.loads(raw) if raw else []
except Exception: ebenen = []
if not isinstance(ebenen, list): return None
wand_eb = None
for e in ebenen:
if not isinstance(e, dict): continue
nm = (e.get("name") or "").lower()
if "wand" in nm or "wände" in nm or "waende" in nm:
wand_eb = e; break
if wand_eb is None: return None
wand_code = wand_eb.get("code") or "20"
if not isinstance(wand_eb.get("children"), list):
wand_eb["children"] = []
oeff_code = wand_code + "o"
oeff_eb = next((c for c in wand_eb["children"] if isinstance(c, dict)
and c.get("code") == oeff_code), None)
changed = False
if oeff_eb is None:
oeff_eb = {
"code": oeff_code, "name": "Öffnungen",
"color": "#888888", "lw": 0.13,
"visible": True, "locked": False, "children": [],
}
wand_eb["children"].append(oeff_eb)
changed = True
if not isinstance(oeff_eb.get("children"), list):
oeff_eb["children"] = []
have_codes = {c.get("code") for c in oeff_eb["children"]
if isinstance(c, dict)}
for kind, spec in _OEFF_PIECE_DEFS.items():
ccode = oeff_code + spec["suffix"]
if ccode in have_codes: continue
oeff_eb["children"].append({
"code": ccode, "name": spec["layer"],
"color": spec["color"], "lw": 0.13,
"visible": True, "locked": False,
})
changed = True
if changed:
try:
doc.Strings.SetString("dossier_ebenen",
json.dumps(ebenen, ensure_ascii=False))
import layer_builder
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
zlist = json.loads(z_raw) if z_raw else []
if zlist: layer_builder.build_layers(doc, zlist, ebenen)
import rhinopanel
rhinopanel._broadcast_state(doc)
except Exception as ex:
print("[ELEMENTE] _ensure_oeff_ebenen_in_doc build:", ex)
return wand_code
def _layer_path_oeff_kind(doc, geschoss_name, kind):
"""Sublayer pro Oeffnungs-Piece-Kind: `<Geschoss>::20_Wände::20o_Öffnungen::20oN_<Kind>`.
Registriert die Ebenen im dossier_ebenen-Tree (damit das Dossier-
Ebenen-Panel sie zeigt) + erstellt die Rhino-Layer-Hierarchie. Liefert
den vollen Layer-Pfad."""
spec = _OEFF_PIECE_DEFS.get(kind)
if spec is None:
return _layer_path_volume(doc, geschoss_name)
# Sicherstellen dass WAENDE in dossier_ebenen ist (auto-add via
# _find_ebene_sublayer_name) + Code extrahieren
wand_sub_name = _find_ebene_sublayer_name(doc, ["wand", "wände", "waende"],
"20", "Wände",
default_color="#0a0a0a",
default_lw=0.50)
# Oeffnungen + 6 Kind-Sublayer in dossier_ebenen registrieren
wand_code = _ensure_oeff_ebenen_in_doc(doc) or "20"
oeff_code = wand_code + "o"
kind_code = oeff_code + spec["suffix"]
# Rhino-Layer-Namen folgen dem `<code>_<name>`-Schema von layer_builder
full = "{}::{}::{}::{}".format(
geschoss_name, wand_sub_name,
"{}_{}".format(oeff_code, "Öffnungen"),
"{}_{}".format(kind_code, spec["layer"]))
layer_idx = _ensure_layer(doc, full)
# Linetype-Default (z.B. 'Dashed' fuer Sturzlinien) — NUR setzen wenn
# der Layer aktuell auf Continuous (Index 0) steht. Sonst hat der User
# bewusst eine andere Linetype gewaehlt und wir wuerden sie bei jedem
# Regen wegmaecken.
lt_name = spec.get("linetype")
if lt_name and layer_idx >= 0:
try:
layer = doc.Layers[layer_idx]
if layer.LinetypeIndex <= 0: # 0 = Continuous, <0 = unset
lt_idx = doc.Linetypes.Find(lt_name)
if lt_idx > 0:
layer.LinetypeIndex = lt_idx
doc.Layers.Modify(layer, layer_idx, True)
except Exception: pass
return full
def _layer_path_oeff_swing(doc, geschoss_name):
"""Türschwung-Linien — Sublayer 'Schwung' unter WAENDE::Öffnungen.
Eigene Ebene damit User die Schwung-Bögen unabhaengig ausblenden kann."""
return _layer_path_oeff_kind(doc, geschoss_name, "schwung")
def _ensure_oeff_material(doc, kind):
"""Findet oder erstellt ein Material fuer ein Oeffnungs-Piece-Kind.
Unterstuetzt Transparenz (fuer Glas) + Index of Refraction. Cached
pro Kind ueber sc.sticky. Liefert Material-Index oder -1.
Defensiv geschrieben bei alten/neuen Rhino-Versionen koennen einzelne
Material-Properties fehlen, wir setzen nur was geht."""
spec = _OEFF_PIECE_DEFS.get(kind)
if spec is None: return -1
cache = sc.sticky.get("_dossier_oeff_mat_cache")
if not isinstance(cache, dict):
cache = {}
sc.sticky["_dossier_oeff_mat_cache"] = cache
cached = cache.get(kind)
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
del cache[kind]
try:
import System.Drawing as SD
h = spec["color"].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_Oeff_" + spec["layer"]
mat.DiffuseColor = SD.Color.FromArgb(255, r, g, b)
try: mat.Transparency = float(spec.get("transparency", 0.0))
except Exception: pass
try: mat.IndexOfRefraction = float(spec.get("ior", 1.0))
except Exception: pass
# Glas zusaetzlich: TransparentColor leicht bluestichig damit Render
# nicht komplett klar wird (sonst sieht es im Standard-Display wie
# ein Loch aus). Optional weiches Reflektions-Highlight.
if kind == "glas":
try: mat.TransparentColor = SD.Color.FromArgb(255, 220, 235, 245)
except Exception: pass
try: mat.Shine = 0.7 * mat.MaxShine
except Exception: pass
try: mat.Reflectivity = 0.05
except Exception: pass
idx = doc.Materials.Add(mat)
if idx >= 0:
cache[kind] = idx
return idx
except Exception as ex:
print("[ELEMENTE] _ensure_oeff_material:", ex)
return -1
def _layer_path_decke(doc, geschoss_name):
"""Decken-Outline + Volumen — Sublayer 'DECKEN' (Code 30)."""
sub = _find_ebene_sublayer_name(doc, ["decke"], "30", "DECKEN",
@@ -1909,7 +2105,7 @@ def _attach_meta(obj_attrs, wall_id, type_, geschoss, dicke, uk_over, ok_over,
oeff_aussenseite=None, oeff_tuer_rahmen=None,
oeff_rahmen_offset=None, oeff_style_id=None,
oeff_tuer_typ=None, oeff_hinge_side=None, oeff_open_angle=None,
oeff_swing_invert=None,
oeff_swing_invert=None, oeff_sturz=None,
geschoss_end=None, treppe_breite=None,
treppe_n=None, treppe_referenz=None,
treppe_modus=None, treppe_lauf_d=None, treppe_art=None,
@@ -2006,6 +2202,9 @@ def _attach_meta(obj_attrs, wall_id, type_, geschoss, dicke, uk_over, ok_over,
try: obj_attrs.SetUserString(_KEY_OEFF_SWING_INVERT,
"1" if bool(oeff_swing_invert) else "0")
except Exception: pass
if oeff_sturz is not None and oeff_sturz in _OEFF_STURZ_OPTIONS:
try: obj_attrs.SetUserString(_KEY_OEFF_STURZ, oeff_sturz)
except Exception: pass
# --- Treppen-Felder ---
if geschoss_end is not None:
obj_attrs.SetUserString(_KEY_GESCHOSS_END, geschoss_end or "")
@@ -2156,8 +2355,8 @@ 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"
odarst = a.GetUserString(_KEY_OEFF_DARSTELLUNG) or "auto"
if odarst not in _OEFF_DARSTELLUNGEN: odarst = "auto"
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"
@@ -2178,6 +2377,8 @@ def _read_meta(obj):
if oangle < 0: oangle = 0.0
elif oangle > 180: oangle = 180.0
oswinv = (a.GetUserString(_KEY_OEFF_SWING_INVERT) == "1")
ostz = a.GetUserString(_KEY_OEFF_STURZ) or "beide"
if ostz not in _OEFF_STURZ_OPTIONS: ostz = "beide"
# Treppen-Felder
gend = a.GetUserString(_KEY_GESCHOSS_END) or ""
try: tb = float(a.GetUserString(_KEY_TREPPE_BREITE) or "1.0")
@@ -2303,6 +2504,7 @@ def _read_meta(obj):
"oeff_hinge_side": ohinge,
"oeff_open_angle": oangle,
"oeff_swing_invert": oswinv,
"oeff_sturz": ostz,
"geschoss_end": gend,
"treppe_breite": tb,
"treppe_n": tn,
@@ -2523,6 +2725,17 @@ def _resolve_rahmen_perp_range(half_d, rahmen_tiefe, rahmen_pos):
return lo, hi
def _resolve_oeff_darstellung(oeff_meta):
"""Per-Object-Setting wenn != 'auto', sonst Doc-Level-Modelldarstellung.
Ergibt immer einen konkreten Wert (einfach/standard/detail)."""
per_obj = oeff_meta.get("oeff_darstellung", "auto")
if per_obj not in _OEFF_DARSTELLUNGEN: per_obj = "auto"
if per_obj != "auto" and per_obj in ("einfach", "standard", "detail"):
return per_obj
doc = Rhino.RhinoDoc.ActiveDoc
return get_aktive_darstellung(doc) if doc is not None else _DARSTELLUNG_DEFAULT_GLOBAL
def _make_tuer_swing_curves(axis_curve, point_on_axis, wall_dicke,
oeff_meta, base_z):
"""Generiert die 2D-Plan-Schwung-Linien einer Tuer:
@@ -2542,7 +2755,7 @@ def _make_tuer_swing_curves(axis_curve, point_on_axis, wall_dicke,
pt, tan, perp = frame
breite = float(oeff_meta.get("oeff_breite", 0.9))
rahmen_b = float(oeff_meta.get("oeff_rahmen_b", 0.06))
darstellung = oeff_meta.get("oeff_darstellung", "standard")
darstellung = _resolve_oeff_darstellung(oeff_meta)
aussenseite = oeff_meta.get("oeff_aussenseite", "rechts")
aus_sign = +1 if aussenseite == "rechts" else -1
hinge_side = oeff_meta.get("oeff_hinge_side", "links")
@@ -2644,11 +2857,72 @@ def _make_tuer_swing_curves(axis_curve, point_on_axis, wall_dicke,
return curves
def _make_tuer_sturz_curves(axis_curve, point_on_axis, wall_dicke,
oeff_meta, base_z):
"""Sturzlinien (Lintel-Projektion) bei 1:100 Tueren — zeigt die
Aussen-/Innen-Kante des Wand-Sturzes als gestrichelte Linie quer ueber
die Oeffnung. Klassische SIA-Plan-Konvention: 'Was oberhalb der
Schnitt-Ebene liegt, wird gestrichelt projiziert'.
Steuerung via oeff_meta['oeff_sturz']:
'keine' keine Linien
'innen' eine Linie an der Wand-Innenseite
'aussen' eine Linie an der Wand-Aussenseite
'beide' beide Linien (Default)
Liefert Liste von LineCurves (0, 1 oder 2 Stueck). Plan-Z = base_z
(= OKFF des Geschosses fuer die Tuer)."""
sturz_mode = oeff_meta.get("oeff_sturz", "beide")
if sturz_mode not in _OEFF_STURZ_OPTIONS or sturz_mode == "keine":
return []
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))
aus_sign = +1 if oeff_meta.get("oeff_aussenseite", "rechts") == "rechts" else -1
half_b = breite * 0.5
half_d = float(wall_dicke) * 0.5
z0 = float(base_z)
# Wand-Achse → 2 Perp-Offsets fuer Aussen-/Innenkante.
# perp_outside_dir = (+aus_sign) * (perp_unit_x, perp_unit_y)
perp_x = -tan.Y
perp_y = +tan.X
outside_perp = +half_d
inside_perp = -half_d
if aus_sign < 0:
outside_perp, inside_perp = inside_perp, outside_perp
# Pro Seite ein LineCurve: links nach rechts entlang tan, am jeweiligen
# perp-Offset (Aussen oder Innen) der Wand.
def _line_at(perp_off):
pl = rg.Point3d(pt.X + tan.X * (-half_b) + perp_x * perp_off,
pt.Y + tan.Y * (-half_b) + perp_y * perp_off, z0)
pr = rg.Point3d(pt.X + tan.X * (+half_b) + perp_x * perp_off,
pt.Y + tan.Y * (+half_b) + perp_y * perp_off, z0)
try: return rg.LineCurve(pl, pr)
except Exception: return None
out = []
if sturz_mode in ("aussen", "beide"):
c = _line_at(outside_perp)
if c is not None: out.append(c)
if sturz_mode in ("innen", "beide"):
c = _line_at(inside_perp)
if c is not None: out.append(c)
return out
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."""
via Boolean-Differenz), Mittelpfosten (pro Fluegel), Glas, Sims aussen.
Liefert eine Liste von (brep, kind)-Tupeln. Kind ist einer von
'rahmen' / 'glas' / 'fluegel' / 'sims' / 'pane' wird vom Caller fuer
Layer-Routing (`_layer_path_oeff_kind`) + Material-Assignment
(`_ensure_oeff_material`) genutzt. Caller persistiert jedes als eigenes
'oeffnung_volume' Object mit der gleichen Oeffnungs-ID + dossier_oeff_piece
UserString."""
frame = _oeff_axis_frame(axis_curve, point_on_axis)
if frame is None: return []
pt, tan, perp = frame
@@ -2664,14 +2938,7 @@ 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"
darstellung = _resolve_oeff_darstellung(oeff_meta)
# 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.
@@ -2723,7 +2990,11 @@ def _make_oeffnung_pieces(axis_curve, point_on_axis, wall_dicke, oeff_meta, base
# 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.
# Bei Tueren ist diese Pane nicht gewuenscht — eine Tuere im Plan
# zeigt nur den Wand-Cutout + Schwung-Bogen, keine flache Scheibe.
if darstellung == "einfach":
if is_tuer:
return []
try:
pane_perp = (frame_perp_lo + frame_perp_hi) * 0.5
# Plane.Origin ans Wand-Achsenpunkt verschoben um pane_perp
@@ -2739,7 +3010,7 @@ def _make_oeffnung_pieces(axis_curve, point_on_axis, wall_dicke, oeff_meta, base
rg.Interval(-half_b, +half_b),
rg.Interval(z_lo, z_hi))
brep = surf.ToBrep()
return [brep] if brep is not None else []
return [(brep, "pane")] if brep is not None else []
except Exception as ex:
print("[ELEMENTE] einfach pane:", ex)
return []
@@ -2768,13 +3039,14 @@ def _make_oeffnung_pieces(axis_curve, point_on_axis, wall_dicke, oeff_meta, base
diff = rg.Brep.CreateBooleanDifference(
[outer_box], [inner_box], 0.001)
if diff and len(diff) > 0:
pieces.append(diff[0])
pieces.append((diff[0], "rahmen"))
else:
pieces.append(outer_box)
pieces.append((outer_box, "rahmen"))
except Exception as ex:
print("[ELEMENTE] Rahmen BoolDiff:", ex)
# --- Mittelpfosten (Fluegel > 1): kleine Stege im inneren Bereich
# --- Mittelpfosten (Fluegel > 1): kleine Stege im inneren Bereich.
# Gleiche Material-Klasse wie Rahmen → 'rahmen'-Kind.
if fluegel > 1:
span = inner_r - inner_l
for i in range(1, fluegel):
@@ -2783,7 +3055,7 @@ def _make_oeffnung_pieces(axis_curve, point_on_axis, wall_dicke, oeff_meta, base
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)
if mp is not None: pieces.append((mp, "rahmen"))
# --- INNERE FUELLUNG: Glas (Fenster oder verglaste Tuer) ODER
# Tuerblatt (massive Tuere ohne Glas). Beides als Box-Brep pro Fluegel.
@@ -2795,7 +3067,10 @@ def _make_oeffnung_pieces(axis_curve, point_on_axis, wall_dicke, oeff_meta, base
else:
fill_t = 0 # nichts (z.B. Fenster ohne Glas)
if fill_t > 0:
# Kind der Fuellung: 'glas' fuer Scheiben (Fenster oder verglaste Tuer),
# 'fluegel' fuer massive Tuerblaetter.
fill_kind = "glas" if has_glas else ("fluegel" if is_tuer else None)
if fill_t > 0 and fill_kind is not None:
fill_mid = (frame_perp_lo + frame_perp_hi) * 0.5
# DETAIL (1:20): Doppelverglasung — 2 Scheiben a 6mm, 16mm SZR
is_double_glas = (darstellung == "detail" and has_glas and not is_tuer)
@@ -2822,12 +3097,12 @@ def _make_oeffnung_pieces(axis_curve, point_on_axis, wall_dicke, oeff_meta, base
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)
if fp is not None: pieces.append((fp, fill_kind))
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)
if fp is not None: pieces.append((fp, fill_kind))
# --- SIMS — nur AUSSEN. aus_sign=+1 -> sims auf +perp, =-1 -> auf
# -perp. Drinnen nie. Sim ist gleichzeitig der visuelle Indikator
@@ -2843,7 +3118,7 @@ def _make_oeffnung_pieces(axis_curve, point_on_axis, wall_dicke, oeff_meta, base
sb = _make_oeff_box(pt, tan,
-half_b - s_oh, +half_b + s_oh,
s_lo, z_lo, p_lo, p_hi)
if sb is not None: pieces.append(sb)
if sb is not None: pieces.append((sb, "sims"))
return pieces
@@ -2853,8 +3128,8 @@ SOURCE_TYPES = ("wand_axis", "decke_outline", "dach_outline",
"stuetze_point", "traeger_axis",
"raum_outline", "decke_aussparung_outline")
VOLUME_TYPES = ("wand_volume", "decke_volume", "dach_volume",
"oeffnung_volume", "oeffnung_swing", "treppe_volume",
"stuetze_volume", "traeger_volume",
"oeffnung_volume", "oeffnung_swing", "oeffnung_sturz",
"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.
@@ -4653,25 +4928,76 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
op_meta["geschoss"], meta["dicke"], "", "",
oeff_typ="tuer",
oeff_parent=op_meta.get("oeff_parent"))
sw_attrs.SetUserString("dossier_oeff_piece", "schwung")
doc.Objects.AddCurve(crv, sw_attrs)
# Sturzlinien (Lintel-Projektion) — nur bei 1:100 Tueren mit
# explizit gesetztem oeff_sturz. Bei standard/detail uebernimmt
# der Rahmen-Brep die Sturz-Darstellung visuell.
old_st = list(_find_objects_by_wall_id(doc, op_meta["id"],
"oeffnung_sturz"))
for o, _m in old_st:
try: doc.Objects.Delete(o.Id, True)
except Exception: pass
if _resolve_oeff_darstellung(op_meta) == "einfach":
sturzes = _make_tuer_sturz_curves(geom, pt_loc, meta["dicke"],
op_meta, op_uk)
if sturzes:
st_layer_idx = _ensure_layer(doc,
_layer_path_oeff_kind(doc, geschoss_name, "sturz"))
for crv in sturzes:
st_attrs = Rhino.DocObjects.ObjectAttributes()
st_attrs.LayerIndex = st_layer_idx
_attach_meta(st_attrs, op_meta["id"], "oeffnung_sturz",
op_meta["geschoss"], meta["dicke"], "", "",
oeff_typ="tuer",
oeff_parent=op_meta.get("oeff_parent"),
oeff_sturz=op_meta.get("oeff_sturz"))
st_attrs.SetUserString("dossier_oeff_piece", "sturz")
doc.Objects.AddCurve(crv, st_attrs)
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):
# Replace-Pfad nur wenn Count UND Kind-Reihenfolge identisch
# geblieben sind — andernfalls stimmt das Layer-/Material-Mapping
# nicht mehr und wir muessen sauber neu anlegen.
can_replace = (len(old_objs) == len(pieces) and len(pieces) > 0)
if can_replace:
for (old_obj, _m), (_pb, k) in zip(old_objs, pieces):
if (old_obj.Attributes.GetUserString("dossier_oeff_piece")
or "") != k:
can_replace = False; break
if can_replace:
for (old_obj, _m), (pbrep, _k) 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.
# Fallback: Anzahl oder Kind 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:
for (pbrep, kind) in pieces:
op_attrs = Rhino.DocObjects.ObjectAttributes()
op_attrs.LayerIndex = op_layer
# Per-Kind Sublayer unter WAENDE::Öffnungen::<Kind>
kind_layer = _ensure_layer(doc,
_layer_path_oeff_kind(doc, geschoss_name, kind))
op_attrs.LayerIndex = kind_layer if kind_layer >= 0 else op_layer
# Per-Kind Material (Holz/Glas/Türblatt/...) — Glas hat
# Transparenz. Edges bleiben schwarz (siehe unten).
mat_idx = _ensure_oeff_material(doc, kind)
if mat_idx >= 0:
op_attrs.MaterialIndex = mat_idx
op_attrs.MaterialSource = (
Rhino.DocObjects.ObjectMaterialSource.MaterialFromObject)
# Edges/Wireframe immer schwarz, entkoppelt vom Layer/Material.
try:
import System.Drawing as SD
op_attrs.ColorSource = (
Rhino.DocObjects.ObjectColorSource.ColorFromObject)
op_attrs.ObjectColor = SD.Color.FromArgb(255, 0, 0, 0)
except Exception: pass
_attach_meta(op_attrs, op_meta["id"], "oeffnung_volume",
op_meta["geschoss"], meta["dicke"], "", "",
oeff_typ=op_meta.get("oeff_typ"),
@@ -4695,12 +5021,14 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
oeff_tuer_typ=op_meta.get("oeff_tuer_typ"),
oeff_hinge_side=op_meta.get("oeff_hinge_side"),
oeff_open_angle=op_meta.get("oeff_open_angle"),
oeff_swing_invert=op_meta.get("oeff_swing_invert"))
oeff_swing_invert=op_meta.get("oeff_swing_invert"),
oeff_sturz=op_meta.get("oeff_sturz"))
op_attrs.SetUserString("dossier_oeff_piece", kind)
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))
src_layer = _ensure_layer(doc, _layer_path_referenz(doc, geschoss_name))
try:
if src_layer >= 0 and src_obj.Attributes.LayerIndex != src_layer:
new_attrs = src_obj.Attributes.Duplicate()
@@ -5312,6 +5640,7 @@ class ElementeBridge(panel_base.BaseBridge):
"hingeSide": meta.get("oeff_hinge_side", "links"),
"openAngle": meta.get("oeff_open_angle", 90.0),
"swingInvert": bool(meta.get("oeff_swing_invert", False)),
"sturz": meta.get("oeff_sturz", "beide"),
})
elif meta["type"] == "treppe_axis":
gs = _geschoss_by_id(doc, meta["geschoss"])
@@ -5688,7 +6017,7 @@ class ElementeBridge(panel_base.BaseBridge):
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_layer = _ensure_layer(doc, _layer_path_referenz(doc, geschoss_name))
axis = axis_curve.DuplicateCurve()
try:
z0 = axis.PointAtStart.Z
@@ -6212,7 +6541,7 @@ class ElementeBridge(panel_base.BaseBridge):
simsa_def = "standard" if is_fenster else "ohne"
glas_def = is_fenster
referenz_def = _last("oeff_referenz", "mid")
darst_def = "standard"
darst_def = "auto"
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
@@ -6359,7 +6688,7 @@ class ElementeBridge(panel_base.BaseBridge):
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))
layer = _ensure_layer(doc, _layer_path_referenz(doc, geschoss_name))
attrs = Rhino.DocObjects.ObjectAttributes()
attrs.LayerIndex = layer
@@ -6382,7 +6711,8 @@ class ElementeBridge(panel_base.BaseBridge):
oeff_style_id=pending_sid,
oeff_tuer_typ="normal",
oeff_hinge_side="links",
oeff_open_angle=90.0)
oeff_open_angle=90.0,
oeff_sturz="beide")
# 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
@@ -7604,7 +7934,7 @@ class ElementeBridge(panel_base.BaseBridge):
# Terrain-Daten (XYZ + Grid) holen, sobald Mesh ODER
# Hoehenlinien gewuenscht sind — beide nutzen das Grid.
need_dem = any(k in kinds for k in
("terrain", "contours", "contour_tin", "contour_schicht"))
("terrain", "contours", "contour_tin", "contour_schicht", "contour_patch"))
mesh_objects = []
merged_grid = None
if need_dem:
@@ -7656,13 +7986,14 @@ class ElementeBridge(panel_base.BaseBridge):
except Exception as ex:
self._push_log("Mesh-Bau fehlgeschlagen: {}".format(ex))
# Contours sind die Grundlage fuer drei moegliche Outputs:
# Contours sind die Grundlage fuer vier moegliche Outputs:
# 'contours' → flache 2D-Curves auf OKFF
# 'contour_tin' → TIN-Mesh aus Contour-Vertices
# 'contour_schicht' → Planare Flaechen pro Hoehe
# 'contour_patch' → NURBS-Patch-Surface durch alle Konturen
# Wir generieren einmal die echten 3D-Curves und teilen
# sie auf die drei Outputs auf.
contour_kinds = ("contours", "contour_tin", "contour_schicht")
# sie auf die Outputs auf.
contour_kinds = ("contours", "contour_tin", "contour_schicht", "contour_patch")
need_contours = any(k in kinds for k in contour_kinds) and merged_grid is not None
raw_contours = []
if need_contours:
@@ -7762,6 +8093,25 @@ class ElementeBridge(panel_base.BaseBridge):
len(schicht_objs)))
except Exception as ex:
self._push_log("Schichtenmodell-Fehler: {}".format(ex))
# Patch-Terrain (NURBS-Surface durch alle Hoehenlinien)
if "contour_patch" in kinds and raw_contours:
try:
patch_obj = swisstopo.generate_patch_from_contours(
d, raw_contours, progress=self._push_log)
if patch_obj:
at = patch_obj.Attributes.Duplicate()
at.SetUserString("dossier_swisstopo_kind", "contour_patch")
d.Objects.ModifyAttributes(patch_obj, at, True)
if z_id:
self._move_to_sublayer(
d, [patch_obj], z_id, "80",
tag="contour_patch",
fallback_name="80_swisstopo",
fallback_color="#909090")
except Exception as ex:
self._push_log("Patch-Terrain-Fehler: {}".format(ex))
# Layer-Move auf aktive Geschoss/80_swisstopo Sublayer
if z_id and mesh_objects:
sub_name = _find_ebene_sublayer_name(
@@ -8833,6 +9183,8 @@ class ElementeBridge(panel_base.BaseBridge):
o_angle = max(0.0, min(180.0, o_angle))
o_swinv = bool(p.get("swingInvert",
old_meta.get("oeff_swing_invert", False)))
o_sturz = p.get("sturz", old_meta.get("oeff_sturz", "beide"))
if o_sturz not in _OEFF_STURZ_OPTIONS: o_sturz = "beide"
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)
@@ -8881,7 +9233,8 @@ class ElementeBridge(panel_base.BaseBridge):
oeff_tuer_typ=o_tuer_typ,
oeff_hinge_side=o_hinge,
oeff_open_angle=o_angle,
oeff_swing_invert=o_swinv)
oeff_swing_invert=o_swinv,
oeff_sturz=o_sturz)
axis_obj.Attributes = attrs
axis_obj.CommitChanges()
parent_id = old_meta.get("oeff_parent", "")
@@ -8932,7 +9285,7 @@ class ElementeBridge(panel_base.BaseBridge):
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))
attrs.LayerIndex = _ensure_layer(doc, _layer_path_referenz(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":
@@ -9953,6 +10306,145 @@ def _sync_orphan_grips(doc):
sc.sticky[_SELECT_BUSY] = False
def _migrate_referenz_layer_once(doc):
"""One-shot pro Doc-Session: wand_axis + oeffnung_point auf den
neuen Referenzlinien-Sublayer migrieren, und alle versehentlich
versteckten Source-Objekte wieder zeigen. Sticky-Flag pro Doc-Id
damit ein Reopen sauber neu migriert.
Triggert zudem proaktiv die Registrierung der Referenzlinien-Ebene
im dossier_ebenen-Tree (auch wenn der Doc noch keine wand_axis-Objekte
hat) dafuer wird _layer_path_referenz fuer jedes existierende
Geschoss aufgerufen, was via _find_ebene_sublayer_name den Auto-Add
+ broadcast_state ausloest."""
if doc is None: return
# Sticky-Version bumped: vorherige Versionen liefen ohne proaktive
# Ebenen-Registrierung — wenn die alten Keys gesetzt sind, wuerde die
# neue Logik nie greifen. v3 = aktuelle Implementierung.
try: key = "_dossier_referenz_migration_v3_" + str(doc.RuntimeSerialNumber)
except Exception: key = "_dossier_referenz_migration_v3_default"
if sc.sticky.get(key): return
sc.sticky[key] = True
n_moved = 0
n_shown = 0
n_geschosse = 0
by_geschoss = {}
try:
# Proaktive Registrierung: fuer jedes Geschoss die Referenzlinien-
# Layer anlegen. Damit erscheint die Ebene im Panel auch wenn noch
# keine Achsen migriert werden muessen. _layer_path_referenz ruft
# intern _find_ebene_sublayer_name, das die Referenzlinien-Ebene
# in dossier_ebenen einfuegt + broadcast_state ausloest.
geschosse = _load_geschosse(doc)
for g in geschosse:
if not isinstance(g, dict): continue
g_id = g.get("id")
g_name = g.get("name")
# Nur echte Geschosse (mit id) — nicht generische Ebenen-Eintraege
# die _load_geschosse als Fallback liefert wenn dossier_zeichnungs-
# ebenen leer ist.
if not g_id or not g_name: continue
try:
ref_idx = _ensure_layer(doc,
_layer_path_referenz(doc, g_name))
if ref_idx >= 0:
by_geschoss[g_name] = ref_idx
n_geschosse += 1
except Exception as ex:
print("[ELEMENTE] referenz-ebene fuer {}: {}".format(g_name, ex))
for obj in list(doc.Objects):
try:
meta = _read_meta(obj)
if not meta: continue
t = meta.get("type")
if t not in ("wand_axis", "oeffnung_point"): continue
# Unhide
if obj.IsHidden:
if doc.Objects.Show(obj.Id, True): n_shown += 1
# Layer-Migration auf Referenzlinien
g_id = meta.get("geschoss")
if not g_id: continue
g = _geschoss_by_id(doc, g_id)
if not g: continue
g_name = g.get("name") or "EG"
ref_idx = by_geschoss.get(g_name)
if ref_idx is None:
ref_idx = _ensure_layer(doc,
_layer_path_referenz(doc, g_name))
by_geschoss[g_name] = ref_idx
if ref_idx < 0: continue
if obj.Attributes.LayerIndex != ref_idx:
a = obj.Attributes.Duplicate()
a.LayerIndex = ref_idx
if doc.Objects.ModifyAttributes(obj, a, True):
n_moved += 1
except Exception: pass
print("[ELEMENTE] Referenz-Migration (v3): {} Geschoss(e) registriert, "
"{} Objekte bewegt, {} eingeblendet".format(
n_geschosse, n_moved, n_shown))
# Finaler Broadcast — falls _find_ebene_sublayer_name den Eintrag
# nicht neu angelegt hat (z.B. weil er schon existiert) wird das
# Panel sonst nicht neu gerendert.
try:
import rhinopanel
rhinopanel._broadcast_state(doc)
except Exception: pass
except Exception as ex:
print("[ELEMENTE] _migrate_referenz_layer_once:", ex)
def _ensure_oeff_ebenen_once(doc):
"""Idle-Tick One-Shot: registriert Oeffnungen-Subtree (Öffnungen +
6 Piece-Kinder) in dossier_ebenen + migriert existierende
oeffnung_volume / oeffnung_swing Objekte auf die neuen code-
praefigierten Sublayer. Damit auch User die schon vorher Tueren/
Fenster gezeichnet haben die Layer im Panel sehen + die alten
plain-Namen-Layer leeren wir damit der Layer-Manager sauber bleibt."""
if doc is None: return
try: key = "_dossier_oeff_subtree_migration_" + str(doc.RuntimeSerialNumber)
except Exception: key = "_dossier_oeff_subtree_migration_default"
if sc.sticky.get(key): return
sc.sticky[key] = True
try:
# 1) Subtree in dossier_ebenen registrieren — feuert build_layers
# + broadcast_state intern
_ensure_oeff_ebenen_in_doc(doc)
# 2) Existierende Objekte auf die neuen Layer ziehen
n_moved = 0
by_target = {} # (geschoss_name, kind) -> layer_idx
for obj in list(doc.Objects):
try:
meta = _read_meta(obj)
if not meta: continue
t = meta.get("type")
if t not in ("oeffnung_volume", "oeffnung_swing"): continue
kind = obj.Attributes.GetUserString("dossier_oeff_piece")
if not kind: continue
if kind not in _OEFF_PIECE_DEFS: continue
g_id = meta.get("geschoss")
if not g_id: continue
g = _geschoss_by_id(doc, g_id)
if not g: continue
g_name = g.get("name") or "EG"
cache_k = (g_name, kind)
target_idx = by_target.get(cache_k)
if target_idx is None:
target_idx = _ensure_layer(doc,
_layer_path_oeff_kind(doc, g_name, kind))
by_target[cache_k] = target_idx
if target_idx < 0: continue
if obj.Attributes.LayerIndex != target_idx:
a = obj.Attributes.Duplicate()
a.LayerIndex = target_idx
if doc.Objects.ModifyAttributes(obj, a, True):
n_moved += 1
except Exception: pass
if n_moved:
print("[ELEMENTE] Oeffnungen-Subtree-Migration: {} Objekte auf neue Sublayer".format(n_moved))
except Exception as ex:
print("[ELEMENTE] _ensure_oeff_ebenen_once:", ex)
def _on_idle_selection(sender, e):
"""Pollt periodisch die Selektion + verarbeitet Pending-Regenerate-Queue.
@@ -9974,6 +10466,15 @@ def _on_idle_selection(sender, e):
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return
# One-shot Migration: alte Source-Curves (wand_axis, oeffnung_point)
# auf den neuen Referenzlinien-Layer schieben + alle versehentlich
# versteckten wieder zeigen (Cleanup nach der abgebrochenen
# Hide-on-Select-Implementation).
_migrate_referenz_layer_once(doc)
# Oeffnungen-Tree in dossier_ebenen anlegen falls noch nicht vorhanden
# (sonst erscheinen die neuen Sublayer nicht im Ebenen-Panel).
_ensure_oeff_ebenen_once(doc)
# 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
+26
View File
@@ -1087,6 +1087,32 @@ class OberleisteBridge(panel_base.BaseBridge):
except Exception as ex:
print("[OBERLEISTE] open layer-combinations:", ex)
# --- Anordnen (DisplayOrder Z-Stack) ----------------------------
# Nutzt Rhinos native _BringToFront / _BringForward / _SendBackward
# / _SendToBack. Diese setzen Attributes.DisplayOrder — keine
# Geometrie-Aenderung, kein Z-Offset. Selection-Check verhindert
# nervigen "Select objects"-Prompt wenn der User den Button leer
# drueckt.
elif t == "ARRANGE":
cmd = {
"front": "_BringToFront",
"forward": "_BringForward",
"backward": "_SendBackward",
"back": "_SendToBack",
}.get(p.get("dir"))
if cmd:
doc = Rhino.RhinoDoc.ActiveDoc
if doc is not None:
try:
sel = list(doc.Objects.GetSelectedObjects(False, False))
except Exception: sel = []
if sel:
try:
Rhino.RhinoApp.SendKeystrokes("\x1b", False)
Rhino.RhinoApp.RunScript(cmd, False)
except Exception as ex:
print("[OBERLEISTE] arrange {}: {}".format(cmd, ex))
# --- Command-Line Integration -----------------------------------
elif t == "RUN_COMMAND":
cmd = (p.get("cmd") or "").strip()
+16 -4
View File
@@ -1148,10 +1148,22 @@ class EbenenBridge(panel_base.BaseBridge):
return
try:
ebenen = json.loads(raw)
for e in ebenen:
if e.get("code") == code:
e[field] = value
break
# Rekursive Suche — Sub-Ebenen (z.B. WAENDE→Öffnungen→Sturz mit
# Code 20o7) liegen mehrere Ebenen tief. Frueher nur Top-Level
# iteriert → Style-Changes an nested Sublayer wurden nicht
# persistiert und kamen beim naechsten broadcast als alte Werte
# zurueck.
def _set_in_tree(lst):
for e in lst:
if not isinstance(e, dict): continue
if e.get("code") == code:
e[field] = value
return True
kids = e.get("children")
if isinstance(kids, list) and _set_in_tree(kids):
return True
return False
_set_in_tree(ebenen)
doc.Strings.SetString("dossier_ebenen", json.dumps(ebenen, ensure_ascii=False))
_broadcast_state(doc)
except Exception as ex:
+65
View File
@@ -844,6 +844,71 @@ def generate_schichtenmodell(doc, contour_curves, progress=None):
return created
def generate_patch_from_contours(doc, contour_curves, progress=None):
"""Erzeugt eine NURBS-Patch-Flaeche durch alle Hoehenlinien — der
kanonische Rhino-Workflow fuer Terrain aus Konturen (Brep.CreatePatch
fittet eine Surface durch Curves + Points). Loft funktioniert hier
nicht, weil benachbarte Konturen unterschiedliche Topologie haben
(Inseln, Halbinseln, geschlossen vs. offen am Rand).
UV-Spans werden aus dem Bounding-Box-Aspect-Ratio abgeleitet, so
dass ein rechteckiger Site mehr Spans in der laengeren Richtung
bekommt.
Liefert das erstellte RhinoObject oder None."""
import System
if not contour_curves: return None
valid = [c for c in contour_curves if c is not None]
if len(valid) < 2:
if progress: progress("Patch braucht mindestens 2 Hoehenlinien")
return None
bb = rg.BoundingBox.Unset
for c in valid:
bb_c = c.GetBoundingBox(True)
if not bb_c.IsValid: continue
if not bb.IsValid:
bb = bb_c
else:
bb.Union(bb_c)
if not bb.IsValid:
if progress: progress("Patch: ungueltige Bounding-Box")
return None
dx = bb.Max.X - bb.Min.X
dy = bb.Max.Y - bb.Min.Y
base_span = 40
if dx >= dy and dy > 1e-6:
aspect = dx / dy
u_spans = int(round(base_span * math.sqrt(aspect)))
v_spans = base_span
elif dx > 1e-6:
aspect = dy / dx
u_spans = base_span
v_spans = int(round(base_span * math.sqrt(aspect)))
else:
u_spans = v_spans = base_span
u_spans = max(8, min(200, u_spans))
v_spans = max(8, min(200, v_spans))
if progress:
progress("Patch aus {} Hoehenlinien ({}x{} UV-Spans)...".format(
len(valid), u_spans, v_spans))
geom_list = System.Collections.Generic.List[rg.GeometryBase]()
for c in valid: geom_list.Add(c)
try:
brep = rg.Brep.CreatePatch(
geom_list, u_spans, v_spans, doc.ModelAbsoluteTolerance)
if brep is None:
if progress: progress("Patch fehlgeschlagen (None zurueck)")
return None
gid = doc.Objects.AddBrep(brep)
if gid and gid != System.Guid.Empty:
obj = doc.Objects.Find(gid)
if progress: progress("→ Patch-Terrain erzeugt")
return obj
except Exception as ex:
if progress: progress("Patch-Fehler: {}".format(ex))
return None
def generate_contour_curves(grid, shift_lv95, m_to_unit, interval=2.0,
progress=None):
"""Generiert Hoehenlinien (Contour-Curves) aus dem Terrain-Grid via