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

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

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

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

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-22 12:34:15 +02:00
parent 9ae8574ab0
commit 0c5f8055a5
13 changed files with 899 additions and 220 deletions
+29
View File
@@ -327,6 +327,8 @@ class AusschnittBridge(panel_base.BaseBridge):
elif t == "DELETE": self._delete(p.get("id"))
elif t == "SET_FOLDER": self._set_field(p.get("id"), "folder", p.get("folder") or "")
elif t == "SET_SCALE": self._set_field(p.get("id"), "scale", p.get("scale") or "")
elif t == "SET_DARSTELLUNG": self._set_field(p.get("id"), "darstellung",
p.get("darstellung") or "")
elif t == "DUPLICATE": self._duplicate(p.get("id"))
elif t == "ADD_FOLDER": self._add_folder(p.get("name"))
elif t == "REMOVE_FOLDER": self._remove_folder(p.get("name"))
@@ -494,6 +496,13 @@ class AusschnittBridge(panel_base.BaseBridge):
print("[AUSSCHNITTE] Live-Skala (Fallback):", ex)
if not scale_str and prior_scale:
scale_str = prior_scale # Perspective -> alten Wert nicht ueberschreiben
# Darstellungs-Override aus dem aktuellen Doc-Setting uebernehmen.
# Leer = "kein Override, per-Object respektieren".
darst = ""
try:
import elemente
darst = elemente.get_aktive_darstellung(doc) or ""
except Exception: pass
return {
"id": existing_id or "snap_" + uuid.uuid4().hex[:8],
"name": name,
@@ -501,6 +510,7 @@ class AusschnittBridge(panel_base.BaseBridge):
"camera": _capture_camera(vp),
"layers": _capture_layers(doc),
"dossier": _capture_dossier_state(doc),
"darstellung": darst,
}
def _save(self, name):
@@ -558,6 +568,21 @@ class AusschnittBridge(panel_base.BaseBridge):
rhinopanel._notify_oberleiste_combs()
except Exception: pass
_apply_dossier_state(doc, snap.get("dossier") or snap.get("pause") or {})
# Darstellung anwenden + Oeffnungen regenerieren
try:
import elemente
new_darst = snap.get("darstellung") or ""
cur_darst = elemente.get_aktive_darstellung(doc) or ""
if new_darst != cur_darst:
elemente.set_aktive_darstellung(doc, new_darst)
elemente.regenerate_all_oeffnungen(doc)
# Oberleiste-Topbar muss neuen Wert spiegeln
try:
b = sc.sticky.get("oberleiste_bridge")
if b is not None: b._send_state(force=True)
except Exception: pass
except Exception as ex:
print("[AUSSCHNITTE] darstellung apply:", ex)
# Overrides: nur anwenden wenn das Snap "applyOverrides" gesetzt hat.
# Sonst bleibt der aktuelle User-Override-State unangetastet.
if snap.get("applyOverrides"):
@@ -786,6 +811,7 @@ class AusschnittBridge(panel_base.BaseBridge):
"overridesEnabled": bool(sn.get("overridesEnabled", False)),
"overridesPreset": sn.get("overridesPreset") or "",
"layerCombination": sn.get("layerCombination") or "",
"darstellung": sn.get("darstellung") or "",
},
"displayModes": display_modes,
"overridesPresets": overrides_presets,
@@ -815,6 +841,9 @@ class AusschnittBridge(panel_base.BaseBridge):
target["overridesPreset"] = (settings.get("overridesPreset") or "").strip()
# Ebenenkombi
target["layerCombination"] = (settings.get("layerCombination") or "").strip()
# Darstellung (SIA-400 LoD Override fuer diesen Ausschnitt)
darst = (settings.get("darstellung") or "").strip()
target["darstellung"] = darst if darst in ("einfach", "standard", "detail") else ""
outer._store(d, snaps)
outer._send_list()
print("[AUSSCHNITTE] Settings fuer '{}' aktualisiert".format(target.get("name")))
+491 -41
View File
@@ -59,6 +59,182 @@ _KEY_OEFF_SIMS_AUS = "dossier_oeff_sims_aus" # Style: "ohne"|"schmal"|"
_KEY_OEFF_SIMS_IN = "dossier_oeff_sims_in" # Style: "ohne"|"schmal"|"standard"|"breit"
_KEY_OEFF_GLAS = "dossier_oeff_glas" # "1"|"0" — sichtbare Glas-Scheibe
_KEY_OEFF_REFERENZ = "dossier_oeff_referenz" # "mid" | "links" | "rechts" — Lage des Klick-Punkts in der Oeffnung
_KEY_OEFF_DARSTELLUNG = "dossier_oeff_darstellung" # "einfach" | "standard" | "detail" — SIA-400 LoD
_KEY_OEFF_AUSSENSEITE = "dossier_oeff_aussenseite" # "links" | "rechts" — welche Wand-Seite ist aussen
_KEY_OEFF_TUER_RAHMEN = "dossier_oeff_tuer_rahmen" # "zarge" | "block" — Tueren-Rahmen-Typ
_KEY_OEFF_RAHMEN_OFFSET = "dossier_oeff_rahmen_offset" # float (m): Abstand Rahmen-Innenkante von Wand-Innenseite
_KEY_AKTIVE_DARSTELLUNG = "dossier_aktive_darstellung" # doc-level global override: einfach|standard|detail|"" (= per-object)
_KEY_OEFF_STYLE_ID = "dossier_oeff_style_id" # per-Object: referenziert einen Style aus dossier_oeff_styles
_KEY_OEFF_STYLES = "dossier_oeff_styles" # JSON-Liste aller Fenster/Tueren-Styles
_KEY_OEFF_STYLE_ACTIVE = "dossier_oeff_style_active" # zuletzt benutzte Style-ID (pro typ)
_OEFF_DARSTELLUNGEN = ("einfach", "standard", "detail")
def get_aktive_darstellung(doc):
"""Liest die doc-level Darstellungs-Override. Leer → per-object."""
if doc is None: return ""
try:
v = doc.Strings.GetValue(_KEY_AKTIVE_DARSTELLUNG) or ""
except Exception:
v = ""
return v if v in _OEFF_DARSTELLUNGEN else ""
def set_aktive_darstellung(doc, value):
"""Setzt die doc-level Darstellungs-Override. Leer → clear."""
if doc is None: return
try:
if value and value in _OEFF_DARSTELLUNGEN:
doc.Strings.SetString(_KEY_AKTIVE_DARSTELLUNG, value)
else:
doc.Strings.Delete(_KEY_AKTIVE_DARSTELLUNG)
except Exception as ex:
print("[ELEMENTE] set_aktive_darstellung:", ex)
# ---------------------------------------------------------------------------
# Fenster/Tueren-Styles (Presets) — analog text_create.list_styles
_OEFF_STYLE_FIELDS = (
"typ", "breite", "hoehe", "brueest",
"rahmenB", "rahmenTiefe", "rahmenOffset",
"fluegel", "simsAus", "glas",
"darstellung", "tuerRahmen",
)
_OEFF_DEFAULT_STYLES = [
{"name": "Fenster Standard", "typ": "fenster",
"breite": 1.20, "hoehe": 1.40, "brueest": 0.90,
"rahmenB": 0.06, "rahmenTiefe": 0.08, "rahmenOffset": 0.05,
"fluegel": 2, "simsAus": "standard", "glas": True,
"darstellung": "standard"},
{"name": "Fenster Gross", "typ": "fenster",
"breite": 2.00, "hoehe": 1.80, "brueest": 0.40,
"rahmenB": 0.08, "rahmenTiefe": 0.10, "rahmenOffset": 0.05,
"fluegel": 3, "simsAus": "breit", "glas": True,
"darstellung": "standard"},
{"name": "Fenster Bandlage (boden)", "typ": "fenster",
"breite": 3.00, "hoehe": 0.60, "brueest": 0.00,
"rahmenB": 0.06, "rahmenTiefe": 0.08, "rahmenOffset": 0.05,
"fluegel": 3, "simsAus": "schmal", "glas": True,
"darstellung": "standard"},
{"name": "Tuer Innen", "typ": "tuer",
"breite": 0.90, "hoehe": 2.10, "brueest": 0.00,
"rahmenB": 0.05, "rahmenTiefe": 0.08, "rahmenOffset": 0.05,
"fluegel": 1, "simsAus": "ohne", "glas": False,
"darstellung": "standard", "tuerRahmen": "zarge"},
{"name": "Tuer Eingang", "typ": "tuer",
"breite": 1.00, "hoehe": 2.20, "brueest": 0.00,
"rahmenB": 0.08, "rahmenTiefe": 0.10, "rahmenOffset": 0.05,
"fluegel": 1, "simsAus": "ohne", "glas": False,
"darstellung": "standard", "tuerRahmen": "block"},
{"name": "Tuer Verglast", "typ": "tuer",
"breite": 0.90, "hoehe": 2.10, "brueest": 0.00,
"rahmenB": 0.06, "rahmenTiefe": 0.08, "rahmenOffset": 0.05,
"fluegel": 1, "simsAus": "ohne", "glas": True,
"darstellung": "standard", "tuerRahmen": "zarge"},
]
def _normalize_oeff_style(s):
"""Filtert auf erlaubte Felder + setzt sinnvolle Defaults."""
out = {}
for k in _OEFF_STYLE_FIELDS:
if k in s: out[k] = s[k]
return out
def list_oeff_styles(doc, typ=None):
"""Liste aller Window/Tuer-Styles. typ='fenster'|'tuer' filtert.
Seedet Defaults beim ersten Zugriff."""
if doc is None: return []
import json, uuid
try:
raw = doc.Strings.GetValue(_KEY_OEFF_STYLES)
if not raw:
seeded = []
for i, s in enumerate(_OEFF_DEFAULT_STYLES):
norm = _normalize_oeff_style(s)
norm["id"] = "os_default_" + str(i)
norm["name"] = s["name"]
seeded.append(norm)
try: doc.Strings.SetString(_KEY_OEFF_STYLES, json.dumps(seeded))
except Exception: pass
items = seeded
else:
parsed = json.loads(raw)
items = parsed if isinstance(parsed, list) else []
out = []
for it in items:
if not isinstance(it, dict): continue
n = _normalize_oeff_style(it)
n["id"] = it.get("id") or ("os_" + uuid.uuid4().hex[:8])
n["name"] = it.get("name") or "Stil"
out.append(n)
if typ in ("fenster", "tuer"):
out = [s for s in out if s.get("typ") == typ]
return out
except Exception as ex:
print("[ELEMENTE] list_oeff_styles:", ex)
return []
def save_oeff_style(doc, name, settings):
"""Speichert (oder updated) einen Style unter `name`. Returns ID."""
if doc is None or not name: return None
import json, uuid
items_all = []
try:
raw = doc.Strings.GetValue(_KEY_OEFF_STYLES)
if raw:
parsed = json.loads(raw)
if isinstance(parsed, list):
items_all = [it for it in parsed if isinstance(it, dict)]
except Exception: pass
sid = None
for it in items_all:
if it.get("name") == name:
sid = it.get("id"); break
norm = _normalize_oeff_style(settings or {})
norm["id"] = sid or ("os_" + uuid.uuid4().hex[:8])
norm["name"] = name
if sid:
items_all = [norm if it.get("id") == sid else it for it in items_all]
else:
items_all.append(norm)
try: doc.Strings.SetString(_KEY_OEFF_STYLES, json.dumps(items_all))
except Exception as ex:
print("[ELEMENTE] save_oeff_style:", ex)
return norm["id"]
def delete_oeff_style(doc, sid):
if doc is None or not sid: return
import json
try:
items = list_oeff_styles(doc)
items = [it for it in items if it.get("id") != sid]
doc.Strings.SetString(_KEY_OEFF_STYLES, json.dumps(items))
except Exception: pass
def get_active_oeff_style_id(doc, typ):
if doc is None: return None
try:
raw = doc.Strings.GetValue(_KEY_OEFF_STYLE_ACTIVE + "_" + typ)
return raw or None
except Exception:
return None
def set_active_oeff_style_id(doc, typ, sid):
if doc is None or typ not in ("fenster", "tuer"): return
try:
doc.Strings.SetString(_KEY_OEFF_STYLE_ACTIVE + "_" + typ, sid or "")
except Exception: pass
_OEFF_AUSSENSEITEN = ("links", "rechts")
_OEFF_TUER_RAHMEN = ("zarge", "block")
_OEFF_REFERENZ_OPTIONS = ("mid", "links", "rechts")
@@ -1705,7 +1881,9 @@ def _attach_meta(obj_attrs, wall_id, type_, geschoss, dicke, uk_over, ok_over,
oeff_rahmen_b=None, oeff_rahmen_tiefe=None, oeff_rahmen_pos=None,
oeff_fluegel=None,
oeff_sims_aus=None, oeff_sims_in=None, oeff_glas=None,
oeff_referenz=None,
oeff_referenz=None, oeff_darstellung=None,
oeff_aussenseite=None, oeff_tuer_rahmen=None,
oeff_rahmen_offset=None, oeff_style_id=None,
geschoss_end=None, treppe_breite=None,
treppe_n=None, treppe_referenz=None,
treppe_modus=None, treppe_lauf_d=None, treppe_art=None,
@@ -1775,6 +1953,20 @@ def _attach_meta(obj_attrs, wall_id, type_, geschoss, dicke, uk_over, ok_over,
obj_attrs.SetUserString(_KEY_OEFF_GLAS, "1" if bool(oeff_glas) else "0")
if oeff_referenz is not None and oeff_referenz in _OEFF_REFERENZ_OPTIONS:
obj_attrs.SetUserString(_KEY_OEFF_REFERENZ, oeff_referenz)
if oeff_darstellung is not None and oeff_darstellung in _OEFF_DARSTELLUNGEN:
obj_attrs.SetUserString(_KEY_OEFF_DARSTELLUNG, oeff_darstellung)
if oeff_aussenseite is not None and oeff_aussenseite in _OEFF_AUSSENSEITEN:
obj_attrs.SetUserString(_KEY_OEFF_AUSSENSEITE, oeff_aussenseite)
if oeff_tuer_rahmen is not None and oeff_tuer_rahmen in _OEFF_TUER_RAHMEN:
obj_attrs.SetUserString(_KEY_OEFF_TUER_RAHMEN, oeff_tuer_rahmen)
if oeff_rahmen_offset is not None:
try:
v = max(0.0, float(oeff_rahmen_offset))
obj_attrs.SetUserString(_KEY_OEFF_RAHMEN_OFFSET, "{:.4f}".format(v))
except Exception: pass
if oeff_style_id is not None:
try: obj_attrs.SetUserString(_KEY_OEFF_STYLE_ID, str(oeff_style_id) or "")
except Exception: pass
# --- Treppen-Felder ---
if geschoss_end is not None:
obj_attrs.SetUserString(_KEY_GESCHOSS_END, geschoss_end or "")
@@ -1925,6 +2117,19 @@ def _read_meta(obj):
ogl = (og_raw == "1") if og_raw in ("0", "1") else is_fenster
oref = a.GetUserString(_KEY_OEFF_REFERENZ) or "mid"
if oref not in _OEFF_REFERENZ_OPTIONS: oref = "mid"
odarst = a.GetUserString(_KEY_OEFF_DARSTELLUNG) or "standard"
if odarst not in _OEFF_DARSTELLUNGEN: odarst = "standard"
oauss = a.GetUserString(_KEY_OEFF_AUSSENSEITE) or "rechts"
if oauss not in _OEFF_AUSSENSEITEN: oauss = "rechts"
otrah = a.GetUserString(_KEY_OEFF_TUER_RAHMEN) or "zarge"
if otrah not in _OEFF_TUER_RAHMEN: otrah = "zarge"
# Rahmen-Offset (m, von Wand-Innenseite). Default 5cm. Wenn Legacy-
# Wert (rahmen_pos) gesetzt aber kein offset, benutzt build-Logik
# weiterhin den Preset.
try: oro = float(a.GetUserString(_KEY_OEFF_RAHMEN_OFFSET) or "0.05")
except Exception: oro = 0.05
if oro < 0: oro = 0.0
ostyle = a.GetUserString(_KEY_OEFF_STYLE_ID) or ""
# Treppen-Felder
gend = a.GetUserString(_KEY_GESCHOSS_END) or ""
try: tb = float(a.GetUserString(_KEY_TREPPE_BREITE) or "1.0")
@@ -2041,6 +2246,11 @@ def _read_meta(obj):
"oeff_sims_in": osi,
"oeff_glas": ogl,
"oeff_referenz": oref,
"oeff_darstellung": odarst,
"oeff_aussenseite": oauss,
"oeff_tuer_rahmen": otrah,
"oeff_rahmen_offset": oro,
"oeff_style_id": ostyle,
"geschoss_end": gend,
"treppe_breite": tb,
"treppe_n": tn,
@@ -2281,6 +2491,26 @@ def _make_oeffnung_pieces(axis_curve, point_on_axis, wall_dicke, oeff_meta, base
sims_in_style = oeff_meta.get("oeff_sims_in", "ohne")
has_glas = bool(oeff_meta.get("oeff_glas", False))
is_tuer = (oeff_meta.get("oeff_typ") == "tuer")
# Doc-level Darstellungs-Override gewinnt vor per-Object-Setting —
# damit Ausschnitt-Wechsel / Oberleiste-Quick-Switch alle Oeffnungen
# konsistent zwingen koennen.
_doc = Rhino.RhinoDoc.ActiveDoc
global_darst = get_aktive_darstellung(_doc) if _doc is not None else ""
darstellung = global_darst or oeff_meta.get("oeff_darstellung", "standard")
if darstellung not in _OEFF_DARSTELLUNGEN:
darstellung = "standard"
# Aussenseite: +1 = aussen ist +Plane.ZAxis (default), -1 = aussen ist
# -Plane.ZAxis (Wand "umgedreht"). Wird verwendet um Sims aus/in
# konsistent zu platzieren unabhaengig von der Wand-Achsen-Richtung.
aussenseite = oeff_meta.get("oeff_aussenseite", "rechts")
aus_sign = +1 if aussenseite == "rechts" else -1
# Tueren-Rahmen-Typ: 'zarge' (klassisch, sitzt IM Wandquerschnitt) oder
# 'block' (Blockrahmen, sitzt UM die Oeffnung und ragt seitlich raus).
tuer_rahmen = oeff_meta.get("oeff_tuer_rahmen", "zarge")
# Rahmen-Offset (m): Abstand der Rahmen-Innenkante von der Wand-
# Innenseite. 0 = bündig innen, wand_dicke-rahmen_t = bündig aussen.
try: rahmen_offset = max(0.0, float(oeff_meta.get("oeff_rahmen_offset", 0.05)))
except Exception: rahmen_offset = 0.05
half_b = breite * 0.5
half_d = float(wall_dicke) * 0.5
@@ -2301,14 +2531,60 @@ def _make_oeffnung_pieces(axis_curve, point_on_axis, wall_dicke, oeff_meta, base
if inner_l >= inner_r - 1e-6 or payload_z_lo >= payload_z_hi - 1e-6:
return [] # Rahmen-Profil zu dick fuer Oeffnung
frame_perp_lo, frame_perp_hi = _resolve_rahmen_perp_range(
half_d, rahmen_t, rahmen_pos)
# Rahmen-Position aus Offset bestimmen. aus_sign=+1 -> Innen ist
# -perp-Seite. Inside-Edge = -half_d + offset. Outside-Edge = inside
# + rahmen_t. Maximaler Offset = wand_dicke - rahmen_t (sonst ragt
# der Rahmen raus). Clamping mit 1mm Inset gegen Z-Fight.
rt = max(0.01, float(rahmen_t))
rt = min(rt, 2.0 * half_d - 0.002)
max_offset = 2.0 * half_d - rt - 0.001
eff_offset = min(max(0.0, rahmen_offset), max(0.0, max_offset))
if aus_sign > 0:
frame_perp_lo = -half_d + eff_offset
frame_perp_hi = frame_perp_lo + rt
else:
frame_perp_hi = +half_d - eff_offset
frame_perp_lo = frame_perp_hi - rt
# --- EINFACH (1:100): nur eine flache Scheibe OHNE Tiefe in der
# Rahmen-Mittelebene (= dort wo das Glas im Standard sitzt). Im
# 2D-Plan ergibt das eine einzelne Linie quer durch die Oeffnung,
# auf dem korrekten Tiefen-Offset.
if darstellung == "einfach":
try:
pane_perp = (frame_perp_lo + frame_perp_hi) * 0.5
# Plane.Origin ans Wand-Achsenpunkt verschoben um pane_perp
# entlang der Plane-Normale (=tan x Z = (0,0,1) cross tan).
normal = rg.Vector3d.CrossProduct(tan, rg.Vector3d(0, 0, 1))
try: normal.Unitize()
except Exception: pass
origin = rg.Point3d(pt.X + normal.X * pane_perp,
pt.Y + normal.Y * pane_perp,
0)
plane = rg.Plane(origin, tan, rg.Vector3d(0, 0, 1))
surf = rg.PlaneSurface(plane,
rg.Interval(-half_b, +half_b),
rg.Interval(z_lo, z_hi))
brep = surf.ToBrep()
return [brep] if brep is not None else []
except Exception as ex:
print("[ELEMENTE] einfach pane:", ex)
return []
pieces = []
# --- RAHMEN: outer box - inner box, sauberer single-Brep
# --- RAHMEN: outer box - inner box, sauberer single-Brep.
# Tueren mit 'block'-Rahmen: outer box ist breiter+hoeher als die
# Oeffnung (Blockrahmen sitzt UM den Wanddurchbruch). Sonst klassische
# Zarge: outer = Oeffnungsmasse.
is_block = (is_tuer and tuer_rahmen == "block")
block_overlap = 0.05 # 5cm seitlich + oben
out_l = (-half_b - block_overlap) if is_block else -half_b
out_r = (+half_b + block_overlap) if is_block else +half_b
out_z_hi = (z_hi + block_overlap) if is_block else z_hi
out_z_lo = z_lo # unten immer bei z_lo (Boden / Bruestung)
try:
outer_box = _make_oeff_box(pt, tan, -half_b, +half_b, z_lo, z_hi,
outer_box = _make_oeff_box(pt, tan, out_l, out_r, out_z_lo, out_z_hi,
frame_perp_lo, frame_perp_hi)
# Inner box leicht laenger in perp Richtung damit der Diff sauber
# durchschneidet (keine Hauchschicht uebrig).
@@ -2348,8 +2624,21 @@ def _make_oeffnung_pieces(axis_curve, point_on_axis, wall_dicke, oeff_meta, base
if fill_t > 0:
fill_mid = (frame_perp_lo + frame_perp_hi) * 0.5
fill_lo = fill_mid - fill_t * 0.5
fill_hi = fill_mid + fill_t * 0.5
# DETAIL (1:20): Doppelverglasung — 2 Scheiben a 6mm, 16mm SZR
is_double_glas = (darstellung == "detail" and has_glas and not is_tuer)
if is_double_glas:
single_t = 0.006
szr = 0.016
total = single_t * 2 + szr
outer_lo = fill_mid - total * 0.5
pane_specs = [
(outer_lo, outer_lo + single_t),
(outer_lo + single_t + szr,
outer_lo + single_t + szr + single_t),
]
else:
pane_specs = [(fill_mid - fill_t * 0.5, fill_mid + fill_t * 0.5)]
for (fl, fh) in pane_specs:
if fluegel > 1:
span = inner_r - inner_l
for i in range(fluegel):
@@ -2359,34 +2648,28 @@ def _make_oeffnung_pieces(axis_curve, point_on_axis, wall_dicke, oeff_meta, base
if i < fluegel - 1: fx_hi -= rahmen_b * 0.5
fp = _make_oeff_box(pt, tan, fx_lo, fx_hi,
payload_z_lo, payload_z_hi,
fill_lo, fill_hi)
fl, fh)
if fp is not None: pieces.append(fp)
else:
fp = _make_oeff_box(pt, tan, inner_l, inner_r,
payload_z_lo, payload_z_hi,
fill_lo, fill_hi)
fl, fh)
if fp is not None: pieces.append(fp)
# --- SIMS AUSSEN (+Plane.ZAxis-Seite) — Platte unter der Oeffnung
# --- SIMS — nur AUSSEN. aus_sign=+1 -> sims auf +perp, =-1 -> auf
# -perp. Drinnen nie. Sim ist gleichzeitig der visuelle Indikator
# fuer die Aussenseite (Test: aussenseite togglen → Sim wechselt).
sa = _OEFF_SIMS_STYLES.get(sims_aus_style)
if sa is not None:
s_t = sa["dicke"]; s_pr = sa["aus"]; s_oh = sa["ueberhang"]
s_lo = z_lo - s_t
if aus_sign > 0:
p_lo, p_hi = +half_d, +half_d + s_pr
else:
p_lo, p_hi = -half_d - s_pr, -half_d
sb = _make_oeff_box(pt, tan,
-half_b - s_oh, +half_b + s_oh,
s_lo, z_lo,
+half_d, +half_d + s_pr)
if sb is not None: pieces.append(sb)
# --- SIMS INNEN (-Plane.ZAxis-Seite) — Platte unter der Oeffnung
si = _OEFF_SIMS_STYLES.get(sims_in_style)
if si is not None:
s_t = si["dicke"]; s_pr = si["aus"]; s_oh = si["ueberhang"]
s_lo = z_lo - s_t
sb = _make_oeff_box(pt, tan,
-half_b - s_oh, +half_b + s_oh,
s_lo, z_lo,
-half_d - s_pr, -half_d)
s_lo, z_lo, p_lo, p_hi)
if sb is not None: pieces.append(sb)
return pieces
@@ -4206,7 +4489,12 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
oeff_sims_aus=op_meta.get("oeff_sims_aus"),
oeff_sims_in=op_meta.get("oeff_sims_in"),
oeff_glas=op_meta.get("oeff_glas"),
oeff_referenz=op_meta.get("oeff_referenz"))
oeff_referenz=op_meta.get("oeff_referenz"),
oeff_darstellung=op_meta.get("oeff_darstellung"),
oeff_aussenseite=op_meta.get("oeff_aussenseite"),
oeff_tuer_rahmen=op_meta.get("oeff_tuer_rahmen"),
oeff_rahmen_offset=op_meta.get("oeff_rahmen_offset"),
oeff_style_id=op_meta.get("oeff_style_id"))
doc.Objects.AddBrep(pbrep, op_attrs)
# Source-Layer migrieren + Volumen-Layer-Index ermitteln
@@ -4684,6 +4972,18 @@ class ElementeBridge(panel_base.BaseBridge):
elif t == "DELETE_WALL": self._delete_wall(p.get("id"))
elif t == "DELETE_ELEMENT": self._delete_wall(p.get("id"))
elif t == "REGENERATE_ALL": self._regenerate_all()
elif t == "SAVE_OEFF_STYLE":
try:
doc = Rhino.RhinoDoc.ActiveDoc
save_oeff_style(doc, p.get("name") or "Stil", p.get("settings") or {})
except Exception as ex: print("[ELEMENTE] save oeff style:", ex)
self._send_state()
elif t == "DELETE_OEFF_STYLE":
try:
doc = Rhino.RhinoDoc.ActiveDoc
delete_oeff_style(doc, p.get("id"))
except Exception as ex: print("[ELEMENTE] del oeff style:", ex)
self._send_state()
elif t == "OPEN_ELEMENTE_UEBERSICHT":
try:
import elemente_uebersicht
@@ -4802,6 +5102,11 @@ class ElementeBridge(panel_base.BaseBridge):
"simsIn": meta.get("oeff_sims_in", "ohne"),
"glas": bool(meta.get("oeff_glas", False)),
"oeffReferenz": meta.get("oeff_referenz", "mid"),
"darstellung": meta.get("oeff_darstellung", "standard"),
"aussenseite": meta.get("oeff_aussenseite", "rechts"),
"tuerRahmen": meta.get("oeff_tuer_rahmen", "zarge"),
"rahmenOffset": meta.get("oeff_rahmen_offset", 0.05),
"styleId": meta.get("oeff_style_id", ""),
})
elif meta["type"] == "treppe_axis":
gs = _geschoss_by_id(doc, meta["geschoss"])
@@ -4911,6 +5216,7 @@ class ElementeBridge(panel_base.BaseBridge):
{"name": n, "color": m["color"],
"hatch": m.get("hatch", ""), "scale": m.get("scale", 1.0)}
for n, m in _MATERIAL_LIBRARY.items()],
"oeffStyles": list_oeff_styles(doc),
}
self.send("STATE", payload)
# An Properties-Satellite-Window forwarden falls offen
@@ -5691,6 +5997,37 @@ class ElementeBridge(panel_base.BaseBridge):
try: preview_base_z = float(axis_curve.PointAtStart.Z)
except Exception: preview_base_z = 0.0
# Entwurfs-Defaults pro Typ — VOR der Loop damit der Stil-Picker
# sie ueberschreiben kann.
is_fenster = (typ == "fenster")
rahmen_b_def = _last("oeff_rahmen_b", 0.06)
rahmen_t_def = _last("oeff_rahmen_tiefe", 0.08)
rahmen_offset_def = _last("oeff_rahmen_offset", 0.05)
fluegel_def = _last("{}_fluegel".format(typ), 2 if is_fenster else 1)
simsa_def = "standard" if is_fenster else "ohne"
glas_def = is_fenster
referenz_def = _last("oeff_referenz", "mid")
darst_def = "standard"
tuer_rahmen_def = "zarge"
# Pending-Style-ID aus sticky (von Stil-Picker gesetzt). Falls noch
# kein Style gepickt, nutzen wir den zuletzt-aktiven fuer diesen typ
# als Default-Source.
active_sid = get_active_oeff_style_id(doc, typ)
if active_sid:
for s in list_oeff_styles(doc, typ):
if s.get("id") == active_sid:
if "rahmenB" in s: rahmen_b_def = float(s["rahmenB"])
if "rahmenTiefe" in s: rahmen_t_def = float(s["rahmenTiefe"])
if "rahmenOffset" in s: rahmen_offset_def = float(s["rahmenOffset"])
if "fluegel" in s: fluegel_def = int(s["fluegel"])
if "simsAus" in s: simsa_def = s["simsAus"]
if "glas" in s: glas_def = bool(s["glas"])
if "darstellung" in s: darst_def = s["darstellung"]
if typ == "tuer" and "tuerRahmen" in s:
tuer_rahmen_def = s["tuerRahmen"]
break
pending_sid = active_sid or ""
# 2) Punkt auf der Achse — constrained an die Wand-Achse
try:
while True:
@@ -5701,8 +6038,12 @@ class ElementeBridge(panel_base.BaseBridge):
prompt += ", Br={:.2f}".format(brueest)
prompt += "]"
gp.SetCommandPrompt(prompt)
try: gp.Constrain(axis_curve, False)
except Exception: pass
# KEINE Constrain mehr — der User soll perpendicular zur
# Achse klicken koennen damit wir die Aussenseite ableiten
# koennen. Position wird via ClosestPoint auf die Achse
# projiziert. Das DynamicDraw-Preview projiziert intern
# auch, der Quader sitzt also weiterhin sauber auf der
# Achse — nur die Cursor-Position bleibt frei.
# Live-Preview: gruener Oeffnungs-Quader mit Glas-Diagonalen,
# Brueest-Marker und Mass-Label oberhalb des Sturzes
try:
@@ -5717,9 +6058,47 @@ class ElementeBridge(panel_base.BaseBridge):
opt_b = gp.AddOption("Breite")
opt_h = gp.AddOption("Hoehe")
opt_br = gp.AddOption("Bruestung") if typ == "fenster" else None
# Stil-Picker: zeigt verfuegbare Styles als Sub-Optionen
opt_st = gp.AddOption("Stil")
rp = gp.Get()
if rp == GetResult.Option:
idx = gp.OptionIndex()
if idx == opt_st:
styles = list_oeff_styles(doc, typ)
if not styles:
print("[ELEMENTE] Keine Styles fuer {}".format(typ))
else:
try:
go = ric.GetOption()
go.SetCommandPrompt("Stil waehlen")
opt_map = []
for s in styles:
safe = s["name"].replace(" ", "_")
opt_map.append((go.AddOption(safe), s))
if go.Get() == GetResult.Option:
oi = go.OptionIndex()
chosen = next((s for (i, s) in opt_map
if i == oi), None)
if chosen is not None:
if "breite" in chosen: breite = float(chosen["breite"])
if "hoehe" in chosen: hoehe = float(chosen["hoehe"])
if typ == "fenster" and "brueest" in chosen:
brueest = float(chosen["brueest"])
if "rahmenB" in chosen: rahmen_b_def = float(chosen["rahmenB"])
if "rahmenTiefe" in chosen: rahmen_t_def = float(chosen["rahmenTiefe"])
if "rahmenOffset" in chosen: rahmen_offset_def = float(chosen["rahmenOffset"])
if "fluegel" in chosen: fluegel_def = int(chosen["fluegel"])
if "simsAus" in chosen: simsa_def = chosen["simsAus"]
if "glas" in chosen: glas_def = bool(chosen["glas"])
if "darstellung" in chosen: darst_def = chosen["darstellung"]
if typ == "tuer" and "tuerRahmen" in chosen:
tuer_rahmen_def = chosen["tuerRahmen"]
pending_sid = chosen["id"]
set_active_oeff_style_id(doc, typ, chosen["id"])
print("[ELEMENTE] Stil '{}' geladen".format(chosen["name"]))
except Exception as ex:
print("[ELEMENTE] Stil-Picker:", ex)
continue
if idx == opt_b:
gn = ric.GetNumber()
gn.SetCommandPrompt("Breite")
@@ -5753,6 +6132,22 @@ class ElementeBridge(panel_base.BaseBridge):
except Exception as ex:
print("[ELEMENTE] ClosestPoint:", ex); return
# Aussenseite aus Click-Richtung ableiten: Vektor on_axis→click_pt
# mit der Perp-Richtung der Achse vergleichen. Vorzeichen entscheidet
# ob aussen=+perp (=rechts) oder aussen=-perp (=links). Bei Klick
# direkt auf der Achse: Default 'rechts'.
detected_aussen = "rechts"
try:
tan_at = axis_curve.TangentAt(t)
perp = rg.Vector3d.CrossProduct(tan_at, rg.Vector3d(0, 0, 1))
dx = click_pt.X - on_axis.X
dy = click_pt.Y - on_axis.Y
side = perp.X * dx + perp.Y * dy
if side < -1e-6: detected_aussen = "links"
elif side > 1e-6: detected_aussen = "rechts"
except Exception as ex:
print("[ELEMENTE] aussenseite detect:", ex)
# Point-Objekt mit Metadaten anlegen
prefix = "fenster_" if typ == "fenster" else "tuer_"
oeff_id = prefix + uuid.uuid4().hex[:10]
@@ -5761,17 +6156,6 @@ class ElementeBridge(panel_base.BaseBridge):
geschoss_name = g.get("name", "EG") if g else "EG"
layer = _ensure_layer(doc, _layer_path_axis(doc, geschoss_name))
# Entwurfs-Defaults pro Typ
is_fenster = (typ == "fenster")
rahmen_b_def = _last("oeff_rahmen_b", 0.06)
rahmen_t_def = _last("oeff_rahmen_tiefe", 0.08)
rahmen_p_def = _last("oeff_rahmen_pos", "mid")
fluegel_def = _last("{}_fluegel".format(typ), 2 if is_fenster else 1)
simsa_def = "standard" if is_fenster else "ohne"
simsi_def = "standard" if is_fenster else "ohne"
glas_def = is_fenster
referenz_def = _last("oeff_referenz", "mid")
attrs = Rhino.DocObjects.ObjectAttributes()
attrs.LayerIndex = layer
_attach_meta(attrs, oeff_id, "oeffnung_point", geschoss,
@@ -5781,12 +6165,16 @@ class ElementeBridge(panel_base.BaseBridge):
oeff_brueest=brueest,
oeff_rahmen_b=rahmen_b_def,
oeff_rahmen_tiefe=rahmen_t_def,
oeff_rahmen_pos=rahmen_p_def,
oeff_rahmen_offset=rahmen_offset_def,
oeff_fluegel=fluegel_def,
oeff_sims_aus=simsa_def,
oeff_sims_in=simsi_def,
oeff_sims_in="ohne",
oeff_glas=glas_def,
oeff_referenz=referenz_def)
oeff_referenz=referenz_def,
oeff_aussenseite=detected_aussen,
oeff_darstellung=darst_def,
oeff_tuer_rahmen=tuer_rahmen_def,
oeff_style_id=pending_sid)
# Oeffnungs-Punkt auf UK+Brueestung-Hoehe platzieren (= visuell auf
# Unterkante Oeffnung). Constraint vergleicht spaeter pt.Z mit
# UK+brueest — wenn der Punkt am axis.Z=0 saesse, wuerde der erste
@@ -8210,6 +8598,41 @@ class ElementeBridge(panel_base.BaseBridge):
glas = bool(p.get("glas", old_meta.get("oeff_glas", otyp == "fenster")))
oref = p.get("oeffReferenz", old_meta.get("oeff_referenz", "mid"))
if oref not in _OEFF_REFERENZ_OPTIONS: oref = "mid"
odarst = p.get("darstellung", old_meta.get("oeff_darstellung", "standard"))
if odarst not in _OEFF_DARSTELLUNGEN: odarst = "standard"
oauss = p.get("aussenseite", old_meta.get("oeff_aussenseite", "rechts"))
if oauss not in _OEFF_AUSSENSEITEN: oauss = "rechts"
otrah = p.get("tuerRahmen", old_meta.get("oeff_tuer_rahmen", "zarge"))
if otrah not in _OEFF_TUER_RAHMEN: otrah = "zarge"
try: oro = float(p.get("rahmenOffset",
old_meta.get("oeff_rahmen_offset", 0.05)))
except Exception: oro = 0.05
if oro < 0: oro = 0.0
# Style-Apply: wenn ein styleId im Patch ist, alle Felder
# aus dem Style ueberschreiben (User-Patch in derselben
# Operation gewinnt aber ueber Style — sonst koennte ein
# Stil das eben gemachte Field-Edit wegmaecken).
o_style_id = p.get("styleId",
old_meta.get("oeff_style_id", "")) or ""
if p.get("styleId") and p.get("styleId") != old_meta.get("oeff_style_id"):
# Style wurde NEU gewaehlt → seine Werte applizieren
styles = list_oeff_styles(doc)
stl = next((s for s in styles if s.get("id") == p["styleId"]), None)
if stl is not None and stl.get("typ") == otyp:
if "breite" in stl: breite = float(stl["breite"])
if "hoehe" in stl: hoehe = float(stl["hoehe"])
if otyp == "fenster" and "brueest" in stl:
brueest = float(stl["brueest"])
if "rahmenB" in stl: rahmen_b = float(stl["rahmenB"])
if "rahmenTiefe" in stl: rahmen_t = float(stl["rahmenTiefe"])
if "rahmenOffset" in stl: oro = float(stl["rahmenOffset"])
if "fluegel" in stl: fluegel = int(stl["fluegel"])
if "simsAus" in stl: simsa = stl["simsAus"]
if "glas" in stl: glas = bool(stl["glas"])
if "darstellung" in stl: odarst = stl["darstellung"]
if otyp == "tuer" and "tuerRahmen" in stl:
otrah = stl["tuerRahmen"]
set_active_oeff_style_id(doc, otyp, p["styleId"])
attrs = axis_obj.Attributes
_attach_meta(attrs, wall_id, "oeffnung_point",
old_meta["geschoss"], old_meta["dicke"],
@@ -8224,7 +8647,12 @@ class ElementeBridge(panel_base.BaseBridge):
oeff_fluegel=fluegel,
oeff_sims_aus=simsa, oeff_sims_in=simsi,
oeff_glas=glas,
oeff_referenz=oref)
oeff_referenz=oref,
oeff_darstellung=odarst,
oeff_aussenseite=oauss,
oeff_tuer_rahmen=otrah,
oeff_rahmen_offset=oro,
oeff_style_id=o_style_id)
axis_obj.Attributes = attrs
axis_obj.CommitChanges()
parent_id = old_meta.get("oeff_parent", "")
@@ -8345,6 +8773,28 @@ class ElementeBridge(panel_base.BaseBridge):
self._send_state()
def regenerate_all_oeffnungen(doc):
"""Modul-API: regen aller Oeffnungen + ihrer Parent-Waende. Wird vom
Ausschnitt-Restore / Oberleiste-Darstellungs-Switch gerufen damit
der doc-level Darstellungs-Override sofort wirkt."""
if doc is None: return 0
seen_walls = set()
n = 0
for obj in list(doc.Objects):
meta = _read_meta(obj)
if meta is None: continue
if meta.get("type") != "oeffnung_point": continue
parent = meta.get("oeff_parent") or ""
if parent and parent not in seen_walls:
seen_walls.add(parent)
_regenerate_element(doc, parent)
n += 1
try: doc.Views.Redraw()
except Exception: pass
print("[ELEMENTE] regen all oeffnungen via {} Waende".format(n))
return n
# --- Event-Listener ---------------------------------------------------------
# Re-Entry-Guard: wenn _regenerate_volume die Brep ersetzt, feuert das
+18
View File
@@ -981,6 +981,18 @@ class OberleisteBridge(panel_base.BaseBridge):
except Exception as ex:
print("[OBERLEISTE] open masse:", ex)
# --- Darstellung (SIA-400 LoD globaler Override) -----------------
elif t == "SET_DARSTELLUNG":
try:
import elemente
doc = Rhino.RhinoDoc.ActiveDoc
new_v = p.get("darstellung") or ""
elemente.set_aktive_darstellung(doc, new_v)
elemente.regenerate_all_oeffnungen(doc)
except Exception as ex:
print("[OBERLEISTE] set darstellung:", ex)
self._send_state(force=True)
# --- Display-Mode -----------------------------------------------
elif t == "SET_DISPLAY_MODE":
n = p.get("name")
@@ -1226,6 +1238,12 @@ class OberleisteBridge(panel_base.BaseBridge):
info["textFonts"] = []
info["textStyles"] = []
info["textStyleActiveId"] = None
# Aktive Darstellung (SIA-400 LoD globaler Override)
try:
import elemente
info["aktiveDarstellung"] = elemente.get_aktive_darstellung(doc) or ""
except Exception:
info["aktiveDarstellung"] = ""
# Norden-Rotation fuer N/O/S/W-Buttons
try:
import kamera
+27
View File
@@ -98,6 +98,8 @@ def _broadcast_state(doc=None, hatch_patterns=None):
"projectZeroMum": zero_mum,
"hatchPatterns": hatch_patterns if hatch_patterns is not None
else _hatch_pattern_names(doc),
"layerCombinations": list_layer_preset_names(doc),
"layerCombinationActive": get_active_comb_name(doc),
}
except Exception as ex:
print("[EBENEN] broadcast prepare:", ex)
@@ -470,6 +472,31 @@ class EbenenBridge(panel_base.BaseBridge):
p.get("hatchPatterns") or [])
elif t == "OPEN_GESCHOSS_DIALOG":
self._open_geschoss_dialog(p.get("zeichnungsebenen") or [])
elif t == "PICK_LAYER_COMBINATION":
doc = Rhino.RhinoDoc.ActiveDoc
name = (p.get("name") or "").strip()
if name:
apply_layer_preset_by_name(doc, name)
else:
set_active_comb_name(doc, None)
_broadcast_state(doc)
_notify_oberleiste_combs()
elif t == "SAVE_LAYER_COMBINATION":
doc = Rhino.RhinoDoc.ActiveDoc
name = (p.get("name") or "").strip()
if name:
save_current_as_layer_preset(doc, name)
_broadcast_state(doc)
_notify_oberleiste_combs()
elif t == "DELETE_LAYER_COMBINATION":
doc = Rhino.RhinoDoc.ActiveDoc
delete_layer_preset(doc, p.get("name") or "")
_broadcast_state(doc)
_notify_oberleiste_combs()
elif t == "OPEN_LAYER_COMBINATIONS_DIALOG":
try: open_layer_combinations_window()
except Exception as ex:
print("[EBENEN] open layer-combinations:", ex)
# ---- Helpers ----
+8 -1
View File
@@ -32,11 +32,16 @@ export default function App() {
const [appliedE, setAppliedE] = useState(INITIAL_EBENEN)
const [eMode, setEMode] = useState('all')
const [hatchPatterns, setHatchPatterns] = useState(['Solid', 'Hatch1', 'Hatch2', 'Hatch3', 'Plus', 'Squares', 'Grid', 'Grid60'])
const [layerCombinations, setLayerCombinations] = useState([])
const [activeKombi, setActiveKombi] = useState(null)
useEffect(() => {
onMessage('STATE_SYNC', ({ ebenen: e, hatchPatterns: hp }) => {
onMessage('STATE_SYNC', ({ ebenen: e, hatchPatterns: hp,
layerCombinations: lc, layerCombinationActive: ac }) => {
if (e) { setEbenen(e); setAppliedE(e) }
if (Array.isArray(hp) && hp.length > 0) setHatchPatterns(hp)
if (Array.isArray(lc)) setLayerCombinations(lc)
if (ac !== undefined) setActiveKombi(ac)
})
onMessage('FIRST_RUN', ({ defaultEbenen } = {}) => {
// Wenn der Dossier-Launcher ein eigenes Schema definiert hat, nutzen wir
@@ -132,6 +137,8 @@ export default function App() {
mode={eMode}
onModeChange={setEMode}
hatchPatterns={hatchPatterns}
layerCombinations={layerCombinations}
activeKombi={activeKombi}
/>
</div>
</div>
+15
View File
@@ -65,6 +65,7 @@ export default function AusschnittSettingsApp() {
overridesEnabled: !!snap.overridesEnabled,
overridesPreset: snap.overridesPreset || '',
layerCombination: snap.layerCombination || '',
darstellung: snap.darstellung || '',
},
})
}
@@ -88,6 +89,20 @@ export default function AusschnittSettingsApp() {
/>
</Field>
<Field label="DARSTELLUNG"
hint="SIA-400 Detaillierungsgrad — leer = per-Element-Setting respektieren">
<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="einfach">Einfach (1:100)</option>
<option value="standard">Standard (1:50)</option>
<option value="detail">Detail (1:20)</option>
</select>
</Field>
<Field label="BILDSCHIRMMODUS"
hint="Display-Mode des Viewports beim Wiederherstellen">
<select
+118 -44
View File
@@ -1,6 +1,6 @@
import { useEffect, useRef, useState } from 'react'
import Icon from './components/Icon'
import { BarToggle, BarButton } from './components/BarControls'
import { BarToggle, BarButton, BarCombo } from './components/BarControls'
import {
onMessage, notifyReady,
createWall, createDecke, createDach,
@@ -8,6 +8,7 @@ import {
createStuetze, createTraeger, createRaum,
openSwisstopo, openSwisstopoDialog, openOsmDialog,
updateElement, deleteElement, openElementeUebersicht, openElementeProperties,
saveOeffStyle, deleteOeffStyle,
} from './lib/rhinoBridge'
const labelXs = {
@@ -494,7 +495,7 @@ function NeuesElementSection({ noGeschoss, activeName, elementsCount }) {
// PropertiesView: gemeinsame Komponente, rendert die passende Property-
// Form je nach Element-Typ. Wiederverwendbar in Inline + Satellite-Window.
export function PropertiesView({ selected, geschosse, materials, hatchPatterns }) {
export function PropertiesView({ selected, geschosse, materials, hatchPatterns, oeffStyles }) {
if (!selected) return null
const upd = (p) => updateElement(selected.id, p)
const del = (label) => () => { if (window.confirm(`${label} löschen?`)) deleteElement(selected.id) }
@@ -521,6 +522,7 @@ export function PropertiesView({ selected, geschosse, materials, hatchPatterns }
return <AussparungProperties aussp={selected} onDelete={del('Aussparung')} />
// fenster/tuer
return <OeffnungProperties oeff={selected} onUpdate={upd}
oeffStyles={oeffStyles || []}
onDelete={del(selected.kind === 'fenster' ? 'Fenster' : 'Tür')} />
}
@@ -566,7 +568,8 @@ export default function ElementeApp() {
selected={selected}
geschosse={geschosse}
materials={state.materials || []}
hatchPatterns={state.hatchPatterns} />
hatchPatterns={state.hatchPatterns}
oeffStyles={state.oeffStyles || []} />
</div>
)}
<NeuesElementSection
@@ -615,7 +618,6 @@ function TragwerkProperties({ el, onUpdate, onDelete }) {
display: 'flex', flexDirection: 'column', gap: 8,
padding: 10, marginBottom: 8,
background: 'var(--bg-section)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
}}>
<div style={{ display: 'flex', alignItems: 'center' }}>
@@ -717,7 +719,6 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns })
display: 'flex', flexDirection: 'column', gap: 8,
padding: 10, marginBottom: 8,
background: 'var(--bg-section)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
}}>
<div style={{ display: 'flex', alignItems: 'center' }}>
@@ -858,7 +859,6 @@ function AussparungProperties({ aussp, onDelete }) {
display: 'flex', flexDirection: 'column', gap: 8,
padding: 10, marginBottom: 8,
background: 'var(--bg-section)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
}}>
<div style={{ display: 'flex', alignItems: 'center' }}>
@@ -910,7 +910,6 @@ function WallProperties({ wall, geschosse, materials, onUpdate, onDelete }) {
display: 'flex', flexDirection: 'column', gap: 8,
padding: 10, marginBottom: 8,
background: 'var(--bg-section)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
}}>
<div style={{ display: 'flex', alignItems: 'center' }}>
@@ -1123,7 +1122,6 @@ function DeckenProperties({ decke, geschosse, onUpdate, onDelete }) {
display: 'flex', flexDirection: 'column', gap: 8,
padding: 10, marginBottom: 8,
background: 'var(--bg-section)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
}}>
<div style={{ display: 'flex', alignItems: 'center' }}>
@@ -1199,7 +1197,6 @@ function DachProperties({ dach, geschosse, onUpdate, onDelete }) {
display: 'flex', flexDirection: 'column', gap: 8,
padding: 10, marginBottom: 8,
background: 'var(--bg-section)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
}}>
<div style={{ display: 'flex', alignItems: 'center' }}>
@@ -1517,7 +1514,6 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
display: 'flex', flexDirection: 'column', gap: 8,
padding: 10, marginBottom: 8,
background: 'var(--bg-section)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
}}>
<div style={{ display: 'flex', alignItems: 'center' }}>
@@ -1684,7 +1680,7 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
)
}
function OeffnungProperties({ oeff, onUpdate, onDelete }) {
function OeffnungProperties({ oeff, onUpdate, onDelete, oeffStyles = [] }) {
const isFenster = oeff.kind === 'fenster'
const label = isFenster ? 'Fenster' : 'Tür'
const icon = isFenster ? 'window' : 'sensor_door'
@@ -1709,16 +1705,13 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
}
const fluegel = oeff.fluegel ?? 1
const rahmenPos = oeff.rahmenPos ?? 'mid'
const simsAus = oeff.simsAus ?? 'ohne'
const simsIn = oeff.simsIn ?? 'ohne'
return (
<div style={{
display: 'flex', flexDirection: 'column', gap: 8,
padding: 10, marginBottom: 8,
background: 'var(--bg-section)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
}}>
<div style={{ display: 'flex', alignItems: 'center' }}>
@@ -1731,6 +1724,97 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
</button>
</div>
{/* Stil-Picker — Liste passender Styles (gefiltert nach typ) */}
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
title="Stil — gespeicherte Properties-Sets fuer Fenster/Tueren">
Stil
</span>
<div style={{ flex: 1, display: 'flex' }}>
<BarCombo
value={oeff.styleId || ''}
onChange={(v) => {
if (v === '__save__') {
const sugg = (oeffStyles.find(s => s.id === oeff.styleId) || {}).name || ''
const n = (window.prompt('Name fuer neuen Stil:', sugg || (isFenster ? 'Mein Fenster' : 'Meine Tuer')) || '').trim()
if (!n) return
saveOeffStyle(n, {
typ: isFenster ? 'fenster' : 'tuer',
breite: oeff.breite, hoehe: oeff.hoehe, brueest: oeff.brueest,
rahmenB: oeff.rahmenB, rahmenTiefe: oeff.rahmenTiefe,
rahmenOffset: oeff.rahmenOffset,
fluegel: oeff.fluegel, simsAus: oeff.simsAus,
glas: oeff.glas, darstellung: oeff.darstellung,
tuerRahmen: oeff.tuerRahmen,
})
return
}
if (v === '__delete__') {
if (oeff.styleId && window.confirm('Aktiven Stil loeschen?'))
deleteOeffStyle(oeff.styleId)
return
}
onUpdate({ styleId: v })
}}
title="Stil anwenden — alle Properties werden gesetzt">
<option value=""> Eigene Werte </option>
{oeffStyles
.filter(s => s.typ === (isFenster ? 'fenster' : 'tuer'))
.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
<option disabled></option>
<option value="__save__">+ Aktuelle als Stil speichern</option>
{oeff.styleId && <option value="__delete__">🗑 Aktiven Stil loeschen</option>}
</BarCombo>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
title="SIA-400 Detaillierungsgrad. Einfach=1:100, Standard=1:50, Detail=1:20">
Darstell.
</span>
<div style={{ flex: 1, display: 'flex' }}>
<BarCombo
value={oeff.darstellung || 'standard'}
onChange={(v) => onUpdate({ darstellung: v })}
title="Detaillierungsgrad — beeinflusst die generierte Geometrie">
<option value="einfach">Einfach (1:100)</option>
<option value="standard">Standard (1:50)</option>
<option value="detail">Detail (1:20)</option>
</BarCombo>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
title="Orientierung — welche Seite der Wand ist aussen. Beim Setzen aus der Click-Richtung erkannt, hier umkehren falls falsch.">
Orient.
</span>
<div style={{ flex: 1, display: 'flex' }}>
<BarToggle label="Umkehren"
onClick={() => onUpdate({ aussenseite:
(oeff.aussenseite || 'rechts') === 'rechts' ? 'links' : 'rechts' })}
title="Aussenseite auf die andere Wandseite umkehren" />
</div>
</div>
{!isFenster && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
title="Tueren-Rahmen-Typ. Zarge sitzt in der Oeffnung, Blockrahmen sitzt aussen herum">
Rahmen
</span>
<div style={{ flex: 1, display: 'flex', gap: 3 }}>
<BarToggle label="Zarge"
active={(oeff.tuerRahmen || 'zarge') === 'zarge'}
onClick={() => onUpdate({ tuerRahmen: 'zarge' })} />
<BarToggle label="Block"
active={(oeff.tuerRahmen || 'zarge') === 'block'}
onClick={() => onUpdate({ tuerRahmen: 'block' })} />
</div>
</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}
@@ -1811,21 +1895,21 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>m</span>
</div>
{/* Rahmen-Lage im Wandquerschnitt */}
{/* Rahmen-Lage: Abstand der Rahmen-Innenkante von der Wand-Innenseite */}
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
title="Lage des Rahmens im Wandquerschnitt">
title="Abstand der Rahmen-Innenkante von der Wand-Innenseite (Aussenseite-Flag oben bestimmt welche Seite innen ist)">
Lage
</span>
<div style={{ flex: 1, display: 'flex', gap: 3 }}>
{RAHMEN_POS_OPTIONS.map(o => (
<BarToggle key={o.code}
label={o.label}
active={rahmenPos === o.code}
onClick={() => onUpdate({ rahmenPos: o.code })}
title={o.hint} />
))}
</div>
<input type="text"
value={String(oeff.rahmenOffset ?? 0.05)}
onChange={(e) => {
const v = parseFloat(e.target.value.replace(',', '.'))
if (!Number.isNaN(v) && v >= 0) onUpdate({ rahmenOffset: v })
}}
onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }}
style={{ flex: 1, fontSize: 11, fontFamily: 'DM Mono, monospace' }} />
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>m v. innen</span>
</div>
{/* Fluegel-Anzahl — nur fuer Fenster (Tueren haben ein einzelnes Tuerblatt) */}
@@ -1846,34 +1930,24 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
</div>
)}
{/* Sims-Stile (aussen / innen) — nur fuer Fenster */}
{/* Sims — nur aussen. Innen gibt's bewusst nicht. Dient zugleich
als visueller Indikator fuer die Aussenseite-Einstellung. */}
{isFenster && (
<>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
title="Aussensims — Platte unter Öffnung, ragt aussen heraus">
Sims a.
Sims
</span>
<select value={simsAus}
onChange={(e) => onUpdate({ simsAus: e.target.value })}
style={{ flex: 1, fontSize: 11 }}>
<div style={{ flex: 1, display: 'flex' }}>
<BarCombo
value={simsAus}
onChange={(v) => onUpdate({ simsAus: v })}
title="Sims-Stil">
{SIMS_OPTIONS.map(o =>
<option key={o.code} value={o.code}>{o.label}</option>)}
</select>
</BarCombo>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
title="Innensims — Platte unter Öffnung, ragt innen heraus">
Sims i.
</span>
<select value={simsIn}
onChange={(e) => onUpdate({ simsIn: e.target.value })}
style={{ flex: 1, fontSize: 11 }}>
{SIMS_OPTIONS.map(o =>
<option key={o.code} value={o.code}>{o.label}</option>)}
</select>
</div>
</>
)}
{/* Glas-Toggle: bei Tueren ersetzt Glas das Tuerblatt (verglaste Tuer) */}
+1
View File
@@ -33,6 +33,7 @@ export default function ElementePropertiesApp() {
geschosse={state.geschosse || []}
materials={state.materials || []}
hatchPatterns={state.hatchPatterns}
oeffStyles={state.oeffStyles || []}
/>
) : (
<div style={{
+10 -38
View File
@@ -14,6 +14,7 @@ import {
setMasseActive, openMasseSettings,
openAbout, createText, setTextSettings,
applyTextStyle, saveTextStyle, deleteTextStyle,
setDarstellung,
} from './lib/rhinoBridge'
const PRESETS = [
@@ -399,47 +400,18 @@ export default function OberleisteApp() {
<option key={dm.id} value={dm.name}>{dm.name}</option>
))}
</BarCombo>
{/* Reihe 1, Spalte 2: Ebenenkombination */}
{/* Reihe 1, Spalte 2: Modelldarstellung (SIA-400 LoD) */}
<BarCombo
icon="layers"
value={state.layerCombinationActive || '__none__'}
onChange={(v) => {
if (v === '__configure__') { openLayerCombinationsDialog(); return }
if (v === '__save__') {
const suggested = state.layerCombinationActive
|| `Kombi ${(state.layerCombinations || []).length + 1}`
const name = (window.prompt('Name für Ebenenkombination:', suggested) || '').trim()
if (!name) return
if ((state.layerCombinations || []).includes(name) &&
!window.confirm(`"${name}" überschreiben?`)) return
saveLayerCombination(name)
return
}
if (v === '__delete__') {
if (state.layerCombinationActive &&
window.confirm(`Kombination "${state.layerCombinationActive}" löschen?`))
deleteLayerCombination(state.layerCombinationActive)
return
}
pickLayerCombination(v === '__none__' ? null : v)
}}
title={state.layerCombinationActive
? `Aktive Kombi: ${state.layerCombinationActive}`
: 'Keine Kombination — manuelle Sichtbarkeit'}
icon="tune"
value={state.aktiveDarstellung || ''}
onChange={(v) => setDarstellung(v)}
title="Darstellungs-Override fuer Fenster/Tueren (SIA-400 LoD)"
width={PRESET_W}
onGear={openLayerCombinationsDialog}
gearTitle="Ebenenkombinationen bearbeiten"
>
<option value="__none__"> Eigene </option>
{(state.layerCombinations || []).map(name => (
<option key={name} value={name}>{name}</option>
))}
<option disabled></option>
<option value="__save__">+ Aktuelle speichern</option>
{state.layerCombinationActive && (
<option value="__delete__">🗑 Aktuelle löschen</option>
)}
<option value="__configure__">Bearbeiten</option>
<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>
</BarCombo>
{/* Reihe 2, Spalte 1: Overrides (Toggle als Icon links) */}
<BarCombo
+34 -8
View File
@@ -14,12 +14,18 @@ export const BAR_H = 22
export function BarCombo({
icon, iconActive, iconClickable, onIconClick, iconTitle,
value, onChange, width, title, children, disabled,
onGear, gearTitle, valueAccent,
onGear, gearTitle, gearIcon, valueAccent,
onSecond, secondIcon, secondTitle,
stretch,
}) {
return (
<div style={{
display: 'inline-flex', alignItems: 'center', gap: 5,
opacity: disabled ? 0.5 : 1, flexShrink: 0,
display: stretch ? 'flex' : 'inline-flex',
alignItems: 'center', gap: 5,
opacity: disabled ? 0.5 : 1,
flex: stretch ? 1 : 'none',
flexShrink: 0,
minWidth: 0,
}}>
{icon && (iconClickable ? (
<button onClick={onIconClick} title={iconTitle}
@@ -51,8 +57,12 @@ export function BarCombo({
e.currentTarget.style.background = 'var(--bg-input)'
}}
style={{
display: 'inline-flex', alignItems: 'stretch',
height: BAR_H + 2, width, boxSizing: 'border-box',
display: stretch ? 'flex' : 'inline-flex', alignItems: 'stretch',
height: BAR_H + 2,
width: stretch ? '100%' : width,
flex: stretch ? 1 : 'none',
minWidth: 0,
boxSizing: 'border-box',
background: 'var(--bg-input)',
border: '1px solid var(--border)',
borderRadius: 999,
@@ -74,20 +84,36 @@ export function BarCombo({
appearance: 'none', WebkitAppearance: 'none',
backgroundImage: 'var(--select-arrow)',
backgroundRepeat: 'no-repeat',
backgroundPosition: onGear ? 'right 1px center' : 'right 10px center',
backgroundPosition: onGear ? 'right 6px center' : 'right 10px center',
cursor: disabled ? 'not-allowed' : 'pointer',
letterSpacing: 0,
}}
>{children}</select>
{/* Trailing-Slots in DOM-Reihenfolge (von Select-Caret nach
aussen rechts): zuerst onGear (Settings), dann onSecond (Add).
Konvention: Settings sitzt immer DIREKT nach dem Caret,
"+" sitzt immer GANZ AUSSEN rechts. */}
{onGear && (
<button onClick={onGear} title={gearTitle}
style={{
background: 'transparent', border: 'none',
padding: '0 8px', cursor: 'pointer',
padding: '0 4px', marginLeft: 3, cursor: 'pointer',
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
flexShrink: 0,
}}>
<Icon name="settings" size={12}
<Icon name={gearIcon || 'settings'} size={12}
style={{ color: 'var(--text-muted)' }} />
</button>
)}
{onSecond && (
<button onClick={onSecond} title={secondTitle}
style={{
background: 'transparent', border: 'none',
padding: '0 8px 0 4px', marginLeft: 2, cursor: 'pointer',
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
flexShrink: 0,
}}>
<Icon name={secondIcon || 'settings'} size={12}
style={{ color: 'var(--text-muted)' }} />
</button>
)}
+83 -36
View File
@@ -3,7 +3,9 @@ import Icon from './Icon'
import ConfirmDeleteEbene from './ConfirmDeleteEbene'
import ContextMenu from './ContextMenu'
import { BarCombo, BarButton } from './BarControls'
import { setLayerStyle, deleteEbene, moveSelectionToEbene, openEbenenSettings } from '../lib/rhinoBridge'
import { setLayerStyle, deleteEbene, moveSelectionToEbene, openEbenenSettings,
pickLayerCombination, saveLayerCombination, deleteLayerCombination,
openLayerCombinationsDialog } from '../lib/rhinoBridge'
const MODES = [
{ value: 'all_force', label: 'Alle anzeigen' },
@@ -245,8 +247,8 @@ function EbeneRow({ e, depth, hasChildren, expanded, onToggleExpand, active, mod
onContextMenu={onContextMenu}
style={{
display: 'flex', alignItems: 'center', gap: 4,
padding: '1px 8px',
paddingLeft: 6 + (depth || 0) * 10,
padding: '1px 12px 1px 0',
paddingLeft: (depth || 0) * 12,
margin: 0,
background: active ? 'var(--active-dim)'
: (e.visible !== false) ? 'var(--bg-item)'
@@ -345,6 +347,7 @@ function SortHeader({ label, sortKey, sortBy, sortDir, onSort, style }) {
export default function EbenenManager({
ebenen, activeCode, onActiveChange, onChange, mode, onModeChange, hatchPatterns,
layerCombinations = [], activeKombi = null,
}) {
const [sortBy, setSortBy] = useState('code')
const [sortDir, setSortDir] = useState('asc')
@@ -548,70 +551,114 @@ export default function EbenenManager({
background: 'var(--bg-section)',
borderBottom: '1px solid var(--border-light)',
}}>
<span className="label-xs">Sichtbarkeit</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<div style={{ flex: 1, minWidth: 0, display: 'flex' }}>
<span className="label-xs">Ebenenkombination</span>
<div style={{ display: 'flex', width: '100%' }}>
<BarCombo
icon="visibility"
value={mode}
onChange={onModeChange}
title="Sichtbarkeits-Modus"
stretch
icon="layers"
value={activeKombi || '__none__'}
onChange={(v) => {
if (v === '__configure__') { openLayerCombinationsDialog(); return }
if (v === '__save__') {
const suggested = activeKombi || `Kombi ${layerCombinations.length + 1}`
const name = (window.prompt('Name für Ebenenkombination:', suggested) || '').trim()
if (!name) return
if (layerCombinations.includes(name) &&
!window.confirm(`"${name}" überschreiben?`)) return
saveLayerCombination(name)
return
}
if (v === '__delete__') {
if (activeKombi &&
window.confirm(`Kombination "${activeKombi}" löschen?`))
deleteLayerCombination(activeKombi)
return
}
pickLayerCombination(v === '__none__' ? null : v)
}}
title={activeKombi
? `Aktive Kombi: ${activeKombi}`
: 'Keine Kombination — manuelle Sichtbarkeit'}
onGear={openLayerCombinationsDialog}
gearTitle="Ebenenkombinationen bearbeiten"
>
{MODES.map(m => <option key={m.value} value={m.value}>{m.label}</option>)}
<option value="__none__"> Eigene </option>
{layerCombinations.map(n => (
<option key={n} value={n}>{n}</option>
))}
<option disabled></option>
<option value="__save__">+ Aktuelle speichern</option>
{activeKombi && (
<option value="__delete__">🗑 Aktuelle löschen</option>
)}
<option value="__configure__">Bearbeiten</option>
</BarCombo>
</div>
<BarButton icon="add" onClick={addNew} title="Ebene hinzufügen" />
</div>
</div>
<div style={{
display: 'flex', alignItems: 'center', gap: 4,
padding: '2px 8px 2px 9px',
display: 'flex', flexDirection: 'column', gap: 4,
padding: '6px 14px',
background: 'var(--bg-section)',
borderBottom: '1px solid var(--border)',
borderBottom: '1px solid var(--border-light)',
}}>
{/* Master-Eye: alle Ebenen sichtbar/unsichtbar */}
<span className="label-xs">Sichtbarkeit</span>
<div style={{ display: 'flex', width: '100%' }}>
<BarCombo
stretch
icon="visibility"
value={mode}
onChange={onModeChange}
title="Sichtbarkeits-Modus"
onSecond={addNew}
secondIcon="add"
secondTitle="Ebene hinzufügen"
>
{MODES.map(m => <option key={m.value} value={m.value}>{m.label}</option>)}
</BarCombo>
</div>
</div>
{/* Sort-Header-Row + Master-Eye/Lock. Padding-Left identisch zu
den Data-Rows damit Eye-Icons aligned sind. Erste 12px-Spanne
spiegelt den Expand-Chevron-Slot der Data-Rows wider. */}
<div style={{
display: 'flex', alignItems: 'center', gap: 4,
padding: '2px 12px 2px 0',
background: 'var(--bg-section)',
borderBottom: '1px solid var(--border-light)',
}}>
<span style={{ width: 12, flexShrink: 0 }} />
<button
className="btn-icon-xs"
onClick={() => {
const anyVisible = ebenen.some(e => e.visible !== false)
// Wenn irgendeine sichtbar -> alle aus. Wenn keine sichtbar -> alle an.
onChange(ebenen.map(e => ({ ...e, visible: !anyVisible })))
if (mode === 'active' || mode === 'all_force') onModeChange('all')
}}
title={ebenen.every(e => e.visible !== false)
? 'Alle Ebenen ausblenden'
: 'Alle Ebenen einblenden'}
style={{ width: 18, height: 18,
? 'Alle Ebenen ausblenden' : 'Alle Ebenen einblenden'}
style={{ width: 16, height: 16,
opacity: (mode === 'active' || mode === 'all_force') ? 0.5 : 1 }}
>
<Icon
name={ebenen.every(e => e.visible !== false) ? 'visibility' : 'visibility_off'}
size={12}
/>
<Icon name={ebenen.every(e => e.visible !== false) ? 'visibility' : 'visibility_off'} size={11} />
</button>
<SortHeader label="Cd" sortKey="code" sortBy={sortBy} sortDir={sortDir} onSort={toggleSort} style={{ width: 24 }} />
<SortHeader label="N" sortKey="code" sortBy={sortBy} sortDir={sortDir} onSort={toggleSort} style={{ width: 22 }} />
<div style={{ width: 12 }} />
<SortHeader label="Name" sortKey="name" sortBy={sortBy} sortDir={sortDir} onSort={toggleSort} style={{ flex: 1 }} />
<SortHeader label="Lw" sortKey="lw" sortBy={sortBy} sortDir={sortDir} onSort={toggleSort} style={{ width: 42, textAlign: 'right', display: 'block' }} />
{/* Master-Lock: alle Ebenen sperren/entsperren */}
<button
className="btn-icon-xs"
onClick={() => {
const anyLocked = ebenen.some(e => e.locked === true)
onChange(ebenen.map(e => ({ ...e, locked: !anyLocked })))
}}
title={ebenen.every(e => e.locked === true)
? 'Alle Ebenen entsperren'
: 'Alle Ebenen sperren'}
style={{ width: 18, height: 18 }}
title={ebenen.every(e => e.locked === true) ? 'Alle Ebenen entsperren' : 'Alle Ebenen sperren'}
style={{ width: 14, height: 14 }}
>
<Icon
name={ebenen.every(e => e.locked === true) ? 'lock' : 'lock_open'}
size={11}
/>
<Icon name={ebenen.every(e => e.locked === true) ? 'lock' : 'lock_open'} size={11} />
</button>
<div style={{ width: 18 }} />
<div style={{ width: 14 }} />
</div>
{(() => {
+23 -15
View File
@@ -48,7 +48,7 @@ function ZeichnungsebeneRow({
onContextMenu={onContextMenu}
style={{
display: 'flex', alignItems: 'center', gap: 4,
padding: '1px 12px',
padding: '1px 12px 1px 0',
margin: 0,
background: active ? 'var(--active-dim)' : 'var(--bg-item)',
borderRadius: active ? 999 : 0,
@@ -58,6 +58,9 @@ function ZeichnungsebeneRow({
minHeight: 24,
}}
>
{/* Spacer-Slot spiegelt den Chevron-Slot bei Ebenen-Rows wider
damit die Eye-Icons beider Panels untereinander stehen. */}
<span style={{ width: 12, flexShrink: 0 }} />
<button
className={`btn-icon-sm ${eyeOn ? 'is-on' : ''}`}
onClick={(ev) => { ev.stopPropagation(); onToggleVisible() }}
@@ -213,32 +216,37 @@ export default function GeschossManager({
borderBottom: '1px solid var(--border-light)',
}}>
<span className="label-xs">Sichtbarkeit</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<div style={{ flex: 1, minWidth: 0, display: 'flex' }}>
<div style={{ display: 'flex', width: '100%' }}>
<BarCombo
stretch
icon="visibility"
value={mode}
onChange={onModeChange}
title="Sichtbarkeits-Modus"
onGear={() => openGeschossDialog(zeichnungsebenen)}
gearIcon="settings"
gearTitle="Einstellungen"
onSecond={addQuick}
secondIcon="add"
secondTitle="Zeichnungsebene hinzufügen"
>
{MODES.map(m => (
<option key={m.value} value={m.value}>{m.label}</option>
))}
</BarCombo>
</div>
<BarButton icon="add" onClick={addQuick} title="Zeichnungsebene hinzufügen" />
<BarButton icon="settings" onClick={() => openGeschossDialog(zeichnungsebenen)} title="Einstellungen" />
</div>
</div>
{/* Master-Row: Master-Eye links + Master-Lock rechts (analog
EbenenManager). */}
{/* Master-Row analog EbenenManager. Padding + Icon-Sizes identisch
damit beide Panels visuell kohaerent sind. Erste 12px-Spanne
spiegelt den Chevron-Slot der Ebenen-Daten-Rows wider. */}
<div style={{
display: 'flex', alignItems: 'center', gap: 5,
padding: '2px 14px',
display: 'flex', alignItems: 'center', gap: 4,
padding: '2px 12px 2px 0',
background: 'var(--bg-section)',
borderBottom: '1px solid var(--border)',
borderBottom: '1px solid var(--border-light)',
}}>
<span style={{ width: 12, flexShrink: 0 }} />
<button
className="btn-icon-xs"
onClick={() => {
@@ -249,12 +257,12 @@ export default function GeschossManager({
title={zeichnungsebenen.every(z => z.visible !== false)
? 'Alle Zeichnungsebenen ausblenden'
: 'Alle Zeichnungsebenen einblenden'}
style={{ width: 18, height: 18,
style={{ width: 16, height: 16,
opacity: (mode === 'active' || mode === 'all_force') ? 0.5 : 1 }}
>
<Icon
name={zeichnungsebenen.every(z => z.visible !== false) ? 'visibility' : 'visibility_off'}
size={12}
size={11}
/>
</button>
<span style={{ flex: 1 }} />
@@ -267,14 +275,14 @@ export default function GeschossManager({
title={zeichnungsebenen.every(z => z.locked === true)
? 'Alle Zeichnungsebenen entsperren'
: 'Alle Zeichnungsebenen sperren'}
style={{ width: 18, height: 18 }}
style={{ width: 14, height: 14 }}
>
<Icon
name={zeichnungsebenen.every(z => z.locked === true) ? 'lock' : 'lock_open'}
size={11}
/>
</button>
<div style={{ width: 18 }} />
<div style={{ width: 14 }} />
</div>
<div>
+5
View File
@@ -172,6 +172,11 @@ export function openOverridesPanel() { send('OPEN_OVERRIDES_PANEL', {}) }
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 || '' }) }
export function saveOeffStyle(name, settings) {
send('SAVE_OEFF_STYLE', { name, settings })
}
export function deleteOeffStyle(id) { send('DELETE_OEFF_STYLE', { id }) }
export function setSectionStyle(enabled, source, color, pattern, scale, rotation) {
send('SET_SECTION_STYLE', { enabled, source, color, pattern, scale, rotation })
}