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_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_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_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_TUER_TYPEN = ("normal", "wandoeffnung")
_OEFF_HINGE_SIDES = ("links", "rechts") _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): def get_aktive_darstellung(doc):
"""Liest die doc-level Darstellungs-Override. Leer → per-object.""" """Liest die doc-level Modelldarstellung. Default = einfach (1:100).
if doc is None: return "" '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: try:
v = doc.Strings.GetValue(_KEY_AKTIVE_DARSTELLUNG) or "" v = doc.Strings.GetValue(_KEY_AKTIVE_DARSTELLUNG) or ""
except Exception: except Exception:
v = "" 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): 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 if doc is None: return
try: try:
if value and value in _OEFF_DARSTELLUNGEN: v = value if value in ("einfach", "standard", "detail") else _DARSTELLUNG_DEFAULT_GLOBAL
doc.Strings.SetString(_KEY_AKTIVE_DARSTELLUNG, value) doc.Strings.SetString(_KEY_AKTIVE_DARSTELLUNG, v)
else:
doc.Strings.Delete(_KEY_AKTIVE_DARSTELLUNG)
except Exception as ex: except Exception as ex:
print("[ELEMENTE] set_aktive_darstellung:", ex) print("[ELEMENTE] set_aktive_darstellung:", ex)
@@ -414,6 +421,30 @@ _OEFF_SIMS_STYLES = {
} }
_OEFF_RAHMEN_POS_OPTIONS = ("aussen", "mid", "innen") _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) ------------------------------ # --- Last-Used-Defaults (sticky, session-life) ------------------------------
# Speichert die letzten Werte (Dicke, Referenz, Modus, Neigung), damit der # 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) return _layer_path_axis(doc, geschoss_name)
def _layer_path_oeff_swing(doc, geschoss_name): def _layer_path_referenz(doc, geschoss_name):
"""Türschwung-Linien — eigener Sublayer (Code 23) damit User die """Sublayer 'Referenzlinien' (Code 19) — eigene Ebene fuer wand_axis +
Schwung-Bögen unabhängig von den Türen-Volumes ausblenden kann.""" oeffnung_point Source-Objekte. Getrennt vom Wand-Volumen-Layer (20)
sub = _find_ebene_sublayer_name(doc, ["schwung", "tuerschwung"], damit der User die Konstruktions-Referenzen ein-/ausblenden kann ohne
"23", "Türschwung", die Volumen-Sichtbarkeit zu verlieren. Wird automatisch im Ebenen-
default_color="#888888", default_lw=0.13) 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) 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): def _layer_path_decke(doc, geschoss_name):
"""Decken-Outline + Volumen — Sublayer 'DECKEN' (Code 30).""" """Decken-Outline + Volumen — Sublayer 'DECKEN' (Code 30)."""
sub = _find_ebene_sublayer_name(doc, ["decke"], "30", "DECKEN", 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_aussenseite=None, oeff_tuer_rahmen=None,
oeff_rahmen_offset=None, oeff_style_id=None, oeff_rahmen_offset=None, oeff_style_id=None,
oeff_tuer_typ=None, oeff_hinge_side=None, oeff_open_angle=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, geschoss_end=None, treppe_breite=None,
treppe_n=None, treppe_referenz=None, treppe_n=None, treppe_referenz=None,
treppe_modus=None, treppe_lauf_d=None, treppe_art=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, try: obj_attrs.SetUserString(_KEY_OEFF_SWING_INVERT,
"1" if bool(oeff_swing_invert) else "0") "1" if bool(oeff_swing_invert) else "0")
except Exception: pass 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 --- # --- Treppen-Felder ---
if geschoss_end is not None: if geschoss_end is not None:
obj_attrs.SetUserString(_KEY_GESCHOSS_END, geschoss_end or "") 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 ogl = (og_raw == "1") if og_raw in ("0", "1") else is_fenster
oref = a.GetUserString(_KEY_OEFF_REFERENZ) or "mid" oref = a.GetUserString(_KEY_OEFF_REFERENZ) or "mid"
if oref not in _OEFF_REFERENZ_OPTIONS: oref = "mid" if oref not in _OEFF_REFERENZ_OPTIONS: oref = "mid"
odarst = a.GetUserString(_KEY_OEFF_DARSTELLUNG) or "standard" odarst = a.GetUserString(_KEY_OEFF_DARSTELLUNG) or "auto"
if odarst not in _OEFF_DARSTELLUNGEN: odarst = "standard" if odarst not in _OEFF_DARSTELLUNGEN: odarst = "auto"
oauss = a.GetUserString(_KEY_OEFF_AUSSENSEITE) or "rechts" oauss = a.GetUserString(_KEY_OEFF_AUSSENSEITE) or "rechts"
if oauss not in _OEFF_AUSSENSEITEN: oauss = "rechts" if oauss not in _OEFF_AUSSENSEITEN: oauss = "rechts"
otrah = a.GetUserString(_KEY_OEFF_TUER_RAHMEN) or "zarge" otrah = a.GetUserString(_KEY_OEFF_TUER_RAHMEN) or "zarge"
@@ -2178,6 +2377,8 @@ def _read_meta(obj):
if oangle < 0: oangle = 0.0 if oangle < 0: oangle = 0.0
elif oangle > 180: oangle = 180.0 elif oangle > 180: oangle = 180.0
oswinv = (a.GetUserString(_KEY_OEFF_SWING_INVERT) == "1") 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 # Treppen-Felder
gend = a.GetUserString(_KEY_GESCHOSS_END) or "" gend = a.GetUserString(_KEY_GESCHOSS_END) or ""
try: tb = float(a.GetUserString(_KEY_TREPPE_BREITE) or "1.0") try: tb = float(a.GetUserString(_KEY_TREPPE_BREITE) or "1.0")
@@ -2303,6 +2504,7 @@ def _read_meta(obj):
"oeff_hinge_side": ohinge, "oeff_hinge_side": ohinge,
"oeff_open_angle": oangle, "oeff_open_angle": oangle,
"oeff_swing_invert": oswinv, "oeff_swing_invert": oswinv,
"oeff_sturz": ostz,
"geschoss_end": gend, "geschoss_end": gend,
"treppe_breite": tb, "treppe_breite": tb,
"treppe_n": tn, "treppe_n": tn,
@@ -2523,6 +2725,17 @@ def _resolve_rahmen_perp_range(half_d, rahmen_tiefe, rahmen_pos):
return lo, hi 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, def _make_tuer_swing_curves(axis_curve, point_on_axis, wall_dicke,
oeff_meta, base_z): oeff_meta, base_z):
"""Generiert die 2D-Plan-Schwung-Linien einer Tuer: """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 pt, tan, perp = frame
breite = float(oeff_meta.get("oeff_breite", 0.9)) breite = float(oeff_meta.get("oeff_breite", 0.9))
rahmen_b = float(oeff_meta.get("oeff_rahmen_b", 0.06)) 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") aussenseite = oeff_meta.get("oeff_aussenseite", "rechts")
aus_sign = +1 if aussenseite == "rechts" else -1 aus_sign = +1 if aussenseite == "rechts" else -1
hinge_side = oeff_meta.get("oeff_hinge_side", "links") 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 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): 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 """Baut die einzelnen Brep-Pieces der Oeffnung — Rahmen (single Brep
via Boolean-Differenz), Mittelpfosten (pro Fluegel), Glas, Sims aussen, via Boolean-Differenz), Mittelpfosten (pro Fluegel), Glas, Sims aussen.
Sims innen. Liefert eine Liste von Breps. Caller persistiert jedes Liefert eine Liste von (brep, kind)-Tupeln. Kind ist einer von
als eigenes 'oeffnung_volume' Object mit der gleichen Oeffnungs-ID.""" '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) frame = _oeff_axis_frame(axis_curve, point_on_axis)
if frame is None: return [] if frame is None: return []
pt, tan, perp = frame 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") sims_in_style = oeff_meta.get("oeff_sims_in", "ohne")
has_glas = bool(oeff_meta.get("oeff_glas", False)) has_glas = bool(oeff_meta.get("oeff_glas", False))
is_tuer = (oeff_meta.get("oeff_typ") == "tuer") is_tuer = (oeff_meta.get("oeff_typ") == "tuer")
# Doc-level Darstellungs-Override gewinnt vor per-Object-Setting — darstellung = _resolve_oeff_darstellung(oeff_meta)
# 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 # Aussenseite: +1 = aussen ist +Plane.ZAxis (default), -1 = aussen ist
# -Plane.ZAxis (Wand "umgedreht"). Wird verwendet um Sims aus/in # -Plane.ZAxis (Wand "umgedreht"). Wird verwendet um Sims aus/in
# konsistent zu platzieren unabhaengig von der Wand-Achsen-Richtung. # 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 # Rahmen-Mittelebene (= dort wo das Glas im Standard sitzt). Im
# 2D-Plan ergibt das eine einzelne Linie quer durch die Oeffnung, # 2D-Plan ergibt das eine einzelne Linie quer durch die Oeffnung,
# auf dem korrekten Tiefen-Offset. # 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 darstellung == "einfach":
if is_tuer:
return []
try: try:
pane_perp = (frame_perp_lo + frame_perp_hi) * 0.5 pane_perp = (frame_perp_lo + frame_perp_hi) * 0.5
# Plane.Origin ans Wand-Achsenpunkt verschoben um pane_perp # 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(-half_b, +half_b),
rg.Interval(z_lo, z_hi)) rg.Interval(z_lo, z_hi))
brep = surf.ToBrep() brep = surf.ToBrep()
return [brep] if brep is not None else [] return [(brep, "pane")] if brep is not None else []
except Exception as ex: except Exception as ex:
print("[ELEMENTE] einfach pane:", ex) print("[ELEMENTE] einfach pane:", ex)
return [] return []
@@ -2768,13 +3039,14 @@ def _make_oeffnung_pieces(axis_curve, point_on_axis, wall_dicke, oeff_meta, base
diff = rg.Brep.CreateBooleanDifference( diff = rg.Brep.CreateBooleanDifference(
[outer_box], [inner_box], 0.001) [outer_box], [inner_box], 0.001)
if diff and len(diff) > 0: if diff and len(diff) > 0:
pieces.append(diff[0]) pieces.append((diff[0], "rahmen"))
else: else:
pieces.append(outer_box) pieces.append((outer_box, "rahmen"))
except Exception as ex: except Exception as ex:
print("[ELEMENTE] Rahmen BoolDiff:", 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: if fluegel > 1:
span = inner_r - inner_l span = inner_r - inner_l
for i in range(1, fluegel): 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 x_hi = x_mid + rahmen_b * 0.5
mp = _make_oeff_box(pt, tan, x_lo, x_hi, payload_z_lo, payload_z_hi, mp = _make_oeff_box(pt, tan, x_lo, x_hi, payload_z_lo, payload_z_hi,
frame_perp_lo, frame_perp_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 # --- INNERE FUELLUNG: Glas (Fenster oder verglaste Tuer) ODER
# Tuerblatt (massive Tuere ohne Glas). Beides als Box-Brep pro Fluegel. # 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: else:
fill_t = 0 # nichts (z.B. Fenster ohne Glas) 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 fill_mid = (frame_perp_lo + frame_perp_hi) * 0.5
# DETAIL (1:20): Doppelverglasung — 2 Scheiben a 6mm, 16mm SZR # DETAIL (1:20): Doppelverglasung — 2 Scheiben a 6mm, 16mm SZR
is_double_glas = (darstellung == "detail" and has_glas and not is_tuer) 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, fp = _make_oeff_box(pt, tan, fx_lo, fx_hi,
payload_z_lo, payload_z_hi, payload_z_lo, payload_z_hi,
fl, fh) fl, fh)
if fp is not None: pieces.append(fp) if fp is not None: pieces.append((fp, fill_kind))
else: else:
fp = _make_oeff_box(pt, tan, inner_l, inner_r, fp = _make_oeff_box(pt, tan, inner_l, inner_r,
payload_z_lo, payload_z_hi, payload_z_lo, payload_z_hi,
fl, fh) 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 # --- SIMS — nur AUSSEN. aus_sign=+1 -> sims auf +perp, =-1 -> auf
# -perp. Drinnen nie. Sim ist gleichzeitig der visuelle Indikator # -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, sb = _make_oeff_box(pt, tan,
-half_b - s_oh, +half_b + s_oh, -half_b - s_oh, +half_b + s_oh,
s_lo, z_lo, p_lo, p_hi) 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 return pieces
@@ -2853,8 +3128,8 @@ SOURCE_TYPES = ("wand_axis", "decke_outline", "dach_outline",
"stuetze_point", "traeger_axis", "stuetze_point", "traeger_axis",
"raum_outline", "decke_aussparung_outline") "raum_outline", "decke_aussparung_outline")
VOLUME_TYPES = ("wand_volume", "decke_volume", "dach_volume", VOLUME_TYPES = ("wand_volume", "decke_volume", "dach_volume",
"oeffnung_volume", "oeffnung_swing", "treppe_volume", "oeffnung_volume", "oeffnung_swing", "oeffnung_sturz",
"stuetze_volume", "traeger_volume", "treppe_volume", "stuetze_volume", "traeger_volume",
"raum_stamp", "raum_fill") "raum_stamp", "raum_fill")
# Oeffnungs-Cutout: Boolean-Difference aus Wand. Zusaetzlich kriegt die # Oeffnungs-Cutout: Boolean-Difference aus Wand. Zusaetzlich kriegt die
# Oeffnung ihr eigenes Volumen (Rahmen + Sims + Glas) als Sub-Element. # 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"], "", "", op_meta["geschoss"], meta["dicke"], "", "",
oeff_typ="tuer", oeff_typ="tuer",
oeff_parent=op_meta.get("oeff_parent")) oeff_parent=op_meta.get("oeff_parent"))
sw_attrs.SetUserString("dossier_oeff_piece", "schwung")
doc.Objects.AddCurve(crv, sw_attrs) 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"], old_objs = list(_find_objects_by_wall_id(doc, op_meta["id"],
"oeffnung_volume")) "oeffnung_volume"))
pieces = _make_oeffnung_pieces(geom, pt_loc, meta["dicke"], pieces = _make_oeffnung_pieces(geom, pt_loc, meta["dicke"],
op_meta, op_uk) op_meta, op_uk)
if len(old_objs) == len(pieces) and len(pieces) > 0: # Replace-Pfad nur wenn Count UND Kind-Reihenfolge identisch
for (old_obj, _old_meta), pbrep in zip(old_objs, pieces): # 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) try: doc.Objects.Replace(old_obj.Id, pbrep)
except Exception as ex: except Exception as ex:
print("[ELEMENTE] replace oeff vol:", ex) print("[ELEMENTE] replace oeff vol:", ex)
continue 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: for o, _m in old_objs:
try: doc.Objects.Delete(o.Id, True) try: doc.Objects.Delete(o.Id, True)
except Exception as ex: print("[ELEMENTE] del old oeff vol:", ex) 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 = 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", _attach_meta(op_attrs, op_meta["id"], "oeffnung_volume",
op_meta["geschoss"], meta["dicke"], "", "", op_meta["geschoss"], meta["dicke"], "", "",
oeff_typ=op_meta.get("oeff_typ"), 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_tuer_typ=op_meta.get("oeff_tuer_typ"),
oeff_hinge_side=op_meta.get("oeff_hinge_side"), oeff_hinge_side=op_meta.get("oeff_hinge_side"),
oeff_open_angle=op_meta.get("oeff_open_angle"), 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) doc.Objects.AddBrep(pbrep, op_attrs)
# Source-Layer migrieren + Volumen-Layer-Index ermitteln # Source-Layer migrieren + Volumen-Layer-Index ermitteln
layer = _ensure_layer(doc, _layer_path_volume(doc, geschoss_name)) 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: try:
if src_layer >= 0 and src_obj.Attributes.LayerIndex != src_layer: if src_layer >= 0 and src_obj.Attributes.LayerIndex != src_layer:
new_attrs = src_obj.Attributes.Duplicate() new_attrs = src_obj.Attributes.Duplicate()
@@ -5312,6 +5640,7 @@ class ElementeBridge(panel_base.BaseBridge):
"hingeSide": meta.get("oeff_hinge_side", "links"), "hingeSide": meta.get("oeff_hinge_side", "links"),
"openAngle": meta.get("oeff_open_angle", 90.0), "openAngle": meta.get("oeff_open_angle", 90.0),
"swingInvert": bool(meta.get("oeff_swing_invert", False)), "swingInvert": bool(meta.get("oeff_swing_invert", False)),
"sturz": meta.get("oeff_sturz", "beide"),
}) })
elif meta["type"] == "treppe_axis": elif meta["type"] == "treppe_axis":
gs = _geschoss_by_id(doc, meta["geschoss"]) gs = _geschoss_by_id(doc, meta["geschoss"])
@@ -5688,7 +6017,7 @@ class ElementeBridge(panel_base.BaseBridge):
wall_id = "wall_" + uuid.uuid4().hex[:10] wall_id = "wall_" + uuid.uuid4().hex[:10]
g = _geschoss_by_id(doc, geschoss_id) g = _geschoss_by_id(doc, geschoss_id)
geschoss_name = g.get("name", "EG") if g else "EG" 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() axis = axis_curve.DuplicateCurve()
try: try:
z0 = axis.PointAtStart.Z z0 = axis.PointAtStart.Z
@@ -6212,7 +6541,7 @@ class ElementeBridge(panel_base.BaseBridge):
simsa_def = "standard" if is_fenster else "ohne" simsa_def = "standard" if is_fenster else "ohne"
glas_def = is_fenster glas_def = is_fenster
referenz_def = _last("oeff_referenz", "mid") referenz_def = _last("oeff_referenz", "mid")
darst_def = "standard" darst_def = "auto"
tuer_rahmen_def = "zarge" tuer_rahmen_def = "zarge"
# Pending-Style-ID aus sticky (von Stil-Picker gesetzt). Falls noch # Pending-Style-ID aus sticky (von Stil-Picker gesetzt). Falls noch
# kein Style gepickt, nutzen wir den zuletzt-aktiven fuer diesen typ # kein Style gepickt, nutzen wir den zuletzt-aktiven fuer diesen typ
@@ -6359,7 +6688,7 @@ class ElementeBridge(panel_base.BaseBridge):
geschoss = wall_meta["geschoss"] geschoss = wall_meta["geschoss"]
g = _geschoss_by_id(doc, geschoss) g = _geschoss_by_id(doc, geschoss)
geschoss_name = g.get("name", "EG") if g else "EG" 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 = Rhino.DocObjects.ObjectAttributes()
attrs.LayerIndex = layer attrs.LayerIndex = layer
@@ -6382,7 +6711,8 @@ class ElementeBridge(panel_base.BaseBridge):
oeff_style_id=pending_sid, oeff_style_id=pending_sid,
oeff_tuer_typ="normal", oeff_tuer_typ="normal",
oeff_hinge_side="links", 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 # Oeffnungs-Punkt auf UK+Brueestung-Hoehe platzieren (= visuell auf
# Unterkante Oeffnung). Constraint vergleicht spaeter pt.Z mit # Unterkante Oeffnung). Constraint vergleicht spaeter pt.Z mit
# UK+brueest — wenn der Punkt am axis.Z=0 saesse, wuerde der erste # 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 # Terrain-Daten (XYZ + Grid) holen, sobald Mesh ODER
# Hoehenlinien gewuenscht sind — beide nutzen das Grid. # Hoehenlinien gewuenscht sind — beide nutzen das Grid.
need_dem = any(k in kinds for k in 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 = [] mesh_objects = []
merged_grid = None merged_grid = None
if need_dem: if need_dem:
@@ -7656,13 +7986,14 @@ class ElementeBridge(panel_base.BaseBridge):
except Exception as ex: except Exception as ex:
self._push_log("Mesh-Bau fehlgeschlagen: {}".format(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 # 'contours' → flache 2D-Curves auf OKFF
# 'contour_tin' → TIN-Mesh aus Contour-Vertices # 'contour_tin' → TIN-Mesh aus Contour-Vertices
# 'contour_schicht' → Planare Flaechen pro Hoehe # 'contour_schicht' → Planare Flaechen pro Hoehe
# 'contour_patch' → NURBS-Patch-Surface durch alle Konturen
# Wir generieren einmal die echten 3D-Curves und teilen # Wir generieren einmal die echten 3D-Curves und teilen
# sie auf die drei Outputs auf. # sie auf die Outputs auf.
contour_kinds = ("contours", "contour_tin", "contour_schicht") 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 need_contours = any(k in kinds for k in contour_kinds) and merged_grid is not None
raw_contours = [] raw_contours = []
if need_contours: if need_contours:
@@ -7762,6 +8093,25 @@ class ElementeBridge(panel_base.BaseBridge):
len(schicht_objs))) len(schicht_objs)))
except Exception as ex: except Exception as ex:
self._push_log("Schichtenmodell-Fehler: {}".format(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 # Layer-Move auf aktive Geschoss/80_swisstopo Sublayer
if z_id and mesh_objects: if z_id and mesh_objects:
sub_name = _find_ebene_sublayer_name( 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_angle = max(0.0, min(180.0, o_angle))
o_swinv = bool(p.get("swingInvert", o_swinv = bool(p.get("swingInvert",
old_meta.get("oeff_swing_invert", False))) 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"): if p.get("styleId") and p.get("styleId") != old_meta.get("oeff_style_id"):
# Style wurde NEU gewaehlt → seine Werte applizieren # Style wurde NEU gewaehlt → seine Werte applizieren
styles = list_oeff_styles(doc) styles = list_oeff_styles(doc)
@@ -8881,7 +9233,8 @@ class ElementeBridge(panel_base.BaseBridge):
oeff_tuer_typ=o_tuer_typ, oeff_tuer_typ=o_tuer_typ,
oeff_hinge_side=o_hinge, oeff_hinge_side=o_hinge,
oeff_open_angle=o_angle, 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.Attributes = attrs
axis_obj.CommitChanges() axis_obj.CommitChanges()
parent_id = old_meta.get("oeff_parent", "") parent_id = old_meta.get("oeff_parent", "")
@@ -8932,7 +9285,7 @@ class ElementeBridge(panel_base.BaseBridge):
g = _geschoss_by_id(doc, geschoss) g = _geschoss_by_id(doc, geschoss)
geschoss_name = g.get("name", "EG") if g else "EG" geschoss_name = g.get("name", "EG") if g else "EG"
if old_meta["type"] == "wand_axis": 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": elif old_meta["type"] == "decke_outline":
attrs.LayerIndex = _ensure_layer(doc, _layer_path_decke(doc, geschoss_name)) attrs.LayerIndex = _ensure_layer(doc, _layer_path_decke(doc, geschoss_name))
elif old_meta["type"] == "dach_outline": elif old_meta["type"] == "dach_outline":
@@ -9953,6 +10306,145 @@ def _sync_orphan_grips(doc):
sc.sticky[_SELECT_BUSY] = False 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): def _on_idle_selection(sender, e):
"""Pollt periodisch die Selektion + verarbeitet Pending-Regenerate-Queue. """Pollt periodisch die Selektion + verarbeitet Pending-Regenerate-Queue.
@@ -9974,6 +10466,15 @@ def _on_idle_selection(sender, e):
doc = Rhino.RhinoDoc.ActiveDoc doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return 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). # 1) Pending Regenerations abarbeiten — debounct (50 ms Ruhe).
# Kein EnableDrawing-Suspend mehr (das hat User-Feedback langsamer # Kein EnableDrawing-Suspend mehr (das hat User-Feedback langsamer
# gemacht und konnte das Volumen "verschwinden" lassen wenn der # 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: except Exception as ex:
print("[OBERLEISTE] open layer-combinations:", 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 ----------------------------------- # --- Command-Line Integration -----------------------------------
elif t == "RUN_COMMAND": elif t == "RUN_COMMAND":
cmd = (p.get("cmd") or "").strip() cmd = (p.get("cmd") or "").strip()
+16 -4
View File
@@ -1148,10 +1148,22 @@ class EbenenBridge(panel_base.BaseBridge):
return return
try: try:
ebenen = json.loads(raw) ebenen = json.loads(raw)
for e in ebenen: # Rekursive Suche — Sub-Ebenen (z.B. WAENDE→Öffnungen→Sturz mit
if e.get("code") == code: # Code 20o7) liegen mehrere Ebenen tief. Frueher nur Top-Level
e[field] = value # iteriert → Style-Changes an nested Sublayer wurden nicht
break # 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)) doc.Strings.SetString("dossier_ebenen", json.dumps(ebenen, ensure_ascii=False))
_broadcast_state(doc) _broadcast_state(doc)
except Exception as ex: except Exception as ex:
+65
View File
@@ -844,6 +844,71 @@ def generate_schichtenmodell(doc, contour_curves, progress=None):
return created 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, def generate_contour_curves(grid, shift_lv95, m_to_unit, interval=2.0,
progress=None): progress=None):
"""Generiert Hoehenlinien (Contour-Curves) aus dem Terrain-Grid via """Generiert Hoehenlinien (Contour-Curves) aus dem Terrain-Grid via
+2 -2
View File
@@ -90,13 +90,13 @@ export default function AusschnittSettingsApp() {
</Field> </Field>
<Field label="DARSTELLUNG" <Field label="DARSTELLUNG"
hint="SIA-400 Detaillierungsgrad — leer = per-Element-Setting respektieren"> hint="SIA-400 Detaillierungsgrad fuer diesen Ausschnitt — leer = beim Restore nicht aendern">
<select <select
value={snap.darstellung || ''} value={snap.darstellung || ''}
onChange={(ev) => set({ darstellung: ev.target.value })} onChange={(ev) => set({ darstellung: ev.target.value })}
style={{ flex: 1, fontSize: 11, minWidth: 0 }} style={{ flex: 1, fontSize: 11, minWidth: 0 }}
> >
<option value=""> per Element </option> <option value=""> nicht aendern </option>
<option value="einfach">Einfach (1:100)</option> <option value="einfach">Einfach (1:100)</option>
<option value="standard">Standard (1:50)</option> <option value="standard">Standard (1:50)</option>
<option value="detail">Detail (1:20)</option> <option value="detail">Detail (1:20)</option>
+10 -3
View File
@@ -22,6 +22,8 @@ function fmtNum(v) {
// Input-Komponente: zeigt formatierten Wert, sendet onCommit bei Enter/Blur. // Input-Komponente: zeigt formatierten Wert, sendet onCommit bei Enter/Blur.
// Verhindert Update waehrend des Tippens, damit der Cursor nicht springt. // Verhindert Update waehrend des Tippens, damit der Cursor nicht springt.
// Pill-Chrome (Border, Radius, bg-input) kommt aus dem globalen CSS
// hier nur der Flex-Container fuer Input + optionalen Suffix.
function NumInput({ value, onCommit, disabled, suffix, width }) { function NumInput({ value, onCommit, disabled, suffix, width }) {
const [text, setText] = useState(fmtNum(value)) const [text, setText] = useState(fmtNum(value))
const [focused, setFocused] = useState(false) const [focused, setFocused] = useState(false)
@@ -32,7 +34,8 @@ function NumInput({ value, onCommit, disabled, suffix, width }) {
else setText(fmtNum(value)) // ungueltig zurueck auf alten Wert else setText(fmtNum(value)) // ungueltig zurueck auf alten Wert
} }
return ( return (
<div style={{ display: 'flex', alignItems: 'center', gap: 4, flex: width ? 0 : 1, width }}> <div style={{ display: 'flex', alignItems: 'center', gap: 4,
flex: width ? 0 : 1, width }}>
<input <input
type="text" type="text"
value={text} value={text}
@@ -44,9 +47,13 @@ function NumInput({ value, onCommit, disabled, suffix, width }) {
if (e.key === 'Enter') { e.target.blur() } if (e.key === 'Enter') { e.target.blur() }
else if (e.key === 'Escape') { setText(fmtNum(value)); e.target.blur() } else if (e.key === 'Escape') { setText(fmtNum(value)); e.target.blur() }
}} }}
style={{ flex: 1, width: '100%', fontFamily: 'DM Mono, monospace', fontSize: 11, textAlign: 'right' }} style={{ flex: 1, width: '100%', textAlign: 'right' }}
/> />
{suffix && <span style={{ fontSize: 10, color: 'var(--text-muted)', flexShrink: 0 }}>{suffix}</span>} {suffix && (
<span style={{ fontSize: 10, color: 'var(--text-muted)', flexShrink: 0 }}>
{suffix}
</span>
)}
</div> </div>
) )
} }
+21 -2
View File
@@ -1777,9 +1777,10 @@ function OeffnungProperties({ oeff, onUpdate, onDelete, oeffStyles = [] }) {
</span> </span>
<div style={{ flex: 1, display: 'flex' }}> <div style={{ flex: 1, display: 'flex' }}>
<BarCombo <BarCombo
value={oeff.darstellung || 'standard'} value={oeff.darstellung || 'auto'}
onChange={(v) => onUpdate({ darstellung: v })} onChange={(v) => onUpdate({ darstellung: v })}
title="Detaillierungsgrad — beeinflusst die generierte Geometrie"> title="Detaillierungsgrad — Auto folgt der Modelldarstellung in der Topbar">
<option value="auto">Nach Modelldarstellung</option>
<option value="einfach">Einfach (1:100)</option> <option value="einfach">Einfach (1:100)</option>
<option value="standard">Standard (1:50)</option> <option value="standard">Standard (1:50)</option>
<option value="detail">Detail (1:20)</option> <option value="detail">Detail (1:20)</option>
@@ -1874,6 +1875,24 @@ function OeffnungProperties({ oeff, onUpdate, onDelete, oeffStyles = [] }) {
</div> </div>
)} )}
{!isFenster && (oeff.tuerTyp || 'normal') === 'normal' && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
title="Sturzlinien-Anzeige bei 1:100 (gestrichelt). Aussen = Linie an Wand-Aussenseite, Innen = Wand-Innenseite, Beide = beide Linien">
Sturz
</span>
<select
value={oeff.sturz || 'beide'}
onChange={(e) => onUpdate({ sturz: e.target.value })}
style={{ flex: 1, fontSize: 11 }}>
<option value="keine">Keine</option>
<option value="innen">Innen</option>
<option value="aussen">Aussen</option>
<option value="beide">Beide</option>
</select>
</div>
)}
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Breite</span> <span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Breite</span>
<input type="text" value={breite} <input type="text" value={breite}
+62 -3
View File
@@ -15,6 +15,7 @@ import {
openAbout, createText, setTextSettings, openAbout, createText, setTextSettings,
applyTextStyle, saveTextStyle, deleteTextStyle, applyTextStyle, saveTextStyle, deleteTextStyle,
setDarstellung, setDarstellung,
arrangeSelection,
} from './lib/rhinoBridge' } from './lib/rhinoBridge'
const PRESETS = [ const PRESETS = [
@@ -403,12 +404,11 @@ export default function OberleisteApp() {
{/* Reihe 1, Spalte 2: Modelldarstellung (SIA-400 LoD) */} {/* Reihe 1, Spalte 2: Modelldarstellung (SIA-400 LoD) */}
<BarCombo <BarCombo
icon="tune" icon="tune"
value={state.aktiveDarstellung || ''} value={state.aktiveDarstellung || 'einfach'}
onChange={(v) => setDarstellung(v)} onChange={(v) => setDarstellung(v)}
title="Darstellungs-Override fuer Fenster/Tueren (SIA-400 LoD)" title="Modelldarstellung — Default fuer Fenster/Tueren auf 'Auto'. Einzelobjekt-Override im Properties-Panel."
width={PRESET_W} width={PRESET_W}
> >
<option value=""> per Element </option>
<option value="einfach">Einfach (1:100)</option> <option value="einfach">Einfach (1:100)</option>
<option value="standard">Standard (1:50)</option> <option value="standard">Standard (1:50)</option>
<option value="detail">Detail (1:20)</option> <option value="detail">Detail (1:20)</option>
@@ -631,6 +631,65 @@ export default function OberleisteApp() {
<div style={sep} /> <div style={sep} />
{/* ====== ANORDNEN (2D-Z-Stack via Rhino-DisplayOrder) ======
2x2-Grid (quadratisch): oben Aufwaerts-Aktionen, unten Abwaerts.
Reihe 1: Vorderste | 1 hoch (vertical_align_top, expand_less)
Reihe 2: 1 runter | Hinterste (expand_more, vertical_align_bottom)
Selection-Check + Rhino-DisplayOrder im Backend, keine Z-Offsets. */}
{(() => {
const CELL = 26 // quadratisch: 2 * CELL Breite, ~2 * BAR_H Hoehe
const Btn = ({ icon, dir, title, isFirst }) => (
<button onClick={() => arrangeSelection(dir)} title={title}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'var(--bg-item-hover)'
e.currentTarget.style.color = 'var(--accent-light)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'var(--bg-input)'
e.currentTarget.style.color = 'var(--text-primary)'
}}
style={{
height: BAR_H, minHeight: BAR_H, maxHeight: BAR_H, width: CELL,
background: 'var(--bg-input)', color: 'var(--text-primary)',
border: 'none',
borderLeft: isFirst ? 'none' : '1px solid var(--border)',
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
cursor: 'pointer', padding: 0, flexShrink: 0,
appearance: 'none', WebkitAppearance: 'none',
lineHeight: 1, boxSizing: 'border-box',
transition: 'background 0.15s, color 0.15s',
}}>
<Icon name={icon} size={11} />
</button>
)
const rowStyle = {
display: 'inline-flex', width: CELL * 2,
height: BAR_H + 2, boxSizing: 'border-box',
border: '1px solid var(--border)', borderRadius: 999,
overflow: 'hidden', flexShrink: 0,
}
return (
<div style={{
display: 'flex', flexDirection: 'column', gap: 4, flexShrink: 0,
}}>
<div style={rowStyle}>
<Btn icon="vertical_align_top" dir="front" isFirst
title="In den Vordergrund (Bring to Front)" />
<Btn icon="expand_less" dir="forward"
title="Eine Stufe hoch (Bring Forward)" />
</div>
<div style={rowStyle}>
<Btn icon="expand_more" dir="backward" isFirst
title="Eine Stufe runter (Send Backward)" />
<Btn icon="vertical_align_bottom" dir="back"
title="In den Hintergrund (Send to Back)" />
</div>
</div>
)
})()}
<div style={sep} />
{/* ====== TEXT-Block (Vectorworks-Stil) ====== {/* ====== TEXT-Block (Vectorworks-Stil) ======
Reihe 1: Style | Font | Size Reihe 1: Style | Font | Size
Reihe 2: [B][I][U] | [L][C][R] | [+] Reihe 2: [B][I][U] | [L][C][R] | [+]
+13 -2
View File
@@ -64,6 +64,7 @@ export default function SwisstopoApp() {
const [getContours, setGetContours] = useState(false) const [getContours, setGetContours] = useState(false)
const [getContourTin,setGetContourTin]= useState(false) const [getContourTin,setGetContourTin]= useState(false)
const [getContourSchicht, setGetContourSchicht] = useState(false) const [getContourSchicht, setGetContourSchicht] = useState(false)
const [getContourPatch, setGetContourPatch] = useState(false)
const [contourInt, setContourInt] = useState('2.0') const [contourInt, setContourInt] = useState('2.0')
// TLM3D deaktiviert: swisstopo liefert nur GDB/SHP/GPKG kein DXF. // TLM3D deaktiviert: swisstopo liefert nur GDB/SHP/GPKG kein DXF.
// Rhino kann das nicht nativ importieren; OSM-Importer ist die Alternative // Rhino kann das nicht nativ importieren; OSM-Importer ist die Alternative
@@ -134,7 +135,7 @@ export default function SwisstopoApp() {
const handleImport = () => { const handleImport = () => {
if (!center) { addLog('Bitte zuerst einen Standort wählen'); return } if (!center) { addLog('Bitte zuerst einen Standort wählen'); return }
if (!getBuild && !getTerrain && !getContours && !getContourTin && !getContourSchicht && !getTlm) { if (!getBuild && !getTerrain && !getContours && !getContourTin && !getContourSchicht && !getContourPatch && !getTlm) {
addLog('Mindestens eine Datenquelle wählen'); return addLog('Mindestens eine Datenquelle wählen'); return
} }
setLogs([]) setLogs([])
@@ -147,6 +148,7 @@ export default function SwisstopoApp() {
if (getContours) kinds.push('contours') if (getContours) kinds.push('contours')
if (getContourTin) kinds.push('contour_tin') if (getContourTin) kinds.push('contour_tin')
if (getContourSchicht)kinds.push('contour_schicht') if (getContourSchicht)kinds.push('contour_schicht')
if (getContourPatch) kinds.push('contour_patch')
if (getTlm) kinds.push('tlm') if (getTlm) kinds.push('tlm')
const tlmList = Object.entries(tlmKinds).filter(([, v]) => v).map(([k]) => k) const tlmList = Object.entries(tlmKinds).filter(([, v]) => v).map(([k]) => k)
send('RUN_IMPORT', { send('RUN_IMPORT', {
@@ -344,7 +346,16 @@ export default function SwisstopoApp() {
<Icon name="stacks" size={13} /> Schichtenmodell aus Höhenlinien <Icon name="stacks" size={13} /> Schichtenmodell aus Höhenlinien
</label> </label>
</Field> </Field>
{(getContours || getContourTin || getContourSchicht) && ( <Field label=""
hint="Patch-Terrain: NURBS-Surface gefittet durch alle Höhenlinien (Rhinos Patch-Befehl). Glatte, kontinuierliche Topo-Oberfläche — die kanonische Methode für Terrain aus Konturen.">
<label style={{ display: 'flex', alignItems: 'center', gap: 6,
fontSize: 11, cursor: 'pointer' }}>
<input type="checkbox" checked={getContourPatch}
onChange={(e) => setGetContourPatch(e.target.checked)} />
<Icon name="landscape" size={13} /> Patch-Terrain aus Höhenlinien
</label>
</Field>
{(getContours || getContourTin || getContourSchicht || getContourPatch) && (
<Field label="HÖHEN-ABSTAND"> <Field label="HÖHEN-ABSTAND">
<Radio value={contourInt} <Radio value={contourInt}
options={[ options={[
+7 -2
View File
@@ -263,15 +263,20 @@ function EbeneRow({ e, depth, hasChildren, expanded, onToggleExpand, active, mod
minHeight: 24, minHeight: 24,
}} }}
> >
{/* Chevron sitzt visuell weiter rechts (marginLeft) marginRight
kompensiert das, damit die nachfolgenden Elemente (Auge, Code,
Farbe, Name) nicht mitrutschen. Spacer fuer kinderlose Zeilen
spiegelt dasselbe Offset, sonst springt die Eye-Spalte zwischen
Parent- und Leaf-Zeilen. */}
{hasChildren ? ( {hasChildren ? (
<button <button
className="btn-icon-xs" className="btn-icon-xs"
onClick={(ev) => { ev.stopPropagation(); onToggleExpand() }} onClick={(ev) => { ev.stopPropagation(); onToggleExpand() }}
title={expanded ? 'Einklappen' : 'Aufklappen'} title={expanded ? 'Einklappen' : 'Aufklappen'}
style={{ width: 12, height: 12 }} style={{ width: 12, height: 12, marginLeft: 6, marginRight: -6 }}
><Icon name={expanded ? 'expand_more' : 'chevron_right'} size={11} /></button> ><Icon name={expanded ? 'expand_more' : 'chevron_right'} size={11} /></button>
) : ( ) : (
<span style={{ width: 12, flexShrink: 0 }} /> <span style={{ width: 12, flexShrink: 0, marginLeft: 6, marginRight: -6 }} />
)} )}
<button <button
className={`btn-icon-sm ${eyeOn ? 'is-on' : ''}`} className={`btn-icon-sm ${eyeOn ? 'is-on' : ''}`}
+20 -4
View File
@@ -161,17 +161,33 @@ input, select {
background: var(--bg-input); background: var(--bg-input);
color: var(--text-primary); color: var(--text-primary);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--r); border-radius: 999px;
padding: 5px 8px; padding: 4px 12px;
outline: none; outline: none;
transition: border-color 0.16s, box-shadow 0.16s; transition: border-color 0.16s, background 0.16s, box-shadow 0.16s;
}
input:hover {
border-color: var(--accent);
background: var(--bg-item-hover);
} }
input:hover { border-color: var(--text-muted); }
input:focus, select:focus { input:focus, select:focus {
border-color: var(--accent); border-color: var(--accent);
box-shadow: 0 0 0 2px var(--accent-dim); box-shadow: 0 0 0 2px var(--accent-dim);
} }
input[type="number"]::-webkit-inner-spin-button { opacity: 0.3; } input[type="number"]::-webkit-inner-spin-button { opacity: 0.3; }
/* Checkboxes + Color-Picker: kein Pill — native rendering. */
input[type="checkbox"], input[type="radio"], input[type="color"],
input[type="file"], input[type="range"] {
border-radius: 0;
padding: 0;
background: transparent;
}
input[type="checkbox"]:hover, input[type="radio"]:hover,
input[type="color"]:hover, input[type="file"]:hover,
input[type="range"]:hover {
background: transparent;
border-color: var(--border);
}
/* Pill-shaped select */ /* Pill-shaped select */
select { select {
+2
View File
@@ -173,6 +173,8 @@ export function openKameraPanel() { send('OPEN_KAMERA_PANEL', {}) }
export function openElementeUebersicht() { send('OPEN_ELEMENTE_UEBERSICHT', {}) } export function openElementeUebersicht() { send('OPEN_ELEMENTE_UEBERSICHT', {}) }
export function openElementeProperties() { send('OPEN_ELEMENTE_PROPERTIES', {}) } export function openElementeProperties() { send('OPEN_ELEMENTE_PROPERTIES', {}) }
export function setDarstellung(d) { send('SET_DARSTELLUNG', { darstellung: d || '' }) } export function setDarstellung(d) { send('SET_DARSTELLUNG', { darstellung: d || '' }) }
// Anordnen — 2D-Z-Stack via Rhino-DisplayOrder. dir: 'front'|'forward'|'backward'|'back'
export function arrangeSelection(dir) { send('ARRANGE', { dir }) }
export function saveOeffStyle(name, settings) { export function saveOeffStyle(name, settings) {
send('SAVE_OEFF_STYLE', { name, settings }) send('SAVE_OEFF_STYLE', { name, settings })
} }