#! 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): 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). """ new_obj = args.NewRhinoObject if new_obj is None or new_obj.Id in _processing: return a = new_obj.Attributes # 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.""" 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=("palette", "#5fa896"))