#! python 3 # -*- coding: utf-8 -*- """ overrides_panel.py OVERRIDES-Panel: Rule-Editor fuer grafische Overrides. Liest/schreibt rhino/overrides.py-Engine. """ import os import sys import uuid 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 overrides PANEL_GUID_STR = "8f2a7b6c-9f3a-4f4d-e6f7-08192a3c4d62" def _list_layer_names(doc): out = [] try: for layer in doc.Layers: if layer is None or layer.IsDeleted: continue try: out.append({"name": layer.Name, "fullPath": layer.FullPath or layer.Name}) except Exception: pass except Exception: pass return out def _list_linetypes(doc): out = [] try: for lt in doc.Linetypes: try: if lt.Name and not lt.IsDeleted: out.append(lt.Name) except Exception: pass except Exception: pass return out def _list_hatch_patterns(doc): out = [] try: for i in range(doc.HatchPatterns.Count): try: hp = doc.HatchPatterns[i] if hp and hp.Name: out.append(hp.Name) except Exception: pass except Exception: pass return out def _payload(doc): cfg = overrides.load_config(doc) return { "enabled": bool(cfg.get("enabled")), "rules": cfg.get("rules") or [], "layers": _list_layer_names(doc), "linetypes": _list_linetypes(doc), "hatchPatterns": _list_hatch_patterns(doc), "presets": overrides.list_presets(), "activePreset": cfg.get("activePreset"), "ruleTemplates": overrides.list_rule_templates(), } class OverridesBridge(panel_base.BaseBridge): def __init__(self): panel_base.BaseBridge.__init__(self, "overrides") def _on_ready(self): self._send_state() def _send_state(self): doc = Rhino.RhinoDoc.ActiveDoc self.send("STATE", _payload(doc)) # Oberleiste mit-informieren: deren Topbar zeigt # Toggle + Preset-Dropdown, das vom selben State abhaengt. # Cache invalidieren, dann force-send, sonst sieht die Topbar # neue Rules/Presets erst beim naechsten Toggle. try: b = sc.sticky.get("oberleiste_bridge") if b is not None: b._cached_overrides = None b._send_state(force=True) except Exception as ex: print("[OVERRIDES] notify oberleiste:", ex) 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" or t == "REQUEST_STATE": self._on_ready() elif t == "SET_ENABLED": overrides.set_enabled(doc, bool(p.get("enabled"))) self._send_state() elif t == "ADD_RULE": cfg = overrides.load_config(doc) rule = p.get("rule") or {} if not rule.get("id"): rule["id"] = "rule_" + uuid.uuid4().hex[:8] rule.setdefault("name", "Neue Regel") rule.setdefault("enabled", True) rule.setdefault("condition", {"type": "layer_name", "operator": "equals", "value": ""}) rule.setdefault("actions", {}) # Neue Regel oben einfuegen (= hoechste Prioritaet) rules = cfg.get("rules") or [] rules.insert(0, rule) overrides.update_rules(doc, rules, cfg.get("enabled")) self._send_state() elif t == "UPDATE_RULE": cfg = overrides.load_config(doc) rid = p.get("id") patch = p.get("rule") or {} rules = cfg.get("rules") or [] for i, r in enumerate(rules): if r.get("id") == rid: rules[i] = patch break overrides.update_rules(doc, rules, cfg.get("enabled")) self._send_state() elif t == "DELETE_RULE": cfg = overrides.load_config(doc) rid = p.get("id") rules = [r for r in (cfg.get("rules") or []) if r.get("id") != rid] overrides.update_rules(doc, rules, cfg.get("enabled")) self._send_state() elif t == "REORDER_RULES": cfg = overrides.load_config(doc) order = p.get("order") or [] by_id = {r.get("id"): r for r in (cfg.get("rules") or [])} new_rules = [by_id[i] for i in order if i in by_id] # Verbleibende (falls Liste inkonsistent) hinten anhaengen for r in (cfg.get("rules") or []): if r not in new_rules: new_rules.append(r) overrides.update_rules(doc, new_rules, cfg.get("enabled")) self._send_state() elif t == "DUPLICATE_RULE": cfg = overrides.load_config(doc) rid = p.get("id") rules = cfg.get("rules") or [] for i, r in enumerate(rules): if r.get("id") == rid: import json clone = json.loads(json.dumps(r, ensure_ascii=False)) clone["id"] = "rule_" + uuid.uuid4().hex[:8] clone["name"] = (r.get("name", "Regel") + " Kopie") rules.insert(i + 1, clone) break overrides.update_rules(doc, rules, cfg.get("enabled")) self._send_state() elif t == "REAPPLY": if overrides.load_config(doc).get("enabled"): overrides.restore_all(doc) overrides.apply_all(doc) self._send_state() elif t == "CLEAR_RULES": # Alle Regeln entfernen und activePreset clearen — wird vom # Topbar/Kombination-Dropdown beim Wechsel auf "— neu / keine —" # gefeuert, damit der Editor wirklich leer ist. cfg = overrides.load_config(doc) overrides.update_rules(doc, [], cfg.get("enabled")) self._send_state() # --- Presets (cross-doc) ---------------------------------------- elif t == "SAVE_PRESET": name = (p.get("name") or "").strip() if name: cfg = overrides.load_config(doc) overrides.save_preset(name, cfg.get("rules") or []) self._send_state() elif t == "LOAD_PRESET": name = (p.get("name") or "").strip() mode = p.get("mode") or "replace" # 'replace' oder 'append' if mode == "replace": # set_active_preset macht alles richtig: Rules ersetzen, # activePreset = name, ggf. neu anwenden. overrides.set_active_preset(doc, name) else: # Append-Mode: bestehende + Preset-Rules. activePreset wird # in update_rules auf None gesetzt — passt, weil's eine # Mischung ist, kein einzelnes Preset mehr. rules = overrides.load_preset(name) if rules is not None: cfg = overrides.load_config(doc) new_rules = list(cfg.get("rules") or []) + list(rules) overrides.update_rules(doc, new_rules, cfg.get("enabled")) self._send_state() elif t == "DELETE_PRESET": name = (p.get("name") or "").strip() if name: overrides.delete_preset(name) self._send_state() # --- Rule-Templates (cross-doc, einzelne Regeln) ---------------- elif t == "SAVE_RULE_TEMPLATE": name = (p.get("name") or "").strip() rule = p.get("rule") or {} if name and isinstance(rule, dict): # ID/Name aus dem Template-Rule herausschneiden (template hat # eigenen Namen, ID wird beim Insert neu generiert) clean = {k: v for k, v in rule.items() if k not in ("id",)} overrides.save_rule_template(name, clean) self._send_state() elif t == "DELETE_RULE_TEMPLATE": name = (p.get("name") or "").strip() if name: overrides.delete_rule_template(name) self._send_state() elif t == "ADD_FROM_TEMPLATE": name = (p.get("name") or "").strip() if not name: return tpl = overrides.load_rule_template(name) if not tpl: return cfg = overrides.load_config(doc) new_rule = dict(tpl) new_rule["id"] = "rule_" + uuid.uuid4().hex[:8] new_rule.setdefault("enabled", True) new_rule.setdefault("name", name) rules = cfg.get("rules") or [] rules.insert(0, new_rule) overrides.update_rules(doc, rules, cfg.get("enabled")) self._send_state() def _ensure_listeners_once(): """Overrides-Listener nur EINMAL global installieren (statt bei jedem open_as_window).""" if sc.sticky.get("overrides_listeners_installed"): return try: overrides.install_listeners() sc.sticky["overrides_listeners_installed"] = True except Exception as ex: print("[OVERRIDES] install_listeners:", ex) def open_as_window(): """Oeffnet OVERRIDES als echtes Rhino-Fenster (Eto.Form + WebView). Wird vom Oberleiste-Bridge bei OPEN_OVERRIDES_PANEL gerufen. Pro Fenster eine eigene OverridesBridge-Instanz. Die letzte Instanz landet in sticky["overrides_bridge"] — andere Panels (Oberleiste) die Cross-Updates an Overrides senden, treffen das aktive Fenster.""" _ensure_listeners_once() b = OverridesBridge() sc.sticky["overrides_bridge"] = b panel_base.open_satellite_window( "overrides", title="OVERRIDES", size=(760, 580), bridge=b) # OVERRIDES laeuft jetzt als Satelliten-Fenster (geoeffnet vom Oberleiste- # Gear-Button), nicht mehr als gedocktes Panel. Listener werden lazy beim # ersten open_as_window installiert. Falls jemand das alte Panel via # Workspace-Layout noch geoeffnet hat, wird es nicht mehr registriert → # Rhino zeigt es leer / nicht mehr an.