Files
DOSSIER/rhino/styles.py
T
karim d558fac2c3 Styles: default pen for new objects + csharp build doc
Gestaltung panel now shows pen controls (color, lineweight, linetype)
when nothing is selected. Settings persist in sticky and are stamped
onto every newly drawn object (curves, text, hatch, dims — not 3D
solids or DOSSIER element geometry) via the existing AddRhinoObject
listener. Active state shown with badge; resets by switching all back
to "Nach Ebene".

Also adds csharp/BUILD.md with full build + post-reinstall checklist.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 02:00:47 +02:00

2251 lines
94 KiB
Python

#! python 3
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
"""
gestaltung.py
GESTALTUNG-Panel: Attribute der Selektion (Farbe, Stiftdicke, Linientyp,
Hatch-Fuellung).
"""
import os
import sys
import math
import json
import time
import Rhino
import Rhino.Geometry as rg
import scriptcontext as sc
import System
import System.Drawing as Drawing
_HERE = os.path.dirname(os.path.abspath(__file__))
if _HERE not in sys.path:
sys.path.insert(0, _HERE)
import panel_base
PANEL_GUID_STR = "4b8d3f2e-5c9d-4e0f-b2c3-d4e5f6071829"
_FROM_LAYER = Rhino.DocObjects.ObjectColorSource.ColorFromLayer
_FROM_OBJECT = Rhino.DocObjects.ObjectColorSource.ColorFromObject
_LW_FROM_LAYER = Rhino.DocObjects.ObjectPlotWeightSource.PlotWeightFromLayer
_LW_FROM_OBJECT = Rhino.DocObjects.ObjectPlotWeightSource.PlotWeightFromObject
_LT_FROM_LAYER = Rhino.DocObjects.ObjectLinetypeSource.LinetypeFromLayer
_LT_FROM_OBJECT = Rhino.DocObjects.ObjectLinetypeSource.LinetypeFromObject
# Print-Pendants: ohne die plottet eine Hatch mit eigener Display-Farbe in
# Layerfarbe (= gleiche Farbe wie der Stift). Mit PlotColorFromObject +
# PlotColor folgt der Druck der gewuenschten Hatch-Farbe.
_PLOT_FROM_LAYER = Rhino.DocObjects.ObjectPlotColorSource.PlotColorFromLayer
_PLOT_FROM_OBJECT = Rhino.DocObjects.ObjectPlotColorSource.PlotColorFromObject
def _sync_plot_color_to_display(attrs):
"""Spiegelt ColorSource/ObjectColor in PlotColorSource/PlotColor.
Wird ueberall aufgerufen wo wir eine Hatch-Farbe setzen, damit Print = Display."""
try:
cs = int(attrs.ColorSource)
if cs == int(_FROM_OBJECT):
attrs.PlotColorSource = _PLOT_FROM_OBJECT
attrs.PlotColor = attrs.ObjectColor
else:
attrs.PlotColorSource = _PLOT_FROM_LAYER
except Exception as ex:
print("[STYLES] sync plot-color:", ex)
_FILL_KEY = "ebenen_fill_hatch_id"
_FILL_SOURCE_KEY = "ebenen_fill_source" # "layer" oder "object"
_FILL_OWNER_KEY = "ebenen_fill_owner" # Curve-ID, auf Hatch set
_NO_FILL_KEY = "ebenen_no_fill" # "1" wenn User Fuellung explizit aus hat
# Loop-Guard fuer Live-Update
_processing = set()
# Sticky-Mapping curve_id_str -> hatch_id_str. Wird beim Anlegen jeder Hatch
# gefuellt und beim on_delete als Fallback gelesen, falls Rhino die UserStrings
# der geloeschten Curve schon weggewischt hat.
def _link_curve_hatch(curve_id, hatch_id):
m = sc.sticky.get("gestaltung_curve_hatch")
if not isinstance(m, dict):
m = {}
sc.sticky["gestaltung_curve_hatch"] = m
m[str(curve_id)] = str(hatch_id)
def _lookup_hatch_for_curve(curve_id):
m = sc.sticky.get("gestaltung_curve_hatch")
if isinstance(m, dict):
return m.get(str(curve_id))
return None
def _unlink_curve(curve_id):
m = sc.sticky.get("gestaltung_curve_hatch")
if isinstance(m, dict):
m.pop(str(curve_id), None)
# Rhino feuert bei Drag/Move oft on_delete + on_add (statt on_replace).
# Wir merken uns kurz die Hatch-Metadaten bei jedem cascade-delete, damit
# wir die Hatch beim sofortigen Re-Add wiederherstellen koennen.
_PENDING_HATCH_TTL = 3.0 # Sekunden — danach gilt's als echter Delete
def _save_pending_hatch(curve_id, hatch_obj):
try:
hg = hatch_obj.Geometry
ha = hatch_obj.Attributes
meta = {
"pattern_idx": int(hg.PatternIndex),
"scale": float(hg.PatternScale),
"rotation": float(hg.PatternRotation),
"color_source": int(ha.ColorSource),
"color_argb": int(ha.ObjectColor.ToArgb()),
"fill_source": ha.GetUserString(_FILL_SOURCE_KEY) or "object",
"timestamp": time.time(),
}
except Exception as ex:
print("[STYLES] save pending-hatch err:", ex)
return
m = sc.sticky.get("gestaltung_pending_hatch")
if not isinstance(m, dict):
m = {}
sc.sticky["gestaltung_pending_hatch"] = m
m[str(curve_id)] = meta
def _take_pending_hatch(curve_id):
m = sc.sticky.get("gestaltung_pending_hatch")
if not isinstance(m, dict): return None
now = time.time()
expired = [k for k, v in list(m.items())
if now - v.get("timestamp", 0) > _PENDING_HATCH_TTL]
for k in expired: m.pop(k, None)
return m.pop(str(curve_id), None)
def _restore_hatch_from_pending(doc, obj, meta):
"""Erzeugt eine Hatch mit den gespeicherten Metadaten (Drag-Recovery)."""
try:
geom = obj.Geometry
except Exception:
return False
if not _is_closed_planar_curve(geom): return False
try:
new_hatches = rg.Hatch.Create(geom,
meta["pattern_idx"], meta["rotation"], meta["scale"], 0.0)
except Exception as ex:
print("[STYLES] restore Hatch.Create:", ex)
return False
if not new_hatches or len(new_hatches) == 0: return False
new_attrs = Rhino.DocObjects.ObjectAttributes()
new_attrs.LayerIndex = obj.Attributes.LayerIndex
try:
new_attrs.ColorSource = Rhino.DocObjects.ObjectColorSource(meta["color_source"])
except Exception:
try: new_attrs.ColorSource = _FROM_LAYER
except Exception: pass
try:
new_attrs.ObjectColor = Drawing.Color.FromArgb(meta["color_argb"])
except Exception:
pass
new_attrs.SetUserString(_FILL_OWNER_KEY, str(obj.Id))
new_attrs.SetUserString(_FILL_SOURCE_KEY, meta.get("fill_source", "object"))
_sync_plot_color_to_display(new_attrs)
try:
hatch_id = doc.Objects.AddHatch(new_hatches[0], new_attrs)
except Exception as ex:
print("[STYLES] restore AddHatch:", ex)
return False
if hatch_id == System.Guid.Empty: return False
try:
ca = obj.Attributes.Duplicate()
ca.SetUserString(_FILL_KEY, str(hatch_id))
_processing.add(obj.Id)
try: doc.Objects.ModifyAttributes(obj, ca, True)
finally: _processing.discard(obj.Id)
except Exception:
pass
_link_curve_hatch(obj.Id, hatch_id)
return True
def _color_to_hex(c):
"""System.Drawing.Color -> '#rrggbb'. Defensive: IronPython c.R liefert
System.Byte das nicht immer sauber in :02x format einrastet -> int()-Cast."""
if c is None:
return None
try:
return "#{:02x}{:02x}{:02x}".format(int(c.R), int(c.G), int(c.B))
except Exception as ex:
print("[STYLES] color-hex Fehler:", ex)
return None
def _hex_to_color(h):
if not isinstance(h, str): h = "888888"
h = h.strip()
if h.startswith("#"): h = h[1:]
if h.startswith(("0x", "0X")): h = h[2:]
if len(h) == 3: # shorthand #rgb -> #rrggbb
h = h[0] * 2 + h[1] * 2 + h[2] * 2
if len(h) != 6 or any(c not in "0123456789abcdefABCDEF" for c in h):
h = "888888"
return Drawing.Color.FromArgb(int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16))
# Default-Pen ("Stift fuer neue Objekte"): wird in on_add auf frisch
# gezeichnete Kurven gestempelt, solange aktiv. Persistiert in Sticky und
# gilt bis der User wieder auf "Nach Ebene" zurueckstellt. Wird vom selben
# UI-Block gesetzt wie der Selektions-Pen — nur ohne Auswahl (siehe Setter).
_DEFAULT_PEN_KEY = "_dossier_default_pen"
def _default_pen():
p = sc.sticky.get(_DEFAULT_PEN_KEY)
if not isinstance(p, dict):
p = {}
return {
"colorSource": p.get("colorSource", "layer"),
"color": p.get("color"),
"lwSource": p.get("lwSource", "layer"),
"lw": p.get("lw"),
"linetypeSource": p.get("linetypeSource", "layer"),
"linetype": p.get("linetype"),
}
def _set_default_pen(**changes):
p = _default_pen()
p.update(changes)
sc.sticky[_DEFAULT_PEN_KEY] = p
def _default_pen_active(pen=None):
p = pen or _default_pen()
return (p["colorSource"] == "object" or p["lwSource"] == "object"
or p["linetypeSource"] == "object")
def _has_selection(doc):
return bool(list(doc.Objects.GetSelectedObjects(False, False)))
def _apply_pen_to_attrs(doc, a, pen):
"""Stempelt den aktiven Default-Pen auf eine Attribut-Kopie (in-place)."""
if pen["colorSource"] == "object":
a.ColorSource = _FROM_OBJECT
if pen["color"]:
a.ObjectColor = _hex_to_color(pen["color"])
_sync_plot_color_to_display(a)
if pen["lwSource"] == "object" and pen["lw"] is not None:
a.PlotWeightSource = _LW_FROM_OBJECT
try:
import massstab
massstab.write_plotweight(doc, a, float(pen["lw"]))
except Exception:
a.PlotWeight = float(pen["lw"])
if pen["linetypeSource"] == "object" and pen["linetype"]:
idx = -1
try: idx = doc.Linetypes.Find(pen["linetype"], True)
except Exception: idx = -1
if idx >= 0:
a.LinetypeSource = _LT_FROM_OBJECT
a.LinetypeIndex = idx
def _new_object_pen_summary(doc, pen):
"""PenBlock-kompatibles Objekt fuer den leeren Panel-Zustand: zeigt den
aktiven Default-Pen, "Nach Ebene"-Vorschau aus der aktuellen Ebene."""
cur = doc.Layers.CurrentLayer
layer_color = _color_to_hex(cur.Color)
try:
import massstab as _ms
layer_lw = round(_ms.read_plotweight(cur), 4)
except Exception:
layer_lw = round(cur.PlotWeight, 4)
layer_lt = _linetype_name(doc, cur.LinetypeIndex)
return {
"colorSource": pen["colorSource"],
"color": pen["color"] or layer_color,
"layerColor": layer_color,
"lwSource": pen["lwSource"],
"lw": pen["lw"] if pen["lw"] is not None else layer_lw,
"layerLw": layer_lw,
"linetypeSource": pen["linetypeSource"],
"linetype": pen["linetype"] or layer_lt,
"layerLinetype": layer_lt,
"linetypes": _all_linetypes(doc),
"layerName": _safe_layer_label(doc, cur, doc.Layers.CurrentLayerIndex),
"geometryKind": "curveOpen",
"active": _default_pen_active(pen),
}
def _force_load_linetypes(doc):
"""Rhinos Linetype-Tabelle wird lazy initialisiert — wir triggern es."""
# 1) Eingebaute Methode (falls present)
for method_name in ("LoadDefaultLinetypes", "LoadDefaults", "LoadStandardLinetypes"):
try:
getattr(doc.Linetypes, method_name)()
return True
except AttributeError:
continue
except Exception:
continue
# 2) Standardnamen suchen triggert internes Laden in einigen Versionen
for name in ("Hidden", "Dashed", "DashDot", "Dots",
"Border", "Center", "Phantom",
"Hidden2", "Dashed2", "DashDot2"):
try:
doc.Linetypes.Find(name, True)
except Exception:
pass
return False
def _all_linetypes(doc):
"""Liefert alle nicht-geloeschten Linetypes mit Namen. Continuous immer enthalten."""
_force_load_linetypes(doc)
out = []
seen = set()
n = 0
try:
n = doc.Linetypes.Count
except Exception:
pass
for i in range(n):
try:
lt = doc.Linetypes[i]
except Exception:
continue
if lt is None:
continue
try:
if lt.IsDeleted:
continue
except Exception:
pass
try:
name = lt.Name
except Exception:
name = None
if not name or name in seen:
continue
seen.add(name)
out.append(name)
# Continuous immer als erstes — Rhinos Default-Linetype, das oft als
# virtueller Eintrag oder unter anderem Namen verbucht ist.
if "Continuous" not in seen:
out.insert(0, "Continuous")
return out
def _all_hatch_patterns(doc):
out = []
for i in range(doc.HatchPatterns.Count):
hp = doc.HatchPatterns[i]
if hp.IsDeleted: continue
if hp.Name: out.append(hp.Name)
if not out:
out.append("Solid")
return out
def _pattern_name(doc, idx):
if idx is None or idx < 0 or idx >= doc.HatchPatterns.Count:
return None
hp = doc.HatchPatterns[idx]
if hp.IsDeleted: return None
return hp.Name
def _linetype_name(doc, idx):
if idx is None or idx < 0 or idx >= doc.Linetypes.Count:
return None
lt = doc.Linetypes[idx]
if lt.IsDeleted:
return None
return lt.Name
def _is_closed_planar_curve(geom):
return isinstance(geom, rg.Curve) and geom.IsClosed and geom.IsPlanar()
def _ebene_fill_for_layer(doc, layer):
"""Sucht in dossier_ebenen (doc.Strings) die zur Ebene gehoerige fill-Definition.
Match per dossier_code UserString auf dem Sublayer.
Returns dict {pattern, source, color, scale, rotation} oder None.
"""
if layer is None: return None
try:
code = layer.GetUserString("dossier_code")
except Exception:
code = None
if not code:
print("[STYLES] _ebene_fill_for_layer: kein dossier_code auf Layer idx={}".format(
getattr(layer, "LayerIndex", "?")))
return None
raw = doc.Strings.GetValue("dossier_ebenen")
if not raw:
print("[STYLES] _ebene_fill_for_layer: dossier_ebenen leer in doc.Strings")
return None
try:
ebenen = json.loads(raw)
except Exception as ex:
print("[STYLES] _ebene_fill_for_layer: json-Fehler:", ex)
return None
if not isinstance(ebenen, list): return None
# Rekursiv durch Tree — Sub-Ebenen sind in children verschachtelt
def _find_by_code(lst, target):
for e in lst:
if not isinstance(e, dict): continue
if e.get("code") == target: return e
kids = e.get("children")
if isinstance(kids, list) and kids:
hit = _find_by_code(kids, target)
if hit is not None: return hit
return None
found = _find_by_code(ebenen, code)
if found is None: return None
e = found
if True:
f = e.get("fill")
if not isinstance(f, dict):
print("[STYLES] _ebene_fill_for_layer: Ebene code={} has NO fill field".format(code))
return None
# lw: Strichstaerke der Hatch-Linien in mm. None = "wie Stift der Ebene"
# (ColorSource/PlotWeightSource bleibt auf FromLayer).
lw_raw = f.get("lw")
lw_val = None
if lw_raw is not None:
try:
v = float(lw_raw)
if v >= 0: lw_val = v
except Exception:
pass
result = {
"pattern": f.get("pattern", "None"),
"source": f.get("source", "layer"),
"color": f.get("color"),
"scale": float(f.get("scale", 1.0)) if f.get("scale") is not None else 1.0,
"rotation": float(f.get("rotation", 0)) if f.get("rotation") is not None else 0.0,
"lw": lw_val,
}
print("[STYLES] _ebene_fill_for_layer code={} -> {}".format(code, result))
return result
print("[STYLES] _ebene_fill_for_layer: code={} nicht in dossier_ebenen gefunden".format(code))
return None
def _apply_ebene_fill(doc, obj):
"""Wenn obj geschlossene Kurve auf einer Ebene mit fill-Settings ist,
erzeugt automatisch eine Hatch entsprechend der Ebenen-Definition."""
if obj is None: return False
try:
attrs = obj.Attributes
except Exception:
return False
# schon gefuellt oder explizit als "keine Fuellung" markiert?
try:
if attrs.GetUserString(_FILL_KEY): return False
if attrs.GetUserString(_NO_FILL_KEY) == "1": return False
except Exception:
pass
try:
geom = obj.Geometry
except Exception:
return False
if not _is_closed_planar_curve(geom): return False
try:
layer_idx = int(attrs.LayerIndex)
except Exception:
return False
if layer_idx < 0 or layer_idx >= doc.Layers.Count: return False
layer = doc.Layers[layer_idx]
fill = _ebene_fill_for_layer(doc, layer)
if fill is None: return False
if fill["pattern"] == "None": return False
pattern_idx = doc.HatchPatterns.Find(fill["pattern"], True)
if pattern_idx < 0:
pattern_idx = doc.HatchPatterns.Find("Solid", True)
if pattern_idx < 0:
pattern_idx = doc.HatchPatterns.CurrentHatchPatternIndex
scale_v = float(fill["scale"]) or 1.0
rot_rad = math.radians(float(fill["rotation"]))
# Massstabs-Multiplikator: layer-Skala ist in "Paper-Units" definiert
# (= so wie sie auf dem Druck aussehen soll). Bei eingestelltem 1:N wird
# entsprechend hochskaliert damit die Hatch auf Paper richtig wirkt.
try:
import massstab
m = massstab.get_current_massstab_factor(doc)
if m and m > 0:
scale_v = scale_v * m
except Exception:
pass
try:
hatches = rg.Hatch.Create(geom, pattern_idx, rot_rad, scale_v, 0.0)
except Exception as ex:
print("[STYLES] Auto-Fill Hatch.Create:", ex)
return False
if not hatches or len(hatches) == 0: return False
from_layer = (fill["source"] == "layer")
new_attrs = Rhino.DocObjects.ObjectAttributes()
new_attrs.LayerIndex = layer_idx
if from_layer:
new_attrs.ColorSource = _FROM_LAYER
else:
new_attrs.ColorSource = _FROM_OBJECT
new_attrs.ObjectColor = _hex_to_color(fill.get("color") or "#888888")
# Hatch-Strichstaerke: wenn lw definiert -> PlotWeight von Object (Print-aware via massstab)
lw_val = fill.get("lw")
if lw_val is not None:
try:
import massstab as _ms_lw
_ms_lw.write_plotweight(doc, new_attrs, float(lw_val))
new_attrs.PlotWeightSource = _LW_FROM_OBJECT
except Exception as _ex:
new_attrs.PlotWeightSource = _LW_FROM_OBJECT
new_attrs.PlotWeight = float(lw_val)
new_attrs.SetUserString(_FILL_OWNER_KEY, str(obj.Id))
new_attrs.SetUserString(_FILL_SOURCE_KEY, "layer") # gekoppelt an Ebene
_sync_plot_color_to_display(new_attrs)
try:
hatch_id = doc.Objects.AddHatch(hatches[0], new_attrs)
except Exception as ex:
print("[STYLES] Auto-Fill AddHatch:", ex)
return False
if hatch_id == System.Guid.Empty: return False
# Wenn Print-Mode aktiv ist, neue Hatch sofort mit Massstab skalieren
try:
import massstab
h_obj = doc.Objects.FindId(hatch_id)
if h_obj is not None:
massstab.post_create_hatch_scale(doc, h_obj, float(fill["scale"]) or 1.0)
except Exception as ex:
print("[STYLES] post_create_hatch_scale (auto-fill):", ex)
try:
ca = obj.Attributes.Duplicate()
ca.SetUserString(_FILL_KEY, str(hatch_id))
_processing.add(obj.Id)
try: doc.Objects.ModifyAttributes(obj, ca, True)
finally: _processing.discard(obj.Id)
except Exception as ex:
print("[STYLES] Auto-Fill UserString:", ex)
_link_curve_hatch(obj.Id, hatch_id)
return True
def refresh_layer_fills(doc):
"""Gleicht Hatches an die aktuellen fill-Settings ihrer zugehoerigen Ebene
an — fuer Hatches die ueber 'Nach Ebene' angelegt wurden (Marker
FILL_SOURCE_KEY=='layer'). Wird beim Apply der Ebenen-Einstellungen
aufgerufen, nicht bei Selection-Events.
Drei Stufen:
1) Pattern/Skala/Rotation der bestehenden Hatches anpassen.
2) Farbe / ColorSource an fill.source + fill.color anpassen — Hatches
mit source=='layer' folgen der Ebenen-Definition. User-Overrides
(source=='object' am Hatch) bleiben unangetastet.
3) Auto-Fill nachziehen: geschlossene Kurven auf Ebenen mit aktivem
Pattern, die noch keine Hatch UND keinen NO_FILL-Marker haben,
bekommen jetzt eine Hatch (so wirken nachtraeglich definierte
Fuellungen auch auf alte Zeichnungen).
* Pattern 'None' in der Ebene loescht KEINE Hatches — der User entfernt
Fuellungen explizit ueber die Gestaltung-Panel.
"""
raw = doc.Strings.GetValue("dossier_ebenen")
if not raw:
return 0
try:
ebenen = json.loads(raw)
except Exception:
return 0
if not isinstance(ebenen, list):
return 0
# Code -> fill-dict fuer schnellen Lookup. Rekursiv durch Children, damit
# Sub-Ebenen-Schraffuren auch wirken (sonst landen Polygone auf z.B.
# 70_osm/7102_Gebaeudeumrisse nie in der Auto-Fill-Logik).
def _walk_fills(lst, out):
for e in lst:
if not isinstance(e, dict): continue
f = e.get("fill")
if isinstance(f, dict) and f.get("pattern") not in (None, "None"):
out[e.get("code")] = {
"pattern": f.get("pattern"),
"source": f.get("source", "layer"),
"color": f.get("color"),
"scale": float(f.get("scale", 1.0)) if f.get("scale") is not None else 1.0,
"rotation": float(f.get("rotation", 0.0)) if f.get("rotation") is not None else 0.0,
}
kids = e.get("children")
if isinstance(kids, list) and kids:
_walk_fills(kids, out)
fill_by_code = {}
_walk_fills(ebenen, fill_by_code)
if not fill_by_code:
return 0
# --- 1+2) Bestehende Layer-Hatches einsammeln ---
targets = []
owner_ids = set()
try:
for obj in doc.Objects:
if obj is None: continue
try:
if obj.IsDeleted: continue
except Exception:
continue
try:
attrs = obj.Attributes
if attrs.GetUserString(_FILL_SOURCE_KEY) != "layer": continue
owner_id_str = attrs.GetUserString(_FILL_OWNER_KEY)
except Exception:
continue
if not owner_id_str: continue
try:
owner_id = System.Guid(owner_id_str)
except Exception:
continue
owner = doc.Objects.FindId(owner_id)
if owner is None or owner.IsDeleted: continue
targets.append((obj, owner))
owner_ids.add(str(owner.Id))
except Exception as ex:
print("[STYLES] refresh_layer_fills scan:", ex)
return 0
updated = 0
color_updated = 0
skipped = 0
for hatch_obj, owner in targets:
try:
layer_idx = owner.Attributes.LayerIndex
except Exception:
continue
layer = doc.Layers[layer_idx] if 0 <= layer_idx < doc.Layers.Count else None
try:
code = layer.GetUserString("dossier_code") if layer is not None else None
except Exception:
code = None
fill = fill_by_code.get(code) if code else None
if fill is None:
skipped += 1
continue
pattern_idx = doc.HatchPatterns.Find(fill["pattern"], True)
if pattern_idx < 0:
pattern_idx = doc.HatchPatterns.Find("Solid", True)
if pattern_idx < 0:
pattern_idx = doc.HatchPatterns.CurrentHatchPatternIndex
scale_v = float(fill["scale"]) or 1.0
rot_rad = math.radians(float(fill["rotation"]))
# Massstab beachten (siehe _apply_ebene_fill)
try:
import massstab
m = massstab.get_current_massstab_factor(doc)
if m and m > 0:
scale_v = scale_v * m
except Exception:
pass
# (1) Geometrie-Refresh wenn Pattern/Skala/Drehung sich geaendert haben
try:
hg = hatch_obj.Geometry
cur_p = hg.PatternIndex
cur_s = hg.PatternScale
cur_r = hg.PatternRotation
except Exception:
cur_p, cur_s, cur_r = -1, -1.0, -1.0
needs_rebuild = not (cur_p == pattern_idx
and abs(cur_s - scale_v) <= 1e-6
and abs(cur_r - rot_rad) <= 1e-6)
if needs_rebuild:
try:
geom = owner.Geometry
if _is_closed_planar_curve(geom):
new_h = rg.Hatch.Create(geom, pattern_idx, rot_rad, scale_v, 0.0)
if new_h and len(new_h) > 0:
_processing.add(hatch_obj.Id)
try:
doc.Objects.Replace(hatch_obj.Id, new_h[0])
finally:
_processing.discard(hatch_obj.Id)
updated += 1
# Print-Mode-aware Skalierung + Original-Update
try:
import massstab as _ms
h_obj = doc.Objects.FindId(hatch_obj.Id)
if h_obj is not None:
_ms.post_create_hatch_scale(doc, h_obj, scale_v)
except Exception as _ex:
print("[STYLES] post_create_hatch_scale (refresh):", _ex)
except Exception as ex:
print("[STYLES] refresh rebuild:", ex)
# (2) Farb-Sync — Hatch mit source=='layer' folgt der Ebenen-Definition
try:
refreshed = doc.Objects.FindId(hatch_obj.Id) or hatch_obj
ha = refreshed.Attributes
want_from_layer = (fill["source"] == "layer")
want_color = _hex_to_color(fill.get("color") or "#888888")
cur_cs = int(ha.ColorSource)
need_change = False
if want_from_layer:
if cur_cs != int(_FROM_LAYER):
need_change = True
else:
if cur_cs != int(_FROM_OBJECT):
need_change = True
else:
try:
if int(ha.ObjectColor.ToArgb()) != int(want_color.ToArgb()):
need_change = True
except Exception:
need_change = True
if need_change:
na = ha.Duplicate()
if want_from_layer:
na.ColorSource = _FROM_LAYER
else:
na.ColorSource = _FROM_OBJECT
na.ObjectColor = want_color
_sync_plot_color_to_display(na)
_processing.add(refreshed.Id)
try:
doc.Objects.ModifyAttributes(refreshed, na, True)
finally:
_processing.discard(refreshed.Id)
color_updated += 1
except Exception as ex:
print("[STYLES] refresh color-sync:", ex)
# (3) Hatch-PlotWeight an fill.lw anpassen (None = wieder ByLayer)
try:
want_lw = fill.get("lw")
refreshed = doc.Objects.FindId(hatch_obj.Id) or hatch_obj
ha = refreshed.Attributes
cur_src = int(ha.PlotWeightSource)
need_lw_change = False
if want_lw is None:
# Auf ByLayer zuruecksetzen
if cur_src != int(_LW_FROM_LAYER):
need_lw_change = True
else:
if cur_src != int(_LW_FROM_OBJECT):
need_lw_change = True
else:
try:
import massstab as _ms_lw_chk
cur_real = _ms_lw_chk.read_plotweight(ha)
if abs(float(cur_real) - float(want_lw)) > 1e-6:
need_lw_change = True
except Exception:
if abs(float(ha.PlotWeight or 0) - float(want_lw)) > 1e-6:
need_lw_change = True
if need_lw_change:
na = ha.Duplicate()
if want_lw is None:
na.PlotWeightSource = _LW_FROM_LAYER
else:
na.PlotWeightSource = _LW_FROM_OBJECT
try:
import massstab as _ms_lw_w
_ms_lw_w.write_plotweight(doc, na, float(want_lw))
except Exception:
na.PlotWeight = float(want_lw)
_processing.add(refreshed.Id)
try:
doc.Objects.ModifyAttributes(refreshed, na, True)
finally:
_processing.discard(refreshed.Id)
except Exception as ex:
print("[STYLES] refresh lw-sync:", ex)
# --- 3) Auto-Fill nachziehen fuer Kurven ohne Hatch ---
added = 0
# Code -> Sublayer-Indizes (alle Zeichnungsebenen)
try:
layers_by_code = {}
for i in range(doc.Layers.Count):
layer = doc.Layers[i]
if layer is None or layer.IsDeleted: continue
try:
c = layer.GetUserString("dossier_code")
except Exception:
c = None
if c and c in fill_by_code:
layers_by_code.setdefault(c, []).append(i)
for code, idxs in layers_by_code.items():
for layer_idx in idxs:
layer = doc.Layers[layer_idx]
try:
curves = list(doc.Objects.FindByLayer(layer))
except Exception:
continue
for obj in curves:
if obj is None: continue
try:
if obj.IsDeleted: continue
except Exception:
continue
# Hatches selbst ueberspringen (FindByLayer liefert auch sie)
if str(obj.Id) in owner_ids:
continue
try:
ga = obj.Attributes
if ga.GetUserString(_FILL_KEY): continue
if ga.GetUserString(_FILL_OWNER_KEY): continue # ist selbst eine Hatch
if ga.GetUserString(_NO_FILL_KEY) == "1": continue
except Exception:
continue
try:
if not _is_closed_planar_curve(obj.Geometry): continue
except Exception:
continue
try:
if _apply_ebene_fill(doc, obj):
added += 1
except Exception as ex:
print("[STYLES] refresh auto-fill:", ex)
except Exception as ex:
print("[STYLES] refresh auto-fill scan:", ex)
if updated or color_updated or added:
doc.Views.Redraw()
print("[STYLES] refresh_layer_fills: pattern={}, farbe={}, neu={}, unchanged={}".format(
updated, color_updated, added, skipped))
return updated + color_updated + added
def repair_plot_colors(doc):
"""Synct PlotColor/PlotColorSource an Color/ColorSource fuer alle Objekte
mit benutzerdefinierter Farbe (ColorSource == FromObject).
Hintergrund: Rhino fuehrt fuer Anzeige und Druck zwei getrennte Farb-
Quellen — ColorSource (Display) und PlotColorSource (Plot). Default fuer
Plot ist 'PlotColorFromLayer'. Setzt der User die Display-Farbe ueber,
bleibt der Plot trotzdem auf Layerfarbe haengen -> Anzeige und Druck
weichen ab. Diese Funktion gleicht beides ab.
Scope: nur Objekte wo ColorSource == FromObject (User hat explizit
ueberschrieben). Objekte mit FromLayer werden nicht angefasst — deren
PlotColorFromLayer Default ist bereits konsistent.
No-op falls schon synchron. Laeuft beim Panel-Start und nach Apply.
"""
fixed = 0
scanned = 0
try:
for obj in doc.Objects:
if obj is None: continue
try:
if obj.IsDeleted: continue
attrs = obj.Attributes
cs = int(attrs.ColorSource)
except Exception:
continue
if cs != int(_FROM_OBJECT):
continue # FromLayer -> Default ist bereits ok
scanned += 1
try:
pcs = int(attrs.PlotColorSource)
need_pcs = (pcs != int(_PLOT_FROM_OBJECT))
need_pcol = False
try:
need_pcol = (int(attrs.PlotColor.ToArgb()) != int(attrs.ObjectColor.ToArgb()))
except Exception:
need_pcol = True
if not (need_pcs or need_pcol):
continue
ha = attrs.Duplicate()
_sync_plot_color_to_display(ha)
_processing.add(obj.Id)
try:
doc.Objects.ModifyAttributes(obj, ha, True)
finally:
_processing.discard(obj.Id)
fixed += 1
except Exception as ex:
print("[STYLES] repair_plot_colors entry:", ex)
except Exception as ex:
print("[STYLES] repair_plot_colors scan:", ex)
return 0
if fixed:
doc.Views.Redraw()
print("[STYLES] repair_plot_colors: {} Objekte repariert (von {} mit Eigenfarbe gescannt)".format(fixed, scanned))
return fixed
def _safe_layer_label(doc, layer, idx):
"""Baut ein ASCII-only Layer-Label aus den dossier_id/dossier_code UserStrings,
um layer.FullPath/Name (kann mit Umlauten auf Mac eine UnicodeDecodeError werfen)
zu vermeiden. Fallback: layer.Name in try/except, sonst Index."""
try:
code = layer.GetUserString("dossier_code")
except Exception:
code = None
if code:
parent_id_str = None
try:
parent_id_str = str(layer.ParentLayerId)
except Exception:
pass
z_id = None
if parent_id_str and parent_id_str != "00000000-0000-0000-0000-000000000000":
try:
for pl in doc.Layers:
try:
if pl.IsDeleted: continue
if str(pl.Id) == parent_id_str:
z_id = pl.GetUserString("dossier_id") or None
break
except Exception:
continue
except Exception:
pass
return "{}/{}".format(z_id or "?", code)
# Kein DOSSIER-Layer — try Name, dann Index
try:
return layer.Name
except Exception:
return "Layer {}".format(idx)
def _selection_summary(doc):
objs = list(doc.Objects.GetSelectedObjects(False, False))
base = {"count": 0, "linetypes": _all_linetypes(doc), "hatchPatterns": _all_hatch_patterns(doc)}
if not objs:
base["newObjectPen"] = _new_object_pen_summary(doc, _default_pen())
return base
color_sources, colors = set(), set()
lw_sources, lws = set(), set()
lt_sources, lts = set(), set()
lt_scales = set()
layer_colors, layer_lws, layer_lts, layer_names = set(), set(), set(), set()
fill_enabled = set()
fill_colors = set()
fill_sources = set()
fill_patterns = set()
fill_scales = set()
fill_rots = set()
has_closed_curves = False
# Section-Style (3D)
sec_enabled = set()
sec_sources = set()
sec_colors = set()
sec_patterns = set()
sec_scales = set()
sec_rots = set()
# Boundary-Subsettings (Schnittkante)
sec_bdy_visible = set()
sec_bdy_colors = set()
sec_bdy_widths = set()
sec_bg_colors = set() # Background-Fill (None wenn FillMode != SolidColor)
# Geometry-Kind-Klassifikation: 'curve' (closed planar 2D), 'curveOpen'
# (offene Kurve), '3d' (Brep/Extrusion/Mesh — Volumen mit Schnittflaeche),
# 'other'. Aggregiert ueber alle Selektions-Objekte zu kind=
# 'curve' / '3d' / 'mixed' / 'other'.
geometry_kinds = set()
for obj in objs:
a = obj.Attributes
color_sources.add(int(a.ColorSource))
oc = _color_to_hex(a.ObjectColor)
if oc: colors.add(oc)
lw_sources.add(int(a.PlotWeightSource))
# Print-Mode-aware: zeige im Panel den "echten" PlotWeight, nicht den
# mit dem Massstab-Faktor multiplizierten Display-Wert.
try:
import massstab as _ms
lws.add(round(_ms.read_plotweight(a), 4))
except Exception:
lws.add(round(a.PlotWeight, 4))
lt_sources.add(int(a.LinetypeSource))
ltn = _linetype_name(doc, a.LinetypeIndex)
if ltn: lts.add(ltn)
for prop in ("LinetypePatternLengthScale", "LinetypeScale"):
if hasattr(a, prop):
try:
lt_scales.add(round(float(getattr(a, prop)), 4))
break
except Exception:
pass
if a.LayerIndex >= 0 and a.LayerIndex < doc.Layers.Count:
layer = doc.Layers[a.LayerIndex]
lc = _color_to_hex(layer.Color)
if lc: layer_colors.add(lc)
try:
import massstab as _ms2
layer_lws.add(round(_ms2.read_plotweight(layer), 4))
except Exception:
layer_lws.add(round(layer.PlotWeight, 4))
ll = _linetype_name(doc, layer.LinetypeIndex)
if ll: layer_lts.add(ll)
# WICHTIG: layer.FullPath/Name liefert auf Mac mit Umlauten (Ä in WAENDE etc.)
# eine UnicodeDecodeError ueber die IronPython<->.NET-Bruecke. Wir benutzen
# stattdessen unsere ASCII-only UserStrings (dossier_id + dossier_code) die wir
# beim Layer-Bau set haben.
nm = _safe_layer_label(doc, layer, a.LayerIndex)
layer_names.add(nm)
# Geometry-Klassifikation. DOSSIER-Source-Curves (wand_axis,
# decke_outline, ...) sind Meta-Geometrie und keine User-facing
# Form — fuer den Section/Fill-Entscheid ignorieren. Dann wird
# eine Wand-Selektion (Achse + Volume) als reines 3D klassifiziert.
g = obj.Geometry
is_3d = isinstance(g, (rg.Brep, rg.Extrusion, rg.Mesh, rg.SubD))
dossier_type = ""
try: dossier_type = a.GetUserString("dossier_element_type") or ""
except Exception: pass
is_dossier_source = dossier_type.endswith(("_axis", "_outline", "_point"))
if isinstance(g, rg.Curve) and not is_dossier_source:
geometry_kinds.add('curve' if (g.IsClosed and g.IsPlanar()) else 'curveOpen')
elif is_3d:
geometry_kinds.add('3d')
elif isinstance(g, rg.Curve) and is_dossier_source:
pass # ignorieren — Volume zaehlt fuer die Klassifikation
else:
geometry_kinds.add('other')
# Section-Style aus Object-Attributes lesen — Rhino 8 Mac packt die
# Settings in ein SectionStyle-Objekt (via GetCustomSectionStyle),
# NICHT in direkte Attribute-Properties wie das alte API.
if is_3d:
src_attr = None
try:
src_attr = getattr(a, "SectionAttributesSource", None)
except Exception: src_attr = None
src_is_object = False
if src_attr is not None:
try:
src_name = str(src_attr).lower()
if "object" in src_name:
sec_sources.add("object"); src_is_object = True
elif "layer" in src_name:
sec_sources.add("layer")
except Exception: pass
# Wenn Source=FromObject: aus dem Custom-SectionStyle lesen.
# Sonst (FromLayer): vom Layer.GetCustomSectionStyle() lesen damit
# die UI auch im Layer-Modus den effektiven Hatch zeigt.
css = None
try:
if src_is_object and hasattr(a, "GetCustomSectionStyle"):
css = a.GetCustomSectionStyle()
if css is None:
# Fallback: Layer-SectionStyle
try:
lyr = doc.Layers[obj.Attributes.LayerIndex]
if hasattr(lyr, "GetCustomSectionStyle"):
css = lyr.GetCustomSectionStyle()
except Exception: pass
except Exception: pass
if css is not None:
# HatchIndex
hidx = None
for n in ("HatchIndex", "HatchPatternIndex"):
if hasattr(css, n):
try:
v = getattr(css, n)
if v is not None: hidx = int(v); break
except Exception: pass
if hidx is not None and hidx >= 0 and hidx < doc.HatchPatterns.Count:
sec_enabled.add(True)
try: sec_patterns.add(doc.HatchPatterns[hidx].Name)
except Exception: pass
elif hidx == -1:
sec_enabled.add(False)
# Scale
for n in ("HatchScale", "HatchPatternScale"):
if hasattr(css, n):
try: sec_scales.add(round(float(getattr(css, n)), 4)); break
except Exception: pass
# Rotation (rad → deg)
for n in ("HatchRotationRadians", "HatchRotation", "HatchAngle"):
if hasattr(css, n):
try: sec_rots.add(round(math.degrees(float(getattr(css, n))), 2)); break
except Exception: pass
# Color
for n in ("HatchPatternColor", "HatchColor", "FillColor"):
if hasattr(css, n):
try:
c = _color_to_hex(getattr(css, n))
if c: sec_colors.add(c); break
except Exception: pass
# Boundary-Settings auslesen
if hasattr(css, "BoundaryVisible"):
try: sec_bdy_visible.add(bool(css.BoundaryVisible))
except Exception: pass
if hasattr(css, "BoundaryColor"):
try:
c = _color_to_hex(css.BoundaryColor)
if c: sec_bdy_colors.add(c)
except Exception: pass
if hasattr(css, "BoundaryWidthScale"):
try: sec_bdy_widths.add(round(float(css.BoundaryWidthScale), 2))
except Exception: pass
# Background nur lesen wenn FillMode != Viewport (sonst transparent)
try:
mode = getattr(css, "BackgroundFillMode", None)
if mode is not None and "viewport" not in str(mode).lower():
c = _color_to_hex(css.BackgroundFillColor)
if c: sec_bg_colors.add(c)
except Exception: pass
else:
sec_enabled.add(False)
# Fuellung
if _is_closed_planar_curve(obj.Geometry):
has_closed_curves = True
hatch_id_str = a.GetUserString(_FILL_KEY)
hatch_obj = None
if hatch_id_str:
try:
hatch_obj = doc.Objects.FindId(System.Guid(hatch_id_str))
except Exception:
hatch_obj = None
if hatch_obj is not None and not hatch_obj.IsDeleted:
fill_enabled.add(True)
ha = hatch_obj.Attributes
# Source aus UserString-Marker, faellt auf ColorSource zurueck
src_marker = None
try:
src_marker = ha.GetUserString(_FILL_SOURCE_KEY)
except Exception:
src_marker = None
if src_marker == "layer":
fill_sources.add("layer")
elif src_marker == "object":
fill_sources.add("object")
elif int(ha.ColorSource) == int(_FROM_LAYER):
fill_sources.add("layer")
else:
fill_sources.add("object")
if int(ha.ColorSource) == int(_FROM_LAYER):
if ha.LayerIndex >= 0 and ha.LayerIndex < doc.Layers.Count:
c = _color_to_hex(doc.Layers[ha.LayerIndex].Color)
if c: fill_colors.add(c)
else:
c = _color_to_hex(ha.ObjectColor)
if c: fill_colors.add(c)
try:
hg = hatch_obj.Geometry
pn = _pattern_name(doc, hg.PatternIndex)
if pn: fill_patterns.add(pn)
# Print-Mode-aware: bei aktivem Print zeigen wir die
# "echte" Skala (= das Original vor der Massstab-
# Multiplikation), nicht den display-skalierten Wert.
eff_scale = hg.PatternScale
try:
orig = hatch_obj.Attributes.GetUserString("dossier_hatch_scale_orig")
if orig: eff_scale = float(orig)
except Exception: pass
fill_scales.add(round(eff_scale, 4))
fill_rots.add(round(math.degrees(hg.PatternRotation), 2))
except Exception:
pass
else:
fill_enabled.add(False)
# Tri-State auch ohne Hatch melden:
# NO_FILL_KEY=='1' -> "none" (User hat explizit aus)
# Curve auf DOSSIER-Sublayer -> "layer" (folgt Ebene, aktuell leer)
# sonst -> "none"
try:
no_fill = (a.GetUserString(_NO_FILL_KEY) == "1")
except Exception:
no_fill = False
if no_fill:
fill_sources.add("none")
else:
on_dossier_layer = False
if a.LayerIndex >= 0 and a.LayerIndex < doc.Layers.Count:
try:
tc = doc.Layers[a.LayerIndex].GetUserString("dossier_code")
on_dossier_layer = bool(tc)
except Exception:
on_dossier_layer = False
fill_sources.add("layer" if on_dossier_layer else "none")
def single(s):
return next(iter(s)) if len(s) == 1 else None
cs = single(color_sources); ls = single(lw_sources); lts_ = single(lt_sources)
result = dict(base)
result.update({
"count": len(objs),
"colorSource": "layer" if cs == int(_FROM_LAYER) else ("object" if cs == int(_FROM_OBJECT) else "mixed"),
"color": single(colors),
"lwSource": "layer" if ls == int(_LW_FROM_LAYER) else ("object" if ls == int(_LW_FROM_OBJECT) else "mixed"),
"lw": single(lws),
"linetypeSource": "layer" if lts_ == int(_LT_FROM_LAYER) else ("object" if lts_ == int(_LT_FROM_OBJECT) else "mixed"),
"linetype": single(lts),
"linetypeScale": single(lt_scales),
"layerColor": single(layer_colors),
"layerLw": single(layer_lws),
"layerLinetype": single(layer_lts),
"layerName": single(layer_names),
"canFill": has_closed_curves,
# Section-Style (3D)
"sectionEnabled": single(sec_enabled),
"sectionSource": single(sec_sources),
"sectionColor": single(sec_colors),
"sectionPattern": single(sec_patterns),
"sectionScale": single(sec_scales),
"sectionRotation": single(sec_rots),
"sectionBoundaryVisible": single(sec_bdy_visible),
"sectionBoundaryColor": single(sec_bdy_colors),
"sectionBoundaryWidthScale": single(sec_bdy_widths),
"sectionBackgroundColor": single(sec_bg_colors),
# geometryKind: 'curve' | 'curveOpen' | '3d' | 'mixed' | 'other'
"geometryKind": (
'mixed' if len(geometry_kinds & {'curve', 'curveOpen', '3d'}) > 1
else (next(iter(geometry_kinds)) if len(geometry_kinds) == 1 else 'other')
),
"fillEnabled": single(fill_enabled),
"fillColor": single(fill_colors),
"fillSource": single(fill_sources),
"fillPattern": single(fill_patterns),
"fillScale": single(fill_scales),
"fillRotation": single(fill_rots),
"hatchPatterns": _all_hatch_patterns(doc),
})
print("[STYLES] sel: n={} colorSrc={} color={} layerColor={}".format(
result.get("count"), result.get("colorSource"),
result.get("color"), result.get("layerColor")))
return result
class GestaltungBridge(panel_base.BaseBridge):
def __init__(self):
panel_base.BaseBridge.__init__(self, "gestaltung")
def _on_ready(self):
doc = Rhino.RhinoDoc.ActiveDoc
try:
before = doc.Linetypes.Count
ok = _force_load_linetypes(doc)
after = doc.Linetypes.Count
print("[STYLES] Linetypes before: {}, nach LoadDefaults({}): {}".format(before, ok, after))
entries = []
for i in range(after):
lt = doc.Linetypes[i]
if lt is None: continue
try: flags = "del" if lt.IsDeleted else ("ref" if lt.IsReference else "ok")
except Exception: flags = "?"
try: nm = lt.Name
except Exception: nm = "?"
entries.append("[{}] {} ({})".format(i, nm, flags))
print("[STYLES] {}".format(" | ".join(entries)))
except Exception as ex:
print("[STYLES] Linetype-Diagnose:", ex)
# One-Shot Repair: aeltere Hatches (vor dem PlotColor-Fix angelegt)
# bekommen ihre Print-Attribute mit Display synchronisiert.
try:
repair_plot_colors(doc)
except Exception as ex:
print("[STYLES] repair on ready:", ex)
self._send_selection()
def handle(self, data):
if not isinstance(data, dict):
return
t = data.get("type", "")
p = data.get("payload") or {}
if not isinstance(p, dict):
p = {}
if t == "READY":
self._on_ready()
elif t == "GET_SELECTION":
self._send_selection()
elif t == "SET_COLOR_SOURCE":
self._set_color_source(p.get("source", "layer"), p.get("color"))
elif t == "SET_LW_SOURCE":
self._set_lw_source(p.get("source", "layer"), p.get("lw"))
elif t == "SET_LINETYPE_SOURCE":
self._set_linetype_source(p.get("source", "layer"), p.get("name"))
elif t == "SET_LINETYPE_SCALE":
self._set_linetype_scale(p.get("scale"))
elif t == "SET_FILL":
self._set_fill(
bool(p.get("enabled")),
p.get("source", "object"),
p.get("color"),
p.get("pattern"),
p.get("scale"),
p.get("rotation"),
)
elif t == "SET_SECTION_STYLE":
self._set_section_style(
bool(p.get("enabled")),
p.get("source", "object"),
p.get("color"),
p.get("pattern"),
p.get("scale"),
p.get("rotation"),
boundary_visible=p.get("boundaryVisible", True),
boundary_width_scale=p.get("boundaryWidthScale", 1.0),
boundary_color_hex=p.get("boundaryColor"),
background_color_hex=p.get("backgroundColor"),
)
def _send_selection(self):
doc = Rhino.RhinoDoc.ActiveDoc
try:
self.send("SELECTION", _selection_summary(doc))
except Exception as ex:
print("[STYLES] Selection:", ex)
# ---- Attribute-Setter ------------------------------------------------
def _modify_each(self, mutator):
"""mutator(attrs) muss die Attrs in-place anpassen."""
doc = Rhino.RhinoDoc.ActiveDoc
objs = list(doc.Objects.GetSelectedObjects(False, False))
for obj in objs:
a = obj.Attributes.Duplicate()
mutator(a, obj)
doc.Objects.ModifyAttributes(obj, a, True)
doc.Views.Redraw()
self._send_selection()
def _set_color_source(self, source, color_hex):
doc = Rhino.RhinoDoc.ActiveDoc
if not _has_selection(doc):
_set_default_pen(colorSource=source,
color=(color_hex if source == "object" else None))
self._send_selection()
return
col = _hex_to_color(color_hex) if (source == "object" and color_hex) else None
def m(a, _obj):
if source == "layer":
a.ColorSource = _FROM_LAYER
else:
a.ColorSource = _FROM_OBJECT
if col is not None: a.ObjectColor = col
# Plot-Pendant mitspiegeln — sonst druckt eine Curve mit eigener
# Display-Farbe trotzdem in Layerfarbe (PlotColorSource bleibt
# auf Default 'PlotColorFromLayer').
_sync_plot_color_to_display(a)
self._modify_each(m)
def _set_lw_source(self, source, lw):
# Print-Mode-aware: bei aktivem Print-View werden PlotWeights skaliert.
# write_plotweight() kuemmert sich um beides (Original-Speicherung +
# Skalierungs-Multiplier).
doc = Rhino.RhinoDoc.ActiveDoc
if not _has_selection(doc):
_set_default_pen(lwSource=source,
lw=(lw if source == "object" else None))
self._send_selection()
return
try:
import massstab
except Exception:
massstab = None
def m(a, _obj):
if source == "layer":
a.PlotWeightSource = _LW_FROM_LAYER
else:
a.PlotWeightSource = _LW_FROM_OBJECT
if lw is not None:
if massstab is not None:
massstab.write_plotweight(doc, a, float(lw))
else:
a.PlotWeight = float(lw)
self._modify_each(m)
def _set_linetype_scale(self, scale):
if scale is None: return
try:
s = float(scale)
except Exception:
return
if s <= 0: return
doc = Rhino.RhinoDoc.ActiveDoc
objs = list(doc.Objects.GetSelectedObjects(False, False))
ok = 0
for obj in objs:
a = obj.Attributes.Duplicate()
applied = False
# Versuch 1: Attribut-Property (Rhino 8)
for prop in ("LinetypePatternLengthScale", "LinetypeScale"):
if hasattr(a, prop):
try:
setattr(a, prop, s)
doc.Objects.ModifyAttributes(obj, a, True)
applied = True
break
except Exception as ex:
print("[STYLES] attr {} fehler: {}".format(prop, ex))
# Versuch 2: direkt auf RhinoObject
if not applied:
for prop in ("LinetypePatternLengthScale", "LinetypeScale"):
if hasattr(obj, prop):
try:
setattr(obj, prop, s)
applied = True
break
except Exception as ex:
print("[STYLES] obj {} fehler: {}".format(prop, ex))
if applied:
ok += 1
doc.Views.Redraw()
if ok == 0:
print("[STYLES] Linetype-Scale nicht unterstuetzt (Rhino-Version?)")
else:
print("[STYLES] Linetype-Scale auf {} Objekt(e) applied".format(ok))
self._send_selection()
def _set_linetype_source(self, source, name):
doc = Rhino.RhinoDoc.ActiveDoc
if not _has_selection(doc):
_set_default_pen(linetypeSource=source,
linetype=(name if source == "object" else None))
self._send_selection()
return
idx = -1
if source == "object" and name:
try:
idx = doc.Linetypes.Find(name, True)
except Exception:
idx = -1
def m(a, _obj):
if source == "layer":
a.LinetypeSource = _LT_FROM_LAYER
else:
a.LinetypeSource = _LT_FROM_OBJECT
if idx >= 0: a.LinetypeIndex = idx
self._modify_each(m)
# ---- Fuellung (Hatch) -----------------------------------------------
def _set_fill(self, enabled, source, color_hex, pattern_name=None, scale=None, rotation_deg=None):
doc = Rhino.RhinoDoc.ActiveDoc
objs = list(doc.Objects.GetSelectedObjects(False, False))
is_layer_source = (source == "layer")
# Werte aus React (nur fuer Object-Source relevant)
passed_pattern_idx = -1
if pattern_name:
passed_pattern_idx = doc.HatchPatterns.Find(pattern_name, True)
if passed_pattern_idx < 0:
passed_pattern_idx = doc.HatchPatterns.Find("Solid", True)
if passed_pattern_idx < 0:
passed_pattern_idx = doc.HatchPatterns.CurrentHatchPatternIndex
passed_color = _hex_to_color(color_hex) if color_hex else _hex_to_color("#cccccc")
passed_scale = float(scale) if scale is not None else 1.0
passed_rot_rad = math.radians(float(rotation_deg)) if rotation_deg is not None else 0.0
for obj in objs:
geom = obj.Geometry
if not _is_closed_planar_curve(geom):
continue
a = obj.Attributes
existing_id_str = a.GetUserString(_FILL_KEY)
existing_hatch = None
if existing_id_str:
try:
existing_hatch = doc.Objects.FindId(System.Guid(existing_id_str))
except Exception:
existing_hatch = None
# Effektive Werte je nach Source bestimmen
# "Nach Ebene" = die fill-Settings der zugehoerigen DOSSIER-Ebene
# (Pattern/Scale/Rotation/Source/Color aus dem Ebenen-Einstellungen-Dialog).
if is_layer_source:
layer_idx = a.LayerIndex
layer = doc.Layers[layer_idx] if 0 <= layer_idx < doc.Layers.Count else None
fill = _ebene_fill_for_layer(doc, layer) if layer is not None else None
if fill is None or fill["pattern"] == "None":
# "Nach Ebene" aber die Ebene hat KEINE Fuellung definiert:
# nichts erzeugen — Curve in "folgt Ebene, aktuell leer"-Zustand
# setzen, damit sie spaeter Auto-Fill bekommt, sobald die Ebene
# ein Pattern bekommt. KEIN Solid-Fallback (gab eine Solid in
# Stiftfarbe, was nicht gewollt ist).
if existing_hatch is not None and not existing_hatch.IsDeleted:
_processing.add(existing_hatch.Id)
try: doc.Objects.Delete(existing_hatch.Id, True)
finally: _processing.discard(existing_hatch.Id)
try:
ca = obj.Attributes.Duplicate()
ca.SetUserString(_FILL_KEY, "")
ca.SetUserString(_NO_FILL_KEY, "")
_processing.add(obj.Id)
try: doc.Objects.ModifyAttributes(obj, ca, True)
finally: _processing.discard(obj.Id)
except Exception as ex:
print("[STYLES] _set_fill follow-layer empty:", ex)
continue
else:
pattern_idx = doc.HatchPatterns.Find(fill["pattern"], True)
if pattern_idx < 0:
pattern_idx = doc.HatchPatterns.Find("Solid", True)
if pattern_idx < 0:
pattern_idx = doc.HatchPatterns.CurrentHatchPatternIndex
scale_v = float(fill["scale"]) or 1.0
rot_rad = math.radians(float(fill["rotation"]))
eff_from_layer = (fill["source"] == "layer")
eff_color = _hex_to_color(fill.get("color") or "#888888") if not eff_from_layer else passed_color
else:
pattern_idx = passed_pattern_idx
scale_v = passed_scale
rot_rad = passed_rot_rad
eff_from_layer = False # Eigene Quelle -> Farbe vom Objekt
eff_color = passed_color
# Massstab-Multiplikator anwenden (Paper-Skala * 1:N).
try:
import massstab
_m = massstab.get_current_massstab_factor(doc)
if _m and _m > 0:
scale_v = scale_v * _m
except Exception:
pass
if enabled:
# Marker "keine Fuellung" aufheben — User will explizit fuellen
try:
if a.GetUserString(_NO_FILL_KEY):
ca = obj.Attributes.Duplicate()
ca.SetUserString(_NO_FILL_KEY, "")
_processing.add(obj.Id)
try: doc.Objects.ModifyAttributes(obj, ca, True)
finally: _processing.discard(obj.Id)
except Exception:
pass
if existing_hatch is not None and not existing_hatch.IsDeleted:
# Pattern / Scale / Rotation: nur Geometrie ersetzen wenn anders
try:
hg = existing_hatch.Geometry
cur_pattern_idx = hg.PatternIndex
cur_scale = hg.PatternScale
cur_rot = hg.PatternRotation
except Exception:
cur_pattern_idx = pattern_idx
cur_scale = scale_v
cur_rot = rot_rad
needs_rebuild = (
cur_pattern_idx != pattern_idx
or abs(cur_scale - scale_v) > 1e-6
or abs(cur_rot - rot_rad) > 1e-6
)
if needs_rebuild:
try:
new_hatches = rg.Hatch.Create(geom, pattern_idx, rot_rad, scale_v, 0.0)
except Exception:
new_hatches = None
if new_hatches and len(new_hatches) > 0:
_processing.add(existing_hatch.Id)
try:
doc.Objects.Replace(existing_hatch.Id, new_hatches[0])
finally:
_processing.discard(existing_hatch.Id)
# Replace: Original-Wert + ggf. Print-Skalierung aktualisieren
try:
import massstab as _ms2
h_obj = doc.Objects.FindId(existing_hatch.Id)
if h_obj is not None:
_ms2.post_create_hatch_scale(doc, h_obj, scale_v)
except Exception as _ex:
print("[STYLES] post_create_hatch_scale (replace):", _ex)
# Farbe / Source / FILL_SOURCE-Marker aktualisieren
refreshed = doc.Objects.FindId(existing_hatch.Id) or existing_hatch
ha = refreshed.Attributes.Duplicate()
if eff_from_layer:
ha.ColorSource = _FROM_LAYER
else:
ha.ColorSource = _FROM_OBJECT
ha.ObjectColor = eff_color
ha.SetUserString(_FILL_SOURCE_KEY, "layer" if is_layer_source else "object")
_sync_plot_color_to_display(ha)
_processing.add(refreshed.Id)
try:
doc.Objects.ModifyAttributes(refreshed, ha, True)
finally:
_processing.discard(refreshed.Id)
else:
try:
hatches = rg.Hatch.Create(geom, pattern_idx, rot_rad, scale_v, 0.0)
except Exception:
hatches = None
if hatches and len(hatches) > 0:
new_attrs = Rhino.DocObjects.ObjectAttributes()
if eff_from_layer:
new_attrs.ColorSource = _FROM_LAYER
else:
new_attrs.ColorSource = _FROM_OBJECT
new_attrs.ObjectColor = eff_color
new_attrs.LayerIndex = a.LayerIndex
new_attrs.SetUserString(_FILL_OWNER_KEY, str(obj.Id))
new_attrs.SetUserString(_FILL_SOURCE_KEY,
"layer" if is_layer_source else "object")
_sync_plot_color_to_display(new_attrs)
hatch_id = doc.Objects.AddHatch(hatches[0], new_attrs)
if hatch_id != System.Guid.Empty:
ca = obj.Attributes.Duplicate()
ca.SetUserString(_FILL_KEY, str(hatch_id))
_processing.add(obj.Id)
try:
doc.Objects.ModifyAttributes(obj, ca, True)
finally:
_processing.discard(obj.Id)
_link_curve_hatch(obj.Id, hatch_id)
# Neue Hatch: Print-Mode-aware skalieren
try:
import massstab as _ms
h_obj = doc.Objects.FindId(hatch_id)
if h_obj is not None:
_ms.post_create_hatch_scale(doc, h_obj, scale_v)
except Exception as _ex:
print("[STYLES] post_create_hatch_scale (set_fill):", _ex)
else:
if existing_hatch is not None and not existing_hatch.IsDeleted:
_processing.add(existing_hatch.Id)
try:
doc.Objects.Delete(existing_hatch.Id, True)
finally:
_processing.discard(existing_hatch.Id)
ca = obj.Attributes.Duplicate()
ca.SetUserString(_FILL_KEY, "")
# Marker setzen: Auto-Fill ueberspringt diese Curve in Zukunft
ca.SetUserString(_NO_FILL_KEY, "1")
_processing.add(obj.Id)
try:
doc.Objects.ModifyAttributes(obj, ca, True)
finally:
_processing.discard(obj.Id)
doc.Views.Redraw()
self._send_selection()
# ---- SectionStyle (per-Object, Rhino 8) -------------------------------
def _set_section_style(self, enabled, source, color_hex,
pattern_name=None, scale=None, rotation_deg=None,
boundary_visible=True, boundary_width_scale=1.0,
boundary_color_hex=None,
background_color_hex=None):
"""Setzt einen Per-Object SectionStyle ueber die Rhino-8 API
(analog zu Layer.SetCustomSectionStyle). source='layer' entfernt
den Custom-Style → Layer-Default greift. source='object' setzt
einen frischen SectionStyle pro Objekt."""
doc = Rhino.RhinoDoc.ActiveDoc
objs = list(doc.Objects.GetSelectedObjects(False, False))
is_layer_source = (source == "layer")
print("[STYLES] _set_section_style: source={} enabled={} pattern={}".format(
source, enabled, pattern_name))
# SectionStyle-Klasse + Source-Enum holen.
# Mac Rhino 8: Enum heisst ObjectSectionAttributesSource (mit
# "Object"-Prefix) — per Inspektion verifiziert. Ohne explizites
# Setzen von Attributes.SectionAttributesSource = FromObject wird
# der Custom-SectionStyle zwar persistiert, aber visuell ignoriert
# weil der Default-Wert FromLayer bleibt.
try:
SS = Rhino.DocObjects.SectionStyle
except Exception as ex:
print("[STYLES] SectionStyle-Klasse fehlt:", ex)
return
SAS = None
for cls_name in ("ObjectSectionAttributesSource", "SectionAttributesSource"):
try:
SAS = getattr(Rhino.DocObjects, cls_name)
if SAS is not None:
print("[STYLES] Source-Enum: Rhino.DocObjects.{}".format(cls_name))
break
except Exception: pass
if SAS is None:
print("[STYLES] WARNUNG: kein Source-Enum gefunden")
if objs and not getattr(self, "_ss_api_logged", False):
o = objs[0]
for meth in ("SetCustomSectionStyle", "RemoveCustomSectionStyle",
"HasCustomSectionStyle", "GetCustomSectionStyle"):
print("[STYLES] RhinoObject.{}: {}".format(
meth, hasattr(o, meth)))
try:
a = o.Attributes
for meth in ("SetCustomSectionStyle", "RemoveCustomSectionStyle"):
print("[STYLES] Attributes.{}: {}".format(
meth, hasattr(a, meth)))
except Exception: pass
self._ss_api_logged = True
# Hatch-Pattern-Index ermitteln
pat_idx = -1
if pattern_name and pattern_name not in ("None", ""):
try: pat_idx = doc.HatchPatterns.Find(pattern_name, True)
except Exception: pat_idx = -1
col = _hex_to_color(color_hex) if color_hex else None
scale_v = float(scale) if scale is not None else 1.0
rot_rad = math.radians(float(rotation_deg)) if rotation_deg is not None else 0.0
def _try_set(target, names, value):
for n in names:
if hasattr(target, n):
try:
setattr(target, n, value)
return n
except Exception: pass
return None
def _apply_custom(obj, style):
"""Setzt Custom-SectionStyle + schaltet SectionAttributesSource
auf FromObject. Beides muss persistiert sein damit Rhino den
Custom-Style auch tatsaechlich rendert."""
try:
a = obj.Attributes.Duplicate()
if hasattr(a, "SetCustomSectionStyle"):
a.SetCustomSectionStyle(style)
# KRITISCH: Source auf FromObject — ohne das ignoriert Rhino
# den Custom-Style und nutzt weiter den Layer-Style.
if SAS is not None and hasattr(a, "SectionAttributesSource"):
try:
a.SectionAttributesSource = SAS.FromObject
except Exception as ex:
print("[STYLES] set Source.FromObject fail:", ex)
ok_modify = doc.Objects.ModifyAttributes(obj, a, True)
_log_post(obj, "Attributes.SetCustomSectionStyle+FromObject",
ok_modify)
return "Attributes.SetCustomSectionStyle"
except Exception as ex:
print("[STYLES] attr.SetCustomSectionStyle fail:", ex)
return None
def _log_post(obj, via, ok_modify=None):
"""Nach SetCustom: pruefen ob Rhino den Style auch behalten hat."""
try:
ob = doc.Objects.FindId(obj.Id) if hasattr(obj, "Id") else obj
if ob is None: ob = obj
a = ob.Attributes
src = "n/a"
if hasattr(a, "SectionAttributesSource"):
try: src = str(a.SectionAttributesSource)
except Exception: pass
got = None
if hasattr(a, "GetCustomSectionStyle"):
try:
css = a.GetCustomSectionStyle()
if css is not None:
got = "HatchIndex={}".format(getattr(css, "HatchIndex", "?"))
except Exception as ex:
got = "get-err: {}".format(ex)
print("[STYLES] post via {} (modify_ok={}): Source={} Got={}".format(
via, ok_modify, src, got))
except Exception as ex:
print("[STYLES] post-check:", ex)
def _remove_custom(obj):
"""Entfernt Custom-SectionStyle + schaltet Source auf FromLayer
zurueck. Damit greift wieder der Layer-Default-SectionStyle."""
try:
a = obj.Attributes.Duplicate()
if hasattr(a, "RemoveCustomSectionStyle"):
a.RemoveCustomSectionStyle()
if SAS is not None and hasattr(a, "SectionAttributesSource"):
try:
a.SectionAttributesSource = SAS.FromLayer
except Exception as ex:
print("[STYLES] set Source.FromLayer fail:", ex)
doc.Objects.ModifyAttributes(obj, a, True)
return "Attributes.RemoveCustomSectionStyle+FromLayer"
except Exception as ex:
print("[STYLES] attr.RemoveCustomSectionStyle fail:", ex)
return None
n_ok = 0
for obj in objs:
geom = obj.Geometry
if not isinstance(geom, (rg.Brep, rg.Extrusion, rg.Mesh, rg.SubD)):
continue
if is_layer_source:
# Custom entfernen → Layer-SectionStyle wird wirksam
via = _remove_custom(obj)
print("[STYLES] obj {}: remove custom via {}".format(
str(obj.Id)[:8], via))
if via: n_ok += 1
continue
# Default-Farbe = Layer-Farbe wenn der User keine Override-Farbe
# gewaehlt hat. Section-Style hat keine "ByLayer"-Source-Option,
# also setzen wir die echte Layer-Farbe explizit auf den Style.
obj_col = col
obj_col_src = "user-override" if col is not None else "n/a"
if obj_col is None:
try:
lyr = doc.Layers[obj.Attributes.LayerIndex]
obj_col = lyr.Color
obj_col_src = "layer({})".format(lyr.FullPath)
except Exception as ex:
obj_col = None
obj_col_src = "fail:{}".format(ex)
print("[STYLES] obj {} color src={} val={}".format(
str(obj.Id)[:8], obj_col_src, obj_col))
# Per-Object: frischen SectionStyle bauen wie in layer_builder
style = SS()
if pattern_name == "None" or not enabled:
_try_set(style, ("HatchIndex", "HatchPatternIndex"), -1)
else:
if pat_idx >= 0:
_try_set(style, ("HatchIndex", "HatchPatternIndex"), pat_idx)
_try_set(style, ("HatchScale", "HatchPatternScale"), scale_v)
_try_set(style, ("HatchRotationRadians", "HatchRotation",
"HatchAngle"), rot_rad)
if obj_col is not None:
# Display- UND Print-Color setzen damit beide matchen
_try_set(style, ("HatchPatternColor", "HatchColor",
"FillColor"), obj_col)
_try_set(style, ("HatchPatternPrintColor",), obj_col)
_try_set(style, ("BoundaryVisible",), bool(boundary_visible))
try:
_try_set(style, ("BoundaryWidthScale",),
float(boundary_width_scale))
except Exception: pass
# Boundary-Farbe: NUR setzen wenn User explizit eine Override-Farbe
# gewaehlt hat. Sonst lassen wir Rhinos Default (schwarz) greifen
# damit Boundary visuell unterscheidbar von der Hatch-Pattern-Farbe
# bleibt. (Sonst wuerde HatchPatternColor=Layer + BoundaryColor=Layer
# die Schnittflaeche als einfarbige Flaeche erscheinen lassen.)
bcol = None
if boundary_color_hex:
try: bcol = _hex_to_color(boundary_color_hex)
except Exception: bcol = None
if bcol is not None:
_try_set(style, ("BoundaryColor",), bcol)
_try_set(style, ("BoundaryPrintColor",), bcol)
# Background-Fill: User-Override (hex) → SolidColor-Mode + Farbe
# Sonst Transparent (Viewport-Mode, Default)
if background_color_hex:
try:
bgcol = _hex_to_color(background_color_hex)
except Exception:
bgcol = None
if bgcol is not None:
_try_set(style, ("BackgroundFillColor",), bgcol)
_try_set(style, ("BackgroundFillPrintColor",), bgcol)
# FillMode auf SolidColor via Enum (mehrere Namens-Varianten)
for en_cls in ("SectionBackgroundFillMode",
"BackgroundFillMode"):
try:
E = getattr(Rhino.DocObjects, en_cls, None)
if E is not None:
_try_set(style, ("BackgroundFillMode",),
E.SolidColor)
break
except Exception: pass
via = _apply_custom(obj, style)
print("[STYLES] obj {}: set custom via {} (hatch_idx={})".format(
str(obj.Id)[:8], via, pat_idx))
if via: n_ok += 1
print("[STYLES] SectionStyle auf {} Objekt(e) appliziert".format(n_ok))
doc.Views.Redraw()
self._send_selection()
# --- Selection-Events ----------------------------------------------------
def _install_selection_listener(bridge):
flag = "gestaltung_selection_listener"
sc.sticky["gestaltung_bridge"] = bridge
if sc.sticky.get(flag):
return
# Selection-Refresh wird via Idle-Event debounced:
# Rhino feuert pro Object-Select/Deselect einzeln. Bei mass-Delete von
# 327 Objekten = 327 refresh-Calls → 327 IPC-Sends in den WebView →
# UI haengt + Command-History wird mit '[STYLES] sel: n=N'
# zugemuellt. Wir setzen nur ein Dirty-Flag und feuern EINMAL beim
# naechsten Idle-Tick.
def refresh(*args):
if sc.sticky.get("dossier_swisstopo_busy"): return
if sc.sticky.get("_dossier_user_transform_active"): return
if sc.sticky.get("_dossier_undo_active"): return
sc.sticky["_gestaltung_selection_dirty"] = True
def on_idle_flush(sender, args):
if not sc.sticky.get("_gestaltung_selection_dirty"): return
if sc.sticky.get("dossier_swisstopo_busy"): return
if sc.sticky.get("_dossier_user_transform_active"): return
if sc.sticky.get("_dossier_undo_active"): return
if sc.sticky.get("_dossier_bulk_op_active"): return
sc.sticky["_gestaltung_selection_dirty"] = False
b = sc.sticky.get("gestaltung_bridge")
if b is not None:
try: b._send_selection()
except Exception: pass
# Idle-Hook nur einmal aufhaengen (sticky guard)
if not sc.sticky.get("_gestaltung_idle_attached"):
try:
Rhino.RhinoApp.Idle += on_idle_flush
sc.sticky["_gestaltung_idle_attached"] = True
except Exception as ex:
print("[STYLES] Idle-Hook fail:", ex)
def on_replace(sender, args):
"""Sync Curve↔Hatch bei Move/Replace:
- Curve hat _FILL_KEY (= hatch_id) → Hatch via Hatch.Create neu auf die
aktuelle Curve aufsetzen (existierender Pfad).
- Hatch hat _FILL_OWNER_KEY (= curve_id) → Curve um den gleichen
Vektor mit-translaten (User hat Hatch alleine verschoben).
"""
if sc.sticky.get("_dossier_undo_active"): return
if sc.sticky.get("_elemente_regen_busy"): return
new_obj = args.NewRhinoObject
if new_obj is None or new_obj.Id in _processing:
return
a = new_obj.Attributes
# Frueher Bail-out wenn das Objekt KEIN Hatch-Coupling hat — irrelevant.
# Erst NACH dieser Pruefung das user-transform-Skip, sonst wuerde die
# Hatch-Sync beim Verschieben einer normalen Polylinie nicht laufen
# (war der Fall nach dem Split: Hatch ging bei _Move nicht mehr mit).
hatch_id_str_quick = a.GetUserString(_FILL_KEY)
owner_id_str_quick = a.GetUserString(_FILL_OWNER_KEY)
if not hatch_id_str_quick and not owner_id_str_quick:
return
# Dossier-eigene Sources (wand_axis etc.) haben weder _FILL_KEY noch
# _FILL_OWNER_KEY — die werden hier oben rausgekickt. User-Transform-
# Skip war eigentlich nur fuer Dossier-Elemente gedacht; reine Hatch-
# gekoppelte Polylinien brauchen die Sync auch waehrend _Move.
# Reverse-Direction: Hatch verschoben/rotiert/skaliert → Curve mitnehmen.
# Wir nehmen die Outer-Boundary direkt aus der (bereits transformed)
# Hatch — funktioniert fuer Move, Rotate, Scale, beliebige Transforms.
if isinstance(new_obj.Geometry, rg.Hatch):
owner_id_str = a.GetUserString(_FILL_OWNER_KEY)
if not owner_id_str:
return
try:
owner_id = System.Guid(owner_id_str)
except Exception:
return
doc2 = Rhino.RhinoDoc.ActiveDoc
owner_obj = doc2.Objects.FindId(owner_id)
if owner_obj is None or owner_obj.IsDeleted:
return
try:
new_curves = new_obj.Geometry.Get3dCurves(True)
except Exception as ex:
print("[STYLES] hatch.Get3dCurves:", ex)
return
if not new_curves or len(new_curves) == 0:
return
new_curve = new_curves[0]
_processing.add(owner_id)
try:
doc2.Objects.Replace(owner_id, new_curve)
except Exception as ex:
print("[STYLES] hatch→curve replace:", ex)
finally:
_processing.discard(owner_id)
return
hatch_id_str = a.GetUserString(_FILL_KEY)
if not hatch_id_str:
return
print("[STYLES] on_replace fuer Curve mit Fill")
try:
hatch_id = System.Guid(hatch_id_str)
except Exception:
return
doc = Rhino.RhinoDoc.ActiveDoc
hatch_obj = doc.Objects.FindId(hatch_id)
if hatch_obj is None or hatch_obj.IsDeleted:
return
geom = new_obj.Geometry
if not _is_closed_planar_curve(geom):
return
try:
hg = hatch_obj.Geometry
pattern_idx = hg.PatternIndex
cur_scale = hg.PatternScale
cur_rot = hg.PatternRotation
except Exception:
pattern_idx = doc.HatchPatterns.CurrentHatchPatternIndex
cur_scale = 1.0
cur_rot = 0.0
try:
new_hatches = rg.Hatch.Create(geom, pattern_idx, cur_rot, cur_scale, 0.0)
except Exception:
return
if not new_hatches or len(new_hatches) == 0:
return
_processing.add(hatch_id)
try:
doc.Objects.Replace(hatch_id, new_hatches[0])
except Exception as ex:
print("[STYLES] Hatch-Update:", ex)
finally:
_processing.discard(hatch_id)
def on_delete(sender, args):
"""Wenn eine Curve geloescht wird, ihre gekoppelte Hatch mitloeschen.
Wenn umgekehrt eine Hatch direkt geloescht wird, den Verweis auf der
Curve aufraeumen damit beim naechsten Toggle keine Geister-Referenz steht."""
if sc.sticky.get("_dossier_undo_active"): return
if sc.sticky.get("_elemente_regen_busy"): return
# Bulk-Op (SelAll + Delete): KEIN frueher Bail — der UserString-
# Schnellcheck unten (kein _FILL_KEY/_FILL_OWNER_KEY → return)
# filtert OSM/Swisstopo-Curves billig raus. Hatch-gekoppelte Curves
# MUESSEN ihre Hatch mitnehmen, sonst bleiben Geister-Hatches
# liegen (Curve+Hatch zerfallen als Einheit).
obj = args.TheObject
if obj is None or obj.Id in _processing:
return
doc = Rhino.RhinoDoc.ActiveDoc
try:
attrs = obj.Attributes
except Exception:
return
# Schneller Bail-out: ohne Hatch-UserString interessiert uns das
# Event nicht. Vermeidet Print-Spam fuer Wand-Sub-Volumen etc. UND
# filtert den user-transform-Pfad: nur Hatch-gekoppelte Objekte
# brauchen die Sync (= Cascade-Delete + pending-Save fuer Recovery).
# Wand-Volumen werden nicht beruehrt.
try:
hatch_id_str = attrs.GetUserString(_FILL_KEY)
except Exception:
hatch_id_str = None
try:
owner_id_str = attrs.GetUserString(_FILL_OWNER_KEY)
except Exception:
owner_id_str = None
if not hatch_id_str and not owner_id_str:
# UserStrings koennen nach Delete leer sein → Sticky-Fallback.
hatch_id_str = _lookup_hatch_for_curve(obj.Id)
if not hatch_id_str:
return
print("[STYLES] on_delete: hatch via sticky map gefunden")
# Pfad A: geloeschte Curve hatte eine Hatch -> Hatch mitloeschen
if hatch_id_str:
try:
hatch_id = System.Guid(hatch_id_str)
except Exception:
hatch_id = None
if hatch_id is not None:
hatch_obj = doc.Objects.FindId(hatch_id)
if hatch_obj is not None and not hatch_obj.IsDeleted:
# Metadaten merken fuer eventuelles Drag-Recovery (Rhino feuert
# bei Drag/Move oft on_delete+on_add statt on_replace)
_save_pending_hatch(obj.Id, hatch_obj)
_processing.add(hatch_id)
try:
ok = doc.Objects.Delete(hatch_id, True)
print("[STYLES] Curve geloescht -> Hatch {} ({})".format(
"weg" if ok else "konnte nicht geloescht werden", hatch_id))
except Exception as ex:
print("[STYLES] Hatch-Loeschen:", ex)
finally:
_processing.discard(hatch_id)
_unlink_curve(obj.Id)
return # Curve-Fall fertig
# Pfad B: geloeschte Hatch hatte einen Owner-Verweis -> Curve aufraeumen
if owner_id_str:
try:
owner_id = System.Guid(owner_id_str)
except Exception:
owner_id = None
if owner_id is not None:
owner_obj = doc.Objects.FindId(owner_id)
if owner_obj is not None and not owner_obj.IsDeleted:
try:
ca = owner_obj.Attributes.Duplicate()
ca.SetUserString(_FILL_KEY, "")
_processing.add(owner_id)
try:
doc.Objects.ModifyAttributes(owner_obj, ca, True)
finally:
_processing.discard(owner_id)
except Exception as ex:
print("[STYLES] Curve-Verweis aufraeumen:", ex)
def on_add(sender, args):
"""Auto-Fill bzw. Drag-Recovery: neues Objekt -> ggf. Hatch erzeugen.
- Wenn das Objekt eben gerade als Teil eines Drag/Move geloescht wurde,
stellen wir die Hatch mit den gemerkten Metadaten wieder her.
- Sonst pruefen wir ob die Ebene ein Auto-Fill konfiguriert hat."""
# Swisstopo-Import importiert tausende Objekte am Stueck — bail.
if sc.sticky.get("dossier_swisstopo_busy"): return
if sc.sticky.get("_dossier_undo_active"): return
if sc.sticky.get("_elemente_regen_busy"): return
obj = args.TheObject
if obj is None:
return
if obj.Id in _processing:
return
doc = Rhino.RhinoDoc.ActiveDoc
# 1) Drag-Recovery: Hatch-Metadaten wurden gerade in on_delete gespeichert?
# MUSS auch waehrend User-Transform laufen (= das ist gerade DER Fall:
# Rhino's Move feuert Delete+Add, on_delete hat pending gespeichert,
# jetzt muss on_add die Hatch wiederherstellen).
pending = _take_pending_hatch(obj.Id)
if pending is not None:
try:
ok = _restore_hatch_from_pending(doc, obj, pending)
except Exception as ex:
print("[STYLES] on_add restore Exception:", ex)
ok = False
if ok:
print("[STYLES] Drag-Recovery: Hatch wiederhergestellt fuer {}".format(obj.Id))
b = sc.sticky.get("gestaltung_bridge")
if b is not None:
try: b._send_selection()
except Exception: pass
return
# 2) Auto-Fill aus Ebenen-Definition — nur ausserhalb User-Transform.
# Waehrend Move/Rotate werden Sub-Volumen erzeugt die kein Auto-Fill
# brauchen, und elemente uebernimmt die Coupling.
if sc.sticky.get("_dossier_user_transform_active"): return
# 2b) Default-Pen ("Stift fuer neue Objekte") auf frisch gezeichnete
# 2D-Objekte stempeln (Kurven, Text, Hatch, Bemassung ...), solange
# aktiv. Ausgeschlossen: 3D-Volumen (Pen ist ein 2D-Begriff) und
# DOSSIER-Element-Geometrie (dossier_element_type), die elemente.py setzt.
pen = _default_pen()
g = obj.Geometry
is_3d = isinstance(g, (rg.Brep, rg.Extrusion, rg.Mesh, rg.SubD))
if _default_pen_active(pen) and g is not None and not is_3d:
try: dossier_type = obj.Attributes.GetUserString("dossier_element_type") or ""
except Exception: dossier_type = ""
if not dossier_type:
try:
a = obj.Attributes.Duplicate()
_apply_pen_to_attrs(doc, a, pen)
_processing.add(obj.Id)
try:
doc.Objects.ModifyAttributes(obj, a, True)
finally:
_processing.discard(obj.Id)
doc.Views.Redraw()
except Exception as ex:
print("[STYLES] on_add default-pen:", ex)
try:
ok = _apply_ebene_fill(doc, obj)
except Exception as ex:
print("[STYLES] on_add Exception:", ex)
return
if ok:
b = sc.sticky.get("gestaltung_bridge")
if b is not None:
try: b._send_selection()
except Exception: pass
def on_modify_attrs(sender, args):
"""Reagiert auf Attribut-Aenderungen an Objekten:
1) Curve auf neue Ebene -> gekoppelte Hatch zieht mit
2) ColorSource -> FromObject -> PlotColorSource/PlotColor mitsynchen
(sonst druckt das Objekt trotz eigener Display-Farbe in Layerfarbe)."""
try:
obj = args.RhinoObject
old_attr = args.OldAttributes
new_attr = args.NewAttributes
old_lyr = old_attr.LayerIndex
new_lyr = new_attr.LayerIndex
except Exception:
return
if obj is None or obj.Id in _processing:
return
# --- (2) Plot-Color Auto-Sync ---
try:
new_cs = int(new_attr.ColorSource)
if new_cs == int(_FROM_OBJECT):
new_pcs = int(new_attr.PlotColorSource)
need_pcs = (new_pcs != int(_PLOT_FROM_OBJECT))
need_pcol = False
try:
need_pcol = (int(new_attr.PlotColor.ToArgb()) != int(new_attr.ObjectColor.ToArgb()))
except Exception:
need_pcol = True
if need_pcs or need_pcol:
doc = Rhino.RhinoDoc.ActiveDoc
ha = new_attr.Duplicate()
_sync_plot_color_to_display(ha)
_processing.add(obj.Id)
try:
doc.Objects.ModifyAttributes(obj, ha, True)
finally:
_processing.discard(obj.Id)
except Exception as ex:
print("[STYLES] on_modify_attrs plot-sync:", ex)
# --- (1) Layer-Wechsel -> Hatch mitziehen ---
if old_lyr == new_lyr:
return
try:
hatch_id_str = new_attr.GetUserString(_FILL_KEY)
except Exception:
hatch_id_str = None
if not hatch_id_str:
return # nur Curves mit gekoppelter Hatch interessieren uns
try:
hatch_id = System.Guid(hatch_id_str)
except Exception:
return
doc = Rhino.RhinoDoc.ActiveDoc
hatch_obj = doc.Objects.FindId(hatch_id)
if hatch_obj is None or hatch_obj.IsDeleted:
return
try:
ha = hatch_obj.Attributes.Duplicate()
if ha.LayerIndex == new_lyr:
return
ha.LayerIndex = new_lyr
_processing.add(hatch_id)
try:
doc.Objects.ModifyAttributes(hatch_obj, ha, True)
finally:
_processing.discard(hatch_id)
print("[STYLES] Curve {} Layer geaendert -> Hatch mitgezogen".format(obj.Id))
except Exception as ex:
print("[STYLES] on_modify_attrs:", ex)
return
# Falls die neue Ebene andere Fill-Settings hat (Pattern/Skala/Drehung),
# die Hatch entsprechend an die neue Layer-Definition angleichen.
try:
refresh_layer_fills(doc)
except Exception as ex:
print("[STYLES] on_modify_attrs refresh:", ex)
Rhino.RhinoDoc.SelectObjects += refresh
Rhino.RhinoDoc.DeselectObjects += refresh
Rhino.RhinoDoc.DeselectAllObjects += refresh
Rhino.RhinoDoc.ReplaceRhinoObject += on_replace
Rhino.RhinoDoc.DeleteRhinoObject += on_delete
Rhino.RhinoDoc.AddRhinoObject += on_add
Rhino.RhinoDoc.ModifyObjectAttributes += on_modify_attrs
sc.sticky[flag] = True
print("[STYLES] Listener active (Selection + Hatch-Live-Update + Ebene-Auto-Fill + Layer-Sync)")
def _bridge_factory():
b = GestaltungBridge()
_install_selection_listener(b)
return b
panel_base.register_and_open("gestaltung", "Gestaltung", PANEL_GUID_STR, _bridge_factory,
icon_spec=("palette", "#5fa896"))