Panels poliert: Ebenenkombi in Oberleiste, Satelliten-Dialoge, Caps weg, Perf

- Ebenenkombination raus aus Ebenen-Panel, in Oberleiste-Topbar +
  Editor-Satellite (AusschnittLayerDialog embedded). doc.Strings
  haelt active_comb_name, auto-clear bei manueller Eye/Lock-Aenderung.
- EbenenSettingsDialog jetzt Satellite mit Ebene-Picker-Dropdown
  (auto-save on switch via SAVE_KEEP).
- Per-Ausschnitt Einstellungen-Satellite (Massstab, Display, Overrides,
  Ebenenkombi). Alte 'Sichtbarkeit bearbeiten'-Option entfernt.
- Layouts/Ausschnitte: Top-Header weg, Sticky-Footer mit Anzahl +
  Aktionen. LayoutDialog ist jetzt Satellite mit Format-Live-Preview.
- Panel-Captions + Default-Ebenen-Namen auf Mixed-Case (Ausschnitte,
  Ebenen, Waende ...). Nur DOSSIER bleibt caps.
- DimensionenApp: Card-Optik raus, REF-Wuerfel mit Kreisen statt
  Quadraten + Hover-Scale.
- GeschossManager angeglichen an EbenenManager: Rechtsklick-Menue,
  Lock-Button, Delete-X, Duplizieren. layer_builder honoriert z.locked.
