961b3c0396
Stable working state after a long iteration session. The plugin now supports:
- Multi-Surface-Select für alle Element-Typen (Türen/Fenster/Treppen/Tragwerk)
- Wand-Z-Drag → unbound mode (UK/OK-Override, Wand vom Geschoss entkoppelt)
- Wand-Z-Drag nimmt verknüpfte Öffnungen mit (Brüstung += delta_z via Idle-Pfad)
- Öffnungs-XY-Drag snapt direktional auf Wand-Tangente
- Öffnungs-Z-Drag passt Brüstung an (Fenster sofort sync, Tür deferred)
- Wand-Delete kaskadiert Öffnungen (deferred via Idle, robust gegen _Rotate/_Move)
- Source-Cascade beim Öffnungs-Delete (deferred analog Wand-Kaskade)
- Listener-Cleanup robust gegen _reset_panels.py Reload (Refs in
_dossier_runtime_event_refs gespeichert, vor Re-Install deregistriert)
- _count_same_id_type filtert IsDeleted (verhindert Source-Duplikat-Bug bei Move)
- Frontend: Brüstungs-Slider für Tür ("Schwelle"), Flügel-Block nur bei Fenster
Plus aus früherer Phase dieser Session:
- Dossier-Launcher Auto-Load via Rhinos StartupCommands-XML
- Default-Pfad zeigt auf gebundeltes startup.py (out-of-the-box für neue User)
- Splash-Window beim Plugin-Load mit native macOS rounded corners
- Diverse Launcher-Verbesserungen (Brüstungs-Default, tauri.conf, capabilities)
Known issue: bei Multi-Select-Move mit vielen Sub-Volumen kann sporadisch
"Unable to transform" auftreten (Rhinos Move-Operation kollidiert mit Wand-
Regen). Tür-spezifischer Defer-Pfad mildert das, Fenster läuft sync.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
798 lines
28 KiB
Python
798 lines
28 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")
|
|
|
|
# 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)
|
|
|
|
|
|
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)")
|