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:
+559
-58
@@ -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 0–180 (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.0–1.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
|
||||
|
||||
@@ -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()
|
||||
|
||||
+14
-2
@@ -1148,10 +1148,22 @@ class EbenenBridge(panel_base.BaseBridge):
|
||||
return
|
||||
try:
|
||||
ebenen = json.loads(raw)
|
||||
for e in ebenen:
|
||||
# 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
|
||||
break
|
||||
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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -90,13 +90,13 @@ export default function AusschnittSettingsApp() {
|
||||
</Field>
|
||||
|
||||
<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
|
||||
value={snap.darstellung || ''}
|
||||
onChange={(ev) => set({ darstellung: ev.target.value })}
|
||||
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="standard">Standard (1:50)</option>
|
||||
<option value="detail">Detail (1:20)</option>
|
||||
|
||||
+10
-3
@@ -22,6 +22,8 @@ function fmtNum(v) {
|
||||
|
||||
// Input-Komponente: zeigt formatierten Wert, sendet onCommit bei Enter/Blur.
|
||||
// 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 }) {
|
||||
const [text, setText] = useState(fmtNum(value))
|
||||
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
|
||||
}
|
||||
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
|
||||
type="text"
|
||||
value={text}
|
||||
@@ -44,9 +47,13 @@ function NumInput({ value, onCommit, disabled, suffix, width }) {
|
||||
if (e.key === 'Enter') { 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>
|
||||
)
|
||||
}
|
||||
|
||||
+21
-2
@@ -1777,9 +1777,10 @@ function OeffnungProperties({ oeff, onUpdate, onDelete, oeffStyles = [] }) {
|
||||
</span>
|
||||
<div style={{ flex: 1, display: 'flex' }}>
|
||||
<BarCombo
|
||||
value={oeff.darstellung || 'standard'}
|
||||
value={oeff.darstellung || 'auto'}
|
||||
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="standard">Standard (1:50)</option>
|
||||
<option value="detail">Detail (1:20)</option>
|
||||
@@ -1874,6 +1875,24 @@ function OeffnungProperties({ oeff, onUpdate, onDelete, oeffStyles = [] }) {
|
||||
</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 }}>
|
||||
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Breite</span>
|
||||
<input type="text" value={breite}
|
||||
|
||||
+62
-3
@@ -15,6 +15,7 @@ import {
|
||||
openAbout, createText, setTextSettings,
|
||||
applyTextStyle, saveTextStyle, deleteTextStyle,
|
||||
setDarstellung,
|
||||
arrangeSelection,
|
||||
} from './lib/rhinoBridge'
|
||||
|
||||
const PRESETS = [
|
||||
@@ -403,12 +404,11 @@ export default function OberleisteApp() {
|
||||
{/* Reihe 1, Spalte 2: Modelldarstellung (SIA-400 LoD) */}
|
||||
<BarCombo
|
||||
icon="tune"
|
||||
value={state.aktiveDarstellung || ''}
|
||||
value={state.aktiveDarstellung || 'einfach'}
|
||||
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}
|
||||
>
|
||||
<option value="">— per Element —</option>
|
||||
<option value="einfach">Einfach (1:100)</option>
|
||||
<option value="standard">Standard (1:50)</option>
|
||||
<option value="detail">Detail (1:20)</option>
|
||||
@@ -631,6 +631,65 @@ export default function OberleisteApp() {
|
||||
|
||||
<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) ======
|
||||
Reihe 1: Style ▼ | Font ▼ | Size ▼
|
||||
Reihe 2: [B][I][U] | [L][C][R] | [+]
|
||||
|
||||
+13
-2
@@ -64,6 +64,7 @@ export default function SwisstopoApp() {
|
||||
const [getContours, setGetContours] = useState(false)
|
||||
const [getContourTin,setGetContourTin]= useState(false)
|
||||
const [getContourSchicht, setGetContourSchicht] = useState(false)
|
||||
const [getContourPatch, setGetContourPatch] = useState(false)
|
||||
const [contourInt, setContourInt] = useState('2.0')
|
||||
// TLM3D deaktiviert: swisstopo liefert nur GDB/SHP/GPKG — kein DXF.
|
||||
// Rhino kann das nicht nativ importieren; OSM-Importer ist die Alternative
|
||||
@@ -134,7 +135,7 @@ export default function SwisstopoApp() {
|
||||
|
||||
const handleImport = () => {
|
||||
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
|
||||
}
|
||||
setLogs([])
|
||||
@@ -147,6 +148,7 @@ export default function SwisstopoApp() {
|
||||
if (getContours) kinds.push('contours')
|
||||
if (getContourTin) kinds.push('contour_tin')
|
||||
if (getContourSchicht)kinds.push('contour_schicht')
|
||||
if (getContourPatch) kinds.push('contour_patch')
|
||||
if (getTlm) kinds.push('tlm')
|
||||
const tlmList = Object.entries(tlmKinds).filter(([, v]) => v).map(([k]) => k)
|
||||
send('RUN_IMPORT', {
|
||||
@@ -344,7 +346,16 @@ export default function SwisstopoApp() {
|
||||
<Icon name="stacks" size={13} /> Schichtenmodell aus Höhenlinien
|
||||
</label>
|
||||
</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">
|
||||
<Radio value={contourInt}
|
||||
options={[
|
||||
|
||||
@@ -263,15 +263,20 @@ function EbeneRow({ e, depth, hasChildren, expanded, onToggleExpand, active, mod
|
||||
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 ? (
|
||||
<button
|
||||
className="btn-icon-xs"
|
||||
onClick={(ev) => { ev.stopPropagation(); onToggleExpand() }}
|
||||
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>
|
||||
) : (
|
||||
<span style={{ width: 12, flexShrink: 0 }} />
|
||||
<span style={{ width: 12, flexShrink: 0, marginLeft: 6, marginRight: -6 }} />
|
||||
)}
|
||||
<button
|
||||
className={`btn-icon-sm ${eyeOn ? 'is-on' : ''}`}
|
||||
|
||||
+20
-4
@@ -161,17 +161,33 @@ input, select {
|
||||
background: var(--bg-input);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--r);
|
||||
padding: 5px 8px;
|
||||
border-radius: 999px;
|
||||
padding: 4px 12px;
|
||||
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 {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 2px var(--accent-dim);
|
||||
}
|
||||
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 */
|
||||
select {
|
||||
|
||||
@@ -173,6 +173,8 @@ export function openKameraPanel() { send('OPEN_KAMERA_PANEL', {}) }
|
||||
export function openElementeUebersicht() { send('OPEN_ELEMENTE_UEBERSICHT', {}) }
|
||||
export function openElementeProperties() { send('OPEN_ELEMENTE_PROPERTIES', {}) }
|
||||
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) {
|
||||
send('SAVE_OEFF_STYLE', { name, settings })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user