- Active Sublayer folgt jetzt dem Geschoss-Wechsel (gleicher Code
  unter neuem Parent).

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-19 03:58:28 +02:00
parent e3918cb155
commit 95031ee2c0
29 changed files with 1708 additions and 713 deletions
+151 -2
View File
@@ -334,6 +334,7 @@ class AusschnittBridge(panel_base.BaseBridge):
elif t == "UPDATE_LAYERS": self._update_layers(p.get("id"), p.get("layers") or [])
elif t == "SAVE_PRESET": self._save_preset(p.get("name"), p.get("layers") or [])
elif t == "DELETE_PRESET": self._delete_preset(p.get("name"))
elif t == "OPEN_SETTINGS": self._open_settings_window(p.get("id"))
def _load(self, doc):
raw = doc.Strings.GetValue(_STORE_KEY)
@@ -537,8 +538,42 @@ class AusschnittBridge(panel_base.BaseBridge):
if view is None: return
vp = view.ActiveViewport
_apply_camera(vp, snap.get("camera"))
_apply_layers_global(doc, snap.get("layers", []))
# Layer-Sichtbarkeit: bevorzugt die referenzierte Ebenenkombi (live —
# zeigt aktuelle Kombi-Definition). Fallback: snap.layers (per-snap
# eingefrorener Zustand).
kombi = (snap.get("layerCombination") or "").strip()
if kombi:
try:
import rhinopanel
rhinopanel.apply_layer_preset_by_name(doc, kombi)
except Exception as ex:
print("[AUSSCHNITTE] kombi-apply '{}':".format(kombi), ex)
_apply_layers_global(doc, snap.get("layers", []))
else:
_apply_layers_global(doc, snap.get("layers", []))
# Eigene Sichtbarkeit → active_comb_name clearen
try:
import rhinopanel
rhinopanel.set_active_comb_name(doc, None)
rhinopanel._notify_oberleiste_combs()
except Exception: pass
_apply_dossier_state(doc, snap.get("dossier") or snap.get("pause") or {})
# Overrides: nur anwenden wenn das Snap "applyOverrides" gesetzt hat.
# Sonst bleibt der aktuelle User-Override-State unangetastet.
if snap.get("applyOverrides"):
try:
import overrides
overrides.set_enabled(doc, bool(snap.get("overridesEnabled")))
overrides.set_active_preset(doc, snap.get("overridesPreset") or None)
# Oberleiste-Cache invalidieren damit Topbar das neue Preset zeigt
try:
b = sc.sticky.get("oberleiste_bridge")
if b is not None:
b._cached_overrides = None
b._send_state(force=True)
except Exception: pass
except Exception as ex:
print("[AUSSCHNITTE] overrides-apply:", ex)
# Viewport ZUERST umbenennen — der per-Viewport-Massstab in massstab.py
# wird unter vp.Name geschluesselt. Erst nach dem Rename schreibt
# _apply_scale unter dem neuen Namen, sonst landet der Wert beim alten
@@ -703,6 +738,120 @@ class AusschnittBridge(panel_base.BaseBridge):
self._store(doc, snaps)
self._send_list()
def _open_settings_window(self, snap_id):
"""Oeffnet ein Satelliten-Fenster (Eto.Form + WebView) mit dem
Ausschnittseinstellungen-Dialog. Lets User editieren: Massstab,
Display-Mode, Overrides, Ebenenkombi."""
if not snap_id: return
doc = Rhino.RhinoDoc.ActiveDoc
snap = next((s for s in self._load(doc) if s.get("id") == snap_id), None)
if not snap:
print("[AUSSCHNITTE] open_settings: snap nicht gefunden", snap_id)
return
outer = self
bridge_holder = {"form": None, "id": snap_id}
panel_base.register_and_open("ausschnitte", "AUSSCHNITTE", PANEL_GUID_STR, AusschnittBridge,
def _payload():
d = Rhino.RhinoDoc.ActiveDoc
sn = next((s for s in outer._load(d) if s.get("id") == bridge_holder["id"]), None)
if sn is None: sn = {}
# Listen fuer Dropdowns
display_modes = []
try:
import oberleiste
display_modes = oberleiste._list_display_modes()
except Exception as ex:
print("[AUSSCHNITTE] display_modes:", ex)
overrides_presets = []
try:
import overrides
overrides_presets = [item.get("name") for item in overrides.list_presets() if item.get("name")]
except Exception as ex:
print("[AUSSCHNITTE] overrides_presets:", ex)
layer_kombis = []
try:
import rhinopanel
layer_kombis = rhinopanel.list_layer_preset_names(d)
except Exception as ex:
print("[AUSSCHNITTE] layer_kombis:", ex)
cam = sn.get("camera") or {}
return {
"snap": {
"id": sn.get("id"),
"name": sn.get("name"),
"scale": sn.get("scale", ""),
"displayMode": cam.get("displayMode"),
"displayModeName": cam.get("displayModeName"),
"applyOverrides": bool(sn.get("applyOverrides", False)),
"overridesEnabled": bool(sn.get("overridesEnabled", False)),
"overridesPreset": sn.get("overridesPreset") or "",
"layerCombination": sn.get("layerCombination") or "",
},
"displayModes": display_modes,
"overridesPresets": overrides_presets,
"layerKombis": layer_kombis,
}
def _persist(settings):
d = Rhino.RhinoDoc.ActiveDoc
snaps = outer._load(d)
sid = bridge_holder["id"]
target = next((s for s in snaps if s.get("id") == sid), None)
if target is None:
print("[AUSSCHNITTE] persist settings: snap weg"); return
# Massstab
sc_val = (settings.get("scale") or "").strip()
target["scale"] = sc_val
# Display Mode in camera nested
cam = target.get("camera") or {}
dm_id = settings.get("displayMode")
dm_nm = settings.get("displayModeName")
if dm_id is not None: cam["displayMode"] = dm_id or None
if dm_nm is not None: cam["displayModeName"] = dm_nm or None
target["camera"] = cam
# Overrides
target["applyOverrides"] = bool(settings.get("applyOverrides"))
target["overridesEnabled"] = bool(settings.get("overridesEnabled"))
target["overridesPreset"] = (settings.get("overridesPreset") or "").strip()
# Ebenenkombi
target["layerCombination"] = (settings.get("layerCombination") or "").strip()
outer._store(d, snaps)
outer._send_list()
print("[AUSSCHNITTE] Settings fuer '{}' aktualisiert".format(target.get("name")))
class _AusschnittSettingsBridge(panel_base.BaseBridge):
def __init__(self):
panel_base.BaseBridge.__init__(self, "ausschnitt_settings")
def _on_ready(self):
self._send_state()
def _send_state(self):
self.send("AUSSCHNITT_SETTINGS_STATE", _payload())
def handle(self, data):
if not isinstance(data, dict): return
t = data.get("type", "")
p = data.get("payload") or {}
if t == "READY":
self._on_ready()
elif t == "SAVE":
_persist(p.get("settings") or {})
try:
f = bridge_holder.get("form")
if f is not None: f.Close()
except Exception: pass
elif t == "CANCEL":
try:
f = bridge_holder.get("form")
if f is not None: f.Close()
except Exception: pass
b = _AusschnittSettingsBridge()
bridge_holder["form"] = panel_base.open_satellite_window(
"ausschnitt_settings",
params=_payload(),
title="Ausschnitt: {}".format(snap.get("name", "")),
size=(420, 540),
bridge=b)
panel_base.register_and_open("ausschnitte", "Ausschnitte", PANEL_GUID_STR, AusschnittBridge,
icon_spec=("crop", "#c87050"))
+1 -1
View File
@@ -608,6 +608,6 @@ def _bridge_factory():
return b
panel_base.register_and_open("dimensionen", "DIMENSIONEN", PANEL_GUID_STR,
panel_base.register_and_open("dimensionen", "Dimensionen", PANEL_GUID_STR,
_bridge_factory,
icon_spec=("aspect_ratio", "#9e7050"))
+20 -3
View File
@@ -366,9 +366,9 @@ def _find_ebene_sublayer_name(doc, keywords, default_code, default_name,
def _layer_path_axis(doc, geschoss_name):
"""Wand-Achse + Volumen — Sublayer 'WÄNDE' (Code 20)."""
"""Wand-Achse + Volumen — Sublayer 'Wände' (Code 20)."""
sub = _find_ebene_sublayer_name(doc, ["wand", "wände", "waende"],
"20", "WÄNDE",
"20", "Wände",
default_color="#0a0a0a", default_lw=0.50)
return "{}::{}".format(geschoss_name, sub)
@@ -4543,6 +4543,23 @@ class ElementeBridge(panel_base.BaseBridge):
elif t == "DELETE_ELEMENT": self._delete_wall(p.get("id"))
elif t == "REGENERATE_ALL": self._regenerate_all()
def _notify_active_geschoss(self):
"""Schlanker Partial-Push: nur activeGeschoss + activeGeschossName.
Wird vom Ebenen-Bridge bei Geschoss-Wechsel gerufen die Element-
Liste ist davon nicht betroffen, ein voller _send_state mit Re-
Enumeration aller Smart-Elemente (200+ in echten Projekten) waere
teuer und unnoetig. React-State macht Shallow-Merge, der Rest des
States bleibt."""
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return
try:
self.send("STATE", {
"activeGeschoss": _active_geschoss_id(doc),
"activeGeschossName": _active_geschoss_name(doc),
})
except Exception as ex:
print("[ELEMENTE] _notify_active_geschoss:", ex)
def _send_state(self):
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None:
@@ -8509,6 +8526,6 @@ def _bridge_factory():
return b
panel_base.register_and_open("elemente", "ELEMENTE", PANEL_GUID_STR,
panel_base.register_and_open("elemente", "Elemente", PANEL_GUID_STR,
_bridge_factory,
icon_spec=("foundation", "#5fa896"))
+1 -1
View File
@@ -1695,5 +1695,5 @@ def _bridge_factory():
return b
panel_base.register_and_open("gestaltung", "GESTALTUNG", PANEL_GUID_STR, _bridge_factory,
panel_base.register_and_open("gestaltung", "Gestaltung", PANEL_GUID_STR, _bridge_factory,
icon_spec=("palette", "#5fa896"))
+6
View File
@@ -578,6 +578,7 @@ def apply_visibility(doc, zeichnungsebenen, ebenen, active_z_id, active_code, z_
children = children_by_parent.get(parent.Id, [])
is_active_z = z["id"] == active_z_id
z_visible_flag = z.get("visible", True)
z_locked_flag = bool(z.get("locked", False))
# Z-Mode -> Parent-Zustand
if is_active_z:
@@ -593,6 +594,11 @@ def apply_visibility(doc, zeichnungsebenen, ebenen, active_z_id, active_code, z_
else: # grey
p_vis, p_grey, p_lock = True, True, False
# Per-Z explizites Sperren ueberlagert (auch fuer die aktive Z) — wer
# eine Geschoss-Ebene sperrt, will dass Klicks ins Leere gehen.
if z_locked_flag:
p_lock = True
parent_changed = False
if parent.IsVisible != p_vis:
parent.IsVisible = p_vis
+65 -1
View File
@@ -281,6 +281,7 @@ class LayoutsBridge(panel_base.BaseBridge):
elif t == "ADD_FOLDER": self._add_folder(p.get("name"))
elif t == "REMOVE_FOLDER": self._remove_folder(p.get("name"))
elif t == "SET_FOLDER": self._set_folder(p.get("id"), p.get("folder") or "")
elif t == "OPEN_LAYOUT_DIALOG": self._open_layout_dialog(p)
# --- State-Snapshot -----------------------------------------------------
@@ -737,6 +738,69 @@ class LayoutsBridge(panel_base.BaseBridge):
print("[LAYOUTS] sync layout:", ex)
self._send_state()
def _open_layout_dialog(self, p):
"""Oeffnet ein Satelliten-Fenster mit dem Layout-Erstellen/Bearbeiten
Dialog. mode = 'new' | 'edit'. Bei 'edit' wird `layout` (id, name,
width, height) mitgeschickt."""
outer = self
mode = (p.get("mode") or "new")
layout = p.get("layout") or None
bridge_holder = {"form": None}
def _apply(payload):
if mode == "new":
outer._new_layout({
"name": payload.get("name") or "",
"format": payload.get("format") or "A3",
"landscape": bool(payload.get("landscape", True)),
"customWidth": payload.get("customWidth"),
"customHeight": payload.get("customHeight"),
})
elif mode == "edit" and layout and layout.get("id"):
outer._set_page_size({
"id": layout.get("id"),
"format": payload.get("format") or "A3",
"landscape": bool(payload.get("landscape", True)),
"customWidth": payload.get("customWidth"),
"customHeight": payload.get("customHeight"),
})
class _LayoutDialogBridge(panel_base.BaseBridge):
def __init__(self):
panel_base.BaseBridge.__init__(self, "layout_dialog")
def _on_ready(self):
self.send("LAYOUT_DIALOG_STATE", {
"mode": mode,
"layout": layout,
})
def handle(self, data):
if not isinstance(data, dict): return
t = data.get("type", "")
pp = data.get("payload") or {}
if t == "READY":
self._on_ready()
elif t == "SAVE":
_apply(pp)
try:
f = bridge_holder.get("form")
if f is not None: f.Close()
except Exception: pass
elif t == "CANCEL":
try:
f = bridge_holder.get("form")
if f is not None: f.Close()
except Exception: pass
b = _LayoutDialogBridge()
title = "Neues Layout" if mode == "new" else "Papierformat: {}".format(
(layout or {}).get("name", ""))
bridge_holder["form"] = panel_base.open_satellite_window(
"layout_dialog",
params={"mode": mode, "layout": layout},
title=title,
size=(440, 380),
bridge=b)
def _bridge_factory():
b = LayoutsBridge()
@@ -744,6 +808,6 @@ def _bridge_factory():
return b
panel_base.register_and_open("layouts", "LAYOUTS", PANEL_GUID_STR,
panel_base.register_and_open("layouts", "Layouts", PANEL_GUID_STR,
_bridge_factory,
icon_spec=("view_quilt", "#7a5fa8"))
+1 -1
View File
@@ -1090,7 +1090,7 @@ def _bridge_factory():
# register_standalone_panel() aufrufen oder die Zeile darunter auskommentieren.
def register_standalone_panel():
panel_base.register_and_open("massstab", "MASSSTAB", PANEL_GUID_STR, _bridge_factory,
panel_base.register_and_open("massstab", "Massstab", PANEL_GUID_STR, _bridge_factory,
icon_spec=("straighten", "#c87050"))
# register_standalone_panel() # auskommentiert — Standard ist OBERLEISTE
+44 -1
View File
@@ -20,6 +20,7 @@ if _HERE not in sys.path:
import panel_base
import massstab
import overrides
import rhinopanel
PANEL_GUID_STR = "7e1f6a5b-8e2f-4f3c-d5e6-f70819203b51"
OVERRIDES_PANEL_GUID_STR = "8f2a7b6c-9f3a-4f4d-e6f7-08192a3c4d62"
@@ -770,6 +771,7 @@ class OberleisteBridge(panel_base.BaseBridge):
self._last_prompt = ""
self._last_state_sig = None # Fingerprint des letzten Push — dedupe
self._cached_overrides = None # (enabled, count) — invalidiert bei Toggle/Update
self._cached_combinations = None # (names, active) — invalidiert bei jeder Comb-Aenderung
# Command-Liste einmalig laden (kann teuer sein -> cachen)
try:
self._all_commands = _list_all_command_names()
@@ -920,6 +922,34 @@ class OberleisteBridge(panel_base.BaseBridge):
except Exception as ex:
print("[OBERLEISTE] open_as_window Overrides:", ex)
# --- Ebenenkombinationen ----------------------------------------
elif t == "PICK_LAYER_COMBINATION":
doc = Rhino.RhinoDoc.ActiveDoc
name = (p.get("name") or "").strip()
if name:
rhinopanel.apply_layer_preset_by_name(doc, name)
else:
# "Eigene" — kein Apply, nur active_comb_name clearen
rhinopanel.set_active_comb_name(doc, None)
self._cached_combinations = None
self._send_state(force=True)
elif t == "SAVE_LAYER_COMBINATION":
doc = Rhino.RhinoDoc.ActiveDoc
name = (p.get("name") or "").strip()
if name:
rhinopanel.save_current_as_layer_preset(doc, name)
self._cached_combinations = None
self._send_state(force=True)
elif t == "DELETE_LAYER_COMBINATION":
doc = Rhino.RhinoDoc.ActiveDoc
rhinopanel.delete_layer_preset(doc, p.get("name") or "")
self._cached_combinations = None
self._send_state(force=True)
elif t == "OPEN_LAYER_COMBINATIONS_DIALOG":
try: rhinopanel.open_layer_combinations_window()
except Exception as ex:
print("[OBERLEISTE] open layer-combinations:", ex)
# --- Command-Line Integration -----------------------------------
elif t == "RUN_COMMAND":
cmd = (p.get("cmd") or "").strip()
@@ -1035,6 +1065,18 @@ class OberleisteBridge(panel_base.BaseBridge):
info["overridesActivePreset"],
_presets_tuple) = self._cached_overrides
info["overridesPresets"] = list(_presets_tuple)
# Ebenenkombinationen — cached (Liste + active). Invalidiert bei
# PICK/SAVE/DELETE und durch Cross-Bridge-Notify aus rhinopanel.py.
if self._cached_combinations is None:
try:
names = tuple(rhinopanel.list_layer_preset_names(doc))
active = rhinopanel.get_active_comb_name(doc)
self._cached_combinations = (names, active)
except Exception:
self._cached_combinations = ((), None)
_names_tuple, _active_comb = self._cached_combinations
info["layerCombinations"] = list(_names_tuple)
info["layerCombinationActive"] = _active_comb
# Command-Line State
prompt = _get_command_prompt()
info["cmdPrompt"] = prompt
@@ -1057,6 +1099,7 @@ class OberleisteBridge(panel_base.BaseBridge):
info["overridesEnabled"], info["overridesCount"],
info.get("overridesActivePreset"),
tuple(info.get("overridesPresets") or ()),
_names_tuple, _active_comb,
prompt,
)
if not force and sig == self._last_state_sig:
@@ -1197,5 +1240,5 @@ def _bridge_factory():
return b
panel_base.register_and_open("oberleiste", "OBERLEISTE", PANEL_GUID_STR, _bridge_factory,
panel_base.register_and_open("oberleiste", "Oberleiste", PANEL_GUID_STR, _bridge_factory,
icon_spec=("menu", "#2f5d54"))
+1 -1
View File
@@ -266,7 +266,7 @@ def open_as_window():
sc.sticky["overrides_bridge"] = b
panel_base.open_satellite_window(
"overrides",
title="OVERRIDES",
title="Overrides",
size=(760, 580),
bridge=b)
+499 -102
View File
@@ -103,6 +103,243 @@ def _broadcast_state(doc=None, hatch_patterns=None):
except Exception: pass
# --- Layer-Kombinationen: Modul-Level Helpers ------------------------------
# Diese Helfer werden sowohl von EbenenBridge (Ebenen-Panel) als auch von
# OberleisteBridge (Top-Bar) und LayerCombinationsBridge (Satelliten-Editor)
# benutzt. doc.Strings ist die einzige Quelle der Wahrheit; nach jedem Write
# rufen die Caller _broadcast_state(doc) + invalidate cross-bridge caches.
_PRESETS_KEY = "dossier_layer_presets"
_ACTIVE_COMB_KEY = "dossier_layer_active_comb"
def load_layer_presets(doc):
raw = doc.Strings.GetValue(_PRESETS_KEY)
if not raw: return []
try:
data = json.loads(raw)
return data if isinstance(data, list) else []
except Exception:
return []
def store_layer_presets(doc, presets):
try:
doc.Strings.SetString(_PRESETS_KEY,
json.dumps(presets, ensure_ascii=False))
except Exception as ex:
print("[EBENEN] store_layer_presets:", ex)
def get_active_comb_name(doc):
try:
v = doc.Strings.GetValue(_ACTIVE_COMB_KEY)
return v if v else None
except Exception:
return None
def set_active_comb_name(doc, name):
try:
doc.Strings.SetString(_ACTIVE_COMB_KEY, name or "")
except Exception as ex:
print("[EBENEN] set_active_comb_name:", ex)
def list_layer_preset_names(doc):
return [p.get("name") for p in load_layer_presets(doc)
if isinstance(p, dict) and p.get("name")]
def _notify_oberleiste_combs():
"""Cache der Oberleiste invalidieren + force-send. Wird gerufen wenn
die Combinations-Liste oder activeCombName sich aendert."""
try:
b = sc.sticky.get("oberleiste_bridge")
if b is not None:
b._cached_combinations = None
b._send_state(force=True)
except Exception as ex:
print("[EBENEN] notify oberleiste combs:", ex)
def _notify_layer_combinations_editor():
"""Satelliten-Fenster (Editor) informieren falls offen — Layer-/Preset-
Liste hat sich geaendert."""
try:
b = sc.sticky.get("layer_combinations_bridge")
if b is not None: b._send_state()
except Exception as ex:
print("[EBENEN] notify layer-combinations editor:", ex)
def apply_layer_preset_by_name(doc, name):
"""Laedt Preset `name` und wendet es an. Setzt active_comb_name.
Liefert True wenn erfolgreich."""
if not name: return False
presets = load_layer_presets(doc)
preset = next((p for p in presets if p.get("name") == name), None)
if preset is None:
print("[EBENEN] apply_layer_preset_by_name: '{}' nicht gefunden".format(name))
return False
payload = {
"layers": preset.get("layers") or [],
"dossierEbenen": preset.get("dossierEbenen"),
"dossierZeichnungsebenen": preset.get("dossierZeichnungsebenen"),
}
# Routing: wenn die EbenenBridge existiert, delegiere — die hat den
# vollen Eye-State-Pfad inkl. STATE_SYNC + Redraw. Sonst inline applien.
eb = sc.sticky.get("ebenen_bridge_ref")
if eb is not None:
try:
eb._apply_combination(payload)
except Exception as ex:
print("[EBENEN] apply via bridge:", ex)
return False
else:
# Fallback: direkt doc.Strings + doc.Layer setzen (kein Bridge offen)
_apply_layer_preset_inline(doc, payload)
set_active_comb_name(doc, name)
_notify_oberleiste_combs()
return True
def _apply_layer_preset_inline(doc, payload):
"""Fallback wenn keine EbenenBridge offen ist — minimaler Layer-State-
Pfad. Setzt doc.Strings + doc.Layer.IsVisible direkt."""
pe = payload.get("dossierEbenen")
pz = payload.get("dossierZeichnungsebenen")
try:
e_raw = doc.Strings.GetValue("dossier_ebenen") or "[]"
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen") or "[]"
e_list = json.loads(e_raw) or []
z_list = json.loads(z_raw) or []
if isinstance(pe, list):
by_code = {x.get("code"): x for x in pe if isinstance(x, dict) and x.get("code")}
for e in e_list:
if not isinstance(e, dict): continue
s = by_code.get(e.get("code"))
if s is None: continue
e["visible"] = bool(s.get("visible", True))
e["locked"] = bool(s.get("locked", False))
if isinstance(pz, list):
by_id = {x.get("id"): x for x in pz if isinstance(x, dict) and x.get("id")}
for z in z_list:
if not isinstance(z, dict): continue
s = by_id.get(z.get("id"))
if s is None: continue
z["visible"] = bool(s.get("visible", True))
doc.Strings.SetString("dossier_ebenen", json.dumps(e_list, ensure_ascii=False))
doc.Strings.SetString("dossier_zeichnungsebenen", json.dumps(z_list, ensure_ascii=False))
except Exception as ex:
print("[EBENEN] inline preset-apply (eye-state):", ex)
# Layer-ID-Pfad als Sekundaer (AUSSCHNITTE-Kompat)
layer_states = payload.get("layers") or []
if layer_states:
by_id = {}
try:
for layer in doc.Layers:
if not layer.IsDeleted: by_id[str(layer.Id)] = layer
except Exception: pass
_set_processing(True)
try:
for ls in layer_states:
layer = by_id.get(ls.get("id"))
if layer is None: continue
try:
want_vis = bool(ls.get("visible", True))
want_lck = bool(ls.get("locked", False))
if layer.IsVisible != want_vis: layer.IsVisible = want_vis
if layer.IsLocked != want_lck: layer.IsLocked = want_lck
except Exception: pass
finally:
_set_processing(False)
try: doc.Views.Redraw()
except Exception: pass
_broadcast_state(doc)
def save_current_as_layer_preset(doc, name):
"""Speichert die aktuellen Eye-States als Preset. Setzt active_comb_name."""
name = (name or "").strip()
if not name: return False
# 1) doc.Layer state (Kompat mit AUSSCHNITTE)
layers = []
try:
for layer in doc.Layers:
if layer is None or layer.IsDeleted: continue
layers.append({
"id": str(layer.Id),
"visible": bool(layer.IsVisible),
"locked": bool(layer.IsLocked),
})
except Exception as ex:
print("[EBENEN] save_current_as_layer_preset enum:", ex)
# 2) Eye-States aus dossier_ebenen / dossier_zeichnungsebenen
pe_state, pz_state = [], []
try:
e_raw = doc.Strings.GetValue("dossier_ebenen")
if e_raw:
for e in (json.loads(e_raw) or []):
if isinstance(e, dict) and e.get("code"):
pe_state.append({
"code": e["code"],
"visible": bool(e.get("visible", True)),
"locked": bool(e.get("locked", False)),
})
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
if z_raw:
for z in (json.loads(z_raw) or []):
if isinstance(z, dict) and z.get("id"):
pz_state.append({
"id": z["id"],
"visible": bool(z.get("visible", True)),
})
except Exception as ex:
print("[EBENEN] save_current_as_layer_preset eye-states:", ex)
presets = load_layer_presets(doc)
new_data = {
"name": name,
"layers": layers,
"dossierEbenen": pe_state,
"dossierZeichnungsebenen": pz_state,
}
existing = next((p for p in presets if p.get("name") == name), None)
if existing is not None:
existing.update(new_data)
else:
presets.append(new_data)
store_layer_presets(doc, presets)
set_active_comb_name(doc, name)
_notify_oberleiste_combs()
_notify_layer_combinations_editor()
print("[EBENEN] '{}' gespeichert: {} Layer + {} Ebenen Eye-State".format(
name, len(layers), len(pe_state)))
return True
def delete_layer_preset(doc, name):
name = (name or "").strip()
if not name: return False
presets = [p for p in load_layer_presets(doc) if p.get("name") != name]
store_layer_presets(doc, presets)
if get_active_comb_name(doc) == name:
set_active_comb_name(doc, None)
_notify_oberleiste_combs()
_notify_layer_combinations_editor()
print("[EBENEN] Kombination '{}' geloescht".format(name))
return True
def clear_active_comb_name(doc):
"""Wird vom EbenenBridge SET_VISIBILITY / APPLY-Pfad gerufen wenn der
User per Hand etwas am Layer-State aendert — dann passt das Preset nicht
mehr und wir markieren 'Eigene'."""
if get_active_comb_name(doc):
set_active_comb_name(doc, None)
_notify_oberleiste_combs()
class EbenenBridge(panel_base.BaseBridge):
"""Gemeinsame Bridge-Klasse fuer beide Panels (Ebenen + Zeichnungsebenen).
Mode bestimmt nur welches WebView die Bridge bedient + welcher sticky-Slot
@@ -266,40 +503,80 @@ class EbenenBridge(panel_base.BaseBridge):
on_save=on_save)
def _open_ebenen_settings(self, ebene, hatch_patterns):
"""Oeffnet ein echtes Rhino-Fenster mit dem EbenenSettingsDialog."""
"""Oeffnet ein echtes Rhino-Fenster mit dem EbenenSettingsDialog.
Mit Dropdown zum Wechsel zwischen Ebenen; jeder Switch persistiert
die aktuelle Ebene live (SAVE_KEEP), Schliess-/Übernehmen-Knopf
persistiert + schliesst (SAVE)."""
if not isinstance(ebene, dict) or not ebene.get("code"):
print("[EBENEN] open_ebenen_settings: kein Ebene-Payload")
return
old_code = ebene["code"]
def on_save(updated):
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return
e_raw = doc.Strings.GetValue("dossier_ebenen")
if not e_raw:
print("[EBENEN] save_ebene: kein e-Store"); return
try:
e_list = json.loads(e_raw)
except Exception as ex:
print("[EBENEN] save_ebene JSON:", ex); return
replaced = False
for i, e in enumerate(e_list):
if isinstance(e, dict) and e.get("code") == old_code:
e_list[i] = updated
replaced = True
break
if not replaced:
print("[EBENEN] save_ebene: code {} nicht gefunden".format(old_code))
return
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
try: z_list = json.loads(z_raw) if z_raw else []
except Exception: z_list = []
self._apply(z_list, e_list, save_z=False, save_e=True)
panel_base.open_satellite_window(
bridge_holder = {"form": None}
apply_self = self
class _EbenenSettingsBridge(panel_base.BaseBridge):
def __init__(self):
panel_base.BaseBridge.__init__(self, "ebenen_settings")
def _on_ready(self):
self._send_state()
def _send_state(self):
doc = Rhino.RhinoDoc.ActiveDoc
e_raw = doc.Strings.GetValue("dossier_ebenen") if doc else None
try: e_list = json.loads(e_raw) if e_raw else []
except Exception: e_list = []
self.send("EBENEN_SETTINGS_STATE", {
"ebenen": e_list,
"hatchPatterns": hatch_patterns,
})
def handle(self, data):
if not isinstance(data, dict): return
t = data.get("type", "")
p = data.get("payload") or {}
if t == "READY":
self._on_ready()
elif t == "SAVE_KEEP":
self._persist(p)
elif t == "SAVE":
self._persist(p)
try:
f = bridge_holder.get("form")
if f is not None: f.Close()
except Exception: pass
elif t == "CANCEL":
try:
f = bridge_holder.get("form")
if f is not None: f.Close()
except Exception: pass
def _persist(self, p):
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return
updated = p.get("ebene") or {}
orig_code = p.get("originalCode") or updated.get("code")
if not (isinstance(updated, dict) and updated.get("code")): return
e_raw = doc.Strings.GetValue("dossier_ebenen")
if not e_raw: return
try: e_list = json.loads(e_raw)
except Exception as ex:
print("[EBENEN] save_ebene JSON:", ex); return
replaced = False
for i, e in enumerate(e_list):
if isinstance(e, dict) and e.get("code") == orig_code:
e_list[i] = updated
replaced = True
break
if not replaced:
print("[EBENEN] save_ebene: code {} nicht gefunden".format(orig_code))
return
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
try: z_list = json.loads(z_raw) if z_raw else []
except Exception: z_list = []
apply_self._apply(z_list, e_list, save_z=False, save_e=True)
self._send_state()
b = _EbenenSettingsBridge()
bridge_holder["form"] = panel_base.open_satellite_window(
"ebenen_settings",
params={"ebene": ebene, "hatchPatterns": hatch_patterns},
title="Ebene: {}_{}".format(ebene.get("code", ""), ebene.get("name", "")),
params={"currentCode": ebene["code"], "hatchPatterns": hatch_patterns},
title="Ebenen-Einstellungen",
size=(420, 600),
on_save=on_save)
bridge=b)
def _open_geschoss_dialog(self, zeichnungsebenen):
"""Oeffnet den vollen GeschossDialog (Mehrfach-Editor) als
@@ -416,6 +693,9 @@ class EbenenBridge(panel_base.BaseBridge):
self._update_clipping()
print("[EBENEN] _apply: send APPLY_OK")
self.send("APPLY_OK", {})
# Strukturelle Aenderung (neue/umbenannte/geloeschte Ebene) → aktives
# Preset passt nicht mehr exakt.
clear_active_comb_name(doc)
# Anderes Panel (Zeichnungsebenen/Ebenen) ueber den neuen State
# informieren — sonst hinkt es hinter der DOM-Persistenz her.
_broadcast_state(doc)
@@ -476,6 +756,7 @@ class EbenenBridge(panel_base.BaseBridge):
s = z_state.get(z.get("id"))
if s is not None:
m["visible"] = s.get("visible", True)
m["locked"] = s.get("locked", False)
merged_z.append(m)
merged_e = []
for e in e_full:
@@ -486,11 +767,33 @@ class EbenenBridge(panel_base.BaseBridge):
m["visible"] = s.get("visible", True)
m["locked"] = s.get("locked", False)
merged_e.append(m)
# Detect whether the merge actually changed any visible/locked values.
# Wenn nicht: das ist nur der Echo-Roundtrip eines apply_layer_preset
# (React-State == doc.Strings → kein User-Click) und wir wollen das
# aktive Preset NICHT clearen.
def _vis_lock_changed(old, new):
old_by = {x.get("id") or x.get("code"): x for x in old if isinstance(x, dict)}
for nx in new:
if not isinstance(nx, dict): continue
key = nx.get("id") or nx.get("code")
if key is None: continue
ox = old_by.get(key)
if ox is None: continue
if (ox.get("visible", True) != nx.get("visible", True)
or ox.get("locked", False) != nx.get("locked", False)):
return True
return False
any_changed = (_vis_lock_changed(z_full, merged_z)
or _vis_lock_changed(e_full, merged_e))
if has_new_structural:
print("[EBENEN] _apply_visibility: structural change pending → skip save (waiting for APPLY)")
else:
doc.Strings.SetString("dossier_zeichnungsebenen", json.dumps(merged_z, ensure_ascii=False))
doc.Strings.SetString("dossier_ebenen", json.dumps(merged_e, ensure_ascii=False))
# User hat per Hand Eye/Lock geaendert → aktives Preset passt nicht
# mehr, auf "Eigene" zuruecksetzen.
if any_changed:
clear_active_comb_name(doc)
# zMode + eMode persistieren, damit bei Split-Send (nur eine
# Panel-Slice) der andere Mode aus dem Doc-Storage faellt anstatt
# auf den Default zu rutschen.
@@ -504,6 +807,24 @@ class EbenenBridge(panel_base.BaseBridge):
if not isinstance(active_z, dict): active_z = {}
active_z_id = active_z.get("id") or doc.Strings.GetValue("dossier_active_id")
active_code = p.get("activeCode") or doc.Strings.GetValue("dossier_active_code")
# Dedupe: identisches SET_VISIBILITY (z.B. STATE_SYNC-Echo nach
# Preset-Apply) loopt sonst unnoetig durch alle ~100 Doc-Layer.
# Signatur aus active-id/code + mode + vis/lock-Liste.
def _sig(zlist, elist):
zs = tuple((z.get("id"),
bool(z.get("visible", True)),
bool(z.get("locked", False)))
for z in zlist if isinstance(z, dict))
es = tuple((e.get("code"),
bool(e.get("visible", True)),
bool(e.get("locked", False)))
for e in elist if isinstance(e, dict))
return (active_z_id, active_code, z_mode, e_mode, zs, es)
cur_sig = _sig(merged_z, merged_e)
if sc.sticky.get("_vis_last_sig") == cur_sig and not any_changed:
# Nichts Neues — Rhino-Layer-State ist schon korrekt.
return
sc.sticky["_vis_last_sig"] = cur_sig
layer_builder.apply_visibility(
doc, merged_z, merged_e, active_z_id, active_code, z_mode, e_mode)
# Cross-Panel-Sync NUR wenn wir nicht in einem structural-pending
@@ -515,17 +836,29 @@ class EbenenBridge(panel_base.BaseBridge):
def _set_active_zeichnungsebene(self, z):
doc = Rhino.RhinoDoc.ActiveDoc
z_id = z.get("id", "")
# Vorigen Stand merken um redundante teure Operationen zu sparen
prev_active_id = doc.Strings.GetValue("dossier_active_id") or ""
doc.Strings.SetString("dossier_active_id", z_id)
# Aktiven Sublayer auf die GLEICHE Ebene unter dem neuen Geschoss
# umschalten — wenn User auf "20 Wände" steht und das Geschoss
# wechselt, soll Rhino's aktiver Layer "1OG::20_Wände" werden statt
# auf der vorigen Geschoss-Ebene haengen zu bleiben.
self._ensure_active_sublayer()
# Cross-Panel-Sync: Ebenen-Panel muss aktive Geschoss-Auswahl
# mitbekommen falls es im "active"-Filter-Mode laeuft.
_broadcast_state(doc)
# Clipping ggf. mitziehen
self._update_clipping(active_z=z)
# Clipping nur antasten wenn entweder das alte oder das neue Geschoss
# eine Clipping-Plane hatte — sonst sparen wir Plane-Delete + Build
# + View-Redraw bei jedem Geschoss-Wechsel ganz.
if self._needs_clipping_update(doc, prev_active_id, z):
self._update_clipping(active_z=z)
# Elemente-Panel informieren: das aktive Geschoss hat gewechselt,
# neue Elemente sollen jetzt automatisch dort verlinkt werden.
# Wichtig: NICHT _send_state() rufen (re-enumeriert alle Elemente,
# 200+ in echten Projekten = spuerbar). Schlanker Partial-Push.
try:
eb = sc.sticky.get("elemente_bridge")
if eb is not None: eb._send_state()
if eb is not None: eb._notify_active_geschoss()
except Exception: pass
if not (z.get("isGeschoss") and z.get("okff") is not None):
return
@@ -546,12 +879,33 @@ class EbenenBridge(panel_base.BaseBridge):
plane.XAxis, plane.YAxis,
)
vp.SetConstructionPlane(new_plane)
view.Redraw()
updated += 1
except Exception as ex:
print("[EBENEN] CPlane fehler ({}): {}".format(vp.Name if vp else "?", ex))
# KEIN doc.Views.Redraw() hier — die folgende SET_VISIBILITY-Round-
# trip (30 ms debounce in React) feuert ohnehin layer_builder
# .apply_visibility() das am Ende selbst redrawt. Sparen wir uns
# einen doppelten Full-Repaint pro Geschoss-Klick.
print("[EBENEN] CPlane Z={} auf {} Top-Style View(s) gesetzt".format(okff, updated))
def _needs_clipping_update(self, doc, prev_active_id, new_z):
"""Liefert True wenn entweder das alte oder das neue Geschoss
hasClipping=True hat. Sonst kann _update_clipping skipped werden
(Plane existiert nicht und muss auch nicht neu gebaut werden)."""
new_has = bool(new_z.get("hasClipping"))
if new_has:
return True
if not prev_active_id:
return False
try:
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
if not z_raw: return False
for z in (json.loads(z_raw) or []):
if isinstance(z, dict) and z.get("id") == prev_active_id:
return bool(z.get("hasClipping"))
except Exception: pass
return False
def _toggle_clipping_for_active(self, enabled):
"""Setzt hasClipping fuer das aktuell aktive Geschoss + persistiert in
doc.Strings + triggert plane-update. Wird vom React-Toggle 'Clipping
@@ -704,20 +1058,10 @@ class EbenenBridge(panel_base.BaseBridge):
_PRESETS_KEY = "dossier_layer_presets"
def _load_presets(self, doc):
raw = doc.Strings.GetValue(self._PRESETS_KEY)
if not raw: return []
try:
data = json.loads(raw)
return data if isinstance(data, list) else []
except Exception:
return []
return load_layer_presets(doc)
def _store_presets(self, doc, presets):
try:
doc.Strings.SetString(self._PRESETS_KEY,
json.dumps(presets, ensure_ascii=False))
except Exception as ex:
print("[EBENEN] _store_presets:", ex)
store_layer_presets(doc, presets)
def _send_combination(self):
"""Schickt aktuelles Layer-State + alle Presets ans Frontend."""
@@ -917,7 +1261,7 @@ class EbenenBridge(panel_base.BaseBridge):
name = (name or "").strip()
if not name: return
doc = Rhino.RhinoDoc.ActiveDoc
presets = self._load_presets(doc)
presets = load_layer_presets(doc)
clean = []
for ls in (layers or []):
lid = ls.get("id")
@@ -932,7 +1276,9 @@ class EbenenBridge(panel_base.BaseBridge):
existing["layers"] = clean
else:
presets.append({"name": name, "layers": clean})
self._store_presets(doc, presets)
store_layer_presets(doc, presets)
_notify_oberleiste_combs()
_notify_layer_combinations_editor()
print("[EBENEN] Kombination '{}' gespeichert ({} Layer)".format(name, len(clean)))
def _save_current_as_preset(self, name):
@@ -944,67 +1290,118 @@ class EbenenBridge(panel_base.BaseBridge):
layers (doc.Layer-Liste) wird parallel mitgespeichert fuer Kompat
mit AUSSCHNITTE (das vom doc.Layer-State liest)."""
name = (name or "").strip()
if not name: return
save_current_as_layer_preset(Rhino.RhinoDoc.ActiveDoc, name)
def _delete_preset(self, name):
delete_layer_preset(Rhino.RhinoDoc.ActiveDoc, name)
class LayerCombinationsBridge(panel_base.BaseBridge):
"""Bridge fuer das Satelliten-Fenster mit dem grossen Ebenenkombinationen-
Editor (AusschnittLayerDialog). Wird vom Oberleiste-Bridge geoeffnet bei
OPEN_LAYER_COMBINATIONS_DIALOG. State wird beim READY-Event geschickt und
bei jeder Aenderung re-emittet."""
def __init__(self):
panel_base.BaseBridge.__init__(self, "layer_combinations")
def _on_ready(self):
self._send_state()
def _send_state(self):
doc = Rhino.RhinoDoc.ActiveDoc
# 1) doc.Layer state (Kompat mit AUSSCHNITTE)
layers = []
if doc is None: return
layers_out = []
try:
for layer in doc.Layers:
if layer is None or layer.IsDeleted: continue
layers.append({
"id": str(layer.Id),
"visible": bool(layer.IsVisible),
"locked": bool(layer.IsLocked),
lid = str(layer.Id)
try: fp = layer.FullPath or layer.Name
except Exception: fp = layer.Name or ""
try: col = "#%02x%02x%02x" % (layer.Color.R, layer.Color.G, layer.Color.B)
except Exception: col = "#888888"
layers_out.append({
"id": lid,
"name": layer.Name,
"fullPath": fp,
"color": col,
"visible": bool(layer.IsVisible),
"locked": bool(layer.IsLocked),
})
layers_out.sort(key=lambda x: x["fullPath"])
except Exception as ex:
print("[EBENEN] _save_current_as_preset enum:", ex)
# 2) Eye-States aus dossier_ebenen / dossier_zeichnungsebenen
pe_state = []
pz_state = []
try:
e_raw = doc.Strings.GetValue("dossier_ebenen")
if e_raw:
for e in (json.loads(e_raw) or []):
if isinstance(e, dict) and e.get("code"):
pe_state.append({
"code": e["code"],
"visible": bool(e.get("visible", True)),
"locked": bool(e.get("locked", False)),
})
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)))
print("[LAYER-COMB] enum:", ex)
self.send("LAYER_COMBINATIONS_STATE", {
"layers": layers_out,
"presets": load_layer_presets(doc),
})
def _delete_preset(self, name):
name = (name or "").strip()
if not name: return
def handle(self, data):
if not isinstance(data, dict): return
t = data.get("type", "")
p = data.get("payload") or {}
if not isinstance(p, dict): p = {}
doc = Rhino.RhinoDoc.ActiveDoc
presets = [p for p in self._load_presets(doc) if p.get("name") != name]
self._store_presets(doc, presets)
print("[EBENEN] Kombination '{}' geloescht".format(name))
if t == "READY" or t == "REQUEST_STATE":
self._on_ready()
elif t == "APPLY_COMBINATION":
# Editor wendet eine Layer-State-Liste an (ohne Preset-Name —
# also "Eigene"). Wir delegieren an die Ebenen-Bridge wenn offen,
# sonst inline. activeCombName auf None setzen.
eb = sc.sticky.get("ebenen_bridge_ref")
if eb is not None:
try: eb._apply_combination(p)
except Exception as ex:
print("[LAYER-COMB] apply via bridge:", ex)
else:
_apply_layer_preset_inline(doc, p)
set_active_comb_name(doc, None)
_notify_oberleiste_combs()
self._send_state()
elif t == "SAVE_PRESET":
# Editor speichert die im Dialog kuratierte Liste unter Namen.
name = (p.get("name") or "").strip()
layers = p.get("layers") or []
if name:
presets = load_layer_presets(doc)
clean = []
for ls in layers:
lid = ls.get("id")
if not lid: continue
clean.append({
"id": lid,
"visible": bool(ls.get("visible", True)),
"locked": bool(ls.get("locked", False)),
})
existing = next((pp for pp in presets if pp.get("name") == name), None)
if existing is not None:
existing["layers"] = clean
else:
presets.append({"name": name, "layers": clean})
store_layer_presets(doc, presets)
_notify_oberleiste_combs()
self._send_state()
elif t == "DELETE_PRESET":
delete_layer_preset(doc, p.get("name") or "")
self._send_state()
elif t == "CANCEL":
try:
form = sc.sticky.get("layer_combinations_form")
if form is not None: form.Close()
except Exception: pass
def open_layer_combinations_window():
"""Oeffnet den Editor als echtes Rhino-Fenster (Eto.Form + WebView).
Wird vom Oberleiste-Bridge bei OPEN_LAYER_COMBINATIONS_DIALOG gerufen."""
b = LayerCombinationsBridge()
sc.sticky["layer_combinations_bridge"] = b
form = panel_base.open_satellite_window(
"layer_combinations",
title="Ebenenkombinationen",
size=(540, 640),
bridge=b)
sc.sticky["layer_combinations_form"] = form
def _ebenen_bridge_factory():
@@ -1087,8 +1484,8 @@ def _install_layer_listener(bridge):
print("[EBENEN] Layer-Listener aktiv")
panel_base.register_and_open("ebenen", "EBENEN", PANEL_GUID_STR, _ebenen_bridge_factory,
panel_base.register_and_open("ebenen", "Ebenen", PANEL_GUID_STR, _ebenen_bridge_factory,
icon_spec=("layers", "#3a6fa8"))
panel_base.register_and_open("zeichnungsebenen", "ZEICHNUNGSEBENEN", PANEL_GUID_STR_Z,
panel_base.register_and_open("zeichnungsebenen", "Zeichnungsebenen", PANEL_GUID_STR_Z,
_zeichnungsebenen_bridge_factory,
icon_spec=("levels", "#3a6fa8"))
+1 -1
View File
@@ -54,5 +54,5 @@ def _bridge_factory():
return WerkzeugeBridge()
panel_base.register_and_open("werkzeuge", "WERKZEUGE", PANEL_GUID_STR, _bridge_factory,
panel_base.register_and_open("werkzeuge", "Werkzeuge", PANEL_GUID_STR, _bridge_factory,
icon_spec=("build", "#3a6fa8"))