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:
+499
-102
@@ -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"))
|
||||
|
||||
Reference in New Issue
Block a user