Compare commits

...

2 Commits

Author SHA1 Message Date
karim 222b00c113 Zeichnungsmanager Master-Controls + Scheren + Startup-Perf + Oeffnung-Preview
UI:
- GeschossManager: Master-Eye + Master-Lock im Header (analog EbenenManager).
  Scheren-Button pro Geschoss togglet hasClipping. Auge ganz links wie bei
  Ebenen. Eye-Logik klar 4-Wege: aktive Z immer hell+on, in 'active'/'all_force'
  fuer non-active gedimmt, sonst spiegelt Flag direkt. Schrift wird NIE gegrayt.
  Neuer Mode 'all_force' = "Alle anzeigen" (ignoriert Eye), 'all' jetzt mit
  Label "Ausgewaehlte" (respektiert Eye). Klick aufs Auge in 'active'/'all_force'
  wechselt automatisch in "Ausgewaehlte" damit Aktion sofort wirkt.

- layer_builder.apply_visibility: neuer z_mode 'all_force' vor visible-Check —
  zeigt jede Z auch wenn Eye=false war.

- elemente._cmd_create_oeffnung: gruene Live-Preview (vertikales Oeffnungs-
  Rechteck + Breiten-Marker + Diagonale) waehrend Fenster/Tuer-Platzierung
  entlang der Wand-Achse. Brueest-Offset aus Geschoss-UK korrekt im Z.

Performance Cold-Start:
- panel_base: Inlined-HTML als Modul-Cache (1x build, n-mal mount). Pro
  Panel-Mount nur noch str.replace + LoadHtml. Spart bei 10 Panels 9x
  ~395 KB Disk-Read + Regex-Pass. Cache-Key = mtime von dist/index.html.

- Timing-Instrumentierung: _t_mark + print_startup_summary. Hook in startup.py
  feuert 3s nach Plugin-Load + listet Wall-time, Top-10, Aggregat pro Phase.

- OberleisteBridge: Command-Enumeration (~1000 Commands) jetzt lazy via
  Rhino.RhinoApp.Idle statt synchron im __init__. Cold-Start nicht blockiert,
  Autocomplete kommt ~1 Frame spaeter.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 04:36:56 +02:00
karim 95031ee2c0 Panels poliert: Ebenenkombi in Oberleiste, Satelliten-Dialoge, Caps weg, Perf
- Ebenenkombination raus aus Ebenen-Panel, in Oberleiste-Topbar +
  Editor-Satellite (AusschnittLayerDialog embedded). doc.Strings
  haelt active_comb_name, auto-clear bei manueller Eye/Lock-Aenderung.
- EbenenSettingsDialog jetzt Satellite mit Ebene-Picker-Dropdown
  (auto-save on switch via SAVE_KEEP).
- Per-Ausschnitt Einstellungen-Satellite (Massstab, Display, Overrides,
  Ebenenkombi). Alte 'Sichtbarkeit bearbeiten'-Option entfernt.
- Layouts/Ausschnitte: Top-Header weg, Sticky-Footer mit Anzahl +
  Aktionen. LayoutDialog ist jetzt Satellite mit Format-Live-Preview.
- Panel-Captions + Default-Ebenen-Namen auf Mixed-Case (Ausschnitte,
  Ebenen, Waende ...). Nur DOSSIER bleibt caps.
- DimensionenApp: Card-Optik raus, REF-Wuerfel mit Kreisen statt
  Quadraten + Hover-Scale.
- GeschossManager angeglichen an EbenenManager: Rechtsklick-Menue,
  Lock-Button, Delete-X, Duplizieren. layer_builder honoriert z.locked.
- Active Sublayer folgt jetzt dem Geschoss-Wechsel (gleicher Code
  unter neuem Parent).

Performance Geschoss-Wechsel:
- elemente._send_state() ersetzt durch _notify_active_geschoss()
  (Partial-Push statt 200+ Elements re-enumerieren).
- _apply_visibility dedupe via sticky last-applied-signature
  (STATE_SYNC-Echo loopt nicht mehr durch alle Layer).
- _update_clipping nur wenn alt oder neu hasClipping=True.
- Redundante doc.Views.Redraw() im CPlane-Pfad entfernt — die folgende
  apply_visibility-Roundtrip redrawt 30ms spaeter ohnehin.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 03:58:28 +02:00
