#! 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)