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,708 @@
|
||||
# ! python3
|
||||
# -*- 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"))
|
||||
|
||||
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"))
|
||||
_apply_layers_global(doc, snap.get("layers", []))
|
||||
_apply_dossier_state(doc, snap.get("dossier") or snap.get("pause") or {})
|
||||
# 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()
|
||||
|
||||
|
||||
panel_base.register_and_open("ausschnitte", "AUSSCHNITTE", PANEL_GUID_STR, AusschnittBridge,
|
||||
icon_spec=("A", "#c87050"))
|
||||
Reference in New Issue
Block a user