#! 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"))