Files
DOSSIER/rhino/gestaltung.py
T
karim 9dc191be4f Initial commit — Dossier Rhino 8 Plugin
OpenStudio-Suite Architektur-Plugin fuer Rhino 8 (Mac):
- Smart-Elemente: Wand, Decke, Dach (Pult/Sattel/Walm/Mansarde),
  Oeffnungen (Fenster/Tueren mit Rahmen + Sims + Glas + Fluegel),
  Treppen (gerade · L · Wendel mit Schrittmass-Validierung)
- Live-Previews mit Step-Lines + Soll-Range-Clamping
- Bidirektionale Selection-Sync zwischen Source-Linie und Volume
- Geschoss-/Ebenen-Verwaltung mit OKFF-Persistenz
- Layouts mit PDF-Export
- Ausschnitte / Massstab / Override-Regeln
- Petrol-Gruen Theme (Rapport-konform)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 04:27:41 +02:00

1636 lines
65 KiB
Python

# ! python3
# -*- coding: utf-8 -*-
"""
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("[GESTALTUNG] 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 gesetzt
_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("[GESTALTUNG] 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("[GESTALTUNG] 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("[GESTALTUNG] 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("[GESTALTUNG] 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))
def _force_load_linetypes(doc):
"""Rhinos Linetype-Tabelle wird lazy initialisiert — wir triggern es."""
# 1) Eingebaute Methode (falls vorhanden)
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("[GESTALTUNG] _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("[GESTALTUNG] _ebene_fill_for_layer: dossier_ebenen leer in doc.Strings")
return None
try:
ebenen = json.loads(raw)
except Exception as ex:
print("[GESTALTUNG] _ebene_fill_for_layer: json-Fehler:", ex)
return None
if not isinstance(ebenen, list): return None
for e in ebenen:
if not isinstance(e, dict): continue
if e.get("code") != code: continue
f = e.get("fill")
if not isinstance(f, dict):
print("[GESTALTUNG] _ebene_fill_for_layer: Ebene code={} hat KEIN fill-Feld".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("[GESTALTUNG] _ebene_fill_for_layer code={} -> {}".format(code, result))
return result
print("[GESTALTUNG] _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("[GESTALTUNG] 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("[GESTALTUNG] 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("[GESTALTUNG] 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("[GESTALTUNG] 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
fill_by_code = {}
for e in ebenen:
if not isinstance(e, dict): continue
f = e.get("fill")
if isinstance(f, dict) and f.get("pattern") not in (None, "None"):
fill_by_code[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,
}
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("[GESTALTUNG] 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("[GESTALTUNG] post_create_hatch_scale (refresh):", _ex)
except Exception as ex:
print("[GESTALTUNG] 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("[GESTALTUNG] 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("[GESTALTUNG] 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("[GESTALTUNG] refresh auto-fill:", ex)
except Exception as ex:
print("[GESTALTUNG] refresh auto-fill scan:", ex)
if updated or color_updated or added:
doc.Views.Redraw()
print("[GESTALTUNG] refresh_layer_fills: pattern={}, farbe={}, neu={}, unveraendert={}".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("[GESTALTUNG] repair_plot_colors entry:", ex)
except Exception as ex:
print("[GESTALTUNG] repair_plot_colors scan:", ex)
return 0
if fixed:
doc.Views.Redraw()
print("[GESTALTUNG] 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:
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
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 gesetzt haben.
nm = _safe_layer_label(doc, layer, a.LayerIndex)
layer_names.add(nm)
# 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,
"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("[GESTALTUNG] 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("[GESTALTUNG] Linetypes vor: {}, 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("[GESTALTUNG] {}".format(" | ".join(entries)))
except Exception as ex:
print("[GESTALTUNG] 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("[GESTALTUNG] 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"),
)
def _send_selection(self):
doc = Rhino.RhinoDoc.ActiveDoc
try:
self.send("SELECTION", _selection_summary(doc))
except Exception as ex:
print("[GESTALTUNG] 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):
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).
try:
import massstab
except Exception:
massstab = None
doc = Rhino.RhinoDoc.ActiveDoc
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("[GESTALTUNG] 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("[GESTALTUNG] obj {} fehler: {}".format(prop, ex))
if applied:
ok += 1
doc.Views.Redraw()
if ok == 0:
print("[GESTALTUNG] Linetype-Scale nicht unterstuetzt (Rhino-Version?)")
else:
print("[GESTALTUNG] Linetype-Scale auf {} Objekt(e) angewendet".format(ok))
self._send_selection()
def _set_linetype_source(self, source, name):
doc = Rhino.RhinoDoc.ActiveDoc
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("[GESTALTUNG] _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("[GESTALTUNG] 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("[GESTALTUNG] 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()
# --- Selection-Events ----------------------------------------------------
def _install_selection_listener(bridge):
flag = "gestaltung_selection_listener"
sc.sticky["gestaltung_bridge"] = bridge
if sc.sticky.get(flag):
return
def refresh(*args):
b = sc.sticky.get("gestaltung_bridge")
if b is not None:
try: b._send_selection()
except Exception: pass
def on_replace(sender, args):
"""Hatch der zugehoerigen Curve mitziehen wenn Curve veraendert wird."""
new_obj = args.NewRhinoObject
if new_obj is None or new_obj.Id in _processing:
return
a = new_obj.Attributes
hatch_id_str = a.GetUserString(_FILL_KEY)
if not hatch_id_str:
return
print("[GESTALTUNG] 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("[GESTALTUNG] 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."""
obj = args.TheObject
try:
print("[GESTALTUNG] on_delete fired id={}".format(obj.Id if obj else None))
except Exception:
pass
if obj is None or obj.Id in _processing:
return
doc = Rhino.RhinoDoc.ActiveDoc
try:
attrs = obj.Attributes
except Exception:
return
# Pfad A: geloeschte Curve hatte eine Hatch -> Hatch mitloeschen
try:
hatch_id_str = attrs.GetUserString(_FILL_KEY)
except Exception:
hatch_id_str = None
# Fallback: Mapping in sc.sticky (UserStrings koennen nach Delete leer sein)
if not hatch_id_str:
hatch_id_str = _lookup_hatch_for_curve(obj.Id)
if hatch_id_str:
print("[GESTALTUNG] on_delete: hatch via sticky map gefunden")
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("[GESTALTUNG] Curve geloescht -> Hatch {} ({})".format(
"weg" if ok else "konnte nicht geloescht werden", hatch_id))
except Exception as ex:
print("[GESTALTUNG] 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
try:
owner_id_str = attrs.GetUserString(_FILL_OWNER_KEY)
except Exception:
owner_id_str = None
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("[GESTALTUNG] 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."""
obj = args.TheObject
if obj is None:
return
try:
geom_kind = type(obj.Geometry).__name__
except Exception:
geom_kind = "?"
if obj.Id in _processing:
return
print("[GESTALTUNG] on_add: id={} type={}".format(obj.Id, geom_kind))
doc = Rhino.RhinoDoc.ActiveDoc
# 1) Drag-Recovery: Hatch-Metadaten wurden gerade in on_delete gespeichert?
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("[GESTALTUNG] on_add restore Exception:", ex)
ok = False
if ok:
print("[GESTALTUNG] 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
try:
ok = _apply_ebene_fill(doc, obj)
except Exception as ex:
print("[GESTALTUNG] on_add Exception:", ex)
return
print("[GESTALTUNG] on_add ok={}".format(ok))
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("[GESTALTUNG] 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("[GESTALTUNG] Curve {} Layer geaendert -> Hatch mitgezogen".format(obj.Id))
except Exception as ex:
print("[GESTALTUNG] 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("[GESTALTUNG] 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("[GESTALTUNG] Listener aktiv (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=("G", "#5fa896"))