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:
+18
-18
@@ -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
@@ -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"))
|
||||
|
||||
@@ -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
@@ -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
@@ -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"))
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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"))
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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()}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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' }}>
|
||||
|
||||
@@ -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
@@ -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,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) {
|
||||
|
||||
Reference in New Issue
Block a user