31 changed files with 2028 additions and 756 deletions
+18 -18
View File
@@ -1076,24 +1076,24 @@ const VIEW_COLOR_FIELDS = [
// src/App.jsx des Rhino-Panels bleiben — wenn der User keine eigene Schema // src/App.jsx des Rhino-Panels bleiben — wenn der User keine eigene Schema
// definiert, schickt das Plugin diese hier als FIRST_RUN-Default. // definiert, schickt das Plugin diese hier als FIRST_RUN-Default.
const DEFAULT_LAYER_SCHEMA = [ const DEFAULT_LAYER_SCHEMA = [
{ code: '00', name: 'RASTER', color: '#484850', lw: 0.13 }, { code: '00', name: 'Raster', color: '#484850', lw: 0.13 },
{ code: '01', name: 'VERMESSUNG', color: '#707078', lw: 0.18 }, { code: '01', name: 'Vermessung', color: '#707078', lw: 0.18 },
{ code: '10', name: 'SITUATION', color: '#909090', lw: 0.18 }, { code: '10', name: 'Situation', color: '#909090', lw: 0.18 },
{ code: '11', name: 'STRASSE', color: '#a89070', lw: 0.18 }, { code: '11', name: 'Strasse', color: '#a89070', lw: 0.18 },
{ code: '12', name: 'GEBAEUDE', color: '#888888', lw: 0.25 }, { code: '12', name: 'Gebaeude', color: '#888888', lw: 0.25 },
{ code: '13', name: 'BAEUME', color: '#50a050', lw: 0.13 }, { code: '13', name: 'Baeume', color: '#50a050', lw: 0.13 },
{ code: '14', name: 'HOEHENLINIEN', color: '#909050', lw: 0.18 }, { code: '14', name: 'Hoehenlinien', color: '#909050', lw: 0.18 },
{ code: '20', name: 'WAENDE', color: '#0a0a0a', lw: 0.50 }, { code: '20', name: 'Waende', color: '#0a0a0a', lw: 0.50 },
{ code: '21', name: 'TUEREN_FENSTER', color: '#5080c8', lw: 0.25 }, { code: '21', name: 'Tueren_Fenster', color: '#5080c8', lw: 0.25 },
{ code: '22', name: 'MOEBEL', color: '#909090', lw: 0.13 }, { code: '22', name: 'Moebel', color: '#909090', lw: 0.13 },
{ code: '25', name: 'STUETZEN', color: '#c87050', lw: 0.50 }, { code: '25', name: 'Stuetzen', color: '#c87050', lw: 0.50 },
{ code: '30', name: 'DECKEN', color: '#605850', lw: 0.35 }, { code: '30', name: 'Decken', color: '#605850', lw: 0.35 },
{ code: '31', name: 'DAECHER', color: '#7a4a3a', lw: 0.35 }, { code: '31', name: 'Daecher', color: '#7a4a3a', lw: 0.35 },
{ code: '35', name: 'TRAEGER', color: '#a87858', lw: 0.50 }, { code: '35', name: 'Traeger', color: '#a87858', lw: 0.50 },
{ code: '50', name: 'TEXT', color: '#d0d0d0', lw: 0.13 }, { code: '50', name: 'Text', color: '#d0d0d0', lw: 0.13 },
{ code: '60', name: 'PLANGRAFIK', color: '#c0a040', lw: 0.13 }, { code: '60', name: 'Plangrafik', color: '#c0a040', lw: 0.13 },
{ code: '90', name: 'REFERENZEN', color: '#585860', lw: 0.13 }, { code: '90', name: 'Referenzen', color: '#585860', lw: 0.13 },
{ code: '99', name: 'KONSTRUKTION', color: '#404048', lw: 0.13 }, { code: '99', name: 'Konstruktion', color: '#404048', lw: 0.13 },
] ]
// Built-in Presets — nicht in den Settings gespeichert, immer verfuegbar. // Built-in Presets — nicht in den Settings gespeichert, immer verfuegbar.
+151 -2
View File
@@ -334,6 +334,7 @@ class AusschnittBridge(panel_base.BaseBridge):
elif t == "UPDATE_LAYERS": self._update_layers(p.get("id"), p.get("layers") or []) elif t == "UPDATE_LAYERS": self._update_layers(p.get("id"), p.get("layers") or [])
elif t == "SAVE_PRESET": self._save_preset(p.get("name"), p.get("layers") or []) elif t == "SAVE_PRESET": self._save_preset(p.get("name"), p.get("layers") or [])
elif t == "DELETE_PRESET": self._delete_preset(p.get("name")) elif t == "DELETE_PRESET": self._delete_preset(p.get("name"))
elif t == "OPEN_SETTINGS": self._open_settings_window(p.get("id"))
def _load(self, doc): def _load(self, doc):
raw = doc.Strings.GetValue(_STORE_KEY) raw = doc.Strings.GetValue(_STORE_KEY)
@@ -537,8 +538,42 @@ class AusschnittBridge(panel_base.BaseBridge):
if view is None: return if view is None: return
vp = view.ActiveViewport vp = view.ActiveViewport
_apply_camera(vp, snap.get("camera")) _apply_camera(vp, snap.get("camera"))
_apply_layers_global(doc, snap.get("layers", [])) # Layer-Sichtbarkeit: bevorzugt die referenzierte Ebenenkombi (live —
# zeigt aktuelle Kombi-Definition). Fallback: snap.layers (per-snap
# eingefrorener Zustand).
kombi = (snap.get("layerCombination") or "").strip()
if kombi:
try:
import rhinopanel
rhinopanel.apply_layer_preset_by_name(doc, kombi)
except Exception as ex:
print("[AUSSCHNITTE] kombi-apply '{}':".format(kombi), ex)
_apply_layers_global(doc, snap.get("layers", []))
else:
_apply_layers_global(doc, snap.get("layers", []))
# Eigene Sichtbarkeit → active_comb_name clearen
try:
import rhinopanel
rhinopanel.set_active_comb_name(doc, None)
rhinopanel._notify_oberleiste_combs()
except Exception: pass
_apply_dossier_state(doc, snap.get("dossier") or snap.get("pause") or {}) _apply_dossier_state(doc, snap.get("dossier") or snap.get("pause") or {})
# Overrides: nur anwenden wenn das Snap "applyOverrides" gesetzt hat.
# Sonst bleibt der aktuelle User-Override-State unangetastet.
if snap.get("applyOverrides"):
try:
import overrides
overrides.set_enabled(doc, bool(snap.get("overridesEnabled")))
overrides.set_active_preset(doc, snap.get("overridesPreset") or None)
# Oberleiste-Cache invalidieren damit Topbar das neue Preset zeigt
try:
b = sc.sticky.get("oberleiste_bridge")
if b is not None:
b._cached_overrides = None
b._send_state(force=True)
except Exception: pass
except Exception as ex:
print("[AUSSCHNITTE] overrides-apply:", ex)
# Viewport ZUERST umbenennen — der per-Viewport-Massstab in massstab.py # Viewport ZUERST umbenennen — der per-Viewport-Massstab in massstab.py
# wird unter vp.Name geschluesselt. Erst nach dem Rename schreibt # wird unter vp.Name geschluesselt. Erst nach dem Rename schreibt
# _apply_scale unter dem neuen Namen, sonst landet der Wert beim alten # _apply_scale unter dem neuen Namen, sonst landet der Wert beim alten
@@ -703,6 +738,120 @@ class AusschnittBridge(panel_base.BaseBridge):
self._store(doc, snaps) self._store(doc, snaps)
self._send_list() self._send_list()
def _open_settings_window(self, snap_id):
"""Oeffnet ein Satelliten-Fenster (Eto.Form + WebView) mit dem
Ausschnittseinstellungen-Dialog. Lets User editieren: Massstab,
Display-Mode, Overrides, Ebenenkombi."""
if not snap_id: return
doc = Rhino.RhinoDoc.ActiveDoc
snap = next((s for s in self._load(doc) if s.get("id") == snap_id), None)
if not snap:
print("[AUSSCHNITTE] open_settings: snap nicht gefunden", snap_id)
return
outer = self
bridge_holder = {"form": None, "id": snap_id}
panel_base.register_and_open("ausschnitte", "AUSSCHNITTE", PANEL_GUID_STR, AusschnittBridge, def _payload():
d = Rhino.RhinoDoc.ActiveDoc
sn = next((s for s in outer._load(d) if s.get("id") == bridge_holder["id"]), None)
if sn is None: sn = {}
# Listen fuer Dropdowns
display_modes = []
try:
import oberleiste
display_modes = oberleiste._list_display_modes()
except Exception as ex:
print("[AUSSCHNITTE] display_modes:", ex)
overrides_presets = []
try:
import overrides
overrides_presets = [item.get("name") for item in overrides.list_presets() if item.get("name")]
except Exception as ex:
print("[AUSSCHNITTE] overrides_presets:", ex)
layer_kombis = []
try:
import rhinopanel
layer_kombis = rhinopanel.list_layer_preset_names(d)
except Exception as ex:
print("[AUSSCHNITTE] layer_kombis:", ex)
cam = sn.get("camera") or {}
return {
"snap": {
"id": sn.get("id"),
"name": sn.get("name"),
"scale": sn.get("scale", ""),
"displayMode": cam.get("displayMode"),
"displayModeName": cam.get("displayModeName"),
"applyOverrides": bool(sn.get("applyOverrides", False)),
"overridesEnabled": bool(sn.get("overridesEnabled", False)),
"overridesPreset": sn.get("overridesPreset") or "",
"layerCombination": sn.get("layerCombination") or "",
},
"displayModes": display_modes,
"overridesPresets": overrides_presets,
"layerKombis": layer_kombis,
}
def _persist(settings):
d = Rhino.RhinoDoc.ActiveDoc
snaps = outer._load(d)
sid = bridge_holder["id"]
target = next((s for s in snaps if s.get("id") == sid), None)
if target is None:
print("[AUSSCHNITTE] persist settings: snap weg"); return
# Massstab
sc_val = (settings.get("scale") or "").strip()
target["scale"] = sc_val
# Display Mode in camera nested
cam = target.get("camera") or {}
dm_id = settings.get("displayMode")
dm_nm = settings.get("displayModeName")
if dm_id is not None: cam["displayMode"] = dm_id or None
if dm_nm is not None: cam["displayModeName"] = dm_nm or None
target["camera"] = cam
# Overrides
target["applyOverrides"] = bool(settings.get("applyOverrides"))
target["overridesEnabled"] = bool(settings.get("overridesEnabled"))
target["overridesPreset"] = (settings.get("overridesPreset") or "").strip()
# Ebenenkombi
target["layerCombination"] = (settings.get("layerCombination") or "").strip()
outer._store(d, snaps)
outer._send_list()
print("[AUSSCHNITTE] Settings fuer '{}' aktualisiert".format(target.get("name")))
class _AusschnittSettingsBridge(panel_base.BaseBridge):
def __init__(self):
panel_base.BaseBridge.__init__(self, "ausschnitt_settings")
def _on_ready(self):
self._send_state()
def _send_state(self):
self.send("AUSSCHNITT_SETTINGS_STATE", _payload())
def handle(self, data):
if not isinstance(data, dict): return
t = data.get("type", "")
p = data.get("payload") or {}
if t == "READY":
self._on_ready()
elif t == "SAVE":
_persist(p.get("settings") or {})
try:
f = bridge_holder.get("form")
if f is not None: f.Close()
except Exception: pass
elif t == "CANCEL":
try:
f = bridge_holder.get("form")
if f is not None: f.Close()
except Exception: pass
b = _AusschnittSettingsBridge()
bridge_holder["form"] = panel_base.open_satellite_window(
"ausschnitt_settings",
params=_payload(),
title="Ausschnitt: {}".format(snap.get("name", "")),
size=(420, 540),
bridge=b)
panel_base.register_and_open("ausschnitte", "Ausschnitte", PANEL_GUID_STR, AusschnittBridge,
icon_spec=("crop", "#c87050")) icon_spec=("crop", "#c87050"))
+1 -1
View File
@@ -608,6 +608,6 @@ def _bridge_factory():
return b return b
panel_base.register_and_open("dimensionen", "DIMENSIONEN", PANEL_GUID_STR, panel_base.register_and_open("dimensionen", "Dimensionen", PANEL_GUID_STR,
_bridge_factory, _bridge_factory,
icon_spec=("aspect_ratio", "#9e7050")) icon_spec=("aspect_ratio", "#9e7050"))
+77 -3
View File
@@ -366,9 +366,9 @@ def _find_ebene_sublayer_name(doc, keywords, default_code, default_name,
def _layer_path_axis(doc, geschoss_name): def _layer_path_axis(doc, geschoss_name):
"""Wand-Achse + Volumen — Sublayer 'WÄNDE' (Code 20).""" """Wand-Achse + Volumen — Sublayer 'Wände' (Code 20)."""
sub = _find_ebene_sublayer_name(doc, ["wand", "wände", "waende"], sub = _find_ebene_sublayer_name(doc, ["wand", "wände", "waende"],
"20", "WÄNDE", "20", "Wände",
default_color="#0a0a0a", default_lw=0.50) default_color="#0a0a0a", default_lw=0.50)
return "{}::{}".format(geschoss_name, sub) return "{}::{}".format(geschoss_name, sub)
@@ -503,6 +503,46 @@ def _make_circle_preview(center):
return handler return handler
def _make_oeffnung_preview(axis_curve, breite, hoehe, brueest, base_z):
"""Preview fuer Fenster/Tuer-Platzierung. Zeigt das vertikale Rechteck
der Oeffnungsflaeche auf Hoehe der Achse (entlang Tangente x Hoehe),
zusaetzlich eine Marker-Linie auf der Achse die die Breite andeutet."""
import System.Drawing as SD
color = SD.Color.FromArgb(255, 95, 200, 180) # Accent gruen
color_axis = SD.Color.FromArgb(180, 95, 200, 180) # halbtransparent
def handler(sender, e):
try:
cur = e.CurrentPoint
ok, t = axis_curve.ClosestPoint(cur)
if not ok: return
on_axis = axis_curve.PointAt(t)
tan = axis_curve.TangentAt(t)
tlen = (tan.X * tan.X + tan.Y * tan.Y) ** 0.5
if tlen < 1e-9: return
tx = tan.X / tlen; ty = tan.Y / tlen
hb = breite * 0.5
z_bot = base_z + brueest
z_top = z_bot + hoehe
cx, cy = on_axis.X, on_axis.Y
# Breiten-Marker auf der Achse
left_xy = rg.Point3d(cx - hb * tx, cy - hb * ty, on_axis.Z)
right_xy = rg.Point3d(cx + hb * tx, cy + hb * ty, on_axis.Z)
e.Display.DrawLine(left_xy, right_xy, color_axis, 1)
# 4 Kanten der vertikalen Oeffnungs-Flaeche
p_lb = rg.Point3d(cx - hb * tx, cy - hb * ty, z_bot)
p_rb = rg.Point3d(cx + hb * tx, cy + hb * ty, z_bot)
p_rt = rg.Point3d(cx + hb * tx, cy + hb * ty, z_top)
p_lt = rg.Point3d(cx - hb * tx, cy - hb * ty, z_top)
for a, b in ((p_lb, p_rb), (p_rb, p_rt),
(p_rt, p_lt), (p_lt, p_lb)):
e.Display.DrawLine(a, b, color, 2)
# Diagonalen — Andeutung der Glasflaeche
e.Display.DrawLine(p_lb, p_rt, color_axis, 1)
e.Display.DrawLine(p_lt, p_rb, color_axis, 1)
except Exception: pass
return handler
def _collect_rectangle(doc, c1): def _collect_rectangle(doc, c1):
"""Achsen-aligned Rechteck aus 2 diagonalen Ecken. Liefert geschlossene """Achsen-aligned Rechteck aus 2 diagonalen Ecken. Liefert geschlossene
PolylineCurve in XY-Ebene auf Z=0.""" PolylineCurve in XY-Ebene auf Z=0."""
@@ -4543,6 +4583,23 @@ class ElementeBridge(panel_base.BaseBridge):
elif t == "DELETE_ELEMENT": 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 == "REGENERATE_ALL": self._regenerate_all()
def _notify_active_geschoss(self):
"""Schlanker Partial-Push: nur activeGeschoss + activeGeschossName.
Wird vom Ebenen-Bridge bei Geschoss-Wechsel gerufen die Element-
Liste ist davon nicht betroffen, ein voller _send_state mit Re-
Enumeration aller Smart-Elemente (200+ in echten Projekten) waere
teuer und unnoetig. React-State macht Shallow-Merge, der Rest des
States bleibt."""
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return
try:
self.send("STATE", {
"activeGeschoss": _active_geschoss_id(doc),
"activeGeschossName": _active_geschoss_name(doc),
})
except Exception as ex:
print("[ELEMENTE] _notify_active_geschoss:", ex)
def _send_state(self): def _send_state(self):
doc = Rhino.RhinoDoc.ActiveDoc doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: if doc is None:
@@ -5497,6 +5554,18 @@ class ElementeBridge(panel_base.BaseBridge):
axis_curve = wall_obj.Geometry axis_curve = wall_obj.Geometry
if not isinstance(axis_curve, rg.Curve): return if not isinstance(axis_curve, rg.Curve): return
# Base-Z fuer das Preview: UK des Geschosses, damit das Brueest-
# Offset visuell stimmt (Achse selbst kann auf einem anderen Z
# liegen je nach Modellierung).
try:
wuk, _wok = _resolve_uk_ok(doc, wall_meta.get("geschoss"),
wall_meta.get("uk_override"),
wall_meta.get("ok_override"))
preview_base_z = float(wuk)
except Exception:
try: preview_base_z = float(axis_curve.PointAtStart.Z)
except Exception: preview_base_z = 0.0
# 2) Punkt auf der Achse — constrained an die Wand-Achse # 2) Punkt auf der Achse — constrained an die Wand-Achse
try: try:
while True: while True:
@@ -5509,6 +5578,11 @@ class ElementeBridge(panel_base.BaseBridge):
gp.SetCommandPrompt(prompt) gp.SetCommandPrompt(prompt)
try: gp.Constrain(axis_curve, False) try: gp.Constrain(axis_curve, False)
except Exception: pass except Exception: pass
# Live-Preview: gruenes Oeffnungs-Rechteck auf der Wand
try:
gp.DynamicDraw += _make_oeffnung_preview(
axis_curve, breite, hoehe, brueest, preview_base_z)
except Exception: pass
opt_b = gp.AddOption("Breite") opt_b = gp.AddOption("Breite")
opt_h = gp.AddOption("Hoehe") opt_h = gp.AddOption("Hoehe")
opt_br = gp.AddOption("Bruestung") if typ == "fenster" else None opt_br = gp.AddOption("Bruestung") if typ == "fenster" else None
@@ -8509,6 +8583,6 @@ def _bridge_factory():
return b return b
panel_base.register_and_open("elemente", "ELEMENTE", PANEL_GUID_STR, panel_base.register_and_open("elemente", "Elemente", PANEL_GUID_STR,
_bridge_factory, _bridge_factory,
icon_spec=("foundation", "#5fa896")) icon_spec=("foundation", "#5fa896"))
+1 -1
View File
@@ -1695,5 +1695,5 @@ def _bridge_factory():
return b return b
panel_base.register_and_open("gestaltung", "GESTALTUNG", PANEL_GUID_STR, _bridge_factory, panel_base.register_and_open("gestaltung", "Gestaltung", PANEL_GUID_STR, _bridge_factory,
icon_spec=("palette", "#5fa896")) icon_spec=("palette", "#5fa896"))
+11
View File
@@ -578,12 +578,18 @@ def apply_visibility(doc, zeichnungsebenen, ebenen, active_z_id, active_code, z_
children = children_by_parent.get(parent.Id, []) children = children_by_parent.get(parent.Id, [])
is_active_z = z["id"] == active_z_id is_active_z = z["id"] == active_z_id
z_visible_flag = z.get("visible", True) z_visible_flag = z.get("visible", True)
z_locked_flag = bool(z.get("locked", False))
# Z-Mode -> Parent-Zustand # Z-Mode -> Parent-Zustand
# 'all_force' kommt VOR dem visible-Flag-Check: zeigt jede Z auch wenn
# das User-Eye sie ausgeblendet hatte. 'all' dagegen respektiert das
# Eye-Flag (= "Ausgewählte" im UI).
if is_active_z: if is_active_z:
p_vis, p_grey, p_lock = True, False, False p_vis, p_grey, p_lock = True, False, False
elif z_mode == "active": elif z_mode == "active":
p_vis, p_grey, p_lock = False, False, False p_vis, p_grey, p_lock = False, False, False
elif z_mode == "all_force":
p_vis, p_grey, p_lock = True, False, False
elif not z_visible_flag: elif not z_visible_flag:
p_vis, p_grey, p_lock = False, False, False p_vis, p_grey, p_lock = False, False, False
elif z_mode == "all": elif z_mode == "all":
@@ -593,6 +599,11 @@ def apply_visibility(doc, zeichnungsebenen, ebenen, active_z_id, active_code, z_
else: # grey else: # grey
p_vis, p_grey, p_lock = True, True, False p_vis, p_grey, p_lock = True, True, False
# Per-Z explizites Sperren ueberlagert (auch fuer die aktive Z) — wer
# eine Geschoss-Ebene sperrt, will dass Klicks ins Leere gehen.
if z_locked_flag:
p_lock = True
parent_changed = False parent_changed = False
if parent.IsVisible != p_vis: if parent.IsVisible != p_vis:
parent.IsVisible = p_vis parent.IsVisible = p_vis
+65 -1
View File
@@ -281,6 +281,7 @@ class LayoutsBridge(panel_base.BaseBridge):
elif t == "ADD_FOLDER": self._add_folder(p.get("name")) elif t == "ADD_FOLDER": self._add_folder(p.get("name"))
elif t == "REMOVE_FOLDER": self._remove_folder(p.get("name")) elif t == "REMOVE_FOLDER": self._remove_folder(p.get("name"))
elif t == "SET_FOLDER": self._set_folder(p.get("id"), p.get("folder") or "") elif t == "SET_FOLDER": self._set_folder(p.get("id"), p.get("folder") or "")
elif t == "OPEN_LAYOUT_DIALOG": self._open_layout_dialog(p)
# --- State-Snapshot ----------------------------------------------------- # --- State-Snapshot -----------------------------------------------------
@@ -737,6 +738,69 @@ class LayoutsBridge(panel_base.BaseBridge):
print("[LAYOUTS] sync layout:", ex) print("[LAYOUTS] sync layout:", ex)
self._send_state() self._send_state()
def _open_layout_dialog(self, p):
"""Oeffnet ein Satelliten-Fenster mit dem Layout-Erstellen/Bearbeiten
Dialog. mode = 'new' | 'edit'. Bei 'edit' wird `layout` (id, name,
width, height) mitgeschickt."""
outer = self
mode = (p.get("mode") or "new")
layout = p.get("layout") or None
bridge_holder = {"form": None}
def _apply(payload):
if mode == "new":
outer._new_layout({
"name": payload.get("name") or "",
"format": payload.get("format") or "A3",
"landscape": bool(payload.get("landscape", True)),
"customWidth": payload.get("customWidth"),
"customHeight": payload.get("customHeight"),
})
elif mode == "edit" and layout and layout.get("id"):
outer._set_page_size({
"id": layout.get("id"),
"format": payload.get("format") or "A3",
"landscape": bool(payload.get("landscape", True)),
"customWidth": payload.get("customWidth"),
"customHeight": payload.get("customHeight"),
})
class _LayoutDialogBridge(panel_base.BaseBridge):
def __init__(self):
panel_base.BaseBridge.__init__(self, "layout_dialog")
def _on_ready(self):
self.send("LAYOUT_DIALOG_STATE", {
"mode": mode,
"layout": layout,
})
def handle(self, data):
if not isinstance(data, dict): return
t = data.get("type", "")
pp = data.get("payload") or {}
if t == "READY":
self._on_ready()
elif t == "SAVE":
_apply(pp)
try:
f = bridge_holder.get("form")
if f is not None: f.Close()
except Exception: pass
elif t == "CANCEL":
try:
f = bridge_holder.get("form")
if f is not None: f.Close()
except Exception: pass
b = _LayoutDialogBridge()
title = "Neues Layout" if mode == "new" else "Papierformat: {}".format(
(layout or {}).get("name", ""))
bridge_holder["form"] = panel_base.open_satellite_window(
"layout_dialog",
params={"mode": mode, "layout": layout},
title=title,
size=(440, 380),
bridge=b)
def _bridge_factory(): def _bridge_factory():
b = LayoutsBridge() b = LayoutsBridge()
@@ -744,6 +808,6 @@ def _bridge_factory():
return b return b
panel_base.register_and_open("layouts", "LAYOUTS", PANEL_GUID_STR, panel_base.register_and_open("layouts", "Layouts", PANEL_GUID_STR,
_bridge_factory, _bridge_factory,
icon_spec=("view_quilt", "#7a5fa8")) icon_spec=("view_quilt", "#7a5fa8"))
+1 -1
View File
@@ -1090,7 +1090,7 @@ def _bridge_factory():
# register_standalone_panel() aufrufen oder die Zeile darunter auskommentieren. # register_standalone_panel() aufrufen oder die Zeile darunter auskommentieren.
def register_standalone_panel(): def register_standalone_panel():
panel_base.register_and_open("massstab", "MASSSTAB", PANEL_GUID_STR, _bridge_factory, panel_base.register_and_open("massstab", "Massstab", PANEL_GUID_STR, _bridge_factory,
icon_spec=("straighten", "#c87050")) icon_spec=("straighten", "#c87050"))
# register_standalone_panel() # auskommentiert — Standard ist OBERLEISTE # register_standalone_panel() # auskommentiert — Standard ist OBERLEISTE
+82 -10
View File
@@ -20,6 +20,7 @@ if _HERE not in sys.path:
import panel_base import panel_base
import massstab import massstab
import overrides import overrides
import rhinopanel
PANEL_GUID_STR = "7e1f6a5b-8e2f-4f3c-d5e6-f70819203b51" PANEL_GUID_STR = "7e1f6a5b-8e2f-4f3c-d5e6-f70819203b51"
OVERRIDES_PANEL_GUID_STR = "8f2a7b6c-9f3a-4f4d-e6f7-08192a3c4d62" OVERRIDES_PANEL_GUID_STR = "8f2a7b6c-9f3a-4f4d-e6f7-08192a3c4d62"
@@ -770,12 +771,37 @@ class OberleisteBridge(panel_base.BaseBridge):
self._last_prompt = "" self._last_prompt = ""
self._last_state_sig = None # Fingerprint des letzten Push — dedupe self._last_state_sig = None # Fingerprint des letzten Push — dedupe
self._cached_overrides = None # (enabled, count) — invalidiert bei Toggle/Update self._cached_overrides = None # (enabled, count) — invalidiert bei Toggle/Update
# Command-Liste einmalig laden (kann teuer sein -> cachen) self._cached_combinations = None # (names, active) — invalidiert bei jeder Comb-Aenderung
try: # Command-Liste LAZY laden — die Enumeration durchlaeuft alle Plugins
self._all_commands = _list_all_command_names() # und ist teuer (~hundert ms). Wird erst beim ersten _send_state, oder
# explizit bei Command-Input-Fokus, gebaut.
self._all_commands = None
def _ensure_commands_loaded_async(self):
"""Schedult die teure Command-Enum auf den naechsten Idle-Tick statt
sie synchron beim ersten _send_state zu laden. Cold-Start-Pfad wird
nicht geblockt; Autocomplete-Liste ist ~1 Frame spaeter da."""
if self._all_commands is not None: return
if getattr(self, "_commands_loading", False): return
self._commands_loading = True
def _load_once(s, e):
try: Rhino.RhinoApp.Idle -= _load_once
except Exception: pass
try:
self._all_commands = _list_all_command_names()
except Exception as ex:
print("[OBERLEISTE] command-enum:", ex)
self._all_commands = []
# Sofort an UI pushen (Autocomplete wird live)
try:
if not getattr(self, "_commands_sent", False):
self.send("STATE", {"allCommands": self._all_commands})
self._commands_sent = True
except Exception: pass
try: Rhino.RhinoApp.Idle += _load_once
except Exception as ex: except Exception as ex:
print("[OBERLEISTE] command-enum:", ex) print("[OBERLEISTE] schedule command-load:", ex)
self._all_commands = [] self._commands_loading = False
def _on_ready(self): def _on_ready(self):
# Bootstrap DPI (gemeinsam mit massstab.py) # Bootstrap DPI (gemeinsam mit massstab.py)
@@ -920,6 +946,34 @@ class OberleisteBridge(panel_base.BaseBridge):
except Exception as ex: except Exception as ex:
print("[OBERLEISTE] open_as_window Overrides:", ex) print("[OBERLEISTE] open_as_window Overrides:", ex)
# --- Ebenenkombinationen ----------------------------------------
elif t == "PICK_LAYER_COMBINATION":
doc = Rhino.RhinoDoc.ActiveDoc
name = (p.get("name") or "").strip()
if name:
rhinopanel.apply_layer_preset_by_name(doc, name)
else:
# "Eigene" — kein Apply, nur active_comb_name clearen
rhinopanel.set_active_comb_name(doc, None)
self._cached_combinations = None
self._send_state(force=True)
elif t == "SAVE_LAYER_COMBINATION":
doc = Rhino.RhinoDoc.ActiveDoc
name = (p.get("name") or "").strip()
if name:
rhinopanel.save_current_as_layer_preset(doc, name)
self._cached_combinations = None
self._send_state(force=True)
elif t == "DELETE_LAYER_COMBINATION":
doc = Rhino.RhinoDoc.ActiveDoc
rhinopanel.delete_layer_preset(doc, p.get("name") or "")
self._cached_combinations = None
self._send_state(force=True)
elif t == "OPEN_LAYER_COMBINATIONS_DIALOG":
try: rhinopanel.open_layer_combinations_window()
except Exception as ex:
print("[OBERLEISTE] open layer-combinations:", ex)
# --- Command-Line Integration ----------------------------------- # --- Command-Line Integration -----------------------------------
elif t == "RUN_COMMAND": elif t == "RUN_COMMAND":
cmd = (p.get("cmd") or "").strip() cmd = (p.get("cmd") or "").strip()
@@ -1035,15 +1089,32 @@ class OberleisteBridge(panel_base.BaseBridge):
info["overridesActivePreset"], info["overridesActivePreset"],
_presets_tuple) = self._cached_overrides _presets_tuple) = self._cached_overrides
info["overridesPresets"] = list(_presets_tuple) info["overridesPresets"] = list(_presets_tuple)
# Ebenenkombinationen — cached (Liste + active). Invalidiert bei
# PICK/SAVE/DELETE und durch Cross-Bridge-Notify aus rhinopanel.py.
if self._cached_combinations is None:
try:
names = tuple(rhinopanel.list_layer_preset_names(doc))
active = rhinopanel.get_active_comb_name(doc)
self._cached_combinations = (names, active)
except Exception:
self._cached_combinations = ((), None)
_names_tuple, _active_comb = self._cached_combinations
info["layerCombinations"] = list(_names_tuple)
info["layerCombinationActive"] = _active_comb
# Command-Line State # Command-Line State
prompt = _get_command_prompt() prompt = _get_command_prompt()
info["cmdPrompt"] = prompt info["cmdPrompt"] = prompt
info["cmdOptions"] = _parse_command_options(prompt) info["cmdOptions"] = _parse_command_options(prompt)
# Command-Autocomplete-Liste — nur einmal initial schicken (gross) # Command-Autocomplete-Liste — Lazy via Idle-Tick (statt im Bridge-
# Init blockierend). Wenn sie noch nicht da ist: einplanen. Wenn da:
# einmalig mitsenden.
if not getattr(self, "_commands_sent", False): if not getattr(self, "_commands_sent", False):
info["allCommands"] = self._all_commands if self._all_commands is None:
self._commands_sent = True self._ensure_commands_loaded_async()
force = True # Erste Push immer feuern else:
info["allCommands"] = self._all_commands
self._commands_sent = True
force = True
# Diff-Check: wenn weder Daten noch force, gar nichts schicken # Diff-Check: wenn weder Daten noch force, gar nichts schicken
# (dedupe Idle-Ticks ohne Aenderung — spart WebView-ExecuteScript Roundtrip) # (dedupe Idle-Ticks ohne Aenderung — spart WebView-ExecuteScript Roundtrip)
sig = ( sig = (
@@ -1057,6 +1128,7 @@ class OberleisteBridge(panel_base.BaseBridge):
info["overridesEnabled"], info["overridesCount"], info["overridesEnabled"], info["overridesCount"],
info.get("overridesActivePreset"), info.get("overridesActivePreset"),
tuple(info.get("overridesPresets") or ()), tuple(info.get("overridesPresets") or ()),
_names_tuple, _active_comb,
prompt, prompt,
) )
if not force and sig == self._last_state_sig: if not force and sig == self._last_state_sig:
@@ -1197,5 +1269,5 @@ def _bridge_factory():
return b return b
panel_base.register_and_open("oberleiste", "OBERLEISTE", PANEL_GUID_STR, _bridge_factory, panel_base.register_and_open("oberleiste", "Oberleiste", PANEL_GUID_STR, _bridge_factory,
icon_spec=("menu", "#2f5d54")) icon_spec=("menu", "#2f5d54"))
+1 -1
View File
@@ -266,7 +266,7 @@ def open_as_window():
sc.sticky["overrides_bridge"] = b sc.sticky["overrides_bridge"] = b
panel_base.open_satellite_window( panel_base.open_satellite_window(
"overrides", "overrides",
title="OVERRIDES", title="Overrides",
size=(760, 580), size=(760, 580),
bridge=b) bridge=b)
+111 -17
View File
@@ -8,6 +8,7 @@ Wird von rhinopanel.py (EBENEN) und gestaltung.py (GESTALTUNG) verwendet.
import os import os
import re import re
import json import json
import time
import Rhino import Rhino
import Rhino.UI as RhinoUI import Rhino.UI as RhinoUI
import Eto.Forms as forms import Eto.Forms as forms
@@ -19,6 +20,45 @@ _HERE = os.path.dirname(os.path.abspath(__file__))
_DIST = os.path.join(_HERE, "..", "dist", "index.html") _DIST = os.path.join(_HERE, "..", "dist", "index.html")
# --- Timing-Instrumentierung ------------------------------------------------
# Schaltbar via sc.sticky["dossier_timing"] = True. Wir loggen die Hot-Paths
# beim Plugin-Start. Tabelle am Ende per panel_base.print_startup_summary().
_TIMINGS = [] # Liste von (phase, label, ms)
_T0 = None # Zeitstempel des allerersten Aufrufs
def _t_mark(phase, label, t_start):
"""Loggt eine Phase + Dauer. Print sofort, plus Aggregat fuers Summary."""
ms = (time.time() - t_start) * 1000.0
_TIMINGS.append((phase, label, ms))
global _T0
if _T0 is None: _T0 = t_start
try:
print("[STARTUP] {:>22s} {:>22s} {:7.1f} ms".format(phase, label, ms))
except Exception: pass
return ms
def print_startup_summary():
"""Aufruf am Ende von startup.py: zeigt total + sortierte Topliste."""
if not _TIMINGS: return
total_wall = (time.time() - (_T0 or time.time())) * 1000.0
total_work = sum(ms for _, _, ms in _TIMINGS)
print("[STARTUP] ===== SUMMARY =====")
print("[STARTUP] Wall-time (first to last mark): {:.1f} ms".format(total_wall))
print("[STARTUP] Sum of measured work: {:.1f} ms".format(total_work))
# Top-10 nach Dauer
top = sorted(_TIMINGS, key=lambda x: -x[2])[:10]
print("[STARTUP] --- Top-10 nach Dauer ---")
for phase, label, ms in top:
print("[STARTUP] {:7.1f} ms {} / {}".format(ms, phase, label))
# Aggregat nach Phase
by_phase = {}
for phase, _, ms in _TIMINGS:
by_phase[phase] = by_phase.get(phase, 0.0) + ms
print("[STARTUP] --- Aggregat nach Phase ---")
for phase, ms in sorted(by_phase.items(), key=lambda x: -x[1]):
print("[STARTUP] {:7.1f} ms {}".format(ms, phase))
# --- Legacy-Migration: traite_* / pause_* -> dossier_* ---------------------- # --- Legacy-Migration: traite_* / pause_* -> dossier_* ----------------------
# #
# Historisch hatte das Plugin nacheinander die Praefixe "traite_" und "pause_" # Historisch hatte das Plugin nacheinander die Praefixe "traite_" und "pause_"
@@ -198,31 +238,37 @@ class BaseBridge(object):
# --- HTML laden ------------------------------------------------------------- # --- HTML laden -------------------------------------------------------------
def load_inline(wv, mode, params=None): # Cache der fertig zusammengebauten Inline-HTML — Disk-IO + CSS/JS-String-
"""Laedt dist/index.html inline und injiziert window.PANEL_MODE. # Inlining laeuft nur EINMAL pro Plugin-Session statt pro Panel-Mount. Bei
# 10 Panels eliminiert das 9x ~395 KB Disk-Read + 9x Regex/Concat-Pass.
# Cache-Key = mtime von dist/index.html — wenn der User neu build, wird
# der Cache automatisch invalidiert.
_INLINE_TEMPLATE = None # (mtime_signature, head_html, body_html_template)
# Sentinel-Marker im Template fuer die per-Mount unterschiedlichen Scripts.
# Wir nutzen einen "Magic-String" statt format/{} damit die JS-Bundle-Inhalte
# (die {} Token enthalten koennen) nicht versehentlich matchen.
_MODE_SCRIPT_PLACEHOLDER = "/*__DOSSIER_MODE_SCRIPT__*/"
`params` (optional dict): wird als `window.PANEL_PARAMS` injiziert. Wird
von Satelliten-Fenstern (z.B. Settings-Dialoge) verwendet um initial- def _build_inline_template():
State an die React-App zu uebergeben.""" """Liest dist/index.html + inline alle CSS/JS. Liefert die HTML-String
mit Placeholder fuer den per-Mount Mode-Script-Block."""
t0 = time.time()
if not os.path.exists(_DIST): if not os.path.exists(_DIST):
print("[{}] dist nicht gefunden".format(mode.upper())) return None, None
return
dist_dir = os.path.dirname(_DIST) dist_dir = os.path.dirname(_DIST)
try:
mtime_sig = os.path.getmtime(_DIST)
except Exception:
mtime_sig = 0
with open(_DIST, "rb") as f: with open(_DIST, "rb") as f:
html = f.read().decode("utf-8") html = f.read().decode("utf-8")
parts = ['<script>window.PANEL_MODE="{}";'.format(mode)] placeholder_script = '<script>' + _MODE_SCRIPT_PLACEHOLDER + '</script>'
if params is not None:
try:
parts.append('window.PANEL_PARAMS=' + json.dumps(params, ensure_ascii=False) + ';')
except Exception as ex:
print("[{}] PANEL_PARAMS serialize: {}".format(mode.upper(), ex))
parts.append('</script>')
mode_script = ''.join(parts)
if "</head>" in html: if "</head>" in html:
html = html.replace("</head>", mode_script + "</head>") html = html.replace("</head>", placeholder_script + "</head>")
else: else:
html = mode_script + html html = placeholder_script + html
def inline_css(m): def inline_css(m):
p = os.path.join(dist_dir, m.group(1).lstrip("./").replace("/", os.sep)) p = os.path.join(dist_dir, m.group(1).lstrip("./").replace("/", os.sep))
@@ -238,7 +284,47 @@ def load_inline(wv, mode, params=None):
html = re.sub(r'<link[^>]+href="(\./assets/[^"]+\.css)"[^>]*/?>', inline_css, html) html = re.sub(r'<link[^>]+href="(\./assets/[^"]+\.css)"[^>]*/?>', inline_css, html)
html = re.sub(r'<script[^>]+src="(\./assets/[^"]+\.js)"[^>]*></script>', inline_js, html) html = re.sub(r'<script[^>]+src="(\./assets/[^"]+\.js)"[^>]*></script>', inline_js, html)
_t_mark("html-build", "build_inline_template", t0)
return mtime_sig, html
def load_inline(wv, mode, params=None):
"""Laedt dist/index.html inline und injiziert window.PANEL_MODE.
`params` (optional dict): wird als `window.PANEL_PARAMS` injiziert. Wird
von Satelliten-Fenstern (z.B. Settings-Dialoge) verwendet um initial-
State an die React-App zu uebergeben.
Performance: das fertige Inline-HTML (mit allen CSS/JS embedded) wird
modul-weit gecached. Pro Aufruf nur noch Mode-Script-Substitute + die
teure WebView.LoadHtml — Disk-IO laeuft 1x pro Plugin-Session."""
t0 = time.time()
global _INLINE_TEMPLATE
# Cache-Refresh wenn dist/index.html sich geaendert hat (neuer Build)
try:
cur_mtime = os.path.getmtime(_DIST)
except Exception:
cur_mtime = 0
if _INLINE_TEMPLATE is None or _INLINE_TEMPLATE[0] != cur_mtime:
sig, tmpl = _build_inline_template()
if tmpl is None:
print("[{}] dist nicht gefunden".format(mode.upper()))
return
_INLINE_TEMPLATE = (sig, tmpl)
# Per-Mount: nur das Mode-Script-Snippet bauen
parts = ['window.PANEL_MODE="{}";'.format(mode)]
if params is not None:
try:
parts.append('window.PANEL_PARAMS=' + json.dumps(params, ensure_ascii=False) + ';')
except Exception as ex:
print("[{}] PANEL_PARAMS serialize: {}".format(mode.upper(), ex))
mode_script = ''.join(parts)
html = _INLINE_TEMPLATE[1].replace(_MODE_SCRIPT_PLACEHOLDER, mode_script)
t_loadhtml = time.time()
wv.LoadHtml(html) wv.LoadHtml(html)
_t_mark("load_inline", mode, t0)
_t_mark("LoadHtml", mode, t_loadhtml)
def attach_webview(panel, bridge, mode): def attach_webview(panel, bridge, mode):
@@ -727,10 +813,12 @@ def register_and_open(mode, caption, guid_str, bridge_factory, icon_spec=None, m
Bei docked Panels wirkt's als Hint, bei float Panels als Bei docked Panels wirkt's als Hint, bei float Panels als
tatsaechliche Startgroesse. tatsaechliche Startgroesse.
""" """
t_outer = time.time()
sticky_reg = "panel_registered_" + mode sticky_reg = "panel_registered_" + mode
sticky_guid = "panel_guid_" + mode sticky_guid = "panel_guid_" + mode
if not sc.sticky.get(sticky_reg): if not sc.sticky.get(sticky_reg):
t_reg = time.time()
plugin = find_plugin() plugin = find_plugin()
if plugin is None: if plugin is None:
print("[{}] Plugin nicht gefunden".format(mode.upper())) print("[{}] Plugin nicht gefunden".format(mode.upper()))
@@ -762,11 +850,13 @@ def register_and_open(mode, caption, guid_str, bridge_factory, icon_spec=None, m
) )
icon = None icon = None
if icon_spec: if icon_spec:
t_icon = time.time()
try: try:
icon = make_panel_icon(icon_spec[0], icon_spec[1]) icon = make_panel_icon(icon_spec[0], icon_spec[1])
except Exception as ex: except Exception as ex:
print("[{}] Icon-Erstellung uebersprungen: {}".format(mode.upper(), ex)) print("[{}] Icon-Erstellung uebersprungen: {}".format(mode.upper(), ex))
icon = None icon = None
_t_mark("icon", mode, t_icon)
registered = False registered = False
registered_with_icon = False registered_with_icon = False
# Erst mit Icon versuchen, dann stillschweigend ohne (Mac Rhino-Panels # Erst mit Icon versuchen, dann stillschweigend ohne (Mac Rhino-Panels
@@ -800,10 +890,14 @@ def register_and_open(mode, caption, guid_str, bridge_factory, icon_spec=None, m
except Exception as ex: except Exception as ex:
print("[{}] Registrierung fehlgeschlagen: {}".format(mode.upper(), ex)) print("[{}] Registrierung fehlgeschlagen: {}".format(mode.upper(), ex))
return return
_t_mark("register", mode, t_reg)
try: try:
t_open = time.time()
guid = sc.sticky.get(sticky_guid, System.Guid(guid_str)) guid = sc.sticky.get(sticky_guid, System.Guid(guid_str))
RhinoUI.Panels.OpenPanel(guid) RhinoUI.Panels.OpenPanel(guid)
_t_mark("OpenPanel", mode, t_open)
print("[{}] Panel geoeffnet".format(mode.upper())) print("[{}] Panel geoeffnet".format(mode.upper()))
except Exception as ex: except Exception as ex:
print("[{}] OpenPanel fehlgeschlagen: {}".format(mode.upper(), ex)) print("[{}] OpenPanel fehlgeschlagen: {}".format(mode.upper(), ex))
_t_mark("register_and_open", mode, t_outer)
+499 -102
View File
@@ -103,6 +103,243 @@ def _broadcast_state(doc=None, hatch_patterns=None):
except Exception: pass except Exception: pass
# --- Layer-Kombinationen: Modul-Level Helpers ------------------------------
# Diese Helfer werden sowohl von EbenenBridge (Ebenen-Panel) als auch von
# OberleisteBridge (Top-Bar) und LayerCombinationsBridge (Satelliten-Editor)
# benutzt. doc.Strings ist die einzige Quelle der Wahrheit; nach jedem Write
# rufen die Caller _broadcast_state(doc) + invalidate cross-bridge caches.
_PRESETS_KEY = "dossier_layer_presets"
_ACTIVE_COMB_KEY = "dossier_layer_active_comb"
def load_layer_presets(doc):
raw = doc.Strings.GetValue(_PRESETS_KEY)
if not raw: return []
try:
data = json.loads(raw)
return data if isinstance(data, list) else []
except Exception:
return []
def store_layer_presets(doc, presets):
try:
doc.Strings.SetString(_PRESETS_KEY,
json.dumps(presets, ensure_ascii=False))
except Exception as ex:
print("[EBENEN] store_layer_presets:", ex)
def get_active_comb_name(doc):
try:
v = doc.Strings.GetValue(_ACTIVE_COMB_KEY)
return v if v else None
except Exception:
return None
def set_active_comb_name(doc, name):
try:
doc.Strings.SetString(_ACTIVE_COMB_KEY, name or "")
except Exception as ex:
print("[EBENEN] set_active_comb_name:", ex)
def list_layer_preset_names(doc):
return [p.get("name") for p in load_layer_presets(doc)
if isinstance(p, dict) and p.get("name")]
def _notify_oberleiste_combs():
"""Cache der Oberleiste invalidieren + force-send. Wird gerufen wenn
die Combinations-Liste oder activeCombName sich aendert."""
try:
b = sc.sticky.get("oberleiste_bridge")
if b is not None:
b._cached_combinations = None
b._send_state(force=True)
except Exception as ex:
print("[EBENEN] notify oberleiste combs:", ex)
def _notify_layer_combinations_editor():
"""Satelliten-Fenster (Editor) informieren falls offen — Layer-/Preset-
Liste hat sich geaendert."""
try:
b = sc.sticky.get("layer_combinations_bridge")
if b is not None: b._send_state()
except Exception as ex:
print("[EBENEN] notify layer-combinations editor:", ex)
def apply_layer_preset_by_name(doc, name):
"""Laedt Preset `name` und wendet es an. Setzt active_comb_name.
Liefert True wenn erfolgreich."""
if not name: return False
presets = load_layer_presets(doc)
preset = next((p for p in presets if p.get("name") == name), None)
if preset is None:
print("[EBENEN] apply_layer_preset_by_name: '{}' nicht gefunden".format(name))
return False
payload = {
"layers": preset.get("layers") or [],
"dossierEbenen": preset.get("dossierEbenen"),
"dossierZeichnungsebenen": preset.get("dossierZeichnungsebenen"),
}
# Routing: wenn die EbenenBridge existiert, delegiere — die hat den
# vollen Eye-State-Pfad inkl. STATE_SYNC + Redraw. Sonst inline applien.
eb = sc.sticky.get("ebenen_bridge_ref")
if eb is not None:
try:
eb._apply_combination(payload)
except Exception as ex:
print("[EBENEN] apply via bridge:", ex)
return False
else:
# Fallback: direkt doc.Strings + doc.Layer setzen (kein Bridge offen)
_apply_layer_preset_inline(doc, payload)
set_active_comb_name(doc, name)
_notify_oberleiste_combs()
return True
def _apply_layer_preset_inline(doc, payload):
"""Fallback wenn keine EbenenBridge offen ist — minimaler Layer-State-
Pfad. Setzt doc.Strings + doc.Layer.IsVisible direkt."""
pe = payload.get("dossierEbenen")
pz = payload.get("dossierZeichnungsebenen")
try:
e_raw = doc.Strings.GetValue("dossier_ebenen") or "[]"
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen") or "[]"
e_list = json.loads(e_raw) or []
z_list = json.loads(z_raw) or []
if isinstance(pe, list):
by_code = {x.get("code"): x for x in pe if isinstance(x, dict) and x.get("code")}
for e in e_list:
if not isinstance(e, dict): continue
s = by_code.get(e.get("code"))
if s is None: continue
e["visible"] = bool(s.get("visible", True))
e["locked"] = bool(s.get("locked", False))
if isinstance(pz, list):
by_id = {x.get("id"): x for x in pz if isinstance(x, dict) and x.get("id")}
for z in z_list:
if not isinstance(z, dict): continue
s = by_id.get(z.get("id"))
if s is None: continue
z["visible"] = bool(s.get("visible", True))
doc.Strings.SetString("dossier_ebenen", json.dumps(e_list, ensure_ascii=False))
doc.Strings.SetString("dossier_zeichnungsebenen", json.dumps(z_list, ensure_ascii=False))
except Exception as ex:
print("[EBENEN] inline preset-apply (eye-state):", ex)
# Layer-ID-Pfad als Sekundaer (AUSSCHNITTE-Kompat)
layer_states = payload.get("layers") or []
if layer_states:
by_id = {}
try:
for layer in doc.Layers:
if not layer.IsDeleted: by_id[str(layer.Id)] = layer
except Exception: pass
_set_processing(True)
try:
for ls in layer_states:
layer = by_id.get(ls.get("id"))
if layer is None: continue
try:
want_vis = bool(ls.get("visible", True))
want_lck = bool(ls.get("locked", False))
if layer.IsVisible != want_vis: layer.IsVisible = want_vis
if layer.IsLocked != want_lck: layer.IsLocked = want_lck
except Exception: pass
finally:
_set_processing(False)
try: doc.Views.Redraw()
except Exception: pass
_broadcast_state(doc)
def save_current_as_layer_preset(doc, name):
"""Speichert die aktuellen Eye-States als Preset. Setzt active_comb_name."""
name = (name or "").strip()
if not name: return False
# 1) doc.Layer state (Kompat mit AUSSCHNITTE)
layers = []
try:
for layer in doc.Layers:
if layer is None or layer.IsDeleted: continue
layers.append({
"id": str(layer.Id),
"visible": bool(layer.IsVisible),
"locked": bool(layer.IsLocked),
})
except Exception as ex:
print("[EBENEN] save_current_as_layer_preset enum:", ex)
# 2) Eye-States aus dossier_ebenen / dossier_zeichnungsebenen
pe_state, pz_state = [], []
try:
e_raw = doc.Strings.GetValue("dossier_ebenen")
if e_raw:
for e in (json.loads(e_raw) or []):
if isinstance(e, dict) and e.get("code"):
pe_state.append({
"code": e["code"],
"visible": bool(e.get("visible", True)),
"locked": bool(e.get("locked", False)),
})
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
if z_raw:
for z in (json.loads(z_raw) or []):
if isinstance(z, dict) and z.get("id"):
pz_state.append({
"id": z["id"],
"visible": bool(z.get("visible", True)),
})
except Exception as ex:
print("[EBENEN] save_current_as_layer_preset eye-states:", ex)
presets = load_layer_presets(doc)
new_data = {
"name": name,
"layers": layers,
"dossierEbenen": pe_state,
"dossierZeichnungsebenen": pz_state,
}
existing = next((p for p in presets if p.get("name") == name), None)
if existing is not None:
existing.update(new_data)
else:
presets.append(new_data)
store_layer_presets(doc, presets)
set_active_comb_name(doc, name)
_notify_oberleiste_combs()
_notify_layer_combinations_editor()
print("[EBENEN] '{}' gespeichert: {} Layer + {} Ebenen Eye-State".format(
name, len(layers), len(pe_state)))
return True
def delete_layer_preset(doc, name):
name = (name or "").strip()
if not name: return False
presets = [p for p in load_layer_presets(doc) if p.get("name") != name]
store_layer_presets(doc, presets)
if get_active_comb_name(doc) == name:
set_active_comb_name(doc, None)
_notify_oberleiste_combs()
_notify_layer_combinations_editor()
print("[EBENEN] Kombination '{}' geloescht".format(name))
return True
def clear_active_comb_name(doc):
"""Wird vom EbenenBridge SET_VISIBILITY / APPLY-Pfad gerufen wenn der
User per Hand etwas am Layer-State aendert — dann passt das Preset nicht
mehr und wir markieren 'Eigene'."""
if get_active_comb_name(doc):
set_active_comb_name(doc, None)
_notify_oberleiste_combs()
class EbenenBridge(panel_base.BaseBridge): class EbenenBridge(panel_base.BaseBridge):
"""Gemeinsame Bridge-Klasse fuer beide Panels (Ebenen + Zeichnungsebenen). """Gemeinsame Bridge-Klasse fuer beide Panels (Ebenen + Zeichnungsebenen).
Mode bestimmt nur welches WebView die Bridge bedient + welcher sticky-Slot Mode bestimmt nur welches WebView die Bridge bedient + welcher sticky-Slot
@@ -266,40 +503,80 @@ class EbenenBridge(panel_base.BaseBridge):
on_save=on_save) on_save=on_save)
def _open_ebenen_settings(self, ebene, hatch_patterns): def _open_ebenen_settings(self, ebene, hatch_patterns):
"""Oeffnet ein echtes Rhino-Fenster mit dem EbenenSettingsDialog.""" """Oeffnet ein echtes Rhino-Fenster mit dem EbenenSettingsDialog.
Mit Dropdown zum Wechsel zwischen Ebenen; jeder Switch persistiert
die aktuelle Ebene live (SAVE_KEEP), Schliess-/Übernehmen-Knopf
persistiert + schliesst (SAVE)."""
if not isinstance(ebene, dict) or not ebene.get("code"): if not isinstance(ebene, dict) or not ebene.get("code"):
print("[EBENEN] open_ebenen_settings: kein Ebene-Payload") print("[EBENEN] open_ebenen_settings: kein Ebene-Payload")
return return
old_code = ebene["code"] bridge_holder = {"form": None}
def on_save(updated): apply_self = self
doc = Rhino.RhinoDoc.ActiveDoc class _EbenenSettingsBridge(panel_base.BaseBridge):
if doc is None: return def __init__(self):
e_raw = doc.Strings.GetValue("dossier_ebenen") panel_base.BaseBridge.__init__(self, "ebenen_settings")
if not e_raw: def _on_ready(self):
print("[EBENEN] save_ebene: kein e-Store"); return self._send_state()
try: def _send_state(self):
e_list = json.loads(e_raw) doc = Rhino.RhinoDoc.ActiveDoc
except Exception as ex: e_raw = doc.Strings.GetValue("dossier_ebenen") if doc else None
print("[EBENEN] save_ebene JSON:", ex); return try: e_list = json.loads(e_raw) if e_raw else []
replaced = False except Exception: e_list = []
for i, e in enumerate(e_list): self.send("EBENEN_SETTINGS_STATE", {
if isinstance(e, dict) and e.get("code") == old_code: "ebenen": e_list,
e_list[i] = updated "hatchPatterns": hatch_patterns,
replaced = True })
break def handle(self, data):
if not replaced: if not isinstance(data, dict): return
print("[EBENEN] save_ebene: code {} nicht gefunden".format(old_code)) t = data.get("type", "")
return p = data.get("payload") or {}
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen") if t == "READY":
try: z_list = json.loads(z_raw) if z_raw else [] self._on_ready()
except Exception: z_list = [] elif t == "SAVE_KEEP":
self._apply(z_list, e_list, save_z=False, save_e=True) self._persist(p)
panel_base.open_satellite_window( elif t == "SAVE":
self._persist(p)
try:
f = bridge_holder.get("form")
if f is not None: f.Close()
except Exception: pass
elif t == "CANCEL":
try:
f = bridge_holder.get("form")
if f is not None: f.Close()
except Exception: pass
def _persist(self, p):
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return
updated = p.get("ebene") or {}
orig_code = p.get("originalCode") or updated.get("code")
if not (isinstance(updated, dict) and updated.get("code")): return
e_raw = doc.Strings.GetValue("dossier_ebenen")
if not e_raw: return
try: e_list = json.loads(e_raw)
except Exception as ex:
print("[EBENEN] save_ebene JSON:", ex); return
replaced = False
for i, e in enumerate(e_list):
if isinstance(e, dict) and e.get("code") == orig_code:
e_list[i] = updated
replaced = True
break
if not replaced:
print("[EBENEN] save_ebene: code {} nicht gefunden".format(orig_code))
return
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
try: z_list = json.loads(z_raw) if z_raw else []
except Exception: z_list = []
apply_self._apply(z_list, e_list, save_z=False, save_e=True)
self._send_state()
b = _EbenenSettingsBridge()
bridge_holder["form"] = panel_base.open_satellite_window(
"ebenen_settings", "ebenen_settings",
params={"ebene": ebene, "hatchPatterns": hatch_patterns}, params={"currentCode": ebene["code"], "hatchPatterns": hatch_patterns},
title="Ebene: {}_{}".format(ebene.get("code", ""), ebene.get("name", "")), title="Ebenen-Einstellungen",
size=(420, 600), size=(420, 600),
on_save=on_save) bridge=b)
def _open_geschoss_dialog(self, zeichnungsebenen): def _open_geschoss_dialog(self, zeichnungsebenen):
"""Oeffnet den vollen GeschossDialog (Mehrfach-Editor) als """Oeffnet den vollen GeschossDialog (Mehrfach-Editor) als
@@ -416,6 +693,9 @@ class EbenenBridge(panel_base.BaseBridge):
self._update_clipping() self._update_clipping()
print("[EBENEN] _apply: send APPLY_OK") print("[EBENEN] _apply: send APPLY_OK")
self.send("APPLY_OK", {}) self.send("APPLY_OK", {})
# Strukturelle Aenderung (neue/umbenannte/geloeschte Ebene) → aktives
# Preset passt nicht mehr exakt.
clear_active_comb_name(doc)
# Anderes Panel (Zeichnungsebenen/Ebenen) ueber den neuen State # Anderes Panel (Zeichnungsebenen/Ebenen) ueber den neuen State
# informieren — sonst hinkt es hinter der DOM-Persistenz her. # informieren — sonst hinkt es hinter der DOM-Persistenz her.
_broadcast_state(doc) _broadcast_state(doc)
@@ -476,6 +756,7 @@ class EbenenBridge(panel_base.BaseBridge):
s = z_state.get(z.get("id")) s = z_state.get(z.get("id"))
if s is not None: if s is not None:
m["visible"] = s.get("visible", True) m["visible"] = s.get("visible", True)
m["locked"] = s.get("locked", False)
merged_z.append(m) merged_z.append(m)
merged_e = [] merged_e = []
for e in e_full: for e in e_full:
@@ -486,11 +767,33 @@ class EbenenBridge(panel_base.BaseBridge):
m["visible"] = s.get("visible", True) m["visible"] = s.get("visible", True)
m["locked"] = s.get("locked", False) m["locked"] = s.get("locked", False)
merged_e.append(m) merged_e.append(m)
# Detect whether the merge actually changed any visible/locked values.
# Wenn nicht: das ist nur der Echo-Roundtrip eines apply_layer_preset
# (React-State == doc.Strings → kein User-Click) und wir wollen das
# aktive Preset NICHT clearen.
def _vis_lock_changed(old, new):
old_by = {x.get("id") or x.get("code"): x for x in old if isinstance(x, dict)}
for nx in new:
if not isinstance(nx, dict): continue
key = nx.get("id") or nx.get("code")
if key is None: continue
ox = old_by.get(key)
if ox is None: continue
if (ox.get("visible", True) != nx.get("visible", True)
or ox.get("locked", False) != nx.get("locked", False)):
return True
return False
any_changed = (_vis_lock_changed(z_full, merged_z)
or _vis_lock_changed(e_full, merged_e))
if has_new_structural: if has_new_structural:
print("[EBENEN] _apply_visibility: structural change pending → skip save (waiting for APPLY)") print("[EBENEN] _apply_visibility: structural change pending → skip save (waiting for APPLY)")
else: else:
doc.Strings.SetString("dossier_zeichnungsebenen", json.dumps(merged_z, ensure_ascii=False)) doc.Strings.SetString("dossier_zeichnungsebenen", json.dumps(merged_z, ensure_ascii=False))
doc.Strings.SetString("dossier_ebenen", json.dumps(merged_e, ensure_ascii=False)) doc.Strings.SetString("dossier_ebenen", json.dumps(merged_e, ensure_ascii=False))
# User hat per Hand Eye/Lock geaendert → aktives Preset passt nicht
# mehr, auf "Eigene" zuruecksetzen.
if any_changed:
clear_active_comb_name(doc)
# zMode + eMode persistieren, damit bei Split-Send (nur eine # zMode + eMode persistieren, damit bei Split-Send (nur eine
# Panel-Slice) der andere Mode aus dem Doc-Storage faellt anstatt # Panel-Slice) der andere Mode aus dem Doc-Storage faellt anstatt
# auf den Default zu rutschen. # auf den Default zu rutschen.
@@ -504,6 +807,24 @@ class EbenenBridge(panel_base.BaseBridge):
if not isinstance(active_z, dict): active_z = {} if not isinstance(active_z, dict): active_z = {}
active_z_id = active_z.get("id") or doc.Strings.GetValue("dossier_active_id") active_z_id = active_z.get("id") or doc.Strings.GetValue("dossier_active_id")
active_code = p.get("activeCode") or doc.Strings.GetValue("dossier_active_code") active_code = p.get("activeCode") or doc.Strings.GetValue("dossier_active_code")
# Dedupe: identisches SET_VISIBILITY (z.B. STATE_SYNC-Echo nach
# Preset-Apply) loopt sonst unnoetig durch alle ~100 Doc-Layer.
# Signatur aus active-id/code + mode + vis/lock-Liste.
def _sig(zlist, elist):
zs = tuple((z.get("id"),
bool(z.get("visible", True)),
bool(z.get("locked", False)))
for z in zlist if isinstance(z, dict))
es = tuple((e.get("code"),
bool(e.get("visible", True)),
bool(e.get("locked", False)))
for e in elist if isinstance(e, dict))
return (active_z_id, active_code, z_mode, e_mode, zs, es)
cur_sig = _sig(merged_z, merged_e)
if sc.sticky.get("_vis_last_sig") == cur_sig and not any_changed:
# Nichts Neues — Rhino-Layer-State ist schon korrekt.
return
sc.sticky["_vis_last_sig"] = cur_sig
layer_builder.apply_visibility( layer_builder.apply_visibility(
doc, merged_z, merged_e, active_z_id, active_code, z_mode, e_mode) doc, merged_z, merged_e, active_z_id, active_code, z_mode, e_mode)
# Cross-Panel-Sync NUR wenn wir nicht in einem structural-pending # Cross-Panel-Sync NUR wenn wir nicht in einem structural-pending
@@ -515,17 +836,29 @@ class EbenenBridge(panel_base.BaseBridge):
def _set_active_zeichnungsebene(self, z): def _set_active_zeichnungsebene(self, z):
doc = Rhino.RhinoDoc.ActiveDoc doc = Rhino.RhinoDoc.ActiveDoc
z_id = z.get("id", "") z_id = z.get("id", "")
# Vorigen Stand merken um redundante teure Operationen zu sparen
prev_active_id = doc.Strings.GetValue("dossier_active_id") or ""
doc.Strings.SetString("dossier_active_id", z_id) doc.Strings.SetString("dossier_active_id", z_id)
# Aktiven Sublayer auf die GLEICHE Ebene unter dem neuen Geschoss
# umschalten — wenn User auf "20 Wände" steht und das Geschoss
# wechselt, soll Rhino's aktiver Layer "1OG::20_Wände" werden statt
# auf der vorigen Geschoss-Ebene haengen zu bleiben.
self._ensure_active_sublayer()
# Cross-Panel-Sync: Ebenen-Panel muss aktive Geschoss-Auswahl # Cross-Panel-Sync: Ebenen-Panel muss aktive Geschoss-Auswahl
# mitbekommen falls es im "active"-Filter-Mode laeuft. # mitbekommen falls es im "active"-Filter-Mode laeuft.
_broadcast_state(doc) _broadcast_state(doc)
# Clipping ggf. mitziehen # Clipping nur antasten wenn entweder das alte oder das neue Geschoss
self._update_clipping(active_z=z) # eine Clipping-Plane hatte — sonst sparen wir Plane-Delete + Build
# + View-Redraw bei jedem Geschoss-Wechsel ganz.
if self._needs_clipping_update(doc, prev_active_id, z):
self._update_clipping(active_z=z)
# Elemente-Panel informieren: das aktive Geschoss hat gewechselt, # Elemente-Panel informieren: das aktive Geschoss hat gewechselt,
# neue Elemente sollen jetzt automatisch dort verlinkt werden. # neue Elemente sollen jetzt automatisch dort verlinkt werden.
# Wichtig: NICHT _send_state() rufen (re-enumeriert alle Elemente,
# 200+ in echten Projekten = spuerbar). Schlanker Partial-Push.
try: try:
eb = sc.sticky.get("elemente_bridge") eb = sc.sticky.get("elemente_bridge")
if eb is not None: eb._send_state() if eb is not None: eb._notify_active_geschoss()
except Exception: pass except Exception: pass
if not (z.get("isGeschoss") and z.get("okff") is not None): if not (z.get("isGeschoss") and z.get("okff") is not None):
return return
@@ -546,12 +879,33 @@ class EbenenBridge(panel_base.BaseBridge):
plane.XAxis, plane.YAxis, plane.XAxis, plane.YAxis,
) )
vp.SetConstructionPlane(new_plane) vp.SetConstructionPlane(new_plane)
view.Redraw()
updated += 1 updated += 1
except Exception as ex: except Exception as ex:
print("[EBENEN] CPlane fehler ({}): {}".format(vp.Name if vp else "?", ex)) print("[EBENEN] CPlane fehler ({}): {}".format(vp.Name if vp else "?", ex))
# KEIN doc.Views.Redraw() hier — die folgende SET_VISIBILITY-Round-
# trip (30 ms debounce in React) feuert ohnehin layer_builder
# .apply_visibility() das am Ende selbst redrawt. Sparen wir uns
# einen doppelten Full-Repaint pro Geschoss-Klick.
print("[EBENEN] CPlane Z={} auf {} Top-Style View(s) gesetzt".format(okff, updated)) print("[EBENEN] CPlane Z={} auf {} Top-Style View(s) gesetzt".format(okff, updated))
def _needs_clipping_update(self, doc, prev_active_id, new_z):
"""Liefert True wenn entweder das alte oder das neue Geschoss
hasClipping=True hat. Sonst kann _update_clipping skipped werden
(Plane existiert nicht und muss auch nicht neu gebaut werden)."""
new_has = bool(new_z.get("hasClipping"))
if new_has:
return True
if not prev_active_id:
return False
try:
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
if not z_raw: return False
for z in (json.loads(z_raw) or []):
if isinstance(z, dict) and z.get("id") == prev_active_id:
return bool(z.get("hasClipping"))
except Exception: pass
return False
def _toggle_clipping_for_active(self, enabled): def _toggle_clipping_for_active(self, enabled):
"""Setzt hasClipping fuer das aktuell aktive Geschoss + persistiert in """Setzt hasClipping fuer das aktuell aktive Geschoss + persistiert in
doc.Strings + triggert plane-update. Wird vom React-Toggle 'Clipping doc.Strings + triggert plane-update. Wird vom React-Toggle 'Clipping
@@ -704,20 +1058,10 @@ class EbenenBridge(panel_base.BaseBridge):
_PRESETS_KEY = "dossier_layer_presets" _PRESETS_KEY = "dossier_layer_presets"
def _load_presets(self, doc): def _load_presets(self, doc):
raw = doc.Strings.GetValue(self._PRESETS_KEY) return load_layer_presets(doc)
if not raw: return []
try:
data = json.loads(raw)
return data if isinstance(data, list) else []
except Exception:
return []
def _store_presets(self, doc, presets): def _store_presets(self, doc, presets):
try: store_layer_presets(doc, presets)
doc.Strings.SetString(self._PRESETS_KEY,
json.dumps(presets, ensure_ascii=False))
except Exception as ex:
print("[EBENEN] _store_presets:", ex)
def _send_combination(self): def _send_combination(self):
"""Schickt aktuelles Layer-State + alle Presets ans Frontend.""" """Schickt aktuelles Layer-State + alle Presets ans Frontend."""
@@ -917,7 +1261,7 @@ class EbenenBridge(panel_base.BaseBridge):
name = (name or "").strip() name = (name or "").strip()
if not name: return if not name: return
doc = Rhino.RhinoDoc.ActiveDoc doc = Rhino.RhinoDoc.ActiveDoc
presets = self._load_presets(doc) presets = load_layer_presets(doc)
clean = [] clean = []
for ls in (layers or []): for ls in (layers or []):
lid = ls.get("id") lid = ls.get("id")
@@ -932,7 +1276,9 @@ class EbenenBridge(panel_base.BaseBridge):
existing["layers"] = clean existing["layers"] = clean
else: else:
presets.append({"name": name, "layers": clean}) presets.append({"name": name, "layers": clean})
self._store_presets(doc, presets) store_layer_presets(doc, presets)
_notify_oberleiste_combs()
_notify_layer_combinations_editor()
print("[EBENEN] Kombination '{}' gespeichert ({} Layer)".format(name, len(clean))) print("[EBENEN] Kombination '{}' gespeichert ({} Layer)".format(name, len(clean)))
def _save_current_as_preset(self, name): def _save_current_as_preset(self, name):
@@ -944,67 +1290,118 @@ class EbenenBridge(panel_base.BaseBridge):
layers (doc.Layer-Liste) wird parallel mitgespeichert fuer Kompat layers (doc.Layer-Liste) wird parallel mitgespeichert fuer Kompat
mit AUSSCHNITTE (das vom doc.Layer-State liest).""" mit AUSSCHNITTE (das vom doc.Layer-State liest)."""
name = (name or "").strip() save_current_as_layer_preset(Rhino.RhinoDoc.ActiveDoc, name)
if not name: return
def _delete_preset(self, name):
delete_layer_preset(Rhino.RhinoDoc.ActiveDoc, name)
class LayerCombinationsBridge(panel_base.BaseBridge):
"""Bridge fuer das Satelliten-Fenster mit dem grossen Ebenenkombinationen-
Editor (AusschnittLayerDialog). Wird vom Oberleiste-Bridge geoeffnet bei
OPEN_LAYER_COMBINATIONS_DIALOG. State wird beim READY-Event geschickt und
bei jeder Aenderung re-emittet."""
def __init__(self):
panel_base.BaseBridge.__init__(self, "layer_combinations")
def _on_ready(self):
self._send_state()
def _send_state(self):
doc = Rhino.RhinoDoc.ActiveDoc doc = Rhino.RhinoDoc.ActiveDoc
# 1) doc.Layer state (Kompat mit AUSSCHNITTE) if doc is None: return
layers = [] layers_out = []
try: try:
for layer in doc.Layers: for layer in doc.Layers:
if layer is None or layer.IsDeleted: continue if layer is None or layer.IsDeleted: continue
layers.append({ lid = str(layer.Id)
"id": str(layer.Id), try: fp = layer.FullPath or layer.Name
"visible": bool(layer.IsVisible), except Exception: fp = layer.Name or ""
"locked": bool(layer.IsLocked), try: col = "#%02x%02x%02x" % (layer.Color.R, layer.Color.G, layer.Color.B)
except Exception: col = "#888888"
layers_out.append({
"id": lid,
"name": layer.Name,
"fullPath": fp,
"color": col,
"visible": bool(layer.IsVisible),
"locked": bool(layer.IsLocked),
}) })
layers_out.sort(key=lambda x: x["fullPath"])
except Exception as ex: except Exception as ex:
print("[EBENEN] _save_current_as_preset enum:", ex) print("[LAYER-COMB] enum:", ex)
# 2) Eye-States aus dossier_ebenen / dossier_zeichnungsebenen self.send("LAYER_COMBINATIONS_STATE", {
pe_state = [] "layers": layers_out,
pz_state = [] "presets": load_layer_presets(doc),
try: })
e_raw = doc.Strings.GetValue("dossier_ebenen")
if e_raw:
for e in (json.loads(e_raw) or []):
if isinstance(e, dict) and e.get("code"):
pe_state.append({
"code": e["code"],
"visible": bool(e.get("visible", True)),
"locked": bool(e.get("locked", False)),
})
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
if z_raw:
for z in (json.loads(z_raw) or []):
if isinstance(z, dict) and z.get("id"):
pz_state.append({
"id": z["id"],
"visible": bool(z.get("visible", True)),
})
except Exception as ex:
print("[EBENEN] _save_current_as_preset eye-states:", ex)
presets = self._load_presets(doc)
new_data = {
"name": name,
"layers": layers,
"dossierEbenen": pe_state,
"dossierZeichnungsebenen": pz_state,
}
existing = next((p for p in presets if p.get("name") == name), None)
if existing is not None:
existing.update(new_data)
else:
presets.append(new_data)
self._store_presets(doc, presets)
print("[EBENEN] '{}' gespeichert: {} Layer + {} Ebenen Eye-State".format(
name, len(layers), len(pe_state)))
def _delete_preset(self, name): def handle(self, data):
name = (name or "").strip() if not isinstance(data, dict): return
if not name: return t = data.get("type", "")
p = data.get("payload") or {}
if not isinstance(p, dict): p = {}
doc = Rhino.RhinoDoc.ActiveDoc doc = Rhino.RhinoDoc.ActiveDoc
presets = [p for p in self._load_presets(doc) if p.get("name") != name]
self._store_presets(doc, presets) if t == "READY" or t == "REQUEST_STATE":
print("[EBENEN] Kombination '{}' geloescht".format(name)) self._on_ready()
elif t == "APPLY_COMBINATION":
# Editor wendet eine Layer-State-Liste an (ohne Preset-Name —
# also "Eigene"). Wir delegieren an die Ebenen-Bridge wenn offen,
# sonst inline. activeCombName auf None setzen.
eb = sc.sticky.get("ebenen_bridge_ref")
if eb is not None:
try: eb._apply_combination(p)
except Exception as ex:
print("[LAYER-COMB] apply via bridge:", ex)
else:
_apply_layer_preset_inline(doc, p)
set_active_comb_name(doc, None)
_notify_oberleiste_combs()
self._send_state()
elif t == "SAVE_PRESET":
# Editor speichert die im Dialog kuratierte Liste unter Namen.
name = (p.get("name") or "").strip()
layers = p.get("layers") or []
if name:
presets = load_layer_presets(doc)
clean = []
for ls in layers:
lid = ls.get("id")
if not lid: continue
clean.append({
"id": lid,
"visible": bool(ls.get("visible", True)),
"locked": bool(ls.get("locked", False)),
})
existing = next((pp for pp in presets if pp.get("name") == name), None)
if existing is not None:
existing["layers"] = clean
else:
presets.append({"name": name, "layers": clean})
store_layer_presets(doc, presets)
_notify_oberleiste_combs()
self._send_state()
elif t == "DELETE_PRESET":
delete_layer_preset(doc, p.get("name") or "")
self._send_state()
elif t == "CANCEL":
try:
form = sc.sticky.get("layer_combinations_form")
if form is not None: form.Close()
except Exception: pass
def open_layer_combinations_window():
"""Oeffnet den Editor als echtes Rhino-Fenster (Eto.Form + WebView).
Wird vom Oberleiste-Bridge bei OPEN_LAYER_COMBINATIONS_DIALOG gerufen."""
b = LayerCombinationsBridge()
sc.sticky["layer_combinations_bridge"] = b
form = panel_base.open_satellite_window(
"layer_combinations",
title="Ebenenkombinationen",
size=(540, 640),
bridge=b)
sc.sticky["layer_combinations_form"] = form
def _ebenen_bridge_factory(): def _ebenen_bridge_factory():
@@ -1087,8 +1484,8 @@ def _install_layer_listener(bridge):
print("[EBENEN] Layer-Listener aktiv") print("[EBENEN] Layer-Listener aktiv")
panel_base.register_and_open("ebenen", "EBENEN", PANEL_GUID_STR, _ebenen_bridge_factory, panel_base.register_and_open("ebenen", "Ebenen", PANEL_GUID_STR, _ebenen_bridge_factory,
icon_spec=("layers", "#3a6fa8")) icon_spec=("layers", "#3a6fa8"))
panel_base.register_and_open("zeichnungsebenen", "ZEICHNUNGSEBENEN", PANEL_GUID_STR_Z, panel_base.register_and_open("zeichnungsebenen", "Zeichnungsebenen", PANEL_GUID_STR_Z,
_zeichnungsebenen_bridge_factory, _zeichnungsebenen_bridge_factory,
icon_spec=("levels", "#3a6fa8")) icon_spec=("levels", "#3a6fa8"))
+11
View File
@@ -135,6 +135,17 @@ def _load_all(sender, e):
print("[STARTUP] {} ({}) FEHLER: {}".format(mod_id, py_name, ex)) print("[STARTUP] {} ({}) FEHLER: {}".format(mod_id, py_name, ex))
# DOSSIERUI Window-Layout — Hinweis fuer manuelles Laden # DOSSIERUI Window-Layout — Hinweis fuer manuelles Laden
_hint_dossier_ui() _hint_dossier_ui()
# Startup-Timing-Summary 3 Sekunden spaeter (nachdem alle async Idle-
# Loads + WebView-Renders durch sind). Manueller Aufruf:
# _RunPythonScript -c "import panel_base; panel_base.print_startup_summary()"
def _summary():
try:
import panel_base
panel_base.print_startup_summary()
except Exception as ex:
print("[STARTUP] summary:", ex)
import threading
threading.Timer(3.0, _summary).start()
# Marker fuer den Launcher-Splash mit Verzoegerung: erst nachdem Rhino die # Marker fuer den Launcher-Splash mit Verzoegerung: erst nachdem Rhino die
# Panels visuell platziert hat (~2s nach Modul-Imports). Pfad ist projekt- # Panels visuell platziert hat (~2s nach Modul-Imports). Pfad ist projekt-
# stabil (gleich wie dossier_settings.json), damit Launcher ohne # stabil (gleich wie dossier_settings.json), damit Launcher ohne
+1 -1
View File
@@ -54,5 +54,5 @@ def _bridge_factory():
return WerkzeugeBridge() return WerkzeugeBridge()
panel_base.register_and_open("werkzeuge", "WERKZEUGE", PANEL_GUID_STR, _bridge_factory, panel_base.register_and_open("werkzeuge", "Werkzeuge", PANEL_GUID_STR, _bridge_factory,
icon_spec=("build", "#3a6fa8")) icon_spec=("build", "#3a6fa8"))
+19 -112
View File
@@ -1,33 +1,29 @@
import { useState, useEffect, useMemo, useRef } from 'react' import { useState, useEffect, useMemo } from 'react'
import EbenenManager from './components/EbenenManager' import EbenenManager from './components/EbenenManager'
import AusschnittLayerDialog from './components/AusschnittLayerDialog'
import { import {
applyAll, setActiveEbene, applyAll, setActiveEbene,
onMessage, notifyReady, applyVisibility, onMessage, notifyReady, applyVisibility,
getCombination, applyCombination,
saveCurrentAsCombination, deleteCombinationPreset,
saveCombinationPreset,
} from './lib/rhinoBridge' } from './lib/rhinoBridge'
const INITIAL_EBENEN = [ const INITIAL_EBENEN = [
{ code: '00', name: 'RASTER', color: '#484850', lw: 0.13, visible: true, locked: false }, { code: '00', name: 'Raster', color: '#484850', lw: 0.13, visible: true, locked: false },
{ code: '01', name: 'VERMESSUNG', color: '#707078', lw: 0.18, visible: true, locked: false }, { code: '01', name: 'Vermessung', color: '#707078', lw: 0.18, visible: true, locked: false },
{ code: '10', name: 'SITUATION', color: '#909090', lw: 0.18, visible: true, locked: false }, { code: '10', name: 'Situation', color: '#909090', lw: 0.18, visible: true, locked: false },
{ code: '11', name: 'STRASSE', color: '#a89070', lw: 0.18, visible: true, locked: false }, { code: '11', name: 'Strasse', color: '#a89070', lw: 0.18, visible: true, locked: false },
{ code: '12', name: 'GEBÄUDE', color: '#888888', lw: 0.25, visible: true, locked: false }, { code: '12', name: 'Gebäude', color: '#888888', lw: 0.25, visible: true, locked: false },
{ code: '13', name: 'BÄUME', color: '#50a050', lw: 0.13, visible: true, locked: false }, { code: '13', name: 'Bäume', color: '#50a050', lw: 0.13, visible: true, locked: false },
{ code: '14', name: 'HÖHENLINIEN', color: '#909050', lw: 0.18, visible: true, locked: false }, { code: '14', name: 'Höhenlinien', color: '#909050', lw: 0.18, visible: true, locked: false },
{ code: '20', name: 'WÄNDE', color: '#0a0a0a', lw: 0.50, visible: true, locked: false }, { code: '20', name: 'Wände', color: '#0a0a0a', lw: 0.50, visible: true, locked: false },
{ code: '21', name: 'TÜREN_FENSTER', color: '#5080c8', lw: 0.25, visible: true, locked: false }, { code: '21', name: 'Türen_Fenster', color: '#5080c8', lw: 0.25, visible: true, locked: false },
{ code: '22', name: 'MÖBEL', color: '#909090', lw: 0.13, visible: true, locked: false }, { code: '22', name: 'Möbel', color: '#909090', lw: 0.13, visible: true, locked: false },
{ code: '25', name: 'STÜTZEN', color: '#c87050', lw: 0.50, visible: true, locked: false }, { code: '25', name: 'Stützen', color: '#c87050', lw: 0.50, visible: true, locked: false },
{ code: '30', name: 'DECKEN', color: '#605850', lw: 0.35, visible: true, locked: false }, { code: '30', name: 'Decken', color: '#605850', lw: 0.35, visible: true, locked: false },
{ code: '31', name: 'DÄCHER', color: '#7a4a3a', lw: 0.35, visible: true, locked: false }, { code: '31', name: 'Dächer', color: '#7a4a3a', lw: 0.35, visible: true, locked: false },
{ code: '35', name: 'TRÄGER', color: '#a87858', lw: 0.50, visible: true, locked: false }, { code: '35', name: 'Träger', color: '#a87858', lw: 0.50, visible: true, locked: false },
{ code: '50', name: 'TEXT', color: '#d0d0d0', lw: 0.13, visible: true, locked: false }, { code: '50', name: 'Text', color: '#d0d0d0', lw: 0.13, visible: true, locked: false },
{ code: '60', name: 'PLANGRAFIK', color: '#c0a040', lw: 0.13, visible: true, locked: false }, { code: '60', name: 'Plangrafik', color: '#c0a040', lw: 0.13, visible: true, locked: false },
{ code: '90', name: 'REFERENZEN', color: '#585860', lw: 0.13, visible: true, locked: false }, { code: '90', name: 'Referenzen', color: '#585860', lw: 0.13, visible: true, locked: false },
{ code: '99', name: 'KONSTRUKTION', color: '#404048', lw: 0.13, visible: true, locked: false }, { code: '99', name: 'Konstruktion', color: '#404048', lw: 0.13, visible: true, locked: false },
] ]
export default function App() { export default function App() {
@@ -36,28 +32,12 @@ export default function App() {
const [appliedE, setAppliedE] = useState(INITIAL_EBENEN) const [appliedE, setAppliedE] = useState(INITIAL_EBENEN)
const [eMode, setEMode] = useState('all') const [eMode, setEMode] = useState('all')
const [hatchPatterns, setHatchPatterns] = useState(['Solid', 'Hatch1', 'Hatch2', 'Hatch3', 'Plus', 'Squares', 'Grid', 'Grid60']) const [hatchPatterns, setHatchPatterns] = useState(['Solid', 'Hatch1', 'Hatch2', 'Hatch3', 'Plus', 'Squares', 'Grid', 'Grid60'])
// Ebenenkombinationen (geteilter Store mit Ausschnitten)
const [combinations, setCombinations] = useState([]) // Liste { name, layers }
const [activeCombName, setActiveCombName] = useState(null) // null = "Eigene"
// Dialog fuer "alle bearbeiten" (Pencil-Button)
const [combDialog, setCombDialog] = useState(null) // { layers, presets } oder null
const wantCombDialogRef = useRef(false)
useEffect(() => { useEffect(() => {
onMessage('STATE_SYNC', ({ ebenen: e, hatchPatterns: hp }) => { onMessage('STATE_SYNC', ({ ebenen: e, hatchPatterns: hp }) => {
if (e) { setEbenen(e); setAppliedE(e) } if (e) { setEbenen(e); setAppliedE(e) }
if (Array.isArray(hp) && hp.length > 0) setHatchPatterns(hp) if (Array.isArray(hp) && hp.length > 0) setHatchPatterns(hp)
}) })
onMessage('COMBINATION_DATA', ({ layers, presets }) => {
setCombinations(presets || [])
if (wantCombDialogRef.current) {
wantCombDialogRef.current = false
setCombDialog({ layers: layers || [], presets: presets || [] })
} else if (combDialog) {
// Dialog ist offen Layer-Liste live aktualisieren (z.B. nach Preset-Save)
setCombDialog(d => d ? { ...d, layers: layers || d.layers, presets: presets || [] } : d)
}
})
onMessage('FIRST_RUN', ({ defaultEbenen } = {}) => { onMessage('FIRST_RUN', ({ defaultEbenen } = {}) => {
// Wenn der Dossier-Launcher ein eigenes Schema definiert hat, nutzen wir // Wenn der Dossier-Launcher ein eigenes Schema definiert hat, nutzen wir
// das statt der hardcoded INITIAL_EBENEN. // das statt der hardcoded INITIAL_EBENEN.
@@ -72,8 +52,6 @@ export default function App() {
if (activeCode) setActiveEbene(activeCode) if (activeCode) setActiveEbene(activeCode)
}) })
notifyReady() notifyReady()
// Initial Liste der Kombinationen holen
setTimeout(() => getCombination(), 200)
// Native Browser-Context-Menu global unterdruecken // Native Browser-Context-Menu global unterdruecken
const blockContext = (ev) => ev.preventDefault() const blockContext = (ev) => ev.preventDefault()
@@ -122,41 +100,6 @@ export default function App() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [structureKey, appliedStructureKey]) }, [structureKey, appliedStructureKey])
// --- Ebenen-Kombinationen ----------------------------------------------
const handlePickCombination = (name) => {
if (!name) { setActiveCombName(null); return }
const preset = combinations.find(p => p.name === name)
if (!preset) return
applyCombination({
layers: preset.layers || [],
dossierEbenen: preset.dossierEbenen,
dossierZeichnungsebenen: preset.dossierZeichnungsebenen,
})
setActiveCombName(name)
}
const handleSaveCurrentCombination = () => {
const suggested = activeCombName || `Kombi ${combinations.length + 1}`
const name = (window.prompt('Name für Ebenenkombination:', suggested) || '').trim()
if (!name) return
if (combinations.some(p => p.name === name) &&
!window.confirm(`"${name}" überschreiben?`)) return
saveCurrentAsCombination(name)
setActiveCombName(name)
}
const handleDeleteCombination = (name) => {
if (!name) return
if (!window.confirm(`Ebenenkombination "${name}" löschen?`)) return
deleteCombinationPreset(name)
if (activeCombName === name) setActiveCombName(null)
}
const handleOpenCombDialog = () => {
wantCombDialogRef.current = true
getCombination()
}
const handleUserVisibilityChange = () => {
if (activeCombName !== null) setActiveCombName(null)
}
return ( return (
<div style={{ <div style={{
display: 'flex', flexDirection: 'column', display: 'flex', flexDirection: 'column',
@@ -173,44 +116,8 @@ export default function App() {
mode={eMode} mode={eMode}
onModeChange={setEMode} onModeChange={setEMode}
hatchPatterns={hatchPatterns} hatchPatterns={hatchPatterns}
combinations={combinations}
activeCombName={activeCombName}
onPickCombination={handlePickCombination}
onSaveCurrentCombination={handleSaveCurrentCombination}
onDeleteCombination={handleDeleteCombination}
onEditCombinations={handleOpenCombDialog}
onUserVisibilityChange={handleUserVisibilityChange}
/> />
</div> </div>
{combDialog && (
<AusschnittLayerDialog
snapName="Ebenenkombinationen bearbeiten"
layers={combDialog.layers}
presets={combDialog.presets}
onClose={() => setCombDialog(null)}
onSave={(layers) => {
applyCombination(layers)
setActiveCombName(null)
setCombDialog(null)
}}
onSavePreset={(name, layers) => {
saveCombinationPreset(name, layers)
setCombDialog(d => d ? {
...d,
presets: [...d.presets.filter(p => p.name !== name), { name, layers }],
} : d)
}}
onDeletePreset={(name) => {
deleteCombinationPreset(name)
setCombDialog(d => d ? {
...d,
presets: d.presets.filter(p => p.name !== name),
} : d)
if (activeCombName === name) setActiveCombName(null)
}}
/>
)}
</div> </div>
) )
} }
+187
View File
@@ -0,0 +1,187 @@
import { useEffect, useState } from 'react'
import { onMessage, notifyReady } from './lib/rhinoBridge'
function send(type, payload = {}) {
if (!window.RHINO_MODE) { console.log('[AusschnittSettings] →', type, payload); return }
document.title = 'RHINOMSG::' + JSON.stringify({ type, payload })
}
function Field({ label, hint, children }) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 3, padding: '6px 0' }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)',
fontWeight: 500, letterSpacing: 0.2 }}>
{label}
</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>{children}</div>
{hint && (
<span style={{ fontSize: 9, color: 'var(--text-muted)', lineHeight: 1.4 }}>{hint}</span>
)}
</div>
)
}
function SectionLabel({ children }) {
return (
<div style={{
fontSize: 9, color: 'var(--text-muted)', fontWeight: 600,
letterSpacing: 0.5, textTransform: 'uppercase',
padding: '10px 0 4px',
borderTop: '1px solid var(--border-light)',
marginTop: 8,
}}>{children}</div>
)
}
export default function AusschnittSettingsApp() {
const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {}
const [snap, setSnap] = useState(initial.snap || {})
const [displayModes, setDisplayModes] = useState(initial.displayModes || [])
const [overridesPresets, setOverridesPresets] = useState(initial.overridesPresets || [])
const [layerKombis, setLayerKombis] = useState(initial.layerKombis || [])
useEffect(() => {
onMessage('AUSSCHNITT_SETTINGS_STATE', (p) => {
if (p.snap) setSnap(p.snap)
if (Array.isArray(p.displayModes)) setDisplayModes(p.displayModes)
if (Array.isArray(p.overridesPresets)) setOverridesPresets(p.overridesPresets)
if (Array.isArray(p.layerKombis)) setLayerKombis(p.layerKombis)
})
notifyReady()
const blockContext = (ev) => ev.preventDefault()
document.addEventListener('contextmenu', blockContext)
return () => document.removeEventListener('contextmenu', blockContext)
}, [])
const set = (patch) => setSnap(s => ({ ...s, ...patch }))
const saveAndClose = () => {
send('SAVE', {
settings: {
scale: snap.scale || '',
displayMode: snap.displayMode || null,
displayModeName: snap.displayModeName || null,
applyOverrides: !!snap.applyOverrides,
overridesEnabled: !!snap.overridesEnabled,
overridesPreset: snap.overridesPreset || '',
layerCombination: snap.layerCombination || '',
},
})
}
return (
<div style={{
position: 'absolute', inset: 0,
background: 'var(--bg-dialog)',
display: 'flex', flexDirection: 'column',
fontFamily: 'var(--font)', color: 'var(--text-primary)',
overflow: 'hidden',
}}>
{/* Body */}
<div style={{ flex: 1, overflowY: 'auto', padding: '8px 14px' }}>
<Field label="MASSSTAB" hint="z.B. 1:50 — leer für unverändert">
<input
value={snap.scale || ''}
onChange={(ev) => set({ scale: ev.target.value })}
placeholder="1:50"
style={{ flex: 1, fontSize: 11, fontFamily: 'var(--font-mono)', minWidth: 0 }}
/>
</Field>
<Field label="BILDSCHIRMMODUS"
hint="Display-Mode des Viewports beim Wiederherstellen">
<select
value={snap.displayMode || ''}
onChange={(ev) => {
const dm = displayModes.find(d => d.id === ev.target.value)
set({
displayMode: ev.target.value || null,
displayModeName: dm ? dm.name : null,
})
}}
style={{ flex: 1, fontSize: 11, minWidth: 0 }}
>
<option value=""> unverändert </option>
{displayModes.map(dm => (
<option key={dm.id} value={dm.id}>{dm.name}</option>
))}
</select>
</Field>
<SectionLabel>Grafische Overrides</SectionLabel>
<Field label="OVERRIDES BEIM RESTORE">
<input
type="checkbox"
checked={!!snap.applyOverrides}
onChange={(ev) => set({ applyOverrides: ev.target.checked })}
style={{ marginRight: 6 }}
/>
<span style={{ fontSize: 10, color: 'var(--text-muted)', flex: 1 }}>
{snap.applyOverrides
? 'Overrides werden gesetzt'
: 'Aktueller Overrides-Zustand bleibt'}
</span>
</Field>
{snap.applyOverrides && (
<>
<Field label="OVERRIDES STATUS">
<select
value={snap.overridesEnabled ? 'on' : 'off'}
onChange={(ev) => set({ overridesEnabled: ev.target.value === 'on' })}
style={{ flex: 1, fontSize: 11, minWidth: 0 }}
>
<option value="on">AN</option>
<option value="off">AUS</option>
</select>
</Field>
<Field label="OVERRIDES PRESET"
hint="Leer = kein Preset (Doc-Rules bleiben)">
<select
value={snap.overridesPreset || ''}
onChange={(ev) => set({ overridesPreset: ev.target.value })}
style={{ flex: 1, fontSize: 11, minWidth: 0 }}
disabled={!snap.overridesEnabled}
>
<option value=""> kein Preset </option>
{overridesPresets.map(name => (
<option key={name} value={name}>{name}</option>
))}
</select>
</Field>
</>
)}
<SectionLabel>Ebenenkombination</SectionLabel>
<Field label="KOMBI"
hint='"Eigene" = die per Snap gespeicherte Sichtbarkeit. Ein Preset überschreibt diese beim Wiederherstellen.'>
<select
value={snap.layerCombination || ''}
onChange={(ev) => set({ layerCombination: ev.target.value })}
style={{ flex: 1, fontSize: 11, minWidth: 0 }}
>
<option value=""> Eigene Sichtbarkeit </option>
{layerKombis.map(name => (
<option key={name} value={name}>{name}</option>
))}
</select>
</Field>
</div>
{/* Footer */}
<div style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '8px 12px',
borderTop: '1px solid var(--border)',
background: 'var(--bg-section)',
}}>
<div style={{ flex: 1 }} />
<button className="btn-text" onClick={() => send('CANCEL', {})}>Abbrechen</button>
<button className="btn-contained" onClick={saveAndClose}>Übernehmen</button>
</div>
</div>
)
}
+25 -58
View File
@@ -1,7 +1,6 @@
import { useState, useEffect, useMemo } from 'react' import { useState, useEffect, useMemo } from 'react'
import Icon from './components/Icon' import Icon from './components/Icon'
import ContextMenu from './components/ContextMenu' import ContextMenu from './components/ContextMenu'
import AusschnittLayerDialog from './components/AusschnittLayerDialog'
import { import {
onMessage, notifyReady, onMessage, notifyReady,
listAusschnitte, saveAusschnitt, updateAusschnitt, listAusschnitte, saveAusschnitt, updateAusschnitt,
@@ -9,8 +8,7 @@ import {
renameAusschnitt, deleteAusschnitt, renameAusschnitt, deleteAusschnitt,
setAusschnittFolder, setAusschnittScale, setAusschnittFolder, setAusschnittScale,
duplicateAusschnitt, addAusschnittFolder, removeAusschnittFolder, duplicateAusschnitt, addAusschnittFolder, removeAusschnittFolder,
getAusschnittLayers, updateAusschnittLayers, openAusschnittSettings,
saveLayerPreset, deleteLayerPreset,
} from './lib/rhinoBridge' } from './lib/rhinoBridge'
function EditableInline({ value, onCommit, autoEdit, style, fontSize }) { function EditableInline({ value, onCommit, autoEdit, style, fontSize }) {
@@ -247,22 +245,16 @@ function RootDropZone({ children, onDragOver, onDragLeave, onDrop, dragOver, emp
export default function AusschnitteApp() { export default function AusschnitteApp() {
const [snaps, setSnaps] = useState([]) const [snaps, setSnaps] = useState([])
const [extraFolders, setExtraFolders] = useState([]) const [extraFolders, setExtraFolders] = useState([])
const [presets, setPresets] = useState([])
const [newName, setNewName] = useState('') const [newName, setNewName] = useState('')
const [ctxMenu, setCtxMenu] = useState(null) const [ctxMenu, setCtxMenu] = useState(null)
const [collapsed, setCollapsed] = useState({}) const [collapsed, setCollapsed] = useState({})
const [draggingId, setDraggingId] = useState(null) const [draggingId, setDraggingId] = useState(null)
const [dragTarget, setDragTarget] = useState(null) const [dragTarget, setDragTarget] = useState(null)
const [layerDialog, setLayerDialog] = useState(null)
useEffect(() => { useEffect(() => {
onMessage('LIST', ({ snapshots, folders, presets }) => { onMessage('LIST', ({ snapshots, folders }) => {
setSnaps(snapshots || []) setSnaps(snapshots || [])
setExtraFolders(folders || []) setExtraFolders(folders || [])
setPresets(presets || [])
})
onMessage('LAYERS_DATA', ({ id, name, layers, presets }) => {
setLayerDialog({ id, name, layers: layers || [], presets: presets || [] })
}) })
notifyReady() notifyReady()
const blockContext = (ev) => ev.preventDefault() const blockContext = (ev) => ev.preventDefault()
@@ -301,7 +293,7 @@ export default function AusschnitteApp() {
{ label: 'Wiederherstellen', icon: 'restore', onClick: () => restoreAusschnitt(id) }, { label: 'Wiederherstellen', icon: 'restore', onClick: () => restoreAusschnitt(id) },
{ label: 'Auf Detail anwenden', icon: 'crop_landscape', onClick: () => applyAusschnittToDetail(id) }, { label: 'Auf Detail anwenden', icon: 'crop_landscape', onClick: () => applyAusschnittToDetail(id) },
{ divider: true }, { divider: true },
{ label: 'Sichtbarkeit bearbeiten…', icon: 'layers', onClick: () => getAusschnittLayers(id) }, { label: 'Ausschnittseinstellungen…', icon: 'tune', onClick: () => openAusschnittSettings(id) },
{ divider: true }, { divider: true },
{ label: 'Duplizieren', icon: 'content_copy', onClick: () => duplicateAusschnitt(id) }, { label: 'Duplizieren', icon: 'content_copy', onClick: () => duplicateAusschnitt(id) },
{ label: 'Aktualisieren', icon: 'sync', onClick: () => updateAusschnitt(id) }, { label: 'Aktualisieren', icon: 'sync', onClick: () => updateAusschnitt(id) },
@@ -361,17 +353,6 @@ export default function AusschnitteApp() {
/> />
) )
const actions = (
<div style={{ display: 'flex', gap: 4 }}>
<button className="btn-icon-tonal" onClick={handleAddFolder} title="Neuer Ordner">
<Icon name="create_new_folder" size={14} />
</button>
<button className="btn-icon-tonal" onClick={() => listAusschnitte()} title="Aktualisieren">
<Icon name="refresh" size={14} />
</button>
</div>
)
const rootItems = groups[''] || [] const rootItems = groups[''] || []
const isEmpty = snaps.length === 0 && allFolders.length === 0 const isEmpty = snaps.length === 0 && allFolders.length === 0
@@ -382,21 +363,6 @@ export default function AusschnitteApp() {
background: 'var(--bg-base)', background: 'var(--bg-base)',
position: 'relative', position: 'relative',
}}> }}>
{/* Fixed Header — wie Layouts/Overrides Pattern */}
<div style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '8px 10px',
borderBottom: '1px solid var(--border)',
flexShrink: 0,
}}>
<span style={{ flex: 1, fontWeight: 600, letterSpacing: '0.08em',
color: 'var(--text-primary)' }}>
AUSSCHNITTE
</span>
<span className="chip" style={{ fontSize: 8 }}>{snaps.length}</span>
{actions}
</div>
<div style={{ flex: 1, overflowY: 'auto', padding: 8 }}> <div style={{ flex: 1, overflowY: 'auto', padding: 8 }}>
{/* Save-Bar als Card */} {/* Save-Bar als Card */}
<div style={{ <div style={{
@@ -481,6 +447,28 @@ export default function AusschnitteApp() {
</div> </div>
</div> </div>
{/* Sticky Footer: Anzahl + Ordner erstellen + Reload */}
<div style={{
display: 'flex', alignItems: 'center', gap: 8,
padding: '8px 10px',
borderTop: '1px solid var(--border)',
background: 'var(--bg-panel)',
flexShrink: 0,
}}>
<span className="chip" style={{
fontSize: 9, minWidth: 22, justifyContent: 'center',
}}>{snaps.length}</span>
<span style={{ fontSize: 10, color: 'var(--text-muted)', flex: 1 }}>
Ausschnitte
</span>
<button className="btn-icon-tonal" onClick={handleAddFolder} title="Neuer Ordner">
<Icon name="create_new_folder" size={14} />
</button>
<button className="btn-icon-tonal" onClick={() => listAusschnitte()} title="Aktualisieren">
<Icon name="refresh" size={14} />
</button>
</div>
{ctxMenu && ( {ctxMenu && (
<ContextMenu <ContextMenu
x={ctxMenu.x} y={ctxMenu.y} x={ctxMenu.x} y={ctxMenu.y}
@@ -489,27 +477,6 @@ export default function AusschnitteApp() {
/> />
)} )}
{layerDialog && (
<AusschnittLayerDialog
snapName={layerDialog.name}
layers={layerDialog.layers}
presets={layerDialog.presets}
onSave={(layers) => {
updateAusschnittLayers(layerDialog.id,
layers.map(l => ({ id: l.id, visible: l.visible, locked: l.locked })))
setLayerDialog(null)
}}
onClose={() => setLayerDialog(null)}
onSavePreset={(name, layers) => {
saveLayerPreset(name, layers)
setLayerDialog(d => d ? { ...d, presets: [...d.presets.filter(p => p.name !== name), { name, layers }] } : d)
}}
onDeletePreset={(name) => {
deleteLayerPreset(name)
setLayerDialog(d => d ? { ...d, presets: d.presets.filter(p => p.name !== name) } : d)
}}
/>
)}
</div> </div>
) )
} }
+53 -79
View File
@@ -50,24 +50,22 @@ function NumInput({ value, onCommit, disabled, suffix, width }) {
) )
} }
// 9-Punkt-Referenzpunkt-Selektor im Illustrator-Stil: sichtbarer BBox-Rahmen, // 9-Punkt-Referenzpunkt-Selektor: sichtbarer BBox-Rahmen, Kreise auf den
// die Punkte sitzen AUF Ecken / Kantenmitten / Zentrum. // Eckpunkten / Kantenmitten / Zentrum.
function RefPointGrid({ ref, onChange }) { function RefPointGrid({ ref, onChange }) {
const SIZE = 26 // Aussenkanten-Quadrat (px) const SIZE = 28 // Aussenkanten-Quadrat (px)
const DOT = 5 // Punkt-Durchmesser (px) const DOT = 6 // Kreis-Durchmesser (px)
// Position pro Code: 0% (min), 50% (mid), 100% (max)
const pct = (c) => c === 'min' ? '0%' : c === 'max' ? '100%' : '50%' const pct = (c) => c === 'min' ? '0%' : c === 'max' ? '100%' : '50%'
return ( return (
<div style={{ <div style={{
position: 'relative', position: 'relative',
width: SIZE, height: SIZE, width: SIZE, height: SIZE,
border: '1px solid var(--text-muted)', border: '1px solid var(--border)',
background: 'transparent', background: 'transparent',
flexShrink: 0, flexShrink: 0,
}}> }}>
{REF_CODES.map(yc => REF_CODES.map(xc => { {REF_CODES.map(yc => REF_CODES.map(xc => {
const active = ref.x === xc && ref.y === yc const active = ref.x === xc && ref.y === yc
// yc 'max' = top in user mental model (Vectorworks/Illustrator)
const topPct = yc === 'max' ? '0%' : yc === 'min' ? '100%' : '50%' const topPct = yc === 'max' ? '0%' : yc === 'min' ? '100%' : '50%'
return ( return (
<button <button
@@ -79,14 +77,20 @@ function RefPointGrid({ ref, onChange }) {
left: pct(xc), top: topPct, left: pct(xc), top: topPct,
transform: 'translate(-50%, -50%)', transform: 'translate(-50%, -50%)',
width: DOT, height: DOT, padding: 0, width: DOT, height: DOT, padding: 0,
borderRadius: 0, // eckig wie Illustrator borderRadius: '50%',
background: active ? 'var(--accent)' : 'var(--text-muted)', background: active ? 'var(--accent)' : 'var(--text-muted)',
border: 'none', border: 'none',
cursor: 'pointer', cursor: 'pointer',
transition: 'background 0.1s', transition: 'background 0.12s, transform 0.12s',
}}
onMouseEnter={(e) => {
if (!active) e.currentTarget.style.background = 'var(--text-primary)'
e.currentTarget.style.transform = 'translate(-50%, -50%) scale(1.25)'
}}
onMouseLeave={(e) => {
if (!active) e.currentTarget.style.background = 'var(--text-muted)'
e.currentTarget.style.transform = 'translate(-50%, -50%) scale(1)'
}} }}
onMouseEnter={(e) => { if (!active) e.currentTarget.style.background = 'var(--text-primary)' }}
onMouseLeave={(e) => { if (!active) e.currentTarget.style.background = 'var(--text-muted)' }}
/> />
) )
}))} }))}
@@ -189,15 +193,13 @@ export default function DimensionenApp() {
background: 'var(--bg-base)', color: 'var(--text-primary)', background: 'var(--bg-base)', color: 'var(--text-primary)',
fontFamily: 'var(--font)', fontSize: 11, fontFamily: 'var(--font)', fontSize: 11,
}}> }}>
<div style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden', padding: 6 }}> <div style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden' }}>
{/* Header: Selektions-Info + World/CPlane */} {/* Header: Selektions-Info + World/CPlane */}
<div style={{ <div style={{
display: 'flex', alignItems: 'center', gap: 6, marginBottom: 6, display: 'flex', alignItems: 'center', gap: 6,
padding: '5px 8px', padding: '8px 12px',
background: 'var(--bg-section)', borderBottom: '1px solid var(--border-light)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
}}> }}>
<Icon name="select_all" size={14} style={{ color: 'var(--text-muted)' }} /> <Icon name="select_all" size={14} style={{ color: 'var(--text-muted)' }} />
<span style={{ flex: 1, fontWeight: 500 }}>{selLabel()}</span> <span style={{ flex: 1, fontWeight: 500 }}>{selLabel()}</span>
@@ -221,11 +223,8 @@ export default function DimensionenApp() {
<div style={{ <div style={{
padding: '32px 16px', textAlign: 'center', padding: '32px 16px', textAlign: 'center',
color: 'var(--text-muted)', fontSize: 11, color: 'var(--text-muted)', fontSize: 11,
border: '1px dashed var(--border)',
borderRadius: 'var(--r-lg)',
background: 'var(--bg-section)',
}}> }}>
<Icon name="aspect_ratio" size={32} style={{ color: 'var(--text-muted)', opacity: 0.5 }} /> <Icon name="aspect_ratio" size={32} style={{ color: 'var(--text-muted)', opacity: 0.4 }} />
<div style={{ marginTop: 8 }}>Keine Selektion.</div> <div style={{ marginTop: 8 }}>Keine Selektion.</div>
<div style={{ marginTop: 4, fontSize: 10 }}> <div style={{ marginTop: 4, fontSize: 10 }}>
In Rhino ein oder mehrere Objekte auswählen. In Rhino ein oder mehrere Objekte auswählen.
@@ -233,38 +232,30 @@ export default function DimensionenApp() {
</div> </div>
) : ( ) : (
<> <>
{/* Referenzpunkt — kompakte einzeilige Card */} {/* Referenzpunkt */}
<div style={{ <div style={{
display: 'flex', alignItems: 'center', gap: 8, display: 'flex', alignItems: 'center', gap: 10,
padding: '6px 8px', marginBottom: 6, padding: '10px 12px',
background: 'var(--bg-section)', borderBottom: '1px solid var(--border-light)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
}}> }}>
<span style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)', <span className="label-xs" style={{ width: 30 }}>Ref</span>
letterSpacing: '0.06em', textTransform: 'uppercase' }}>
Ref
</span>
<RefPointGrid ref={ref} onChange={onRefChange} /> <RefPointGrid ref={ref} onChange={onRefChange} />
<div style={{ flex: 1 }} />
<RefZSelector z={ref.z} onChange={(z) => onRefChange({ ...ref, z })} /> <RefZSelector z={ref.z} onChange={(z) => onRefChange({ ...ref, z })} />
</div> </div>
{/* Position + Abmessungen nebeneinander */} {/* Position + BBox nebeneinander */}
<div style={{ display: 'flex', gap: 6, marginBottom: 6 }}> <div style={{
<div style={{ display: 'flex', gap: 16,
flex: 1, display: 'flex', flexDirection: 'column', gap: 4, padding: '10px 12px',
padding: '6px 8px', borderBottom: '1px solid var(--border-light)',
background: 'var(--bg-section)', }}>
border: '1px solid var(--border)', <div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 4, minWidth: 0 }}>
borderRadius: 'var(--r-lg)', <div style={{ display: 'flex', justifyContent: 'space-between',
minWidth: 0, alignItems: 'baseline', marginBottom: 2 }}>
}}> <span className="label-xs">Position</span>
<div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)', <span style={{ fontFamily: 'DM Mono, monospace', fontSize: 9,
letterSpacing: '0.06em', textTransform: 'uppercase', color: 'var(--text-muted)' }}>
display: 'flex', justifyContent: 'space-between' }}>
<span>Position</span>
<span style={{ fontWeight: 400, textTransform: 'none',
fontFamily: 'DM Mono, monospace', fontSize: 9 }}>
{state.planeName} {state.planeName}
</span> </span>
</div> </div>
@@ -272,18 +263,9 @@ export default function DimensionenApp() {
<Field label="Y"><NumInput value={pos.y} onCommit={(v) => setDimPosition('y', v)} /></Field> <Field label="Y"><NumInput value={pos.y} onCommit={(v) => setDimPosition('y', v)} /></Field>
<Field label="Z"><NumInput value={pos.z} onCommit={(v) => setDimPosition('z', v)} /></Field> <Field label="Z"><NumInput value={pos.z} onCommit={(v) => setDimPosition('z', v)} /></Field>
</div> </div>
<div style={{ <div style={{ width: 1, background: 'var(--border-light)' }} />
flex: 1, display: 'flex', flexDirection: 'column', gap: 4, <div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 4, minWidth: 0 }}>
padding: '6px 8px', <span className="label-xs" style={{ marginBottom: 2 }}>BBox</span>
background: 'var(--bg-section)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
minWidth: 0,
}}>
<div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)',
letterSpacing: '0.06em', textTransform: 'uppercase' }}>
BBox
</div>
<Field label="B"><NumInput value={dims?.width} onCommit={(v) => setDimDimension('width', v)} /></Field> <Field label="B"><NumInput value={dims?.width} onCommit={(v) => setDimDimension('width', v)} /></Field>
<Field label="T"><NumInput value={dims?.depth} onCommit={(v) => setDimDimension('depth', v)} /></Field> <Field label="T"><NumInput value={dims?.depth} onCommit={(v) => setDimDimension('depth', v)} /></Field>
<Field label="H"><NumInput value={dims?.height} onCommit={(v) => setDimDimension('height', v)} /></Field> <Field label="H"><NumInput value={dims?.height} onCommit={(v) => setDimDimension('height', v)} /></Field>
@@ -294,22 +276,19 @@ export default function DimensionenApp() {
{shape && ( {shape && (
<div style={{ <div style={{
display: 'flex', flexDirection: 'column', gap: 4, display: 'flex', flexDirection: 'column', gap: 4,
padding: '6px 8px', marginBottom: 6, padding: '10px 12px',
background: 'var(--bg-section)', borderBottom: '1px solid var(--border-light)',
border: '1px solid var(--accent)',
borderRadius: 'var(--r-lg)',
}}> }}>
<div style={{ fontSize: 10, fontWeight: 600, color: 'var(--accent)', <span className="label-xs" style={{ color: 'var(--accent)' }}>
letterSpacing: '0.06em', textTransform: 'uppercase' }}>
{shape.type === 'circle' && 'Kreis'} {shape.type === 'circle' && 'Kreis'}
{shape.type === 'rectangle' && 'Rechteck'} {shape.type === 'rectangle' && 'Rechteck'}
{shape.type === 'line' && 'Linie'} {shape.type === 'line' && 'Linie'}
</div> </span>
{shape.type === 'circle' && ( {shape.type === 'circle' && (
<Field label="R"><NumInput value={shape.radius} onCommit={(v) => setCircleRadius(v)} /></Field> <Field label="R"><NumInput value={shape.radius} onCommit={(v) => setCircleRadius(v)} /></Field>
)} )}
{shape.type === 'rectangle' && ( {shape.type === 'rectangle' && (
<div style={{ display: 'flex', gap: 6 }}> <div style={{ display: 'flex', gap: 8 }}>
<Field label="W" style={{ flex: 1 }}> <Field label="W" style={{ flex: 1 }}>
<NumInput value={shape.width} <NumInput value={shape.width}
onCommit={(v) => setRectangleDims(v, shape.height)} /> onCommit={(v) => setRectangleDims(v, shape.height)} />
@@ -321,7 +300,7 @@ export default function DimensionenApp() {
</div> </div>
)} )}
{shape.type === 'line' && ( {shape.type === 'line' && (
<div style={{ display: 'flex', gap: 6 }}> <div style={{ display: 'flex', gap: 8 }}>
<Field label="L" style={{ flex: 1 }}> <Field label="L" style={{ flex: 1 }}>
<NumInput value={shape.length} onCommit={(v) => setLineLength(v)} /> <NumInput value={shape.length} onCommit={(v) => setLineLength(v)} />
</Field> </Field>
@@ -333,19 +312,13 @@ export default function DimensionenApp() {
</div> </div>
)} )}
{/* Rotation — kompakt einzeilig */} {/* Rotation */}
<div style={{ <div style={{
display: 'flex', alignItems: 'center', gap: 6, display: 'flex', alignItems: 'center', gap: 6,
padding: '6px 8px', marginBottom: 6, padding: '10px 12px',
background: 'var(--bg-section)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
}}> }}>
<span style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)', <span className="label-xs" style={{ width: 50 }}>Drehen</span>
letterSpacing: '0.06em', textTransform: 'uppercase' }}> <div style={{ width: 56 }}>
Drehen
</span>
<div style={{ flex: 1 }}>
<NumInput value={rotationDelta} onCommit={setRotationDelta} suffix="°" /> <NumInput value={rotationDelta} onCommit={setRotationDelta} suffix="°" />
</div> </div>
<button <button
@@ -357,12 +330,13 @@ export default function DimensionenApp() {
> >
<Icon name="rotate_right" size={13} /> <Icon name="rotate_right" size={13} />
</button> </button>
<div style={{ flex: 1 }} />
{[-90, -45, 45, 90].map(a => ( {[-90, -45, 45, 90].map(a => (
<button <button
key={a} key={a}
className="btn-outlined" className="btn-outlined"
onClick={() => setDimRotationZ(a)} onClick={() => setDimRotationZ(a)}
style={{ padding: '3px 5px', fontSize: 9, minWidth: 28 }} style={{ padding: '3px 6px', fontSize: 9, minWidth: 28 }}
title={`${a}°`} title={`${a}°`}
> >
{a > 0 ? '+' : ''}{a}° {a > 0 ? '+' : ''}{a}°
+46 -10
View File
@@ -1,37 +1,73 @@
import { useEffect } from 'react' import { useEffect, useState, useRef } from 'react'
import EbenenSettingsDialog from './components/EbenenSettingsDialog' import EbenenSettingsDialog from './components/EbenenSettingsDialog'
import { notifyReady } from './lib/rhinoBridge' import { notifyReady, onMessage } from './lib/rhinoBridge'
function bridgeSend(type, payload = {}) { function bridgeSend(type, payload = {}) {
if (!window.RHINO_MODE) { console.log('[Bridge] →', type, payload); return } if (!window.RHINO_MODE) { console.log('[Bridge] →', type, payload); return }
const json = JSON.stringify({ type, payload }) document.title = 'RHINOMSG::' + JSON.stringify({ type, payload })
document.title = 'RHINOMSG::' + json
} }
export default function EbenenSettingsApp() { export default function EbenenSettingsApp() {
const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {} const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {}
const [ebenen, setEbenen] = useState(initial.ebenen || [])
const [hatchPatterns, setHatchPatterns] = useState(initial.hatchPatterns || ['Solid'])
const [selectedCode, setSelectedCode] = useState(initial.currentCode || initial.ebene?.code || '')
// Aktuell editiertes Draft. originalCode = Code beim Aufmachen des Editors
// (kann sich beim Save aendern wenn User CODE-Feld umbenannt hat).
const [originalCode, setOriginalCode] = useState(initial.currentCode || initial.ebene?.code || '')
const dialogKey = useRef(0) // Force Dialog-Remount beim Wechsel
useEffect(() => { useEffect(() => {
onMessage('EBENEN_SETTINGS_STATE', ({ ebenen: list, hatchPatterns: hp }) => {
if (Array.isArray(list)) setEbenen(list)
if (Array.isArray(hp) && hp.length) setHatchPatterns(hp)
})
notifyReady() notifyReady()
const blockContext = (ev) => ev.preventDefault() const blockContext = (ev) => ev.preventDefault()
document.addEventListener('contextmenu', blockContext) document.addEventListener('contextmenu', blockContext)
return () => document.removeEventListener('contextmenu', blockContext) return () => document.removeEventListener('contextmenu', blockContext)
}, []) }, [])
const ebene = initial.ebene || initial const sortedEbenen = [...ebenen].sort((a, b) => {
const hatchPatterns = initial.hatchPatterns || ['Solid'] const ca = parseInt(a.code, 10), cb = parseInt(b.code, 10)
if (!isNaN(ca) && !isNaN(cb)) return ca - cb
return (a.code || '').localeCompare(b.code || '')
})
if (!ebene || typeof ebene !== 'object' || !ebene.code) { const currentEbene = ebenen.find(e => e.code === selectedCode)
return <div style={{ padding: 20, color: 'var(--text-muted)' }}>Keine Daten</div> || ebenen.find(e => e.code === originalCode)
|| initial.ebene
|| null
if (!currentEbene) {
return <div style={{ padding: 20, color: 'var(--text-muted)', fontSize: 11 }}>Keine Ebene gefunden</div>
}
const switchTo = (newCode, currentDraft) => {
if (!newCode || newCode === selectedCode) return
// Aktuelles Draft live persistieren bevor wir wechseln wenn der User
// gerade etwas geaendert hatte ohne 'Übernehmen' zu druecken, geht es
// sonst verloren.
if (currentDraft && originalCode) {
bridgeSend('SAVE_KEEP', { ebene: currentDraft, originalCode })
}
setSelectedCode(newCode)
setOriginalCode(newCode)
dialogKey.current += 1 // Force-Remount frisches Draft aus ebenen[newCode]
} }
return ( return (
<EbenenSettingsDialog <EbenenSettingsDialog
key={dialogKey.current}
embedded embedded
ebene={ebene} ebene={currentEbene}
hatchPatterns={hatchPatterns} hatchPatterns={hatchPatterns}
onSave={(updated) => bridgeSend('SAVE', updated)} onSave={(updated) => bridgeSend('SAVE', { ebene: updated, originalCode })}
onClose={() => bridgeSend('CANCEL', {})} onClose={() => bridgeSend('CANCEL', {})}
pickerEbenen={sortedEbenen}
pickerSelected={currentEbene.code}
onPickEbene={(newCode, currentDraft) => switchTo(newCode, currentDraft)}
/> />
) )
} }
+1 -1
View File
@@ -513,7 +513,7 @@ export default function ElementeApp() {
padding: '8px 10px', borderBottom: '1px solid var(--border)', padding: '8px 10px', borderBottom: '1px solid var(--border)',
flexShrink: 0, flexShrink: 0,
}}> }}>
<span style={{ flex: 1, fontWeight: 600, letterSpacing: '0.08em' }}>ELEMENTE</span> <span style={{ flex: 1, fontWeight: 600 }}>Elemente</span>
<span className="chip" style={{ fontSize: 8 }}>{elements.length}</span> <span className="chip" style={{ fontSize: 8 }}>{elements.length}</span>
<button <button
onClick={() => exportRaeume()} onClick={() => exportRaeume()}
+44
View File
@@ -0,0 +1,44 @@
import { useState, useEffect } from 'react'
import AusschnittLayerDialog from './components/AusschnittLayerDialog'
import { onMessage, notifyReady } from './lib/rhinoBridge'
function send(type, payload) {
if (!window.RHINO_MODE) { console.log('[LayerCombinations] →', type, payload); return }
document.title = 'RHINOMSG::' + JSON.stringify({ type, payload: payload || {} })
}
export default function LayerCombinationsApp() {
const [layers, setLayers] = useState([])
const [presets, setPresets] = useState([])
useEffect(() => {
onMessage('LAYER_COMBINATIONS_STATE', ({ layers: ls, presets: ps }) => {
if (Array.isArray(ls)) setLayers(ls)
if (Array.isArray(ps)) setPresets(ps)
})
notifyReady()
}, [])
return (
<div style={{
position: 'absolute', inset: 0,
background: 'var(--bg-base)',
display: 'flex',
fontFamily: 'var(--font)',
color: 'var(--text-primary)',
}}>
<AusschnittLayerDialog
embedded
snapName="Ebenenkombinationen"
layers={layers}
presets={presets}
onClose={() => send('CANCEL')}
onSave={(draft) => send('APPLY_COMBINATION', {
layers: draft.map(l => ({ id: l.id, visible: l.visible, locked: l.locked })),
})}
onSavePreset={(name, layerStates) => send('SAVE_PRESET', { name, layers: layerStates })}
onDeletePreset={(name) => send('DELETE_PRESET', { name })}
/>
</div>
)
}
+207
View File
@@ -0,0 +1,207 @@
import { useEffect, useState } from 'react'
import Icon from './components/Icon'
import { onMessage, notifyReady } from './lib/rhinoBridge'
function send(type, payload = {}) {
if (!window.RHINO_MODE) { console.log('[LayoutDialog] →', type, payload); return }
document.title = 'RHINOMSG::' + JSON.stringify({ type, payload })
}
const PAPER_SIZES = ['A4', 'A3', 'A2', 'A1', 'A0', 'Letter']
export default function LayoutDialogApp() {
const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {}
const [mode, setMode] = useState(initial.mode || 'new')
const [layout, setLayout] = useState(initial.layout || null)
const [name, setName] = useState('')
const [format, setFormat] = useState('A3')
const [landscape, setLandscape] = useState(true)
const [cw, setCw] = useState('420')
const [ch, setCh] = useState('297')
useEffect(() => {
onMessage('LAYOUT_DIALOG_STATE', (p) => {
if (p.mode) setMode(p.mode)
if (p.layout) {
setLayout(p.layout)
if (p.mode === 'edit') {
setFormat('custom')
setCw(String(Math.round(p.layout.width || 420)))
setCh(String(Math.round(p.layout.height || 297)))
}
}
})
notifyReady()
const blockContext = (ev) => ev.preventDefault()
document.addEventListener('contextmenu', blockContext)
return () => document.removeEventListener('contextmenu', blockContext)
}, [])
const editing = mode === 'edit'
const submit = () => {
const payload = { name: name.trim(), format, landscape }
if (format === 'custom') {
const w = parseFloat(cw), h = parseFloat(ch)
if (!(w > 0) || !(h > 0)) { alert('Bitte gültige Größe eingeben.'); return }
payload.customWidth = w
payload.customHeight = h
}
send('SAVE', payload)
}
return (
<div style={{
position: 'absolute', inset: 0,
background: 'var(--bg-dialog)',
display: 'flex', flexDirection: 'column',
fontFamily: 'var(--font)', color: 'var(--text-primary)',
overflow: 'hidden',
}}>
{/* Body */}
<div style={{ flex: 1, overflowY: 'auto', padding: '12px 16px',
display: 'flex', flexDirection: 'column', gap: 14 }}>
{!editing && (
<Field label="Name">
<input
type="text" value={name}
onChange={(e) => setName(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') submit() }}
placeholder="z.B. Grundriss EG"
autoFocus
style={{ width: '100%', fontSize: 12, padding: '6px 8px' }}
/>
</Field>
)}
<Field label="Papierformat">
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
{PAPER_SIZES.map(f => (
<button key={f}
onClick={() => setFormat(f)}
className={format === f ? 'btn-contained' : 'btn-outlined'}
style={{ padding: '5px 12px', fontSize: 11 }}>
{f}
</button>
))}
<button
onClick={() => setFormat('custom')}
className={format === 'custom' ? 'btn-contained' : 'btn-outlined'}
style={{ padding: '5px 12px', fontSize: 11 }}>
Eigene
</button>
</div>
</Field>
{format === 'custom' ? (
<Field label="Größe (mm)">
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<input
type="text" value={cw}
onChange={(e) => setCw(e.target.value)}
placeholder="Breite"
style={{ flex: 1, fontFamily: 'DM Mono, monospace',
fontSize: 12, textAlign: 'right', padding: '6px 8px' }}
/>
<span style={{ color: 'var(--text-muted)', fontSize: 11 }}>×</span>
<input
type="text" value={ch}
onChange={(e) => setCh(e.target.value)}
placeholder="Höhe"
style={{ flex: 1, fontFamily: 'DM Mono, monospace',
fontSize: 12, textAlign: 'right', padding: '6px 8px' }}
/>
<span style={{ color: 'var(--text-muted)', fontSize: 10, width: 22 }}>mm</span>
</div>
</Field>
) : (
<Field label="Ausrichtung">
<div style={{ display: 'flex', gap: 6 }}>
<button
onClick={() => setLandscape(true)}
className={landscape ? 'btn-contained' : 'btn-outlined'}
style={{ flex: 1, padding: '8px 12px', fontSize: 11,
display: 'flex', gap: 6, alignItems: 'center',
justifyContent: 'center' }}>
<Icon name="crop_landscape" size={16} /> Quer
</button>
<button
onClick={() => setLandscape(false)}
className={!landscape ? 'btn-contained' : 'btn-outlined'}
style={{ flex: 1, padding: '8px 12px', fontSize: 11,
display: 'flex', gap: 6, alignItems: 'center',
justifyContent: 'center' }}>
<Icon name="crop_portrait" size={16} /> Hoch
</button>
</div>
</Field>
)}
{/* Preview */}
<FormatPreview format={format} landscape={landscape}
customW={parseFloat(cw)} customH={parseFloat(ch)} />
</div>
{/* Footer */}
<div style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '10px 14px',
borderTop: '1px solid var(--border)',
background: 'var(--bg-section)',
}}>
<div style={{ flex: 1 }} />
<button className="btn-text" onClick={() => send('CANCEL', {})}>Abbrechen</button>
<button className="btn-contained" onClick={submit}
disabled={!editing && !name.trim()}
title={!editing && !name.trim() ? 'Erst einen Namen eingeben' : ''}>
{editing ? 'Anwenden' : 'Erstellen'}
</button>
</div>
</div>
)
}
function Field({ label, children }) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<span className="label-xs">{label}</span>
{children}
</div>
)
}
const PAPER_DIMS = {
A0: [841, 1189], A1: [594, 841], A2: [420, 594], A3: [297, 420],
A4: [210, 297], Letter: [216, 279],
}
function FormatPreview({ format, landscape, customW, customH }) {
let w, h
if (format === 'custom') { w = customW; h = customH }
else {
const dims = PAPER_DIMS[format]
if (!dims) return null
w = dims[0]; h = dims[1]
if (landscape) { w = dims[1]; h = dims[0] }
}
if (!(w > 0) || !(h > 0)) return null
const MAX = 120
const scale = Math.min(MAX / w, MAX / h)
const pw = w * scale, ph = h * scale
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6, marginTop: 6 }}>
<div style={{
width: pw, height: ph,
background: 'var(--bg-input)',
border: '1.5px solid var(--accent)',
boxShadow: '0 2px 8px rgba(0,0,0,0.2)',
transition: 'width 0.2s, height 0.2s',
}} />
<span style={{ fontSize: 10, color: 'var(--text-muted)',
fontFamily: 'DM Mono, monospace' }}>
{Math.round(w)} × {Math.round(h)} mm
</span>
</div>
)
}
+74 -198
View File
@@ -3,14 +3,13 @@ import Icon from './components/Icon'
import ContextMenu from './components/ContextMenu' import ContextMenu from './components/ContextMenu'
import { import {
onMessage, notifyReady, onMessage, notifyReady,
listLayouts, newLayout, deleteLayout, renameLayout, activateLayout, listLayouts, deleteLayout, renameLayout, activateLayout,
addDetail, deleteDetail, bindAusschnitt, syncDetail, syncLayout, addDetail, deleteDetail, bindAusschnitt, syncDetail, syncLayout,
setPageSize, exportPdf, exportPdfAll, exportPdfMany, exportPdf, exportPdfAll, exportPdfMany,
addLayoutFolder, removeLayoutFolder, setLayoutFolder, addLayoutFolder, removeLayoutFolder, setLayoutFolder,
openLayoutDialog,
} from './lib/rhinoBridge' } from './lib/rhinoBridge'
const PAPER_SIZES = ['A4', 'A3', 'A2', 'A1', 'A0', 'Letter']
const PAPER_FORMATS_MM = { const PAPER_FORMATS_MM = {
A0: [841, 1189], A1: [594, 841], A2: [420, 594], A3: [297, 420], A0: [841, 1189], A1: [594, 841], A2: [420, 594], A3: [297, 420],
A4: [210, 297], Letter: [216, 279], A4: [210, 297], Letter: [216, 279],
@@ -112,7 +111,6 @@ function EditableName({ value, onCommit, style, title, forceEdit, onEditDone })
export default function LayoutsApp() { export default function LayoutsApp() {
const [state, setState] = useState({ layouts: [], snapshots: [], details: {}, folders: [] }) const [state, setState] = useState({ layouts: [], snapshots: [], details: {}, folders: [] })
const [selectedId, setSelectedId] = useState(null) const [selectedId, setSelectedId] = useState(null)
const [dialog, setDialog] = useState(null) // { mode: 'new' | 'edit', layout? }
const [checked, setChecked] = useState(new Set()) // Multi-Select IDs const [checked, setChecked] = useState(new Set()) // Multi-Select IDs
const [collapsedFolders, setCollapsedFolders] = useState(new Set()) const [collapsedFolders, setCollapsedFolders] = useState(new Set())
const [draggingId, setDraggingId] = useState(null) const [draggingId, setDraggingId] = useState(null)
@@ -187,8 +185,10 @@ export default function LayoutsApp() {
{ divider: true }, { divider: true },
{ label: 'Als PDF exportieren', icon: 'picture_as_pdf', { label: 'Als PDF exportieren', icon: 'picture_as_pdf',
onClick: () => exportPdf(l.id, 300) }, onClick: () => exportPdf(l.id, 300) },
{ label: 'Papierformat aendern', icon: 'aspect_ratio', { label: 'Papierformat ändern', icon: 'aspect_ratio',
onClick: () => setDialog({ mode: 'edit', layout: l }) }, onClick: () => openLayoutDialog('edit', {
id: l.id, name: l.name, width: l.widthMm, height: l.heightMm,
}) },
{ divider: true }, { divider: true },
...(folders.length > 0 ? [ ...(folders.length > 0 ? [
...folders.map(f => ({ ...folders.map(f => ({
@@ -276,57 +276,6 @@ export default function LayoutsApp() {
background: 'var(--bg-base)', color: 'var(--text-primary)', background: 'var(--bg-base)', color: 'var(--text-primary)',
fontFamily: 'var(--font)', fontSize: 11, fontFamily: 'var(--font)', fontSize: 11,
}}> }}>
{/* Header */}
<div style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '8px 10px',
borderBottom: '1px solid var(--border)',
}}>
<span style={{ flex: 1, fontWeight: 600, letterSpacing: '0.08em' }}>LAYOUTS</span>
<button
onClick={handleExportSelection}
className="btn-icon-tonal"
disabled={checked.size === 0}
title={checked.size > 0
? `Auswahl (${checked.size}) als ein PDF exportieren`
: 'Erst Layouts ankreuzen'}
style={{ display: 'flex', alignItems: 'center', gap: 4 }}
>
<Icon name="picture_as_pdf" size={14} />
{checked.size > 0 && <span style={{ fontSize: 10 }}>({checked.size})</span>}
</button>
<button
onClick={() => exportPdfAll(300)}
className="btn-icon-tonal"
disabled={layouts.length === 0}
title="Alle Layouts als ein PDF exportieren"
>
<Icon name="picture_as_pdf" size={14} />
<span style={{ fontSize: 9, marginLeft: 2 }}>·</span>
</button>
<button
onClick={handleNewFolder}
className="btn-icon-tonal"
title="Neuer Ordner"
>
<Icon name="create_new_folder" size={14} />
</button>
<button
onClick={() => setDialog({ mode: 'new' })}
className="btn-add"
title="Neues Layout erstellen"
>
<Icon name="add" size={16} />
</button>
<button
onClick={() => listLayouts()}
className="btn-icon-tonal"
title="Aktualisieren"
>
<Icon name="refresh" size={14} />
</button>
</div>
<div style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden', padding: 8 }}> <div style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden', padding: 8 }}>
{/* Layout-Liste */} {/* Layout-Liste */}
{layouts.length === 0 && folders.length === 0 ? ( {layouts.length === 0 && folders.length === 0 ? (
@@ -340,7 +289,7 @@ export default function LayoutsApp() {
<Icon name="dashboard" size={32} style={{ color: 'var(--text-muted)', opacity: 0.5 }} /> <Icon name="dashboard" size={32} style={{ color: 'var(--text-muted)', opacity: 0.5 }} />
<div style={{ marginTop: 8 }}>Noch keine Layouts.</div> <div style={{ marginTop: 8 }}>Noch keine Layouts.</div>
<div style={{ marginTop: 4, fontSize: 10 }}> <div style={{ marginTop: 4, fontSize: 10 }}>
Oben <Icon name="add" size={11} /> klicken um ein neues Layout anzulegen. Unten <Icon name="add" size={11} /> klicken um ein neues Layout anzulegen.
</div> </div>
</div> </div>
) : ( ) : (
@@ -610,6 +559,72 @@ export default function LayoutsApp() {
)} )}
</div> </div>
{/* Sticky Footer: Anzahl + Aktionen */}
<div style={{
display: 'flex', alignItems: 'center', gap: 8,
padding: '8px 10px',
borderTop: '1px solid var(--border)',
background: 'var(--bg-panel)',
flexShrink: 0,
}}>
<span className="chip" style={{
fontSize: 9, minWidth: 22, justifyContent: 'center',
}}>{layouts.length}</span>
<span style={{ fontSize: 10, color: 'var(--text-muted)', flex: 1 }}>
Layouts
</span>
{/* PDF-Aktionen: feste Breite damit das Auswahl-Counter den Footer
nicht horizontal verschiebt. */}
<button
onClick={handleExportSelection}
className="btn-icon-tonal"
disabled={checked.size === 0}
title={checked.size > 0
? `Auswahl (${checked.size}) als ein PDF exportieren`
: 'Erst Layouts ankreuzen'}
style={{ width: 'auto', minWidth: 26, padding: '0 8px', gap: 3 }}
>
<Icon name="picture_as_pdf" size={14} />
{checked.size > 0 && (
<span style={{ fontSize: 9, fontFamily: 'DM Mono, monospace' }}>
{checked.size}
</span>
)}
</button>
<button
onClick={() => exportPdfAll(300)}
className="btn-icon-tonal"
disabled={layouts.length === 0}
title="Alle Layouts als ein PDF exportieren"
style={{ width: 'auto', minWidth: 26, padding: '0 8px', gap: 3 }}
>
<Icon name="picture_as_pdf" size={14} />
<span style={{ fontSize: 9 }}>·</span>
</button>
<div style={{ width: 1, height: 18, background: 'var(--border)' }} />
<button
onClick={handleNewFolder}
className="btn-icon-tonal"
title="Neuer Ordner"
>
<Icon name="create_new_folder" size={14} />
</button>
<button
onClick={() => listLayouts()}
className="btn-icon-tonal"
title="Aktualisieren"
>
<Icon name="refresh" size={14} />
</button>
<button
onClick={() => openLayoutDialog('new', null)}
className="btn-add"
title="Neues Layout erstellen"
>
<Icon name="add" size={16} />
</button>
</div>
{/* Kontextmenue */} {/* Kontextmenue */}
{ctxMenu && ( {ctxMenu && (
<ContextMenu <ContextMenu
@@ -627,145 +642,6 @@ export default function LayoutsApp() {
/> />
)} )}
{/* Layout-Dialog: New oder Edit (Papierformat aendern) */}
{dialog && (
<LayoutDialog
mode={dialog.mode}
layout={dialog.layout}
onCancel={() => setDialog(null)}
onSubmit={(p) => {
if (dialog.mode === 'new') {
newLayout(p.name, p.format, p.landscape, p.customWidth, p.customHeight)
} else {
setPageSize(dialog.layout.id, p.format, p.landscape, p.customWidth, p.customHeight)
}
setDialog(null)
}}
/>
)}
</div>
)
}
function LayoutDialog({ mode, layout, onCancel, onSubmit }) {
const editing = mode === 'edit'
const [name, setName] = useState('')
const [format, setFormat] = useState('A3')
const [landscape, setLandscape] = useState(true)
const [cw, setCw] = useState('420') // mm
const [ch, setCh] = useState('297') // mm
// Wenn editieren: aktuelle Layout-Groesse pre-fillen (custom-Mode default)
useEffect(() => {
if (editing && layout) {
// BBox in Doc-Einheiten wir kennen die Einheit nicht direkt im
// Frontend. Fuer den Edit-Modus zeigen wir die Groesse als Zahlen an
// und schicken sie als "custom" mit der mm-Annahme. Wenn das Doc nicht
// auf mm steht, ergibt sich eine kleine Konvertier-Unschaerfe das
// Backend rechnet mm-Werte konsistent in Doc-Units um.
setFormat('custom')
setCw(String(Math.round(layout.width)))
setCh(String(Math.round(layout.height)))
}
}, [editing, layout])
return (
<div style={{
position: 'fixed', inset: 0,
background: 'rgba(0,0,0,0.55)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
zIndex: 100,
}} onClick={(e) => { if (e.target === e.currentTarget) onCancel() }}>
<div style={{
width: 340, background: 'var(--bg-base)',
border: '1px solid var(--border)', borderRadius: 'var(--r-lg)',
display: 'flex', flexDirection: 'column', overflow: 'hidden',
}}>
<div style={{ padding: '10px 12px', borderBottom: '1px solid var(--border)',
fontWeight: 600 }}>
{editing ? `Papierformat: ${layout?.name}` : 'Neues Layout'}
</div>
<div style={{ padding: 12, display: 'flex', flexDirection: 'column', gap: 10 }}>
{!editing && (
<div>
<div style={labelXs}>Name</div>
<input type="text" value={name} onChange={(e) => setName(e.target.value)}
placeholder="z.B. Grundriss EG"
autoFocus
style={{ width: '100%', marginTop: 4 }} />
</div>
)}
<div>
<div style={labelXs}>Papierformat</div>
<div style={{ display: 'flex', gap: 4, marginTop: 4, flexWrap: 'wrap' }}>
{PAPER_SIZES.map(f => (
<button key={f}
onClick={() => setFormat(f)}
className={format === f ? 'btn-contained' : 'btn-outlined'}
style={{ padding: '4px 10px', fontSize: 11 }}>
{f}
</button>
))}
<button
onClick={() => setFormat('custom')}
className={format === 'custom' ? 'btn-contained' : 'btn-outlined'}
style={{ padding: '4px 10px', fontSize: 11 }}>
Eigene
</button>
</div>
</div>
{format === 'custom' ? (
<div>
<div style={labelXs}>Eigene Groesse (mm)</div>
<div style={{ display: 'flex', gap: 6, marginTop: 4, alignItems: 'center' }}>
<input type="text" value={cw} onChange={(e) => setCw(e.target.value)}
placeholder="Breite"
style={{ flex: 1, fontFamily: 'DM Mono, monospace', fontSize: 11 }} />
<span style={{ color: 'var(--text-muted)', fontSize: 10 }}>×</span>
<input type="text" value={ch} onChange={(e) => setCh(e.target.value)}
placeholder="Höhe"
style={{ flex: 1, fontFamily: 'DM Mono, monospace', fontSize: 11 }} />
<span style={{ color: 'var(--text-muted)', fontSize: 10 }}>mm</span>
</div>
</div>
) : (
<div>
<div style={labelXs}>Ausrichtung</div>
<div style={{ display: 'flex', gap: 4, marginTop: 4 }}>
<button
onClick={() => setLandscape(true)}
className={landscape ? 'btn-contained' : 'btn-outlined'}
style={{ padding: '4px 10px', fontSize: 11, display: 'flex', gap: 4, alignItems: 'center' }}>
<Icon name="crop_landscape" size={12} /> Quer
</button>
<button
onClick={() => setLandscape(false)}
className={!landscape ? 'btn-contained' : 'btn-outlined'}
style={{ padding: '4px 10px', fontSize: 11, display: 'flex', gap: 4, alignItems: 'center' }}>
<Icon name="crop_portrait" size={12} /> Hoch
</button>
</div>
</div>
)}
</div>
<div style={{ padding: 10, borderTop: '1px solid var(--border)',
display: 'flex', justifyContent: 'flex-end', gap: 6 }}>
<button onClick={onCancel}>Abbrechen</button>
<button className="btn-contained"
onClick={() => {
const payload = { name: name.trim(), format, landscape }
if (format === 'custom') {
const w = parseFloat(cw), h = parseFloat(ch)
if (!(w > 0) || !(h > 0)) { alert('Bitte gueltige Groesse eingeben.'); return }
payload.customWidth = w
payload.customHeight = h
}
onSubmit(payload)
}}>
{editing ? 'Anwenden' : 'Erstellen'}
</button>
</div>
</div>
</div> </div>
) )
} }
+57
View File
@@ -7,6 +7,8 @@ import {
setMassstabDpi, detectMassstabDpi, setMassstabDpi, detectMassstabDpi,
setView, setDisplayMode, setView, setDisplayMode,
toggleOverrides, setOverridesPreset, openOverridesPanel, toggleOverrides, setOverridesPreset, openOverridesPanel,
pickLayerCombination, saveLayerCombination,
deleteLayerCombination, openLayerCombinationsDialog,
openDossierSettings, openDossierSettings,
} from './lib/rhinoBridge' } from './lib/rhinoBridge'
@@ -125,6 +127,7 @@ export default function OberleisteApp() {
overridesEnabled: false, overridesCount: 0, overridesEnabled: false, overridesCount: 0,
cmdPrompt: '', cmdOptions: [], cmdPrompt: '', cmdOptions: [],
overridesActivePreset: null, overridesPresets: [], overridesActivePreset: null, overridesPresets: [],
layerCombinations: [], layerCombinationActive: null,
}) })
const [appliedScale, setAppliedScale] = useState(null) const [appliedScale, setAppliedScale] = useState(null)
const appliedScaleRef = useRef(null) const appliedScaleRef = useRef(null)
@@ -380,6 +383,60 @@ export default function OberleisteApp() {
title="Overrides-Regel-Editor öffnen" title="Overrides-Regel-Editor öffnen"
/> />
<div style={sep} />
{/* ====== GRUPPE: EBENENKOMBINATION ====== */}
<span style={groupLabel}>Kombi</span>
<select
value={state.layerCombinationActive || '__none__'}
onChange={(e) => {
const v = e.target.value
if (v === '__configure__') { openLayerCombinationsDialog(); return }
if (v === '__delete__') {
if (state.layerCombinationActive &&
window.confirm(`Kombination "${state.layerCombinationActive}" löschen?`))
deleteLayerCombination(state.layerCombinationActive)
return
}
pickLayerCombination(v === '__none__' ? null : v)
}}
style={{ ...pillSelect, width: 140 }}
title={state.layerCombinationActive
? `Aktive Kombi: ${state.layerCombinationActive}`
: 'Keine Kombination — manuelle Sichtbarkeit'}
>
<option value="__none__"> Eigene </option>
{(state.layerCombinations || []).map(name => (
<option key={name} value={name}>{name}</option>
))}
{state.layerCombinationActive && (
<>
<option disabled></option>
<option value="__delete__">🗑 Aktuelle löschen</option>
</>
)}
<option disabled></option>
<option value="__configure__">Bearbeiten</option>
</select>
<ToolButton
onClick={() => {
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)
}}
icon="add"
title="Aktuelle Sichtbarkeit als neue Kombination speichern"
/>
<ToolButton
onClick={openLayerCombinationsDialog}
icon="edit"
title="Ebenenkombinationen bearbeiten"
/>
{/* Spacer am rechten Rand */} {/* Spacer am rechten Rand */}
<div style={{ flex: 1 }} /> <div style={{ flex: 1 }} />
</div> </div>
+2 -2
View File
@@ -56,10 +56,10 @@ export default function ZeichnungsebenenApp() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
// Sichtbarkeit live anwenden bei Mode-/Visibility-Aenderungen // Sichtbarkeit live anwenden bei Mode-/Visibility-/Lock-Aenderungen
const visibilityKey = useMemo(() => ( const visibilityKey = useMemo(() => (
activeId + '|' + zMode + '|' + activeId + '|' + zMode + '|' +
zeichnungsebenen.map(z => `${z.id}:${z.visible !== false ? 1 : 0}`).join(',') zeichnungsebenen.map(z => `${z.id}:${z.visible !== false ? 1 : 0}:${z.locked ? 1 : 0}`).join(',')
), [activeId, zMode, zeichnungsebenen]) ), [activeId, zMode, zeichnungsebenen])
useEffect(() => { useEffect(() => {
+38 -30
View File
@@ -22,6 +22,7 @@ export default function AusschnittLayerDialog({
snapName, layers, presets, snapName, layers, presets,
onSave, onClose, onSave, onClose,
onSavePreset, onDeletePreset, onSavePreset, onDeletePreset,
embedded = false,
}) { }) {
// Welche Kombination wird gerade angezeigt? null = aktueller Doc-State // Welche Kombination wird gerade angezeigt? null = aktueller Doc-State
const [selectedPreset, setSelectedPreset] = useState(null) const [selectedPreset, setSelectedPreset] = useState(null)
@@ -104,41 +105,47 @@ export default function AusschnittLayerDialog({
onSave(draft) onSave(draft)
} }
return ( const wrapperStyle = embedded
<div style={{ ? { position: 'absolute', inset: 0, display: 'flex' }
position: 'absolute', inset: 0, zIndex: 150, : { position: 'absolute', inset: 0, zIndex: 150,
background: 'var(--bg-overlay)', background: 'var(--bg-overlay)',
display: 'flex', alignItems: 'flex-start', justifyContent: 'center', display: 'flex', alignItems: 'flex-start', justifyContent: 'center',
paddingTop: 30, paddingTop: 30 }
}}> const cardStyle = embedded
<div style={{ ? { flex: 1, display: 'flex', flexDirection: 'column',
background: 'var(--bg-dialog)', background: 'var(--bg-dialog)', overflow: 'hidden' }
: { background: 'var(--bg-dialog)',
border: '1px solid var(--border)', border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)', borderRadius: 'var(--r-lg)',
boxShadow: 'var(--shadow-3)', boxShadow: 'var(--shadow-3)',
width: 'calc(100% - 24px)', maxWidth: 480, width: 'calc(100% - 24px)', maxWidth: 480,
maxHeight: 'calc(100vh - 60px)', maxHeight: 'calc(100vh - 60px)',
display: 'flex', flexDirection: 'column', display: 'flex', flexDirection: 'column',
overflow: 'hidden', overflow: 'hidden' }
}}> return (
{/* Header */} <div style={wrapperStyle}>
<div style={{ <div style={cardStyle}>
display: 'flex', alignItems: 'center', gap: 8, {/* Header im embedded-Modus weggelassen (Satellite-Fenster hat schon
padding: '12px 16px', seine native Title-Bar mit Close-Button) */}
borderBottom: '1px solid var(--border)', {!embedded && (
}}> <div style={{
<Icon name="layers" size={16} style={{ color: 'var(--text-secondary)' }} /> display: 'flex', alignItems: 'center', gap: 8,
<span style={{ flex: 1, fontWeight: 600, fontSize: 12 }}> padding: '12px 16px',
{snapName} borderBottom: '1px solid var(--border)',
</span> }}>
{dirty && ( <Icon name="layers" size={16} style={{ color: 'var(--text-secondary)' }} />
<span style={{ fontSize: 10, color: 'var(--warn)', <span style={{ flex: 1, fontWeight: 600, fontSize: 12 }}>
padding: '2px 6px', borderRadius: 'var(--r)', {snapName}
background: 'var(--warn-dim)' }} </span>
title="Ungespeicherte Änderungen"></span> {dirty && (
)} <span style={{ fontSize: 10, color: 'var(--warn)',
<button onClick={onClose} style={{ color: 'var(--text-muted)', fontSize: 16, padding: '0 4px' }}>×</button> padding: '2px 6px', borderRadius: 'var(--r)',
</div> background: 'var(--warn-dim)' }}
title="Ungespeicherte Änderungen"></span>
)}
<button onClick={onClose} style={{ color: 'var(--text-muted)', fontSize: 16, padding: '0 4px' }}>×</button>
</div>
)}
{/* Preset-Auswahl */} {/* Preset-Auswahl */}
<div style={{ <div style={{
@@ -235,7 +242,8 @@ export default function AusschnittLayerDialog({
</div> </div>
{/* Layer-Liste */} {/* Layer-Liste */}
<div style={{ flex: 1, overflowY: 'auto', minHeight: 200, maxHeight: '50vh' }}> <div style={{ flex: 1, overflowY: 'auto', minHeight: 200,
maxHeight: embedded ? 'none' : '50vh' }}>
{filtered.length === 0 ? ( {filtered.length === 0 ? (
<div style={{ padding: '30px 14px', textAlign: 'center', color: 'var(--text-muted)', fontSize: 11 }}> <div style={{ padding: '30px 14px', textAlign: 'center', color: 'var(--text-muted)', fontSize: 11 }}>
Keine Ebenen gefunden. Keine Ebenen gefunden.
-59
View File
@@ -255,9 +255,6 @@ function SortHeader({ label, sortKey, sortBy, sortDir, onSort, style }) {
export default function EbenenManager({ export default function EbenenManager({
ebenen, activeCode, onActiveChange, onChange, mode, onModeChange, hatchPatterns, ebenen, activeCode, onActiveChange, onChange, mode, onModeChange, hatchPatterns,
combinations = [], activeCombName = null,
onPickCombination, onSaveCurrentCombination, onDeleteCombination,
onEditCombinations, onUserVisibilityChange,
}) { }) {
const [sortBy, setSortBy] = useState('code') const [sortBy, setSortBy] = useState('code')
const [sortDir, setSortDir] = useState('asc') const [sortDir, setSortDir] = useState('asc')
@@ -306,12 +303,10 @@ export default function EbenenManager({
const handleToggleVisible = (code) => { const handleToggleVisible = (code) => {
const cur = ebenen.find(e => e.code === code) const cur = ebenen.find(e => e.code === code)
if (cur) updateByCode(code, { visible: !(cur.visible !== false) }) if (cur) updateByCode(code, { visible: !(cur.visible !== false) })
if (onUserVisibilityChange) onUserVisibilityChange()
} }
const handleToggleLock = (code) => { const handleToggleLock = (code) => {
const cur = ebenen.find(e => e.code === code) const cur = ebenen.find(e => e.code === code)
if (cur) updateByCode(code, { locked: !cur.locked }) if (cur) updateByCode(code, { locked: !cur.locked })
if (onUserVisibilityChange) onUserVisibilityChange()
} }
const handleColorChange = (code, color) => { const handleColorChange = (code, color) => {
updateByCode(code, { color }) updateByCode(code, { color })
@@ -426,58 +421,6 @@ export default function EbenenManager({
return ( return (
<> <>
{/* Ebenenkombinationen — Label + Dropdown + Save-As-Plus */}
<div style={{
display: 'flex', flexDirection: 'column', gap: 4,
padding: '6px 14px',
background: 'var(--bg-section)',
borderBottom: '1px solid var(--border-light)',
}}>
<span className="label-xs">Ebenenkombination</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<select
value={activeCombName || '__custom__'}
onChange={(ev) => {
const v = ev.target.value
if (v === '__custom__') return
if (v === '__delete__') {
if (activeCombName && onDeleteCombination) onDeleteCombination(activeCombName)
return
}
if (onPickCombination) onPickCombination(v)
}}
style={{ flex: 1, minWidth: 0 }}
title={activeCombName ? `Aktiv: ${activeCombName}` : 'Eigene Sichtbarkeit (keine Kombination)'}
>
<option value="__custom__">{activeCombName ? activeCombName : 'Eigene'}</option>
{combinations.length > 0 && <option disabled></option>}
{combinations.map(p => (
<option key={p.name} value={p.name}>{p.name}</option>
))}
{activeCombName && combinations.some(p => p.name === activeCombName) && (
<>
<option disabled></option>
<option value="__delete__">🗑 Aktuelle löschen</option>
</>
)}
</select>
<button
className="btn-icon-sm"
onClick={() => onSaveCurrentCombination && onSaveCurrentCombination()}
title="Aktuelle Sichtbarkeit als neue Kombination speichern"
>
<Icon name="add" size={14} />
</button>
<button
className="btn-icon-sm"
onClick={() => onEditCombinations && onEditCombinations()}
title="Alle Kombinationen bearbeiten (Dialog)"
>
<Icon name="edit" size={13} />
</button>
</div>
</div>
<div style={{ <div style={{
display: 'flex', flexDirection: 'column', gap: 4, display: 'flex', flexDirection: 'column', gap: 4,
padding: '6px 14px', padding: '6px 14px',
@@ -512,7 +455,6 @@ export default function EbenenManager({
const anyVisible = ebenen.some(e => e.visible !== false) const anyVisible = ebenen.some(e => e.visible !== false)
// Wenn irgendeine sichtbar -> alle aus. Wenn keine sichtbar -> alle an. // Wenn irgendeine sichtbar -> alle aus. Wenn keine sichtbar -> alle an.
onChange(ebenen.map(e => ({ ...e, visible: !anyVisible }))) onChange(ebenen.map(e => ({ ...e, visible: !anyVisible })))
if (onUserVisibilityChange) onUserVisibilityChange()
}} }}
title={ebenen.every(e => e.visible !== false) title={ebenen.every(e => e.visible !== false)
? 'Alle Ebenen ausblenden' ? 'Alle Ebenen ausblenden'
@@ -534,7 +476,6 @@ export default function EbenenManager({
onClick={() => { onClick={() => {
const anyLocked = ebenen.some(e => e.locked === true) const anyLocked = ebenen.some(e => e.locked === true)
onChange(ebenen.map(e => ({ ...e, locked: !anyLocked }))) onChange(ebenen.map(e => ({ ...e, locked: !anyLocked })))
if (onUserVisibilityChange) onUserVisibilityChange()
}} }}
title={ebenen.every(e => e.locked === true) title={ebenen.every(e => e.locked === true)
? 'Alle Ebenen entsperren' ? 'Alle Ebenen entsperren'
+46 -15
View File
@@ -27,7 +27,10 @@ function SectionLabel({ children }) {
) )
} }
export default function EbenenSettingsDialog({ ebene, hatchPatterns = ['Solid'], onSave, onClose, embedded = false }) { export default function EbenenSettingsDialog({
ebene, hatchPatterns = ['Solid'], onSave, onClose, embedded = false,
pickerEbenen = null, pickerSelected = null, onPickEbene = null,
}) {
const [draft, setDraft] = useState({ const [draft, setDraft] = useState({
...ebene, ...ebene,
fill: { fill: {
@@ -107,21 +110,49 @@ export default function EbenenSettingsDialog({ ebene, hatchPatterns = ['Solid'],
return ( return (
<div style={wrapperStyle}> <div style={wrapperStyle}>
<div style={innerStyle}> <div style={innerStyle}>
{/* Header */} {/* Header embedded zeigt nur das Ebenen-Picker-Dropdown (kein
<div style={{ Title + kein Close, dafuer hat das Fenster seine native Title-
display: 'flex', alignItems: 'center', gap: 6, Bar). Modal-Variante zeigt den klassischen Header. */}
padding: '10px 12px', {embedded && pickerEbenen ? (
borderBottom: '1px solid var(--border)', <div style={{
}}> display: 'flex', alignItems: 'center', gap: 6,
<Icon name="settings" size={14} style={{ color: 'var(--text-secondary)', flexShrink: 0 }} /> padding: '8px 12px',
<span style={{ borderBottom: '1px solid var(--border)',
flex: 1, fontWeight: 600, fontSize: 11,
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}> }}>
{ebene.code} {ebene.name} <span style={{ fontSize: 9, color: 'var(--text-muted)',
</span> textTransform: 'uppercase', letterSpacing: 0.5 }}>
<button onClick={onClose} style={{ color: 'var(--text-muted)', fontSize: 16, padding: '0 4px', lineHeight: 1 }}>×</button> Ebene
</div> </span>
<select
value={pickerSelected || draft.code}
onChange={(ev) => onPickEbene && onPickEbene(ev.target.value, draft)}
style={{ flex: 1, fontSize: 11, minWidth: 0,
fontFamily: 'var(--font-mono)' }}
title="Zwischen Ebenen wechseln — aktuelle Änderungen werden mit übernommen"
>
{pickerEbenen.map(e => (
<option key={e.code} value={e.code}>
{e.code} {e.name}
</option>
))}
</select>
</div>
) : !embedded && (
<div style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '10px 12px',
borderBottom: '1px solid var(--border)',
}}>
<Icon name="settings" size={14} style={{ color: 'var(--text-secondary)', flexShrink: 0 }} />
<span style={{
flex: 1, fontWeight: 600, fontSize: 11,
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>
{ebene.code} {ebene.name}
</span>
<button onClick={onClose} style={{ color: 'var(--text-muted)', fontSize: 16, padding: '0 4px', lineHeight: 1 }}>×</button>
</div>
)}
{/* Body */} {/* Body */}
<div style={{ padding: '6px 12px 4px', overflowY: 'auto' }}> <div style={{ padding: '6px 12px 4px', overflowY: 'auto' }}>
+183 -33
View File
@@ -1,37 +1,75 @@
import { useState } from 'react'
import Icon from './Icon' import Icon from './Icon'
import ContextMenu from './ContextMenu'
import { openGeschossSettings, openGeschossDialog } from '../lib/rhinoBridge' import { openGeschossSettings, openGeschossDialog } from '../lib/rhinoBridge'
function GeschossBadge({ name }) { function GeschossBadge({ name }) {
return <span className="chip chip-info">{name}</span> return <span className="chip chip-info">{name}</span>
} }
function ZeichnungsebeneRow({ z, active, mode, onClick, onToggleVisible, onSettings }) { function ZeichnungsebeneRow({
// Eye-State auch fuer die aktive Zeichnungsebene anzeigen (User-Intention) z, active, mode, onClick, onContextMenu,
const eyeShown = mode !== 'active' onToggleVisible, onToggleLock, onToggleClipping, onDelete,
}) {
const isGeschoss = !!z.isGeschoss const isGeschoss = !!z.isGeschoss
// Eye-Logik: die aktive Z ist IMMER sichtbar (Backend forciert das), also
// zeigen wir ihr Auge immer als "an" ohne Ruecksicht aufs visible-Flag.
// Nicht-aktive: in 'all_force' ist visible-Flag ueberschrieben (alle an),
// in 'active' ueberschrieben (alle aus) Auge dimmt. Sonst (Ausgewaehlte/
// grey) reflektiert es das Flag direkt.
let eyeIcon, eyeOn, eyeOpacity, eyeTitle
if (active) {
eyeIcon = 'visibility'
eyeOn = true
eyeOpacity = 1
eyeTitle = z.visible !== false
? 'Sichtbar (aktive Zeichnungsebene)'
: 'Normalerweise ausgeblendet — wird gezeigt weil aktiv'
} else if (mode === 'all_force') {
eyeIcon = 'visibility'
eyeOn = true
eyeOpacity = 0.35
eyeTitle = 'Im „Alle anzeigen"-Mode immer sichtbar — Klick wechselt in „Ausgewählte"'
} else if (mode === 'active') {
eyeIcon = z.visible !== false ? 'visibility' : 'visibility_off'
eyeOn = false
eyeOpacity = 0.35
eyeTitle = 'Im „Nur aktive"-Mode ausgeblendet — Klick wechselt in „Ausgewählte"'
} else {
eyeIcon = z.visible !== false ? 'visibility' : 'visibility_off'
eyeOn = z.visible !== false
eyeOpacity = 1
eyeTitle = z.visible !== false ? 'Ausblenden' : 'Einblenden'
}
return ( return (
<div <div
onClick={onClick} onClick={onClick}
onContextMenu={onContextMenu}
style={{ style={{
display: 'flex', alignItems: 'center', gap: 8, display: 'flex', alignItems: 'center', gap: 6,
padding: '4px 12px', padding: '4px 12px',
margin: active ? '1px 6px' : '0', margin: active ? '1px 6px' : '0',
background: active ? 'var(--active-dim)' : 'var(--bg-item)', background: active ? 'var(--active-dim)' : 'var(--bg-item)',
// Pill-Form fuer die aktive Zeichnungsebene
borderRadius: active ? 999 : 0, borderRadius: active ? 999 : 0,
borderLeft: active ? 'none' : '3px solid transparent', borderLeft: active ? 'none' : '3px solid transparent',
borderBottom: active ? 'none' : '1px solid var(--border-light)', borderBottom: active ? 'none' : '1px solid var(--border-light)',
boxShadow: active ? 'inset 0 0 0 1px var(--active-light)' : 'none', boxShadow: active ? 'inset 0 0 0 1px var(--active-light)' : 'none',
cursor: 'pointer', cursor: 'pointer',
userSelect: 'none', userSelect: 'none',
opacity: (!active && z.visible === false && mode !== 'all') ? 0.45 : 1,
}} }}
> >
<button
className={`btn-icon-sm ${eyeOn ? 'is-on' : ''}`}
onClick={(ev) => { ev.stopPropagation(); onToggleVisible() }}
title={eyeTitle}
style={{ opacity: eyeOpacity }}
><Icon name={eyeIcon} size={14} /></button>
<span style={{ <span style={{
fontWeight: active ? 700 : 500, fontWeight: active ? 700 : 500,
fontSize: 12, fontSize: 12,
color: active ? 'var(--active-light)' : 'var(--text-label)', color: active ? 'var(--active-light)' : 'var(--text-label)',
flex: 1, flex: 1, minWidth: 0,
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>{z.name}</span> }}>{z.name}</span>
@@ -43,37 +81,38 @@ function ZeichnungsebeneRow({ z, active, mode, onClick, onToggleVisible, onSetti
{isGeschoss && <GeschossBadge name={z.name} />} {isGeschoss && <GeschossBadge name={z.name} />}
{isGeschoss && z.hasClipping && ( {isGeschoss ? (
<Icon name="content_cut" size={12} style={{ color: 'var(--accent)', flexShrink: 0 }} title="Clipping Plane aktiv" />
)}
{eyeShown ? (
<button <button
className={`btn-icon-sm ${z.visible !== false ? 'is-on' : ''}`} className={`btn-icon-xs ${z.hasClipping ? 'is-on' : ''}`}
onClick={(ev) => { ev.stopPropagation(); onToggleVisible() }} onClick={(ev) => { ev.stopPropagation(); onToggleClipping() }}
title={ title={z.hasClipping
active ? 'Clipping Plane ausschalten'
? (z.visible !== false : 'Clipping Plane einschalten (Schnitt auf Schnitthöhe)'}
? 'Normalerweise sichtbar (aktive Zeichnungsebene wird trotzdem gezeigt)' style={{ color: z.hasClipping ? 'var(--accent)' : undefined }}
: 'Normalerweise ausgeblendet — wird nur sichtbar weil aktiv') ><Icon name="content_cut" size={12} /></button>
: (z.visible !== false ? 'Ausblenden' : 'Einblenden')
}
><Icon name={z.visible !== false ? 'visibility' : 'visibility_off'} size={14} /></button>
) : ( ) : (
<span style={{ width: 18, flexShrink: 0 }} /> <span style={{ width: 18, flexShrink: 0 }} />
)} )}
<button <button
className="btn-icon-xs" className="btn-icon-xs"
onClick={(ev) => { ev.stopPropagation(); onSettings() }} onClick={(ev) => { ev.stopPropagation(); onToggleLock() }}
title="Einstellungen" title={z.locked ? 'Entsperren' : 'Sperren'}
><Icon name="settings" size={12} /></button> style={{ color: z.locked ? 'var(--warn)' : undefined }}
><Icon name={z.locked ? 'lock' : 'lock_open'} size={12} /></button>
<button
className="btn-icon-xs"
onClick={(ev) => { ev.stopPropagation(); onDelete() }}
title="Löschen"
><Icon name="close" size={12} /></button>
</div> </div>
) )
} }
const MODES = [ const MODES = [
{ value: 'all', label: 'Alle anzeigen' }, { value: 'all_force', label: 'Alle anzeigen' },
{ value: 'all', label: 'Ausgewählte' },
{ value: 'active', label: 'Nur aktive' }, { value: 'active', label: 'Nur aktive' },
{ value: 'grey', label: 'Andere grau' }, { value: 'grey', label: 'Andere grau' },
{ value: 'grey_locked', label: 'Andere grau & gesperrt' }, { value: 'grey_locked', label: 'Andere grau & gesperrt' },
@@ -83,8 +122,7 @@ export default function GeschossManager({
zeichnungsebenen, activeId, onActiveChange, onChange, recalcOkff, zeichnungsebenen, activeId, onActiveChange, onChange, recalcOkff,
mode, onModeChange, mode, onModeChange,
}) { }) {
// dialogOpen-State entfaellt Bearbeiten-Dialog laeuft jetzt als const [ctxMenu, setCtxMenu] = useState(null) // { x, y, id }
// Satelliten-Fenster via openGeschossDialog().
const sorted = [...zeichnungsebenen].reverse() const sorted = [...zeichnungsebenen].reverse()
const gesamthoehe = zeichnungsebenen const gesamthoehe = zeichnungsebenen
@@ -93,9 +131,8 @@ export default function GeschossManager({
const addQuick = () => { const addQuick = () => {
// Standard: NICHT-Geschoss-Zeichnungsebene (z.B. Möblierung, Bemassung, // Standard: NICHT-Geschoss-Zeichnungsebene (z.B. Möblierung, Bemassung,
// Plangrafik etc.). User kann via Row-Settings-Cog auf Geschoss // Plangrafik etc.). User kann via Row-Kontextmenue auf Geschoss
// umschalten, oder via Bearbeiten-Dialog (Pencil) ein Geschoss // umschalten oder via Bearbeiten-Dialog (Pencil) ein Geschoss erstellen.
// direkt erstellen.
const nonGeschossCount = zeichnungsebenen.filter(z => !z.isGeschoss).length const nonGeschossCount = zeichnungsebenen.filter(z => !z.isGeschoss).length
const newZ = { const newZ = {
id: `z_${Date.now()}`, id: `z_${Date.now()}`,
@@ -103,12 +140,69 @@ export default function GeschossManager({
isGeschoss: false, isGeschoss: false,
visible: true, visible: true,
} }
console.log('[ZEICHNUNGSEBENEN-UI] addQuick →', { newZ, countBefore: zeichnungsebenen.length })
onChange([...zeichnungsebenen, newZ]) onChange([...zeichnungsebenen, newZ])
} }
const toggleVisible = (id) => { const toggleVisible = (id) => {
onChange(zeichnungsebenen.map(z => z.id === id ? { ...z, visible: !(z.visible !== false) } : z)) onChange(zeichnungsebenen.map(z => z.id === id ? { ...z, visible: !(z.visible !== false) } : z))
// In "active" / "all_force" greift visible-Flag nicht wer aufs Auge
// klickt will offensichtlich Sichtbarkeit kontrollieren, also direkt
// in den "Ausgewählte"-Mode wechseln damit die Aktion wirkt.
if (mode === 'active' || mode === 'all_force') onModeChange('all')
}
const toggleLock = (id) => {
onChange(zeichnungsebenen.map(z => z.id === id ? { ...z, locked: !z.locked } : z))
}
const toggleClipping = (id) => {
onChange(zeichnungsebenen.map(z => z.id === id ? { ...z, hasClipping: !z.hasClipping } : z))
}
const duplicate = (id) => {
const src = zeichnungsebenen.find(z => z.id === id)
if (!src) return
const clone = {
...src,
id: `z_${Date.now()}`,
name: `${src.name} Kopie`,
}
// Direkt nach dem Original einfuegen
const idx = zeichnungsebenen.findIndex(z => z.id === id)
const next = [...zeichnungsebenen]
next.splice(idx + 1, 0, clone)
onChange(next)
}
const remove = (id) => {
if (zeichnungsebenen.length <= 1) return
const target = zeichnungsebenen.find(z => z.id === id)
if (!target) return
if (!window.confirm(`"${target.name}" wirklich löschen?`)) return
onChange(zeichnungsebenen.filter(z => z.id !== id))
if (activeId === id) {
const next = zeichnungsebenen.find(z => z.id !== id)
if (next) onActiveChange(next.id)
}
}
const openContextMenu = (ev, id) => {
ev.preventDefault(); ev.stopPropagation()
setCtxMenu({ x: ev.clientX, y: ev.clientY, id })
}
const ctxItems = (id) => {
const z = zeichnungsebenen.find(x => x.id === id)
if (!z) return []
return [
{ label: 'Einstellungen…', icon: 'settings', onClick: () => openGeschossSettings(z) },
{ divider: true },
{ label: 'Duplizieren', icon: 'content_copy', onClick: () => duplicate(id) },
{ divider: true },
{ label: 'Löschen', icon: 'delete', danger: true,
disabled: zeichnungsebenen.length <= 1,
onClick: () => remove(id) },
]
} }
return ( return (
@@ -151,6 +245,52 @@ export default function GeschossManager({
</span> </span>
</div> </div>
{/* Master-Row: Master-Eye links + Master-Lock rechts (analog
EbenenManager). */}
<div style={{
display: 'flex', alignItems: 'center', gap: 5,
padding: '2px 14px',
background: 'var(--bg-section)',
borderBottom: '1px solid var(--border)',
}}>
<button
className="btn-icon-xs"
onClick={() => {
const anyVisible = zeichnungsebenen.some(z => z.visible !== false)
onChange(zeichnungsebenen.map(z => ({ ...z, visible: !anyVisible })))
if (mode === 'active' || mode === 'all_force') onModeChange('all')
}}
title={zeichnungsebenen.every(z => z.visible !== false)
? 'Alle Zeichnungsebenen ausblenden'
: 'Alle Zeichnungsebenen einblenden'}
style={{ width: 18, height: 18,
opacity: (mode === 'active' || mode === 'all_force') ? 0.5 : 1 }}
>
<Icon
name={zeichnungsebenen.every(z => z.visible !== false) ? 'visibility' : 'visibility_off'}
size={12}
/>
</button>
<span style={{ flex: 1 }} />
<button
className="btn-icon-xs"
onClick={() => {
const anyLocked = zeichnungsebenen.some(z => z.locked === true)
onChange(zeichnungsebenen.map(z => ({ ...z, locked: !anyLocked })))
}}
title={zeichnungsebenen.every(z => z.locked === true)
? 'Alle Zeichnungsebenen entsperren'
: 'Alle Zeichnungsebenen sperren'}
style={{ width: 18, height: 18 }}
>
<Icon
name={zeichnungsebenen.every(z => z.locked === true) ? 'lock' : 'lock_open'}
size={11}
/>
</button>
<div style={{ width: 18 }} />
</div>
<div> <div>
{sorted.map(z => ( {sorted.map(z => (
<ZeichnungsebeneRow <ZeichnungsebeneRow
@@ -159,12 +299,22 @@ export default function GeschossManager({
active={z.id === activeId} active={z.id === activeId}
mode={mode} mode={mode}
onClick={() => onActiveChange(z.id)} onClick={() => onActiveChange(z.id)}
onContextMenu={(ev) => openContextMenu(ev, z.id)}
onToggleVisible={() => toggleVisible(z.id)} onToggleVisible={() => toggleVisible(z.id)}
onSettings={() => openGeschossSettings(z)} onToggleLock={() => toggleLock(z.id)}
onToggleClipping={() => toggleClipping(z.id)}
onDelete={() => remove(z.id)}
/> />
))} ))}
</div> </div>
{ctxMenu && (
<ContextMenu
x={ctxMenu.x} y={ctxMenu.y}
items={ctxItems(ctxMenu.id)}
onClose={() => setCtxMenu(null)}
/>
)}
</> </>
) )
} }
+10 -1
View File
@@ -112,6 +112,7 @@ export function getAusschnittLayers(id) { send('GET_LAYERS', { i
export function updateAusschnittLayers(id, layers) { send('UPDATE_LAYERS', { id, layers }) } export function updateAusschnittLayers(id, layers) { send('UPDATE_LAYERS', { id, layers }) }
export function saveLayerPreset(name, layers) { send('SAVE_PRESET', { name, layers }) } export function saveLayerPreset(name, layers) { send('SAVE_PRESET', { name, layers }) }
export function deleteLayerPreset(name) { send('DELETE_PRESET', { name }) } export function deleteLayerPreset(name) { send('DELETE_PRESET', { name }) }
export function openAusschnittSettings(id) { send('OPEN_SETTINGS', { id }) }
// --- Gestaltung-Panel --- // --- Gestaltung-Panel ---
export function requestSelection() { export function requestSelection() {
@@ -168,6 +169,11 @@ export function toggleOverrides(on) { send('TOGGLE_OVERRIDES', { enabled: !
export function setOverridesPreset(name) { send('SET_OVERRIDES_PRESET', { name: name || null }) } export function setOverridesPreset(name) { send('SET_OVERRIDES_PRESET', { name: name || null }) }
export function saveOverridesPreset(name) { send('SAVE_OVERRIDES_PRESET', { name }) } export function saveOverridesPreset(name) { send('SAVE_OVERRIDES_PRESET', { name }) }
export function openOverridesPanel() { send('OPEN_OVERRIDES_PANEL', {}) } export function openOverridesPanel() { send('OPEN_OVERRIDES_PANEL', {}) }
// Ebenenkombinationen (gehosted in Oberleiste, gleicher Store wie EBENEN)
export function pickLayerCombination(name) { send('PICK_LAYER_COMBINATION', { name: name || null }) }
export function saveLayerCombination(name) { send('SAVE_LAYER_COMBINATION', { name }) }
export function deleteLayerCombination(name) { send('DELETE_LAYER_COMBINATION', { name }) }
export function openLayerCombinationsDialog() { send('OPEN_LAYER_COMBINATIONS_DIALOG', {}) }
export function runCommand(cmd) { send('RUN_COMMAND', { cmd }) } export function runCommand(cmd) { send('RUN_COMMAND', { cmd }) }
export function sendKeys(text, enter) { send('SEND_KEYS', { text, enter: enter !== false }) } export function sendKeys(text, enter) { send('SEND_KEYS', { text, enter: enter !== false }) }
export function cancelCommand() { send('CANCEL_COMMAND', {}) } export function cancelCommand() { send('CANCEL_COMMAND', {}) }
@@ -232,6 +238,7 @@ export function setPageSize(id, format, landscape, customWidth, customHeight) {
customWidth, customHeight }) customWidth, customHeight })
} }
export function exportPdf(id, dpi) { send('EXPORT_PDF', { id, dpi: dpi || 300 }) } export function exportPdf(id, dpi) { send('EXPORT_PDF', { id, dpi: dpi || 300 }) }
export function openLayoutDialog(mode, layout) { send('OPEN_LAYOUT_DIALOG', { mode: mode || 'new', layout: layout || null }) }
export function exportPdfAll(dpi) { send('EXPORT_PDF', { dpi: dpi || 300 }) } export function exportPdfAll(dpi) { send('EXPORT_PDF', { dpi: dpi || 300 }) }
export function exportPdfMany(ids, dpi) { send('EXPORT_PDF', { ids, dpi: dpi || 300 }) } export function exportPdfMany(ids, dpi) { send('EXPORT_PDF', { ids, dpi: dpi || 300 }) }
export function addLayoutFolder(name) { send('ADD_FOLDER', { name }) } export function addLayoutFolder(name) { send('ADD_FOLDER', { name }) }
@@ -304,7 +311,9 @@ export function applyVisibility(activeZ, zeichnungsebenen, activeCode, ebenen, z
const zList = Array.isArray(a.zeichnungsebenen) ? a.zeichnungsebenen : [] const zList = Array.isArray(a.zeichnungsebenen) ? a.zeichnungsebenen : []
const eList = Array.isArray(a.ebenen) ? a.ebenen : [] const eList = Array.isArray(a.ebenen) ? a.ebenen : []
const slimZ = zList.map(z => ({ const slimZ = zList.map(z => ({
id: z.id, name: z.name, visible: z.visible !== false, id: z.id, name: z.name,
visible: z.visible !== false,
locked: z.locked === true,
})) }))
const slimE = eList.map(e => ({ const slimE = eList.map(e => ({
code: e.code, visible: e.visible !== false, locked: e.locked === true, code: e.code, visible: e.visible !== false, locked: e.locked === true,
+6
View File
@@ -6,6 +6,9 @@ import ZeichnungsebenenApp from './ZeichnungsebenenApp.jsx'
import GeschossSettingsApp from './GeschossSettingsApp.jsx' import GeschossSettingsApp from './GeschossSettingsApp.jsx'
import EbenenSettingsApp from './EbenenSettingsApp.jsx' import EbenenSettingsApp from './EbenenSettingsApp.jsx'
import GeschossDialogApp from './GeschossDialogApp.jsx' import GeschossDialogApp from './GeschossDialogApp.jsx'
import LayerCombinationsApp from './LayerCombinationsApp.jsx'
import AusschnittSettingsApp from './AusschnittSettingsApp.jsx'
import LayoutDialogApp from './LayoutDialogApp.jsx'
import GestaltungApp from './GestaltungApp.jsx' import GestaltungApp from './GestaltungApp.jsx'
import AusschnitteApp from './AusschnitteApp.jsx' import AusschnitteApp from './AusschnitteApp.jsx'
import MassstabApp from './MassstabApp.jsx' import MassstabApp from './MassstabApp.jsx'
@@ -30,6 +33,9 @@ const RootApp = mode === 'gestaltung' ? GestaltungApp
: mode === 'geschoss_settings' ? GeschossSettingsApp : mode === 'geschoss_settings' ? GeschossSettingsApp
: mode === 'ebenen_settings' ? EbenenSettingsApp : mode === 'ebenen_settings' ? EbenenSettingsApp
: mode === 'geschoss_dialog' ? GeschossDialogApp : mode === 'geschoss_dialog' ? GeschossDialogApp
: mode === 'layer_combinations' ? LayerCombinationsApp
: mode === 'ausschnitt_settings' ? AusschnittSettingsApp
: mode === 'layout_dialog' ? LayoutDialogApp
: App : App
window.onerror = function (msg, src, line, col, err) { window.onerror = function (msg, src, line, col, err) {