95031ee2c0
- Ebenenkombination raus aus Ebenen-Panel, in Oberleiste-Topbar + Editor-Satellite (AusschnittLayerDialog embedded). doc.Strings haelt active_comb_name, auto-clear bei manueller Eye/Lock-Aenderung. - EbenenSettingsDialog jetzt Satellite mit Ebene-Picker-Dropdown (auto-save on switch via SAVE_KEEP). - Per-Ausschnitt Einstellungen-Satellite (Massstab, Display, Overrides, Ebenenkombi). Alte 'Sichtbarkeit bearbeiten'-Option entfernt. - Layouts/Ausschnitte: Top-Header weg, Sticky-Footer mit Anzahl + Aktionen. LayoutDialog ist jetzt Satellite mit Format-Live-Preview. - Panel-Captions + Default-Ebenen-Namen auf Mixed-Case (Ausschnitte, Ebenen, Waende ...). Nur DOSSIER bleibt caps. - DimensionenApp: Card-Optik raus, REF-Wuerfel mit Kreisen statt Quadraten + Hover-Scale. - GeschossManager angeglichen an EbenenManager: Rechtsklick-Menue, Lock-Button, Delete-X, Duplizieren. layer_builder honoriert z.locked. - Active Sublayer folgt jetzt dem Geschoss-Wechsel (gleicher Code unter neuem Parent). Performance Geschoss-Wechsel: - elemente._send_state() ersetzt durch _notify_active_geschoss() (Partial-Push statt 200+ Elements re-enumerieren). - _apply_visibility dedupe via sticky last-applied-signature (STATE_SYNC-Echo loopt nicht mehr durch alle Layer). - _update_clipping nur wenn alt oder neu hasClipping=True. - Redundante doc.Views.Redraw() im CPlane-Pfad entfernt — die folgende apply_visibility-Roundtrip redrawt 30ms spaeter ohnehin. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1700 lines
69 KiB
Python
1700 lines
69 KiB
Python
#! python 3
|
|
# -*- 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):
|
|
# Waehrend Move/Rotate/Mirror/Scale schweigen — Rhino oszilliert die
|
|
# Selection pro transformiertem Object mehrfach (deselect→delete→add→
|
|
# reselect). Bei 7 Objekten sind das ~100 IPC-Sends in den WebView,
|
|
# was sich als „Regen" anfuehlt. elemente._on_command_end refresht
|
|
# nach dem Command einmalig.
|
|
if sc.sticky.get("_dossier_user_transform_active"): return
|
|
if sc.sticky.get("_dossier_undo_active"): return
|
|
b = sc.sticky.get("gestaltung_bridge")
|
|
if b is not None:
|
|
try: b._send_selection()
|
|
except Exception: pass
|
|
|
|
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("[GESTALTUNG] 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("[GESTALTUNG] hatch→curve replace:", ex)
|
|
finally:
|
|
_processing.discard(owner_id)
|
|
return
|
|
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."""
|
|
if sc.sticky.get("_dossier_undo_active"): return
|
|
if sc.sticky.get("_elemente_regen_busy"): return
|
|
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("[GESTALTUNG] 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("[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
|
|
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."""
|
|
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("[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 — 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
|
|
try:
|
|
ok = _apply_ebene_fill(doc, obj)
|
|
except Exception as ex:
|
|
print("[GESTALTUNG] 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("[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=("palette", "#5fa896"))
|