Initial commit — Dossier Rhino 8 Plugin
OpenStudio-Suite Architektur-Plugin fuer Rhino 8 (Mac): - Smart-Elemente: Wand, Decke, Dach (Pult/Sattel/Walm/Mansarde), Oeffnungen (Fenster/Tueren mit Rahmen + Sims + Glas + Fluegel), Treppen (gerade · L · Wendel mit Schrittmass-Validierung) - Live-Previews mit Step-Lines + Soll-Range-Clamping - Bidirektionale Selection-Sync zwischen Source-Linie und Volume - Geschoss-/Ebenen-Verwaltung mit OKFF-Persistenz - Layouts mit PDF-Export - Ausschnitte / Massstab / Override-Regeln - Petrol-Gruen Theme (Rapport-konform) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,436 @@
|
||||
# ! python3
|
||||
# -*- 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 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"])
|
||||
|
||||
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.
|
||||
"""
|
||||
import Rhino.Geometry as rg
|
||||
existing = _find_clipping_plane(doc)
|
||||
is_geschoss = bool(active_z and active_z.get("isGeschoss") and active_z.get("okff") is not None)
|
||||
if (not enabled) or (not is_geschoss):
|
||||
if existing is not None:
|
||||
try:
|
||||
doc.Objects.Delete(existing.Id, True)
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
okff = float(active_z.get("okff", 0.0))
|
||||
sh = float(active_z.get("schnitthoehe", 1.0))
|
||||
cut_z = okff + sh
|
||||
plane = rg.Plane(rg.Point3d(0.0, 0.0, cut_z), rg.Vector3d.ZAxis)
|
||||
du, dv = 50000.0, 50000.0
|
||||
if existing is not None:
|
||||
try:
|
||||
new_surf = rg.PlaneSurface(plane, rg.Interval(-du/2.0, du/2.0), rg.Interval(-dv/2.0, dv/2.0))
|
||||
doc.Objects.Replace(existing.Id, new_surf)
|
||||
except Exception as ex:
|
||||
print("[EBENEN] Clip-Update:", ex)
|
||||
else:
|
||||
vp_ids = []
|
||||
for view in doc.Views:
|
||||
try:
|
||||
vp_ids.append(view.ActiveViewportID)
|
||||
except Exception:
|
||||
try: vp_ids.append(view.ActiveViewport.Id)
|
||||
except Exception: pass
|
||||
try:
|
||||
new_id = doc.Objects.AddClippingPlane(plane, du, dv, vp_ids)
|
||||
if new_id != System.Guid.Empty:
|
||||
obj = doc.Objects.FindId(new_id)
|
||||
if obj is not None:
|
||||
attrs = obj.Attributes.Duplicate()
|
||||
attrs.SetUserString(_CLIP_KEY, "1")
|
||||
attrs.Mode = Rhino.DocObjects.ObjectMode.Locked
|
||||
doc.Objects.ModifyAttributes(obj, attrs, True)
|
||||
print("[EBENEN] Clipping-Plane bei Z={} erstellt".format(cut_z))
|
||||
except Exception as ex:
|
||||
print("[EBENEN] Clip-Create:", 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-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
|
||||
|
||||
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()
|
||||
Reference in New Issue
Block a user