Files
DOSSIER/rhino/layer_builder.py
T
karim 95031ee2c0 Panels poliert: Ebenenkombi in Oberleiste, Satelliten-Dialoge, Caps weg, Perf
- 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>
2026-05-19 03:58:28 +02:00

671 lines
24 KiB
Python

#! python 3
# -*- coding: utf-8 -*-
"""
layer_builder.py
Layer-Struktur:
<Zeichnungsebene-Name>
+-- 00_RASTER
+-- 01_VERMESSUNG
+-- 20_WAENDE
+-- ...
Jede Zeichnungsebene erhaelt alle definierten Ebenen als Sublayer.
"""
import System
import System.Drawing as Drawing
import Rhino
GREY = Drawing.Color.FromArgb(150, 150, 150)
_FROM_LAYER = Rhino.DocObjects.ObjectColorSource.ColorFromLayer
_FROM_OBJECT = Rhino.DocObjects.ObjectColorSource.ColorFromObject
_EMPTY_GUID = System.Guid.Empty
def _color(hex_str):
h = (hex_str or "#888888").lstrip("#")
if len(h) == 3:
h = "".join(c * 2 for c in h)
return Drawing.Color.FromArgb(int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16))
def _is_top_level(layer):
return layer.ParentLayerId == _EMPTY_GUID
def _find_top_by_id(doc, dossier_id):
for i, layer in enumerate(doc.Layers):
if not _is_top_level(layer):
continue
v = layer.GetUserString("dossier_id")
if v == dossier_id:
return i
return -1
def _find_top_by_name(doc, name):
for i, layer in enumerate(doc.Layers):
if _is_top_level(layer) and layer.Name == name:
return i
return -1
def _find_sublayer_by_code(doc, parent_id, code):
prefix = code + "_"
for i, layer in enumerate(doc.Layers):
if layer.ParentLayerId == parent_id and layer.Name.startswith(prefix):
return i
return -1
def _add_layer(doc, name, parent_id=None, color=None, lw=None):
layer = Rhino.DocObjects.Layer()
layer.Name = name
if parent_id is not None and parent_id != _EMPTY_GUID:
layer.ParentLayerId = parent_id
if color is not None:
layer.Color = color
if lw is not None:
layer.PlotWeight = lw
return doc.Layers.Add(layer)
def _find_hatch_pattern_index(doc, name):
"""Sucht einen Hatch-Pattern-Index per Name (case-insensitive). -1 wenn nicht da."""
if not name or name == "None":
return -1
target = name.strip().lower()
try:
for i in range(doc.HatchPatterns.Count):
hp = doc.HatchPatterns[i]
if hp is None or hp.IsDeleted: continue
if hp.Name and hp.Name.strip().lower() == target:
return i
except Exception as ex:
print("[EBENEN] hatch lookup:", ex)
return -1
def _find_linetype_index(doc, name):
"""Sucht einen Linetype-Index per Name. -1 = ByLayer."""
if not name or name in ("byLayer", "by_layer", "ByLayer"):
return -1
target = name.strip().lower()
try:
for i in range(doc.Linetypes.Count):
lt = doc.Linetypes[i]
if lt is None or lt.IsDeleted: continue
if lt.Name and lt.Name.strip().lower() == target:
return i
except Exception: pass
return -1
def _apply_section_style(doc, layer, section_cfg, layer_color):
"""Setzt einen Custom-SectionStyle auf den Layer aus dem Dossier-section-dict.
Nutzt Rhino-8's Python-3-API (Rhino.DocObjects.SectionStyle +
Layer.SetCustomSectionStyle / RemoveCustomSectionStyle). In IPy 2.7
sind diese Methoden nicht exponiert — dort no-op (mit Print-Warnung).
"""
if not section_cfg or not isinstance(section_cfg, dict):
return
has_setter = hasattr(layer, "SetCustomSectionStyle")
has_remover = hasattr(layer, "RemoveCustomSectionStyle")
if not has_setter:
# IPy-2.7-Pfad: API nicht da, leise raus.
return
try:
SS = Rhino.DocObjects.SectionStyle
except Exception as ex:
print("[EBENEN] SectionStyle-Klasse nicht da:", ex); return
# Wenn alles "leer/Default": Custom-Style abschalten
pat = (section_cfg.get("hatchPattern") or "None").strip()
show = section_cfg.get("boundaryShow", True)
if pat == "None" and not show and has_remover:
try: layer.RemoveCustomSectionStyle()
except Exception: pass
return
style = SS()
# --- Hatch ---
if pat and pat != "None":
hp_idx = _find_hatch_pattern_index(doc, pat)
if hp_idx >= 0:
# Property-Name probieren — Rhino-8 hat HatchIndex
for prop in ("HatchIndex", "HatchPatternIndex"):
if hasattr(style, prop):
try: setattr(style, prop, hp_idx); break
except Exception: pass
# Hatch-Scale
for prop in ("HatchScale", "HatchPatternScale"):
if hasattr(style, prop):
try: setattr(style, prop, float(section_cfg.get("hatchScale") or 1.0)); break
except Exception: pass
# Hatch-Rotation (Rhino erwartet Radians — wir bekommen Grad)
import math
rot_deg = float(section_cfg.get("hatchRotation") or 0)
for prop in ("HatchRotation", "HatchAngle"):
if hasattr(style, prop):
try:
setattr(style, prop, math.radians(rot_deg))
break
except Exception: pass
# Hatch-Color (null = ByObject = nicht setzen)
hatch_color = section_cfg.get("hatchColor")
if hatch_color:
for prop in ("HatchColor", "FillColor"):
if hasattr(style, prop):
try: setattr(style, prop, _color(hatch_color)); break
except Exception: pass
# Background
bg = section_cfg.get("background")
if bg in ("object", "byObject"):
for prop in ("BackgroundColorUsage", "FillBackground"):
if hasattr(style, prop):
# Enum-Werte sind versioniert; wir versuchen via int
try: setattr(style, prop, 1); break
except Exception: pass
# --- Boundary ---
if hasattr(style, "BoundaryVisible"):
try: style.BoundaryVisible = bool(show)
except Exception: pass
elif hasattr(style, "ShowBoundary"):
try: style.ShowBoundary = bool(show)
except Exception: pass
if show:
# Boundary color
bc = section_cfg.get("boundaryColor")
if bc:
for prop in ("BoundaryColor", "OutlineColor", "EdgeColor"):
if hasattr(style, prop):
try: setattr(style, prop, _color(bc)); break
except Exception: pass
# Boundary width scale
ws = float(section_cfg.get("boundaryWidthScale") or 1.0)
for prop in ("BoundaryWidthScale", "EdgeWidthScale", "OutlineWidthScale"):
if hasattr(style, prop):
try: setattr(style, prop, ws); break
except Exception: pass
# Linetype
lt = section_cfg.get("boundaryLinetype")
if lt and lt not in ("byLayer", "ByLayer"):
lt_idx = _find_linetype_index(doc, lt)
for prop in ("BoundaryLinetypeIndex", "EdgeLinetypeIndex"):
if hasattr(style, prop):
try: setattr(style, prop, lt_idx); break
except Exception: pass
# Section open objects
soo = bool(section_cfg.get("sectionOpenObjects", True))
for prop in ("SectionOpenObjects", "ClipOpenObjects"):
if hasattr(style, prop):
try: setattr(style, prop, soo); break
except Exception: pass
# Style auf Layer setzen
try:
layer.SetCustomSectionStyle(style)
except Exception as ex:
print("[EBENEN] SetCustomSectionStyle({}): {}".format(layer.Name, ex))
def build_layers(doc, zeichnungsebenen, ebenen):
"""
Stellt sicher dass fuer jede Zeichnungsebene ein Parent-Layer existiert
und unter jedem alle Ebenen als Sublayer angelegt/aktualisiert sind.
"""
for z in zeichnungsebenen:
z_id = z["id"]
z_name = z["name"]
# Parent finden oder anlegen
idx = _find_top_by_id(doc, z_id)
if idx < 0:
idx = _find_top_by_name(doc, z_name)
if idx < 0:
idx = _add_layer(doc, z_name)
doc.Layers[idx].SetUserString("dossier_id", z_id)
else:
parent = doc.Layers[idx]
if parent.Name != z_name:
parent.Name = z_name
parent.SetUserString("dossier_id", z_id)
parent_id = doc.Layers[idx].Id
# Sublayer pro Ebene
for e in ebenen:
sub_name = "{}_{}".format(e["code"], e["name"])
col = _color(e.get("color"))
lw = float(e.get("lw", 0.13))
sub_idx = _find_sublayer_by_code(doc, parent_id, e["code"])
if sub_idx < 0:
sub_idx = _add_layer(doc, sub_name, parent_id, col, lw)
doc.Layers[sub_idx].SetUserString("dossier_code", e["code"])
else:
sub = doc.Layers[sub_idx]
if sub.Name != sub_name:
sub.Name = sub_name
sub.Color = col
try:
import massstab as _ms
_ms.write_plotweight(doc, sub, float(lw))
except Exception:
sub.PlotWeight = lw
sub.SetUserString("dossier_code", e["code"])
# Section Style anwenden (Py3-only — IPy 2.7 no-op)
try:
_apply_section_style(doc, doc.Layers[sub_idx],
e.get("section"), e.get("color"))
except Exception as ex:
print("[EBENEN] section-style apply ({}): {}".format(sub_name, ex))
doc.Views.Redraw()
print("[EBENEN] {} Zeichnungsebenen x {} Ebenen aktualisiert".format(
len(zeichnungsebenen), len(ebenen)))
def update_layer_style(doc, code, color_hex=None, lw=None):
"""Aendert Farbe und/oder Stiftdicke fuer alle Sublayer mit dem gegebenen Code."""
col = _color(color_hex) if color_hex else None
try:
import massstab as _ms
except Exception:
_ms = None
for i, layer in enumerate(doc.Layers):
if _is_top_level(layer):
continue
if layer.Name.startswith(code + "_"):
if col is not None:
layer.Color = col
if lw is not None:
if _ms is not None:
_ms.write_plotweight(doc, layer, float(lw))
else:
layer.PlotWeight = float(lw)
doc.Views.Redraw()
def set_ebene_visible(doc, code, visible):
"""Schaltet alle Sublayer mit Code in/aus Zeichnungsebenen."""
for i, layer in enumerate(doc.Layers):
if _is_top_level(layer):
continue
if layer.Name.startswith(code + "_"):
layer.IsVisible = visible
doc.Views.Redraw()
def set_ebene_locked(doc, code, locked):
for i, layer in enumerate(doc.Layers):
if _is_top_level(layer):
continue
if layer.Name.startswith(code + "_"):
layer.IsLocked = locked
doc.Views.Redraw()
def delete_ebene(doc, code, move_to=None):
"""
Loescht alle Sublayer mit dem gegebenen Code in allen Zeichnungsebenen.
Falls move_to gesetzt: verschiebt vorher alle Objekte zum Sublayer
mit move_to-Code unter dem selben Parent. Sonst: loescht Objekte mit.
"""
if not code:
return
from_prefix = code + "_"
to_prefix = (move_to + "_") if move_to else None
# Top-Level Parents finden
parents = [layer for layer in doc.Layers if _is_top_level(layer)]
moved = 0
deleted_objs = 0
deleted_layers = 0
for parent in parents:
from_layer = None
to_layer = None
for layer in doc.Layers:
if layer.ParentLayerId != parent.Id:
continue
if layer.Name.startswith(from_prefix):
from_layer = layer
elif to_prefix and layer.Name.startswith(to_prefix):
to_layer = layer
if from_layer is None:
continue
from_idx = doc.Layers.FindByFullPath(from_layer.FullPath, -1)
if from_idx < 0:
continue
objs = list(doc.Objects.FindByLayer(doc.Layers[from_idx]))
if move_to and to_layer is not None:
to_idx = doc.Layers.FindByFullPath(to_layer.FullPath, -1)
if to_idx >= 0:
for obj in objs:
attrs = obj.Attributes.Duplicate()
attrs.LayerIndex = to_idx
if doc.Objects.ModifyAttributes(obj, attrs, True):
moved += 1
else:
for obj in objs:
if doc.Objects.Delete(obj.Id, True):
deleted_objs += 1
# Sublayer loeschen
try:
if doc.Layers.Delete(from_idx, True):
deleted_layers += 1
except Exception as ex:
print("[EBENEN] Layer-Delete:", ex)
doc.Views.Redraw()
print("[EBENEN] Ebene {} entfernt: {} Sublayer, {} Objekte verschoben, {} Objekte geloescht".format(
code, deleted_layers, moved, deleted_objs))
# --- Clipping Plane Management ----------------------------------------------
_CLIP_KEY = "dossier_clipping_plane"
def _find_clipping_plane(doc):
for obj in doc.Objects:
try:
if obj.Attributes.GetUserString(_CLIP_KEY) == "1":
return obj
except Exception:
pass
return None
def update_clipping_plane(doc, active_z, enabled):
"""
Erstellt/aktualisiert/entfernt die DOSSIER-Clipping-Plane an OKFF + Schnitthoehe
des aktiven Geschosses. Plane zeigt nach +Z, schneidet alles oberhalb weg.
Diagnostik via [CLIP]-Praefix in den Print-Ausgaben — bei Problemen die
Rhino-Konsole nach 'CLIP' filtern.
"""
import Rhino.Geometry as rg
z_name = (active_z or {}).get("name") if active_z else None
print("[CLIP] update: enabled={} active='{}' isGeschoss={} okff={} sh={}".format(
enabled,
z_name,
bool(active_z and active_z.get("isGeschoss")),
(active_z or {}).get("okff"),
(active_z or {}).get("schnitthoehe"),
))
existing = _find_clipping_plane(doc)
print("[CLIP] existing: {}".format(existing.Id if existing else "none"))
is_geschoss = bool(active_z and active_z.get("isGeschoss") and active_z.get("okff") is not None)
# IMMER vorhandene Plane loeschen — bei Re-Enable wollen wir frische
# vp_ids (alte koennten leer/falsch sein, dann clippt das Replace zwar
# die Geometrie aber keinen Viewport).
if existing is not None:
try:
doc.Objects.Delete(existing.Id, True)
print("[CLIP] alte Plane geloescht")
except Exception as ex:
print("[CLIP] Delete fehlgeschlagen:", ex)
if (not enabled) or (not is_geschoss):
print("[CLIP] disabled — fertig (enabled={}, isGeschoss={})".format(enabled, is_geschoss))
doc.Views.Redraw()
return
# dict.get(k, default) liefert default NUR wenn Key fehlt — bei
# Key-vorhanden-aber-None gibt's None zurueck. float(None) crasht.
# Daher explizit None-faangen:
okff_raw = active_z.get("okff")
sh_raw = active_z.get("schnitthoehe")
okff = float(okff_raw) if okff_raw is not None else 0.0
sh = float(sh_raw) if sh_raw is not None else 1.0
cut_z = okff + sh
print("[CLIP] cut_z={} (okff={}, schnitthoehe={})".format(cut_z, okff, sh))
# Normal nach -Z = sichtbar bleibt UNTERHALB der Plane (Grundriss-Schnitt:
# man steht auf der Boden-Seite, alles darueber wird weggeschnitten).
# Mit +Z waere es genau umgekehrt (Decke + Rest oben sichtbar).
plane = rg.Plane(rg.Point3d(0.0, 0.0, cut_z), rg.Vector3d(0.0, 0.0, -1.0))
du, dv = 50000.0, 50000.0
# Viewport-IDs sammeln — wir wollen ALLE Modell-Viewports clippen,
# nicht nur den gerade aktiven. Sammeln aus mehreren Quellen +
# dedupen damit die Plane in Top/Front/Right/Perspective gleichzeitig
# wirkt.
vp_ids = []
seen = set()
def _add(vpid):
if vpid is None: return
try:
key = str(vpid)
except Exception: return
if key in seen or vpid == System.Guid.Empty: return
seen.add(key); vp_ids.append(vpid)
# Methode 1: GetViewList — alle Modell-Views (kein Page-Layout)
try:
views = doc.Views.GetViewList(True, False)
for v in views:
try: _add(v.ActiveViewport.Id)
except Exception: pass
try: _add(v.MainViewport.Id)
except Exception: pass
except Exception as ex:
print("[CLIP] GetViewList Fehler:", ex)
# Methode 2: Iteration ueber Views (Fallback falls GetViewList anders)
try:
for view in doc.Views:
try: _add(view.ActiveViewport.Id)
except Exception: pass
try: _add(view.MainViewport.Id)
except Exception: pass
try: _add(view.ActiveViewportID)
except Exception: pass
except Exception as ex:
print("[CLIP] doc.Views iteration Fehler:", ex)
# Namen fuer Debug-Output sammeln
vp_names = []
try:
for view in doc.Views:
try: vp_names.append(view.ActiveViewport.Name)
except Exception: pass
except Exception: pass
print("[CLIP] {} Viewport-ID(s) gesammelt: {}".format(
len(vp_ids), ", ".join(vp_names) or "(keine Namen)"))
if not vp_ids:
print("[CLIP] WARNUNG: keine Viewports — Plane wuerde nichts schneiden")
try:
new_id = doc.Objects.AddClippingPlane(plane, du, dv, vp_ids)
if new_id == System.Guid.Empty:
print("[CLIP] AddClippingPlane lieferte Empty Guid — Fehler")
return
obj = doc.Objects.FindId(new_id)
if obj is None:
print("[CLIP] FindId nach Erstellung lieferte None — Object weg")
return
attrs = obj.Attributes.Duplicate()
attrs.SetUserString(_CLIP_KEY, "1")
# Mode = Normal damit die Plane in Mac Rhino voll sichtbar ist.
# Locked-Mode rendert auf Mac oft nur ein blasses Edge. Wer den
# Plane-Boundary nicht selektieren will, kann via Layer locken.
attrs.Mode = Rhino.DocObjects.ObjectMode.Normal
doc.Objects.ModifyAttributes(obj, attrs, True)
print("[CLIP] Plane erstellt: Z={}, ID={}, du/dv={}/{}".format(
cut_z, new_id, du, dv))
except Exception as ex:
print("[CLIP] AddClippingPlane Fehler:", ex)
doc.Views.Redraw()
def cleanup_default_layers(doc):
"""Loescht leere Rhino-Default-Layer (Default, Layer 01, ...) — nicht-leere bleiben unberuehrt."""
import re
pattern = re.compile(r'^(default|layer\s*0*\d+)$', re.IGNORECASE)
deleted = []
for i in range(doc.Layers.Count - 1, -1, -1):
layer = doc.Layers[i]
if layer.IsDeleted:
continue
if not _is_top_level(layer):
continue
if not pattern.match(layer.Name.strip()):
continue
try:
# Name VOR Delete sichern — sonst liefert layer.Name danach None
nm = layer.Name
if doc.Layers.Delete(i, True):
if nm: deleted.append(nm)
except Exception:
pass
if deleted:
print("[EBENEN] Default-Layer entfernt: {}".format(", ".join(deleted)))
def set_active_sublayer(doc, zeichnungsebene_id, code):
"""Macht den Sublayer 'code' unter Zeichnungsebene 'zeichnungsebene_id' aktiv."""
parent_idx = _find_top_by_id(doc, zeichnungsebene_id)
if parent_idx < 0:
print("[EBENEN] Parent-Layer fuer Zeichnungsebene {} nicht gefunden".format(zeichnungsebene_id))
return
parent_id = doc.Layers[parent_idx].Id
sub_idx = _find_sublayer_by_code(doc, parent_id, code)
if sub_idx >= 0:
doc.Layers.SetCurrentLayerIndex(sub_idx, True)
else:
print("[EBENEN] Sublayer mit Code {} unter Parent {} nicht gefunden".format(code, doc.Layers[parent_idx].Name))
def apply_visibility(doc, zeichnungsebenen, ebenen, active_z_id, active_code, z_mode, e_mode):
"""
Kombinierte Sichtbarkeit aus Z-Mode (Zeichnungsebenen) und E-Mode (Ebenen).
Beide Modi: 'all' | 'active' | 'grey' | 'grey_locked'
"""
canonical = {e["code"]: _color(e.get("color")) for e in ebenen}
e_eye_vis = {e["code"]: e.get("visible", True) for e in ebenen}
e_eye_locked = {e["code"]: e.get("locked", False) for e in ebenen}
id_to_top, name_to_top, children_by_parent = {}, {}, {}
for layer in doc.Layers:
if _is_top_level(layer):
uid = layer.GetUserString("dossier_id")
if uid:
id_to_top[uid] = layer
name_to_top[layer.Name] = layer
else:
children_by_parent.setdefault(layer.ParentLayerId, []).append(layer)
for z in zeichnungsebenen:
parent = id_to_top.get(z["id"]) or name_to_top.get(z["name"])
if parent is None:
continue
children = children_by_parent.get(parent.Id, [])
is_active_z = z["id"] == active_z_id
z_visible_flag = z.get("visible", True)
z_locked_flag = bool(z.get("locked", False))
# Z-Mode -> Parent-Zustand
if is_active_z:
p_vis, p_grey, p_lock = True, False, False
elif z_mode == "active":
p_vis, p_grey, p_lock = False, False, False
elif not z_visible_flag:
p_vis, p_grey, p_lock = False, False, False
elif z_mode == "all":
p_vis, p_grey, p_lock = True, False, False
elif z_mode == "grey_locked":
p_vis, p_grey, p_lock = True, True, True
else: # grey
p_vis, p_grey, p_lock = True, True, False
# Per-Z explizites Sperren ueberlagert (auch fuer die aktive Z) — wer
# eine Geschoss-Ebene sperrt, will dass Klicks ins Leere gehen.
if z_locked_flag:
p_lock = True
parent_changed = False
if parent.IsVisible != p_vis:
parent.IsVisible = p_vis
parent_changed = True
if parent.IsLocked != p_lock:
parent.IsLocked = p_lock
parent_changed = True
if parent_changed:
try: doc.Layers.Modify(parent, parent.LayerIndex, True)
except Exception: pass
if not p_vis:
continue # Children erben Parent-Hidden
# E-Mode -> Sublayer-Zustand
for child in children:
if "_" not in child.Name:
continue
code = child.Name.split("_", 1)[0]
if code not in canonical:
continue
is_active_e = (code == active_code)
eye_v = e_eye_vis.get(code, True)
eye_l = e_eye_locked.get(code, False)
if is_active_e:
e_vis, e_grey, e_lock = True, False, False
elif e_mode == "active":
e_vis, e_grey, e_lock = False, False, False
elif not eye_v:
e_vis, e_grey, e_lock = False, False, False
elif e_mode == "all":
e_vis, e_grey, e_lock = True, False, False
elif e_mode == "grey_locked":
e_vis, e_grey, e_lock = True, True, True
else: # grey
e_vis, e_grey, e_lock = True, True, False
# Kombination
child_vis = e_vis
child_grey = p_grey or e_grey
child_lock = e_lock or eye_l
changed = False
if child.IsVisible != child_vis:
child.IsVisible = child_vis
changed = True
if child.IsLocked != child_lock:
child.IsLocked = child_lock
changed = True
if child_grey:
if child.Color != GREY:
child.Color = GREY
changed = True
else:
canon = canonical.get(code)
if canon is not None and child.Color != canon:
child.Color = canon
changed = True
# In neueren Rhino-Versionen committed der Property-Setter direkt,
# in manchen Faellen (besonders auf Mac) wird IsLocked nicht
# persistiert ohne explizites Modify. Defensiv:
if changed:
try:
doc.Layers.Modify(child, child.LayerIndex, True)
except Exception:
pass
doc.Views.Redraw()