Kamera-Panel + Iso-Button in der Oberleiste
Oberleiste: - View-Gruppe: Iso-Button neu zwischen Right und Persp - matchView: Iso = parallel ohne orthogonalen Standard-Namen, Perspektive = !parallel — beide via Projektions-Flag unterschieden (Rhino-Viewport-Name ist oft "Perspective" fuer beide) - Camera-Knopf (Icon: videocam) oeffnet das neue Kamera-Panel - SET_VIEW Backend: 'Iso' faelltt auf kamera._set_iso(vp, "NE") - OPEN_KAMERA_PANEL Handler Kamera-Panel (neu — rhino/kamera.py + src/KameraApp.jsx): - Viewport-Name + Projektions-Toggle (Persp/Parallel) - 4 Iso-Quick-Buttons (NW/NE/SE/SW) — true-iso 35°/45°, Kamera-Distanz auto aus Szenen-BBox - Vec3-Felder fuer Kamera-Position + Blick-Ziel (numerisch editierbar, m) - Distanz read-only - Brennweite (mm) bei Persp, Frustum-Breite (m) bei Parallel - Zoom-Extents-Button - Presets: speichern + anwenden + loeschen, persistiert in doc.Strings["dossier_kamera_presets"] (JSON) - Eto-Form-Satelliten-Fenster (420x600) via panel_base.open_satellite_window Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+290
@@ -0,0 +1,290 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
kamera.py
|
||||
Kamera-Panel: liest/setzt Viewport-Kamera (Position, Target, Projektion,
|
||||
FOV/Frustum, Linse). Persistiert Presets in doc.Strings unter
|
||||
`dossier_kamera_presets` (JSON-Liste).
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import math
|
||||
import uuid
|
||||
import Rhino
|
||||
import Rhino.Geometry as rg
|
||||
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 = "9c3f8a2d-7b4e-4a6f-9c12-1d4e5f6a7b89"
|
||||
_PRESETS_KEY = "dossier_kamera_presets"
|
||||
|
||||
|
||||
def _read_viewport(vp):
|
||||
"""Liest den aktuellen Zustand eines Viewports als dict."""
|
||||
if vp is None: return None
|
||||
try:
|
||||
loc = vp.CameraLocation
|
||||
tgt = vp.CameraTarget
|
||||
is_par = bool(vp.IsParallelProjection)
|
||||
# FOV in Grad (vertical) — gilt nur fuer Perspective
|
||||
try:
|
||||
fov_v = float(vp.Camera35mmLensLength) # Linsen-Brennweite mm
|
||||
except Exception:
|
||||
fov_v = 50.0
|
||||
# Frustum-Breite (m bei m-Doc) — gilt nur fuer Parallel
|
||||
try:
|
||||
f = vp.GetFrustum()
|
||||
# GetFrustum returns (bool ok, l, r, b, t, n, f)
|
||||
ok = bool(f[0]) if isinstance(f, tuple) else False
|
||||
if ok:
|
||||
_l = f[1]; _r = f[2]
|
||||
frustum_w = abs(_r - _l)
|
||||
else:
|
||||
frustum_w = 0.0
|
||||
except Exception:
|
||||
frustum_w = 0.0
|
||||
# Distanz Kamera→Target
|
||||
dx = loc.X - tgt.X; dy = loc.Y - tgt.Y; dz = loc.Z - tgt.Z
|
||||
dist = math.sqrt(dx*dx + dy*dy + dz*dz)
|
||||
return {
|
||||
"name": vp.Name or "",
|
||||
"parallel": is_par,
|
||||
"loc": [loc.X, loc.Y, loc.Z],
|
||||
"target": [tgt.X, tgt.Y, tgt.Z],
|
||||
"lensMm": fov_v,
|
||||
"frustumW": frustum_w,
|
||||
"distance": dist,
|
||||
}
|
||||
except Exception as ex:
|
||||
print("[KAMERA] read viewport:", ex)
|
||||
return None
|
||||
|
||||
|
||||
def _active_viewport():
|
||||
try:
|
||||
v = Rhino.RhinoDoc.ActiveDoc.Views.ActiveView
|
||||
return v.ActiveViewport if v else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _set_viewport(vp, data):
|
||||
"""Schreibt Position/Target/Projektion/Linse zurueck."""
|
||||
if vp is None or not isinstance(data, dict): return
|
||||
try:
|
||||
# Projektion zuerst — danach Frustum/Linse konsistent
|
||||
if "parallel" in data:
|
||||
par = bool(data.get("parallel"))
|
||||
if par != bool(vp.IsParallelProjection):
|
||||
if par: vp.ChangeToParallelProjection(True)
|
||||
else: vp.ChangeToPerspectiveProjection(True, 50.0)
|
||||
loc_cur = vp.CameraLocation
|
||||
tgt_cur = vp.CameraTarget
|
||||
loc_arr = data.get("loc")
|
||||
tgt_arr = data.get("target")
|
||||
if isinstance(loc_arr, list) and len(loc_arr) == 3:
|
||||
loc_cur = rg.Point3d(float(loc_arr[0]), float(loc_arr[1]), float(loc_arr[2]))
|
||||
if isinstance(tgt_arr, list) and len(tgt_arr) == 3:
|
||||
tgt_cur = rg.Point3d(float(tgt_arr[0]), float(tgt_arr[1]), float(tgt_arr[2]))
|
||||
vp.SetCameraLocations(tgt_cur, loc_cur)
|
||||
# Linse / Frustum
|
||||
if not vp.IsParallelProjection and "lensMm" in data:
|
||||
try:
|
||||
lens = float(data.get("lensMm") or 50.0)
|
||||
vp.Camera35mmLensLength = lens
|
||||
except Exception: pass
|
||||
if vp.IsParallelProjection and "frustumW" in data:
|
||||
try:
|
||||
w = float(data.get("frustumW") or 0.0)
|
||||
if w > 0:
|
||||
# Rhino hat keine direkte SetFrustumWidth — wir nutzen
|
||||
# SetViewProjection-Hack via SetFrustum (l, r, b, t, n, f)
|
||||
cur = vp.GetFrustum()
|
||||
if isinstance(cur, tuple) and len(cur) >= 7 and cur[0]:
|
||||
l_cur, r_cur, b_cur, t_cur, n_cur, f_cur = cur[1], cur[2], cur[3], cur[4], cur[5], cur[6]
|
||||
cur_w = abs(r_cur - l_cur)
|
||||
if cur_w > 1e-9:
|
||||
ratio = w / cur_w
|
||||
mid_lr = (l_cur + r_cur) / 2.0
|
||||
mid_bt = (b_cur + t_cur) / 2.0
|
||||
half_w = w / 2.0
|
||||
half_h = abs(t_cur - b_cur) / 2.0 * ratio
|
||||
vp.SetFrustum(mid_lr - half_w, mid_lr + half_w,
|
||||
mid_bt - half_h, mid_bt + half_h,
|
||||
n_cur, f_cur)
|
||||
except Exception as ex:
|
||||
print("[KAMERA] frustum set:", ex)
|
||||
try:
|
||||
Rhino.RhinoDoc.ActiveDoc.Views.ActiveView.Redraw()
|
||||
except Exception: pass
|
||||
except Exception as ex:
|
||||
print("[KAMERA] set viewport:", ex)
|
||||
|
||||
|
||||
def _set_iso(vp, octant="NE"):
|
||||
"""Setzt eine standard-architektonische Iso-Ansicht.
|
||||
octant: 'NE' | 'NW' | 'SE' | 'SW' — die XY-Diagonale aus der die Kamera blickt.
|
||||
Winkel: 35.264° (true iso) vertikal, 45° horizontal."""
|
||||
if vp is None: return
|
||||
try:
|
||||
# Aktuelle Szenen-BBox als Target-Mitte
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
bb = doc.Objects.GetSelectedObjects(False, False)
|
||||
# Falls keine Selektion: ganze Szene
|
||||
try:
|
||||
bbox = doc.Objects.BoundingBoxCorners
|
||||
except Exception:
|
||||
bbox = None
|
||||
target = rg.Point3d(0, 0, 0)
|
||||
diag = 50.0
|
||||
try:
|
||||
scene_bb = None
|
||||
for obj in doc.Objects:
|
||||
if obj is None: continue
|
||||
gb = obj.Geometry.GetBoundingBox(True)
|
||||
if not gb.IsValid: continue
|
||||
if scene_bb is None: scene_bb = gb
|
||||
else: scene_bb.Union(gb)
|
||||
if scene_bb is not None and scene_bb.IsValid:
|
||||
target = scene_bb.Center
|
||||
diag = max(scene_bb.Diagonal.Length, 10.0)
|
||||
except Exception: pass
|
||||
|
||||
sign_x = 1 if "E" in octant.upper() else -1
|
||||
sign_y = 1 if "N" in octant.upper() else -1
|
||||
|
||||
# True iso direction: (sign_x, sign_y, 1) normalized * distance
|
||||
dx = sign_x; dy = sign_y; dz = 1.0
|
||||
L = math.sqrt(dx*dx + dy*dy + dz*dz)
|
||||
dist = diag * 1.6
|
||||
loc = rg.Point3d(target.X + dx/L * dist,
|
||||
target.Y + dy/L * dist,
|
||||
target.Z + dz/L * dist)
|
||||
if not vp.IsParallelProjection:
|
||||
vp.ChangeToParallelProjection(True)
|
||||
vp.SetCameraLocations(target, loc)
|
||||
try: vp.ZoomExtents()
|
||||
except Exception: pass
|
||||
Rhino.RhinoDoc.ActiveDoc.Views.ActiveView.Redraw()
|
||||
except Exception as ex:
|
||||
print("[KAMERA] set iso:", ex)
|
||||
|
||||
|
||||
def _load_presets(doc):
|
||||
try:
|
||||
raw = doc.Strings.GetValue(_PRESETS_KEY)
|
||||
if not raw: return []
|
||||
items = json.loads(raw)
|
||||
return items if isinstance(items, list) else []
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def _save_presets(doc, items):
|
||||
try:
|
||||
doc.Strings.SetString(_PRESETS_KEY, json.dumps(items))
|
||||
except Exception as ex:
|
||||
print("[KAMERA] save presets:", ex)
|
||||
|
||||
|
||||
def _payload():
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
vp = _active_viewport()
|
||||
return {
|
||||
"viewport": _read_viewport(vp),
|
||||
"presets": _load_presets(doc),
|
||||
}
|
||||
|
||||
|
||||
class KameraBridge(panel_base.BaseBridge):
|
||||
def __init__(self):
|
||||
panel_base.BaseBridge.__init__(self, "kamera")
|
||||
|
||||
def _on_ready(self):
|
||||
self._send_state()
|
||||
|
||||
def _send_state(self):
|
||||
self.send("STATE", _payload())
|
||||
|
||||
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" or t == "REQUEST_STATE":
|
||||
self._on_ready()
|
||||
elif t == "SET_VIEWPORT":
|
||||
vp = _active_viewport()
|
||||
_set_viewport(vp, p)
|
||||
self._send_state()
|
||||
elif t == "SET_ISO":
|
||||
vp = _active_viewport()
|
||||
_set_iso(vp, p.get("octant") or "NE")
|
||||
self._send_state()
|
||||
elif t == "SET_PROJECTION":
|
||||
vp = _active_viewport()
|
||||
if vp is not None:
|
||||
par = bool(p.get("parallel"))
|
||||
if par: vp.ChangeToParallelProjection(True)
|
||||
else: vp.ChangeToPerspectiveProjection(True, 50.0)
|
||||
try: Rhino.RhinoDoc.ActiveDoc.Views.ActiveView.Redraw()
|
||||
except Exception: pass
|
||||
self._send_state()
|
||||
elif t == "ZOOM_EXTENTS":
|
||||
vp = _active_viewport()
|
||||
if vp is not None:
|
||||
try: vp.ZoomExtents()
|
||||
except Exception: pass
|
||||
try: Rhino.RhinoDoc.ActiveDoc.Views.ActiveView.Redraw()
|
||||
except Exception: pass
|
||||
self._send_state()
|
||||
elif t == "SAVE_PRESET":
|
||||
name = (p.get("name") or "").strip() or "Preset"
|
||||
vp = _active_viewport()
|
||||
state = _read_viewport(vp)
|
||||
if state is None: return
|
||||
items = _load_presets(doc)
|
||||
entry = {
|
||||
"id": "kp_" + uuid.uuid4().hex[:8],
|
||||
"name": name,
|
||||
"parallel": state["parallel"],
|
||||
"loc": state["loc"],
|
||||
"target": state["target"],
|
||||
"lensMm": state["lensMm"],
|
||||
"frustumW": state["frustumW"],
|
||||
}
|
||||
items.append(entry)
|
||||
_save_presets(doc, items)
|
||||
self._send_state()
|
||||
elif t == "APPLY_PRESET":
|
||||
pid = p.get("id")
|
||||
items = _load_presets(doc)
|
||||
entry = next((x for x in items if x.get("id") == pid), None)
|
||||
if entry is None: return
|
||||
vp = _active_viewport()
|
||||
_set_viewport(vp, entry)
|
||||
self._send_state()
|
||||
elif t == "DELETE_PRESET":
|
||||
pid = p.get("id")
|
||||
items = [x for x in _load_presets(doc) if x.get("id") != pid]
|
||||
_save_presets(doc, items)
|
||||
self._send_state()
|
||||
|
||||
|
||||
def open_as_window():
|
||||
"""Oeffnet KAMERA als Eto-Form + WebView (analog Overrides)."""
|
||||
b = KameraBridge()
|
||||
sc.sticky["kamera_bridge"] = b
|
||||
panel_base.open_satellite_window(
|
||||
"kamera",
|
||||
title="Kamera",
|
||||
size=(420, 600),
|
||||
bridge=b)
|
||||
@@ -878,6 +878,25 @@ class OberleisteBridge(panel_base.BaseBridge):
|
||||
if v in ("Top", "Front", "Right", "Perspective", "Left", "Back", "Bottom"):
|
||||
_run("_-{} _Enter".format(v))
|
||||
self._send_state(force=True)
|
||||
elif v == "Iso":
|
||||
# Standard-Architektur-Isometrie aus NE, true-iso 35°/45°.
|
||||
# Implementiert in kamera.py — Logik dort, damit Kamera-
|
||||
# Panel und Topbar dieselbe Berechnung teilen.
|
||||
try:
|
||||
import kamera
|
||||
vp = kamera._active_viewport()
|
||||
kamera._set_iso(vp, "NE")
|
||||
except Exception as ex:
|
||||
print("[OBERLEISTE] iso:", ex)
|
||||
self._send_state(force=True)
|
||||
|
||||
# --- Kamera-Panel oeffnen ---------------------------------------
|
||||
elif t == "OPEN_KAMERA_PANEL":
|
||||
try:
|
||||
import kamera
|
||||
kamera.open_as_window()
|
||||
except Exception as ex:
|
||||
print("[OBERLEISTE] open kamera:", ex)
|
||||
|
||||
# --- Display-Mode -----------------------------------------------
|
||||
elif t == "SET_DISPLAY_MODE":
|
||||
|
||||
Reference in New Issue
Block a user