diff --git a/rhino/kamera.py b/rhino/kamera.py index 6fda821..29faa77 100644 --- a/rhino/kamera.py +++ b/rhino/kamera.py @@ -23,6 +23,112 @@ import panel_base PANEL_GUID_STR = "9c3f8a2d-7b4e-4a6f-9c12-1d4e5f6a7b89" _PRESETS_KEY = "dossier_kamera_presets" +_NORTH_KEY = "dossier_north_angle" # Grad im Uhrzeigersinn von +Y + + +# --------------------------------------------------------------------------- +# Norden-Rotation: Default +Y = Norden. Bei rotierten Projekten (Site-Plaene, +# swissBUILDINGS in LV95-Orientierung) kann der User einen Winkel definieren. +# Wirkt auf N/O/S/W-View-Buttons + (optional) Iso-Octanten. + +def get_north_angle(doc): + if doc is None: return 0.0 + try: + v = doc.Strings.GetValue(_NORTH_KEY) + return float(v) if v else 0.0 + except Exception: + return 0.0 + + +def set_north_angle(doc, angle): + if doc is None: return + try: + a = float(angle) % 360.0 + doc.Strings.SetString(_NORTH_KEY, "{:.3f}".format(a)) + except Exception as ex: + print("[KAMERA] set north:", ex) + + +def _scene_target_and_diag(doc): + """Centroid der Szenen-BBox + Diagonal-Laenge. Fallback (0,0,0)/50m.""" + 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 + return target, diag + + +def set_cardinal_view(vp, cardinal): + """Setzt N/O/S/W-Ansicht unter Beruecksichtigung der Norden-Rotation. + cardinal: 'N' | 'O' | 'S' | 'W'.""" + if vp is None: return + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: return + try: + north_deg = get_north_angle(doc) + # Norden-Einheitsvektor (Uhrzeigersinn-Rotation von +Y aus Top-View): + # angle=0 → (0,1,0); angle=90 → (1,0,0); angle=180 → (0,-1,0). + nrad = math.radians(north_deg) + north = (math.sin(nrad), math.cos(nrad)) + east = (math.cos(nrad), -math.sin(nrad)) + c = cardinal.upper() + if c == "N": dx, dy = north + elif c == "S": dx, dy = -north[0], -north[1] + elif c == "O" or c == "E": dx, dy = east + elif c == "W": dx, dy = -east[0], -east[1] + else: return + + target, diag = _scene_target_and_diag(doc) + dist = diag * 1.6 + # Kamera auf Hoehe des Target-Z (echte Elevation, horizontal blickend) + loc = rg.Point3d(target.X + dx * dist, + target.Y + dy * dist, + target.Z) + if not vp.IsParallelProjection: + vp.ChangeToParallelProjection(True) + vp.SetCameraLocations(target, loc) + # Camera-Up muss +Z sein (sonst kippt die Ansicht) + try: vp.CameraUp = rg.Vector3d.ZAxis + except Exception: pass + try: vp.ZoomExtents() + except Exception: pass + Rhino.RhinoDoc.ActiveDoc.Views.ActiveView.Redraw() + except Exception as ex: + print("[KAMERA] set cardinal:", ex) + + +def set_top_view(vp): + """Plan-Ansicht (oben), rotiert nach Norden-Setting.""" + if vp is None: return + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: return + try: + target, diag = _scene_target_and_diag(doc) + loc = rg.Point3d(target.X, target.Y, target.Z + diag * 2) + if not vp.IsParallelProjection: + vp.ChangeToParallelProjection(True) + vp.SetCameraLocations(target, loc) + # Plan-Norden zeigt nach oben im Viewport → Up-Vector = north + north_deg = get_north_angle(doc) + nrad = math.radians(north_deg) + try: + vp.CameraUp = rg.Vector3d(math.sin(nrad), math.cos(nrad), 0) + except Exception: pass + try: vp.ZoomExtents() + except Exception: pass + Rhino.RhinoDoc.ActiveDoc.Views.ActiveView.Redraw() + except Exception as ex: + print("[KAMERA] set top:", ex) def _read_viewport(vp): @@ -128,39 +234,26 @@ def _set_viewport(vp, data): 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.""" + """Setzt eine standard-architektonische Iso-Ansicht (35.26° vertikal, + 45° horizontal). octant: 'NE' | 'NW' | 'SE' | 'SW' — die XY-Diagonale + relativ zum Norden des Projekts (= rotiert mit dossier_north_angle).""" if vp is None: return + doc = Rhino.RhinoDoc.ActiveDoc + if doc 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 + target, diag = _scene_target_and_diag(doc) + # Octant-Basisvektor (vor Norden-Rotation): NE = (+1,+1) + sign_e = 1 if "E" in octant.upper() else -1 + sign_n = 1 if "N" in octant.upper() else -1 + north_deg = get_north_angle(doc) + nrad = math.radians(north_deg) + # Norden-Vektor + Osten-Vektor im Welt-Koordinatensystem + north_vec = (math.sin(nrad), math.cos(nrad)) + east_vec = (math.cos(nrad), -math.sin(nrad)) + # Iso-XY-Richtung = sign_e * east + sign_n * north + dx = sign_e * east_vec[0] + sign_n * north_vec[0] + dy = sign_e * east_vec[1] + sign_n * north_vec[1] + dz = 1.0 L = math.sqrt(dx*dx + dy*dy + dz*dz) dist = diag * 1.6 loc = rg.Point3d(target.X + dx/L * dist, @@ -197,8 +290,9 @@ def _payload(): doc = Rhino.RhinoDoc.ActiveDoc vp = _active_viewport() return { - "viewport": _read_viewport(vp), - "presets": _load_presets(doc), + "viewport": _read_viewport(vp), + "presets": _load_presets(doc), + "northAngle": get_north_angle(doc), } @@ -229,6 +323,14 @@ class KameraBridge(panel_base.BaseBridge): vp = _active_viewport() _set_iso(vp, p.get("octant") or "NE") self._send_state() + elif t == "SET_NORTH_ANGLE": + set_north_angle(Rhino.RhinoDoc.ActiveDoc, p.get("angle") or 0) + self._send_state() + # Topbar refreshen damit dort der neue Winkel sichtbar ist + try: + b = sc.sticky.get("oberleiste_bridge") + if b is not None: b._send_state(force=True) + except Exception: pass elif t == "SET_PROJECTION": vp = _active_viewport() if vp is not None: diff --git a/rhino/oberleiste.py b/rhino/oberleiste.py index 63b7268..bee7e91 100644 --- a/rhino/oberleiste.py +++ b/rhino/oberleiste.py @@ -875,20 +875,40 @@ class OberleisteBridge(panel_base.BaseBridge): # --- View-Switcher ---------------------------------------------- elif t == "SET_VIEW": v = p.get("view") - if v in ("Top", "Front", "Right", "Perspective", "Left", "Back", "Bottom"): + try: + import kamera + vp = kamera._active_viewport() + except Exception: + vp = None + if v == "Top": + # Plan rotiert mit Norden — Up-Vektor zeigt Norden + try: + import kamera + kamera.set_top_view(vp) + except Exception as ex: + print("[OBERLEISTE] top:", ex) + self._send_state(force=True) + elif v == "Perspective": _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) + elif v in ("N", "O", "S", "W"): + try: + import kamera + kamera.set_cardinal_view(vp, v) + except Exception as ex: + print("[OBERLEISTE] cardinal:", ex) + self._send_state(force=True) + elif v in ("Front", "Right", "Left", "Back", "Bottom"): + # Legacy direkte Rhino-Views (falls noch wo benutzt) + _run("_-{} _Enter".format(v)) + self._send_state(force=True) # --- Kamera-Panel oeffnen --------------------------------------- elif t == "OPEN_KAMERA_PANEL": @@ -1177,6 +1197,12 @@ class OberleisteBridge(panel_base.BaseBridge): self._fonts_sent = True except Exception: info["textSettings"] = {} + # Norden-Rotation fuer N/O/S/W-Buttons + try: + import kamera + info["northAngle"] = kamera.get_north_angle(doc) + except Exception: + info["northAngle"] = 0 # Command-Line State prompt = _get_command_prompt() info["cmdPrompt"] = prompt diff --git a/src/KameraApp.jsx b/src/KameraApp.jsx index f3966f9..acb1c72 100644 --- a/src/KameraApp.jsx +++ b/src/KameraApp.jsx @@ -4,6 +4,7 @@ import { onMessage, notifyReady, setKameraViewport, setKameraProjection, setKameraIso, kameraZoomExtents, saveKameraPreset, applyKameraPreset, deleteKameraPreset, + setKameraNorthAngle, } from './lib/rhinoBridge' const labelXs = { @@ -76,11 +77,13 @@ export default function KameraApp() { const [vp, setVp] = useState(null) const [presets, setPresets] = useState([]) const [presetName, setPresetName] = useState('') + const [northAngle, setNorthAngleState] = useState(0) useEffect(() => { onMessage('STATE', (s) => { setVp(s.viewport || null) setPresets(s.presets || []) + if (typeof s.northAngle === 'number') setNorthAngleState(s.northAngle) }) notifyReady() }, []) @@ -142,6 +145,38 @@ export default function KameraApp() { + {/* Plan-Norden — Rotations-Winkel im Uhrzeigersinn von +Y */} +