Files
DOSSIER/rhino/ausschnitte.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

858 lines
33 KiB
Python

#! python 3
# -*- coding: utf-8 -*-
"""
ausschnitte.py
AUSSCHNITTE-Panel: speichert Viewport-Ausschnitte mit Kamera, Display-Mode,
Layer-Sichtbarkeit, DOSSIER-State. Anwendbar im Model-Space und auf Layout-Details.
"""
import os
import sys
import math
import json
import uuid
import Rhino
import System
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
PANEL_GUID_STR = "5c9e4f3f-6d0e-4f1f-b3c4-d5e6f7081a2b"
_STORE_KEY = "dossier_ausschnitte"
_FOLDERS_KEY = "dossier_ausschnitt_folders"
_PRESETS_KEY = "dossier_layer_presets"
def _orientation_from_camera(loc, tgt, parallel=True):
"""Bestimmt Orientierung:
- perspective = perspektivische Projektion (kein orthogonaler Schnitt)
- horizontal = parallele Projektion mit Blick weitgehend nach unten/oben (Grundriss)
- vertical = parallele Projektion mit Blick weitgehend seitlich (Schnitt/Ansicht)
"""
if not parallel:
return "perspective"
try:
dx = tgt[0] - loc[0]
dy = tgt[1] - loc[1]
dz = tgt[2] - loc[2]
m = math.sqrt(dx * dx + dy * dy + dz * dz)
if m <= 0: return "vertical"
return "horizontal" if (abs(dz) / m) > 0.7 else "vertical"
except Exception:
return "vertical"
def _parse_scale(scale_str):
"""Parse '1:50' / '1=50' / '50' → (page, model). Gibt None zurueck wenn nicht parsebar."""
if not scale_str:
return None
s = scale_str.strip()
for sep in (":", "=", "/"):
if sep in s:
try:
a, b = s.split(sep, 1)
pa = float(a.strip())
pb = float(b.strip())
if pa > 0 and pb > 0:
return (pa, pb)
except Exception:
pass
break
# nur Zahl: 1:N angenommen
try:
n = float(s)
if n > 0:
return (1.0, n)
except Exception:
pass
return None
# --- Capture / Apply Helpers ------------------------------------------------
def _capture_camera(vp):
dm = vp.DisplayMode
dm_id = str(dm.Id) if dm else None
dm_nm = dm.LocalName if dm else None
loc = [vp.CameraLocation.X, vp.CameraLocation.Y, vp.CameraLocation.Z]
tgt = [vp.CameraTarget.X, vp.CameraTarget.Y, vp.CameraTarget.Z]
# Frustum-Breite mitsichern: bei Parallelprojektion bestimmt sie den Zoom.
# Ohne sie laesst sich der gespeicherte Ausschnitt nicht rekonstruieren.
frustum_w = None
frustum_h = None
try:
ok, l, r, b, t, n, f = vp.GetFrustum()
if ok:
frustum_w = float(r - l)
frustum_h = float(t - b)
except Exception:
pass
return {
"viewName": vp.Name,
"location": loc,
"target": tgt,
"up": [vp.CameraUp.X, vp.CameraUp.Y, vp.CameraUp.Z],
"lens": float(vp.Camera35mmLensLength) if vp.Camera35mmLensLength else 50.0,
"parallel": bool(vp.IsParallelProjection),
"displayMode": dm_id,
"displayModeName": dm_nm,
"orientation": _orientation_from_camera(loc, tgt, bool(vp.IsParallelProjection)),
"frustumWidth": frustum_w,
"frustumHeight": frustum_h,
}
def _apply_camera(vp, cam):
if not cam: return
try:
loc = Rhino.Geometry.Point3d(*cam["location"])
tgt = Rhino.Geometry.Point3d(*cam["target"])
up = Rhino.Geometry.Vector3d(*cam["up"]) if cam.get("up") else None
if cam.get("parallel"):
vp.ChangeToParallelProjection(True)
else:
vp.ChangeToPerspectiveProjection(True, float(cam.get("lens", 50)))
vp.SetCameraLocations(tgt, loc)
if up:
try: vp.CameraUp = up
except Exception: pass
dm_id = cam.get("displayMode")
if dm_id:
try:
mode = Rhino.Display.DisplayModeDescription.GetDisplayMode(System.Guid(dm_id))
if mode is not None: vp.DisplayMode = mode
except Exception:
pass
# Zoom (Frustum-Breite) rekonstruieren — nur bei Parallelprojektion sinnvoll.
# Bei Perspective bestimmt die Lens-Length den Bildausschnitt, dort
# waere ein Magnify kontraproduktiv.
fw = cam.get("frustumWidth")
if fw and fw > 0 and cam.get("parallel"):
try:
ok, l, r, b, t, n, f = vp.GetFrustum()
if ok:
cur_w = float(r - l)
if cur_w > 0:
factor = cur_w / float(fw)
if 1e-9 < factor < 1e9:
try:
vp.Magnify(float(factor), False)
except Exception:
try:
vp.Magnify(float(factor))
except Exception:
Rhino.RhinoApp.RunScript(
"_-Zoom _Factor {:.6f} _Enter".format(factor), False)
except Exception as ex:
print("[AUSSCHNITTE] Frustum-Apply:", ex)
except Exception as ex:
print("[AUSSCHNITTE] Camera-Apply:", ex)
def _capture_layers(doc):
out = []
for layer in doc.Layers:
if layer.IsDeleted: continue
out.append({
"id": str(layer.Id),
"visible": bool(layer.IsVisible),
"locked": bool(layer.IsLocked),
})
return out
def _apply_layers_global(doc, layers):
by_id = {}
for layer in doc.Layers:
if not layer.IsDeleted:
by_id[str(layer.Id)] = layer
for ls in layers:
layer = by_id.get(ls.get("id"))
if layer is None: continue
if layer.IsVisible != ls.get("visible", True):
layer.IsVisible = ls.get("visible", True)
if layer.IsLocked != ls.get("locked", False):
layer.IsLocked = ls.get("locked", False)
def _apply_layers_per_viewport(doc, layers, vp_id):
"""Setzt Sichtbarkeit pro Viewport (fuer Layout-Details)."""
by_id = {}
for layer in doc.Layers:
if not layer.IsDeleted:
by_id[str(layer.Id)] = layer
for ls in layers:
layer = by_id.get(ls.get("id"))
if layer is None: continue
try:
layer.SetPerViewportVisible(vp_id, ls.get("visible", True))
except Exception:
pass
_DOSSIER_KEYS = (
"dossier_zeichnungsebenen",
"dossier_ebenen",
"dossier_active_id",
"dossier_active_code",
)
def _capture_dossier_state(doc):
out = {}
for k in _DOSSIER_KEYS:
v = doc.Strings.GetValue(k)
if v is not None:
out[k] = v
return out
def _apply_dossier_state(doc, state):
for k, v in (state or {}).items():
if v is not None:
doc.Strings.SetString(k, v)
def _find_selected_detail(doc):
"""Sucht nach einem aktuell selektierten Detail-Viewport-Objekt."""
for obj in doc.Objects.GetSelectedObjects(False, False):
if isinstance(obj, Rhino.DocObjects.DetailViewObject):
return obj
return None
def _load_snapshots(doc):
"""Modul-interne Snapshot-Liste (cross-modul nutzbar)."""
raw = doc.Strings.GetValue(_STORE_KEY)
if not raw: return []
try:
data = json.loads(raw)
return data if isinstance(data, list) else []
except Exception:
return []
def apply_snapshot_to_detail(doc, detail, snap_id):
"""Wendet einen Ausschnitt auf ein konkretes Detail-Object an. Wird vom
Layouts-Modul benutzt, um Ausschnitt-Detail-Bindings zu synchronisieren.
Liefert True bei Erfolg."""
snap = next((s for s in _load_snapshots(doc) if s.get("id") == snap_id), None)
if not snap:
print("[AUSSCHNITTE] apply_to_detail: snap nicht gefunden", snap_id)
return False
# Page-View ermitteln (fuer SetActiveDetail/SetPageAsActive)
page_view = None
try:
for view in doc.Views:
if isinstance(view, Rhino.Display.RhinoPageView):
try:
if any(d.Id == detail.Id for d in view.GetDetailViews()):
page_view = view
break
except Exception:
continue
except Exception as ex:
print("[AUSSCHNITTE] page-view-suche:", ex)
# Detail muss aktiv sein, damit Kamera-Aenderungen anschlagen
was_active = False
try: was_active = detail.IsActive
except Exception: pass
if page_view is not None and not was_active:
try: page_view.SetActiveDetail(detail.Id)
except Exception as ex: print("[AUSSCHNITTE] SetActiveDetail:", ex)
# Kamera + Layer + Name
vp = detail.Viewport
_apply_camera(vp, snap.get("camera"))
_apply_layers_per_viewport(doc, snap.get("layers", []), vp.Id)
try:
new_name = snap.get("name")
if new_name and vp.Name != new_name:
vp.Name = new_name
except Exception as ex:
print("[AUSSCHNITTE] Detail-Rename:", ex)
# Massstab
ratio = _parse_scale(snap.get("scale", ""))
if ratio is not None:
page_v, model_v = ratio
for label, setter in (
("DetailGeometry.SetScale", lambda: detail.DetailGeometry.SetScale(model_v, page_v)),
("Detail.SetScale", lambda: detail.SetScale(model_v, page_v)),
):
try:
setter()
break
except Exception:
continue
# Commit + Deaktivieren
try: detail.CommitViewportChanges()
except Exception:
try: detail.CommitChanges()
except Exception: pass
if page_view is not None and not was_active:
try: page_view.SetPageAsActive()
except Exception: pass
try:
(page_view or doc.Views).Redraw()
except Exception:
doc.Views.Redraw()
print("[AUSSCHNITTE] '{}' auf Detail {} angewendet".format(snap.get("name"), detail.Id))
return True
# --- Bridge -----------------------------------------------------------------
class AusschnittBridge(panel_base.BaseBridge):
def __init__(self):
panel_base.BaseBridge.__init__(self, "ausschnitte")
def _on_ready(self):
self._send_list()
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 == "LIST": self._send_list()
elif t == "SAVE": self._save(p.get("name", "Ausschnitt"))
elif t == "UPDATE": self._update(p.get("id"))
elif t == "RESTORE": self._restore(p.get("id"))
elif t == "APPLY_TO_DETAIL":self._apply_to_detail(p.get("id"))
elif t == "RENAME": self._rename(p.get("id"), p.get("name"))
elif t == "DELETE": self._delete(p.get("id"))
elif t == "SET_FOLDER": self._set_field(p.get("id"), "folder", p.get("folder") or "")
elif t == "SET_SCALE": self._set_field(p.get("id"), "scale", p.get("scale") or "")
elif t == "DUPLICATE": self._duplicate(p.get("id"))
elif t == "ADD_FOLDER": self._add_folder(p.get("name"))
elif t == "REMOVE_FOLDER": self._remove_folder(p.get("name"))
elif t == "GET_LAYERS": self._send_layers(p.get("id"))
elif t == "UPDATE_LAYERS": self._update_layers(p.get("id"), p.get("layers") or [])
elif t == "SAVE_PRESET": self._save_preset(p.get("name"), p.get("layers") or [])
elif t == "DELETE_PRESET": self._delete_preset(p.get("name"))
elif t == "OPEN_SETTINGS": self._open_settings_window(p.get("id"))
def _load(self, doc):
raw = doc.Strings.GetValue(_STORE_KEY)
if not raw: return []
try:
data = json.loads(raw)
return data if isinstance(data, list) else []
except Exception:
return []
def _store(self, doc, snaps):
doc.Strings.SetString(_STORE_KEY, json.dumps(snaps, ensure_ascii=False))
def _load_folders(self, doc):
raw = doc.Strings.GetValue(_FOLDERS_KEY)
if not raw: return []
try:
data = json.loads(raw)
return data if isinstance(data, list) else []
except Exception:
return []
def _store_folders(self, doc, folders):
doc.Strings.SetString(_FOLDERS_KEY, json.dumps(folders, ensure_ascii=False))
def _load_presets(self, doc):
raw = doc.Strings.GetValue(_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):
doc.Strings.SetString(_PRESETS_KEY, json.dumps(presets, ensure_ascii=False))
def _send_list(self):
doc = Rhino.RhinoDoc.ActiveDoc
snaps = self._load(doc)
explicit_folders = self._load_folders(doc)
# Aus Snapshots zusaetzliche Ordner ableiten (falls Snap auf nicht-existenten Ordner zeigt)
for s in snaps:
f = s.get("folder", "")
if f and f not in explicit_folders:
explicit_folders.append(f)
slim = []
for s in snaps:
cam = s.get("camera", {}) or {}
orient = cam.get("orientation")
if not orient:
loc = cam.get("location") or [0, 0, 0]
tgt = cam.get("target") or [0, 0, 0]
orient = _orientation_from_camera(loc, tgt, bool(cam.get("parallel", True)))
slim.append({
"id": s.get("id"),
"name": s.get("name"),
"folder": s.get("folder", ""),
"scale": s.get("scale", ""),
"orientation": orient,
"displayModeName": cam.get("displayModeName"),
"parallel": cam.get("parallel", False),
})
preset_summary = [{"name": p.get("name"), "count": len(p.get("layers") or [])}
for p in self._load_presets(doc)]
self.send("LIST", {
"snapshots": slim,
"folders": explicit_folders,
"presets": preset_summary,
})
def _add_folder(self, name):
if not name: return
name = name.strip()
if not name: return
doc = Rhino.RhinoDoc.ActiveDoc
folders = self._load_folders(doc)
if name not in folders:
folders.append(name)
self._store_folders(doc, folders)
self._send_list()
def _remove_folder(self, name):
if not name: return
doc = Rhino.RhinoDoc.ActiveDoc
# Aus Folder-Liste entfernen
folders = [f for f in self._load_folders(doc) if f != name]
self._store_folders(doc, folders)
# Snapshots aus diesem Ordner herausnehmen (auf root)
snaps = self._load(doc)
for s in snaps:
if s.get("folder") == name:
s["folder"] = ""
self._store(doc, snaps)
self._send_list()
def _duplicate(self, snap_id):
if not snap_id: return
doc = Rhino.RhinoDoc.ActiveDoc
snaps = self._load(doc)
src = next((s for s in snaps if s.get("id") == snap_id), None)
if not src: return
# Tiefe Kopie via JSON
copy = json.loads(json.dumps(src, ensure_ascii=False))
copy["id"] = "snap_" + uuid.uuid4().hex[:8]
copy["name"] = (src.get("name", "Ausschnitt") + " Kopie")
# Direkt nach Original einfuegen
idx = snaps.index(src)
snaps.insert(idx + 1, copy)
self._store(doc, snaps)
self._send_list()
print("[AUSSCHNITTE] '{}' dupliziert".format(src.get("name")))
def _set_field(self, snap_id, field, value):
if not snap_id: return
doc = Rhino.RhinoDoc.ActiveDoc
snaps = self._load(doc)
for s in snaps:
if s.get("id") == snap_id:
s[field] = value
break
self._store(doc, snaps)
self._send_list()
def _capture(self, doc, name, existing_id=None, prior_scale=""):
view = doc.Views.ActiveView
if view is None:
print("[AUSSCHNITTE] Keine aktive View")
return None
vp = view.ActiveViewport
# Aktuelle Skala vom MASSSTAB-Modul holen — nur sinnvoll bei Parallel-
# projektion. In Perspective bleibt scale leer (Fallback: prior_scale).
scale_str = ""
try:
import massstab
# Bewusst der EINGESTELLTE Wert (User-Intent), nicht der live aus
# dem Viewport berechnete. Letzterer drifted bei Pan/Zoom.
ratio = massstab.get_applied_scale_ratio()
if ratio is not None and ratio > 0:
if ratio >= 10:
scale_str = "1:{:.0f}".format(ratio)
else:
scale_str = "1:{:.1f}".format(ratio)
except Exception as ex:
print("[AUSSCHNITTE] Live-Skala lesen:", ex)
# Fallback: wenn kein Massstab gepinnt war, die aus dem Frustum
# berechnete Live-Skala speichern. So bleibt das Massstab-Dropdown
# nach Restore konsistent (auch wenn der eigentliche Zoom-Restore
# bereits ueber frustumWidth in _apply_camera laeuft).
if not scale_str:
try:
import massstab
live = massstab.get_current_scale_ratio()
if live is not None and live > 0:
scale_str = "1:{:.0f}".format(live) if live >= 10 else "1:{:.1f}".format(live)
except Exception as ex:
print("[AUSSCHNITTE] Live-Skala (Fallback):", ex)
if not scale_str and prior_scale:
scale_str = prior_scale # Perspective -> alten Wert nicht ueberschreiben
return {
"id": existing_id or "snap_" + uuid.uuid4().hex[:8],
"name": name,
"scale": scale_str,
"camera": _capture_camera(vp),
"layers": _capture_layers(doc),
"dossier": _capture_dossier_state(doc),
}
def _save(self, name):
doc = Rhino.RhinoDoc.ActiveDoc
snap = self._capture(doc, name)
if snap is None: return
snaps = self._load(doc)
snaps.append(snap)
self._store(doc, snaps)
self._send_list()
print("[AUSSCHNITTE] '{}' gespeichert".format(name))
def _update(self, snap_id):
doc = Rhino.RhinoDoc.ActiveDoc
snaps = self._load(doc)
target = next((s for s in snaps if s.get("id") == snap_id), None)
if not target: return
updated = self._capture(doc, target.get("name", "Ausschnitt"),
existing_id=snap_id,
prior_scale=target.get("scale", ""))
if updated is None: return
for i, s in enumerate(snaps):
if s.get("id") == snap_id:
snaps[i] = updated
break
self._store(doc, snaps)
self._send_list()
print("[AUSSCHNITTE] '{}' aktualisiert".format(target.get("name")))
def _restore(self, snap_id):
doc = Rhino.RhinoDoc.ActiveDoc
snap = next((s for s in self._load(doc) if s.get("id") == snap_id), None)
if not snap: return
view = doc.Views.ActiveView
if view is None: return
vp = view.ActiveViewport
_apply_camera(vp, snap.get("camera"))
# Layer-Sichtbarkeit: bevorzugt die referenzierte Ebenenkombi (live —
# zeigt aktuelle Kombi-Definition). Fallback: snap.layers (per-snap
# eingefrorener Zustand).
kombi = (snap.get("layerCombination") or "").strip()
if kombi:
try:
import rhinopanel
rhinopanel.apply_layer_preset_by_name(doc, kombi)
except Exception as ex:
print("[AUSSCHNITTE] kombi-apply '{}':".format(kombi), ex)
_apply_layers_global(doc, snap.get("layers", []))
else:
_apply_layers_global(doc, snap.get("layers", []))
# Eigene Sichtbarkeit → active_comb_name clearen
try:
import rhinopanel
rhinopanel.set_active_comb_name(doc, None)
rhinopanel._notify_oberleiste_combs()
except Exception: pass
_apply_dossier_state(doc, snap.get("dossier") or snap.get("pause") or {})
# Overrides: nur anwenden wenn das Snap "applyOverrides" gesetzt hat.
# Sonst bleibt der aktuelle User-Override-State unangetastet.
if snap.get("applyOverrides"):
try:
import overrides
overrides.set_enabled(doc, bool(snap.get("overridesEnabled")))
overrides.set_active_preset(doc, snap.get("overridesPreset") or None)
# Oberleiste-Cache invalidieren damit Topbar das neue Preset zeigt
try:
b = sc.sticky.get("oberleiste_bridge")
if b is not None:
b._cached_overrides = None
b._send_state(force=True)
except Exception: pass
except Exception as ex:
print("[AUSSCHNITTE] overrides-apply:", ex)
# Viewport ZUERST umbenennen — der per-Viewport-Massstab in massstab.py
# wird unter vp.Name geschluesselt. Erst nach dem Rename schreibt
# _apply_scale unter dem neuen Namen, sonst landet der Wert beim alten
# Ausschnitt und der neue zeigt "1:?".
try:
new_name = snap.get("name")
if new_name and vp.Name != new_name:
vp.Name = new_name
except Exception as ex:
print("[AUSSCHNITTE] Rename:", ex)
# Gespeicherten Massstab anwenden (z.B. "1:50") — falls vorhanden und
# Viewport parallel ist (in Perspective ignoriert massstab._apply_scale).
try:
scale_str = (snap.get("scale") or "").strip()
if scale_str:
ratio = _parse_scale(scale_str)
if ratio:
_, model_v = ratio # (page=1, model=N) -> N
import massstab
massstab._apply_scale(doc, vp, float(model_v))
print("[AUSSCHNITTE] Massstab gesetzt auf 1:{} (applied={})".format(
model_v, massstab.get_applied_scale_ratio()))
# Andere Panels (Massstab, Oberleiste) sofort ueber den
# neuen appliedScale informieren — sonst zeigt das Dropdown
# noch den vorherigen Wert bis zum naechsten Idle-Tick mit
# Aenderung an der Live-Skala.
for key in ("massstab_bridge", "oberleiste_bridge"):
try:
b = sc.sticky.get(key)
print("[AUSSCHNITTE] force-send via {}: {}".format(key, "OK" if b is not None else "MISSING"))
if b is not None:
b._send_state(force=True)
except Exception as e:
print("[AUSSCHNITTE] force-send {} failed: {}".format(key, e))
except Exception as ex:
print("[AUSSCHNITTE] Massstab-Restore:", ex)
view.Redraw()
print("[AUSSCHNITTE] '{}' wiederhergestellt".format(snap.get("name")))
def _apply_to_detail(self, snap_id):
doc = Rhino.RhinoDoc.ActiveDoc
# 1) Detail aus Selektion oder aktiver PageView ermitteln
detail = _find_selected_detail(doc)
if detail is None:
try:
av = doc.Views.ActiveView
if isinstance(av, Rhino.Display.RhinoPageView):
for d in av.GetDetailViews():
if d.IsActive:
detail = d
break
except Exception as ex:
print("[AUSSCHNITTE] Active-Detail-Suche:", ex)
if detail is None:
print("[AUSSCHNITTE] Kein Detail ausgewaehlt — bitte:")
print(" 1) ins Layout wechseln")
print(" 2) Detail-Rahmen einmal anklicken (so dass er hervorgehoben ist)")
print(" 3) erneut 'Auf Detail anwenden' waehlen")
return
# 2) Delegieren an den oeffentlichen Helper
apply_snapshot_to_detail(doc, detail, snap_id)
def _send_layers(self, snap_id):
if not snap_id: return
doc = Rhino.RhinoDoc.ActiveDoc
snap = next((s for s in self._load(doc) if s.get("id") == snap_id), None)
if not snap:
print("[AUSSCHNITTE] Snap nicht gefunden:", snap_id)
return
snap_by_id = {}
for ls in (snap.get("layers") or []):
snap_by_id[ls.get("id")] = ls
layers = []
for layer in doc.Layers:
if layer.IsDeleted: continue
lid = str(layer.Id)
ls = snap_by_id.get(lid, {})
layers.append({
"id": lid,
"name": layer.Name,
"fullPath": layer.FullPath,
"color": "#%02x%02x%02x" % (layer.Color.R, layer.Color.G, layer.Color.B),
"visible": bool(ls.get("visible", layer.IsVisible)),
"locked": bool(ls.get("locked", layer.IsLocked)),
})
layers.sort(key=lambda x: x["fullPath"])
presets = self._load_presets(doc)
self.send("LAYERS_DATA", {
"id": snap_id,
"name": snap.get("name"),
"layers": layers,
"presets": presets,
})
def _update_layers(self, snap_id, layers):
if not snap_id: return
doc = Rhino.RhinoDoc.ActiveDoc
snaps = self._load(doc)
target = next((s for s in snaps if s.get("id") == snap_id), None)
if not target: return
new_list = []
for ls in layers:
lid = ls.get("id")
if not lid: continue
new_list.append({
"id": lid,
"visible": bool(ls.get("visible", True)),
"locked": bool(ls.get("locked", False)),
})
target["layers"] = new_list
self._store(doc, snaps)
self._send_list()
print("[AUSSCHNITTE] Ebenen-Sichtbarkeit von '{}' aktualisiert".format(target.get("name")))
def _save_preset(self, name, layers):
if not name: return
name = name.strip()
if not name: return
doc = Rhino.RhinoDoc.ActiveDoc
presets = self._load_presets(doc)
clean = []
for ls in layers:
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)
self._send_list()
print("[AUSSCHNITTE] Ebenenkombination '{}' gespeichert ({} Ebenen)".format(name, len(clean)))
def _delete_preset(self, name):
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)
self._send_list()
def _rename(self, snap_id, name):
if not snap_id or not name: return
doc = Rhino.RhinoDoc.ActiveDoc
snaps = self._load(doc)
for s in snaps:
if s.get("id") == snap_id:
s["name"] = name
break
self._store(doc, snaps)
self._send_list()
def _delete(self, snap_id):
if not snap_id: return
doc = Rhino.RhinoDoc.ActiveDoc
snaps = [s for s in self._load(doc) if s.get("id") != snap_id]
self._store(doc, snaps)
self._send_list()
def _open_settings_window(self, snap_id):
"""Oeffnet ein Satelliten-Fenster (Eto.Form + WebView) mit dem
Ausschnittseinstellungen-Dialog. Lets User editieren: Massstab,
Display-Mode, Overrides, Ebenenkombi."""
if not snap_id: return
doc = Rhino.RhinoDoc.ActiveDoc
snap = next((s for s in self._load(doc) if s.get("id") == snap_id), None)
if not snap:
print("[AUSSCHNITTE] open_settings: snap nicht gefunden", snap_id)
return
outer = self
bridge_holder = {"form": None, "id": snap_id}
def _payload():
d = Rhino.RhinoDoc.ActiveDoc
sn = next((s for s in outer._load(d) if s.get("id") == bridge_holder["id"]), None)
if sn is None: sn = {}
# Listen fuer Dropdowns
display_modes = []
try:
import oberleiste
display_modes = oberleiste._list_display_modes()
except Exception as ex:
print("[AUSSCHNITTE] display_modes:", ex)
overrides_presets = []
try:
import overrides
overrides_presets = [item.get("name") for item in overrides.list_presets() if item.get("name")]
except Exception as ex:
print("[AUSSCHNITTE] overrides_presets:", ex)
layer_kombis = []
try:
import rhinopanel
layer_kombis = rhinopanel.list_layer_preset_names(d)
except Exception as ex:
print("[AUSSCHNITTE] layer_kombis:", ex)
cam = sn.get("camera") or {}
return {
"snap": {
"id": sn.get("id"),
"name": sn.get("name"),
"scale": sn.get("scale", ""),
"displayMode": cam.get("displayMode"),
"displayModeName": cam.get("displayModeName"),
"applyOverrides": bool(sn.get("applyOverrides", False)),
"overridesEnabled": bool(sn.get("overridesEnabled", False)),
"overridesPreset": sn.get("overridesPreset") or "",
"layerCombination": sn.get("layerCombination") or "",
},
"displayModes": display_modes,
"overridesPresets": overrides_presets,
"layerKombis": layer_kombis,
}
def _persist(settings):
d = Rhino.RhinoDoc.ActiveDoc
snaps = outer._load(d)
sid = bridge_holder["id"]
target = next((s for s in snaps if s.get("id") == sid), None)
if target is None:
print("[AUSSCHNITTE] persist settings: snap weg"); return
# Massstab
sc_val = (settings.get("scale") or "").strip()
target["scale"] = sc_val
# Display Mode in camera nested
cam = target.get("camera") or {}
dm_id = settings.get("displayMode")
dm_nm = settings.get("displayModeName")
if dm_id is not None: cam["displayMode"] = dm_id or None
if dm_nm is not None: cam["displayModeName"] = dm_nm or None
target["camera"] = cam
# Overrides
target["applyOverrides"] = bool(settings.get("applyOverrides"))
target["overridesEnabled"] = bool(settings.get("overridesEnabled"))
target["overridesPreset"] = (settings.get("overridesPreset") or "").strip()
# Ebenenkombi
target["layerCombination"] = (settings.get("layerCombination") or "").strip()
outer._store(d, snaps)
outer._send_list()
print("[AUSSCHNITTE] Settings fuer '{}' aktualisiert".format(target.get("name")))
class _AusschnittSettingsBridge(panel_base.BaseBridge):
def __init__(self):
panel_base.BaseBridge.__init__(self, "ausschnitt_settings")
def _on_ready(self):
self._send_state()
def _send_state(self):
self.send("AUSSCHNITT_SETTINGS_STATE", _payload())
def handle(self, data):
if not isinstance(data, dict): return
t = data.get("type", "")
p = data.get("payload") or {}
if t == "READY":
self._on_ready()
elif t == "SAVE":
_persist(p.get("settings") or {})
try:
f = bridge_holder.get("form")
if f is not None: f.Close()
except Exception: pass
elif t == "CANCEL":
try:
f = bridge_holder.get("form")
if f is not None: f.Close()
except Exception: pass
b = _AusschnittSettingsBridge()
bridge_holder["form"] = panel_base.open_satellite_window(
"ausschnitt_settings",
params=_payload(),
title="Ausschnitt: {}".format(snap.get("name", "")),
size=(420, 540),
bridge=b)
panel_base.register_and_open("ausschnitte", "Ausschnitte", PANEL_GUID_STR, AusschnittBridge,
icon_spec=("crop", "#c87050"))