View-Toggle 2x4: TOP/ISO/PERSP/Cam + N/O/S/W mit Norden-Rotation

User-Vorschlag: Architektur-konformes View-Layout — 3D-Views oben, 4
Gebaeudeansichten unten. Plus Norden-Rotation als Doc-Setting damit bei
rotierten Projekten (swissBUILDINGS, Sonnenberechnungen) die richtigen
Wandansichten gepickt werden.

Backend (rhino/kamera.py):
- get_north_angle/set_north_angle — doc.Strings["dossier_north_angle"]
  (Grad im Uhrzeigersinn von +Y, default 0°)
- _scene_target_and_diag(doc) — gemeinsamer Helper fuer Szenen-Center +
  Diagonal-Distanz
- set_cardinal_view(vp, 'N'|'O'|'S'|'W'): rotiert Kamera-Position via
  Norden-Vektor. Parallel-Projektion, Camera-Z = Target-Z (echte
  Elevation), Up-Vektor +Z.
- set_top_view(vp): Plan-Ansicht mit Norden = Up-Vektor (Plan rotiert
  visuell wenn Norden != +Y)
- _set_iso(vp, octant): Octant-Richtung jetzt aus north+east-Vektoren
  konstruiert → ISO rotiert mit Norden mit
- Bridge-Handler SET_NORTH_ANGLE + state.northAngle, notify Oberleiste

Backend (oberleiste.py):
- SET_VIEW erweitert: Top → kamera.set_top_view, N/O/S/W →
  kamera.set_cardinal_view, Iso → kamera._set_iso. Front/Right/etc bleibt
  als Legacy direkt-Rhino-Call.
- State liefert northAngle

Frontend (OberleisteApp):
- VIEWS_ROW1: TOP/ISO/PERSP + Kamera-Settings-Button (Icons only)
- VIEWS_ROW2: N/O/S/W als DM-Mono-Buchstaben
- 2x4-Grid, VIEW_W=140 (konsistent mit Massstab-Pills), CELL_W=35
- matchView nur fuer Top/Iso/Perspective; Cardinals haben keinen
  Active-State (Viewport-Name ist nicht zuverlaessig erkennbar)

Frontend (KameraApp):
- Plan-Norden Section mit Number-Input (Grad, 0.5°-Step) + Reset-Button
- Hinweis-Text dass Wirkung auf TOP/ISO/N/O/S/W geht

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-21 00:04:44 +02:00
parent 2252ffd2f9
commit 205c626a5a
5 changed files with 306 additions and 99 deletions
+135 -33
View File
@@ -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: