Files
DOSSIER/rhino/rhinopanel.py
T
karim 53fea10cba Clipping: Geschoss-Wechsel loescht Plane nicht mehr
Bug: React's SET_ACTIVE-Message schickt nur Minimal-Payload
`{id, name, isGeschoss, okff}` — ohne hasClipping/schnitthoehe.
`_update_clipping` las `enabled = active_z.get("hasClipping")` aus
diesem Minimal-Payload → False → Plane geloescht. Beim Zurueck-
wechseln auf EG mit aktiviertem Clipping war die Plane weg
obwohl der Toggle im Panel weiter „aktiv" zeigte.

Fix: `_update_clipping` nimmt nur die `id` aus dem uebergebenen
Hint (oder aus `dossier_active_id`) und holt sich den vollen
Geschoss-Record aus `dossier_zeichnungsebenen` doc.String. Damit
sind hasClipping, schnitthoehe, hoehe, visible immer verfuegbar.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 23:26:23 +02:00

897 lines
38 KiB
Python

#! python 3
# -*- coding: utf-8 -*-
"""
rhinopanel.py
Oeffnet das EBENEN-Panel (Zeichnungsebenen + globale Ebenen).
"""
import os
import sys
import json
import Rhino
import scriptcontext as sc
_HERE = os.path.dirname(os.path.abspath(__file__))
if _HERE not in sys.path:
sys.path.insert(0, _HERE)
import panel_base
import layer_builder
PANEL_GUID_STR = "3a7f2e1d-4b8c-4d9e-a1b2-c3d4e5f60718"
# Loop-Guard fuer Layer-Events (verhindert Endlos-Schleife bei eigenen Aenderungen)
def _is_processing():
return bool(sc.sticky.get("ebenen_processing_layer", False))
def _set_processing(v):
sc.sticky["ebenen_processing_layer"] = bool(v)
def _hatch_pattern_names(doc):
"""Liefert alle Hatch-Pattern-Namen aus doc.HatchPatterns als Liste."""
out = []
try:
for i in range(doc.HatchPatterns.Count):
try:
hp = doc.HatchPatterns[i]
if hp is None or hp.IsDeleted: continue
if hp.Name: out.append(hp.Name)
except Exception:
continue
except Exception:
pass
if not out: out = ["Solid"]
return out
def _read_launcher_schema():
"""Liest das Default-Layer-Schema aus dossier_settings.json (Launcher-Pfad).
Liefert eine Liste {code, name, color, lw} oder None wenn nicht gesetzt."""
paths = [
os.path.expanduser("~/Library/Application Support/"
"ch.gabrielevarano.Dossier/dossier_settings.json"),
os.path.expanduser("~/Library/Application Support/"
"RhinoPanel/dossier_settings.json"),
]
for p in paths:
try:
if not os.path.isfile(p): continue
with open(p, "rb") as f:
data = json.loads(f.read().decode("utf-8"))
schema = (data or {}).get("layerSchema")
if isinstance(schema, list) and schema:
# Sanity: alle vier Pflichtfelder vorhanden
clean = [r for r in schema
if isinstance(r, dict)
and r.get("code") and r.get("name")
and r.get("color") is not None
and r.get("lw") is not None]
if clean: return clean
except Exception as ex:
print("[EBENEN] launcher-schema lesen ({}):".format(p), ex)
return None
class EbenenBridge(panel_base.BaseBridge):
def __init__(self):
panel_base.BaseBridge.__init__(self, "ebenen")
def _on_ready(self):
doc = Rhino.RhinoDoc.ActiveDoc
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
e_raw = doc.Strings.GetValue("dossier_ebenen")
if z_raw or e_raw:
try:
z = json.loads(z_raw) if z_raw else None
e = json.loads(e_raw) if e_raw else None
if z and e:
layer_builder.build_layers(doc, z, e)
layer_builder.cleanup_default_layers(doc)
self._ensure_active_sublayer()
self.send("STATE_SYNC", {
"zeichnungsebenen": z,
"ebenen": e,
"hatchPatterns": _hatch_pattern_names(doc),
})
except Exception as ex:
print("[EBENEN] State-Sync:", ex)
else:
payload = {"hatchPatterns": _hatch_pattern_names(doc)}
# Falls der User im Launcher eigene Default-Ebenen definiert hat,
# mitschicken — React nimmt's statt seiner hardcoded INITIAL_EBENEN.
launcher_schema = _read_launcher_schema()
if launcher_schema:
payload["defaultEbenen"] = launcher_schema
print("[EBENEN] FIRST_RUN mit Launcher-Schema ({} Ebenen)".format(
len(launcher_schema)))
self.send("FIRST_RUN", payload)
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
if t == "READY":
self._on_ready()
elif t == "APPLY":
self._apply(p.get("zeichnungsebenen") or [], p.get("ebenen") or [])
elif t == "LAYER_STYLE":
layer_builder.update_layer_style(doc, p["code"], p.get("color"), p.get("lw"))
if p.get("color") is not None:
self._update_ebene_field(p["code"], "color", p["color"])
if p.get("lw") is not None:
self._update_ebene_field(p["code"], "lw", p["lw"])
elif t == "SET_ACTIVE":
self._set_active_zeichnungsebene(p)
elif t == "SET_ACTIVE_LAYER":
code = p.get("code", "")
if code:
doc.Strings.SetString("dossier_active_code", code)
self._set_active_sublayer(code)
elif t == "DELETE_EBENE":
layer_builder.delete_ebene(doc, p.get("code", ""), p.get("moveTo"))
self._remove_ebene_from_state(p.get("code", ""))
elif t == "MOVE_SELECTION_TO_LAYER":
self._move_selection_to_layer(p.get("code", ""))
elif t == "SET_VISIBILITY":
self._apply_visibility(p)
elif t == "SET_CLIPPING":
# Toggle ohne Full-Apply — wirkt live auf das aktuell aktive
# Geschoss. Erwartet payload {enabled: bool}.
enabled = bool(p.get("enabled"))
self._toggle_clipping_for_active(enabled)
# --- Ebenen-Kombinationen (geteilter Store mit Ausschnitten) -------
elif t == "GET_COMBINATION":
self._send_combination()
elif t == "APPLY_COMBINATION":
self._apply_combination(p)
self._send_combination()
elif t == "SAVE_PRESET":
self._save_preset(p.get("name") or "", p.get("layers") or [])
self._send_combination()
elif t == "SAVE_CURRENT_AS_PRESET":
self._save_current_as_preset(p.get("name") or "")
self._send_combination()
elif t == "DELETE_PRESET":
self._delete_preset(p.get("name") or "")
self._send_combination()
# ---- Helpers ----
def _apply(self, zeichnungsebenen, ebenen):
print("[EBENEN] _apply START z={} e={}".format(
len(zeichnungsebenen) if zeichnungsebenen else 0,
len(ebenen) if ebenen else 0))
doc = Rhino.RhinoDoc.ActiveDoc
# Vor dem Schreiben: alten Fill-Stand snapshotten, damit wir hinterher
# entscheiden koennen ob refresh_layer_fills sich lohnt.
def _fill_signature(e_list):
out = {}
if not isinstance(e_list, list): return out
for e in e_list:
if not isinstance(e, dict): continue
f = e.get("fill")
if not isinstance(f, dict): continue
if f.get("pattern") in (None, "None"): continue
# lw kann None sein -> als Sentinel ein eindeutiger Wert
lw_raw = f.get("lw")
try:
lw_sig = round(float(lw_raw), 6) if lw_raw is not None else None
except Exception:
lw_sig = None
out[e.get("code")] = (
f.get("pattern"),
f.get("source", "layer"),
(f.get("color") or "").lower(),
round(float(f.get("scale") or 1.0), 6),
round(float(f.get("rotation") or 0.0), 6),
lw_sig,
)
return out
old_e_raw = doc.Strings.GetValue("dossier_ebenen")
old_sig = {}
if old_e_raw:
try: old_sig = _fill_signature(json.loads(old_e_raw))
except Exception: old_sig = {}
new_sig = _fill_signature(ebenen)
fill_changed = (old_sig != new_sig)
_set_processing(True)
try:
print("[EBENEN] _apply: build_layers ...")
layer_builder.build_layers(doc, zeichnungsebenen, ebenen)
print("[EBENEN] _apply: json.dumps ...")
# WICHTIG: ensure_ascii=False umgeht einen Bug in Rhinos eigener
# json/encoder.py die bei ASCII-escape s.decode('utf-8') aufruft
# und dabei mit 0xC4 (Umlaut) in den CP1252-Decoder lauft.
z_json = json.dumps(zeichnungsebenen, ensure_ascii=False)
e_json = json.dumps(ebenen, ensure_ascii=False)
print("[EBENEN] _apply: SetString ...")
doc.Strings.SetString("dossier_zeichnungsebenen", z_json)
doc.Strings.SetString("dossier_ebenen", e_json)
# Smart-Elemente (Waende) regenerieren — Geschoss-Hoehen/OKFF
# haben sich evtl. geaendert, gebundene Waende muessen neu
# extrudiert werden. Best-effort, faengt jeden Fehler ab.
try:
elem_bridge = sc.sticky.get("elemente_bridge")
if elem_bridge is not None:
elem_bridge._regenerate_all()
except Exception as _ex:
print("[EBENEN] elemente regen:", _ex)
n_with_fill = sum(1 for e in ebenen if isinstance(e, dict)
and isinstance(e.get("fill"), dict)
and e["fill"].get("pattern") not in (None, "None"))
print("[EBENEN] dossier_ebenen gespeichert: {} Ebenen, davon {} mit fill, JSON-len={}".format(
len(ebenen), n_with_fill, len(e_json)))
re_read = doc.Strings.GetValue("dossier_ebenen")
print("[EBENEN] dossier_ebenen verifiziert: len={}".format(len(re_read) if re_read else 0))
print("[EBENEN] _apply: cleanup_default_layers ...")
layer_builder.cleanup_default_layers(doc)
print("[EBENEN] _apply: ensure_active_sublayer ...")
self._ensure_active_sublayer()
# Existierende 'Nach Ebene'-Hatches an neue Pattern/Skala/Drehung
# angleichen — ABER nur wenn die Fill-Signatur sich tatsaechlich
# geaendert hat (nicht bei reinen Name/Farb-Aenderungen, die das
# Settings-Dialog auch triggern koennte).
try:
import gestaltung
if fill_changed:
gestaltung.refresh_layer_fills(doc)
else:
print("[EBENEN] _apply: fill-Signatur unveraendert -> kein Hatch-Refresh")
# Plot-Color Repair laeuft immer (no-op falls schon synchron)
gestaltung.repair_plot_colors(doc)
except Exception as ex:
print("[EBENEN] gestaltung sync:", ex)
finally:
_set_processing(False)
print("[EBENEN] _apply: update_clipping ...")
self._update_clipping()
print("[EBENEN] _apply: send APPLY_OK")
self.send("APPLY_OK", {})
print("[EBENEN] _apply: DONE")
def _ensure_active_sublayer(self):
"""Setzt den aktiven Rhino-Layer auf den DOSSIER-Sublayer (Fallback: erste Z + 20_WAENDE)."""
doc = Rhino.RhinoDoc.ActiveDoc
z_id = doc.Strings.GetValue("dossier_active_id")
code = doc.Strings.GetValue("dossier_active_code") or "20"
if not z_id:
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
if z_raw:
try:
z_list = json.loads(z_raw)
if z_list:
z_id = z_list[0].get("id", "")
if z_id:
doc.Strings.SetString("dossier_active_id", z_id)
except Exception:
pass
if z_id and code:
layer_builder.set_active_sublayer(doc, z_id, code)
def _apply_visibility(self, p):
doc = Rhino.RhinoDoc.ActiveDoc
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
e_raw = doc.Strings.GetValue("dossier_ebenen")
if not z_raw or not e_raw:
return
try:
z_full = json.loads(z_raw) or []
e_full = json.loads(e_raw) or []
except Exception:
return
payload_z = p.get("zeichnungsebenen") or []
payload_e = p.get("ebenen") or []
z_state = {z["id"]: z for z in payload_z if isinstance(z, dict) and z.get("id")}
e_state = {e["code"]: e for e in payload_e if isinstance(e, dict) and e.get("code")}
merged_z = []
for z in z_full:
if not isinstance(z, dict): continue
m = dict(z)
s = z_state.get(z.get("id"))
if s is not None:
m["visible"] = s.get("visible", True)
merged_z.append(m)
merged_e = []
for e in e_full:
if not isinstance(e, dict): continue
m = dict(e)
s = e_state.get(e.get("code"))
if s is not None:
m["visible"] = s.get("visible", True)
m["locked"] = s.get("locked", False)
merged_e.append(m)
doc.Strings.SetString("dossier_zeichnungsebenen", json.dumps(merged_z, ensure_ascii=False))
doc.Strings.SetString("dossier_ebenen", json.dumps(merged_e, ensure_ascii=False))
active_z = p.get("activeZ") or {}
if not isinstance(active_z, dict): active_z = {}
layer_builder.apply_visibility(
doc, merged_z, merged_e,
active_z.get("id"),
p.get("activeCode"),
p.get("zMode") or "active",
p.get("eMode") or "all",
)
def _set_active_zeichnungsebene(self, z):
doc = Rhino.RhinoDoc.ActiveDoc
z_id = z.get("id", "")
doc.Strings.SetString("dossier_active_id", z_id)
# Clipping ggf. mitziehen
self._update_clipping(active_z=z)
# Elemente-Panel informieren: das aktive Geschoss hat gewechselt,
# neue Elemente sollen jetzt automatisch dort verlinkt werden.
try:
eb = sc.sticky.get("elemente_bridge")
if eb is not None: eb._send_state()
except Exception: pass
if not (z.get("isGeschoss") and z.get("okff") is not None):
return
okff = float(z["okff"])
updated = 0
for view in doc.Views:
try:
vp = view.ActiveViewport
cp = vp.ConstructionPlane()
plane = cp.Plane if hasattr(cp, "Plane") else cp
# Nur Views deren CPlane horizontal liegt (Normal in +/-Z) -
# also Top/Plan-Style. Right/Front/Perspective haben vertikale
# CPlanes; ein Z-Shift waere dort optisch verwirrend.
if abs(plane.Normal.Z) < 0.99:
continue
new_plane = Rhino.Geometry.Plane(
Rhino.Geometry.Point3d(plane.Origin.X, plane.Origin.Y, okff),
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))
print("[EBENEN] CPlane Z={} auf {} Top-Style View(s) gesetzt".format(okff, updated))
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
Plane' direkt aufgerufen (ohne Full-Apply)."""
doc = Rhino.RhinoDoc.ActiveDoc
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
active_id = doc.Strings.GetValue("dossier_active_id")
if not z_raw or not active_id:
print("[CLIP] toggle: kein aktives Geschoss")
return
try:
z_list = json.loads(z_raw)
except Exception as ex:
print("[CLIP] toggle JSON-decode:", ex); return
active_z = None
for z in z_list:
if z.get("id") == active_id:
z["hasClipping"] = bool(enabled)
active_z = z
break
if active_z is None:
print("[CLIP] toggle: active_id={} nicht in Liste".format(active_id))
return
try:
doc.Strings.SetString("dossier_zeichnungsebenen",
json.dumps(z_list, ensure_ascii=False))
except Exception as ex:
print("[CLIP] toggle SetString:", ex)
self._update_clipping(active_z=active_z)
# State an React zurueckspiegeln, damit das Eye-Icon im GeschossManager
# synchron bleibt.
try:
self.send("STATE_SYNC", {
"zeichnungsebenen": z_list,
"ebenen": json.loads(doc.Strings.GetValue("dossier_ebenen") or "[]"),
"hatchPatterns": _hatch_pattern_names(doc),
})
except Exception: pass
def _update_clipping(self, active_z=None):
"""Clipping-Plane folgt aktivem Geschoss — nur wenn dessen hasClipping=True.
IMMER aus doc.Strings lesen statt das uebergebene `active_z` direkt zu
verwenden — der SET_ACTIVE-Pfad schickt ein Minimal-Payload (nur
id/name/isGeschoss/okff) ohne hasClipping/schnitthoehe. Wenn wir uns
darauf verließen, wuerde der Geschoss-Wechsel jede Clipping-Plane
loeschen weil enabled=False auswertet. Der persistierte Record in
doc.Strings hat die vollen Daten."""
doc = Rhino.RhinoDoc.ActiveDoc
# ID aus Hint oder aus dem persistierten active_id
active_id = None
if isinstance(active_z, dict):
active_id = active_z.get("id")
if not active_id:
active_id = doc.Strings.GetValue("dossier_active_id")
# Vollen Record aus doc.Strings holen
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
if z_raw and active_id:
try:
z_list = json.loads(z_raw)
active_z = next((z for z in z_list if z.get("id") == active_id), None)
except Exception:
pass
# Volles Dump des Active-Geschosses fuer Diagnose.
try:
print("[CLIP] active_z keys: {}".format(
sorted(active_z.keys()) if active_z else None))
print("[CLIP] active_z dump: {}".format(json.dumps(active_z, ensure_ascii=False)))
except Exception: pass
enabled = bool(active_z and active_z.get("hasClipping"))
_set_processing(True)
try:
layer_builder.update_clipping_plane(doc, active_z, enabled)
finally:
_set_processing(False)
def _move_selection_to_layer(self, code):
if not code:
return
doc = Rhino.RhinoDoc.ActiveDoc
z_id = doc.Strings.GetValue("dossier_active_id")
if not z_id:
print("[EBENEN] Keine aktive Zeichnungsebene")
return
parent_idx = layer_builder._find_top_by_id(doc, z_id)
if parent_idx < 0:
print("[EBENEN] Parent fuer aktive Zeichnungsebene nicht gefunden")
return
parent_id = doc.Layers[parent_idx].Id
sub_idx = layer_builder._find_sublayer_by_code(doc, parent_id, code)
if sub_idx < 0:
print("[EBENEN] Sublayer {} unter {} nicht gefunden".format(code, doc.Layers[parent_idx].Name))
return
objs = list(doc.Objects.GetSelectedObjects(False, False))
moved = 0
for obj in objs:
attrs = obj.Attributes.Duplicate()
attrs.LayerIndex = sub_idx
if doc.Objects.ModifyAttributes(obj, attrs, True):
moved += 1
doc.Views.Redraw()
print("[EBENEN] {} Objekt(e) auf {} verschoben".format(moved, doc.Layers[sub_idx].FullPath))
def _set_active_sublayer(self, code):
if not code:
return
doc = Rhino.RhinoDoc.ActiveDoc
z_id = doc.Strings.GetValue("dossier_active_id")
if not z_id:
# Fallback: erste Zeichnungsebene aus persistiertem State
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
if z_raw:
try:
z_list = json.loads(z_raw)
if z_list:
z_id = z_list[0].get("id", "")
if z_id:
doc.Strings.SetString("dossier_active_id", z_id)
except Exception:
pass
if z_id:
layer_builder.set_active_sublayer(doc, z_id, code)
else:
print("[EBENEN] Aktive Zeichnungsebene unbekannt — Layer wird nicht gesetzt")
def _remove_ebene_from_state(self, code):
doc = Rhino.RhinoDoc.ActiveDoc
raw = doc.Strings.GetValue("dossier_ebenen")
if not raw:
return
try:
ebenen = [e for e in json.loads(raw) if e.get("code") != code]
doc.Strings.SetString("dossier_ebenen", json.dumps(ebenen, ensure_ascii=False))
except Exception as ex:
print("[EBENEN] remove:", ex)
def _update_ebene_field(self, code, field, value):
doc = Rhino.RhinoDoc.ActiveDoc
raw = doc.Strings.GetValue("dossier_ebenen")
if not raw:
return
try:
ebenen = json.loads(raw)
for e in ebenen:
if e.get("code") == code:
e[field] = value
break
doc.Strings.SetString("dossier_ebenen", json.dumps(ebenen, ensure_ascii=False))
except Exception as ex:
print("[EBENEN] update:", ex)
# ---- Ebenen-Kombinationen / Presets (geteilt mit AUSSCHNITTE) --------
_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 []
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)
def _send_combination(self):
"""Schickt aktuelles Layer-State + alle Presets ans Frontend."""
doc = Rhino.RhinoDoc.ActiveDoc
layers_out = []
try:
for layer in doc.Layers:
if layer is None or layer.IsDeleted: continue
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] _send_combination layers:", ex)
try:
presets = self._load_presets(doc)
except Exception:
presets = []
self.send("COMBINATION_DATA", {
"layers": layers_out,
"presets": presets,
})
def _apply_combination(self, payload):
"""Wendet Preset an. payload kann sein:
- Liste [{id, visible, locked}, ...] (alt / AUSSCHNITTE-Dialog)
- Dict { layers, dossierEbenen?, dossierZeichnungsebenen? } (neu)
Eye-State-Pfad (bevorzugt): aktualisiert dossier_ebenen und
dossier_zeichnungsebenen direkt, pusht STATE_SYNC. React triggert
dann SET_VISIBILITY und apply_visibility setzt doc.Layer korrekt
unter Beruecksichtigung von z_mode/e_mode.
Layer-ID-Pfad (Fallback): setzt doc.Layer.IsVisible direkt.
"""
doc = Rhino.RhinoDoc.ActiveDoc
# Payload normalisieren
if isinstance(payload, dict):
layer_states = payload.get("layers") or []
pe_states = payload.get("dossierEbenen")
pz_states = payload.get("dossierZeichnungsebenen")
else:
layer_states = payload or []
pe_states = None
pz_states = None
# --- Eye-State-Pfad (wenn vorhanden) ---
if pe_states is not None or pz_states is not None:
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 pe_states is not None:
by_code = {x.get("code"): x for x in pe_states 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 pz_states is not None:
by_id = {x.get("id"): x for x in pz_states 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))
# STATE_SYNC pushen — React's visibilityKey aendert sich,
# applyVisibility fires, backend apply_visibility setzt doc.Layer
# state korrekt unter z_mode/e_mode-Beachtung.
self.send("STATE_SYNC", {
"zeichnungsebenen": z_list,
"ebenen": e_list,
})
try: doc.Views.Redraw()
except Exception: pass
print("[EBENEN] Eye-State-Preset angewandt: {} Ebenen, {} Zeichnungsebenen".format(
len(pe_states or []), len(pz_states or [])))
return
except Exception as ex:
print("[EBENEN] _apply_combination eye-state:", ex)
# Fall through zum Layer-ID-Pfad als Fallback
# --- Layer-ID-Pfad (alt / AUSSCHNITTE) ---
by_id = {}
for layer in doc.Layers:
if not layer.IsDeleted:
by_id[str(layer.Id)] = layer
n = 0
# Erst: doc.Layer Visibility setzen
_set_processing(True)
try:
for ls in (layer_states or []):
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
n += 1
except Exception: pass
finally:
_set_processing(False)
# Dann: dossier_ebenen/dossier_zeichnungsebenen Eye-State synchronisieren.
# Map: doc.Layer.Id -> {visible, locked}
state_by_id = {ls.get("id"): ls for ls in (layer_states or []) if ls.get("id")}
try:
e_raw = doc.Strings.GetValue("dossier_ebenen")
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
ebenen_list = json.loads(e_raw) if e_raw else []
z_list = json.loads(z_raw) if z_raw else []
# Sublayer -> dossier_code mapping via Rhino-Layer UserString
code_by_layer_id = {}
zid_by_layer_id = {}
for layer in doc.Layers:
if layer is None or layer.IsDeleted: continue
c = layer.GetUserString("dossier_code")
i = layer.GetUserString("dossier_id")
if c: code_by_layer_id[str(layer.Id)] = c
if i: zid_by_layer_id[str(layer.Id)] = i
# Pro Dossier-Ebene: wenn mind. ein matchender Sublayer im preset war,
# sync visible/locked.
updated_e = False
for e in ebenen_list:
if not isinstance(e, dict): continue
code = e.get("code")
if not code: continue
# Suche eine Layer-Id mit diesem code, deren state im preset ist
for lid, c in code_by_layer_id.items():
if c != code: continue
s = state_by_id.get(lid)
if s is None: continue
new_vis = bool(s.get("visible", True))
new_lck = bool(s.get("locked", False))
if e.get("visible", True) != new_vis:
e["visible"] = new_vis
updated_e = True
if (e.get("locked", False)) != new_lck:
e["locked"] = new_lck
updated_e = True
break
updated_z = False
for z in z_list:
if not isinstance(z, dict): continue
zid = z.get("id")
if not zid: continue
for lid, z_uid in zid_by_layer_id.items():
if z_uid != zid: continue
s = state_by_id.get(lid)
if s is None: continue
new_vis = bool(s.get("visible", True))
if z.get("visible", True) != new_vis:
z["visible"] = new_vis
updated_z = True
break
if updated_e:
doc.Strings.SetString("dossier_ebenen", json.dumps(ebenen_list, ensure_ascii=False))
if updated_z:
doc.Strings.SetString("dossier_zeichnungsebenen", json.dumps(z_list, ensure_ascii=False))
# STATE_SYNC ans React-Panel pushen damit Eye-Icons matchen
if updated_e or updated_z:
try:
self.send("STATE_SYNC", {
"zeichnungsebenen": z_list,
"ebenen": ebenen_list,
})
except Exception as ex:
print("[EBENEN] STATE_SYNC push:", ex)
except Exception as ex:
print("[EBENEN] _apply_combination sync:", ex)
try: doc.Views.Redraw()
except Exception: pass
print("[EBENEN] Kombination angewandt: {} Layer".format(n))
def _save_preset(self, name, layers):
name = (name or "").strip()
if not name: return
doc = Rhino.RhinoDoc.ActiveDoc
presets = self._load_presets(doc)
clean = []
for ls in (layers or []):
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((p for p in presets if p.get("name") == name), None)
if existing is not None:
existing["layers"] = clean
else:
presets.append({"name": name, "layers": clean})
self._store_presets(doc, presets)
print("[EBENEN] Kombination '{}' gespeichert ({} Layer)".format(name, len(clean)))
def _save_current_as_preset(self, name):
"""Speichert die aktuellen Eye-States (dossier_ebenen + dossier_zeichnungs-
ebenen) als Preset — NICHT die berechneten doc.Layer.IsVisible-Werte.
Sonst wuerde der z_mode/e_mode-Override (z.B. 'active' nur 1 Layer
sichtbar) ins Preset einbacken und beim Apply nicht wieder restorbar
sein.
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
doc = Rhino.RhinoDoc.ActiveDoc
# 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_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)))
def _delete_preset(self, name):
name = (name or "").strip()
if not name: return
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))
def _ebenen_bridge_factory():
bridge = EbenenBridge()
_install_layer_listener(bridge)
return bridge
def _install_layer_listener(bridge):
"""Reagiert auf externe Aenderungen in Rhinos Layer-Tabelle (Rename, Delete)."""
if sc.sticky.get("ebenen_layer_listener"):
sc.sticky["ebenen_bridge_ref"] = bridge
return
sc.sticky["ebenen_bridge_ref"] = bridge
def on_layer_event(sender, args):
if _is_processing():
return
try:
doc = args.Document
evt = args.EventType
# Nur Modify-Events interessieren uns (Rename, Color etc.)
if evt != Rhino.DocObjects.Tables.LayerTableEventType.Modified:
return
idx = args.LayerIndex
if idx < 0 or idx >= doc.Layers.Count:
return
layer = doc.Layers[idx]
dossier_id = layer.GetUserString("dossier_id")
dossier_code = layer.GetUserString("dossier_code")
if not (dossier_id or dossier_code):
return
updated = False
if dossier_id:
raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
if raw:
try:
z_list = json.loads(raw)
for z in z_list:
if z.get("id") == dossier_id and z.get("name") != layer.Name:
z["name"] = layer.Name
updated = True
break
if updated:
doc.Strings.SetString("dossier_zeichnungsebenen", json.dumps(z_list, ensure_ascii=False))
except Exception:
pass
elif dossier_code:
raw = doc.Strings.GetValue("dossier_ebenen")
if raw:
try:
e_list = json.loads(raw)
# Layer-Name ist "CC_NAME" — wir extrahieren NAME
if "_" in layer.Name:
new_name = layer.Name.split("_", 1)[1]
for e in e_list:
if e.get("code") == dossier_code and e.get("name") != new_name:
e["name"] = new_name
updated = True
break
if updated:
doc.Strings.SetString("dossier_ebenen", json.dumps(e_list, ensure_ascii=False))
except Exception:
pass
if updated:
b = sc.sticky.get("ebenen_bridge_ref")
if b is not None:
try:
b._on_ready() # sendet aktualisiertes STATE_SYNC
except Exception:
pass
except Exception as ex:
print("[EBENEN] Layer-Event:", ex)
Rhino.RhinoDoc.LayerTableEvent += on_layer_event
sc.sticky["ebenen_layer_listener"] = True
print("[EBENEN] Layer-Listener aktiv")
panel_base.register_and_open("ebenen", "EBENEN", PANEL_GUID_STR, _ebenen_bridge_factory,
icon_spec=("layers", "#3a6fa8"))