Initial commit — Dossier Rhino 8 Plugin
OpenStudio-Suite Architektur-Plugin fuer Rhino 8 (Mac): - Smart-Elemente: Wand, Decke, Dach (Pult/Sattel/Walm/Mansarde), Oeffnungen (Fenster/Tueren mit Rahmen + Sims + Glas + Fluegel), Treppen (gerade · L · Wendel mit Schrittmass-Validierung) - Live-Previews mit Step-Lines + Soll-Range-Clamping - Bidirektionale Selection-Sync zwischen Source-Linie und Volume - Geschoss-/Ebenen-Verwaltung mit OKFF-Persistenz - Layouts mit PDF-Export - Ausschnitte / Massstab / Override-Regeln - Petrol-Gruen Theme (Rapport-konform) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,718 @@
|
||||
# ! python3
|
||||
# -*- 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")
|
||||
|
||||
# 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
|
||||
|
||||
_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)
|
||||
|
||||
|
||||
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)
|
||||
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_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
|
||||
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
|
||||
if _restore_hatch(doc, obj):
|
||||
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)")
|
||||
Reference in New Issue
Block a user