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
|
// src/App.jsx des Rhino-Panels bleiben — wenn der User keine eigene Schema
|
||||||
// definiert, schickt das Plugin diese hier als FIRST_RUN-Default.
|
// definiert, schickt das Plugin diese hier als FIRST_RUN-Default.
|
||||||
const DEFAULT_LAYER_SCHEMA = [
|
const DEFAULT_LAYER_SCHEMA = [
|
||||||
{ code: '00', name: 'RASTER', color: '#484850', lw: 0.13 },
|
{ code: '00', name: 'Raster', color: '#484850', lw: 0.13 },
|
||||||
{ code: '01', name: 'VERMESSUNG', color: '#707078', lw: 0.18 },
|
{ code: '01', name: 'Vermessung', color: '#707078', lw: 0.18 },
|
||||||
{ code: '10', name: 'SITUATION', color: '#909090', lw: 0.18 },
|
{ code: '10', name: 'Situation', color: '#909090', lw: 0.18 },
|
||||||
{ code: '11', name: 'STRASSE', color: '#a89070', lw: 0.18 },
|
{ code: '11', name: 'Strasse', color: '#a89070', lw: 0.18 },
|
||||||
{ code: '12', name: 'GEBAEUDE', color: '#888888', lw: 0.25 },
|
{ code: '12', name: 'Gebaeude', color: '#888888', lw: 0.25 },
|
||||||
{ code: '13', name: 'BAEUME', color: '#50a050', lw: 0.13 },
|
{ code: '13', name: 'Baeume', color: '#50a050', lw: 0.13 },
|
||||||
{ code: '14', name: 'HOEHENLINIEN', color: '#909050', lw: 0.18 },
|
{ code: '14', name: 'Hoehenlinien', color: '#909050', lw: 0.18 },
|
||||||
{ code: '20', name: 'WAENDE', color: '#0a0a0a', lw: 0.50 },
|
{ code: '20', name: 'Waende', color: '#0a0a0a', lw: 0.50 },
|
||||||
{ code: '21', name: 'TUEREN_FENSTER', color: '#5080c8', lw: 0.25 },
|
{ code: '21', name: 'Tueren_Fenster', color: '#5080c8', lw: 0.25 },
|
||||||
{ code: '22', name: 'MOEBEL', color: '#909090', lw: 0.13 },
|
{ code: '22', name: 'Moebel', color: '#909090', lw: 0.13 },
|
||||||
{ code: '25', name: 'STUETZEN', color: '#c87050', lw: 0.50 },
|
{ code: '25', name: 'Stuetzen', color: '#c87050', lw: 0.50 },
|
||||||
{ code: '30', name: 'DECKEN', color: '#605850', lw: 0.35 },
|
{ code: '30', name: 'Decken', color: '#605850', lw: 0.35 },
|
||||||
{ code: '31', name: 'DAECHER', color: '#7a4a3a', lw: 0.35 },
|
{ code: '31', name: 'Daecher', color: '#7a4a3a', lw: 0.35 },
|
||||||
{ code: '35', name: 'TRAEGER', color: '#a87858', lw: 0.50 },
|
{ code: '35', name: 'Traeger', color: '#a87858', lw: 0.50 },
|
||||||
{ code: '50', name: 'TEXT', color: '#d0d0d0', lw: 0.13 },
|
{ code: '50', name: 'Text', color: '#d0d0d0', lw: 0.13 },
|
||||||
{ code: '60', name: 'PLANGRAFIK', color: '#c0a040', lw: 0.13 },
|
{ code: '60', name: 'Plangrafik', color: '#c0a040', lw: 0.13 },
|
||||||
{ code: '90', name: 'REFERENZEN', color: '#585860', lw: 0.13 },
|
{ code: '90', name: 'Referenzen', color: '#585860', lw: 0.13 },
|
||||||
{ code: '99', name: 'KONSTRUKTION', color: '#404048', lw: 0.13 },
|
{ code: '99', name: 'Konstruktion', color: '#404048', lw: 0.13 },
|
||||||
]
|
]
|
||||||
|
|
||||||
// Built-in Presets — nicht in den Settings gespeichert, immer verfuegbar.
|
// Built-in Presets — nicht in den Settings gespeichert, immer verfuegbar.
|
||||||
|
|||||||
+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 == "UPDATE_LAYERS": self._update_layers(p.get("id"), p.get("layers") or [])
|
||||||
elif t == "SAVE_PRESET": self._save_preset(p.get("name"), p.get("layers") or [])
|
elif t == "SAVE_PRESET": self._save_preset(p.get("name"), p.get("layers") or [])
|
||||||
elif t == "DELETE_PRESET": self._delete_preset(p.get("name"))
|
elif t == "DELETE_PRESET": self._delete_preset(p.get("name"))
|
||||||
|
elif t == "OPEN_SETTINGS": self._open_settings_window(p.get("id"))
|
||||||
|
|
||||||
def _load(self, doc):
|
def _load(self, doc):
|
||||||
raw = doc.Strings.GetValue(_STORE_KEY)
|
raw = doc.Strings.GetValue(_STORE_KEY)
|
||||||
@@ -537,8 +538,42 @@ class AusschnittBridge(panel_base.BaseBridge):
|
|||||||
if view is None: return
|
if view is None: return
|
||||||
vp = view.ActiveViewport
|
vp = view.ActiveViewport
|
||||||
_apply_camera(vp, snap.get("camera"))
|
_apply_camera(vp, snap.get("camera"))
|
||||||
|
# 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", []))
|
_apply_layers_global(doc, snap.get("layers", []))
|
||||||
|
else:
|
||||||
|
_apply_layers_global(doc, snap.get("layers", []))
|
||||||
|
# Eigene Sichtbarkeit → active_comb_name clearen
|
||||||
|
try:
|
||||||
|
import rhinopanel
|
||||||
|
rhinopanel.set_active_comb_name(doc, None)
|
||||||
|
rhinopanel._notify_oberleiste_combs()
|
||||||
|
except Exception: pass
|
||||||
_apply_dossier_state(doc, snap.get("dossier") or snap.get("pause") or {})
|
_apply_dossier_state(doc, snap.get("dossier") or snap.get("pause") or {})
|
||||||
|
# Overrides: nur anwenden wenn das Snap "applyOverrides" gesetzt hat.
|
||||||
|
# Sonst bleibt der aktuelle User-Override-State unangetastet.
|
||||||
|
if snap.get("applyOverrides"):
|
||||||
|
try:
|
||||||
|
import overrides
|
||||||
|
overrides.set_enabled(doc, bool(snap.get("overridesEnabled")))
|
||||||
|
overrides.set_active_preset(doc, snap.get("overridesPreset") or None)
|
||||||
|
# Oberleiste-Cache invalidieren damit Topbar das neue Preset zeigt
|
||||||
|
try:
|
||||||
|
b = sc.sticky.get("oberleiste_bridge")
|
||||||
|
if b is not None:
|
||||||
|
b._cached_overrides = None
|
||||||
|
b._send_state(force=True)
|
||||||
|
except Exception: pass
|
||||||
|
except Exception as ex:
|
||||||
|
print("[AUSSCHNITTE] overrides-apply:", ex)
|
||||||
# Viewport ZUERST umbenennen — der per-Viewport-Massstab in massstab.py
|
# Viewport ZUERST umbenennen — der per-Viewport-Massstab in massstab.py
|
||||||
# wird unter vp.Name geschluesselt. Erst nach dem Rename schreibt
|
# wird unter vp.Name geschluesselt. Erst nach dem Rename schreibt
|
||||||
# _apply_scale unter dem neuen Namen, sonst landet der Wert beim alten
|
# _apply_scale unter dem neuen Namen, sonst landet der Wert beim alten
|
||||||
@@ -703,6 +738,120 @@ class AusschnittBridge(panel_base.BaseBridge):
|
|||||||
self._store(doc, snaps)
|
self._store(doc, snaps)
|
||||||
self._send_list()
|
self._send_list()
|
||||||
|
|
||||||
|
def _open_settings_window(self, snap_id):
|
||||||
|
"""Oeffnet ein Satelliten-Fenster (Eto.Form + WebView) mit dem
|
||||||
|
Ausschnittseinstellungen-Dialog. Lets User editieren: Massstab,
|
||||||
|
Display-Mode, Overrides, Ebenenkombi."""
|
||||||
|
if not snap_id: return
|
||||||
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
snap = next((s for s in self._load(doc) if s.get("id") == snap_id), None)
|
||||||
|
if not snap:
|
||||||
|
print("[AUSSCHNITTE] open_settings: snap nicht gefunden", snap_id)
|
||||||
|
return
|
||||||
|
outer = self
|
||||||
|
bridge_holder = {"form": None, "id": snap_id}
|
||||||
|
|
||||||
panel_base.register_and_open("ausschnitte", "AUSSCHNITTE", PANEL_GUID_STR, AusschnittBridge,
|
def _payload():
|
||||||
|
d = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
sn = next((s for s in outer._load(d) if s.get("id") == bridge_holder["id"]), None)
|
||||||
|
if sn is None: sn = {}
|
||||||
|
# Listen fuer Dropdowns
|
||||||
|
display_modes = []
|
||||||
|
try:
|
||||||
|
import oberleiste
|
||||||
|
display_modes = oberleiste._list_display_modes()
|
||||||
|
except Exception as ex:
|
||||||
|
print("[AUSSCHNITTE] display_modes:", ex)
|
||||||
|
overrides_presets = []
|
||||||
|
try:
|
||||||
|
import overrides
|
||||||
|
overrides_presets = [item.get("name") for item in overrides.list_presets() if item.get("name")]
|
||||||
|
except Exception as ex:
|
||||||
|
print("[AUSSCHNITTE] overrides_presets:", ex)
|
||||||
|
layer_kombis = []
|
||||||
|
try:
|
||||||
|
import rhinopanel
|
||||||
|
layer_kombis = rhinopanel.list_layer_preset_names(d)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[AUSSCHNITTE] layer_kombis:", ex)
|
||||||
|
cam = sn.get("camera") or {}
|
||||||
|
return {
|
||||||
|
"snap": {
|
||||||
|
"id": sn.get("id"),
|
||||||
|
"name": sn.get("name"),
|
||||||
|
"scale": sn.get("scale", ""),
|
||||||
|
"displayMode": cam.get("displayMode"),
|
||||||
|
"displayModeName": cam.get("displayModeName"),
|
||||||
|
"applyOverrides": bool(sn.get("applyOverrides", False)),
|
||||||
|
"overridesEnabled": bool(sn.get("overridesEnabled", False)),
|
||||||
|
"overridesPreset": sn.get("overridesPreset") or "",
|
||||||
|
"layerCombination": sn.get("layerCombination") or "",
|
||||||
|
},
|
||||||
|
"displayModes": display_modes,
|
||||||
|
"overridesPresets": overrides_presets,
|
||||||
|
"layerKombis": layer_kombis,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _persist(settings):
|
||||||
|
d = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
snaps = outer._load(d)
|
||||||
|
sid = bridge_holder["id"]
|
||||||
|
target = next((s for s in snaps if s.get("id") == sid), None)
|
||||||
|
if target is None:
|
||||||
|
print("[AUSSCHNITTE] persist settings: snap weg"); return
|
||||||
|
# Massstab
|
||||||
|
sc_val = (settings.get("scale") or "").strip()
|
||||||
|
target["scale"] = sc_val
|
||||||
|
# Display Mode in camera nested
|
||||||
|
cam = target.get("camera") or {}
|
||||||
|
dm_id = settings.get("displayMode")
|
||||||
|
dm_nm = settings.get("displayModeName")
|
||||||
|
if dm_id is not None: cam["displayMode"] = dm_id or None
|
||||||
|
if dm_nm is not None: cam["displayModeName"] = dm_nm or None
|
||||||
|
target["camera"] = cam
|
||||||
|
# Overrides
|
||||||
|
target["applyOverrides"] = bool(settings.get("applyOverrides"))
|
||||||
|
target["overridesEnabled"] = bool(settings.get("overridesEnabled"))
|
||||||
|
target["overridesPreset"] = (settings.get("overridesPreset") or "").strip()
|
||||||
|
# Ebenenkombi
|
||||||
|
target["layerCombination"] = (settings.get("layerCombination") or "").strip()
|
||||||
|
outer._store(d, snaps)
|
||||||
|
outer._send_list()
|
||||||
|
print("[AUSSCHNITTE] Settings fuer '{}' aktualisiert".format(target.get("name")))
|
||||||
|
|
||||||
|
class _AusschnittSettingsBridge(panel_base.BaseBridge):
|
||||||
|
def __init__(self):
|
||||||
|
panel_base.BaseBridge.__init__(self, "ausschnitt_settings")
|
||||||
|
def _on_ready(self):
|
||||||
|
self._send_state()
|
||||||
|
def _send_state(self):
|
||||||
|
self.send("AUSSCHNITT_SETTINGS_STATE", _payload())
|
||||||
|
def handle(self, data):
|
||||||
|
if not isinstance(data, dict): return
|
||||||
|
t = data.get("type", "")
|
||||||
|
p = data.get("payload") or {}
|
||||||
|
if t == "READY":
|
||||||
|
self._on_ready()
|
||||||
|
elif t == "SAVE":
|
||||||
|
_persist(p.get("settings") or {})
|
||||||
|
try:
|
||||||
|
f = bridge_holder.get("form")
|
||||||
|
if f is not None: f.Close()
|
||||||
|
except Exception: pass
|
||||||
|
elif t == "CANCEL":
|
||||||
|
try:
|
||||||
|
f = bridge_holder.get("form")
|
||||||
|
if f is not None: f.Close()
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
b = _AusschnittSettingsBridge()
|
||||||
|
bridge_holder["form"] = panel_base.open_satellite_window(
|
||||||
|
"ausschnitt_settings",
|
||||||
|
params=_payload(),
|
||||||
|
title="Ausschnitt: {}".format(snap.get("name", "")),
|
||||||
|
size=(420, 540),
|
||||||
|
bridge=b)
|
||||||
|
|
||||||
|
|
||||||
|
panel_base.register_and_open("ausschnitte", "Ausschnitte", PANEL_GUID_STR, AusschnittBridge,
|
||||||
icon_spec=("crop", "#c87050"))
|
icon_spec=("crop", "#c87050"))
|
||||||
|
|||||||
@@ -608,6 +608,6 @@ def _bridge_factory():
|
|||||||
return b
|
return b
|
||||||
|
|
||||||
|
|
||||||
panel_base.register_and_open("dimensionen", "DIMENSIONEN", PANEL_GUID_STR,
|
panel_base.register_and_open("dimensionen", "Dimensionen", PANEL_GUID_STR,
|
||||||
_bridge_factory,
|
_bridge_factory,
|
||||||
icon_spec=("aspect_ratio", "#9e7050"))
|
icon_spec=("aspect_ratio", "#9e7050"))
|
||||||
|
|||||||
+20
-3
@@ -366,9 +366,9 @@ def _find_ebene_sublayer_name(doc, keywords, default_code, default_name,
|
|||||||
|
|
||||||
|
|
||||||
def _layer_path_axis(doc, geschoss_name):
|
def _layer_path_axis(doc, geschoss_name):
|
||||||
"""Wand-Achse + Volumen — Sublayer 'WÄNDE' (Code 20)."""
|
"""Wand-Achse + Volumen — Sublayer 'Wände' (Code 20)."""
|
||||||
sub = _find_ebene_sublayer_name(doc, ["wand", "wände", "waende"],
|
sub = _find_ebene_sublayer_name(doc, ["wand", "wände", "waende"],
|
||||||
"20", "WÄNDE",
|
"20", "Wände",
|
||||||
default_color="#0a0a0a", default_lw=0.50)
|
default_color="#0a0a0a", default_lw=0.50)
|
||||||
return "{}::{}".format(geschoss_name, sub)
|
return "{}::{}".format(geschoss_name, sub)
|
||||||
|
|
||||||
@@ -4543,6 +4543,23 @@ class ElementeBridge(panel_base.BaseBridge):
|
|||||||
elif t == "DELETE_ELEMENT": self._delete_wall(p.get("id"))
|
elif t == "DELETE_ELEMENT": self._delete_wall(p.get("id"))
|
||||||
elif t == "REGENERATE_ALL": self._regenerate_all()
|
elif t == "REGENERATE_ALL": self._regenerate_all()
|
||||||
|
|
||||||
|
def _notify_active_geschoss(self):
|
||||||
|
"""Schlanker Partial-Push: nur activeGeschoss + activeGeschossName.
|
||||||
|
Wird vom Ebenen-Bridge bei Geschoss-Wechsel gerufen — die Element-
|
||||||
|
Liste ist davon nicht betroffen, ein voller _send_state mit Re-
|
||||||
|
Enumeration aller Smart-Elemente (200+ in echten Projekten) waere
|
||||||
|
teuer und unnoetig. React-State macht Shallow-Merge, der Rest des
|
||||||
|
States bleibt."""
|
||||||
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
if doc is None: return
|
||||||
|
try:
|
||||||
|
self.send("STATE", {
|
||||||
|
"activeGeschoss": _active_geschoss_id(doc),
|
||||||
|
"activeGeschossName": _active_geschoss_name(doc),
|
||||||
|
})
|
||||||
|
except Exception as ex:
|
||||||
|
print("[ELEMENTE] _notify_active_geschoss:", ex)
|
||||||
|
|
||||||
def _send_state(self):
|
def _send_state(self):
|
||||||
doc = Rhino.RhinoDoc.ActiveDoc
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
if doc is None:
|
if doc is None:
|
||||||
@@ -8509,6 +8526,6 @@ def _bridge_factory():
|
|||||||
return b
|
return b
|
||||||
|
|
||||||
|
|
||||||
panel_base.register_and_open("elemente", "ELEMENTE", PANEL_GUID_STR,
|
panel_base.register_and_open("elemente", "Elemente", PANEL_GUID_STR,
|
||||||
_bridge_factory,
|
_bridge_factory,
|
||||||
icon_spec=("foundation", "#5fa896"))
|
icon_spec=("foundation", "#5fa896"))
|
||||||
|
|||||||
+1
-1
@@ -1695,5 +1695,5 @@ def _bridge_factory():
|
|||||||
return b
|
return b
|
||||||
|
|
||||||
|
|
||||||
panel_base.register_and_open("gestaltung", "GESTALTUNG", PANEL_GUID_STR, _bridge_factory,
|
panel_base.register_and_open("gestaltung", "Gestaltung", PANEL_GUID_STR, _bridge_factory,
|
||||||
icon_spec=("palette", "#5fa896"))
|
icon_spec=("palette", "#5fa896"))
|
||||||
|
|||||||
@@ -578,6 +578,7 @@ def apply_visibility(doc, zeichnungsebenen, ebenen, active_z_id, active_code, z_
|
|||||||
children = children_by_parent.get(parent.Id, [])
|
children = children_by_parent.get(parent.Id, [])
|
||||||
is_active_z = z["id"] == active_z_id
|
is_active_z = z["id"] == active_z_id
|
||||||
z_visible_flag = z.get("visible", True)
|
z_visible_flag = z.get("visible", True)
|
||||||
|
z_locked_flag = bool(z.get("locked", False))
|
||||||
|
|
||||||
# Z-Mode -> Parent-Zustand
|
# Z-Mode -> Parent-Zustand
|
||||||
if is_active_z:
|
if is_active_z:
|
||||||
@@ -593,6 +594,11 @@ def apply_visibility(doc, zeichnungsebenen, ebenen, active_z_id, active_code, z_
|
|||||||
else: # grey
|
else: # grey
|
||||||
p_vis, p_grey, p_lock = True, True, False
|
p_vis, p_grey, p_lock = True, True, False
|
||||||
|
|
||||||
|
# Per-Z explizites Sperren ueberlagert (auch fuer die aktive Z) — wer
|
||||||
|
# eine Geschoss-Ebene sperrt, will dass Klicks ins Leere gehen.
|
||||||
|
if z_locked_flag:
|
||||||
|
p_lock = True
|
||||||
|
|
||||||
parent_changed = False
|
parent_changed = False
|
||||||
if parent.IsVisible != p_vis:
|
if parent.IsVisible != p_vis:
|
||||||
parent.IsVisible = p_vis
|
parent.IsVisible = p_vis
|
||||||
|
|||||||
+65
-1
@@ -281,6 +281,7 @@ class LayoutsBridge(panel_base.BaseBridge):
|
|||||||
elif t == "ADD_FOLDER": self._add_folder(p.get("name"))
|
elif t == "ADD_FOLDER": self._add_folder(p.get("name"))
|
||||||
elif t == "REMOVE_FOLDER": self._remove_folder(p.get("name"))
|
elif t == "REMOVE_FOLDER": self._remove_folder(p.get("name"))
|
||||||
elif t == "SET_FOLDER": self._set_folder(p.get("id"), p.get("folder") or "")
|
elif t == "SET_FOLDER": self._set_folder(p.get("id"), p.get("folder") or "")
|
||||||
|
elif t == "OPEN_LAYOUT_DIALOG": self._open_layout_dialog(p)
|
||||||
|
|
||||||
# --- State-Snapshot -----------------------------------------------------
|
# --- State-Snapshot -----------------------------------------------------
|
||||||
|
|
||||||
@@ -737,6 +738,69 @@ class LayoutsBridge(panel_base.BaseBridge):
|
|||||||
print("[LAYOUTS] sync layout:", ex)
|
print("[LAYOUTS] sync layout:", ex)
|
||||||
self._send_state()
|
self._send_state()
|
||||||
|
|
||||||
|
def _open_layout_dialog(self, p):
|
||||||
|
"""Oeffnet ein Satelliten-Fenster mit dem Layout-Erstellen/Bearbeiten
|
||||||
|
Dialog. mode = 'new' | 'edit'. Bei 'edit' wird `layout` (id, name,
|
||||||
|
width, height) mitgeschickt."""
|
||||||
|
outer = self
|
||||||
|
mode = (p.get("mode") or "new")
|
||||||
|
layout = p.get("layout") or None
|
||||||
|
bridge_holder = {"form": None}
|
||||||
|
|
||||||
|
def _apply(payload):
|
||||||
|
if mode == "new":
|
||||||
|
outer._new_layout({
|
||||||
|
"name": payload.get("name") or "",
|
||||||
|
"format": payload.get("format") or "A3",
|
||||||
|
"landscape": bool(payload.get("landscape", True)),
|
||||||
|
"customWidth": payload.get("customWidth"),
|
||||||
|
"customHeight": payload.get("customHeight"),
|
||||||
|
})
|
||||||
|
elif mode == "edit" and layout and layout.get("id"):
|
||||||
|
outer._set_page_size({
|
||||||
|
"id": layout.get("id"),
|
||||||
|
"format": payload.get("format") or "A3",
|
||||||
|
"landscape": bool(payload.get("landscape", True)),
|
||||||
|
"customWidth": payload.get("customWidth"),
|
||||||
|
"customHeight": payload.get("customHeight"),
|
||||||
|
})
|
||||||
|
|
||||||
|
class _LayoutDialogBridge(panel_base.BaseBridge):
|
||||||
|
def __init__(self):
|
||||||
|
panel_base.BaseBridge.__init__(self, "layout_dialog")
|
||||||
|
def _on_ready(self):
|
||||||
|
self.send("LAYOUT_DIALOG_STATE", {
|
||||||
|
"mode": mode,
|
||||||
|
"layout": layout,
|
||||||
|
})
|
||||||
|
def handle(self, data):
|
||||||
|
if not isinstance(data, dict): return
|
||||||
|
t = data.get("type", "")
|
||||||
|
pp = data.get("payload") or {}
|
||||||
|
if t == "READY":
|
||||||
|
self._on_ready()
|
||||||
|
elif t == "SAVE":
|
||||||
|
_apply(pp)
|
||||||
|
try:
|
||||||
|
f = bridge_holder.get("form")
|
||||||
|
if f is not None: f.Close()
|
||||||
|
except Exception: pass
|
||||||
|
elif t == "CANCEL":
|
||||||
|
try:
|
||||||
|
f = bridge_holder.get("form")
|
||||||
|
if f is not None: f.Close()
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
b = _LayoutDialogBridge()
|
||||||
|
title = "Neues Layout" if mode == "new" else "Papierformat: {}".format(
|
||||||
|
(layout or {}).get("name", ""))
|
||||||
|
bridge_holder["form"] = panel_base.open_satellite_window(
|
||||||
|
"layout_dialog",
|
||||||
|
params={"mode": mode, "layout": layout},
|
||||||
|
title=title,
|
||||||
|
size=(440, 380),
|
||||||
|
bridge=b)
|
||||||
|
|
||||||
|
|
||||||
def _bridge_factory():
|
def _bridge_factory():
|
||||||
b = LayoutsBridge()
|
b = LayoutsBridge()
|
||||||
@@ -744,6 +808,6 @@ def _bridge_factory():
|
|||||||
return b
|
return b
|
||||||
|
|
||||||
|
|
||||||
panel_base.register_and_open("layouts", "LAYOUTS", PANEL_GUID_STR,
|
panel_base.register_and_open("layouts", "Layouts", PANEL_GUID_STR,
|
||||||
_bridge_factory,
|
_bridge_factory,
|
||||||
icon_spec=("view_quilt", "#7a5fa8"))
|
icon_spec=("view_quilt", "#7a5fa8"))
|
||||||
|
|||||||
+1
-1
@@ -1090,7 +1090,7 @@ def _bridge_factory():
|
|||||||
# register_standalone_panel() aufrufen oder die Zeile darunter auskommentieren.
|
# register_standalone_panel() aufrufen oder die Zeile darunter auskommentieren.
|
||||||
|
|
||||||
def register_standalone_panel():
|
def register_standalone_panel():
|
||||||
panel_base.register_and_open("massstab", "MASSSTAB", PANEL_GUID_STR, _bridge_factory,
|
panel_base.register_and_open("massstab", "Massstab", PANEL_GUID_STR, _bridge_factory,
|
||||||
icon_spec=("straighten", "#c87050"))
|
icon_spec=("straighten", "#c87050"))
|
||||||
|
|
||||||
# register_standalone_panel() # auskommentiert — Standard ist OBERLEISTE
|
# register_standalone_panel() # auskommentiert — Standard ist OBERLEISTE
|
||||||
|
|||||||
+44
-1
@@ -20,6 +20,7 @@ if _HERE not in sys.path:
|
|||||||
import panel_base
|
import panel_base
|
||||||
import massstab
|
import massstab
|
||||||
import overrides
|
import overrides
|
||||||
|
import rhinopanel
|
||||||
|
|
||||||
PANEL_GUID_STR = "7e1f6a5b-8e2f-4f3c-d5e6-f70819203b51"
|
PANEL_GUID_STR = "7e1f6a5b-8e2f-4f3c-d5e6-f70819203b51"
|
||||||
OVERRIDES_PANEL_GUID_STR = "8f2a7b6c-9f3a-4f4d-e6f7-08192a3c4d62"
|
OVERRIDES_PANEL_GUID_STR = "8f2a7b6c-9f3a-4f4d-e6f7-08192a3c4d62"
|
||||||
@@ -770,6 +771,7 @@ class OberleisteBridge(panel_base.BaseBridge):
|
|||||||
self._last_prompt = ""
|
self._last_prompt = ""
|
||||||
self._last_state_sig = None # Fingerprint des letzten Push — dedupe
|
self._last_state_sig = None # Fingerprint des letzten Push — dedupe
|
||||||
self._cached_overrides = None # (enabled, count) — invalidiert bei Toggle/Update
|
self._cached_overrides = None # (enabled, count) — invalidiert bei Toggle/Update
|
||||||
|
self._cached_combinations = None # (names, active) — invalidiert bei jeder Comb-Aenderung
|
||||||
# Command-Liste einmalig laden (kann teuer sein -> cachen)
|
# Command-Liste einmalig laden (kann teuer sein -> cachen)
|
||||||
try:
|
try:
|
||||||
self._all_commands = _list_all_command_names()
|
self._all_commands = _list_all_command_names()
|
||||||
@@ -920,6 +922,34 @@ class OberleisteBridge(panel_base.BaseBridge):
|
|||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[OBERLEISTE] open_as_window Overrides:", ex)
|
print("[OBERLEISTE] open_as_window Overrides:", ex)
|
||||||
|
|
||||||
|
# --- Ebenenkombinationen ----------------------------------------
|
||||||
|
elif t == "PICK_LAYER_COMBINATION":
|
||||||
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
name = (p.get("name") or "").strip()
|
||||||
|
if name:
|
||||||
|
rhinopanel.apply_layer_preset_by_name(doc, name)
|
||||||
|
else:
|
||||||
|
# "Eigene" — kein Apply, nur active_comb_name clearen
|
||||||
|
rhinopanel.set_active_comb_name(doc, None)
|
||||||
|
self._cached_combinations = None
|
||||||
|
self._send_state(force=True)
|
||||||
|
elif t == "SAVE_LAYER_COMBINATION":
|
||||||
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
name = (p.get("name") or "").strip()
|
||||||
|
if name:
|
||||||
|
rhinopanel.save_current_as_layer_preset(doc, name)
|
||||||
|
self._cached_combinations = None
|
||||||
|
self._send_state(force=True)
|
||||||
|
elif t == "DELETE_LAYER_COMBINATION":
|
||||||
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
rhinopanel.delete_layer_preset(doc, p.get("name") or "")
|
||||||
|
self._cached_combinations = None
|
||||||
|
self._send_state(force=True)
|
||||||
|
elif t == "OPEN_LAYER_COMBINATIONS_DIALOG":
|
||||||
|
try: rhinopanel.open_layer_combinations_window()
|
||||||
|
except Exception as ex:
|
||||||
|
print("[OBERLEISTE] open layer-combinations:", ex)
|
||||||
|
|
||||||
# --- Command-Line Integration -----------------------------------
|
# --- Command-Line Integration -----------------------------------
|
||||||
elif t == "RUN_COMMAND":
|
elif t == "RUN_COMMAND":
|
||||||
cmd = (p.get("cmd") or "").strip()
|
cmd = (p.get("cmd") or "").strip()
|
||||||
@@ -1035,6 +1065,18 @@ class OberleisteBridge(panel_base.BaseBridge):
|
|||||||
info["overridesActivePreset"],
|
info["overridesActivePreset"],
|
||||||
_presets_tuple) = self._cached_overrides
|
_presets_tuple) = self._cached_overrides
|
||||||
info["overridesPresets"] = list(_presets_tuple)
|
info["overridesPresets"] = list(_presets_tuple)
|
||||||
|
# Ebenenkombinationen — cached (Liste + active). Invalidiert bei
|
||||||
|
# PICK/SAVE/DELETE und durch Cross-Bridge-Notify aus rhinopanel.py.
|
||||||
|
if self._cached_combinations is None:
|
||||||
|
try:
|
||||||
|
names = tuple(rhinopanel.list_layer_preset_names(doc))
|
||||||
|
active = rhinopanel.get_active_comb_name(doc)
|
||||||
|
self._cached_combinations = (names, active)
|
||||||
|
except Exception:
|
||||||
|
self._cached_combinations = ((), None)
|
||||||
|
_names_tuple, _active_comb = self._cached_combinations
|
||||||
|
info["layerCombinations"] = list(_names_tuple)
|
||||||
|
info["layerCombinationActive"] = _active_comb
|
||||||
# Command-Line State
|
# Command-Line State
|
||||||
prompt = _get_command_prompt()
|
prompt = _get_command_prompt()
|
||||||
info["cmdPrompt"] = prompt
|
info["cmdPrompt"] = prompt
|
||||||
@@ -1057,6 +1099,7 @@ class OberleisteBridge(panel_base.BaseBridge):
|
|||||||
info["overridesEnabled"], info["overridesCount"],
|
info["overridesEnabled"], info["overridesCount"],
|
||||||
info.get("overridesActivePreset"),
|
info.get("overridesActivePreset"),
|
||||||
tuple(info.get("overridesPresets") or ()),
|
tuple(info.get("overridesPresets") or ()),
|
||||||
|
_names_tuple, _active_comb,
|
||||||
prompt,
|
prompt,
|
||||||
)
|
)
|
||||||
if not force and sig == self._last_state_sig:
|
if not force and sig == self._last_state_sig:
|
||||||
@@ -1197,5 +1240,5 @@ def _bridge_factory():
|
|||||||
return b
|
return b
|
||||||
|
|
||||||
|
|
||||||
panel_base.register_and_open("oberleiste", "OBERLEISTE", PANEL_GUID_STR, _bridge_factory,
|
panel_base.register_and_open("oberleiste", "Oberleiste", PANEL_GUID_STR, _bridge_factory,
|
||||||
icon_spec=("menu", "#2f5d54"))
|
icon_spec=("menu", "#2f5d54"))
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ def open_as_window():
|
|||||||
sc.sticky["overrides_bridge"] = b
|
sc.sticky["overrides_bridge"] = b
|
||||||
panel_base.open_satellite_window(
|
panel_base.open_satellite_window(
|
||||||
"overrides",
|
"overrides",
|
||||||
title="OVERRIDES",
|
title="Overrides",
|
||||||
size=(760, 580),
|
size=(760, 580),
|
||||||
bridge=b)
|
bridge=b)
|
||||||
|
|
||||||
|
|||||||
+480
-83
@@ -103,6 +103,243 @@ def _broadcast_state(doc=None, hatch_patterns=None):
|
|||||||
except Exception: pass
|
except Exception: pass
|
||||||
|
|
||||||
|
|
||||||
|
# --- Layer-Kombinationen: Modul-Level Helpers ------------------------------
|
||||||
|
# Diese Helfer werden sowohl von EbenenBridge (Ebenen-Panel) als auch von
|
||||||
|
# OberleisteBridge (Top-Bar) und LayerCombinationsBridge (Satelliten-Editor)
|
||||||
|
# benutzt. doc.Strings ist die einzige Quelle der Wahrheit; nach jedem Write
|
||||||
|
# rufen die Caller _broadcast_state(doc) + invalidate cross-bridge caches.
|
||||||
|
|
||||||
|
_PRESETS_KEY = "dossier_layer_presets"
|
||||||
|
_ACTIVE_COMB_KEY = "dossier_layer_active_comb"
|
||||||
|
|
||||||
|
|
||||||
|
def load_layer_presets(doc):
|
||||||
|
raw = doc.Strings.GetValue(_PRESETS_KEY)
|
||||||
|
if not raw: return []
|
||||||
|
try:
|
||||||
|
data = json.loads(raw)
|
||||||
|
return data if isinstance(data, list) else []
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def store_layer_presets(doc, presets):
|
||||||
|
try:
|
||||||
|
doc.Strings.SetString(_PRESETS_KEY,
|
||||||
|
json.dumps(presets, ensure_ascii=False))
|
||||||
|
except Exception as ex:
|
||||||
|
print("[EBENEN] store_layer_presets:", ex)
|
||||||
|
|
||||||
|
|
||||||
|
def get_active_comb_name(doc):
|
||||||
|
try:
|
||||||
|
v = doc.Strings.GetValue(_ACTIVE_COMB_KEY)
|
||||||
|
return v if v else None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def set_active_comb_name(doc, name):
|
||||||
|
try:
|
||||||
|
doc.Strings.SetString(_ACTIVE_COMB_KEY, name or "")
|
||||||
|
except Exception as ex:
|
||||||
|
print("[EBENEN] set_active_comb_name:", ex)
|
||||||
|
|
||||||
|
|
||||||
|
def list_layer_preset_names(doc):
|
||||||
|
return [p.get("name") for p in load_layer_presets(doc)
|
||||||
|
if isinstance(p, dict) and p.get("name")]
|
||||||
|
|
||||||
|
|
||||||
|
def _notify_oberleiste_combs():
|
||||||
|
"""Cache der Oberleiste invalidieren + force-send. Wird gerufen wenn
|
||||||
|
die Combinations-Liste oder activeCombName sich aendert."""
|
||||||
|
try:
|
||||||
|
b = sc.sticky.get("oberleiste_bridge")
|
||||||
|
if b is not None:
|
||||||
|
b._cached_combinations = None
|
||||||
|
b._send_state(force=True)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[EBENEN] notify oberleiste combs:", ex)
|
||||||
|
|
||||||
|
|
||||||
|
def _notify_layer_combinations_editor():
|
||||||
|
"""Satelliten-Fenster (Editor) informieren falls offen — Layer-/Preset-
|
||||||
|
Liste hat sich geaendert."""
|
||||||
|
try:
|
||||||
|
b = sc.sticky.get("layer_combinations_bridge")
|
||||||
|
if b is not None: b._send_state()
|
||||||
|
except Exception as ex:
|
||||||
|
print("[EBENEN] notify layer-combinations editor:", ex)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_layer_preset_by_name(doc, name):
|
||||||
|
"""Laedt Preset `name` und wendet es an. Setzt active_comb_name.
|
||||||
|
Liefert True wenn erfolgreich."""
|
||||||
|
if not name: return False
|
||||||
|
presets = load_layer_presets(doc)
|
||||||
|
preset = next((p for p in presets if p.get("name") == name), None)
|
||||||
|
if preset is None:
|
||||||
|
print("[EBENEN] apply_layer_preset_by_name: '{}' nicht gefunden".format(name))
|
||||||
|
return False
|
||||||
|
payload = {
|
||||||
|
"layers": preset.get("layers") or [],
|
||||||
|
"dossierEbenen": preset.get("dossierEbenen"),
|
||||||
|
"dossierZeichnungsebenen": preset.get("dossierZeichnungsebenen"),
|
||||||
|
}
|
||||||
|
# Routing: wenn die EbenenBridge existiert, delegiere — die hat den
|
||||||
|
# vollen Eye-State-Pfad inkl. STATE_SYNC + Redraw. Sonst inline applien.
|
||||||
|
eb = sc.sticky.get("ebenen_bridge_ref")
|
||||||
|
if eb is not None:
|
||||||
|
try:
|
||||||
|
eb._apply_combination(payload)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[EBENEN] apply via bridge:", ex)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
# Fallback: direkt doc.Strings + doc.Layer setzen (kein Bridge offen)
|
||||||
|
_apply_layer_preset_inline(doc, payload)
|
||||||
|
set_active_comb_name(doc, name)
|
||||||
|
_notify_oberleiste_combs()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_layer_preset_inline(doc, payload):
|
||||||
|
"""Fallback wenn keine EbenenBridge offen ist — minimaler Layer-State-
|
||||||
|
Pfad. Setzt doc.Strings + doc.Layer.IsVisible direkt."""
|
||||||
|
pe = payload.get("dossierEbenen")
|
||||||
|
pz = payload.get("dossierZeichnungsebenen")
|
||||||
|
try:
|
||||||
|
e_raw = doc.Strings.GetValue("dossier_ebenen") or "[]"
|
||||||
|
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen") or "[]"
|
||||||
|
e_list = json.loads(e_raw) or []
|
||||||
|
z_list = json.loads(z_raw) or []
|
||||||
|
if isinstance(pe, list):
|
||||||
|
by_code = {x.get("code"): x for x in pe if isinstance(x, dict) and x.get("code")}
|
||||||
|
for e in e_list:
|
||||||
|
if not isinstance(e, dict): continue
|
||||||
|
s = by_code.get(e.get("code"))
|
||||||
|
if s is None: continue
|
||||||
|
e["visible"] = bool(s.get("visible", True))
|
||||||
|
e["locked"] = bool(s.get("locked", False))
|
||||||
|
if isinstance(pz, list):
|
||||||
|
by_id = {x.get("id"): x for x in pz if isinstance(x, dict) and x.get("id")}
|
||||||
|
for z in z_list:
|
||||||
|
if not isinstance(z, dict): continue
|
||||||
|
s = by_id.get(z.get("id"))
|
||||||
|
if s is None: continue
|
||||||
|
z["visible"] = bool(s.get("visible", True))
|
||||||
|
doc.Strings.SetString("dossier_ebenen", json.dumps(e_list, ensure_ascii=False))
|
||||||
|
doc.Strings.SetString("dossier_zeichnungsebenen", json.dumps(z_list, ensure_ascii=False))
|
||||||
|
except Exception as ex:
|
||||||
|
print("[EBENEN] inline preset-apply (eye-state):", ex)
|
||||||
|
# Layer-ID-Pfad als Sekundaer (AUSSCHNITTE-Kompat)
|
||||||
|
layer_states = payload.get("layers") or []
|
||||||
|
if layer_states:
|
||||||
|
by_id = {}
|
||||||
|
try:
|
||||||
|
for layer in doc.Layers:
|
||||||
|
if not layer.IsDeleted: by_id[str(layer.Id)] = layer
|
||||||
|
except Exception: pass
|
||||||
|
_set_processing(True)
|
||||||
|
try:
|
||||||
|
for ls in layer_states:
|
||||||
|
layer = by_id.get(ls.get("id"))
|
||||||
|
if layer is None: continue
|
||||||
|
try:
|
||||||
|
want_vis = bool(ls.get("visible", True))
|
||||||
|
want_lck = bool(ls.get("locked", False))
|
||||||
|
if layer.IsVisible != want_vis: layer.IsVisible = want_vis
|
||||||
|
if layer.IsLocked != want_lck: layer.IsLocked = want_lck
|
||||||
|
except Exception: pass
|
||||||
|
finally:
|
||||||
|
_set_processing(False)
|
||||||
|
try: doc.Views.Redraw()
|
||||||
|
except Exception: pass
|
||||||
|
_broadcast_state(doc)
|
||||||
|
|
||||||
|
|
||||||
|
def save_current_as_layer_preset(doc, name):
|
||||||
|
"""Speichert die aktuellen Eye-States als Preset. Setzt active_comb_name."""
|
||||||
|
name = (name or "").strip()
|
||||||
|
if not name: return False
|
||||||
|
# 1) doc.Layer state (Kompat mit AUSSCHNITTE)
|
||||||
|
layers = []
|
||||||
|
try:
|
||||||
|
for layer in doc.Layers:
|
||||||
|
if layer is None or layer.IsDeleted: continue
|
||||||
|
layers.append({
|
||||||
|
"id": str(layer.Id),
|
||||||
|
"visible": bool(layer.IsVisible),
|
||||||
|
"locked": bool(layer.IsLocked),
|
||||||
|
})
|
||||||
|
except Exception as ex:
|
||||||
|
print("[EBENEN] save_current_as_layer_preset enum:", ex)
|
||||||
|
# 2) Eye-States aus dossier_ebenen / dossier_zeichnungsebenen
|
||||||
|
pe_state, pz_state = [], []
|
||||||
|
try:
|
||||||
|
e_raw = doc.Strings.GetValue("dossier_ebenen")
|
||||||
|
if e_raw:
|
||||||
|
for e in (json.loads(e_raw) or []):
|
||||||
|
if isinstance(e, dict) and e.get("code"):
|
||||||
|
pe_state.append({
|
||||||
|
"code": e["code"],
|
||||||
|
"visible": bool(e.get("visible", True)),
|
||||||
|
"locked": bool(e.get("locked", False)),
|
||||||
|
})
|
||||||
|
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
|
||||||
|
if z_raw:
|
||||||
|
for z in (json.loads(z_raw) or []):
|
||||||
|
if isinstance(z, dict) and z.get("id"):
|
||||||
|
pz_state.append({
|
||||||
|
"id": z["id"],
|
||||||
|
"visible": bool(z.get("visible", True)),
|
||||||
|
})
|
||||||
|
except Exception as ex:
|
||||||
|
print("[EBENEN] save_current_as_layer_preset eye-states:", ex)
|
||||||
|
presets = load_layer_presets(doc)
|
||||||
|
new_data = {
|
||||||
|
"name": name,
|
||||||
|
"layers": layers,
|
||||||
|
"dossierEbenen": pe_state,
|
||||||
|
"dossierZeichnungsebenen": pz_state,
|
||||||
|
}
|
||||||
|
existing = next((p for p in presets if p.get("name") == name), None)
|
||||||
|
if existing is not None:
|
||||||
|
existing.update(new_data)
|
||||||
|
else:
|
||||||
|
presets.append(new_data)
|
||||||
|
store_layer_presets(doc, presets)
|
||||||
|
set_active_comb_name(doc, name)
|
||||||
|
_notify_oberleiste_combs()
|
||||||
|
_notify_layer_combinations_editor()
|
||||||
|
print("[EBENEN] '{}' gespeichert: {} Layer + {} Ebenen Eye-State".format(
|
||||||
|
name, len(layers), len(pe_state)))
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def delete_layer_preset(doc, name):
|
||||||
|
name = (name or "").strip()
|
||||||
|
if not name: return False
|
||||||
|
presets = [p for p in load_layer_presets(doc) if p.get("name") != name]
|
||||||
|
store_layer_presets(doc, presets)
|
||||||
|
if get_active_comb_name(doc) == name:
|
||||||
|
set_active_comb_name(doc, None)
|
||||||
|
_notify_oberleiste_combs()
|
||||||
|
_notify_layer_combinations_editor()
|
||||||
|
print("[EBENEN] Kombination '{}' geloescht".format(name))
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def clear_active_comb_name(doc):
|
||||||
|
"""Wird vom EbenenBridge SET_VISIBILITY / APPLY-Pfad gerufen wenn der
|
||||||
|
User per Hand etwas am Layer-State aendert — dann passt das Preset nicht
|
||||||
|
mehr und wir markieren 'Eigene'."""
|
||||||
|
if get_active_comb_name(doc):
|
||||||
|
set_active_comb_name(doc, None)
|
||||||
|
_notify_oberleiste_combs()
|
||||||
|
|
||||||
|
|
||||||
class EbenenBridge(panel_base.BaseBridge):
|
class EbenenBridge(panel_base.BaseBridge):
|
||||||
"""Gemeinsame Bridge-Klasse fuer beide Panels (Ebenen + Zeichnungsebenen).
|
"""Gemeinsame Bridge-Klasse fuer beide Panels (Ebenen + Zeichnungsebenen).
|
||||||
Mode bestimmt nur welches WebView die Bridge bedient + welcher sticky-Slot
|
Mode bestimmt nur welches WebView die Bridge bedient + welcher sticky-Slot
|
||||||
@@ -266,40 +503,80 @@ class EbenenBridge(panel_base.BaseBridge):
|
|||||||
on_save=on_save)
|
on_save=on_save)
|
||||||
|
|
||||||
def _open_ebenen_settings(self, ebene, hatch_patterns):
|
def _open_ebenen_settings(self, ebene, hatch_patterns):
|
||||||
"""Oeffnet ein echtes Rhino-Fenster mit dem EbenenSettingsDialog."""
|
"""Oeffnet ein echtes Rhino-Fenster mit dem EbenenSettingsDialog.
|
||||||
|
Mit Dropdown zum Wechsel zwischen Ebenen; jeder Switch persistiert
|
||||||
|
die aktuelle Ebene live (SAVE_KEEP), Schliess-/Übernehmen-Knopf
|
||||||
|
persistiert + schliesst (SAVE)."""
|
||||||
if not isinstance(ebene, dict) or not ebene.get("code"):
|
if not isinstance(ebene, dict) or not ebene.get("code"):
|
||||||
print("[EBENEN] open_ebenen_settings: kein Ebene-Payload")
|
print("[EBENEN] open_ebenen_settings: kein Ebene-Payload")
|
||||||
return
|
return
|
||||||
old_code = ebene["code"]
|
bridge_holder = {"form": None}
|
||||||
def on_save(updated):
|
apply_self = self
|
||||||
|
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
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
if doc is None: return
|
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")
|
e_raw = doc.Strings.GetValue("dossier_ebenen")
|
||||||
if not e_raw:
|
if not e_raw: return
|
||||||
print("[EBENEN] save_ebene: kein e-Store"); return
|
try: e_list = json.loads(e_raw)
|
||||||
try:
|
|
||||||
e_list = json.loads(e_raw)
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[EBENEN] save_ebene JSON:", ex); return
|
print("[EBENEN] save_ebene JSON:", ex); return
|
||||||
replaced = False
|
replaced = False
|
||||||
for i, e in enumerate(e_list):
|
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
|
e_list[i] = updated
|
||||||
replaced = True
|
replaced = True
|
||||||
break
|
break
|
||||||
if not replaced:
|
if not replaced:
|
||||||
print("[EBENEN] save_ebene: code {} nicht gefunden".format(old_code))
|
print("[EBENEN] save_ebene: code {} nicht gefunden".format(orig_code))
|
||||||
return
|
return
|
||||||
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
|
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
|
||||||
try: z_list = json.loads(z_raw) if z_raw else []
|
try: z_list = json.loads(z_raw) if z_raw else []
|
||||||
except Exception: z_list = []
|
except Exception: z_list = []
|
||||||
self._apply(z_list, e_list, save_z=False, save_e=True)
|
apply_self._apply(z_list, e_list, save_z=False, save_e=True)
|
||||||
panel_base.open_satellite_window(
|
self._send_state()
|
||||||
|
b = _EbenenSettingsBridge()
|
||||||
|
bridge_holder["form"] = panel_base.open_satellite_window(
|
||||||
"ebenen_settings",
|
"ebenen_settings",
|
||||||
params={"ebene": ebene, "hatchPatterns": hatch_patterns},
|
params={"currentCode": ebene["code"], "hatchPatterns": hatch_patterns},
|
||||||
title="Ebene: {}_{}".format(ebene.get("code", ""), ebene.get("name", "")),
|
title="Ebenen-Einstellungen",
|
||||||
size=(420, 600),
|
size=(420, 600),
|
||||||
on_save=on_save)
|
bridge=b)
|
||||||
|
|
||||||
def _open_geschoss_dialog(self, zeichnungsebenen):
|
def _open_geschoss_dialog(self, zeichnungsebenen):
|
||||||
"""Oeffnet den vollen GeschossDialog (Mehrfach-Editor) als
|
"""Oeffnet den vollen GeschossDialog (Mehrfach-Editor) als
|
||||||
@@ -416,6 +693,9 @@ class EbenenBridge(panel_base.BaseBridge):
|
|||||||
self._update_clipping()
|
self._update_clipping()
|
||||||
print("[EBENEN] _apply: send APPLY_OK")
|
print("[EBENEN] _apply: send APPLY_OK")
|
||||||
self.send("APPLY_OK", {})
|
self.send("APPLY_OK", {})
|
||||||
|
# Strukturelle Aenderung (neue/umbenannte/geloeschte Ebene) → aktives
|
||||||
|
# Preset passt nicht mehr exakt.
|
||||||
|
clear_active_comb_name(doc)
|
||||||
# Anderes Panel (Zeichnungsebenen/Ebenen) ueber den neuen State
|
# Anderes Panel (Zeichnungsebenen/Ebenen) ueber den neuen State
|
||||||
# informieren — sonst hinkt es hinter der DOM-Persistenz her.
|
# informieren — sonst hinkt es hinter der DOM-Persistenz her.
|
||||||
_broadcast_state(doc)
|
_broadcast_state(doc)
|
||||||
@@ -476,6 +756,7 @@ class EbenenBridge(panel_base.BaseBridge):
|
|||||||
s = z_state.get(z.get("id"))
|
s = z_state.get(z.get("id"))
|
||||||
if s is not None:
|
if s is not None:
|
||||||
m["visible"] = s.get("visible", True)
|
m["visible"] = s.get("visible", True)
|
||||||
|
m["locked"] = s.get("locked", False)
|
||||||
merged_z.append(m)
|
merged_z.append(m)
|
||||||
merged_e = []
|
merged_e = []
|
||||||
for e in e_full:
|
for e in e_full:
|
||||||
@@ -486,11 +767,33 @@ class EbenenBridge(panel_base.BaseBridge):
|
|||||||
m["visible"] = s.get("visible", True)
|
m["visible"] = s.get("visible", True)
|
||||||
m["locked"] = s.get("locked", False)
|
m["locked"] = s.get("locked", False)
|
||||||
merged_e.append(m)
|
merged_e.append(m)
|
||||||
|
# Detect whether the merge actually changed any visible/locked values.
|
||||||
|
# Wenn nicht: das ist nur der Echo-Roundtrip eines apply_layer_preset
|
||||||
|
# (React-State == doc.Strings → kein User-Click) und wir wollen das
|
||||||
|
# aktive Preset NICHT clearen.
|
||||||
|
def _vis_lock_changed(old, new):
|
||||||
|
old_by = {x.get("id") or x.get("code"): x for x in old if isinstance(x, dict)}
|
||||||
|
for nx in new:
|
||||||
|
if not isinstance(nx, dict): continue
|
||||||
|
key = nx.get("id") or nx.get("code")
|
||||||
|
if key is None: continue
|
||||||
|
ox = old_by.get(key)
|
||||||
|
if ox is None: continue
|
||||||
|
if (ox.get("visible", True) != nx.get("visible", True)
|
||||||
|
or ox.get("locked", False) != nx.get("locked", False)):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
any_changed = (_vis_lock_changed(z_full, merged_z)
|
||||||
|
or _vis_lock_changed(e_full, merged_e))
|
||||||
if has_new_structural:
|
if has_new_structural:
|
||||||
print("[EBENEN] _apply_visibility: structural change pending → skip save (waiting for APPLY)")
|
print("[EBENEN] _apply_visibility: structural change pending → skip save (waiting for APPLY)")
|
||||||
else:
|
else:
|
||||||
doc.Strings.SetString("dossier_zeichnungsebenen", json.dumps(merged_z, ensure_ascii=False))
|
doc.Strings.SetString("dossier_zeichnungsebenen", json.dumps(merged_z, ensure_ascii=False))
|
||||||
doc.Strings.SetString("dossier_ebenen", json.dumps(merged_e, ensure_ascii=False))
|
doc.Strings.SetString("dossier_ebenen", json.dumps(merged_e, ensure_ascii=False))
|
||||||
|
# User hat per Hand Eye/Lock geaendert → aktives Preset passt nicht
|
||||||
|
# mehr, auf "Eigene" zuruecksetzen.
|
||||||
|
if any_changed:
|
||||||
|
clear_active_comb_name(doc)
|
||||||
# zMode + eMode persistieren, damit bei Split-Send (nur eine
|
# zMode + eMode persistieren, damit bei Split-Send (nur eine
|
||||||
# Panel-Slice) der andere Mode aus dem Doc-Storage faellt anstatt
|
# Panel-Slice) der andere Mode aus dem Doc-Storage faellt anstatt
|
||||||
# auf den Default zu rutschen.
|
# auf den Default zu rutschen.
|
||||||
@@ -504,6 +807,24 @@ class EbenenBridge(panel_base.BaseBridge):
|
|||||||
if not isinstance(active_z, dict): active_z = {}
|
if not isinstance(active_z, dict): active_z = {}
|
||||||
active_z_id = active_z.get("id") or doc.Strings.GetValue("dossier_active_id")
|
active_z_id = active_z.get("id") or doc.Strings.GetValue("dossier_active_id")
|
||||||
active_code = p.get("activeCode") or doc.Strings.GetValue("dossier_active_code")
|
active_code = p.get("activeCode") or doc.Strings.GetValue("dossier_active_code")
|
||||||
|
# Dedupe: identisches SET_VISIBILITY (z.B. STATE_SYNC-Echo nach
|
||||||
|
# Preset-Apply) loopt sonst unnoetig durch alle ~100 Doc-Layer.
|
||||||
|
# Signatur aus active-id/code + mode + vis/lock-Liste.
|
||||||
|
def _sig(zlist, elist):
|
||||||
|
zs = tuple((z.get("id"),
|
||||||
|
bool(z.get("visible", True)),
|
||||||
|
bool(z.get("locked", False)))
|
||||||
|
for z in zlist if isinstance(z, dict))
|
||||||
|
es = tuple((e.get("code"),
|
||||||
|
bool(e.get("visible", True)),
|
||||||
|
bool(e.get("locked", False)))
|
||||||
|
for e in elist if isinstance(e, dict))
|
||||||
|
return (active_z_id, active_code, z_mode, e_mode, zs, es)
|
||||||
|
cur_sig = _sig(merged_z, merged_e)
|
||||||
|
if sc.sticky.get("_vis_last_sig") == cur_sig and not any_changed:
|
||||||
|
# Nichts Neues — Rhino-Layer-State ist schon korrekt.
|
||||||
|
return
|
||||||
|
sc.sticky["_vis_last_sig"] = cur_sig
|
||||||
layer_builder.apply_visibility(
|
layer_builder.apply_visibility(
|
||||||
doc, merged_z, merged_e, active_z_id, active_code, z_mode, e_mode)
|
doc, merged_z, merged_e, active_z_id, active_code, z_mode, e_mode)
|
||||||
# Cross-Panel-Sync NUR wenn wir nicht in einem structural-pending
|
# Cross-Panel-Sync NUR wenn wir nicht in einem structural-pending
|
||||||
@@ -515,17 +836,29 @@ class EbenenBridge(panel_base.BaseBridge):
|
|||||||
def _set_active_zeichnungsebene(self, z):
|
def _set_active_zeichnungsebene(self, z):
|
||||||
doc = Rhino.RhinoDoc.ActiveDoc
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
z_id = z.get("id", "")
|
z_id = z.get("id", "")
|
||||||
|
# Vorigen Stand merken um redundante teure Operationen zu sparen
|
||||||
|
prev_active_id = doc.Strings.GetValue("dossier_active_id") or ""
|
||||||
doc.Strings.SetString("dossier_active_id", z_id)
|
doc.Strings.SetString("dossier_active_id", z_id)
|
||||||
|
# Aktiven Sublayer auf die GLEICHE Ebene unter dem neuen Geschoss
|
||||||
|
# umschalten — wenn User auf "20 Wände" steht und das Geschoss
|
||||||
|
# wechselt, soll Rhino's aktiver Layer "1OG::20_Wände" werden statt
|
||||||
|
# auf der vorigen Geschoss-Ebene haengen zu bleiben.
|
||||||
|
self._ensure_active_sublayer()
|
||||||
# Cross-Panel-Sync: Ebenen-Panel muss aktive Geschoss-Auswahl
|
# Cross-Panel-Sync: Ebenen-Panel muss aktive Geschoss-Auswahl
|
||||||
# mitbekommen falls es im "active"-Filter-Mode laeuft.
|
# mitbekommen falls es im "active"-Filter-Mode laeuft.
|
||||||
_broadcast_state(doc)
|
_broadcast_state(doc)
|
||||||
# Clipping ggf. mitziehen
|
# Clipping nur antasten wenn entweder das alte oder das neue Geschoss
|
||||||
|
# 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)
|
self._update_clipping(active_z=z)
|
||||||
# Elemente-Panel informieren: das aktive Geschoss hat gewechselt,
|
# Elemente-Panel informieren: das aktive Geschoss hat gewechselt,
|
||||||
# neue Elemente sollen jetzt automatisch dort verlinkt werden.
|
# neue Elemente sollen jetzt automatisch dort verlinkt werden.
|
||||||
|
# Wichtig: NICHT _send_state() rufen (re-enumeriert alle Elemente,
|
||||||
|
# 200+ in echten Projekten = spuerbar). Schlanker Partial-Push.
|
||||||
try:
|
try:
|
||||||
eb = sc.sticky.get("elemente_bridge")
|
eb = sc.sticky.get("elemente_bridge")
|
||||||
if eb is not None: eb._send_state()
|
if eb is not None: eb._notify_active_geschoss()
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
if not (z.get("isGeschoss") and z.get("okff") is not None):
|
if not (z.get("isGeschoss") and z.get("okff") is not None):
|
||||||
return
|
return
|
||||||
@@ -546,12 +879,33 @@ class EbenenBridge(panel_base.BaseBridge):
|
|||||||
plane.XAxis, plane.YAxis,
|
plane.XAxis, plane.YAxis,
|
||||||
)
|
)
|
||||||
vp.SetConstructionPlane(new_plane)
|
vp.SetConstructionPlane(new_plane)
|
||||||
view.Redraw()
|
|
||||||
updated += 1
|
updated += 1
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[EBENEN] CPlane fehler ({}): {}".format(vp.Name if vp else "?", ex))
|
print("[EBENEN] CPlane fehler ({}): {}".format(vp.Name if vp else "?", ex))
|
||||||
|
# KEIN doc.Views.Redraw() hier — die folgende SET_VISIBILITY-Round-
|
||||||
|
# trip (30 ms debounce in React) feuert ohnehin layer_builder
|
||||||
|
# .apply_visibility() das am Ende selbst redrawt. Sparen wir uns
|
||||||
|
# einen doppelten Full-Repaint pro Geschoss-Klick.
|
||||||
print("[EBENEN] CPlane Z={} auf {} Top-Style View(s) gesetzt".format(okff, updated))
|
print("[EBENEN] CPlane Z={} auf {} Top-Style View(s) gesetzt".format(okff, updated))
|
||||||
|
|
||||||
|
def _needs_clipping_update(self, doc, prev_active_id, new_z):
|
||||||
|
"""Liefert True wenn entweder das alte oder das neue Geschoss
|
||||||
|
hasClipping=True hat. Sonst kann _update_clipping skipped werden
|
||||||
|
(Plane existiert nicht und muss auch nicht neu gebaut werden)."""
|
||||||
|
new_has = bool(new_z.get("hasClipping"))
|
||||||
|
if new_has:
|
||||||
|
return True
|
||||||
|
if not prev_active_id:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
|
||||||
|
if not z_raw: return False
|
||||||
|
for z in (json.loads(z_raw) or []):
|
||||||
|
if isinstance(z, dict) and z.get("id") == prev_active_id:
|
||||||
|
return bool(z.get("hasClipping"))
|
||||||
|
except Exception: pass
|
||||||
|
return False
|
||||||
|
|
||||||
def _toggle_clipping_for_active(self, enabled):
|
def _toggle_clipping_for_active(self, enabled):
|
||||||
"""Setzt hasClipping fuer das aktuell aktive Geschoss + persistiert in
|
"""Setzt hasClipping fuer das aktuell aktive Geschoss + persistiert in
|
||||||
doc.Strings + triggert plane-update. Wird vom React-Toggle 'Clipping
|
doc.Strings + triggert plane-update. Wird vom React-Toggle 'Clipping
|
||||||
@@ -704,20 +1058,10 @@ class EbenenBridge(panel_base.BaseBridge):
|
|||||||
_PRESETS_KEY = "dossier_layer_presets"
|
_PRESETS_KEY = "dossier_layer_presets"
|
||||||
|
|
||||||
def _load_presets(self, doc):
|
def _load_presets(self, doc):
|
||||||
raw = doc.Strings.GetValue(self._PRESETS_KEY)
|
return load_layer_presets(doc)
|
||||||
if not raw: return []
|
|
||||||
try:
|
|
||||||
data = json.loads(raw)
|
|
||||||
return data if isinstance(data, list) else []
|
|
||||||
except Exception:
|
|
||||||
return []
|
|
||||||
|
|
||||||
def _store_presets(self, doc, presets):
|
def _store_presets(self, doc, presets):
|
||||||
try:
|
store_layer_presets(doc, presets)
|
||||||
doc.Strings.SetString(self._PRESETS_KEY,
|
|
||||||
json.dumps(presets, ensure_ascii=False))
|
|
||||||
except Exception as ex:
|
|
||||||
print("[EBENEN] _store_presets:", ex)
|
|
||||||
|
|
||||||
def _send_combination(self):
|
def _send_combination(self):
|
||||||
"""Schickt aktuelles Layer-State + alle Presets ans Frontend."""
|
"""Schickt aktuelles Layer-State + alle Presets ans Frontend."""
|
||||||
@@ -917,7 +1261,7 @@ class EbenenBridge(panel_base.BaseBridge):
|
|||||||
name = (name or "").strip()
|
name = (name or "").strip()
|
||||||
if not name: return
|
if not name: return
|
||||||
doc = Rhino.RhinoDoc.ActiveDoc
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
presets = self._load_presets(doc)
|
presets = load_layer_presets(doc)
|
||||||
clean = []
|
clean = []
|
||||||
for ls in (layers or []):
|
for ls in (layers or []):
|
||||||
lid = ls.get("id")
|
lid = ls.get("id")
|
||||||
@@ -932,7 +1276,9 @@ class EbenenBridge(panel_base.BaseBridge):
|
|||||||
existing["layers"] = clean
|
existing["layers"] = clean
|
||||||
else:
|
else:
|
||||||
presets.append({"name": name, "layers": clean})
|
presets.append({"name": name, "layers": clean})
|
||||||
self._store_presets(doc, presets)
|
store_layer_presets(doc, presets)
|
||||||
|
_notify_oberleiste_combs()
|
||||||
|
_notify_layer_combinations_editor()
|
||||||
print("[EBENEN] Kombination '{}' gespeichert ({} Layer)".format(name, len(clean)))
|
print("[EBENEN] Kombination '{}' gespeichert ({} Layer)".format(name, len(clean)))
|
||||||
|
|
||||||
def _save_current_as_preset(self, name):
|
def _save_current_as_preset(self, name):
|
||||||
@@ -944,67 +1290,118 @@ class EbenenBridge(panel_base.BaseBridge):
|
|||||||
|
|
||||||
layers (doc.Layer-Liste) wird parallel mitgespeichert fuer Kompat
|
layers (doc.Layer-Liste) wird parallel mitgespeichert fuer Kompat
|
||||||
mit AUSSCHNITTE (das vom doc.Layer-State liest)."""
|
mit AUSSCHNITTE (das vom doc.Layer-State liest)."""
|
||||||
name = (name or "").strip()
|
save_current_as_layer_preset(Rhino.RhinoDoc.ActiveDoc, name)
|
||||||
if not name: return
|
|
||||||
|
def _delete_preset(self, name):
|
||||||
|
delete_layer_preset(Rhino.RhinoDoc.ActiveDoc, name)
|
||||||
|
|
||||||
|
|
||||||
|
class LayerCombinationsBridge(panel_base.BaseBridge):
|
||||||
|
"""Bridge fuer das Satelliten-Fenster mit dem grossen Ebenenkombinationen-
|
||||||
|
Editor (AusschnittLayerDialog). Wird vom Oberleiste-Bridge geoeffnet bei
|
||||||
|
OPEN_LAYER_COMBINATIONS_DIALOG. State wird beim READY-Event geschickt und
|
||||||
|
bei jeder Aenderung re-emittet."""
|
||||||
|
def __init__(self):
|
||||||
|
panel_base.BaseBridge.__init__(self, "layer_combinations")
|
||||||
|
|
||||||
|
def _on_ready(self):
|
||||||
|
self._send_state()
|
||||||
|
|
||||||
|
def _send_state(self):
|
||||||
doc = Rhino.RhinoDoc.ActiveDoc
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
# 1) doc.Layer state (Kompat mit AUSSCHNITTE)
|
if doc is None: return
|
||||||
layers = []
|
layers_out = []
|
||||||
try:
|
try:
|
||||||
for layer in doc.Layers:
|
for layer in doc.Layers:
|
||||||
if layer is None or layer.IsDeleted: continue
|
if layer is None or layer.IsDeleted: continue
|
||||||
layers.append({
|
lid = str(layer.Id)
|
||||||
"id": str(layer.Id),
|
try: fp = layer.FullPath or layer.Name
|
||||||
|
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),
|
"visible": bool(layer.IsVisible),
|
||||||
"locked": bool(layer.IsLocked),
|
"locked": bool(layer.IsLocked),
|
||||||
})
|
})
|
||||||
|
layers_out.sort(key=lambda x: x["fullPath"])
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[EBENEN] _save_current_as_preset enum:", ex)
|
print("[LAYER-COMB] enum:", ex)
|
||||||
# 2) Eye-States aus dossier_ebenen / dossier_zeichnungsebenen
|
self.send("LAYER_COMBINATIONS_STATE", {
|
||||||
pe_state = []
|
"layers": layers_out,
|
||||||
pz_state = []
|
"presets": load_layer_presets(doc),
|
||||||
try:
|
|
||||||
e_raw = doc.Strings.GetValue("dossier_ebenen")
|
|
||||||
if e_raw:
|
|
||||||
for e in (json.loads(e_raw) or []):
|
|
||||||
if isinstance(e, dict) and e.get("code"):
|
|
||||||
pe_state.append({
|
|
||||||
"code": e["code"],
|
|
||||||
"visible": bool(e.get("visible", True)),
|
|
||||||
"locked": bool(e.get("locked", False)),
|
|
||||||
})
|
})
|
||||||
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
|
|
||||||
if z_raw:
|
|
||||||
for z in (json.loads(z_raw) or []):
|
|
||||||
if isinstance(z, dict) and z.get("id"):
|
|
||||||
pz_state.append({
|
|
||||||
"id": z["id"],
|
|
||||||
"visible": bool(z.get("visible", True)),
|
|
||||||
})
|
|
||||||
except Exception as ex:
|
|
||||||
print("[EBENEN] _save_current_as_preset eye-states:", ex)
|
|
||||||
presets = self._load_presets(doc)
|
|
||||||
new_data = {
|
|
||||||
"name": name,
|
|
||||||
"layers": layers,
|
|
||||||
"dossierEbenen": pe_state,
|
|
||||||
"dossierZeichnungsebenen": pz_state,
|
|
||||||
}
|
|
||||||
existing = next((p for p in presets if p.get("name") == name), None)
|
|
||||||
if existing is not None:
|
|
||||||
existing.update(new_data)
|
|
||||||
else:
|
|
||||||
presets.append(new_data)
|
|
||||||
self._store_presets(doc, presets)
|
|
||||||
print("[EBENEN] '{}' gespeichert: {} Layer + {} Ebenen Eye-State".format(
|
|
||||||
name, len(layers), len(pe_state)))
|
|
||||||
|
|
||||||
def _delete_preset(self, name):
|
def handle(self, data):
|
||||||
name = (name or "").strip()
|
if not isinstance(data, dict): return
|
||||||
if not name: return
|
t = data.get("type", "")
|
||||||
|
p = data.get("payload") or {}
|
||||||
|
if not isinstance(p, dict): p = {}
|
||||||
doc = Rhino.RhinoDoc.ActiveDoc
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
presets = [p for p in self._load_presets(doc) if p.get("name") != name]
|
|
||||||
self._store_presets(doc, presets)
|
if t == "READY" or t == "REQUEST_STATE":
|
||||||
print("[EBENEN] Kombination '{}' geloescht".format(name))
|
self._on_ready()
|
||||||
|
elif t == "APPLY_COMBINATION":
|
||||||
|
# Editor wendet eine Layer-State-Liste an (ohne Preset-Name —
|
||||||
|
# also "Eigene"). Wir delegieren an die Ebenen-Bridge wenn offen,
|
||||||
|
# sonst inline. activeCombName auf None setzen.
|
||||||
|
eb = sc.sticky.get("ebenen_bridge_ref")
|
||||||
|
if eb is not None:
|
||||||
|
try: eb._apply_combination(p)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[LAYER-COMB] apply via bridge:", ex)
|
||||||
|
else:
|
||||||
|
_apply_layer_preset_inline(doc, p)
|
||||||
|
set_active_comb_name(doc, None)
|
||||||
|
_notify_oberleiste_combs()
|
||||||
|
self._send_state()
|
||||||
|
elif t == "SAVE_PRESET":
|
||||||
|
# Editor speichert die im Dialog kuratierte Liste unter Namen.
|
||||||
|
name = (p.get("name") or "").strip()
|
||||||
|
layers = p.get("layers") or []
|
||||||
|
if name:
|
||||||
|
presets = load_layer_presets(doc)
|
||||||
|
clean = []
|
||||||
|
for ls in layers:
|
||||||
|
lid = ls.get("id")
|
||||||
|
if not lid: continue
|
||||||
|
clean.append({
|
||||||
|
"id": lid,
|
||||||
|
"visible": bool(ls.get("visible", True)),
|
||||||
|
"locked": bool(ls.get("locked", False)),
|
||||||
|
})
|
||||||
|
existing = next((pp for pp in presets if pp.get("name") == name), None)
|
||||||
|
if existing is not None:
|
||||||
|
existing["layers"] = clean
|
||||||
|
else:
|
||||||
|
presets.append({"name": name, "layers": clean})
|
||||||
|
store_layer_presets(doc, presets)
|
||||||
|
_notify_oberleiste_combs()
|
||||||
|
self._send_state()
|
||||||
|
elif t == "DELETE_PRESET":
|
||||||
|
delete_layer_preset(doc, p.get("name") or "")
|
||||||
|
self._send_state()
|
||||||
|
elif t == "CANCEL":
|
||||||
|
try:
|
||||||
|
form = sc.sticky.get("layer_combinations_form")
|
||||||
|
if form is not None: form.Close()
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
|
||||||
|
def open_layer_combinations_window():
|
||||||
|
"""Oeffnet den Editor als echtes Rhino-Fenster (Eto.Form + WebView).
|
||||||
|
Wird vom Oberleiste-Bridge bei OPEN_LAYER_COMBINATIONS_DIALOG gerufen."""
|
||||||
|
b = LayerCombinationsBridge()
|
||||||
|
sc.sticky["layer_combinations_bridge"] = b
|
||||||
|
form = panel_base.open_satellite_window(
|
||||||
|
"layer_combinations",
|
||||||
|
title="Ebenenkombinationen",
|
||||||
|
size=(540, 640),
|
||||||
|
bridge=b)
|
||||||
|
sc.sticky["layer_combinations_form"] = form
|
||||||
|
|
||||||
|
|
||||||
def _ebenen_bridge_factory():
|
def _ebenen_bridge_factory():
|
||||||
@@ -1087,8 +1484,8 @@ def _install_layer_listener(bridge):
|
|||||||
print("[EBENEN] Layer-Listener aktiv")
|
print("[EBENEN] Layer-Listener aktiv")
|
||||||
|
|
||||||
|
|
||||||
panel_base.register_and_open("ebenen", "EBENEN", PANEL_GUID_STR, _ebenen_bridge_factory,
|
panel_base.register_and_open("ebenen", "Ebenen", PANEL_GUID_STR, _ebenen_bridge_factory,
|
||||||
icon_spec=("layers", "#3a6fa8"))
|
icon_spec=("layers", "#3a6fa8"))
|
||||||
panel_base.register_and_open("zeichnungsebenen", "ZEICHNUNGSEBENEN", PANEL_GUID_STR_Z,
|
panel_base.register_and_open("zeichnungsebenen", "Zeichnungsebenen", PANEL_GUID_STR_Z,
|
||||||
_zeichnungsebenen_bridge_factory,
|
_zeichnungsebenen_bridge_factory,
|
||||||
icon_spec=("levels", "#3a6fa8"))
|
icon_spec=("levels", "#3a6fa8"))
|
||||||
|
|||||||
+1
-1
@@ -54,5 +54,5 @@ def _bridge_factory():
|
|||||||
return WerkzeugeBridge()
|
return WerkzeugeBridge()
|
||||||
|
|
||||||
|
|
||||||
panel_base.register_and_open("werkzeuge", "WERKZEUGE", PANEL_GUID_STR, _bridge_factory,
|
panel_base.register_and_open("werkzeuge", "Werkzeuge", PANEL_GUID_STR, _bridge_factory,
|
||||||
icon_spec=("build", "#3a6fa8"))
|
icon_spec=("build", "#3a6fa8"))
|
||||||
|
|||||||
+19
-112
@@ -1,33 +1,29 @@
|
|||||||
import { useState, useEffect, useMemo, useRef } from 'react'
|
import { useState, useEffect, useMemo } from 'react'
|
||||||
import EbenenManager from './components/EbenenManager'
|
import EbenenManager from './components/EbenenManager'
|
||||||
import AusschnittLayerDialog from './components/AusschnittLayerDialog'
|
|
||||||
import {
|
import {
|
||||||
applyAll, setActiveEbene,
|
applyAll, setActiveEbene,
|
||||||
onMessage, notifyReady, applyVisibility,
|
onMessage, notifyReady, applyVisibility,
|
||||||
getCombination, applyCombination,
|
|
||||||
saveCurrentAsCombination, deleteCombinationPreset,
|
|
||||||
saveCombinationPreset,
|
|
||||||
} from './lib/rhinoBridge'
|
} from './lib/rhinoBridge'
|
||||||
|
|
||||||
const INITIAL_EBENEN = [
|
const INITIAL_EBENEN = [
|
||||||
{ code: '00', name: 'RASTER', color: '#484850', lw: 0.13, visible: true, locked: false },
|
{ code: '00', name: 'Raster', color: '#484850', lw: 0.13, visible: true, locked: false },
|
||||||
{ code: '01', name: 'VERMESSUNG', color: '#707078', lw: 0.18, visible: true, locked: false },
|
{ code: '01', name: 'Vermessung', color: '#707078', lw: 0.18, visible: true, locked: false },
|
||||||
{ code: '10', name: 'SITUATION', color: '#909090', lw: 0.18, visible: true, locked: false },
|
{ code: '10', name: 'Situation', color: '#909090', lw: 0.18, visible: true, locked: false },
|
||||||
{ code: '11', name: 'STRASSE', color: '#a89070', lw: 0.18, visible: true, locked: false },
|
{ code: '11', name: 'Strasse', color: '#a89070', lw: 0.18, visible: true, locked: false },
|
||||||
{ code: '12', name: 'GEBÄUDE', color: '#888888', lw: 0.25, visible: true, locked: false },
|
{ code: '12', name: 'Gebäude', color: '#888888', lw: 0.25, visible: true, locked: false },
|
||||||
{ code: '13', name: 'BÄUME', color: '#50a050', lw: 0.13, visible: true, locked: false },
|
{ code: '13', name: 'Bäume', color: '#50a050', lw: 0.13, visible: true, locked: false },
|
||||||
{ code: '14', name: 'HÖHENLINIEN', color: '#909050', lw: 0.18, visible: true, locked: false },
|
{ code: '14', name: 'Höhenlinien', color: '#909050', lw: 0.18, visible: true, locked: false },
|
||||||
{ code: '20', name: 'WÄNDE', color: '#0a0a0a', lw: 0.50, visible: true, locked: false },
|
{ code: '20', name: 'Wände', color: '#0a0a0a', lw: 0.50, visible: true, locked: false },
|
||||||
{ code: '21', name: 'TÜREN_FENSTER', color: '#5080c8', lw: 0.25, visible: true, locked: false },
|
{ code: '21', name: 'Türen_Fenster', color: '#5080c8', lw: 0.25, visible: true, locked: false },
|
||||||
{ code: '22', name: 'MÖBEL', color: '#909090', lw: 0.13, visible: true, locked: false },
|
{ code: '22', name: 'Möbel', color: '#909090', lw: 0.13, visible: true, locked: false },
|
||||||
{ code: '25', name: 'STÜTZEN', color: '#c87050', lw: 0.50, visible: true, locked: false },
|
{ code: '25', name: 'Stützen', color: '#c87050', lw: 0.50, visible: true, locked: false },
|
||||||
{ code: '30', name: 'DECKEN', color: '#605850', lw: 0.35, visible: true, locked: false },
|
{ code: '30', name: 'Decken', color: '#605850', lw: 0.35, visible: true, locked: false },
|
||||||
{ code: '31', name: 'DÄCHER', color: '#7a4a3a', lw: 0.35, visible: true, locked: false },
|
{ code: '31', name: 'Dächer', color: '#7a4a3a', lw: 0.35, visible: true, locked: false },
|
||||||
{ code: '35', name: 'TRÄGER', color: '#a87858', lw: 0.50, visible: true, locked: false },
|
{ code: '35', name: 'Träger', color: '#a87858', lw: 0.50, visible: true, locked: false },
|
||||||
{ code: '50', name: 'TEXT', color: '#d0d0d0', lw: 0.13, visible: true, locked: false },
|
{ code: '50', name: 'Text', color: '#d0d0d0', lw: 0.13, visible: true, locked: false },
|
||||||
{ code: '60', name: 'PLANGRAFIK', color: '#c0a040', lw: 0.13, visible: true, locked: false },
|
{ code: '60', name: 'Plangrafik', color: '#c0a040', lw: 0.13, visible: true, locked: false },
|
||||||
{ code: '90', name: 'REFERENZEN', color: '#585860', lw: 0.13, visible: true, locked: false },
|
{ code: '90', name: 'Referenzen', color: '#585860', lw: 0.13, visible: true, locked: false },
|
||||||
{ code: '99', name: 'KONSTRUKTION', color: '#404048', lw: 0.13, visible: true, locked: false },
|
{ code: '99', name: 'Konstruktion', color: '#404048', lw: 0.13, visible: true, locked: false },
|
||||||
]
|
]
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
@@ -36,28 +32,12 @@ export default function App() {
|
|||||||
const [appliedE, setAppliedE] = useState(INITIAL_EBENEN)
|
const [appliedE, setAppliedE] = useState(INITIAL_EBENEN)
|
||||||
const [eMode, setEMode] = useState('all')
|
const [eMode, setEMode] = useState('all')
|
||||||
const [hatchPatterns, setHatchPatterns] = useState(['Solid', 'Hatch1', 'Hatch2', 'Hatch3', 'Plus', 'Squares', 'Grid', 'Grid60'])
|
const [hatchPatterns, setHatchPatterns] = useState(['Solid', 'Hatch1', 'Hatch2', 'Hatch3', 'Plus', 'Squares', 'Grid', 'Grid60'])
|
||||||
// Ebenenkombinationen (geteilter Store mit Ausschnitten)
|
|
||||||
const [combinations, setCombinations] = useState([]) // Liste { name, layers }
|
|
||||||
const [activeCombName, setActiveCombName] = useState(null) // null = "Eigene"
|
|
||||||
// Dialog fuer "alle bearbeiten" (Pencil-Button)
|
|
||||||
const [combDialog, setCombDialog] = useState(null) // { layers, presets } oder null
|
|
||||||
const wantCombDialogRef = useRef(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onMessage('STATE_SYNC', ({ ebenen: e, hatchPatterns: hp }) => {
|
onMessage('STATE_SYNC', ({ ebenen: e, hatchPatterns: hp }) => {
|
||||||
if (e) { setEbenen(e); setAppliedE(e) }
|
if (e) { setEbenen(e); setAppliedE(e) }
|
||||||
if (Array.isArray(hp) && hp.length > 0) setHatchPatterns(hp)
|
if (Array.isArray(hp) && hp.length > 0) setHatchPatterns(hp)
|
||||||
})
|
})
|
||||||
onMessage('COMBINATION_DATA', ({ layers, presets }) => {
|
|
||||||
setCombinations(presets || [])
|
|
||||||
if (wantCombDialogRef.current) {
|
|
||||||
wantCombDialogRef.current = false
|
|
||||||
setCombDialog({ layers: layers || [], presets: presets || [] })
|
|
||||||
} else if (combDialog) {
|
|
||||||
// Dialog ist offen — Layer-Liste live aktualisieren (z.B. nach Preset-Save)
|
|
||||||
setCombDialog(d => d ? { ...d, layers: layers || d.layers, presets: presets || [] } : d)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
onMessage('FIRST_RUN', ({ defaultEbenen } = {}) => {
|
onMessage('FIRST_RUN', ({ defaultEbenen } = {}) => {
|
||||||
// Wenn der Dossier-Launcher ein eigenes Schema definiert hat, nutzen wir
|
// Wenn der Dossier-Launcher ein eigenes Schema definiert hat, nutzen wir
|
||||||
// das statt der hardcoded INITIAL_EBENEN.
|
// das statt der hardcoded INITIAL_EBENEN.
|
||||||
@@ -72,8 +52,6 @@ export default function App() {
|
|||||||
if (activeCode) setActiveEbene(activeCode)
|
if (activeCode) setActiveEbene(activeCode)
|
||||||
})
|
})
|
||||||
notifyReady()
|
notifyReady()
|
||||||
// Initial Liste der Kombinationen holen
|
|
||||||
setTimeout(() => getCombination(), 200)
|
|
||||||
|
|
||||||
// Native Browser-Context-Menu global unterdruecken
|
// Native Browser-Context-Menu global unterdruecken
|
||||||
const blockContext = (ev) => ev.preventDefault()
|
const blockContext = (ev) => ev.preventDefault()
|
||||||
@@ -122,41 +100,6 @@ export default function App() {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [structureKey, appliedStructureKey])
|
}, [structureKey, appliedStructureKey])
|
||||||
|
|
||||||
// --- Ebenen-Kombinationen ----------------------------------------------
|
|
||||||
const handlePickCombination = (name) => {
|
|
||||||
if (!name) { setActiveCombName(null); return }
|
|
||||||
const preset = combinations.find(p => p.name === name)
|
|
||||||
if (!preset) return
|
|
||||||
applyCombination({
|
|
||||||
layers: preset.layers || [],
|
|
||||||
dossierEbenen: preset.dossierEbenen,
|
|
||||||
dossierZeichnungsebenen: preset.dossierZeichnungsebenen,
|
|
||||||
})
|
|
||||||
setActiveCombName(name)
|
|
||||||
}
|
|
||||||
const handleSaveCurrentCombination = () => {
|
|
||||||
const suggested = activeCombName || `Kombi ${combinations.length + 1}`
|
|
||||||
const name = (window.prompt('Name für Ebenenkombination:', suggested) || '').trim()
|
|
||||||
if (!name) return
|
|
||||||
if (combinations.some(p => p.name === name) &&
|
|
||||||
!window.confirm(`"${name}" überschreiben?`)) return
|
|
||||||
saveCurrentAsCombination(name)
|
|
||||||
setActiveCombName(name)
|
|
||||||
}
|
|
||||||
const handleDeleteCombination = (name) => {
|
|
||||||
if (!name) return
|
|
||||||
if (!window.confirm(`Ebenenkombination "${name}" löschen?`)) return
|
|
||||||
deleteCombinationPreset(name)
|
|
||||||
if (activeCombName === name) setActiveCombName(null)
|
|
||||||
}
|
|
||||||
const handleOpenCombDialog = () => {
|
|
||||||
wantCombDialogRef.current = true
|
|
||||||
getCombination()
|
|
||||||
}
|
|
||||||
const handleUserVisibilityChange = () => {
|
|
||||||
if (activeCombName !== null) setActiveCombName(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex', flexDirection: 'column',
|
display: 'flex', flexDirection: 'column',
|
||||||
@@ -173,44 +116,8 @@ export default function App() {
|
|||||||
mode={eMode}
|
mode={eMode}
|
||||||
onModeChange={setEMode}
|
onModeChange={setEMode}
|
||||||
hatchPatterns={hatchPatterns}
|
hatchPatterns={hatchPatterns}
|
||||||
combinations={combinations}
|
|
||||||
activeCombName={activeCombName}
|
|
||||||
onPickCombination={handlePickCombination}
|
|
||||||
onSaveCurrentCombination={handleSaveCurrentCombination}
|
|
||||||
onDeleteCombination={handleDeleteCombination}
|
|
||||||
onEditCombinations={handleOpenCombDialog}
|
|
||||||
onUserVisibilityChange={handleUserVisibilityChange}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{combDialog && (
|
|
||||||
<AusschnittLayerDialog
|
|
||||||
snapName="Ebenenkombinationen bearbeiten"
|
|
||||||
layers={combDialog.layers}
|
|
||||||
presets={combDialog.presets}
|
|
||||||
onClose={() => setCombDialog(null)}
|
|
||||||
onSave={(layers) => {
|
|
||||||
applyCombination(layers)
|
|
||||||
setActiveCombName(null)
|
|
||||||
setCombDialog(null)
|
|
||||||
}}
|
|
||||||
onSavePreset={(name, layers) => {
|
|
||||||
saveCombinationPreset(name, layers)
|
|
||||||
setCombDialog(d => d ? {
|
|
||||||
...d,
|
|
||||||
presets: [...d.presets.filter(p => p.name !== name), { name, layers }],
|
|
||||||
} : d)
|
|
||||||
}}
|
|
||||||
onDeletePreset={(name) => {
|
|
||||||
deleteCombinationPreset(name)
|
|
||||||
setCombDialog(d => d ? {
|
|
||||||
...d,
|
|
||||||
presets: d.presets.filter(p => p.name !== name),
|
|
||||||
} : d)
|
|
||||||
if (activeCombName === name) setActiveCombName(null)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { useState, useEffect, useMemo } from 'react'
|
||||||
import Icon from './components/Icon'
|
import Icon from './components/Icon'
|
||||||
import ContextMenu from './components/ContextMenu'
|
import ContextMenu from './components/ContextMenu'
|
||||||
import AusschnittLayerDialog from './components/AusschnittLayerDialog'
|
|
||||||
import {
|
import {
|
||||||
onMessage, notifyReady,
|
onMessage, notifyReady,
|
||||||
listAusschnitte, saveAusschnitt, updateAusschnitt,
|
listAusschnitte, saveAusschnitt, updateAusschnitt,
|
||||||
@@ -9,8 +8,7 @@ import {
|
|||||||
renameAusschnitt, deleteAusschnitt,
|
renameAusschnitt, deleteAusschnitt,
|
||||||
setAusschnittFolder, setAusschnittScale,
|
setAusschnittFolder, setAusschnittScale,
|
||||||
duplicateAusschnitt, addAusschnittFolder, removeAusschnittFolder,
|
duplicateAusschnitt, addAusschnittFolder, removeAusschnittFolder,
|
||||||
getAusschnittLayers, updateAusschnittLayers,
|
openAusschnittSettings,
|
||||||
saveLayerPreset, deleteLayerPreset,
|
|
||||||
} from './lib/rhinoBridge'
|
} from './lib/rhinoBridge'
|
||||||
|
|
||||||
function EditableInline({ value, onCommit, autoEdit, style, fontSize }) {
|
function EditableInline({ value, onCommit, autoEdit, style, fontSize }) {
|
||||||
@@ -247,22 +245,16 @@ function RootDropZone({ children, onDragOver, onDragLeave, onDrop, dragOver, emp
|
|||||||
export default function AusschnitteApp() {
|
export default function AusschnitteApp() {
|
||||||
const [snaps, setSnaps] = useState([])
|
const [snaps, setSnaps] = useState([])
|
||||||
const [extraFolders, setExtraFolders] = useState([])
|
const [extraFolders, setExtraFolders] = useState([])
|
||||||
const [presets, setPresets] = useState([])
|
|
||||||
const [newName, setNewName] = useState('')
|
const [newName, setNewName] = useState('')
|
||||||
const [ctxMenu, setCtxMenu] = useState(null)
|
const [ctxMenu, setCtxMenu] = useState(null)
|
||||||
const [collapsed, setCollapsed] = useState({})
|
const [collapsed, setCollapsed] = useState({})
|
||||||
const [draggingId, setDraggingId] = useState(null)
|
const [draggingId, setDraggingId] = useState(null)
|
||||||
const [dragTarget, setDragTarget] = useState(null)
|
const [dragTarget, setDragTarget] = useState(null)
|
||||||
const [layerDialog, setLayerDialog] = useState(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onMessage('LIST', ({ snapshots, folders, presets }) => {
|
onMessage('LIST', ({ snapshots, folders }) => {
|
||||||
setSnaps(snapshots || [])
|
setSnaps(snapshots || [])
|
||||||
setExtraFolders(folders || [])
|
setExtraFolders(folders || [])
|
||||||
setPresets(presets || [])
|
|
||||||
})
|
|
||||||
onMessage('LAYERS_DATA', ({ id, name, layers, presets }) => {
|
|
||||||
setLayerDialog({ id, name, layers: layers || [], presets: presets || [] })
|
|
||||||
})
|
})
|
||||||
notifyReady()
|
notifyReady()
|
||||||
const blockContext = (ev) => ev.preventDefault()
|
const blockContext = (ev) => ev.preventDefault()
|
||||||
@@ -301,7 +293,7 @@ export default function AusschnitteApp() {
|
|||||||
{ label: 'Wiederherstellen', icon: 'restore', onClick: () => restoreAusschnitt(id) },
|
{ label: 'Wiederherstellen', icon: 'restore', onClick: () => restoreAusschnitt(id) },
|
||||||
{ label: 'Auf Detail anwenden', icon: 'crop_landscape', onClick: () => applyAusschnittToDetail(id) },
|
{ label: 'Auf Detail anwenden', icon: 'crop_landscape', onClick: () => applyAusschnittToDetail(id) },
|
||||||
{ divider: true },
|
{ divider: true },
|
||||||
{ label: 'Sichtbarkeit bearbeiten…', icon: 'layers', onClick: () => getAusschnittLayers(id) },
|
{ label: 'Ausschnittseinstellungen…', icon: 'tune', onClick: () => openAusschnittSettings(id) },
|
||||||
{ divider: true },
|
{ divider: true },
|
||||||
{ label: 'Duplizieren', icon: 'content_copy', onClick: () => duplicateAusschnitt(id) },
|
{ label: 'Duplizieren', icon: 'content_copy', onClick: () => duplicateAusschnitt(id) },
|
||||||
{ label: 'Aktualisieren', icon: 'sync', onClick: () => updateAusschnitt(id) },
|
{ label: 'Aktualisieren', icon: 'sync', onClick: () => updateAusschnitt(id) },
|
||||||
@@ -361,17 +353,6 @@ export default function AusschnitteApp() {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
const actions = (
|
|
||||||
<div style={{ display: 'flex', gap: 4 }}>
|
|
||||||
<button className="btn-icon-tonal" onClick={handleAddFolder} title="Neuer Ordner">
|
|
||||||
<Icon name="create_new_folder" size={14} />
|
|
||||||
</button>
|
|
||||||
<button className="btn-icon-tonal" onClick={() => listAusschnitte()} title="Aktualisieren">
|
|
||||||
<Icon name="refresh" size={14} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
const rootItems = groups[''] || []
|
const rootItems = groups[''] || []
|
||||||
const isEmpty = snaps.length === 0 && allFolders.length === 0
|
const isEmpty = snaps.length === 0 && allFolders.length === 0
|
||||||
|
|
||||||
@@ -382,21 +363,6 @@ export default function AusschnitteApp() {
|
|||||||
background: 'var(--bg-base)',
|
background: 'var(--bg-base)',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
}}>
|
}}>
|
||||||
{/* Fixed Header — wie Layouts/Overrides Pattern */}
|
|
||||||
<div style={{
|
|
||||||
display: 'flex', alignItems: 'center', gap: 6,
|
|
||||||
padding: '8px 10px',
|
|
||||||
borderBottom: '1px solid var(--border)',
|
|
||||||
flexShrink: 0,
|
|
||||||
}}>
|
|
||||||
<span style={{ flex: 1, fontWeight: 600, letterSpacing: '0.08em',
|
|
||||||
color: 'var(--text-primary)' }}>
|
|
||||||
AUSSCHNITTE
|
|
||||||
</span>
|
|
||||||
<span className="chip" style={{ fontSize: 8 }}>{snaps.length}</span>
|
|
||||||
{actions}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ flex: 1, overflowY: 'auto', padding: 8 }}>
|
<div style={{ flex: 1, overflowY: 'auto', padding: 8 }}>
|
||||||
{/* Save-Bar als Card */}
|
{/* Save-Bar als Card */}
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -481,6 +447,28 @@ export default function AusschnitteApp() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Sticky Footer: Anzahl + Ordner erstellen + Reload */}
|
||||||
|
<div style={{
|
||||||
|
display: 'flex', alignItems: 'center', gap: 8,
|
||||||
|
padding: '8px 10px',
|
||||||
|
borderTop: '1px solid var(--border)',
|
||||||
|
background: 'var(--bg-panel)',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}>
|
||||||
|
<span className="chip" style={{
|
||||||
|
fontSize: 9, minWidth: 22, justifyContent: 'center',
|
||||||
|
}}>{snaps.length}</span>
|
||||||
|
<span style={{ fontSize: 10, color: 'var(--text-muted)', flex: 1 }}>
|
||||||
|
Ausschnitte
|
||||||
|
</span>
|
||||||
|
<button className="btn-icon-tonal" onClick={handleAddFolder} title="Neuer Ordner">
|
||||||
|
<Icon name="create_new_folder" size={14} />
|
||||||
|
</button>
|
||||||
|
<button className="btn-icon-tonal" onClick={() => listAusschnitte()} title="Aktualisieren">
|
||||||
|
<Icon name="refresh" size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{ctxMenu && (
|
{ctxMenu && (
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
x={ctxMenu.x} y={ctxMenu.y}
|
x={ctxMenu.x} y={ctxMenu.y}
|
||||||
@@ -489,27 +477,6 @@ export default function AusschnitteApp() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{layerDialog && (
|
|
||||||
<AusschnittLayerDialog
|
|
||||||
snapName={layerDialog.name}
|
|
||||||
layers={layerDialog.layers}
|
|
||||||
presets={layerDialog.presets}
|
|
||||||
onSave={(layers) => {
|
|
||||||
updateAusschnittLayers(layerDialog.id,
|
|
||||||
layers.map(l => ({ id: l.id, visible: l.visible, locked: l.locked })))
|
|
||||||
setLayerDialog(null)
|
|
||||||
}}
|
|
||||||
onClose={() => setLayerDialog(null)}
|
|
||||||
onSavePreset={(name, layers) => {
|
|
||||||
saveLayerPreset(name, layers)
|
|
||||||
setLayerDialog(d => d ? { ...d, presets: [...d.presets.filter(p => p.name !== name), { name, layers }] } : d)
|
|
||||||
}}
|
|
||||||
onDeletePreset={(name) => {
|
|
||||||
deleteLayerPreset(name)
|
|
||||||
setLayerDialog(d => d ? { ...d, presets: d.presets.filter(p => p.name !== name) } : d)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+51
-77
@@ -50,24 +50,22 @@ function NumInput({ value, onCommit, disabled, suffix, width }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9-Punkt-Referenzpunkt-Selektor im Illustrator-Stil: sichtbarer BBox-Rahmen,
|
// 9-Punkt-Referenzpunkt-Selektor: sichtbarer BBox-Rahmen, Kreise auf den
|
||||||
// die Punkte sitzen AUF Ecken / Kantenmitten / Zentrum.
|
// Eckpunkten / Kantenmitten / Zentrum.
|
||||||
function RefPointGrid({ ref, onChange }) {
|
function RefPointGrid({ ref, onChange }) {
|
||||||
const SIZE = 26 // Aussenkanten-Quadrat (px)
|
const SIZE = 28 // Aussenkanten-Quadrat (px)
|
||||||
const DOT = 5 // Punkt-Durchmesser (px)
|
const DOT = 6 // Kreis-Durchmesser (px)
|
||||||
// Position pro Code: 0% (min), 50% (mid), 100% (max)
|
|
||||||
const pct = (c) => c === 'min' ? '0%' : c === 'max' ? '100%' : '50%'
|
const pct = (c) => c === 'min' ? '0%' : c === 'max' ? '100%' : '50%'
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
width: SIZE, height: SIZE,
|
width: SIZE, height: SIZE,
|
||||||
border: '1px solid var(--text-muted)',
|
border: '1px solid var(--border)',
|
||||||
background: 'transparent',
|
background: 'transparent',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
}}>
|
}}>
|
||||||
{REF_CODES.map(yc => REF_CODES.map(xc => {
|
{REF_CODES.map(yc => REF_CODES.map(xc => {
|
||||||
const active = ref.x === xc && ref.y === yc
|
const active = ref.x === xc && ref.y === yc
|
||||||
// yc 'max' = top in user mental model (Vectorworks/Illustrator)
|
|
||||||
const topPct = yc === 'max' ? '0%' : yc === 'min' ? '100%' : '50%'
|
const topPct = yc === 'max' ? '0%' : yc === 'min' ? '100%' : '50%'
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@@ -79,14 +77,20 @@ function RefPointGrid({ ref, onChange }) {
|
|||||||
left: pct(xc), top: topPct,
|
left: pct(xc), top: topPct,
|
||||||
transform: 'translate(-50%, -50%)',
|
transform: 'translate(-50%, -50%)',
|
||||||
width: DOT, height: DOT, padding: 0,
|
width: DOT, height: DOT, padding: 0,
|
||||||
borderRadius: 0, // eckig wie Illustrator
|
borderRadius: '50%',
|
||||||
background: active ? 'var(--accent)' : 'var(--text-muted)',
|
background: active ? 'var(--accent)' : 'var(--text-muted)',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
transition: 'background 0.1s',
|
transition: 'background 0.12s, transform 0.12s',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
if (!active) e.currentTarget.style.background = 'var(--text-primary)'
|
||||||
|
e.currentTarget.style.transform = 'translate(-50%, -50%) scale(1.25)'
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
if (!active) e.currentTarget.style.background = 'var(--text-muted)'
|
||||||
|
e.currentTarget.style.transform = 'translate(-50%, -50%) scale(1)'
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => { if (!active) e.currentTarget.style.background = 'var(--text-primary)' }}
|
|
||||||
onMouseLeave={(e) => { if (!active) e.currentTarget.style.background = 'var(--text-muted)' }}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}))}
|
}))}
|
||||||
@@ -189,15 +193,13 @@ export default function DimensionenApp() {
|
|||||||
background: 'var(--bg-base)', color: 'var(--text-primary)',
|
background: 'var(--bg-base)', color: 'var(--text-primary)',
|
||||||
fontFamily: 'var(--font)', fontSize: 11,
|
fontFamily: 'var(--font)', fontSize: 11,
|
||||||
}}>
|
}}>
|
||||||
<div style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden', padding: 6 }}>
|
<div style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden' }}>
|
||||||
|
|
||||||
{/* Header: Selektions-Info + World/CPlane */}
|
{/* Header: Selektions-Info + World/CPlane */}
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex', alignItems: 'center', gap: 6, marginBottom: 6,
|
display: 'flex', alignItems: 'center', gap: 6,
|
||||||
padding: '5px 8px',
|
padding: '8px 12px',
|
||||||
background: 'var(--bg-section)',
|
borderBottom: '1px solid var(--border-light)',
|
||||||
border: '1px solid var(--border)',
|
|
||||||
borderRadius: 'var(--r-lg)',
|
|
||||||
}}>
|
}}>
|
||||||
<Icon name="select_all" size={14} style={{ color: 'var(--text-muted)' }} />
|
<Icon name="select_all" size={14} style={{ color: 'var(--text-muted)' }} />
|
||||||
<span style={{ flex: 1, fontWeight: 500 }}>{selLabel()}</span>
|
<span style={{ flex: 1, fontWeight: 500 }}>{selLabel()}</span>
|
||||||
@@ -221,11 +223,8 @@ export default function DimensionenApp() {
|
|||||||
<div style={{
|
<div style={{
|
||||||
padding: '32px 16px', textAlign: 'center',
|
padding: '32px 16px', textAlign: 'center',
|
||||||
color: 'var(--text-muted)', fontSize: 11,
|
color: 'var(--text-muted)', fontSize: 11,
|
||||||
border: '1px dashed var(--border)',
|
|
||||||
borderRadius: 'var(--r-lg)',
|
|
||||||
background: 'var(--bg-section)',
|
|
||||||
}}>
|
}}>
|
||||||
<Icon name="aspect_ratio" size={32} style={{ color: 'var(--text-muted)', opacity: 0.5 }} />
|
<Icon name="aspect_ratio" size={32} style={{ color: 'var(--text-muted)', opacity: 0.4 }} />
|
||||||
<div style={{ marginTop: 8 }}>Keine Selektion.</div>
|
<div style={{ marginTop: 8 }}>Keine Selektion.</div>
|
||||||
<div style={{ marginTop: 4, fontSize: 10 }}>
|
<div style={{ marginTop: 4, fontSize: 10 }}>
|
||||||
In Rhino ein oder mehrere Objekte auswählen.
|
In Rhino ein oder mehrere Objekte auswählen.
|
||||||
@@ -233,38 +232,30 @@ export default function DimensionenApp() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Referenzpunkt — kompakte einzeilige Card */}
|
{/* Referenzpunkt */}
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex', alignItems: 'center', gap: 8,
|
display: 'flex', alignItems: 'center', gap: 10,
|
||||||
padding: '6px 8px', marginBottom: 6,
|
padding: '10px 12px',
|
||||||
background: 'var(--bg-section)',
|
borderBottom: '1px solid var(--border-light)',
|
||||||
border: '1px solid var(--border)',
|
|
||||||
borderRadius: 'var(--r-lg)',
|
|
||||||
}}>
|
}}>
|
||||||
<span style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)',
|
<span className="label-xs" style={{ width: 30 }}>Ref</span>
|
||||||
letterSpacing: '0.06em', textTransform: 'uppercase' }}>
|
|
||||||
Ref
|
|
||||||
</span>
|
|
||||||
<RefPointGrid ref={ref} onChange={onRefChange} />
|
<RefPointGrid ref={ref} onChange={onRefChange} />
|
||||||
|
<div style={{ flex: 1 }} />
|
||||||
<RefZSelector z={ref.z} onChange={(z) => onRefChange({ ...ref, z })} />
|
<RefZSelector z={ref.z} onChange={(z) => onRefChange({ ...ref, z })} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Position + Abmessungen nebeneinander */}
|
{/* Position + BBox nebeneinander */}
|
||||||
<div style={{ display: 'flex', gap: 6, marginBottom: 6 }}>
|
|
||||||
<div style={{
|
<div style={{
|
||||||
flex: 1, display: 'flex', flexDirection: 'column', gap: 4,
|
display: 'flex', gap: 16,
|
||||||
padding: '6px 8px',
|
padding: '10px 12px',
|
||||||
background: 'var(--bg-section)',
|
borderBottom: '1px solid var(--border-light)',
|
||||||
border: '1px solid var(--border)',
|
|
||||||
borderRadius: 'var(--r-lg)',
|
|
||||||
minWidth: 0,
|
|
||||||
}}>
|
}}>
|
||||||
<div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)',
|
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 4, minWidth: 0 }}>
|
||||||
letterSpacing: '0.06em', textTransform: 'uppercase',
|
<div style={{ display: 'flex', justifyContent: 'space-between',
|
||||||
display: 'flex', justifyContent: 'space-between' }}>
|
alignItems: 'baseline', marginBottom: 2 }}>
|
||||||
<span>Position</span>
|
<span className="label-xs">Position</span>
|
||||||
<span style={{ fontWeight: 400, textTransform: 'none',
|
<span style={{ fontFamily: 'DM Mono, monospace', fontSize: 9,
|
||||||
fontFamily: 'DM Mono, monospace', fontSize: 9 }}>
|
color: 'var(--text-muted)' }}>
|
||||||
{state.planeName}
|
{state.planeName}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -272,18 +263,9 @@ export default function DimensionenApp() {
|
|||||||
<Field label="Y"><NumInput value={pos.y} onCommit={(v) => setDimPosition('y', v)} /></Field>
|
<Field label="Y"><NumInput value={pos.y} onCommit={(v) => setDimPosition('y', v)} /></Field>
|
||||||
<Field label="Z"><NumInput value={pos.z} onCommit={(v) => setDimPosition('z', v)} /></Field>
|
<Field label="Z"><NumInput value={pos.z} onCommit={(v) => setDimPosition('z', v)} /></Field>
|
||||||
</div>
|
</div>
|
||||||
<div style={{
|
<div style={{ width: 1, background: 'var(--border-light)' }} />
|
||||||
flex: 1, display: 'flex', flexDirection: 'column', gap: 4,
|
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 4, minWidth: 0 }}>
|
||||||
padding: '6px 8px',
|
<span className="label-xs" style={{ marginBottom: 2 }}>BBox</span>
|
||||||
background: 'var(--bg-section)',
|
|
||||||
border: '1px solid var(--border)',
|
|
||||||
borderRadius: 'var(--r-lg)',
|
|
||||||
minWidth: 0,
|
|
||||||
}}>
|
|
||||||
<div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)',
|
|
||||||
letterSpacing: '0.06em', textTransform: 'uppercase' }}>
|
|
||||||
BBox
|
|
||||||
</div>
|
|
||||||
<Field label="B"><NumInput value={dims?.width} onCommit={(v) => setDimDimension('width', v)} /></Field>
|
<Field label="B"><NumInput value={dims?.width} onCommit={(v) => setDimDimension('width', v)} /></Field>
|
||||||
<Field label="T"><NumInput value={dims?.depth} onCommit={(v) => setDimDimension('depth', v)} /></Field>
|
<Field label="T"><NumInput value={dims?.depth} onCommit={(v) => setDimDimension('depth', v)} /></Field>
|
||||||
<Field label="H"><NumInput value={dims?.height} onCommit={(v) => setDimDimension('height', v)} /></Field>
|
<Field label="H"><NumInput value={dims?.height} onCommit={(v) => setDimDimension('height', v)} /></Field>
|
||||||
@@ -294,22 +276,19 @@ export default function DimensionenApp() {
|
|||||||
{shape && (
|
{shape && (
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex', flexDirection: 'column', gap: 4,
|
display: 'flex', flexDirection: 'column', gap: 4,
|
||||||
padding: '6px 8px', marginBottom: 6,
|
padding: '10px 12px',
|
||||||
background: 'var(--bg-section)',
|
borderBottom: '1px solid var(--border-light)',
|
||||||
border: '1px solid var(--accent)',
|
|
||||||
borderRadius: 'var(--r-lg)',
|
|
||||||
}}>
|
}}>
|
||||||
<div style={{ fontSize: 10, fontWeight: 600, color: 'var(--accent)',
|
<span className="label-xs" style={{ color: 'var(--accent)' }}>
|
||||||
letterSpacing: '0.06em', textTransform: 'uppercase' }}>
|
|
||||||
{shape.type === 'circle' && 'Kreis'}
|
{shape.type === 'circle' && 'Kreis'}
|
||||||
{shape.type === 'rectangle' && 'Rechteck'}
|
{shape.type === 'rectangle' && 'Rechteck'}
|
||||||
{shape.type === 'line' && 'Linie'}
|
{shape.type === 'line' && 'Linie'}
|
||||||
</div>
|
</span>
|
||||||
{shape.type === 'circle' && (
|
{shape.type === 'circle' && (
|
||||||
<Field label="R"><NumInput value={shape.radius} onCommit={(v) => setCircleRadius(v)} /></Field>
|
<Field label="R"><NumInput value={shape.radius} onCommit={(v) => setCircleRadius(v)} /></Field>
|
||||||
)}
|
)}
|
||||||
{shape.type === 'rectangle' && (
|
{shape.type === 'rectangle' && (
|
||||||
<div style={{ display: 'flex', gap: 6 }}>
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
<Field label="W" style={{ flex: 1 }}>
|
<Field label="W" style={{ flex: 1 }}>
|
||||||
<NumInput value={shape.width}
|
<NumInput value={shape.width}
|
||||||
onCommit={(v) => setRectangleDims(v, shape.height)} />
|
onCommit={(v) => setRectangleDims(v, shape.height)} />
|
||||||
@@ -321,7 +300,7 @@ export default function DimensionenApp() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{shape.type === 'line' && (
|
{shape.type === 'line' && (
|
||||||
<div style={{ display: 'flex', gap: 6 }}>
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
<Field label="L" style={{ flex: 1 }}>
|
<Field label="L" style={{ flex: 1 }}>
|
||||||
<NumInput value={shape.length} onCommit={(v) => setLineLength(v)} />
|
<NumInput value={shape.length} onCommit={(v) => setLineLength(v)} />
|
||||||
</Field>
|
</Field>
|
||||||
@@ -333,19 +312,13 @@ export default function DimensionenApp() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Rotation — kompakt einzeilig */}
|
{/* Rotation */}
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex', alignItems: 'center', gap: 6,
|
display: 'flex', alignItems: 'center', gap: 6,
|
||||||
padding: '6px 8px', marginBottom: 6,
|
padding: '10px 12px',
|
||||||
background: 'var(--bg-section)',
|
|
||||||
border: '1px solid var(--border)',
|
|
||||||
borderRadius: 'var(--r-lg)',
|
|
||||||
}}>
|
}}>
|
||||||
<span style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)',
|
<span className="label-xs" style={{ width: 50 }}>Drehen</span>
|
||||||
letterSpacing: '0.06em', textTransform: 'uppercase' }}>
|
<div style={{ width: 56 }}>
|
||||||
Drehen
|
|
||||||
</span>
|
|
||||||
<div style={{ flex: 1 }}>
|
|
||||||
<NumInput value={rotationDelta} onCommit={setRotationDelta} suffix="°" />
|
<NumInput value={rotationDelta} onCommit={setRotationDelta} suffix="°" />
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -357,12 +330,13 @@ export default function DimensionenApp() {
|
|||||||
>
|
>
|
||||||
<Icon name="rotate_right" size={13} />
|
<Icon name="rotate_right" size={13} />
|
||||||
</button>
|
</button>
|
||||||
|
<div style={{ flex: 1 }} />
|
||||||
{[-90, -45, 45, 90].map(a => (
|
{[-90, -45, 45, 90].map(a => (
|
||||||
<button
|
<button
|
||||||
key={a}
|
key={a}
|
||||||
className="btn-outlined"
|
className="btn-outlined"
|
||||||
onClick={() => setDimRotationZ(a)}
|
onClick={() => setDimRotationZ(a)}
|
||||||
style={{ padding: '3px 5px', fontSize: 9, minWidth: 28 }}
|
style={{ padding: '3px 6px', fontSize: 9, minWidth: 28 }}
|
||||||
title={`${a}°`}
|
title={`${a}°`}
|
||||||
>
|
>
|
||||||
{a > 0 ? '+' : ''}{a}°
|
{a > 0 ? '+' : ''}{a}°
|
||||||
|
|||||||
+46
-10
@@ -1,37 +1,73 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect, useState, useRef } from 'react'
|
||||||
import EbenenSettingsDialog from './components/EbenenSettingsDialog'
|
import EbenenSettingsDialog from './components/EbenenSettingsDialog'
|
||||||
import { notifyReady } from './lib/rhinoBridge'
|
import { notifyReady, onMessage } from './lib/rhinoBridge'
|
||||||
|
|
||||||
function bridgeSend(type, payload = {}) {
|
function bridgeSend(type, payload = {}) {
|
||||||
if (!window.RHINO_MODE) { console.log('[Bridge] →', type, payload); return }
|
if (!window.RHINO_MODE) { console.log('[Bridge] →', type, payload); return }
|
||||||
const json = JSON.stringify({ type, payload })
|
document.title = 'RHINOMSG::' + JSON.stringify({ type, payload })
|
||||||
document.title = 'RHINOMSG::' + json
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EbenenSettingsApp() {
|
export default function EbenenSettingsApp() {
|
||||||
const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {}
|
const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {}
|
||||||
|
|
||||||
|
const [ebenen, setEbenen] = useState(initial.ebenen || [])
|
||||||
|
const [hatchPatterns, setHatchPatterns] = useState(initial.hatchPatterns || ['Solid'])
|
||||||
|
const [selectedCode, setSelectedCode] = useState(initial.currentCode || initial.ebene?.code || '')
|
||||||
|
// Aktuell editiertes Draft. originalCode = Code beim Aufmachen des Editors
|
||||||
|
// (kann sich beim Save aendern wenn User CODE-Feld umbenannt hat).
|
||||||
|
const [originalCode, setOriginalCode] = useState(initial.currentCode || initial.ebene?.code || '')
|
||||||
|
const dialogKey = useRef(0) // Force Dialog-Remount beim Wechsel
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
onMessage('EBENEN_SETTINGS_STATE', ({ ebenen: list, hatchPatterns: hp }) => {
|
||||||
|
if (Array.isArray(list)) setEbenen(list)
|
||||||
|
if (Array.isArray(hp) && hp.length) setHatchPatterns(hp)
|
||||||
|
})
|
||||||
notifyReady()
|
notifyReady()
|
||||||
const blockContext = (ev) => ev.preventDefault()
|
const blockContext = (ev) => ev.preventDefault()
|
||||||
document.addEventListener('contextmenu', blockContext)
|
document.addEventListener('contextmenu', blockContext)
|
||||||
return () => document.removeEventListener('contextmenu', blockContext)
|
return () => document.removeEventListener('contextmenu', blockContext)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const ebene = initial.ebene || initial
|
const sortedEbenen = [...ebenen].sort((a, b) => {
|
||||||
const hatchPatterns = initial.hatchPatterns || ['Solid']
|
const ca = parseInt(a.code, 10), cb = parseInt(b.code, 10)
|
||||||
|
if (!isNaN(ca) && !isNaN(cb)) return ca - cb
|
||||||
|
return (a.code || '').localeCompare(b.code || '')
|
||||||
|
})
|
||||||
|
|
||||||
if (!ebene || typeof ebene !== 'object' || !ebene.code) {
|
const currentEbene = ebenen.find(e => e.code === selectedCode)
|
||||||
return <div style={{ padding: 20, color: 'var(--text-muted)' }}>Keine Daten</div>
|
|| ebenen.find(e => e.code === originalCode)
|
||||||
|
|| initial.ebene
|
||||||
|
|| null
|
||||||
|
|
||||||
|
if (!currentEbene) {
|
||||||
|
return <div style={{ padding: 20, color: 'var(--text-muted)', fontSize: 11 }}>Keine Ebene gefunden</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
const switchTo = (newCode, currentDraft) => {
|
||||||
|
if (!newCode || newCode === selectedCode) return
|
||||||
|
// Aktuelles Draft live persistieren bevor wir wechseln — wenn der User
|
||||||
|
// gerade etwas geaendert hatte ohne 'Übernehmen' zu druecken, geht es
|
||||||
|
// sonst verloren.
|
||||||
|
if (currentDraft && originalCode) {
|
||||||
|
bridgeSend('SAVE_KEEP', { ebene: currentDraft, originalCode })
|
||||||
|
}
|
||||||
|
setSelectedCode(newCode)
|
||||||
|
setOriginalCode(newCode)
|
||||||
|
dialogKey.current += 1 // Force-Remount → frisches Draft aus ebenen[newCode]
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EbenenSettingsDialog
|
<EbenenSettingsDialog
|
||||||
|
key={dialogKey.current}
|
||||||
embedded
|
embedded
|
||||||
ebene={ebene}
|
ebene={currentEbene}
|
||||||
hatchPatterns={hatchPatterns}
|
hatchPatterns={hatchPatterns}
|
||||||
onSave={(updated) => bridgeSend('SAVE', updated)}
|
onSave={(updated) => bridgeSend('SAVE', { ebene: updated, originalCode })}
|
||||||
onClose={() => bridgeSend('CANCEL', {})}
|
onClose={() => bridgeSend('CANCEL', {})}
|
||||||
|
pickerEbenen={sortedEbenen}
|
||||||
|
pickerSelected={currentEbene.code}
|
||||||
|
onPickEbene={(newCode, currentDraft) => switchTo(newCode, currentDraft)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -513,7 +513,7 @@ export default function ElementeApp() {
|
|||||||
padding: '8px 10px', borderBottom: '1px solid var(--border)',
|
padding: '8px 10px', borderBottom: '1px solid var(--border)',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
}}>
|
}}>
|
||||||
<span style={{ flex: 1, fontWeight: 600, letterSpacing: '0.08em' }}>ELEMENTE</span>
|
<span style={{ flex: 1, fontWeight: 600 }}>Elemente</span>
|
||||||
<span className="chip" style={{ fontSize: 8 }}>{elements.length}</span>
|
<span className="chip" style={{ fontSize: 8 }}>{elements.length}</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => exportRaeume()}
|
onClick={() => exportRaeume()}
|
||||||
|
|||||||
@@ -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 ContextMenu from './components/ContextMenu'
|
||||||
import {
|
import {
|
||||||
onMessage, notifyReady,
|
onMessage, notifyReady,
|
||||||
listLayouts, newLayout, deleteLayout, renameLayout, activateLayout,
|
listLayouts, deleteLayout, renameLayout, activateLayout,
|
||||||
addDetail, deleteDetail, bindAusschnitt, syncDetail, syncLayout,
|
addDetail, deleteDetail, bindAusschnitt, syncDetail, syncLayout,
|
||||||
setPageSize, exportPdf, exportPdfAll, exportPdfMany,
|
exportPdf, exportPdfAll, exportPdfMany,
|
||||||
addLayoutFolder, removeLayoutFolder, setLayoutFolder,
|
addLayoutFolder, removeLayoutFolder, setLayoutFolder,
|
||||||
|
openLayoutDialog,
|
||||||
} from './lib/rhinoBridge'
|
} from './lib/rhinoBridge'
|
||||||
|
|
||||||
const PAPER_SIZES = ['A4', 'A3', 'A2', 'A1', 'A0', 'Letter']
|
|
||||||
|
|
||||||
const PAPER_FORMATS_MM = {
|
const PAPER_FORMATS_MM = {
|
||||||
A0: [841, 1189], A1: [594, 841], A2: [420, 594], A3: [297, 420],
|
A0: [841, 1189], A1: [594, 841], A2: [420, 594], A3: [297, 420],
|
||||||
A4: [210, 297], Letter: [216, 279],
|
A4: [210, 297], Letter: [216, 279],
|
||||||
@@ -112,7 +111,6 @@ function EditableName({ value, onCommit, style, title, forceEdit, onEditDone })
|
|||||||
export default function LayoutsApp() {
|
export default function LayoutsApp() {
|
||||||
const [state, setState] = useState({ layouts: [], snapshots: [], details: {}, folders: [] })
|
const [state, setState] = useState({ layouts: [], snapshots: [], details: {}, folders: [] })
|
||||||
const [selectedId, setSelectedId] = useState(null)
|
const [selectedId, setSelectedId] = useState(null)
|
||||||
const [dialog, setDialog] = useState(null) // { mode: 'new' | 'edit', layout? }
|
|
||||||
const [checked, setChecked] = useState(new Set()) // Multi-Select IDs
|
const [checked, setChecked] = useState(new Set()) // Multi-Select IDs
|
||||||
const [collapsedFolders, setCollapsedFolders] = useState(new Set())
|
const [collapsedFolders, setCollapsedFolders] = useState(new Set())
|
||||||
const [draggingId, setDraggingId] = useState(null)
|
const [draggingId, setDraggingId] = useState(null)
|
||||||
@@ -187,8 +185,10 @@ export default function LayoutsApp() {
|
|||||||
{ divider: true },
|
{ divider: true },
|
||||||
{ label: 'Als PDF exportieren', icon: 'picture_as_pdf',
|
{ label: 'Als PDF exportieren', icon: 'picture_as_pdf',
|
||||||
onClick: () => exportPdf(l.id, 300) },
|
onClick: () => exportPdf(l.id, 300) },
|
||||||
{ label: 'Papierformat aendern', icon: 'aspect_ratio',
|
{ label: 'Papierformat ändern', icon: 'aspect_ratio',
|
||||||
onClick: () => setDialog({ mode: 'edit', layout: l }) },
|
onClick: () => openLayoutDialog('edit', {
|
||||||
|
id: l.id, name: l.name, width: l.widthMm, height: l.heightMm,
|
||||||
|
}) },
|
||||||
{ divider: true },
|
{ divider: true },
|
||||||
...(folders.length > 0 ? [
|
...(folders.length > 0 ? [
|
||||||
...folders.map(f => ({
|
...folders.map(f => ({
|
||||||
@@ -276,57 +276,6 @@ export default function LayoutsApp() {
|
|||||||
background: 'var(--bg-base)', color: 'var(--text-primary)',
|
background: 'var(--bg-base)', color: 'var(--text-primary)',
|
||||||
fontFamily: 'var(--font)', fontSize: 11,
|
fontFamily: 'var(--font)', fontSize: 11,
|
||||||
}}>
|
}}>
|
||||||
{/* Header */}
|
|
||||||
<div style={{
|
|
||||||
display: 'flex', alignItems: 'center', gap: 6,
|
|
||||||
padding: '8px 10px',
|
|
||||||
borderBottom: '1px solid var(--border)',
|
|
||||||
}}>
|
|
||||||
<span style={{ flex: 1, fontWeight: 600, letterSpacing: '0.08em' }}>LAYOUTS</span>
|
|
||||||
<button
|
|
||||||
onClick={handleExportSelection}
|
|
||||||
className="btn-icon-tonal"
|
|
||||||
disabled={checked.size === 0}
|
|
||||||
title={checked.size > 0
|
|
||||||
? `Auswahl (${checked.size}) als ein PDF exportieren`
|
|
||||||
: 'Erst Layouts ankreuzen'}
|
|
||||||
style={{ display: 'flex', alignItems: 'center', gap: 4 }}
|
|
||||||
>
|
|
||||||
<Icon name="picture_as_pdf" size={14} />
|
|
||||||
{checked.size > 0 && <span style={{ fontSize: 10 }}>({checked.size})</span>}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => exportPdfAll(300)}
|
|
||||||
className="btn-icon-tonal"
|
|
||||||
disabled={layouts.length === 0}
|
|
||||||
title="Alle Layouts als ein PDF exportieren"
|
|
||||||
>
|
|
||||||
<Icon name="picture_as_pdf" size={14} />
|
|
||||||
<span style={{ fontSize: 9, marginLeft: 2 }}>·∗</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleNewFolder}
|
|
||||||
className="btn-icon-tonal"
|
|
||||||
title="Neuer Ordner"
|
|
||||||
>
|
|
||||||
<Icon name="create_new_folder" size={14} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setDialog({ mode: 'new' })}
|
|
||||||
className="btn-add"
|
|
||||||
title="Neues Layout erstellen"
|
|
||||||
>
|
|
||||||
<Icon name="add" size={16} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => listLayouts()}
|
|
||||||
className="btn-icon-tonal"
|
|
||||||
title="Aktualisieren"
|
|
||||||
>
|
|
||||||
<Icon name="refresh" size={14} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden', padding: 8 }}>
|
<div style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden', padding: 8 }}>
|
||||||
{/* Layout-Liste */}
|
{/* Layout-Liste */}
|
||||||
{layouts.length === 0 && folders.length === 0 ? (
|
{layouts.length === 0 && folders.length === 0 ? (
|
||||||
@@ -340,7 +289,7 @@ export default function LayoutsApp() {
|
|||||||
<Icon name="dashboard" size={32} style={{ color: 'var(--text-muted)', opacity: 0.5 }} />
|
<Icon name="dashboard" size={32} style={{ color: 'var(--text-muted)', opacity: 0.5 }} />
|
||||||
<div style={{ marginTop: 8 }}>Noch keine Layouts.</div>
|
<div style={{ marginTop: 8 }}>Noch keine Layouts.</div>
|
||||||
<div style={{ marginTop: 4, fontSize: 10 }}>
|
<div style={{ marginTop: 4, fontSize: 10 }}>
|
||||||
Oben <Icon name="add" size={11} /> klicken um ein neues Layout anzulegen.
|
Unten <Icon name="add" size={11} /> klicken um ein neues Layout anzulegen.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -610,6 +559,72 @@ export default function LayoutsApp() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Sticky Footer: Anzahl + Aktionen */}
|
||||||
|
<div style={{
|
||||||
|
display: 'flex', alignItems: 'center', gap: 8,
|
||||||
|
padding: '8px 10px',
|
||||||
|
borderTop: '1px solid var(--border)',
|
||||||
|
background: 'var(--bg-panel)',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}>
|
||||||
|
<span className="chip" style={{
|
||||||
|
fontSize: 9, minWidth: 22, justifyContent: 'center',
|
||||||
|
}}>{layouts.length}</span>
|
||||||
|
<span style={{ fontSize: 10, color: 'var(--text-muted)', flex: 1 }}>
|
||||||
|
Layouts
|
||||||
|
</span>
|
||||||
|
{/* PDF-Aktionen: feste Breite damit das Auswahl-Counter den Footer
|
||||||
|
nicht horizontal verschiebt. */}
|
||||||
|
<button
|
||||||
|
onClick={handleExportSelection}
|
||||||
|
className="btn-icon-tonal"
|
||||||
|
disabled={checked.size === 0}
|
||||||
|
title={checked.size > 0
|
||||||
|
? `Auswahl (${checked.size}) als ein PDF exportieren`
|
||||||
|
: 'Erst Layouts ankreuzen'}
|
||||||
|
style={{ width: 'auto', minWidth: 26, padding: '0 8px', gap: 3 }}
|
||||||
|
>
|
||||||
|
<Icon name="picture_as_pdf" size={14} />
|
||||||
|
{checked.size > 0 && (
|
||||||
|
<span style={{ fontSize: 9, fontFamily: 'DM Mono, monospace' }}>
|
||||||
|
{checked.size}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => exportPdfAll(300)}
|
||||||
|
className="btn-icon-tonal"
|
||||||
|
disabled={layouts.length === 0}
|
||||||
|
title="Alle Layouts als ein PDF exportieren"
|
||||||
|
style={{ width: 'auto', minWidth: 26, padding: '0 8px', gap: 3 }}
|
||||||
|
>
|
||||||
|
<Icon name="picture_as_pdf" size={14} />
|
||||||
|
<span style={{ fontSize: 9 }}>·∗</span>
|
||||||
|
</button>
|
||||||
|
<div style={{ width: 1, height: 18, background: 'var(--border)' }} />
|
||||||
|
<button
|
||||||
|
onClick={handleNewFolder}
|
||||||
|
className="btn-icon-tonal"
|
||||||
|
title="Neuer Ordner"
|
||||||
|
>
|
||||||
|
<Icon name="create_new_folder" size={14} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => listLayouts()}
|
||||||
|
className="btn-icon-tonal"
|
||||||
|
title="Aktualisieren"
|
||||||
|
>
|
||||||
|
<Icon name="refresh" size={14} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => openLayoutDialog('new', null)}
|
||||||
|
className="btn-add"
|
||||||
|
title="Neues Layout erstellen"
|
||||||
|
>
|
||||||
|
<Icon name="add" size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Kontextmenue */}
|
{/* Kontextmenue */}
|
||||||
{ctxMenu && (
|
{ctxMenu && (
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
@@ -627,145 +642,6 @@ export default function LayoutsApp() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Layout-Dialog: New oder Edit (Papierformat aendern) */}
|
|
||||||
{dialog && (
|
|
||||||
<LayoutDialog
|
|
||||||
mode={dialog.mode}
|
|
||||||
layout={dialog.layout}
|
|
||||||
onCancel={() => setDialog(null)}
|
|
||||||
onSubmit={(p) => {
|
|
||||||
if (dialog.mode === 'new') {
|
|
||||||
newLayout(p.name, p.format, p.landscape, p.customWidth, p.customHeight)
|
|
||||||
} else {
|
|
||||||
setPageSize(dialog.layout.id, p.format, p.landscape, p.customWidth, p.customHeight)
|
|
||||||
}
|
|
||||||
setDialog(null)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function LayoutDialog({ mode, layout, onCancel, onSubmit }) {
|
|
||||||
const editing = mode === 'edit'
|
|
||||||
const [name, setName] = useState('')
|
|
||||||
const [format, setFormat] = useState('A3')
|
|
||||||
const [landscape, setLandscape] = useState(true)
|
|
||||||
const [cw, setCw] = useState('420') // mm
|
|
||||||
const [ch, setCh] = useState('297') // mm
|
|
||||||
|
|
||||||
// Wenn editieren: aktuelle Layout-Groesse pre-fillen (custom-Mode default)
|
|
||||||
useEffect(() => {
|
|
||||||
if (editing && layout) {
|
|
||||||
// BBox in Doc-Einheiten — wir kennen die Einheit nicht direkt im
|
|
||||||
// Frontend. Fuer den Edit-Modus zeigen wir die Groesse als Zahlen an
|
|
||||||
// und schicken sie als "custom" mit der mm-Annahme. Wenn das Doc nicht
|
|
||||||
// auf mm steht, ergibt sich eine kleine Konvertier-Unschaerfe — das
|
|
||||||
// Backend rechnet mm-Werte konsistent in Doc-Units um.
|
|
||||||
setFormat('custom')
|
|
||||||
setCw(String(Math.round(layout.width)))
|
|
||||||
setCh(String(Math.round(layout.height)))
|
|
||||||
}
|
|
||||||
}, [editing, layout])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{
|
|
||||||
position: 'fixed', inset: 0,
|
|
||||||
background: 'rgba(0,0,0,0.55)',
|
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
||||||
zIndex: 100,
|
|
||||||
}} onClick={(e) => { if (e.target === e.currentTarget) onCancel() }}>
|
|
||||||
<div style={{
|
|
||||||
width: 340, background: 'var(--bg-base)',
|
|
||||||
border: '1px solid var(--border)', borderRadius: 'var(--r-lg)',
|
|
||||||
display: 'flex', flexDirection: 'column', overflow: 'hidden',
|
|
||||||
}}>
|
|
||||||
<div style={{ padding: '10px 12px', borderBottom: '1px solid var(--border)',
|
|
||||||
fontWeight: 600 }}>
|
|
||||||
{editing ? `Papierformat: ${layout?.name}` : 'Neues Layout'}
|
|
||||||
</div>
|
|
||||||
<div style={{ padding: 12, display: 'flex', flexDirection: 'column', gap: 10 }}>
|
|
||||||
{!editing && (
|
|
||||||
<div>
|
|
||||||
<div style={labelXs}>Name</div>
|
|
||||||
<input type="text" value={name} onChange={(e) => setName(e.target.value)}
|
|
||||||
placeholder="z.B. Grundriss EG"
|
|
||||||
autoFocus
|
|
||||||
style={{ width: '100%', marginTop: 4 }} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div>
|
|
||||||
<div style={labelXs}>Papierformat</div>
|
|
||||||
<div style={{ display: 'flex', gap: 4, marginTop: 4, flexWrap: 'wrap' }}>
|
|
||||||
{PAPER_SIZES.map(f => (
|
|
||||||
<button key={f}
|
|
||||||
onClick={() => setFormat(f)}
|
|
||||||
className={format === f ? 'btn-contained' : 'btn-outlined'}
|
|
||||||
style={{ padding: '4px 10px', fontSize: 11 }}>
|
|
||||||
{f}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
<button
|
|
||||||
onClick={() => setFormat('custom')}
|
|
||||||
className={format === 'custom' ? 'btn-contained' : 'btn-outlined'}
|
|
||||||
style={{ padding: '4px 10px', fontSize: 11 }}>
|
|
||||||
Eigene
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{format === 'custom' ? (
|
|
||||||
<div>
|
|
||||||
<div style={labelXs}>Eigene Groesse (mm)</div>
|
|
||||||
<div style={{ display: 'flex', gap: 6, marginTop: 4, alignItems: 'center' }}>
|
|
||||||
<input type="text" value={cw} onChange={(e) => setCw(e.target.value)}
|
|
||||||
placeholder="Breite"
|
|
||||||
style={{ flex: 1, fontFamily: 'DM Mono, monospace', fontSize: 11 }} />
|
|
||||||
<span style={{ color: 'var(--text-muted)', fontSize: 10 }}>×</span>
|
|
||||||
<input type="text" value={ch} onChange={(e) => setCh(e.target.value)}
|
|
||||||
placeholder="Höhe"
|
|
||||||
style={{ flex: 1, fontFamily: 'DM Mono, monospace', fontSize: 11 }} />
|
|
||||||
<span style={{ color: 'var(--text-muted)', fontSize: 10 }}>mm</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div>
|
|
||||||
<div style={labelXs}>Ausrichtung</div>
|
|
||||||
<div style={{ display: 'flex', gap: 4, marginTop: 4 }}>
|
|
||||||
<button
|
|
||||||
onClick={() => setLandscape(true)}
|
|
||||||
className={landscape ? 'btn-contained' : 'btn-outlined'}
|
|
||||||
style={{ padding: '4px 10px', fontSize: 11, display: 'flex', gap: 4, alignItems: 'center' }}>
|
|
||||||
<Icon name="crop_landscape" size={12} /> Quer
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setLandscape(false)}
|
|
||||||
className={!landscape ? 'btn-contained' : 'btn-outlined'}
|
|
||||||
style={{ padding: '4px 10px', fontSize: 11, display: 'flex', gap: 4, alignItems: 'center' }}>
|
|
||||||
<Icon name="crop_portrait" size={12} /> Hoch
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div style={{ padding: 10, borderTop: '1px solid var(--border)',
|
|
||||||
display: 'flex', justifyContent: 'flex-end', gap: 6 }}>
|
|
||||||
<button onClick={onCancel}>Abbrechen</button>
|
|
||||||
<button className="btn-contained"
|
|
||||||
onClick={() => {
|
|
||||||
const payload = { name: name.trim(), format, landscape }
|
|
||||||
if (format === 'custom') {
|
|
||||||
const w = parseFloat(cw), h = parseFloat(ch)
|
|
||||||
if (!(w > 0) || !(h > 0)) { alert('Bitte gueltige Groesse eingeben.'); return }
|
|
||||||
payload.customWidth = w
|
|
||||||
payload.customHeight = h
|
|
||||||
}
|
|
||||||
onSubmit(payload)
|
|
||||||
}}>
|
|
||||||
{editing ? 'Anwenden' : 'Erstellen'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import {
|
|||||||
setMassstabDpi, detectMassstabDpi,
|
setMassstabDpi, detectMassstabDpi,
|
||||||
setView, setDisplayMode,
|
setView, setDisplayMode,
|
||||||
toggleOverrides, setOverridesPreset, openOverridesPanel,
|
toggleOverrides, setOverridesPreset, openOverridesPanel,
|
||||||
|
pickLayerCombination, saveLayerCombination,
|
||||||
|
deleteLayerCombination, openLayerCombinationsDialog,
|
||||||
openDossierSettings,
|
openDossierSettings,
|
||||||
} from './lib/rhinoBridge'
|
} from './lib/rhinoBridge'
|
||||||
|
|
||||||
@@ -125,6 +127,7 @@ export default function OberleisteApp() {
|
|||||||
overridesEnabled: false, overridesCount: 0,
|
overridesEnabled: false, overridesCount: 0,
|
||||||
cmdPrompt: '', cmdOptions: [],
|
cmdPrompt: '', cmdOptions: [],
|
||||||
overridesActivePreset: null, overridesPresets: [],
|
overridesActivePreset: null, overridesPresets: [],
|
||||||
|
layerCombinations: [], layerCombinationActive: null,
|
||||||
})
|
})
|
||||||
const [appliedScale, setAppliedScale] = useState(null)
|
const [appliedScale, setAppliedScale] = useState(null)
|
||||||
const appliedScaleRef = useRef(null)
|
const appliedScaleRef = useRef(null)
|
||||||
@@ -380,6 +383,60 @@ export default function OberleisteApp() {
|
|||||||
title="Overrides-Regel-Editor öffnen"
|
title="Overrides-Regel-Editor öffnen"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div style={sep} />
|
||||||
|
|
||||||
|
{/* ====== GRUPPE: EBENENKOMBINATION ====== */}
|
||||||
|
<span style={groupLabel}>Kombi</span>
|
||||||
|
<select
|
||||||
|
value={state.layerCombinationActive || '__none__'}
|
||||||
|
onChange={(e) => {
|
||||||
|
const v = e.target.value
|
||||||
|
if (v === '__configure__') { openLayerCombinationsDialog(); return }
|
||||||
|
if (v === '__delete__') {
|
||||||
|
if (state.layerCombinationActive &&
|
||||||
|
window.confirm(`Kombination "${state.layerCombinationActive}" löschen?`))
|
||||||
|
deleteLayerCombination(state.layerCombinationActive)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pickLayerCombination(v === '__none__' ? null : v)
|
||||||
|
}}
|
||||||
|
style={{ ...pillSelect, width: 140 }}
|
||||||
|
title={state.layerCombinationActive
|
||||||
|
? `Aktive Kombi: ${state.layerCombinationActive}`
|
||||||
|
: 'Keine Kombination — manuelle Sichtbarkeit'}
|
||||||
|
>
|
||||||
|
<option value="__none__">— Eigene —</option>
|
||||||
|
{(state.layerCombinations || []).map(name => (
|
||||||
|
<option key={name} value={name}>{name}</option>
|
||||||
|
))}
|
||||||
|
{state.layerCombinationActive && (
|
||||||
|
<>
|
||||||
|
<option disabled>──────────</option>
|
||||||
|
<option value="__delete__">🗑 Aktuelle löschen</option>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<option disabled>──────────</option>
|
||||||
|
<option value="__configure__">Bearbeiten…</option>
|
||||||
|
</select>
|
||||||
|
<ToolButton
|
||||||
|
onClick={() => {
|
||||||
|
const suggested = state.layerCombinationActive
|
||||||
|
|| `Kombi ${(state.layerCombinations || []).length + 1}`
|
||||||
|
const name = (window.prompt('Name für Ebenenkombination:', suggested) || '').trim()
|
||||||
|
if (!name) return
|
||||||
|
if ((state.layerCombinations || []).includes(name) &&
|
||||||
|
!window.confirm(`"${name}" überschreiben?`)) return
|
||||||
|
saveLayerCombination(name)
|
||||||
|
}}
|
||||||
|
icon="add"
|
||||||
|
title="Aktuelle Sichtbarkeit als neue Kombination speichern"
|
||||||
|
/>
|
||||||
|
<ToolButton
|
||||||
|
onClick={openLayerCombinationsDialog}
|
||||||
|
icon="edit"
|
||||||
|
title="Ebenenkombinationen bearbeiten"
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Spacer am rechten Rand */}
|
{/* Spacer am rechten Rand */}
|
||||||
<div style={{ flex: 1 }} />
|
<div style={{ flex: 1 }} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -56,10 +56,10 @@ export default function ZeichnungsebenenApp() {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Sichtbarkeit live anwenden bei Mode-/Visibility-Aenderungen
|
// Sichtbarkeit live anwenden bei Mode-/Visibility-/Lock-Aenderungen
|
||||||
const visibilityKey = useMemo(() => (
|
const visibilityKey = useMemo(() => (
|
||||||
activeId + '|' + zMode + '|' +
|
activeId + '|' + zMode + '|' +
|
||||||
zeichnungsebenen.map(z => `${z.id}:${z.visible !== false ? 1 : 0}`).join(',')
|
zeichnungsebenen.map(z => `${z.id}:${z.visible !== false ? 1 : 0}:${z.locked ? 1 : 0}`).join(',')
|
||||||
), [activeId, zMode, zeichnungsebenen])
|
), [activeId, zMode, zeichnungsebenen])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export default function AusschnittLayerDialog({
|
|||||||
snapName, layers, presets,
|
snapName, layers, presets,
|
||||||
onSave, onClose,
|
onSave, onClose,
|
||||||
onSavePreset, onDeletePreset,
|
onSavePreset, onDeletePreset,
|
||||||
|
embedded = false,
|
||||||
}) {
|
}) {
|
||||||
// Welche Kombination wird gerade angezeigt? null = aktueller Doc-State
|
// Welche Kombination wird gerade angezeigt? null = aktueller Doc-State
|
||||||
const [selectedPreset, setSelectedPreset] = useState(null)
|
const [selectedPreset, setSelectedPreset] = useState(null)
|
||||||
@@ -104,24 +105,29 @@ export default function AusschnittLayerDialog({
|
|||||||
onSave(draft)
|
onSave(draft)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const wrapperStyle = embedded
|
||||||
<div style={{
|
? { position: 'absolute', inset: 0, display: 'flex' }
|
||||||
position: 'absolute', inset: 0, zIndex: 150,
|
: { position: 'absolute', inset: 0, zIndex: 150,
|
||||||
background: 'var(--bg-overlay)',
|
background: 'var(--bg-overlay)',
|
||||||
display: 'flex', alignItems: 'flex-start', justifyContent: 'center',
|
display: 'flex', alignItems: 'flex-start', justifyContent: 'center',
|
||||||
paddingTop: 30,
|
paddingTop: 30 }
|
||||||
}}>
|
const cardStyle = embedded
|
||||||
<div style={{
|
? { flex: 1, display: 'flex', flexDirection: 'column',
|
||||||
background: 'var(--bg-dialog)',
|
background: 'var(--bg-dialog)', overflow: 'hidden' }
|
||||||
|
: { background: 'var(--bg-dialog)',
|
||||||
border: '1px solid var(--border)',
|
border: '1px solid var(--border)',
|
||||||
borderRadius: 'var(--r-lg)',
|
borderRadius: 'var(--r-lg)',
|
||||||
boxShadow: 'var(--shadow-3)',
|
boxShadow: 'var(--shadow-3)',
|
||||||
width: 'calc(100% - 24px)', maxWidth: 480,
|
width: 'calc(100% - 24px)', maxWidth: 480,
|
||||||
maxHeight: 'calc(100vh - 60px)',
|
maxHeight: 'calc(100vh - 60px)',
|
||||||
display: 'flex', flexDirection: 'column',
|
display: 'flex', flexDirection: 'column',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden' }
|
||||||
}}>
|
return (
|
||||||
{/* Header */}
|
<div style={wrapperStyle}>
|
||||||
|
<div style={cardStyle}>
|
||||||
|
{/* Header — im embedded-Modus weggelassen (Satellite-Fenster hat schon
|
||||||
|
seine native Title-Bar mit Close-Button) */}
|
||||||
|
{!embedded && (
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex', alignItems: 'center', gap: 8,
|
display: 'flex', alignItems: 'center', gap: 8,
|
||||||
padding: '12px 16px',
|
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>
|
<button onClick={onClose} style={{ color: 'var(--text-muted)', fontSize: 16, padding: '0 4px' }}>×</button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Preset-Auswahl */}
|
{/* Preset-Auswahl */}
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -235,7 +242,8 @@ export default function AusschnittLayerDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Layer-Liste */}
|
{/* Layer-Liste */}
|
||||||
<div style={{ flex: 1, overflowY: 'auto', minHeight: 200, maxHeight: '50vh' }}>
|
<div style={{ flex: 1, overflowY: 'auto', minHeight: 200,
|
||||||
|
maxHeight: embedded ? 'none' : '50vh' }}>
|
||||||
{filtered.length === 0 ? (
|
{filtered.length === 0 ? (
|
||||||
<div style={{ padding: '30px 14px', textAlign: 'center', color: 'var(--text-muted)', fontSize: 11 }}>
|
<div style={{ padding: '30px 14px', textAlign: 'center', color: 'var(--text-muted)', fontSize: 11 }}>
|
||||||
Keine Ebenen gefunden.
|
Keine Ebenen gefunden.
|
||||||
|
|||||||
@@ -255,9 +255,6 @@ function SortHeader({ label, sortKey, sortBy, sortDir, onSort, style }) {
|
|||||||
|
|
||||||
export default function EbenenManager({
|
export default function EbenenManager({
|
||||||
ebenen, activeCode, onActiveChange, onChange, mode, onModeChange, hatchPatterns,
|
ebenen, activeCode, onActiveChange, onChange, mode, onModeChange, hatchPatterns,
|
||||||
combinations = [], activeCombName = null,
|
|
||||||
onPickCombination, onSaveCurrentCombination, onDeleteCombination,
|
|
||||||
onEditCombinations, onUserVisibilityChange,
|
|
||||||
}) {
|
}) {
|
||||||
const [sortBy, setSortBy] = useState('code')
|
const [sortBy, setSortBy] = useState('code')
|
||||||
const [sortDir, setSortDir] = useState('asc')
|
const [sortDir, setSortDir] = useState('asc')
|
||||||
@@ -306,12 +303,10 @@ export default function EbenenManager({
|
|||||||
const handleToggleVisible = (code) => {
|
const handleToggleVisible = (code) => {
|
||||||
const cur = ebenen.find(e => e.code === code)
|
const cur = ebenen.find(e => e.code === code)
|
||||||
if (cur) updateByCode(code, { visible: !(cur.visible !== false) })
|
if (cur) updateByCode(code, { visible: !(cur.visible !== false) })
|
||||||
if (onUserVisibilityChange) onUserVisibilityChange()
|
|
||||||
}
|
}
|
||||||
const handleToggleLock = (code) => {
|
const handleToggleLock = (code) => {
|
||||||
const cur = ebenen.find(e => e.code === code)
|
const cur = ebenen.find(e => e.code === code)
|
||||||
if (cur) updateByCode(code, { locked: !cur.locked })
|
if (cur) updateByCode(code, { locked: !cur.locked })
|
||||||
if (onUserVisibilityChange) onUserVisibilityChange()
|
|
||||||
}
|
}
|
||||||
const handleColorChange = (code, color) => {
|
const handleColorChange = (code, color) => {
|
||||||
updateByCode(code, { color })
|
updateByCode(code, { color })
|
||||||
@@ -426,58 +421,6 @@ export default function EbenenManager({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Ebenenkombinationen — Label + Dropdown + Save-As-Plus */}
|
|
||||||
<div style={{
|
|
||||||
display: 'flex', flexDirection: 'column', gap: 4,
|
|
||||||
padding: '6px 14px',
|
|
||||||
background: 'var(--bg-section)',
|
|
||||||
borderBottom: '1px solid var(--border-light)',
|
|
||||||
}}>
|
|
||||||
<span className="label-xs">Ebenenkombination</span>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
||||||
<select
|
|
||||||
value={activeCombName || '__custom__'}
|
|
||||||
onChange={(ev) => {
|
|
||||||
const v = ev.target.value
|
|
||||||
if (v === '__custom__') return
|
|
||||||
if (v === '__delete__') {
|
|
||||||
if (activeCombName && onDeleteCombination) onDeleteCombination(activeCombName)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (onPickCombination) onPickCombination(v)
|
|
||||||
}}
|
|
||||||
style={{ flex: 1, minWidth: 0 }}
|
|
||||||
title={activeCombName ? `Aktiv: ${activeCombName}` : 'Eigene Sichtbarkeit (keine Kombination)'}
|
|
||||||
>
|
|
||||||
<option value="__custom__">{activeCombName ? activeCombName : 'Eigene'}</option>
|
|
||||||
{combinations.length > 0 && <option disabled>──────────</option>}
|
|
||||||
{combinations.map(p => (
|
|
||||||
<option key={p.name} value={p.name}>{p.name}</option>
|
|
||||||
))}
|
|
||||||
{activeCombName && combinations.some(p => p.name === activeCombName) && (
|
|
||||||
<>
|
|
||||||
<option disabled>──────────</option>
|
|
||||||
<option value="__delete__">🗑 Aktuelle löschen</option>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
<button
|
|
||||||
className="btn-icon-sm"
|
|
||||||
onClick={() => onSaveCurrentCombination && onSaveCurrentCombination()}
|
|
||||||
title="Aktuelle Sichtbarkeit als neue Kombination speichern"
|
|
||||||
>
|
|
||||||
<Icon name="add" size={14} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn-icon-sm"
|
|
||||||
onClick={() => onEditCombinations && onEditCombinations()}
|
|
||||||
title="Alle Kombinationen bearbeiten (Dialog)"
|
|
||||||
>
|
|
||||||
<Icon name="edit" size={13} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex', flexDirection: 'column', gap: 4,
|
display: 'flex', flexDirection: 'column', gap: 4,
|
||||||
padding: '6px 14px',
|
padding: '6px 14px',
|
||||||
@@ -512,7 +455,6 @@ export default function EbenenManager({
|
|||||||
const anyVisible = ebenen.some(e => e.visible !== false)
|
const anyVisible = ebenen.some(e => e.visible !== false)
|
||||||
// Wenn irgendeine sichtbar -> alle aus. Wenn keine sichtbar -> alle an.
|
// Wenn irgendeine sichtbar -> alle aus. Wenn keine sichtbar -> alle an.
|
||||||
onChange(ebenen.map(e => ({ ...e, visible: !anyVisible })))
|
onChange(ebenen.map(e => ({ ...e, visible: !anyVisible })))
|
||||||
if (onUserVisibilityChange) onUserVisibilityChange()
|
|
||||||
}}
|
}}
|
||||||
title={ebenen.every(e => e.visible !== false)
|
title={ebenen.every(e => e.visible !== false)
|
||||||
? 'Alle Ebenen ausblenden'
|
? 'Alle Ebenen ausblenden'
|
||||||
@@ -534,7 +476,6 @@ export default function EbenenManager({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
const anyLocked = ebenen.some(e => e.locked === true)
|
const anyLocked = ebenen.some(e => e.locked === true)
|
||||||
onChange(ebenen.map(e => ({ ...e, locked: !anyLocked })))
|
onChange(ebenen.map(e => ({ ...e, locked: !anyLocked })))
|
||||||
if (onUserVisibilityChange) onUserVisibilityChange()
|
|
||||||
}}
|
}}
|
||||||
title={ebenen.every(e => e.locked === true)
|
title={ebenen.every(e => e.locked === true)
|
||||||
? 'Alle Ebenen entsperren'
|
? 'Alle Ebenen entsperren'
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ function SectionLabel({ children }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EbenenSettingsDialog({ ebene, hatchPatterns = ['Solid'], onSave, onClose, embedded = false }) {
|
export default function EbenenSettingsDialog({
|
||||||
|
ebene, hatchPatterns = ['Solid'], onSave, onClose, embedded = false,
|
||||||
|
pickerEbenen = null, pickerSelected = null, onPickEbene = null,
|
||||||
|
}) {
|
||||||
const [draft, setDraft] = useState({
|
const [draft, setDraft] = useState({
|
||||||
...ebene,
|
...ebene,
|
||||||
fill: {
|
fill: {
|
||||||
@@ -107,7 +110,34 @@ export default function EbenenSettingsDialog({ ebene, hatchPatterns = ['Solid'],
|
|||||||
return (
|
return (
|
||||||
<div style={wrapperStyle}>
|
<div style={wrapperStyle}>
|
||||||
<div style={innerStyle}>
|
<div style={innerStyle}>
|
||||||
{/* Header */}
|
{/* Header — embedded zeigt nur das Ebenen-Picker-Dropdown (kein
|
||||||
|
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={{
|
<div style={{
|
||||||
display: 'flex', alignItems: 'center', gap: 6,
|
display: 'flex', alignItems: 'center', gap: 6,
|
||||||
padding: '10px 12px',
|
padding: '10px 12px',
|
||||||
@@ -122,6 +152,7 @@ export default function EbenenSettingsDialog({ ebene, hatchPatterns = ['Solid'],
|
|||||||
</span>
|
</span>
|
||||||
<button onClick={onClose} style={{ color: 'var(--text-muted)', fontSize: 16, padding: '0 4px', lineHeight: 1 }}>×</button>
|
<button onClick={onClose} style={{ color: 'var(--text-muted)', fontSize: 16, padding: '0 4px', lineHeight: 1 }}>×</button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Body */}
|
{/* Body */}
|
||||||
<div style={{ padding: '6px 12px 4px', overflowY: 'auto' }}>
|
<div style={{ padding: '6px 12px 4px', overflowY: 'auto' }}>
|
||||||
|
|||||||
@@ -1,23 +1,29 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
import Icon from './Icon'
|
import Icon from './Icon'
|
||||||
|
import ContextMenu from './ContextMenu'
|
||||||
import { openGeschossSettings, openGeschossDialog } from '../lib/rhinoBridge'
|
import { openGeschossSettings, openGeschossDialog } from '../lib/rhinoBridge'
|
||||||
|
|
||||||
function GeschossBadge({ name }) {
|
function GeschossBadge({ name }) {
|
||||||
return <span className="chip chip-info">{name}</span>
|
return <span className="chip chip-info">{name}</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
function ZeichnungsebeneRow({ z, active, mode, onClick, onToggleVisible, onSettings }) {
|
function ZeichnungsebeneRow({
|
||||||
// Eye-State auch fuer die aktive Zeichnungsebene anzeigen (User-Intention)
|
z, active, mode, onClick, onContextMenu,
|
||||||
|
onToggleVisible, onToggleLock, onDelete,
|
||||||
|
}) {
|
||||||
const eyeShown = mode !== 'active'
|
const eyeShown = mode !== 'active'
|
||||||
const isGeschoss = !!z.isGeschoss
|
const isGeschoss = !!z.isGeschoss
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
onContextMenu={onContextMenu}
|
||||||
style={{
|
style={{
|
||||||
display: 'flex', alignItems: 'center', gap: 8,
|
display: 'flex', alignItems: 'center', gap: 6,
|
||||||
padding: '4px 12px',
|
padding: '4px 12px',
|
||||||
margin: active ? '1px 6px' : '0',
|
margin: active ? '1px 6px' : '0',
|
||||||
background: active ? 'var(--active-dim)' : 'var(--bg-item)',
|
background: active ? 'var(--active-dim)'
|
||||||
// Pill-Form fuer die aktive Zeichnungsebene
|
: (z.visible !== false) ? 'var(--bg-item)'
|
||||||
|
: 'var(--bg-panel)',
|
||||||
borderRadius: active ? 999 : 0,
|
borderRadius: active ? 999 : 0,
|
||||||
borderLeft: active ? 'none' : '3px solid transparent',
|
borderLeft: active ? 'none' : '3px solid transparent',
|
||||||
borderBottom: active ? 'none' : '1px solid var(--border-light)',
|
borderBottom: active ? 'none' : '1px solid var(--border-light)',
|
||||||
@@ -31,7 +37,7 @@ function ZeichnungsebeneRow({ z, active, mode, onClick, onToggleVisible, onSetti
|
|||||||
fontWeight: active ? 700 : 500,
|
fontWeight: active ? 700 : 500,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: active ? 'var(--active-light)' : 'var(--text-label)',
|
color: active ? 'var(--active-light)' : 'var(--text-label)',
|
||||||
flex: 1,
|
flex: 1, minWidth: 0,
|
||||||
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
||||||
}}>{z.name}</span>
|
}}>{z.name}</span>
|
||||||
|
|
||||||
@@ -65,9 +71,16 @@ function ZeichnungsebeneRow({ z, active, mode, onClick, onToggleVisible, onSetti
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
className="btn-icon-xs"
|
className="btn-icon-xs"
|
||||||
onClick={(ev) => { ev.stopPropagation(); onSettings() }}
|
onClick={(ev) => { ev.stopPropagation(); onToggleLock() }}
|
||||||
title="Einstellungen"
|
title={z.locked ? 'Entsperren' : 'Sperren'}
|
||||||
><Icon name="settings" size={12} /></button>
|
style={{ color: z.locked ? 'var(--warn)' : undefined }}
|
||||||
|
><Icon name={z.locked ? 'lock' : 'lock_open'} size={12} /></button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="btn-icon-xs"
|
||||||
|
onClick={(ev) => { ev.stopPropagation(); onDelete() }}
|
||||||
|
title="Löschen"
|
||||||
|
><Icon name="close" size={12} /></button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -83,8 +96,7 @@ export default function GeschossManager({
|
|||||||
zeichnungsebenen, activeId, onActiveChange, onChange, recalcOkff,
|
zeichnungsebenen, activeId, onActiveChange, onChange, recalcOkff,
|
||||||
mode, onModeChange,
|
mode, onModeChange,
|
||||||
}) {
|
}) {
|
||||||
// dialogOpen-State entfaellt — Bearbeiten-Dialog laeuft jetzt als
|
const [ctxMenu, setCtxMenu] = useState(null) // { x, y, id }
|
||||||
// Satelliten-Fenster via openGeschossDialog().
|
|
||||||
|
|
||||||
const sorted = [...zeichnungsebenen].reverse()
|
const sorted = [...zeichnungsebenen].reverse()
|
||||||
const gesamthoehe = zeichnungsebenen
|
const gesamthoehe = zeichnungsebenen
|
||||||
@@ -93,9 +105,8 @@ export default function GeschossManager({
|
|||||||
|
|
||||||
const addQuick = () => {
|
const addQuick = () => {
|
||||||
// Standard: NICHT-Geschoss-Zeichnungsebene (z.B. Möblierung, Bemassung,
|
// Standard: NICHT-Geschoss-Zeichnungsebene (z.B. Möblierung, Bemassung,
|
||||||
// Plangrafik etc.). User kann via Row-Settings-Cog auf Geschoss
|
// Plangrafik etc.). User kann via Row-Kontextmenue auf Geschoss
|
||||||
// umschalten, oder via Bearbeiten-Dialog (Pencil) ein Geschoss
|
// umschalten oder via Bearbeiten-Dialog (Pencil) ein Geschoss erstellen.
|
||||||
// direkt erstellen.
|
|
||||||
const nonGeschossCount = zeichnungsebenen.filter(z => !z.isGeschoss).length
|
const nonGeschossCount = zeichnungsebenen.filter(z => !z.isGeschoss).length
|
||||||
const newZ = {
|
const newZ = {
|
||||||
id: `z_${Date.now()}`,
|
id: `z_${Date.now()}`,
|
||||||
@@ -103,7 +114,6 @@ export default function GeschossManager({
|
|||||||
isGeschoss: false,
|
isGeschoss: false,
|
||||||
visible: true,
|
visible: true,
|
||||||
}
|
}
|
||||||
console.log('[ZEICHNUNGSEBENEN-UI] addQuick →', { newZ, countBefore: zeichnungsebenen.length })
|
|
||||||
onChange([...zeichnungsebenen, newZ])
|
onChange([...zeichnungsebenen, newZ])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,6 +121,56 @@ export default function GeschossManager({
|
|||||||
onChange(zeichnungsebenen.map(z => z.id === id ? { ...z, visible: !(z.visible !== false) } : z))
|
onChange(zeichnungsebenen.map(z => z.id === id ? { ...z, visible: !(z.visible !== false) } : z))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -159,12 +219,21 @@ export default function GeschossManager({
|
|||||||
active={z.id === activeId}
|
active={z.id === activeId}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
onClick={() => onActiveChange(z.id)}
|
onClick={() => onActiveChange(z.id)}
|
||||||
|
onContextMenu={(ev) => openContextMenu(ev, z.id)}
|
||||||
onToggleVisible={() => toggleVisible(z.id)}
|
onToggleVisible={() => toggleVisible(z.id)}
|
||||||
onSettings={() => openGeschossSettings(z)}
|
onToggleLock={() => toggleLock(z.id)}
|
||||||
|
onDelete={() => remove(z.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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 updateAusschnittLayers(id, layers) { send('UPDATE_LAYERS', { id, layers }) }
|
||||||
export function saveLayerPreset(name, layers) { send('SAVE_PRESET', { name, layers }) }
|
export function saveLayerPreset(name, layers) { send('SAVE_PRESET', { name, layers }) }
|
||||||
export function deleteLayerPreset(name) { send('DELETE_PRESET', { name }) }
|
export function deleteLayerPreset(name) { send('DELETE_PRESET', { name }) }
|
||||||
|
export function openAusschnittSettings(id) { send('OPEN_SETTINGS', { id }) }
|
||||||
|
|
||||||
// --- Gestaltung-Panel ---
|
// --- Gestaltung-Panel ---
|
||||||
export function requestSelection() {
|
export function requestSelection() {
|
||||||
@@ -168,6 +169,11 @@ export function toggleOverrides(on) { send('TOGGLE_OVERRIDES', { enabled: !
|
|||||||
export function setOverridesPreset(name) { send('SET_OVERRIDES_PRESET', { name: name || null }) }
|
export function setOverridesPreset(name) { send('SET_OVERRIDES_PRESET', { name: name || null }) }
|
||||||
export function saveOverridesPreset(name) { send('SAVE_OVERRIDES_PRESET', { name }) }
|
export function saveOverridesPreset(name) { send('SAVE_OVERRIDES_PRESET', { name }) }
|
||||||
export function openOverridesPanel() { send('OPEN_OVERRIDES_PANEL', {}) }
|
export function openOverridesPanel() { send('OPEN_OVERRIDES_PANEL', {}) }
|
||||||
|
// Ebenenkombinationen (gehosted in Oberleiste, gleicher Store wie EBENEN)
|
||||||
|
export function pickLayerCombination(name) { send('PICK_LAYER_COMBINATION', { name: name || null }) }
|
||||||
|
export function saveLayerCombination(name) { send('SAVE_LAYER_COMBINATION', { name }) }
|
||||||
|
export function deleteLayerCombination(name) { send('DELETE_LAYER_COMBINATION', { name }) }
|
||||||
|
export function openLayerCombinationsDialog() { send('OPEN_LAYER_COMBINATIONS_DIALOG', {}) }
|
||||||
export function runCommand(cmd) { send('RUN_COMMAND', { cmd }) }
|
export function runCommand(cmd) { send('RUN_COMMAND', { cmd }) }
|
||||||
export function sendKeys(text, enter) { send('SEND_KEYS', { text, enter: enter !== false }) }
|
export function sendKeys(text, enter) { send('SEND_KEYS', { text, enter: enter !== false }) }
|
||||||
export function cancelCommand() { send('CANCEL_COMMAND', {}) }
|
export function cancelCommand() { send('CANCEL_COMMAND', {}) }
|
||||||
@@ -232,6 +238,7 @@ export function setPageSize(id, format, landscape, customWidth, customHeight) {
|
|||||||
customWidth, customHeight })
|
customWidth, customHeight })
|
||||||
}
|
}
|
||||||
export function exportPdf(id, dpi) { send('EXPORT_PDF', { id, dpi: dpi || 300 }) }
|
export function exportPdf(id, dpi) { send('EXPORT_PDF', { id, dpi: dpi || 300 }) }
|
||||||
|
export function openLayoutDialog(mode, layout) { send('OPEN_LAYOUT_DIALOG', { mode: mode || 'new', layout: layout || null }) }
|
||||||
export function exportPdfAll(dpi) { send('EXPORT_PDF', { dpi: dpi || 300 }) }
|
export function exportPdfAll(dpi) { send('EXPORT_PDF', { dpi: dpi || 300 }) }
|
||||||
export function exportPdfMany(ids, dpi) { send('EXPORT_PDF', { ids, dpi: dpi || 300 }) }
|
export function exportPdfMany(ids, dpi) { send('EXPORT_PDF', { ids, dpi: dpi || 300 }) }
|
||||||
export function addLayoutFolder(name) { send('ADD_FOLDER', { name }) }
|
export function addLayoutFolder(name) { send('ADD_FOLDER', { name }) }
|
||||||
@@ -304,7 +311,9 @@ export function applyVisibility(activeZ, zeichnungsebenen, activeCode, ebenen, z
|
|||||||
const zList = Array.isArray(a.zeichnungsebenen) ? a.zeichnungsebenen : []
|
const zList = Array.isArray(a.zeichnungsebenen) ? a.zeichnungsebenen : []
|
||||||
const eList = Array.isArray(a.ebenen) ? a.ebenen : []
|
const eList = Array.isArray(a.ebenen) ? a.ebenen : []
|
||||||
const slimZ = zList.map(z => ({
|
const slimZ = zList.map(z => ({
|
||||||
id: z.id, name: z.name, visible: z.visible !== false,
|
id: z.id, name: z.name,
|
||||||
|
visible: z.visible !== false,
|
||||||
|
locked: z.locked === true,
|
||||||
}))
|
}))
|
||||||
const slimE = eList.map(e => ({
|
const slimE = eList.map(e => ({
|
||||||
code: e.code, visible: e.visible !== false, locked: e.locked === true,
|
code: e.code, visible: e.visible !== false, locked: e.locked === true,
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import ZeichnungsebenenApp from './ZeichnungsebenenApp.jsx'
|
|||||||
import GeschossSettingsApp from './GeschossSettingsApp.jsx'
|
import GeschossSettingsApp from './GeschossSettingsApp.jsx'
|
||||||
import EbenenSettingsApp from './EbenenSettingsApp.jsx'
|
import EbenenSettingsApp from './EbenenSettingsApp.jsx'
|
||||||
import GeschossDialogApp from './GeschossDialogApp.jsx'
|
import GeschossDialogApp from './GeschossDialogApp.jsx'
|
||||||
|
import LayerCombinationsApp from './LayerCombinationsApp.jsx'
|
||||||
|
import AusschnittSettingsApp from './AusschnittSettingsApp.jsx'
|
||||||
|
import LayoutDialogApp from './LayoutDialogApp.jsx'
|
||||||
import GestaltungApp from './GestaltungApp.jsx'
|
import GestaltungApp from './GestaltungApp.jsx'
|
||||||
import AusschnitteApp from './AusschnitteApp.jsx'
|
import AusschnitteApp from './AusschnitteApp.jsx'
|
||||||
import MassstabApp from './MassstabApp.jsx'
|
import MassstabApp from './MassstabApp.jsx'
|
||||||
@@ -30,6 +33,9 @@ const RootApp = mode === 'gestaltung' ? GestaltungApp
|
|||||||
: mode === 'geschoss_settings' ? GeschossSettingsApp
|
: mode === 'geschoss_settings' ? GeschossSettingsApp
|
||||||
: mode === 'ebenen_settings' ? EbenenSettingsApp
|
: mode === 'ebenen_settings' ? EbenenSettingsApp
|
||||||
: mode === 'geschoss_dialog' ? GeschossDialogApp
|
: mode === 'geschoss_dialog' ? GeschossDialogApp
|
||||||
|
: mode === 'layer_combinations' ? LayerCombinationsApp
|
||||||
|
: mode === 'ausschnitt_settings' ? AusschnittSettingsApp
|
||||||
|
: mode === 'layout_dialog' ? LayoutDialogApp
|
||||||
: App
|
: App
|
||||||
|
|
||||||
window.onerror = function (msg, src, line, col, err) {
|
window.onerror = function (msg, src, line, col, err) {
|
||||||
|
|||||||
Reference in New Issue
Block a user