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 */} +
+ Plan-Norden (Rotation von +Y, im Uhrzeigersinn) +
+ { + const a = parseFloat(e.target.value) + if (!isNaN(a)) { + setNorthAngleState(a) + setKameraNorthAngle(a) + } + }} + style={{ flex: 1, fontSize: 12, padding: '4px 8px', + fontFamily: 'DM Mono, monospace' }} + /> + ° + +
+ + Norden = +Y bei 0°. Bei rotierten Projekten (z.B. swissBUILDINGS in + LV95-Orientierung oder Sonnenberechnungen) hier den Plan-Norden in + Grad eintragen. Wirkt auf TOP, ISO und N/O/S/W-Ansichten. + +
+ {/* Iso-Quick-Picker */}
Isometrie (Standard, true-iso 35°/45°) diff --git a/src/OberleisteApp.jsx b/src/OberleisteApp.jsx index 0552a02..832e081 100644 --- a/src/OberleisteApp.jsx +++ b/src/OberleisteApp.jsx @@ -22,12 +22,19 @@ const PRESETS = [ { value: 500, label: '1:500' }, { value: 1000, label: '1:1000' }, ] -const VIEWS = [ - { value: 'Top', icon: 'view_quilt', label: 'Top' }, - { value: 'Front', icon: 'north', label: 'Front' }, - { value: 'Right', icon: 'east', label: 'Right' }, - { value: 'Iso', icon: 'view_in_ar', label: 'Iso' }, - { value: 'Perspective', icon: '3d_rotation', label: 'Persp' }, +// Reihe 1: 3D-Ansichten (Top, Iso, Persp) + Kamera-Button +// Reihe 2: 4 Gebaeudeansichten (Norden/Osten/Sueden/Westen) — Buchstaben +// als Symbol, rotieren mit dossier_north_angle. +const VIEWS_ROW1 = [ + { value: 'Top', icon: 'crop_landscape', kind: 'icon' }, + { value: 'Iso', icon: 'view_in_ar', kind: 'icon' }, + { value: 'Perspective', icon: '3d_rotation', kind: 'icon' }, +] +const VIEWS_ROW2 = [ + { value: 'N', label: 'N', kind: 'letter' }, + { value: 'O', label: 'O', kind: 'letter' }, + { value: 'S', label: 'S', kind: 'letter' }, + { value: 'W', label: 'W', kind: 'letter' }, ] function fmtScale(s) { @@ -307,6 +314,7 @@ export default function OberleisteApp() { massePresets: [], masseActiveId: null, textSettings: { font: 'Helvetica', size: 0.20, bold: false, italic: false }, textFonts: [], + northAngle: 0, }) const [appliedScale, setAppliedScale] = useState(null) const appliedScaleRef = useRef(null) @@ -365,20 +373,19 @@ export default function OberleisteApp() { if (appliedScale && appliedScale > 0) setMassstab(appliedScale) } - // Aktuelles View-Match. Orthogonale Standard-Views matchen via viewName. - // 'Iso' und 'Perspective' werden via parallel-Flag unterschieden — die - // Rhino-Viewport-Namen sind beide oft "Perspective". + // Aktuelles View-Match. Top/Persp/Iso werden via parallel-Flag und + // viewName unterschieden. N/O/S/W: kein zuverlaessiges View-Match + // ueber den Viewport-Namen (wir setzen die Camera direkt) — daher + // kein Active-State fuer Cardinals. const matchView = (v) => { const name = (state.viewName || '').toLowerCase() - const ortho = ['top', 'front', 'right', 'bottom', 'left', 'back'] + if (v === 'Top') return name === 'top' + if (v === 'Perspective') return state.parallel === false if (v === 'Iso') { - // Parallel-Projektion + kein orthogonaler Standardname → Iso + const ortho = ['top', 'front', 'right', 'bottom', 'left', 'back'] return state.parallel === true && !ortho.includes(name) } - if (v === 'Perspective') { - return state.parallel === false - } - return name === v.toLowerCase() + return false } // (Command-Bar wurde entfernt — Rhinos eigene Command-Line wird benutzt.) @@ -435,52 +442,88 @@ export default function OberleisteApp() {
- {/* ====== VIEW (Top/Front/Right/Iso/Persp + Kamera) ====== - Segmented-Pill-Gruppe analog Vectorworks. Active = accent fill. */} -
- {VIEWS.map((v, idx) => { - const isFirst = idx === 0 - const isLast = idx === VIEWS.length - 1 - const isActive = matchView(v.value) - return ( - - ) - })} -
- openKameraPanel()} - title="Kamera-Einstellungen (Position, Target, Linse, Presets)" /> + {/* ====== VIEW 2x4 Grid ====== + Reihe 1: TOP / ISO / PERSP / 📷 (Kamera-Settings) + Reihe 2: N / O / S / W (rotieren mit dossier_north_angle) + */} + {(() => { + const VIEW_W = 140 // konsistent mit Massstab-Pills + const CELL_W = Math.floor(VIEW_W / 4) + const cellStyle = (isActive, isFirst) => ({ + height: BAR_H, width: CELL_W, + background: isActive ? 'var(--accent)' : 'var(--bg-input)', + color: isActive ? 'var(--bg-panel)' : 'var(--text-primary)', + border: 'none', + borderLeft: isFirst ? 'none' : '1px solid var(--border)', + display: 'inline-flex', alignItems: 'center', justifyContent: 'center', + gap: 4, fontWeight: isActive ? 600 : 500, + cursor: 'pointer', flexShrink: 0, padding: 0, + transition: 'background 0.15s, color 0.15s', + }) + const hoverIn = (isActive) => (e) => { + if (isActive) return + e.currentTarget.style.background = 'var(--bg-item-hover)' + e.currentTarget.style.color = 'var(--accent-light)' + } + const hoverOut = (isActive) => (e) => { + if (isActive) return + e.currentTarget.style.background = 'var(--bg-input)' + e.currentTarget.style.color = 'var(--text-primary)' + } + // Reihe 1: 3 View-Icons + Kamera-Settings + const row1 = [ + ...VIEWS_ROW1, + { value: '__camera__', icon: 'videocam', kind: 'icon' }, + ] + return ( +
+ {/* Reihe 1 */} +
+ {row1.map((v, idx) => { + const isActive = v.value !== '__camera__' && matchView(v.value) + const label = v.value === '__camera__' ? 'Kamera-Einstellungen' + : `Ansicht ${v.value}` + return ( + + ) + })} +
+ {/* Reihe 2: N/O/S/W als Buchstaben */} +
+ {VIEWS_ROW2.map((v, idx) => ( + + ))} +
+
+ ) + })()}
diff --git a/src/lib/rhinoBridge.js b/src/lib/rhinoBridge.js index ed79704..603663b 100644 --- a/src/lib/rhinoBridge.js +++ b/src/lib/rhinoBridge.js @@ -191,6 +191,7 @@ export function kameraZoomExtents() { send('ZOOM_EXTENTS', {}) } export function saveKameraPreset(name) { send('SAVE_PRESET', { name }) } export function applyKameraPreset(id) { send('APPLY_PRESET', { id }) } export function deleteKameraPreset(id) { send('DELETE_PRESET', { id }) } +export function setKameraNorthAngle(angle) { send('SET_NORTH_ANGLE', { angle: Number(angle) || 0 }) } // Ebenenkombinationen (gehosted in Oberleiste, gleicher Store wie EBENEN) export function pickLayerCombination(name) { send('PICK_LAYER_COMBINATION', { name: name || null }) } export function saveLayerCombination(name) { send('SAVE_LAYER_COMBINATION', { name }) }