#! python 3 # -*- coding: utf-8 -*- """ overrides.py Engine fuer regelbasierte grafische Overrides (ArchiCAD Graphical Overrides / Vectorworks Datenvisualisierung). Datenmodell (gespeichert als JSON in doc.Strings["dossier_overrides"]): { "enabled": true, "rules": [ { "id": "rule_abc", "name": "Bestand grau", "enabled": true, "condition": { "type": "layer_name" | "user_string" | "object_name", "operator": "equals" | "contains" | "starts_with" | "not_equals", "value": "WAND_BESTAND", "key": "brandschutz" # nur fuer user_string }, "actions": { "color": "#888888" or null, "lineweight": 0.25 or null, "linetype": "Dashed" or null } }, ... (oberste Regel hat hoechste Prioritaet) ] } Verhalten: - Mehrere Regeln matchen additiv: Actions aller passenden Regeln werden kombiniert. Bei Konflikt fuer die selbe Property gewinnt die in der Liste WEITER OBEN stehende Regel. - Originalwerte werden in UserStrings pro Objekt gesichert -> reversibel. - Engine wird via apply_all(doc) / restore_all(doc) gesteuert. """ import os import sys import json import Rhino import System import System.Drawing as Drawing import scriptcontext as sc _HERE = os.path.dirname(os.path.abspath(__file__)) if _HERE not in sys.path: sys.path.insert(0, _HERE) _STORE_KEY = "dossier_overrides" # Globale Presets (cross-doc) — Datei im User-Home _PRESETS_DIR = os.path.expanduser("~/Library/Application Support/RhinoPanel") _PRESETS_PATH = os.path.join(_PRESETS_DIR, "override_presets.json") # Rule-Templates: einzelne wiederverwendbare Regeln (cross-doc). Andere # Datei damit User Combo-Presets und Einzel-Templates separat verwalten kann. _RULE_TPL_PATH = os.path.join(_PRESETS_DIR, "override_rule_templates.json") # UserString-Keys fuer Original-Backups (pro Objekt) _ORIG_COLOR_SRC = "dossier_or_csrc" _ORIG_COLOR = "dossier_or_color" _ORIG_LW_SRC = "dossier_or_lwsrc" _ORIG_LW = "dossier_or_lw" _ORIG_LT_SRC = "dossier_or_ltsrc" _ORIG_LT = "dossier_or_lt" _OVERRIDDEN = "dossier_or_done" # "1" wenn Object aktuell overridden ist # Hatch-Override: Originalwerte werden auf dem Hatch-Objekt selbst gespeichert. # Link Curve -> Hatch nutzt den FILL_KEY den gestaltung.py setzt. _GEST_FILL_KEY = "ebenen_fill_hatch_id" # auf Curve _ORIG_HP = "dossier_or_hatch_pidx" # auf Hatch — original PatternIndex _ORIG_HS = "dossier_or_hatch_scale" # auf Hatch — original PatternScale _HATCH_OVERRIDDEN = "dossier_or_hatch_done" # "1" wenn Hatch aktuell overridden _ORIG_HC_SRC = "dossier_or_hatch_csrc" # auf Hatch — original ColorSource _ORIG_HC = "dossier_or_hatch_color" # auf Hatch — original Color _HATCH_COLOR_OVERRIDDEN = "dossier_or_hatch_color_done" _FROM_LAYER = Rhino.DocObjects.ObjectColorSource.ColorFromLayer _FROM_OBJECT = Rhino.DocObjects.ObjectColorSource.ColorFromObject _LW_FROM_LAY = Rhino.DocObjects.ObjectPlotWeightSource.PlotWeightFromLayer _LW_FROM_OBJ = Rhino.DocObjects.ObjectPlotWeightSource.PlotWeightFromObject _LT_FROM_LAY = Rhino.DocObjects.ObjectLinetypeSource.LinetypeFromLayer _LT_FROM_OBJ = Rhino.DocObjects.ObjectLinetypeSource.LinetypeFromObject # --- Daten lesen/schreiben -------------------------------------------------- def load_config(doc): if doc is None: return {"enabled": False, "rules": []} try: raw = doc.Strings.GetValue(_STORE_KEY) if not raw: return {"enabled": False, "rules": []} data = json.loads(raw) if not isinstance(data, dict): return {"enabled": False, "rules": []} data.setdefault("enabled", False) data.setdefault("rules", []) return data except Exception as ex: print("[OVERRIDES] load_config:", ex) return {"enabled": False, "rules": []} def save_config(doc, cfg): if doc is None: return try: doc.Strings.SetString(_STORE_KEY, json.dumps(cfg, ensure_ascii=False)) except Exception as ex: print("[OVERRIDES] save_config:", ex) # --- Presets (global, cross-doc) ------------------------------------------- def _read_presets_file(): try: if os.path.isfile(_PRESETS_PATH): with open(_PRESETS_PATH, "rb") as f: data = json.loads(f.read().decode("utf-8")) if isinstance(data, list): return data # Migration: alte dict-Form -> list if isinstance(data, dict) and "presets" in data: return data.get("presets") or [] except Exception as ex: print("[OVERRIDES] read_presets:", ex) return [] def _write_presets_file(presets): try: if not os.path.isdir(_PRESETS_DIR): os.makedirs(_PRESETS_DIR) with open(_PRESETS_PATH, "wb") as f: f.write(json.dumps(presets or [], ensure_ascii=False, indent=2).encode("utf-8")) return True except Exception as ex: print("[OVERRIDES] write_presets:", ex) return False def list_presets(): """Liefert Liste von {name, ruleCount}.""" out = [] for p in _read_presets_file(): if not isinstance(p, dict): continue out.append({ "name": p.get("name", "(ohne Name)"), "ruleCount": len(p.get("rules") or []), }) return out def save_preset(name, rules): """Speichert/ueberschreibt Preset mit gegebenem Namen.""" if not name or not isinstance(name, str): return False name = name.strip() if not name: return False presets = _read_presets_file() # Existierendes Preset mit gleichem Namen ersetzen for i, p in enumerate(presets): if isinstance(p, dict) and p.get("name") == name: presets[i] = {"name": name, "rules": rules or []} return _write_presets_file(presets) # Sonst anhaengen presets.append({"name": name, "rules": rules or []}) return _write_presets_file(presets) def load_preset(name): """Liefert die Rules-Liste eines Presets oder None.""" for p in _read_presets_file(): if isinstance(p, dict) and p.get("name") == name: # Deep-Copy via JSON damit der Aufrufer keine Datei-Daten teilt return json.loads(json.dumps(p.get("rules") or [])) return None def delete_preset(name): presets = _read_presets_file() new = [p for p in presets if not (isinstance(p, dict) and p.get("name") == name)] if len(new) == len(presets): return False return _write_presets_file(new) # --- Rule-Templates: einzelne wiederverwendbare Regeln (cross-doc) ---------- def _read_rule_templates(): if not os.path.isfile(_RULE_TPL_PATH): return [] try: with open(_RULE_TPL_PATH, "rb") as f: data = json.loads(f.read().decode("utf-8")) if isinstance(data, list): return data if isinstance(data, dict) and "templates" in data: return data.get("templates") or [] except Exception as ex: print("[OVERRIDES] read_rule_templates:", ex) return [] def _write_rule_templates(templates): try: if not os.path.isdir(_PRESETS_DIR): os.makedirs(_PRESETS_DIR) with open(_RULE_TPL_PATH, "wb") as f: f.write(json.dumps(templates or [], ensure_ascii=False, indent=2).encode("utf-8")) return True except Exception as ex: print("[OVERRIDES] write_rule_templates:", ex) return False def list_rule_templates(): """Liefert Liste von {name, rule} fuer alle gespeicherten Templates.""" out = [] for t in _read_rule_templates(): if not isinstance(t, dict): continue out.append({"name": t.get("name", "(ohne Name)"), "rule": t.get("rule") or {}}) return out def save_rule_template(name, rule): """Speichert/ueberschreibt eine Regel als Template unter name.""" if not name or not isinstance(name, str): return False name = name.strip() if not name or not isinstance(rule, dict): return False templates = _read_rule_templates() for i, t in enumerate(templates): if isinstance(t, dict) and t.get("name") == name: templates[i] = {"name": name, "rule": rule} return _write_rule_templates(templates) templates.append({"name": name, "rule": rule}) return _write_rule_templates(templates) def load_rule_template(name): """Liefert die Rule eines Templates oder None.""" for t in _read_rule_templates(): if isinstance(t, dict) and t.get("name") == name: return json.loads(json.dumps(t.get("rule") or {})) return None def delete_rule_template(name): templates = _read_rule_templates() new = [t for t in templates if not (isinstance(t, dict) and t.get("name") == name)] if len(new) == len(templates): return False return _write_rule_templates(new) def set_active_preset(doc, name): """Aktiviert ein gespeichertes Preset: kopiert dessen Rules ins Doc-Config und markiert es als activePreset. Wenn name leer/None: aktives Preset geclear-t, Rules bleiben unveraendert (User waehlt "kein Preset"). Bei aktivem enabled-Flag wird sofort neu angewendet. True bei Erfolg.""" if doc is None: return False cfg = load_config(doc) if name: rules = load_preset(name) if rules is None: return False cfg["rules"] = rules cfg["activePreset"] = name else: cfg["activePreset"] = None save_config(doc, cfg) if cfg.get("enabled"): # Erst restore (alte Overrides zuruecknehmen), dann apply mit neuen Rules. restore_all(doc) apply_all(doc) return True def get_active_preset(doc): """Aktuell aktives Preset-Namen oder None.""" if doc is None: return None return load_config(doc).get("activePreset") # --- Helpers ---------------------------------------------------------------- def _color_to_hex(c): if c is None: return None try: return "#{:02x}{:02x}{:02x}".format(int(c.R), int(c.G), int(c.B)) except Exception: return None def _hex_to_color(h): if not isinstance(h, str): return Drawing.Color.FromArgb(136, 136, 136) h = h.strip() if h.startswith("#"): h = h[1:] if len(h) != 6: return Drawing.Color.FromArgb(136, 136, 136) try: return Drawing.Color.FromArgb(int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)) except Exception: return Drawing.Color.FromArgb(136, 136, 136) def _layer_name_for(doc, obj): try: idx = obj.Attributes.LayerIndex if 0 <= idx < doc.Layers.Count: return doc.Layers[idx].Name or "" except Exception: pass return "" def _layer_full_path_for(doc, obj): try: idx = obj.Attributes.LayerIndex if 0 <= idx < doc.Layers.Count: return doc.Layers[idx].FullPath or "" except Exception: pass return "" def _user_string_for(obj, key): try: v = obj.Attributes.GetUserString(key) return v if v is not None else "" except Exception: return "" def _object_name_for(obj): try: return obj.Attributes.Name or "" except Exception: return "" def _compare(actual, op, expected): actual = actual if actual is not None else "" expected = expected if expected is not None else "" if op == "equals": return str(actual) == str(expected) if op == "not_equals": return str(actual) != str(expected) if op == "contains": return str(expected) in str(actual) if op == "starts_with": return str(actual).startswith(str(expected)) if op == "ends_with": return str(actual).endswith(str(expected)) return False def _match_leaf(doc, obj, condition): """Evaluates a single leaf condition (layer_name / user_string / object_name).""" if not isinstance(condition, dict): return False t = condition.get("type") op = condition.get("operator") or "equals" v = condition.get("value") if t == "layer_name": return _compare(_layer_name_for(doc, obj), op, v) or \ _compare(_layer_full_path_for(doc, obj), op, v) if t == "user_string": return _compare(_user_string_for(obj, condition.get("key", "")), op, v) if t == "object_name": return _compare(_object_name_for(obj), op, v) return False def _match_rule(doc, obj, rule): """Evaluates rule. Unterstuetzt zwei Formate: - Legacy: rule.condition = {single leaf} - Neu: rule.conditions = [leaf, leaf, ...] + rule.conditionsLogic = "and" | "or" """ # Neue Form (Liste) conds = rule.get("conditions") if isinstance(conds, list) and conds: logic = (rule.get("conditionsLogic") or "and").lower() if logic == "or": for c in conds: if _match_leaf(doc, obj, c): return True return False # default: and for c in conds: if not _match_leaf(doc, obj, c): return False return True # Legacy single condition return _match_leaf(doc, obj, rule.get("condition") or {}) # --- Apply / Restore -------------------------------------------------------- def _backup_original(attrs): """Sichert originale Attribute in UserStrings (nur beim ersten Mal).""" if attrs.GetUserString(_OVERRIDDEN) == "1": return # bereits gesichert try: attrs.SetUserString(_ORIG_COLOR_SRC, str(int(attrs.ColorSource))) c_hex = _color_to_hex(attrs.ObjectColor) or "#888888" attrs.SetUserString(_ORIG_COLOR, c_hex) attrs.SetUserString(_ORIG_LW_SRC, str(int(attrs.PlotWeightSource))) attrs.SetUserString(_ORIG_LW, "{:.6f}".format(float(attrs.PlotWeight or 0))) attrs.SetUserString(_ORIG_LT_SRC, str(int(attrs.LinetypeSource))) attrs.SetUserString(_ORIG_LT, str(int(attrs.LinetypeIndex))) attrs.SetUserString(_OVERRIDDEN, "1") except Exception as ex: print("[OVERRIDES] _backup_original:", ex) def _restore_original(doc, obj): """Stellt urspruengliche Attribute aus UserStrings wieder her. Beinhaltet auch das Restoring eines ggf. ueberschriebenen Hatches.""" a = obj.Attributes # Hatch separat zuruecksetzen — kann auch ohne Curve-Override # passiert sein (z.B. wenn Override nur den Pattern aendert) _restore_hatch(doc, obj) _restore_hatch_color(doc, obj) if a.GetUserString(_OVERRIDDEN) != "1": return False try: new_a = a.Duplicate() cs = a.GetUserString(_ORIG_COLOR_SRC) if cs: new_a.ColorSource = Rhino.DocObjects.ObjectColorSource(int(cs)) c = a.GetUserString(_ORIG_COLOR) if c: new_a.ObjectColor = _hex_to_color(c) lws = a.GetUserString(_ORIG_LW_SRC) if lws: new_a.PlotWeightSource = Rhino.DocObjects.ObjectPlotWeightSource(int(lws)) lw = a.GetUserString(_ORIG_LW) if lw: try: new_a.PlotWeight = float(lw) except Exception: pass lts = a.GetUserString(_ORIG_LT_SRC) if lts: new_a.LinetypeSource = Rhino.DocObjects.ObjectLinetypeSource(int(lts)) lt = a.GetUserString(_ORIG_LT) if lt: try: new_a.LinetypeIndex = int(lt) except Exception: pass # Backup-Marker entfernen for k in (_ORIG_COLOR_SRC, _ORIG_COLOR, _ORIG_LW_SRC, _ORIG_LW, _ORIG_LT_SRC, _ORIG_LT, _OVERRIDDEN): new_a.SetUserString(k, "") doc.Objects.ModifyAttributes(obj, new_a, True) return True except Exception as ex: print("[OVERRIDES] _restore_original:", ex) return False def _compose_overrides(doc, obj, rules): """Sammelt Actions aller matchenden Regeln. Bei Konflikten gewinnt die Regel die WEITER OBEN in der Liste steht (= niedrigerer Index).""" composed = {} for rule in rules: if not rule.get("enabled", True): continue if not _match_rule(doc, obj, rule): continue for prop, val in (rule.get("actions") or {}).items(): if val is None or val == "": continue if prop not in composed: composed[prop] = val return composed def _find_linked_hatch(doc, curve_obj): """Findet den via gestaltung verlinkten Hatch zur Curve (oder None).""" try: hid_s = curve_obj.Attributes.GetUserString(_GEST_FILL_KEY) if not hid_s: return None h = doc.Objects.FindId(System.Guid(hid_s)) if h is None or h.IsDeleted: return None return h except Exception: return None def _apply_hatch_override(doc, curve_obj, pattern_name, scale_val): """Modifiziert den verlinkten Hatch der Curve. Original wird auf dem Hatch in UserStrings gesichert. Liefert True bei Aenderung. Wenn keine Hatch existiert: stiller No-op (User soll erst via Gestaltung eine Basis-Hatch anlegen — Overrides modifizieren, erzeugen nicht).""" h = _find_linked_hatch(doc, curve_obj) if h is None: return False try: hg = h.Geometry ha = h.Attributes # Backup einmalig sichern if ha.GetUserString(_HATCH_OVERRIDDEN) != "1": try: ha.SetUserString(_ORIG_HP, str(int(hg.PatternIndex))) ha.SetUserString(_ORIG_HS, "{:.6f}".format(float(hg.PatternScale))) ha.SetUserString(_HATCH_OVERRIDDEN, "1") doc.Objects.ModifyAttributes(h, ha, True) except Exception as ex: print("[OVERRIDES] hatch backup:", ex) # Pattern wechseln (Geometrie neu erzeugen — PatternIndex ist read-only) new_pidx = hg.PatternIndex if pattern_name: try: idx = doc.HatchPatterns.Find(pattern_name, True) if idx >= 0: new_pidx = idx except Exception: pass new_scale = float(scale_val) if scale_val else float(hg.PatternScale) try: # Hatch-Geometrie neu instanzieren (PatternIndex/Scale aendern direkt) new_hg = hg.Duplicate() try: new_hg.PatternIndex = new_pidx except Exception: pass try: new_hg.PatternScale = new_scale except Exception: pass doc.Objects.Replace(h.Id, new_hg) return True except Exception as ex: print("[OVERRIDES] hatch replace:", ex) return False except Exception as ex: print("[OVERRIDES] _apply_hatch_override:", ex) return False def _restore_hatch(doc, curve_obj): """Stellt Hatch-Pattern und -Scale aus dem Backup wieder her.""" h = _find_linked_hatch(doc, curve_obj) if h is None: return False a = h.Attributes if a.GetUserString(_HATCH_OVERRIDDEN) != "1": return False try: orig_pidx_s = a.GetUserString(_ORIG_HP) orig_scale_s = a.GetUserString(_ORIG_HS) hg = h.Geometry.Duplicate() if orig_pidx_s: try: hg.PatternIndex = int(orig_pidx_s) except Exception: pass if orig_scale_s: try: hg.PatternScale = float(orig_scale_s) except Exception: pass doc.Objects.Replace(h.Id, hg) # Backup-Marker entfernen h2 = doc.Objects.FindId(h.Id) if h2 is not None: new_a = h2.Attributes.Duplicate() for k in (_ORIG_HP, _ORIG_HS, _HATCH_OVERRIDDEN): new_a.SetUserString(k, "") doc.Objects.ModifyAttributes(h2, new_a, True) return True except Exception as ex: print("[OVERRIDES] _restore_hatch:", ex) return False def _apply_hatch_color_override(doc, curve_obj, color_hex): """Setzt ObjectColor + ColorSource des verlinkten Hatches auf color_hex. Backup wird einmalig auf dem Hatch in UserStrings gesichert.""" h = _find_linked_hatch(doc, curve_obj) if h is None: return False try: ha = h.Attributes if ha.GetUserString(_HATCH_COLOR_OVERRIDDEN) != "1": try: ha.SetUserString(_ORIG_HC_SRC, str(int(ha.ColorSource))) ha.SetUserString(_ORIG_HC, _color_to_hex(ha.ObjectColor)) ha.SetUserString(_HATCH_COLOR_OVERRIDDEN, "1") doc.Objects.ModifyAttributes(h, ha, True) except Exception as ex: print("[OVERRIDES] hatch-color backup:", ex) new_a = h.Attributes.Duplicate() new_a.ColorSource = _FROM_OBJECT new_a.ObjectColor = _hex_to_color(color_hex) try: new_a.PlotColorSource = Rhino.DocObjects.ObjectPlotColorSource.PlotColorFromObject new_a.PlotColor = new_a.ObjectColor except Exception: pass doc.Objects.ModifyAttributes(h, new_a, True) return True except Exception as ex: print("[OVERRIDES] _apply_hatch_color_override:", ex) return False def _restore_hatch_color(doc, curve_obj): """Stellt ColorSource + ObjectColor des verlinkten Hatches aus Backup wieder her.""" h = _find_linked_hatch(doc, curve_obj) if h is None: return False a = h.Attributes if a.GetUserString(_HATCH_COLOR_OVERRIDDEN) != "1": return False try: orig_src = a.GetUserString(_ORIG_HC_SRC) or "1" # default ColorFromObject orig_col = a.GetUserString(_ORIG_HC) or "#f5f5f5" new_a = h.Attributes.Duplicate() # ColorSource zuruecksetzen — Enum.ToObject ist in IronPython3 # zuverlaessiger als der direkte int->Enum-Konstruktor. try: val = int(orig_src) new_a.ColorSource = System.Enum.ToObject( Rhino.DocObjects.ObjectColorSource, val) except Exception: new_a.ColorSource = _FROM_OBJECT try: new_a.ObjectColor = _hex_to_color(orig_col) except Exception: new_a.ObjectColor = Drawing.Color.FromArgb(245, 245, 245) # PlotColor mit-resetten try: new_a.PlotColorSource = ( Rhino.DocObjects.ObjectPlotColorSource.PlotColorFromObject) new_a.PlotColor = new_a.ObjectColor except Exception: pass for k in (_ORIG_HC_SRC, _ORIG_HC, _HATCH_COLOR_OVERRIDDEN): try: new_a.SetUserString(k, "") except Exception: pass doc.Objects.ModifyAttributes(h, new_a, True) return True except Exception as ex: print("[OVERRIDES] _restore_hatch_color:", ex) return False def _apply_to_object(doc, obj, overrides): """Setzt die Override-Werte am Objekt. Sichert vorher Originale.""" if not overrides: return False a = obj.Attributes _backup_original(a) new_a = a.Duplicate() changed = False if "color" in overrides: col = _hex_to_color(overrides["color"]) new_a.ColorSource = _FROM_OBJECT new_a.ObjectColor = col # Plot-Color mitspiegeln (sonst druckt's wieder in Layerfarbe) try: new_a.PlotColorSource = Rhino.DocObjects.ObjectPlotColorSource.PlotColorFromObject new_a.PlotColor = col except Exception: pass changed = True # Verlinkten Hatch (Gestaltung-Fuellung) auch einfaerben — sonst # bleibt die Fuellung in der Original-Farbe waehrend die Outline schon # die Override-Farbe traegt. try: _apply_hatch_color_override(doc, obj, overrides["color"]) except Exception: pass if "lineweight" in overrides: try: new_a.PlotWeightSource = _LW_FROM_OBJ new_a.PlotWeight = float(overrides["lineweight"]) changed = True except Exception: pass if "linetype" in overrides: try: idx = doc.Linetypes.Find(overrides["linetype"], True) if idx >= 0: new_a.LinetypeSource = _LT_FROM_OBJ new_a.LinetypeIndex = idx changed = True except Exception: pass if changed: try: doc.Objects.ModifyAttributes(obj, new_a, True) except Exception as ex: print("[OVERRIDES] apply ModifyAttributes:", ex) # Hatch-Override (separater Pfad, modifiziert das verlinkte Hatch) if "hatchPattern" in overrides or "hatchScale" in overrides: if _apply_hatch_override(doc, obj, overrides.get("hatchPattern"), overrides.get("hatchScale")): changed = True return changed def apply_all(doc): """Wendet alle aktiven Regeln auf alle Objekte im Doc an. Objekte die NICHT (mehr) matchen werden auf Originale zurueckgesetzt.""" if doc is None: return 0, 0 cfg = load_config(doc) if not cfg.get("enabled"): return 0, 0 rules = cfg.get("rules") or [] if not rules: return 0, 0 n_applied = 0 n_restored = 0 _set_applying(True) try: for obj in doc.Objects: if obj is None or obj.IsDeleted: continue ovs = _compose_overrides(doc, obj, rules) if ovs: if _apply_to_object(doc, obj, ovs): n_applied += 1 else: # Kein Match aber war evtl. vorher overridden -> restore if obj.Attributes.GetUserString(_OVERRIDDEN) == "1": if _restore_original(doc, obj): n_restored += 1 try: doc.Views.Redraw() except Exception: pass print("[OVERRIDES] apply_all: {} applied, {} restored".format(n_applied, n_restored)) except Exception as ex: print("[OVERRIDES] apply_all:", ex) finally: _set_applying(False) return n_applied, n_restored def restore_all(doc): """Stellt alle Originale wieder her (Overrides aus).""" if doc is None: return 0 n = 0 _set_applying(True) try: for obj in doc.Objects: if obj is None or obj.IsDeleted: continue had_attr_override = (obj.Attributes.GetUserString(_OVERRIDDEN) == "1") # _restore_original kuemmert sich auch um den verlinkten Hatch — # auch wenn die Curve selbst keinen Attribut-Override hatte. if had_attr_override: if _restore_original(doc, obj): n += 1 else: # Vielleicht nur Hatch-Override (Pattern und/oder Color) r1 = _restore_hatch(doc, obj) r2 = _restore_hatch_color(doc, obj) if r1 or r2: n += 1 try: doc.Views.Redraw() except Exception: pass print("[OVERRIDES] restore_all: {} Objekte".format(n)) except Exception as ex: print("[OVERRIDES] restore_all:", ex) finally: _set_applying(False) return n def set_enabled(doc, enabled): """Master-Toggle: an -> apply_all, aus -> restore_all + Config-Flag setzen.""" cfg = load_config(doc) cfg["enabled"] = bool(enabled) save_config(doc, cfg) if enabled: apply_all(doc) else: restore_all(doc) def update_rules(doc, rules, enabled=None): """Schreibt eine neue Regel-Liste. Wenn enabled vorher an war, wird nach dem Speichern apply_all (mit Restore-cleanup) ausgefuehrt. Manuelle Aenderungen an den Rules clearen den activePreset — sonst behauptet das Topbar-Dropdown weiter, das alte Preset sei aktiv obwohl die Rules davon driften (Variante C: Preset ist read-only Snapshot).""" cfg = load_config(doc) if enabled is not None: cfg["enabled"] = bool(enabled) cfg["rules"] = rules or [] cfg["activePreset"] = None save_config(doc, cfg) if cfg.get("enabled"): # Erst alles zuruecksetzen, dann neu anwenden — sonst koennten alte # Overrides "kleben" wenn die neue Regelmenge sie nicht mehr enthaelt. restore_all(doc) apply_all(doc) # --- Live-Update via Doc-Events -------------------------------------------- def _is_applying(): return bool(sc.sticky.get("overrides_applying")) def _set_applying(v): sc.sticky["overrides_applying"] = bool(v) def _apply_to_single_object(doc, obj): """Re-evaluate Overrides fuer ein einzelnes Objekt. Aufgerufen von den Event-Handlern bei neu/geaenderten Objekten.""" if doc is None or obj is None: return cfg = load_config(doc) if not cfg.get("enabled"): return rules = cfg.get("rules") or [] if not rules: # Engine aus oder keine Regeln -> wenn vorher overridden, restore try: if obj.Attributes.GetUserString(_OVERRIDDEN) == "1": _restore_original(doc, obj) except Exception: pass return try: ovs = _compose_overrides(doc, obj, rules) if ovs: _apply_to_object(doc, obj, ovs) elif obj.Attributes.GetUserString(_OVERRIDDEN) == "1": _restore_original(doc, obj) except Exception as ex: print("[OVERRIDES] live single-apply:", ex) def install_listeners(): """Hookt einmalig Rhino-Events fuer Live-Update. Idempotent via sticky-Flag.""" if sc.sticky.get("overrides_listeners"): return def on_add(s, e): if _is_applying(): return try: doc = getattr(e, "TheDoc", None) or Rhino.RhinoDoc.ActiveDoc obj = getattr(e, "TheObject", None) if not obj or not doc: return _set_applying(True) try: _apply_to_single_object(doc, obj) finally: _set_applying(False) except Exception as ex: print("[OVERRIDES] on_add:", ex) _set_applying(False) def on_replace(s, e): # Wird auch von ModifyAttributes gefeuert -> Guard if _is_applying(): return try: doc = getattr(e, "TheDoc", None) or Rhino.RhinoDoc.ActiveDoc obj = getattr(e, "NewRhinoObject", None) or getattr(e, "TheObject", None) if not obj or not doc: return _set_applying(True) try: _apply_to_single_object(doc, obj) finally: _set_applying(False) except Exception as ex: print("[OVERRIDES] on_replace:", ex) _set_applying(False) def on_layer_table(s, e): # Layer geaendert (Name, Properties, ...) — Regeln mit layer_name # koennten andere Matches haben. Vollstaendiges Reapply. if _is_applying(): return try: doc = getattr(e, "Document", None) or Rhino.RhinoDoc.ActiveDoc cfg = load_config(doc) if not cfg.get("enabled"): return _set_applying(True) try: restore_all(doc) apply_all(doc) finally: _set_applying(False) except Exception as ex: print("[OVERRIDES] on_layer_table:", ex) _set_applying(False) try: Rhino.RhinoDoc.AddRhinoObject += on_add Rhino.RhinoDoc.ReplaceRhinoObject += on_replace Rhino.RhinoDoc.LayerTableEvent += on_layer_table except Exception as ex: print("[OVERRIDES] install_listeners:", ex) return sc.sticky["overrides_listeners"] = True print("[OVERRIDES] Live-Update Listener aktiv (Add/Replace/LayerTable)")