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,798 @@
|
||||
# ! python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
rhinopanel.py
|
||||
Oeffnet das EBENEN-Panel (Zeichnungsebenen + globale Ebenen).
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import Rhino
|
||||
import scriptcontext as sc
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
if _HERE not in sys.path:
|
||||
sys.path.insert(0, _HERE)
|
||||
|
||||
import panel_base
|
||||
import layer_builder
|
||||
|
||||
PANEL_GUID_STR = "3a7f2e1d-4b8c-4d9e-a1b2-c3d4e5f60718"
|
||||
|
||||
# Loop-Guard fuer Layer-Events (verhindert Endlos-Schleife bei eigenen Aenderungen)
|
||||
def _is_processing():
|
||||
return bool(sc.sticky.get("ebenen_processing_layer", False))
|
||||
|
||||
def _set_processing(v):
|
||||
sc.sticky["ebenen_processing_layer"] = bool(v)
|
||||
|
||||
|
||||
def _hatch_pattern_names(doc):
|
||||
"""Liefert alle Hatch-Pattern-Namen aus doc.HatchPatterns als Liste."""
|
||||
out = []
|
||||
try:
|
||||
for i in range(doc.HatchPatterns.Count):
|
||||
try:
|
||||
hp = doc.HatchPatterns[i]
|
||||
if hp is None or hp.IsDeleted: continue
|
||||
if hp.Name: out.append(hp.Name)
|
||||
except Exception:
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
if not out: out = ["Solid"]
|
||||
return out
|
||||
|
||||
|
||||
class EbenenBridge(panel_base.BaseBridge):
|
||||
def __init__(self):
|
||||
panel_base.BaseBridge.__init__(self, "ebenen")
|
||||
|
||||
def _on_ready(self):
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
|
||||
e_raw = doc.Strings.GetValue("dossier_ebenen")
|
||||
if z_raw or e_raw:
|
||||
try:
|
||||
z = json.loads(z_raw) if z_raw else None
|
||||
e = json.loads(e_raw) if e_raw else None
|
||||
if z and e:
|
||||
layer_builder.build_layers(doc, z, e)
|
||||
layer_builder.cleanup_default_layers(doc)
|
||||
self._ensure_active_sublayer()
|
||||
self.send("STATE_SYNC", {
|
||||
"zeichnungsebenen": z,
|
||||
"ebenen": e,
|
||||
"hatchPatterns": _hatch_pattern_names(doc),
|
||||
})
|
||||
except Exception as ex:
|
||||
print("[EBENEN] State-Sync:", ex)
|
||||
else:
|
||||
self.send("FIRST_RUN", {"hatchPatterns": _hatch_pattern_names(doc)})
|
||||
|
||||
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 = {}
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
|
||||
if t == "READY":
|
||||
self._on_ready()
|
||||
elif t == "APPLY":
|
||||
self._apply(p.get("zeichnungsebenen") or [], p.get("ebenen") or [])
|
||||
elif t == "LAYER_STYLE":
|
||||
layer_builder.update_layer_style(doc, p["code"], p.get("color"), p.get("lw"))
|
||||
if p.get("color") is not None:
|
||||
self._update_ebene_field(p["code"], "color", p["color"])
|
||||
if p.get("lw") is not None:
|
||||
self._update_ebene_field(p["code"], "lw", p["lw"])
|
||||
elif t == "SET_ACTIVE":
|
||||
self._set_active_zeichnungsebene(p)
|
||||
elif t == "SET_ACTIVE_LAYER":
|
||||
code = p.get("code", "")
|
||||
if code:
|
||||
doc.Strings.SetString("dossier_active_code", code)
|
||||
self._set_active_sublayer(code)
|
||||
elif t == "DELETE_EBENE":
|
||||
layer_builder.delete_ebene(doc, p.get("code", ""), p.get("moveTo"))
|
||||
self._remove_ebene_from_state(p.get("code", ""))
|
||||
elif t == "MOVE_SELECTION_TO_LAYER":
|
||||
self._move_selection_to_layer(p.get("code", ""))
|
||||
elif t == "SET_VISIBILITY":
|
||||
self._apply_visibility(p)
|
||||
# --- Ebenen-Kombinationen (geteilter Store mit Ausschnitten) -------
|
||||
elif t == "GET_COMBINATION":
|
||||
self._send_combination()
|
||||
elif t == "APPLY_COMBINATION":
|
||||
self._apply_combination(p)
|
||||
self._send_combination()
|
||||
elif t == "SAVE_PRESET":
|
||||
self._save_preset(p.get("name") or "", p.get("layers") or [])
|
||||
self._send_combination()
|
||||
elif t == "SAVE_CURRENT_AS_PRESET":
|
||||
self._save_current_as_preset(p.get("name") or "")
|
||||
self._send_combination()
|
||||
elif t == "DELETE_PRESET":
|
||||
self._delete_preset(p.get("name") or "")
|
||||
self._send_combination()
|
||||
|
||||
# ---- Helpers ----
|
||||
|
||||
def _apply(self, zeichnungsebenen, ebenen):
|
||||
print("[EBENEN] _apply START z={} e={}".format(
|
||||
len(zeichnungsebenen) if zeichnungsebenen else 0,
|
||||
len(ebenen) if ebenen else 0))
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
|
||||
# Vor dem Schreiben: alten Fill-Stand snapshotten, damit wir hinterher
|
||||
# entscheiden koennen ob refresh_layer_fills sich lohnt.
|
||||
def _fill_signature(e_list):
|
||||
out = {}
|
||||
if not isinstance(e_list, list): return out
|
||||
for e in e_list:
|
||||
if not isinstance(e, dict): continue
|
||||
f = e.get("fill")
|
||||
if not isinstance(f, dict): continue
|
||||
if f.get("pattern") in (None, "None"): continue
|
||||
# lw kann None sein -> als Sentinel ein eindeutiger Wert
|
||||
lw_raw = f.get("lw")
|
||||
try:
|
||||
lw_sig = round(float(lw_raw), 6) if lw_raw is not None else None
|
||||
except Exception:
|
||||
lw_sig = None
|
||||
out[e.get("code")] = (
|
||||
f.get("pattern"),
|
||||
f.get("source", "layer"),
|
||||
(f.get("color") or "").lower(),
|
||||
round(float(f.get("scale") or 1.0), 6),
|
||||
round(float(f.get("rotation") or 0.0), 6),
|
||||
lw_sig,
|
||||
)
|
||||
return out
|
||||
old_e_raw = doc.Strings.GetValue("dossier_ebenen")
|
||||
old_sig = {}
|
||||
if old_e_raw:
|
||||
try: old_sig = _fill_signature(json.loads(old_e_raw))
|
||||
except Exception: old_sig = {}
|
||||
new_sig = _fill_signature(ebenen)
|
||||
fill_changed = (old_sig != new_sig)
|
||||
|
||||
_set_processing(True)
|
||||
try:
|
||||
print("[EBENEN] _apply: build_layers ...")
|
||||
layer_builder.build_layers(doc, zeichnungsebenen, ebenen)
|
||||
print("[EBENEN] _apply: json.dumps ...")
|
||||
# WICHTIG: ensure_ascii=False umgeht einen Bug in Rhinos eigener
|
||||
# json/encoder.py die bei ASCII-escape s.decode('utf-8') aufruft
|
||||
# und dabei mit 0xC4 (Umlaut) in den CP1252-Decoder lauft.
|
||||
z_json = json.dumps(zeichnungsebenen, ensure_ascii=False)
|
||||
e_json = json.dumps(ebenen, ensure_ascii=False)
|
||||
print("[EBENEN] _apply: SetString ...")
|
||||
doc.Strings.SetString("dossier_zeichnungsebenen", z_json)
|
||||
doc.Strings.SetString("dossier_ebenen", e_json)
|
||||
# Smart-Elemente (Waende) regenerieren — Geschoss-Hoehen/OKFF
|
||||
# haben sich evtl. geaendert, gebundene Waende muessen neu
|
||||
# extrudiert werden. Best-effort, faengt jeden Fehler ab.
|
||||
try:
|
||||
elem_bridge = sc.sticky.get("elemente_bridge")
|
||||
if elem_bridge is not None:
|
||||
elem_bridge._regenerate_all()
|
||||
except Exception as _ex:
|
||||
print("[EBENEN] elemente regen:", _ex)
|
||||
n_with_fill = sum(1 for e in ebenen if isinstance(e, dict)
|
||||
and isinstance(e.get("fill"), dict)
|
||||
and e["fill"].get("pattern") not in (None, "None"))
|
||||
print("[EBENEN] dossier_ebenen gespeichert: {} Ebenen, davon {} mit fill, JSON-len={}".format(
|
||||
len(ebenen), n_with_fill, len(e_json)))
|
||||
re_read = doc.Strings.GetValue("dossier_ebenen")
|
||||
print("[EBENEN] dossier_ebenen verifiziert: len={}".format(len(re_read) if re_read else 0))
|
||||
print("[EBENEN] _apply: cleanup_default_layers ...")
|
||||
layer_builder.cleanup_default_layers(doc)
|
||||
print("[EBENEN] _apply: ensure_active_sublayer ...")
|
||||
self._ensure_active_sublayer()
|
||||
# Existierende 'Nach Ebene'-Hatches an neue Pattern/Skala/Drehung
|
||||
# angleichen — ABER nur wenn die Fill-Signatur sich tatsaechlich
|
||||
# geaendert hat (nicht bei reinen Name/Farb-Aenderungen, die das
|
||||
# Settings-Dialog auch triggern koennte).
|
||||
try:
|
||||
import gestaltung
|
||||
if fill_changed:
|
||||
gestaltung.refresh_layer_fills(doc)
|
||||
else:
|
||||
print("[EBENEN] _apply: fill-Signatur unveraendert -> kein Hatch-Refresh")
|
||||
# Plot-Color Repair laeuft immer (no-op falls schon synchron)
|
||||
gestaltung.repair_plot_colors(doc)
|
||||
except Exception as ex:
|
||||
print("[EBENEN] gestaltung sync:", ex)
|
||||
finally:
|
||||
_set_processing(False)
|
||||
print("[EBENEN] _apply: update_clipping ...")
|
||||
self._update_clipping()
|
||||
print("[EBENEN] _apply: send APPLY_OK")
|
||||
self.send("APPLY_OK", {})
|
||||
print("[EBENEN] _apply: DONE")
|
||||
|
||||
def _ensure_active_sublayer(self):
|
||||
"""Setzt den aktiven Rhino-Layer auf den DOSSIER-Sublayer (Fallback: erste Z + 20_WAENDE)."""
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
z_id = doc.Strings.GetValue("dossier_active_id")
|
||||
code = doc.Strings.GetValue("dossier_active_code") or "20"
|
||||
if not z_id:
|
||||
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
|
||||
if z_raw:
|
||||
try:
|
||||
z_list = json.loads(z_raw)
|
||||
if z_list:
|
||||
z_id = z_list[0].get("id", "")
|
||||
if z_id:
|
||||
doc.Strings.SetString("dossier_active_id", z_id)
|
||||
except Exception:
|
||||
pass
|
||||
if z_id and code:
|
||||
layer_builder.set_active_sublayer(doc, z_id, code)
|
||||
|
||||
def _apply_visibility(self, p):
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
|
||||
e_raw = doc.Strings.GetValue("dossier_ebenen")
|
||||
if not z_raw or not e_raw:
|
||||
return
|
||||
try:
|
||||
z_full = json.loads(z_raw) or []
|
||||
e_full = json.loads(e_raw) or []
|
||||
except Exception:
|
||||
return
|
||||
payload_z = p.get("zeichnungsebenen") or []
|
||||
payload_e = p.get("ebenen") or []
|
||||
z_state = {z["id"]: z for z in payload_z if isinstance(z, dict) and z.get("id")}
|
||||
e_state = {e["code"]: e for e in payload_e if isinstance(e, dict) and e.get("code")}
|
||||
merged_z = []
|
||||
for z in z_full:
|
||||
if not isinstance(z, dict): continue
|
||||
m = dict(z)
|
||||
s = z_state.get(z.get("id"))
|
||||
if s is not None:
|
||||
m["visible"] = s.get("visible", True)
|
||||
merged_z.append(m)
|
||||
merged_e = []
|
||||
for e in e_full:
|
||||
if not isinstance(e, dict): continue
|
||||
m = dict(e)
|
||||
s = e_state.get(e.get("code"))
|
||||
if s is not None:
|
||||
m["visible"] = s.get("visible", True)
|
||||
m["locked"] = s.get("locked", False)
|
||||
merged_e.append(m)
|
||||
doc.Strings.SetString("dossier_zeichnungsebenen", json.dumps(merged_z, ensure_ascii=False))
|
||||
doc.Strings.SetString("dossier_ebenen", json.dumps(merged_e, ensure_ascii=False))
|
||||
active_z = p.get("activeZ") or {}
|
||||
if not isinstance(active_z, dict): active_z = {}
|
||||
layer_builder.apply_visibility(
|
||||
doc, merged_z, merged_e,
|
||||
active_z.get("id"),
|
||||
p.get("activeCode"),
|
||||
p.get("zMode") or "active",
|
||||
p.get("eMode") or "all",
|
||||
)
|
||||
|
||||
def _set_active_zeichnungsebene(self, z):
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
z_id = z.get("id", "")
|
||||
doc.Strings.SetString("dossier_active_id", z_id)
|
||||
# Clipping ggf. mitziehen
|
||||
self._update_clipping(active_z=z)
|
||||
# Elemente-Panel informieren: das aktive Geschoss hat gewechselt,
|
||||
# neue Elemente sollen jetzt automatisch dort verlinkt werden.
|
||||
try:
|
||||
eb = sc.sticky.get("elemente_bridge")
|
||||
if eb is not None: eb._send_state()
|
||||
except Exception: pass
|
||||
if not (z.get("isGeschoss") and z.get("okff") is not None):
|
||||
return
|
||||
okff = float(z["okff"])
|
||||
updated = 0
|
||||
for view in doc.Views:
|
||||
try:
|
||||
vp = view.ActiveViewport
|
||||
cp = vp.ConstructionPlane()
|
||||
plane = cp.Plane if hasattr(cp, "Plane") else cp
|
||||
# Nur Views deren CPlane horizontal liegt (Normal in +/-Z) -
|
||||
# also Top/Plan-Style. Right/Front/Perspective haben vertikale
|
||||
# CPlanes; ein Z-Shift waere dort optisch verwirrend.
|
||||
if abs(plane.Normal.Z) < 0.99:
|
||||
continue
|
||||
new_plane = Rhino.Geometry.Plane(
|
||||
Rhino.Geometry.Point3d(plane.Origin.X, plane.Origin.Y, okff),
|
||||
plane.XAxis, plane.YAxis,
|
||||
)
|
||||
vp.SetConstructionPlane(new_plane)
|
||||
view.Redraw()
|
||||
updated += 1
|
||||
except Exception as ex:
|
||||
print("[EBENEN] CPlane fehler ({}): {}".format(vp.Name if vp else "?", ex))
|
||||
print("[EBENEN] CPlane Z={} auf {} Top-Style View(s) gesetzt".format(okff, updated))
|
||||
|
||||
def _update_clipping(self, active_z=None):
|
||||
"""Clipping-Plane folgt aktivem Geschoss — nur wenn dessen hasClipping=True."""
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
if active_z is None:
|
||||
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
|
||||
active_id = doc.Strings.GetValue("dossier_active_id")
|
||||
if z_raw and active_id:
|
||||
try:
|
||||
z_list = json.loads(z_raw)
|
||||
active_z = next((z for z in z_list if z.get("id") == active_id), None)
|
||||
except Exception:
|
||||
active_z = None
|
||||
enabled = bool(active_z and active_z.get("hasClipping"))
|
||||
_set_processing(True)
|
||||
try:
|
||||
layer_builder.update_clipping_plane(doc, active_z, enabled)
|
||||
finally:
|
||||
_set_processing(False)
|
||||
|
||||
def _move_selection_to_layer(self, code):
|
||||
if not code:
|
||||
return
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
z_id = doc.Strings.GetValue("dossier_active_id")
|
||||
if not z_id:
|
||||
print("[EBENEN] Keine aktive Zeichnungsebene")
|
||||
return
|
||||
parent_idx = layer_builder._find_top_by_id(doc, z_id)
|
||||
if parent_idx < 0:
|
||||
print("[EBENEN] Parent fuer aktive Zeichnungsebene nicht gefunden")
|
||||
return
|
||||
parent_id = doc.Layers[parent_idx].Id
|
||||
sub_idx = layer_builder._find_sublayer_by_code(doc, parent_id, code)
|
||||
if sub_idx < 0:
|
||||
print("[EBENEN] Sublayer {} unter {} nicht gefunden".format(code, doc.Layers[parent_idx].Name))
|
||||
return
|
||||
objs = list(doc.Objects.GetSelectedObjects(False, False))
|
||||
moved = 0
|
||||
for obj in objs:
|
||||
attrs = obj.Attributes.Duplicate()
|
||||
attrs.LayerIndex = sub_idx
|
||||
if doc.Objects.ModifyAttributes(obj, attrs, True):
|
||||
moved += 1
|
||||
doc.Views.Redraw()
|
||||
print("[EBENEN] {} Objekt(e) auf {} verschoben".format(moved, doc.Layers[sub_idx].FullPath))
|
||||
|
||||
def _set_active_sublayer(self, code):
|
||||
if not code:
|
||||
return
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
z_id = doc.Strings.GetValue("dossier_active_id")
|
||||
if not z_id:
|
||||
# Fallback: erste Zeichnungsebene aus persistiertem State
|
||||
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
|
||||
if z_raw:
|
||||
try:
|
||||
z_list = json.loads(z_raw)
|
||||
if z_list:
|
||||
z_id = z_list[0].get("id", "")
|
||||
if z_id:
|
||||
doc.Strings.SetString("dossier_active_id", z_id)
|
||||
except Exception:
|
||||
pass
|
||||
if z_id:
|
||||
layer_builder.set_active_sublayer(doc, z_id, code)
|
||||
else:
|
||||
print("[EBENEN] Aktive Zeichnungsebene unbekannt — Layer wird nicht gesetzt")
|
||||
|
||||
def _remove_ebene_from_state(self, code):
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
raw = doc.Strings.GetValue("dossier_ebenen")
|
||||
if not raw:
|
||||
return
|
||||
try:
|
||||
ebenen = [e for e in json.loads(raw) if e.get("code") != code]
|
||||
doc.Strings.SetString("dossier_ebenen", json.dumps(ebenen, ensure_ascii=False))
|
||||
except Exception as ex:
|
||||
print("[EBENEN] remove:", ex)
|
||||
|
||||
def _update_ebene_field(self, code, field, value):
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
raw = doc.Strings.GetValue("dossier_ebenen")
|
||||
if not raw:
|
||||
return
|
||||
try:
|
||||
ebenen = json.loads(raw)
|
||||
for e in ebenen:
|
||||
if e.get("code") == code:
|
||||
e[field] = value
|
||||
break
|
||||
doc.Strings.SetString("dossier_ebenen", json.dumps(ebenen, ensure_ascii=False))
|
||||
except Exception as ex:
|
||||
print("[EBENEN] update:", ex)
|
||||
|
||||
# ---- Ebenen-Kombinationen / Presets (geteilt mit AUSSCHNITTE) --------
|
||||
|
||||
_PRESETS_KEY = "dossier_layer_presets"
|
||||
|
||||
def _load_presets(self, doc):
|
||||
raw = doc.Strings.GetValue(self._PRESETS_KEY)
|
||||
if not raw: return []
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
return data if isinstance(data, list) else []
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def _store_presets(self, doc, presets):
|
||||
try:
|
||||
doc.Strings.SetString(self._PRESETS_KEY,
|
||||
json.dumps(presets, ensure_ascii=False))
|
||||
except Exception as ex:
|
||||
print("[EBENEN] _store_presets:", ex)
|
||||
|
||||
def _send_combination(self):
|
||||
"""Schickt aktuelles Layer-State + alle Presets ans Frontend."""
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
layers_out = []
|
||||
try:
|
||||
for layer in doc.Layers:
|
||||
if layer is None or layer.IsDeleted: continue
|
||||
lid = str(layer.Id)
|
||||
try:
|
||||
fp = layer.FullPath or layer.Name
|
||||
except Exception:
|
||||
fp = layer.Name or ""
|
||||
try:
|
||||
col = "#%02x%02x%02x" % (layer.Color.R, layer.Color.G, layer.Color.B)
|
||||
except Exception:
|
||||
col = "#888888"
|
||||
layers_out.append({
|
||||
"id": lid,
|
||||
"name": layer.Name,
|
||||
"fullPath": fp,
|
||||
"color": col,
|
||||
"visible": bool(layer.IsVisible),
|
||||
"locked": bool(layer.IsLocked),
|
||||
})
|
||||
layers_out.sort(key=lambda x: x["fullPath"])
|
||||
except Exception as ex:
|
||||
print("[EBENEN] _send_combination layers:", ex)
|
||||
try:
|
||||
presets = self._load_presets(doc)
|
||||
except Exception:
|
||||
presets = []
|
||||
self.send("COMBINATION_DATA", {
|
||||
"layers": layers_out,
|
||||
"presets": presets,
|
||||
})
|
||||
|
||||
def _apply_combination(self, payload):
|
||||
"""Wendet Preset an. payload kann sein:
|
||||
- Liste [{id, visible, locked}, ...] (alt / AUSSCHNITTE-Dialog)
|
||||
- Dict { layers, dossierEbenen?, dossierZeichnungsebenen? } (neu)
|
||||
|
||||
Eye-State-Pfad (bevorzugt): aktualisiert dossier_ebenen und
|
||||
dossier_zeichnungsebenen direkt, pusht STATE_SYNC. React triggert
|
||||
dann SET_VISIBILITY und apply_visibility setzt doc.Layer korrekt
|
||||
unter Beruecksichtigung von z_mode/e_mode.
|
||||
|
||||
Layer-ID-Pfad (Fallback): setzt doc.Layer.IsVisible direkt.
|
||||
"""
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
|
||||
# Payload normalisieren
|
||||
if isinstance(payload, dict):
|
||||
layer_states = payload.get("layers") or []
|
||||
pe_states = payload.get("dossierEbenen")
|
||||
pz_states = payload.get("dossierZeichnungsebenen")
|
||||
else:
|
||||
layer_states = payload or []
|
||||
pe_states = None
|
||||
pz_states = None
|
||||
|
||||
# --- Eye-State-Pfad (wenn vorhanden) ---
|
||||
if pe_states is not None or pz_states is not None:
|
||||
try:
|
||||
e_raw = doc.Strings.GetValue("dossier_ebenen") or "[]"
|
||||
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen") or "[]"
|
||||
e_list = json.loads(e_raw) or []
|
||||
z_list = json.loads(z_raw) or []
|
||||
if pe_states is not None:
|
||||
by_code = {x.get("code"): x for x in pe_states if isinstance(x, dict) and x.get("code")}
|
||||
for e in e_list:
|
||||
if not isinstance(e, dict): continue
|
||||
s = by_code.get(e.get("code"))
|
||||
if s is None: continue
|
||||
e["visible"] = bool(s.get("visible", True))
|
||||
e["locked"] = bool(s.get("locked", False))
|
||||
if pz_states is not None:
|
||||
by_id = {x.get("id"): x for x in pz_states if isinstance(x, dict) and x.get("id")}
|
||||
for z in z_list:
|
||||
if not isinstance(z, dict): continue
|
||||
s = by_id.get(z.get("id"))
|
||||
if s is None: continue
|
||||
z["visible"] = bool(s.get("visible", True))
|
||||
doc.Strings.SetString("dossier_ebenen", json.dumps(e_list, ensure_ascii=False))
|
||||
doc.Strings.SetString("dossier_zeichnungsebenen", json.dumps(z_list, ensure_ascii=False))
|
||||
# STATE_SYNC pushen — React's visibilityKey aendert sich,
|
||||
# applyVisibility fires, backend apply_visibility setzt doc.Layer
|
||||
# state korrekt unter z_mode/e_mode-Beachtung.
|
||||
self.send("STATE_SYNC", {
|
||||
"zeichnungsebenen": z_list,
|
||||
"ebenen": e_list,
|
||||
})
|
||||
try: doc.Views.Redraw()
|
||||
except Exception: pass
|
||||
print("[EBENEN] Eye-State-Preset angewandt: {} Ebenen, {} Zeichnungsebenen".format(
|
||||
len(pe_states or []), len(pz_states or [])))
|
||||
return
|
||||
except Exception as ex:
|
||||
print("[EBENEN] _apply_combination eye-state:", ex)
|
||||
# Fall through zum Layer-ID-Pfad als Fallback
|
||||
|
||||
# --- Layer-ID-Pfad (alt / AUSSCHNITTE) ---
|
||||
by_id = {}
|
||||
for layer in doc.Layers:
|
||||
if not layer.IsDeleted:
|
||||
by_id[str(layer.Id)] = layer
|
||||
n = 0
|
||||
# Erst: doc.Layer Visibility setzen
|
||||
_set_processing(True)
|
||||
try:
|
||||
for ls in (layer_states or []):
|
||||
layer = by_id.get(ls.get("id"))
|
||||
if layer is None: continue
|
||||
try:
|
||||
want_vis = bool(ls.get("visible", True))
|
||||
want_lck = bool(ls.get("locked", False))
|
||||
if layer.IsVisible != want_vis:
|
||||
layer.IsVisible = want_vis
|
||||
if layer.IsLocked != want_lck:
|
||||
layer.IsLocked = want_lck
|
||||
n += 1
|
||||
except Exception: pass
|
||||
finally:
|
||||
_set_processing(False)
|
||||
# Dann: dossier_ebenen/dossier_zeichnungsebenen Eye-State synchronisieren.
|
||||
# Map: doc.Layer.Id -> {visible, locked}
|
||||
state_by_id = {ls.get("id"): ls for ls in (layer_states or []) if ls.get("id")}
|
||||
try:
|
||||
e_raw = doc.Strings.GetValue("dossier_ebenen")
|
||||
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
|
||||
ebenen_list = json.loads(e_raw) if e_raw else []
|
||||
z_list = json.loads(z_raw) if z_raw else []
|
||||
# Sublayer -> dossier_code mapping via Rhino-Layer UserString
|
||||
code_by_layer_id = {}
|
||||
zid_by_layer_id = {}
|
||||
for layer in doc.Layers:
|
||||
if layer is None or layer.IsDeleted: continue
|
||||
c = layer.GetUserString("dossier_code")
|
||||
i = layer.GetUserString("dossier_id")
|
||||
if c: code_by_layer_id[str(layer.Id)] = c
|
||||
if i: zid_by_layer_id[str(layer.Id)] = i
|
||||
# Pro Dossier-Ebene: wenn mind. ein matchender Sublayer im preset war,
|
||||
# sync visible/locked.
|
||||
updated_e = False
|
||||
for e in ebenen_list:
|
||||
if not isinstance(e, dict): continue
|
||||
code = e.get("code")
|
||||
if not code: continue
|
||||
# Suche eine Layer-Id mit diesem code, deren state im preset ist
|
||||
for lid, c in code_by_layer_id.items():
|
||||
if c != code: continue
|
||||
s = state_by_id.get(lid)
|
||||
if s is None: continue
|
||||
new_vis = bool(s.get("visible", True))
|
||||
new_lck = bool(s.get("locked", False))
|
||||
if e.get("visible", True) != new_vis:
|
||||
e["visible"] = new_vis
|
||||
updated_e = True
|
||||
if (e.get("locked", False)) != new_lck:
|
||||
e["locked"] = new_lck
|
||||
updated_e = True
|
||||
break
|
||||
updated_z = False
|
||||
for z in z_list:
|
||||
if not isinstance(z, dict): continue
|
||||
zid = z.get("id")
|
||||
if not zid: continue
|
||||
for lid, z_uid in zid_by_layer_id.items():
|
||||
if z_uid != zid: continue
|
||||
s = state_by_id.get(lid)
|
||||
if s is None: continue
|
||||
new_vis = bool(s.get("visible", True))
|
||||
if z.get("visible", True) != new_vis:
|
||||
z["visible"] = new_vis
|
||||
updated_z = True
|
||||
break
|
||||
if updated_e:
|
||||
doc.Strings.SetString("dossier_ebenen", json.dumps(ebenen_list, ensure_ascii=False))
|
||||
if updated_z:
|
||||
doc.Strings.SetString("dossier_zeichnungsebenen", json.dumps(z_list, ensure_ascii=False))
|
||||
# STATE_SYNC ans React-Panel pushen damit Eye-Icons matchen
|
||||
if updated_e or updated_z:
|
||||
try:
|
||||
self.send("STATE_SYNC", {
|
||||
"zeichnungsebenen": z_list,
|
||||
"ebenen": ebenen_list,
|
||||
})
|
||||
except Exception as ex:
|
||||
print("[EBENEN] STATE_SYNC push:", ex)
|
||||
except Exception as ex:
|
||||
print("[EBENEN] _apply_combination sync:", ex)
|
||||
try: doc.Views.Redraw()
|
||||
except Exception: pass
|
||||
print("[EBENEN] Kombination angewandt: {} Layer".format(n))
|
||||
|
||||
def _save_preset(self, name, layers):
|
||||
name = (name or "").strip()
|
||||
if not name: return
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
presets = self._load_presets(doc)
|
||||
clean = []
|
||||
for ls in (layers or []):
|
||||
lid = ls.get("id")
|
||||
if not lid: continue
|
||||
clean.append({
|
||||
"id": lid,
|
||||
"visible": bool(ls.get("visible", True)),
|
||||
"locked": bool(ls.get("locked", False)),
|
||||
})
|
||||
existing = next((p for p in presets if p.get("name") == name), None)
|
||||
if existing is not None:
|
||||
existing["layers"] = clean
|
||||
else:
|
||||
presets.append({"name": name, "layers": clean})
|
||||
self._store_presets(doc, presets)
|
||||
print("[EBENEN] Kombination '{}' gespeichert ({} Layer)".format(name, len(clean)))
|
||||
|
||||
def _save_current_as_preset(self, name):
|
||||
"""Speichert die aktuellen Eye-States (dossier_ebenen + dossier_zeichnungs-
|
||||
ebenen) als Preset — NICHT die berechneten doc.Layer.IsVisible-Werte.
|
||||
Sonst wuerde der z_mode/e_mode-Override (z.B. 'active' nur 1 Layer
|
||||
sichtbar) ins Preset einbacken und beim Apply nicht wieder restorbar
|
||||
sein.
|
||||
|
||||
layers (doc.Layer-Liste) wird parallel mitgespeichert fuer Kompat
|
||||
mit AUSSCHNITTE (das vom doc.Layer-State liest)."""
|
||||
name = (name or "").strip()
|
||||
if not name: return
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
# 1) doc.Layer state (Kompat mit AUSSCHNITTE)
|
||||
layers = []
|
||||
try:
|
||||
for layer in doc.Layers:
|
||||
if layer is None or layer.IsDeleted: continue
|
||||
layers.append({
|
||||
"id": str(layer.Id),
|
||||
"visible": bool(layer.IsVisible),
|
||||
"locked": bool(layer.IsLocked),
|
||||
})
|
||||
except Exception as ex:
|
||||
print("[EBENEN] _save_current_as_preset enum:", ex)
|
||||
# 2) Eye-States aus dossier_ebenen / dossier_zeichnungsebenen
|
||||
pe_state = []
|
||||
pz_state = []
|
||||
try:
|
||||
e_raw = doc.Strings.GetValue("dossier_ebenen")
|
||||
if e_raw:
|
||||
for e in (json.loads(e_raw) or []):
|
||||
if isinstance(e, dict) and e.get("code"):
|
||||
pe_state.append({
|
||||
"code": e["code"],
|
||||
"visible": bool(e.get("visible", True)),
|
||||
"locked": bool(e.get("locked", False)),
|
||||
})
|
||||
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
|
||||
if z_raw:
|
||||
for z in (json.loads(z_raw) or []):
|
||||
if isinstance(z, dict) and z.get("id"):
|
||||
pz_state.append({
|
||||
"id": z["id"],
|
||||
"visible": bool(z.get("visible", True)),
|
||||
})
|
||||
except Exception as ex:
|
||||
print("[EBENEN] _save_current_as_preset eye-states:", ex)
|
||||
presets = self._load_presets(doc)
|
||||
new_data = {
|
||||
"name": name,
|
||||
"layers": layers,
|
||||
"dossierEbenen": pe_state,
|
||||
"dossierZeichnungsebenen": pz_state,
|
||||
}
|
||||
existing = next((p for p in presets if p.get("name") == name), None)
|
||||
if existing is not None:
|
||||
existing.update(new_data)
|
||||
else:
|
||||
presets.append(new_data)
|
||||
self._store_presets(doc, presets)
|
||||
print("[EBENEN] '{}' gespeichert: {} Layer + {} Ebenen Eye-State".format(
|
||||
name, len(layers), len(pe_state)))
|
||||
|
||||
def _delete_preset(self, name):
|
||||
name = (name or "").strip()
|
||||
if not name: return
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
presets = [p for p in self._load_presets(doc) if p.get("name") != name]
|
||||
self._store_presets(doc, presets)
|
||||
print("[EBENEN] Kombination '{}' geloescht".format(name))
|
||||
|
||||
|
||||
def _ebenen_bridge_factory():
|
||||
bridge = EbenenBridge()
|
||||
_install_layer_listener(bridge)
|
||||
return bridge
|
||||
|
||||
|
||||
def _install_layer_listener(bridge):
|
||||
"""Reagiert auf externe Aenderungen in Rhinos Layer-Tabelle (Rename, Delete)."""
|
||||
if sc.sticky.get("ebenen_layer_listener"):
|
||||
sc.sticky["ebenen_bridge_ref"] = bridge
|
||||
return
|
||||
sc.sticky["ebenen_bridge_ref"] = bridge
|
||||
|
||||
def on_layer_event(sender, args):
|
||||
if _is_processing():
|
||||
return
|
||||
try:
|
||||
doc = args.Document
|
||||
evt = args.EventType
|
||||
# Nur Modify-Events interessieren uns (Rename, Color etc.)
|
||||
if evt != Rhino.DocObjects.Tables.LayerTableEventType.Modified:
|
||||
return
|
||||
idx = args.LayerIndex
|
||||
if idx < 0 or idx >= doc.Layers.Count:
|
||||
return
|
||||
layer = doc.Layers[idx]
|
||||
dossier_id = layer.GetUserString("dossier_id")
|
||||
dossier_code = layer.GetUserString("dossier_code")
|
||||
if not (dossier_id or dossier_code):
|
||||
return
|
||||
updated = False
|
||||
if dossier_id:
|
||||
raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
|
||||
if raw:
|
||||
try:
|
||||
z_list = json.loads(raw)
|
||||
for z in z_list:
|
||||
if z.get("id") == dossier_id and z.get("name") != layer.Name:
|
||||
z["name"] = layer.Name
|
||||
updated = True
|
||||
break
|
||||
if updated:
|
||||
doc.Strings.SetString("dossier_zeichnungsebenen", json.dumps(z_list, ensure_ascii=False))
|
||||
except Exception:
|
||||
pass
|
||||
elif dossier_code:
|
||||
raw = doc.Strings.GetValue("dossier_ebenen")
|
||||
if raw:
|
||||
try:
|
||||
e_list = json.loads(raw)
|
||||
# Layer-Name ist "CC_NAME" — wir extrahieren NAME
|
||||
if "_" in layer.Name:
|
||||
new_name = layer.Name.split("_", 1)[1]
|
||||
for e in e_list:
|
||||
if e.get("code") == dossier_code and e.get("name") != new_name:
|
||||
e["name"] = new_name
|
||||
updated = True
|
||||
break
|
||||
if updated:
|
||||
doc.Strings.SetString("dossier_ebenen", json.dumps(e_list, ensure_ascii=False))
|
||||
except Exception:
|
||||
pass
|
||||
if updated:
|
||||
b = sc.sticky.get("ebenen_bridge_ref")
|
||||
if b is not None:
|
||||
try:
|
||||
b._on_ready() # sendet aktualisiertes STATE_SYNC
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as ex:
|
||||
print("[EBENEN] Layer-Event:", ex)
|
||||
|
||||
Rhino.RhinoDoc.LayerTableEvent += on_layer_event
|
||||
sc.sticky["ebenen_layer_listener"] = True
|
||||
print("[EBENEN] Layer-Listener aktiv")
|
||||
|
||||
|
||||
panel_base.register_and_open("ebenen", "EBENEN", PANEL_GUID_STR, _ebenen_bridge_factory,
|
||||
icon_spec=("E", "#3a6fa8"))
|
||||
Reference in New Issue
Block a user