diff --git a/rhino/kamera.py b/rhino/kamera.py new file mode 100644 index 0000000..6fda821 --- /dev/null +++ b/rhino/kamera.py @@ -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) diff --git a/rhino/oberleiste.py b/rhino/oberleiste.py index 317ad41..643f5fd 100644 --- a/rhino/oberleiste.py +++ b/rhino/oberleiste.py @@ -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": diff --git a/src/KameraApp.jsx b/src/KameraApp.jsx new file mode 100644 index 0000000..f3966f9 --- /dev/null +++ b/src/KameraApp.jsx @@ -0,0 +1,284 @@ +import { useState, useEffect } from 'react' +import Icon from './components/Icon' +import { + onMessage, notifyReady, + setKameraViewport, setKameraProjection, setKameraIso, + kameraZoomExtents, saveKameraPreset, applyKameraPreset, deleteKameraPreset, +} from './lib/rhinoBridge' + +const labelXs = { + fontSize: 9, color: 'var(--text-muted)', + textTransform: 'uppercase', letterSpacing: '0.06em', + fontWeight: 600, +} + +function NumberField({ label, value, onCommit, suffix, step = 0.1 }) { + const [draft, setDraft] = useState(value != null ? value.toFixed(3) : '') + useEffect(() => { + setDraft(value != null ? value.toFixed(3) : '') + }, [value]) + const commit = () => { + const n = parseFloat(draft) + if (!isNaN(n)) onCommit(n) + else setDraft(value != null ? value.toFixed(3) : '') + } + return ( +