Files
DOSSIER/rhino/overrides.py
T
karim e3918cb155 Overrides-Fenster aufgeräumt + Rule-Templates
UX-Cleanup:
- Globaler AN/AUS-Toggle entfernt — den gibt's bereits in der
  Oberleiste, doppelt war redundant.
- Reload/Refresh-Button entfernt — Backend re-applied automatisch
  bei jeder Regel-Aenderung, manuelles Reload nicht noetig.
- + (Neue Regel) wurde aus dem Header in eine neue Sektion
  UNTER der Kombinationen-Card verschoben.

Neues Feature: Rule-Templates (einzelne wiederverwendbare Regeln)
- Storage: ~/Library/.../override_rule_templates.json (cross-doc,
  parallel zu den Kombinationen-Presets)
- API in overrides.py: list/save/load/delete_rule_template
- Bridge-Messages: SAVE_RULE_TEMPLATE, DELETE_RULE_TEMPLATE,
  ADD_FROM_TEMPLATE
- State enthaelt jetzt ruleTemplates: [{name, rule}]

UI:
- Neuer Bereich "Neue Regel" unter Kombinationen: [+ leer] +
  [+ Aus Vorlage ▼ dropdown]
- Vorlage waehlen → insert auf hoechste Prio (gleich wie addRule)
- Im Dropdown unten: "🗑 <name> loeschen" zum Entfernen einer Vorlage
- Im Rule-Kontextmenue: neuer Eintrag "Als Vorlage speichern…"
  fragt nach Name, speichert die Regel cross-doc

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 02:10:31 +02:00

867 lines
31 KiB
Python

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