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>
This commit is contained in:
2026-05-19 03:58:28 +02:00
parent e3918cb155
commit 95031ee2c0
29 changed files with 1708 additions and 713 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
// definiert, schickt das Plugin diese hier als FIRST_RUN-Default.
const DEFAULT_LAYER_SCHEMA = [
{ code: '00', name: 'RASTER', color: '#484850', lw: 0.13 },
{ code: '01', name: 'VERMESSUNG', color: '#707078', lw: 0.18 },
{ code: '10', name: 'SITUATION', color: '#909090', lw: 0.18 },
{ code: '11', name: 'STRASSE', color: '#a89070', lw: 0.18 },
{ code: '12', name: 'GEBAEUDE', color: '#888888', lw: 0.25 },
{ code: '13', name: 'BAEUME', color: '#50a050', lw: 0.13 },
{ code: '14', name: 'HOEHENLINIEN', color: '#909050', lw: 0.18 },
{ code: '20', name: 'WAENDE', color: '#0a0a0a', lw: 0.50 },
{ code: '21', name: 'TUEREN_FENSTER', color: '#5080c8', lw: 0.25 },
{ code: '22', name: 'MOEBEL', color: '#909090', lw: 0.13 },
{ code: '25', name: 'STUETZEN', color: '#c87050', lw: 0.50 },
{ code: '30', name: 'DECKEN', color: '#605850', lw: 0.35 },
{ code: '31', name: 'DAECHER', color: '#7a4a3a', lw: 0.35 },
{ code: '35', name: 'TRAEGER', color: '#a87858', lw: 0.50 },
{ code: '50', name: 'TEXT', color: '#d0d0d0', lw: 0.13 },
{ code: '60', name: 'PLANGRAFIK', color: '#c0a040', lw: 0.13 },
{ code: '90', name: 'REFERENZEN', color: '#585860', lw: 0.13 },
{ code: '99', name: 'KONSTRUKTION', color: '#404048', lw: 0.13 },
{ code: '00', name: 'Raster', color: '#484850', lw: 0.13 },
{ code: '01', name: 'Vermessung', color: '#707078', lw: 0.18 },
{ code: '10', name: 'Situation', color: '#909090', lw: 0.18 },
{ code: '11', name: 'Strasse', color: '#a89070', lw: 0.18 },
{ code: '12', name: 'Gebaeude', color: '#888888', lw: 0.25 },
{ code: '13', name: 'Baeume', color: '#50a050', lw: 0.13 },
{ code: '14', name: 'Hoehenlinien', color: '#909050', lw: 0.18 },
{ code: '20', name: 'Waende', color: '#0a0a0a', lw: 0.50 },
{ code: '21', name: 'Tueren_Fenster', color: '#5080c8', lw: 0.25 },
{ code: '22', name: 'Moebel', color: '#909090', lw: 0.13 },
{ code: '25', name: 'Stuetzen', color: '#c87050', lw: 0.50 },
{ code: '30', name: 'Decken', color: '#605850', lw: 0.35 },
{ code: '31', name: 'Daecher', color: '#7a4a3a', lw: 0.35 },
{ code: '35', name: 'Traeger', color: '#a87858', lw: 0.50 },
{ code: '50', name: 'Text', color: '#d0d0d0', lw: 0.13 },
{ code: '60', name: 'Plangrafik', color: '#c0a040', lw: 0.13 },
{ code: '90', name: 'Referenzen', color: '#585860', lw: 0.13 },
{ code: '99', name: 'Konstruktion', color: '#404048', lw: 0.13 },
]
// Built-in Presets — nicht in den Settings gespeichert, immer verfuegbar.
+150 -1
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 == "SAVE_PRESET": self._save_preset(p.get("name"), p.get("layers") or [])
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):
raw = doc.Strings.GetValue(_STORE_KEY)
@@ -537,8 +538,42 @@ class AusschnittBridge(panel_base.BaseBridge):
if view is None: return
vp = view.ActiveViewport
_apply_camera(vp, snap.get("camera"))
# 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 {})
# 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
# wird unter vp.Name geschluesselt. Erst nach dem Rename schreibt
# _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._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"))
+1 -1
View File
@@ -608,6 +608,6 @@ def _bridge_factory():
return b
panel_base.register_and_open("dimensionen", "DIMENSIONEN", PANEL_GUID_STR,
panel_base.register_and_open("dimensionen", "Dimensionen", PANEL_GUID_STR,
_bridge_factory,
icon_spec=("aspect_ratio", "#9e7050"))
+20 -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):
"""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"],
"20", "WÄNDE",
"20", "Wände",
default_color="#0a0a0a", default_lw=0.50)
return "{}::{}".format(geschoss_name, sub)
@@ -4543,6 +4543,23 @@ class ElementeBridge(panel_base.BaseBridge):
elif t == "DELETE_ELEMENT": self._delete_wall(p.get("id"))
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):
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None:
@@ -8509,6 +8526,6 @@ def _bridge_factory():
return b
panel_base.register_and_open("elemente", "ELEMENTE", PANEL_GUID_STR,
panel_base.register_and_open("elemente", "Elemente", PANEL_GUID_STR,
_bridge_factory,
icon_spec=("foundation", "#5fa896"))
+1 -1
View File
@@ -1695,5 +1695,5 @@ def _bridge_factory():
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"))
+6
View File
@@ -578,6 +578,7 @@ def apply_visibility(doc, zeichnungsebenen, ebenen, active_z_id, active_code, z_
children = children_by_parent.get(parent.Id, [])
is_active_z = z["id"] == active_z_id
z_visible_flag = z.get("visible", True)
z_locked_flag = bool(z.get("locked", False))
# Z-Mode -> Parent-Zustand
if is_active_z:
@@ -593,6 +594,11 @@ def apply_visibility(doc, zeichnungsebenen, ebenen, active_z_id, active_code, z_
else: # grey
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
if 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 == "REMOVE_FOLDER": self._remove_folder(p.get("name"))
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 -----------------------------------------------------
@@ -737,6 +738,69 @@ class LayoutsBridge(panel_base.BaseBridge):
print("[LAYOUTS] sync layout:", ex)
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():
b = LayoutsBridge()
@@ -744,6 +808,6 @@ def _bridge_factory():
return b
panel_base.register_and_open("layouts", "LAYOUTS", PANEL_GUID_STR,
panel_base.register_and_open("layouts", "Layouts", PANEL_GUID_STR,
_bridge_factory,
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.
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"))
# register_standalone_panel() # auskommentiert — Standard ist OBERLEISTE
+44 -1
View File
@@ -20,6 +20,7 @@ if _HERE not in sys.path:
import panel_base
import massstab
import overrides
import rhinopanel
PANEL_GUID_STR = "7e1f6a5b-8e2f-4f3c-d5e6-f70819203b51"
OVERRIDES_PANEL_GUID_STR = "8f2a7b6c-9f3a-4f4d-e6f7-08192a3c4d62"
@@ -770,6 +771,7 @@ class OberleisteBridge(panel_base.BaseBridge):
self._last_prompt = ""
self._last_state_sig = None # Fingerprint des letzten Push — dedupe
self._cached_overrides = None # (enabled, count) — invalidiert bei Toggle/Update
self._cached_combinations = None # (names, active) — invalidiert bei jeder Comb-Aenderung
# Command-Liste einmalig laden (kann teuer sein -> cachen)
try:
self._all_commands = _list_all_command_names()
@@ -920,6 +922,34 @@ class OberleisteBridge(panel_base.BaseBridge):
except Exception as 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 -----------------------------------
elif t == "RUN_COMMAND":
cmd = (p.get("cmd") or "").strip()
@@ -1035,6 +1065,18 @@ class OberleisteBridge(panel_base.BaseBridge):
info["overridesActivePreset"],
_presets_tuple) = self._cached_overrides
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
prompt = _get_command_prompt()
info["cmdPrompt"] = prompt
@@ -1057,6 +1099,7 @@ class OberleisteBridge(panel_base.BaseBridge):
info["overridesEnabled"], info["overridesCount"],
info.get("overridesActivePreset"),
tuple(info.get("overridesPresets") or ()),
_names_tuple, _active_comb,
prompt,
)
if not force and sig == self._last_state_sig:
@@ -1197,5 +1240,5 @@ def _bridge_factory():
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"))
+1 -1
View File
@@ -266,7 +266,7 @@ def open_as_window():
sc.sticky["overrides_bridge"] = b
panel_base.open_satellite_window(
"overrides",
title="OVERRIDES",
title="Overrides",
size=(760, 580),
bridge=b)
+480 -83
View File
@@ -103,6 +103,243 @@ def _broadcast_state(doc=None, hatch_patterns=None):
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):
"""Gemeinsame Bridge-Klasse fuer beide Panels (Ebenen + Zeichnungsebenen).
Mode bestimmt nur welches WebView die Bridge bedient + welcher sticky-Slot
@@ -266,40 +503,80 @@ class EbenenBridge(panel_base.BaseBridge):
on_save=on_save)
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"):
print("[EBENEN] open_ebenen_settings: kein Ebene-Payload")
return
old_code = ebene["code"]
def on_save(updated):
bridge_holder = {"form": None}
apply_self = self
class _EbenenSettingsBridge(panel_base.BaseBridge):
def __init__(self):
panel_base.BaseBridge.__init__(self, "ebenen_settings")
def _on_ready(self):
self._send_state()
def _send_state(self):
doc = Rhino.RhinoDoc.ActiveDoc
e_raw = doc.Strings.GetValue("dossier_ebenen") if doc else None
try: e_list = json.loads(e_raw) if e_raw else []
except Exception: e_list = []
self.send("EBENEN_SETTINGS_STATE", {
"ebenen": e_list,
"hatchPatterns": hatch_patterns,
})
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_KEEP":
self._persist(p)
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:
print("[EBENEN] save_ebene: kein e-Store"); return
try:
e_list = json.loads(e_raw)
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") == old_code:
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(old_code))
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 = []
self._apply(z_list, e_list, save_z=False, save_e=True)
panel_base.open_satellite_window(
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",
params={"ebene": ebene, "hatchPatterns": hatch_patterns},
title="Ebene: {}_{}".format(ebene.get("code", ""), ebene.get("name", "")),
params={"currentCode": ebene["code"], "hatchPatterns": hatch_patterns},
title="Ebenen-Einstellungen",
size=(420, 600),
on_save=on_save)
bridge=b)
def _open_geschoss_dialog(self, zeichnungsebenen):
"""Oeffnet den vollen GeschossDialog (Mehrfach-Editor) als
@@ -416,6 +693,9 @@ class EbenenBridge(panel_base.BaseBridge):
self._update_clipping()
print("[EBENEN] _apply: 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
# informieren — sonst hinkt es hinter der DOM-Persistenz her.
_broadcast_state(doc)
@@ -476,6 +756,7 @@ class EbenenBridge(panel_base.BaseBridge):
s = z_state.get(z.get("id"))
if s is not None:
m["visible"] = s.get("visible", True)
m["locked"] = s.get("locked", False)
merged_z.append(m)
merged_e = []
for e in e_full:
@@ -486,11 +767,33 @@ class EbenenBridge(panel_base.BaseBridge):
m["visible"] = s.get("visible", True)
m["locked"] = s.get("locked", False)
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:
print("[EBENEN] _apply_visibility: structural change pending → skip save (waiting for APPLY)")
else:
doc.Strings.SetString("dossier_zeichnungsebenen", json.dumps(merged_z, 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
# Panel-Slice) der andere Mode aus dem Doc-Storage faellt anstatt
# auf den Default zu rutschen.
@@ -504,6 +807,24 @@ class EbenenBridge(panel_base.BaseBridge):
if not isinstance(active_z, dict): active_z = {}
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")
# 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(
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
@@ -515,17 +836,29 @@ class EbenenBridge(panel_base.BaseBridge):
def _set_active_zeichnungsebene(self, z):
doc = Rhino.RhinoDoc.ActiveDoc
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)
# 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
# mitbekommen falls es im "active"-Filter-Mode laeuft.
_broadcast_state(doc)
# Clipping ggf. mitziehen
# Clipping nur antasten wenn entweder das alte oder das neue Geschoss
# 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,
# 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:
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
if not (z.get("isGeschoss") and z.get("okff") is not None):
return
@@ -546,12 +879,33 @@ class EbenenBridge(panel_base.BaseBridge):
plane.XAxis, plane.YAxis,
)
vp.SetConstructionPlane(new_plane)
view.Redraw()
updated += 1
except Exception as 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))
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):
"""Setzt hasClipping fuer das aktuell aktive Geschoss + persistiert in
doc.Strings + triggert plane-update. Wird vom React-Toggle 'Clipping
@@ -704,20 +1058,10 @@ class EbenenBridge(panel_base.BaseBridge):
_PRESETS_KEY = "dossier_layer_presets"
def _load_presets(self, doc):
raw = doc.Strings.GetValue(self._PRESETS_KEY)
if not raw: return []
try:
data = json.loads(raw)
return data if isinstance(data, list) else []
except Exception:
return []
return load_layer_presets(doc)
def _store_presets(self, doc, presets):
try:
doc.Strings.SetString(self._PRESETS_KEY,
json.dumps(presets, ensure_ascii=False))
except Exception as ex:
print("[EBENEN] _store_presets:", ex)
store_layer_presets(doc, presets)
def _send_combination(self):
"""Schickt aktuelles Layer-State + alle Presets ans Frontend."""
@@ -917,7 +1261,7 @@ class EbenenBridge(panel_base.BaseBridge):
name = (name or "").strip()
if not name: return
doc = Rhino.RhinoDoc.ActiveDoc
presets = self._load_presets(doc)
presets = load_layer_presets(doc)
clean = []
for ls in (layers or []):
lid = ls.get("id")
@@ -932,7 +1276,9 @@ class EbenenBridge(panel_base.BaseBridge):
existing["layers"] = clean
else:
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)))
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
mit AUSSCHNITTE (das vom doc.Layer-State liest)."""
name = (name or "").strip()
if not name: return
save_current_as_layer_preset(Rhino.RhinoDoc.ActiveDoc, name)
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
# 1) doc.Layer state (Kompat mit AUSSCHNITTE)
layers = []
if doc is None: return
layers_out = []
try:
for layer in doc.Layers:
if layer is None or layer.IsDeleted: continue
layers.append({
"id": str(layer.Id),
lid = str(layer.Id)
try: fp = layer.FullPath or layer.Name
except Exception: fp = layer.Name or ""
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:
print("[EBENEN] _save_current_as_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)),
print("[LAYER-COMB] enum:", ex)
self.send("LAYER_COMBINATIONS_STATE", {
"layers": layers_out,
"presets": load_layer_presets(doc),
})
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):
name = (name or "").strip()
if not name: return
def handle(self, data):
if not isinstance(data, dict): return
t = data.get("type", "")
p = data.get("payload") or {}
if not isinstance(p, dict): p = {}
doc = Rhino.RhinoDoc.ActiveDoc
presets = [p for p in self._load_presets(doc) if p.get("name") != name]
self._store_presets(doc, presets)
print("[EBENEN] Kombination '{}' geloescht".format(name))
if t == "READY" or t == "REQUEST_STATE":
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():
@@ -1087,8 +1484,8 @@ def _install_layer_listener(bridge):
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"))
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,
icon_spec=("levels", "#3a6fa8"))
+1 -1
View File
@@ -54,5 +54,5 @@ def _bridge_factory():
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"))
+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 AusschnittLayerDialog from './components/AusschnittLayerDialog'
import {
applyAll, setActiveEbene,
onMessage, notifyReady, applyVisibility,
getCombination, applyCombination,
saveCurrentAsCombination, deleteCombinationPreset,
saveCombinationPreset,
} from './lib/rhinoBridge'
const INITIAL_EBENEN = [
{ 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: '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: '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: '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: '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: '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: '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: '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: '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: '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: '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: '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: '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: '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: '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: '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: '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: '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 },
]
export default function App() {
@@ -36,28 +32,12 @@ export default function App() {
const [appliedE, setAppliedE] = useState(INITIAL_EBENEN)
const [eMode, setEMode] = useState('all')
const [hatchPatterns, setHatchPatterns] = useState(['Solid', 'Hatch1', 'Hatch2', 'Hatch3', 'Plus', 'Squares', 'Grid', 'Grid60'])
// 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(() => {
onMessage('STATE_SYNC', ({ ebenen: e, hatchPatterns: hp }) => {
if (e) { setEbenen(e); setAppliedE(e) }
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 } = {}) => {
// Wenn der Dossier-Launcher ein eigenes Schema definiert hat, nutzen wir
// das statt der hardcoded INITIAL_EBENEN.
@@ -72,8 +52,6 @@ export default function App() {
if (activeCode) setActiveEbene(activeCode)
})
notifyReady()
// Initial Liste der Kombinationen holen
setTimeout(() => getCombination(), 200)
// Native Browser-Context-Menu global unterdruecken
const blockContext = (ev) => ev.preventDefault()
@@ -122,41 +100,6 @@ export default function App() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [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 (
<div style={{
display: 'flex', flexDirection: 'column',
@@ -173,44 +116,8 @@ export default function App() {
mode={eMode}
onModeChange={setEMode}
hatchPatterns={hatchPatterns}
combinations={combinations}
activeCombName={activeCombName}
onPickCombination={handlePickCombination}
onSaveCurrentCombination={handleSaveCurrentCombination}
onDeleteCombination={handleDeleteCombination}
onEditCombinations={handleOpenCombDialog}
onUserVisibilityChange={handleUserVisibilityChange}
/>
</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>
)
}
+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 Icon from './components/Icon'
import ContextMenu from './components/ContextMenu'
import AusschnittLayerDialog from './components/AusschnittLayerDialog'
import {
onMessage, notifyReady,
listAusschnitte, saveAusschnitt, updateAusschnitt,
@@ -9,8 +8,7 @@ import {
renameAusschnitt, deleteAusschnitt,
setAusschnittFolder, setAusschnittScale,
duplicateAusschnitt, addAusschnittFolder, removeAusschnittFolder,
getAusschnittLayers, updateAusschnittLayers,
saveLayerPreset, deleteLayerPreset,
openAusschnittSettings,
} from './lib/rhinoBridge'
function EditableInline({ value, onCommit, autoEdit, style, fontSize }) {
@@ -247,22 +245,16 @@ function RootDropZone({ children, onDragOver, onDragLeave, onDrop, dragOver, emp
export default function AusschnitteApp() {
const [snaps, setSnaps] = useState([])
const [extraFolders, setExtraFolders] = useState([])
const [presets, setPresets] = useState([])
const [newName, setNewName] = useState('')
const [ctxMenu, setCtxMenu] = useState(null)
const [collapsed, setCollapsed] = useState({})
const [draggingId, setDraggingId] = useState(null)
const [dragTarget, setDragTarget] = useState(null)
const [layerDialog, setLayerDialog] = useState(null)
useEffect(() => {
onMessage('LIST', ({ snapshots, folders, presets }) => {
onMessage('LIST', ({ snapshots, folders }) => {
setSnaps(snapshots || [])
setExtraFolders(folders || [])
setPresets(presets || [])
})
onMessage('LAYERS_DATA', ({ id, name, layers, presets }) => {
setLayerDialog({ id, name, layers: layers || [], presets: presets || [] })
})
notifyReady()
const blockContext = (ev) => ev.preventDefault()
@@ -301,7 +293,7 @@ export default function AusschnitteApp() {
{ label: 'Wiederherstellen', icon: 'restore', onClick: () => restoreAusschnitt(id) },
{ label: 'Auf Detail anwenden', icon: 'crop_landscape', onClick: () => applyAusschnittToDetail(id) },
{ divider: true },
{ label: 'Sichtbarkeit bearbeiten…', icon: 'layers', onClick: () => getAusschnittLayers(id) },
{ label: 'Ausschnittseinstellungen…', icon: 'tune', onClick: () => openAusschnittSettings(id) },
{ divider: true },
{ label: 'Duplizieren', icon: 'content_copy', onClick: () => duplicateAusschnitt(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 isEmpty = snaps.length === 0 && allFolders.length === 0
@@ -382,21 +363,6 @@ export default function AusschnitteApp() {
background: 'var(--bg-base)',
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 }}>
{/* Save-Bar als Card */}
<div style={{
@@ -481,6 +447,28 @@ export default function AusschnitteApp() {
</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 && (
<ContextMenu
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>
)
}
+51 -77
View File
@@ -50,24 +50,22 @@ function NumInput({ value, onCommit, disabled, suffix, width }) {
)
}
// 9-Punkt-Referenzpunkt-Selektor im Illustrator-Stil: sichtbarer BBox-Rahmen,
// die Punkte sitzen AUF Ecken / Kantenmitten / Zentrum.
// 9-Punkt-Referenzpunkt-Selektor: sichtbarer BBox-Rahmen, Kreise auf den
// Eckpunkten / Kantenmitten / Zentrum.
function RefPointGrid({ ref, onChange }) {
const SIZE = 26 // Aussenkanten-Quadrat (px)
const DOT = 5 // Punkt-Durchmesser (px)
// Position pro Code: 0% (min), 50% (mid), 100% (max)
const SIZE = 28 // Aussenkanten-Quadrat (px)
const DOT = 6 // Kreis-Durchmesser (px)
const pct = (c) => c === 'min' ? '0%' : c === 'max' ? '100%' : '50%'
return (
<div style={{
position: 'relative',
width: SIZE, height: SIZE,
border: '1px solid var(--text-muted)',
border: '1px solid var(--border)',
background: 'transparent',
flexShrink: 0,
}}>
{REF_CODES.map(yc => REF_CODES.map(xc => {
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%'
return (
<button
@@ -79,14 +77,20 @@ function RefPointGrid({ ref, onChange }) {
left: pct(xc), top: topPct,
transform: 'translate(-50%, -50%)',
width: DOT, height: DOT, padding: 0,
borderRadius: 0, // eckig wie Illustrator
borderRadius: '50%',
background: active ? 'var(--accent)' : 'var(--text-muted)',
border: 'none',
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)',
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 */}
<div style={{
display: 'flex', alignItems: 'center', gap: 6, marginBottom: 6,
padding: '5px 8px',
background: 'var(--bg-section)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
display: 'flex', alignItems: 'center', gap: 6,
padding: '8px 12px',
borderBottom: '1px solid var(--border-light)',
}}>
<Icon name="select_all" size={14} style={{ color: 'var(--text-muted)' }} />
<span style={{ flex: 1, fontWeight: 500 }}>{selLabel()}</span>
@@ -221,11 +223,8 @@ export default function DimensionenApp() {
<div style={{
padding: '32px 16px', textAlign: 'center',
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: 4, fontSize: 10 }}>
In Rhino ein oder mehrere Objekte auswählen.
@@ -233,38 +232,30 @@ export default function DimensionenApp() {
</div>
) : (
<>
{/* Referenzpunkt — kompakte einzeilige Card */}
{/* Referenzpunkt */}
<div style={{
display: 'flex', alignItems: 'center', gap: 8,
padding: '6px 8px', marginBottom: 6,
background: 'var(--bg-section)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
display: 'flex', alignItems: 'center', gap: 10,
padding: '10px 12px',
borderBottom: '1px solid var(--border-light)',
}}>
<span style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)',
letterSpacing: '0.06em', textTransform: 'uppercase' }}>
Ref
</span>
<span className="label-xs" style={{ width: 30 }}>Ref</span>
<RefPointGrid ref={ref} onChange={onRefChange} />
<div style={{ flex: 1 }} />
<RefZSelector z={ref.z} onChange={(z) => onRefChange({ ...ref, z })} />
</div>
{/* Position + Abmessungen nebeneinander */}
<div style={{ display: 'flex', gap: 6, marginBottom: 6 }}>
{/* Position + BBox nebeneinander */}
<div style={{
flex: 1, display: 'flex', flexDirection: 'column', gap: 4,
padding: '6px 8px',
background: 'var(--bg-section)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
minWidth: 0,
display: 'flex', gap: 16,
padding: '10px 12px',
borderBottom: '1px solid var(--border-light)',
}}>
<div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)',
letterSpacing: '0.06em', textTransform: 'uppercase',
display: 'flex', justifyContent: 'space-between' }}>
<span>Position</span>
<span style={{ fontWeight: 400, textTransform: 'none',
fontFamily: 'DM Mono, monospace', fontSize: 9 }}>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 4, minWidth: 0 }}>
<div style={{ display: 'flex', justifyContent: 'space-between',
alignItems: 'baseline', marginBottom: 2 }}>
<span className="label-xs">Position</span>
<span style={{ fontFamily: 'DM Mono, monospace', fontSize: 9,
color: 'var(--text-muted)' }}>
{state.planeName}
</span>
</div>
@@ -272,18 +263,9 @@ export default function DimensionenApp() {
<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>
</div>
<div style={{
flex: 1, display: 'flex', flexDirection: 'column', gap: 4,
padding: '6px 8px',
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>
<div style={{ width: 1, background: 'var(--border-light)' }} />
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 4, minWidth: 0 }}>
<span className="label-xs" style={{ marginBottom: 2 }}>BBox</span>
<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="H"><NumInput value={dims?.height} onCommit={(v) => setDimDimension('height', v)} /></Field>
@@ -294,22 +276,19 @@ export default function DimensionenApp() {
{shape && (
<div style={{
display: 'flex', flexDirection: 'column', gap: 4,
padding: '6px 8px', marginBottom: 6,
background: 'var(--bg-section)',
border: '1px solid var(--accent)',
borderRadius: 'var(--r-lg)',
padding: '10px 12px',
borderBottom: '1px solid var(--border-light)',
}}>
<div style={{ fontSize: 10, fontWeight: 600, color: 'var(--accent)',
letterSpacing: '0.06em', textTransform: 'uppercase' }}>
<span className="label-xs" style={{ color: 'var(--accent)' }}>
{shape.type === 'circle' && 'Kreis'}
{shape.type === 'rectangle' && 'Rechteck'}
{shape.type === 'line' && 'Linie'}
</div>
</span>
{shape.type === 'circle' && (
<Field label="R"><NumInput value={shape.radius} onCommit={(v) => setCircleRadius(v)} /></Field>
)}
{shape.type === 'rectangle' && (
<div style={{ display: 'flex', gap: 6 }}>
<div style={{ display: 'flex', gap: 8 }}>
<Field label="W" style={{ flex: 1 }}>
<NumInput value={shape.width}
onCommit={(v) => setRectangleDims(v, shape.height)} />
@@ -321,7 +300,7 @@ export default function DimensionenApp() {
</div>
)}
{shape.type === 'line' && (
<div style={{ display: 'flex', gap: 6 }}>
<div style={{ display: 'flex', gap: 8 }}>
<Field label="L" style={{ flex: 1 }}>
<NumInput value={shape.length} onCommit={(v) => setLineLength(v)} />
</Field>
@@ -333,19 +312,13 @@ export default function DimensionenApp() {
</div>
)}
{/* Rotation — kompakt einzeilig */}
{/* Rotation */}
<div style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '6px 8px', marginBottom: 6,
background: 'var(--bg-section)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
padding: '10px 12px',
}}>
<span style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)',
letterSpacing: '0.06em', textTransform: 'uppercase' }}>
Drehen
</span>
<div style={{ flex: 1 }}>
<span className="label-xs" style={{ width: 50 }}>Drehen</span>
<div style={{ width: 56 }}>
<NumInput value={rotationDelta} onCommit={setRotationDelta} suffix="°" />
</div>
<button
@@ -357,12 +330,13 @@ export default function DimensionenApp() {
>
<Icon name="rotate_right" size={13} />
</button>
<div style={{ flex: 1 }} />
{[-90, -45, 45, 90].map(a => (
<button
key={a}
className="btn-outlined"
onClick={() => setDimRotationZ(a)}
style={{ padding: '3px 5px', fontSize: 9, minWidth: 28 }}
style={{ padding: '3px 6px', fontSize: 9, minWidth: 28 }}
title={`${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 { notifyReady } from './lib/rhinoBridge'
import { notifyReady, onMessage } from './lib/rhinoBridge'
function bridgeSend(type, payload = {}) {
if (!window.RHINO_MODE) { console.log('[Bridge] →', type, payload); return }
const json = JSON.stringify({ type, payload })
document.title = 'RHINOMSG::' + json
document.title = 'RHINOMSG::' + JSON.stringify({ type, payload })
}
export default function EbenenSettingsApp() {
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(() => {
onMessage('EBENEN_SETTINGS_STATE', ({ ebenen: list, hatchPatterns: hp }) => {
if (Array.isArray(list)) setEbenen(list)
if (Array.isArray(hp) && hp.length) setHatchPatterns(hp)
})
notifyReady()
const blockContext = (ev) => ev.preventDefault()
document.addEventListener('contextmenu', blockContext)
return () => document.removeEventListener('contextmenu', blockContext)
}, [])
const ebene = initial.ebene || initial
const hatchPatterns = initial.hatchPatterns || ['Solid']
const sortedEbenen = [...ebenen].sort((a, b) => {
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) {
return <div style={{ padding: 20, color: 'var(--text-muted)' }}>Keine Daten</div>
const currentEbene = ebenen.find(e => e.code === selectedCode)
|| 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 (
<EbenenSettingsDialog
key={dialogKey.current}
embedded
ebene={ebene}
ebene={currentEbene}
hatchPatterns={hatchPatterns}
onSave={(updated) => bridgeSend('SAVE', updated)}
onSave={(updated) => bridgeSend('SAVE', { ebene: updated, originalCode })}
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)',
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>
<button
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 {
onMessage, notifyReady,
listLayouts, newLayout, deleteLayout, renameLayout, activateLayout,
listLayouts, deleteLayout, renameLayout, activateLayout,
addDetail, deleteDetail, bindAusschnitt, syncDetail, syncLayout,
setPageSize, exportPdf, exportPdfAll, exportPdfMany,
exportPdf, exportPdfAll, exportPdfMany,
addLayoutFolder, removeLayoutFolder, setLayoutFolder,
openLayoutDialog,
} from './lib/rhinoBridge'
const PAPER_SIZES = ['A4', 'A3', 'A2', 'A1', 'A0', 'Letter']
const PAPER_FORMATS_MM = {
A0: [841, 1189], A1: [594, 841], A2: [420, 594], A3: [297, 420],
A4: [210, 297], Letter: [216, 279],
@@ -112,7 +111,6 @@ function EditableName({ value, onCommit, style, title, forceEdit, onEditDone })
export default function LayoutsApp() {
const [state, setState] = useState({ layouts: [], snapshots: [], details: {}, folders: [] })
const [selectedId, setSelectedId] = useState(null)
const [dialog, setDialog] = useState(null) // { mode: 'new' | 'edit', layout? }
const [checked, setChecked] = useState(new Set()) // Multi-Select IDs
const [collapsedFolders, setCollapsedFolders] = useState(new Set())
const [draggingId, setDraggingId] = useState(null)
@@ -187,8 +185,10 @@ export default function LayoutsApp() {
{ divider: true },
{ label: 'Als PDF exportieren', icon: 'picture_as_pdf',
onClick: () => exportPdf(l.id, 300) },
{ label: 'Papierformat aendern', icon: 'aspect_ratio',
onClick: () => setDialog({ mode: 'edit', layout: l }) },
{ label: 'Papierformat ändern', icon: 'aspect_ratio',
onClick: () => openLayoutDialog('edit', {
id: l.id, name: l.name, width: l.widthMm, height: l.heightMm,
}) },
{ divider: true },
...(folders.length > 0 ? [
...folders.map(f => ({
@@ -276,57 +276,6 @@ export default function LayoutsApp() {
background: 'var(--bg-base)', color: 'var(--text-primary)',
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 }}>
{/* Layout-Liste */}
{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 }} />
<div style={{ marginTop: 8 }}>Noch keine Layouts.</div>
<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>
) : (
@@ -610,6 +559,72 @@ export default function LayoutsApp() {
)}
</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 */}
{ctxMenu && (
<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>
)
}
+57
View File
@@ -7,6 +7,8 @@ import {
setMassstabDpi, detectMassstabDpi,
setView, setDisplayMode,
toggleOverrides, setOverridesPreset, openOverridesPanel,
pickLayerCombination, saveLayerCombination,
deleteLayerCombination, openLayerCombinationsDialog,
openDossierSettings,
} from './lib/rhinoBridge'
@@ -125,6 +127,7 @@ export default function OberleisteApp() {
overridesEnabled: false, overridesCount: 0,
cmdPrompt: '', cmdOptions: [],
overridesActivePreset: null, overridesPresets: [],
layerCombinations: [], layerCombinationActive: null,
})
const [appliedScale, setAppliedScale] = useState(null)
const appliedScaleRef = useRef(null)
@@ -380,6 +383,60 @@ export default function OberleisteApp() {
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 */}
<div style={{ flex: 1 }} />
</div>
+2 -2
View File
@@ -56,10 +56,10 @@ export default function ZeichnungsebenenApp() {
// 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(() => (
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])
useEffect(() => {
+19 -11
View File
@@ -22,6 +22,7 @@ export default function AusschnittLayerDialog({
snapName, layers, presets,
onSave, onClose,
onSavePreset, onDeletePreset,
embedded = false,
}) {
// Welche Kombination wird gerade angezeigt? null = aktueller Doc-State
const [selectedPreset, setSelectedPreset] = useState(null)
@@ -104,24 +105,29 @@ export default function AusschnittLayerDialog({
onSave(draft)
}
return (
<div style={{
position: 'absolute', inset: 0, zIndex: 150,
const wrapperStyle = embedded
? { position: 'absolute', inset: 0, display: 'flex' }
: { position: 'absolute', inset: 0, zIndex: 150,
background: 'var(--bg-overlay)',
display: 'flex', alignItems: 'flex-start', justifyContent: 'center',
paddingTop: 30,
}}>
<div style={{
background: 'var(--bg-dialog)',
paddingTop: 30 }
const cardStyle = embedded
? { flex: 1, display: 'flex', flexDirection: 'column',
background: 'var(--bg-dialog)', overflow: 'hidden' }
: { background: 'var(--bg-dialog)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
boxShadow: 'var(--shadow-3)',
width: 'calc(100% - 24px)', maxWidth: 480,
maxHeight: 'calc(100vh - 60px)',
display: 'flex', flexDirection: 'column',
overflow: 'hidden',
}}>
{/* Header */}
overflow: 'hidden' }
return (
<div style={wrapperStyle}>
<div style={cardStyle}>
{/* Header im embedded-Modus weggelassen (Satellite-Fenster hat schon
seine native Title-Bar mit Close-Button) */}
{!embedded && (
<div style={{
display: 'flex', alignItems: 'center', gap: 8,
padding: '12px 16px',
@@ -139,6 +145,7 @@ export default function AusschnittLayerDialog({
)}
<button onClick={onClose} style={{ color: 'var(--text-muted)', fontSize: 16, padding: '0 4px' }}>×</button>
</div>
)}
{/* Preset-Auswahl */}
<div style={{
@@ -235,7 +242,8 @@ export default function AusschnittLayerDialog({
</div>
{/* 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 ? (
<div style={{ padding: '30px 14px', textAlign: 'center', color: 'var(--text-muted)', fontSize: 11 }}>
Keine Ebenen gefunden.
-59
View File
@@ -255,9 +255,6 @@ function SortHeader({ label, sortKey, sortBy, sortDir, onSort, style }) {
export default function EbenenManager({
ebenen, activeCode, onActiveChange, onChange, mode, onModeChange, hatchPatterns,
combinations = [], activeCombName = null,
onPickCombination, onSaveCurrentCombination, onDeleteCombination,
onEditCombinations, onUserVisibilityChange,
}) {
const [sortBy, setSortBy] = useState('code')
const [sortDir, setSortDir] = useState('asc')
@@ -306,12 +303,10 @@ export default function EbenenManager({
const handleToggleVisible = (code) => {
const cur = ebenen.find(e => e.code === code)
if (cur) updateByCode(code, { visible: !(cur.visible !== false) })
if (onUserVisibilityChange) onUserVisibilityChange()
}
const handleToggleLock = (code) => {
const cur = ebenen.find(e => e.code === code)
if (cur) updateByCode(code, { locked: !cur.locked })
if (onUserVisibilityChange) onUserVisibilityChange()
}
const handleColorChange = (code, color) => {
updateByCode(code, { color })
@@ -426,58 +421,6 @@ export default function EbenenManager({
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={{
display: 'flex', flexDirection: 'column', gap: 4,
padding: '6px 14px',
@@ -512,7 +455,6 @@ export default function EbenenManager({
const anyVisible = ebenen.some(e => e.visible !== false)
// Wenn irgendeine sichtbar -> alle aus. Wenn keine sichtbar -> alle an.
onChange(ebenen.map(e => ({ ...e, visible: !anyVisible })))
if (onUserVisibilityChange) onUserVisibilityChange()
}}
title={ebenen.every(e => e.visible !== false)
? 'Alle Ebenen ausblenden'
@@ -534,7 +476,6 @@ export default function EbenenManager({
onClick={() => {
const anyLocked = ebenen.some(e => e.locked === true)
onChange(ebenen.map(e => ({ ...e, locked: !anyLocked })))
if (onUserVisibilityChange) onUserVisibilityChange()
}}
title={ebenen.every(e => e.locked === true)
? 'Alle Ebenen entsperren'
+33 -2
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({
...ebene,
fill: {
@@ -107,7 +110,34 @@ export default function EbenenSettingsDialog({ ebene, hatchPatterns = ['Solid'],
return (
<div style={wrapperStyle}>
<div style={innerStyle}>
{/* Header */}
{/* Header embedded zeigt nur das Ebenen-Picker-Dropdown (kein
Title + kein Close, dafuer hat das Fenster seine native Title-
Bar). Modal-Variante zeigt den klassischen Header. */}
{embedded && pickerEbenen ? (
<div style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '8px 12px',
borderBottom: '1px solid var(--border)',
}}>
<span style={{ fontSize: 9, color: 'var(--text-muted)',
textTransform: 'uppercase', letterSpacing: 0.5 }}>
Ebene
</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',
@@ -122,6 +152,7 @@ export default function EbenenSettingsDialog({ ebene, hatchPatterns = ['Solid'],
</span>
<button onClick={onClose} style={{ color: 'var(--text-muted)', fontSize: 16, padding: '0 4px', lineHeight: 1 }}>×</button>
</div>
)}
{/* Body */}
<div style={{ padding: '6px 12px 4px', overflowY: 'auto' }}>
+85 -16
View File
@@ -1,23 +1,29 @@
import { useState } from 'react'
import Icon from './Icon'
import ContextMenu from './ContextMenu'
import { openGeschossSettings, openGeschossDialog } from '../lib/rhinoBridge'
function GeschossBadge({ name }) {
return <span className="chip chip-info">{name}</span>
}
function ZeichnungsebeneRow({ z, active, mode, onClick, onToggleVisible, onSettings }) {
// Eye-State auch fuer die aktive Zeichnungsebene anzeigen (User-Intention)
function ZeichnungsebeneRow({
z, active, mode, onClick, onContextMenu,
onToggleVisible, onToggleLock, onDelete,
}) {
const eyeShown = mode !== 'active'
const isGeschoss = !!z.isGeschoss
return (
<div
onClick={onClick}
onContextMenu={onContextMenu}
style={{
display: 'flex', alignItems: 'center', gap: 8,
display: 'flex', alignItems: 'center', gap: 6,
padding: '4px 12px',
margin: active ? '1px 6px' : '0',
background: active ? 'var(--active-dim)' : 'var(--bg-item)',
// Pill-Form fuer die aktive Zeichnungsebene
background: active ? 'var(--active-dim)'
: (z.visible !== false) ? 'var(--bg-item)'
: 'var(--bg-panel)',
borderRadius: active ? 999 : 0,
borderLeft: active ? 'none' : '3px solid transparent',
borderBottom: active ? 'none' : '1px solid var(--border-light)',
@@ -31,7 +37,7 @@ function ZeichnungsebeneRow({ z, active, mode, onClick, onToggleVisible, onSetti
fontWeight: active ? 700 : 500,
fontSize: 12,
color: active ? 'var(--active-light)' : 'var(--text-label)',
flex: 1,
flex: 1, minWidth: 0,
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>{z.name}</span>
@@ -65,9 +71,16 @@ function ZeichnungsebeneRow({ z, active, mode, onClick, onToggleVisible, onSetti
<button
className="btn-icon-xs"
onClick={(ev) => { ev.stopPropagation(); onSettings() }}
title="Einstellungen"
><Icon name="settings" size={12} /></button>
onClick={(ev) => { ev.stopPropagation(); onToggleLock() }}
title={z.locked ? 'Entsperren' : 'Sperren'}
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>
)
}
@@ -83,8 +96,7 @@ export default function GeschossManager({
zeichnungsebenen, activeId, onActiveChange, onChange, recalcOkff,
mode, onModeChange,
}) {
// dialogOpen-State entfaellt Bearbeiten-Dialog laeuft jetzt als
// Satelliten-Fenster via openGeschossDialog().
const [ctxMenu, setCtxMenu] = useState(null) // { x, y, id }
const sorted = [...zeichnungsebenen].reverse()
const gesamthoehe = zeichnungsebenen
@@ -93,9 +105,8 @@ export default function GeschossManager({
const addQuick = () => {
// Standard: NICHT-Geschoss-Zeichnungsebene (z.B. Möblierung, Bemassung,
// Plangrafik etc.). User kann via Row-Settings-Cog auf Geschoss
// umschalten, oder via Bearbeiten-Dialog (Pencil) ein Geschoss
// direkt erstellen.
// Plangrafik etc.). User kann via Row-Kontextmenue auf Geschoss
// umschalten oder via Bearbeiten-Dialog (Pencil) ein Geschoss erstellen.
const nonGeschossCount = zeichnungsebenen.filter(z => !z.isGeschoss).length
const newZ = {
id: `z_${Date.now()}`,
@@ -103,7 +114,6 @@ export default function GeschossManager({
isGeschoss: false,
visible: true,
}
console.log('[ZEICHNUNGSEBENEN-UI] addQuick →', { newZ, countBefore: zeichnungsebenen.length })
onChange([...zeichnungsebenen, newZ])
}
@@ -111,6 +121,56 @@ export default function GeschossManager({
onChange(zeichnungsebenen.map(z => z.id === id ? { ...z, visible: !(z.visible !== false) } : z))
}
const toggleLock = (id) => {
onChange(zeichnungsebenen.map(z => z.id === id ? { ...z, locked: !z.locked } : 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 (
<>
<div style={{
@@ -159,12 +219,21 @@ export default function GeschossManager({
active={z.id === activeId}
mode={mode}
onClick={() => onActiveChange(z.id)}
onContextMenu={(ev) => openContextMenu(ev, z.id)}
onToggleVisible={() => toggleVisible(z.id)}
onSettings={() => openGeschossSettings(z)}
onToggleLock={() => toggleLock(z.id)}
onDelete={() => remove(z.id)}
/>
))}
</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 saveLayerPreset(name, layers) { send('SAVE_PRESET', { name, layers }) }
export function deleteLayerPreset(name) { send('DELETE_PRESET', { name }) }
export function openAusschnittSettings(id) { send('OPEN_SETTINGS', { id }) }
// --- Gestaltung-Panel ---
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 saveOverridesPreset(name) { send('SAVE_OVERRIDES_PRESET', { name }) }
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 sendKeys(text, enter) { send('SEND_KEYS', { text, enter: enter !== false }) }
export function cancelCommand() { send('CANCEL_COMMAND', {}) }
@@ -232,6 +238,7 @@ export function setPageSize(id, format, landscape, customWidth, customHeight) {
customWidth, customHeight })
}
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 exportPdfMany(ids, dpi) { send('EXPORT_PDF', { ids, dpi: dpi || 300 }) }
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 eList = Array.isArray(a.ebenen) ? a.ebenen : []
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 => ({
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 EbenenSettingsApp from './EbenenSettingsApp.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 AusschnitteApp from './AusschnitteApp.jsx'
import MassstabApp from './MassstabApp.jsx'
@@ -30,6 +33,9 @@ const RootApp = mode === 'gestaltung' ? GestaltungApp
: mode === 'geschoss_settings' ? GeschossSettingsApp
: mode === 'ebenen_settings' ? EbenenSettingsApp
: mode === 'geschoss_dialog' ? GeschossDialogApp
: mode === 'layer_combinations' ? LayerCombinationsApp
: mode === 'ausschnitt_settings' ? AusschnittSettingsApp
: mode === 'layout_dialog' ? LayoutDialogApp
: App
window.onerror = function (msg, src, line, col, err) {