#! python 3 # -*- coding: utf-8 -*- """ layer_builder.py Layer-Struktur: +-- 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 _try_set(obj, prop_names, value): """Versucht den Wert auf das erste vorhandene Property zu setzen. Liefert den Property-Namen bei Erfolg, sonst None.""" if isinstance(prop_names, str): prop_names = (prop_names,) for prop in prop_names: if hasattr(obj, prop): try: setattr(obj, prop, value) return prop except Exception as ex: # Property da, aber Wert nicht akzeptiert (z.B. enum-conversion) # — anderen Namen probieren statt aufgeben continue return None def _enum_int(*candidates): """Liefert den ersten Enum-Wert aus den Kandidaten der existiert. candidates = list of (module-path-list, value-name). Bei keiner Match liefert None.""" for path, val_name in candidates: try: obj = Rhino for p in path: obj = getattr(obj, p) return getattr(obj, val_name) except Exception: continue return None 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. Wichtig: viele Farb-/Linetype-Properties greifen nur wenn der zugehoerige "*Source"-Wert auf ColorFromObject / LinetypeFromObject steht. Das setzen wir explizit. """ 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: return # IPy-2.7 — keine API try: SS = Rhino.DocObjects.SectionStyle except Exception as ex: print("[EBENEN] SectionStyle-Klasse nicht da:", ex); return pat = (section_cfg.get("hatchPattern") or "None").strip() show = bool(section_cfg.get("boundaryShow", True)) diag = "[SS:{}]".format(layer.Name if layer else "?") # Wenn weder Hatch noch Boundary → Custom-Style entfernen if pat == "None" and not show: if has_remover: try: layer.RemoveCustomSectionStyle() print(diag, "removed (kein Hatch + kein Boundary)") except Exception as ex: print(diag, "remove fehlgeschlagen:", ex) return style = SS() # Property-Inventar einmal beim ersten Aufruf loggen — hilft bei # API-Verschiebungen zwischen Rhino-Versionen sofort den Mismatch zu sehen. if not getattr(_apply_section_style, "_props_logged", False): props = [n for n in dir(style) if not n.startswith("_") and not callable(getattr(style, n, None))] print("[SS] verfuegbare Properties:", ", ".join(sorted(props))) _apply_section_style._props_logged = True # --- Hatch --- if pat and pat != "None": hp_idx = _find_hatch_pattern_index(doc, pat) if hp_idx >= 0: set_to = _try_set(style, ("HatchIndex", "HatchPatternIndex"), hp_idx) print(diag, "HatchIndex={} via {}".format(hp_idx, set_to)) else: print(diag, "Hatch '{}' nicht in HatchPatterns gefunden".format(pat)) scale_v = float(section_cfg.get("hatchScale") or 1.0) _try_set(style, ("HatchScale", "HatchPatternScale"), scale_v) import math rot_deg = float(section_cfg.get("hatchRotation") or 0) _try_set(style, ("HatchRotation", "HatchAngle"), math.radians(rot_deg)) # Hatch-Color: explizit ColorFromObject setzen damit der eigene Wert greift hatch_color = section_cfg.get("hatchColor") if hatch_color: col = _color(hatch_color) set_color = _try_set(style, ("HatchColor", "FillColor"), col) # Source auf "FromObject" — sonst nutzt Rhino den Layer-Color src_from_object = _enum_int( (("DocObjects", "ObjectColorSource"), "ColorFromObject")) if src_from_object is not None: _try_set(style, ("HatchColorSource", "FillColorSource"), src_from_object) print(diag, "HatchColor via {}".format(set_color)) # Background (viewport=0/transparent vs object=1) bg = section_cfg.get("background") if bg in ("object", "byObject"): # Versuche Enum-Konstanten zu finden for prop_names, en_paths in ( (("BackgroundFillMode", "BackgroundColorUsage", "FillBackground"), (("DocObjects", "SectionBackgroundFillMode"), "SolidColor")), ): en_val = _enum_int(en_paths) if en_val is not None and _try_set(style, prop_names, en_val): break # Fallback: bool/int = 1 else: _try_set(style, ("FillBackground",), True) # --- Boundary --- set_show = _try_set(style, ("BoundaryVisible", "ShowBoundary"), show) print(diag, "BoundaryVisible={} via {}".format(show, set_show)) if show: # Boundary-Color: setze Color + Source auf FromObject bc = section_cfg.get("boundaryColor") if bc: col = _color(bc) set_to = _try_set(style, ("BoundaryColor", "OutlineColor", "EdgeColor"), col) src_from_object = _enum_int( (("DocObjects", "ObjectColorSource"), "ColorFromObject")) if src_from_object is not None: _try_set(style, ("BoundaryColorSource", "OutlineColorSource", "EdgeColorSource"), src_from_object) print(diag, "BoundaryColor={} via {}".format(bc, set_to)) # Width-Scale auf PlotWeight uebertragen (RW8 hat keine WidthScale direkt; # alternative Property-Namen probieren) ws = float(section_cfg.get("boundaryWidthScale") or 1.0) set_to = _try_set(style, ("BoundaryWidthScale", "EdgeWidthScale", "OutlineWidthScale", "PlotWeightScale"), ws) if not set_to: # Direkte PlotWeight setzen wenn Layer-PlotWeight bekannt try: base_lw = float(getattr(layer, "PlotWeight", 0.25) or 0.25) except Exception: base_lw = 0.25 _try_set(style, ("BoundaryPlotWeight", "PlotWeight"), base_lw * ws) # Linetype: Index + Source auf LinetypeFromObject lt = section_cfg.get("boundaryLinetype") if lt and lt not in ("byLayer", "ByLayer"): lt_idx = _find_linetype_index(doc, lt) set_to = _try_set(style, ("BoundaryLinetypeIndex", "EdgeLinetypeIndex"), lt_idx) lt_src = _enum_int( (("DocObjects", "ObjectLinetypeSource"), "LinetypeFromObject")) if lt_src is not None: _try_set(style, ("BoundaryLinetypeSource", "EdgeLinetypeSource"), lt_src) print(diag, "BoundaryLinetype={} idx={} via {}".format(lt, lt_idx, set_to)) # SectionOpenObjects: bei nicht-geschlossener Geometrie auch schneiden soo = bool(section_cfg.get("sectionOpenObjects", True)) _try_set(style, ("SectionOpenObjects", "ClipOpenObjects", "SectionCutsOpenObjects"), soo) # Style auf Layer setzen + explizit Modify damit Mac-Rhino den Layer # persistiert (sonst greift's nicht immer) try: layer.SetCustomSectionStyle(style) except Exception as ex: print(diag, "SetCustomSectionStyle FAIL:", ex) return try: doc.Layers.Modify(layer, layer.LayerIndex, True) except Exception: pass print(diag, "OK applied") def walk_ebenen(ebenen, parent_path=()): """Iteriert Ebenen-Baum (flach + Children). Liefert Tuples (path, ebene) wobei path ein Tuple der Codes von der Root bis zu dieser Ebene ist (inkl. eigener Code). Beispiel: walk_ebenen([{'code':'20','children':[{'code':'01'}]}]) → [(('20',), e20), (('20','01'), e01)]""" out = [] if not ebenen: return out for e in ebenen: if not isinstance(e, dict): continue code = e.get("code") if not code: continue path = parent_path + (code,) out.append((path, e)) children = e.get("children") if isinstance(children, list) and children: out.extend(walk_ebenen(children, path)) return out def _build_ebene_layer(doc, parent_id, e, diag_prefix=""): """Findet/erstellt einen Sublayer fuer eine Ebene unter parent_id. Liefert den layer_idx oder -1. Setzt Farbe/LW/Section-Style.""" code = e.get("code") or "" name = e.get("name") or "Ebene" sub_name = "{}_{}".format(code, name) if code else name col = _color(e.get("color")) lw = float(e.get("lw", 0.13)) sub_idx = _find_sublayer_by_code(doc, parent_id, code) if code else -1 if sub_idx < 0: sub_idx = _add_layer(doc, sub_name, parent_id, col, lw) if sub_idx >= 0 and code: doc.Layers[sub_idx].SetUserString("dossier_code", 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 if code: sub.SetUserString("dossier_code", 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( diag_prefix, sub_name, ex)) return sub_idx def _build_ebenen_recursive(doc, parent_id, ebenen, diag_prefix=""): """Rekursive Ebenen-Erstellung: jeder Eintrag wird als Sublayer angelegt, seine 'children' werden unter dem neu erstellten Sublayer angelegt.""" if not ebenen: return for e in ebenen: if not isinstance(e, dict): continue sub_idx = _build_ebene_layer(doc, parent_id, e, diag_prefix=diag_prefix) if sub_idx < 0: continue children = e.get("children") if isinstance(children, list) and children: child_parent_id = doc.Layers[sub_idx].Id _build_ebenen_recursive(doc, child_parent_id, children, diag_prefix=diag_prefix + e.get("name", "") + "/") def build_layers(doc, zeichnungsebenen, ebenen): """Stellt sicher dass fuer jede Zeichnungsebene ein Parent-Layer existiert und unter jedem alle Ebenen (rekursiv inkl. children) 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 _build_ebenen_recursive(doc, parent_id, ebenen, diag_prefix=z_name + "/") doc.Views.Redraw() n_total = len(walk_ebenen(ebenen)) print("[EBENEN] {} Zeichnungsebenen x {} Ebenen aktualisiert (inkl. {} Sub)".format( len(zeichnungsebenen), len(ebenen), max(0, n_total - len(ebenen)))) def _layer_matches_code(layer, code): """True wenn der Layer zu der Ebene mit `code` gehoert. Akzeptiert sowohl Top-Sub-Layer (Geschoss/CODE_Name) als auch Sub-Sub-Layer (Geschoss/Parent/CODE_Name) — Match via Name-Prefix `code_`.""" if _is_top_level(layer): return False return layer.Name.startswith(code + "_") def update_layer_style(doc, code, color_hex=None, lw=None): """Aendert Farbe und/oder Stiftdicke fuer alle Sublayer mit dem gegebenen Code — auch tief verschachtelte (Sub-Sub-Layer mit gleichem Code-Prefix).""" col = _color(color_hex) if color_hex else None try: import massstab as _ms except Exception: _ms = None for layer in doc.Layers: if not _layer_matches_code(layer, code): continue 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 (auch tief verschachtelte).""" for layer in doc.Layers: if _layer_matches_code(layer, code): layer.IsVisible = visible doc.Views.Redraw() def set_ebene_locked(doc, code, locked): for layer in doc.Layers: if _layer_matches_code(layer, 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' Versteht den hierarchischen Ebenen-Baum: Children erben ParentLayerId vom Sub-Layer (nicht vom Geschoss). Sub-Sub-Layer werden rekursiv mitgepflegt. """ # Flat walk durch Ebenen-Tree (top + children) — alle Codes mit ihren # Eye/Lock-Flags. flat_ebenen = [e for _path, e in walk_ebenen(ebenen)] canonical = {e["code"]: _color(e.get("color")) for e in flat_ebenen} e_eye_vis = {e["code"]: e.get("visible", True) for e in flat_ebenen} e_eye_locked = {e["code"]: e.get("locked", False) for e in flat_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 # 'all_force' kommt VOR dem visible-Flag-Check: zeigt jede Z auch wenn # das User-Eye sie ausgeblendet hatte. 'all' dagegen respektiert das # Eye-Flag (= "Ausgewählte" im UI). 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 z_mode == "all_force": p_vis, p_grey, p_lock = True, 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 → Sub-Layer (rekursiv durch Tree; Sub-Sub-Layer haben Parent # = Sub-Layer, nicht das Geschoss — also iterativ in die Tiefe). def _apply_to_sublayer(child, p_grey_eff): if "_" not in child.Name: return code = child.Name.split("_", 1)[0] if code not in canonical: return 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 child_vis = e_vis child_grey = p_grey_eff 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 if changed: try: doc.Layers.Modify(child, child.LayerIndex, True) except Exception: pass # Sub-Sub-Layer rekursiv (Children dieses Sub-Layers). # Sub-Sub-Layer erben den 'grey'-Zustand des Parents. for grand in children_by_parent.get(child.Id, []): _apply_to_sublayer(grand, child_grey) for child in children: _apply_to_sublayer(child, p_grey) doc.Views.Redraw()