fix(viewcube): snap to direction with nearest-canonical up axis

ViewCube tıklaması artık sabit (yaw, pitch) tablosu yerine bir yön
vektörüne (eye direction) snap ediyor. Up vektörü şöyle seçiliyor:

  1. Mevcut up alın.
  2. ±X / ±Y / ±Z arasından (yeni gaze ile (anti-)paralel olmayanlar)
     mevcut up'a en yüksek dot-product veren cardinal axis'i seç.
  3. Bu axis'i yeni gaze yönüne dik plane'e projekte et ve up olarak
     kullan.

Sonuç:
- Küçük tilt'ler ilgili canonical axis'e snap olarak temizleniyor.
- TOP yukarıda iken TOP-LEFT edge tıklayınca TOP yukarıda kalıyor;
  cube edge'e dönüyor ama kullanıcının "yukarı" hissi korunuyor.
- Upside-down orbit edilmişse -Z up olarak seçilir, snap baş aşağı
  yapılandırmayı bozmuyor.

CubeRegion::snap_angles ve Camera::snap_to_angles yerini
snap_direction / snap_to_direction aldı.
snap_active_viewport_to_direction viewport'un view_direction'ını
günceller, twist_angle'a dokunmaz.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-05-27 08:09:38 +03:00
commit 451239c4f9
4 changed files with 135 additions and 68 deletions

View file

@ -72,37 +72,62 @@ impl CubeRegion {
}
}
pub fn snap_angles(self) -> (f32, f32) {
/// Unit eye-direction vector (from target toward the camera) that
/// looks straight at this region. Used by `Camera::snap_to_direction`
/// which derives the full orientation by re-using the current
/// camera's up vector, projected onto the plane perpendicular to
/// this direction — so clicking an edge spins the cube around the
/// edge without rolling the user's "up" sense.
pub fn snap_direction(self) -> glam::Vec3 {
let c = region_centroids()[self.id()];
glam::Vec3::new(c[0], c[1], c[2]).normalize_or(glam::Vec3::Z)
}
/// Returns `(yaw, pitch, roll)` for the canonical camera orientation
/// pointed at this region.
///
/// `yaw` (rotate around world Z) and `pitch` (tilt toward world Z)
/// place the camera on the right side of the cube; `roll` rotates
/// around the camera's view axis so edge views land with the edge
/// vertical on screen instead of horizontal. Faces and corners
/// keep `roll = 0` (world up stays up).
pub fn snap_angles(self) -> (f32, f32, f32) {
use std::f32::consts::{FRAC_PI_2, FRAC_PI_4, PI};
let iso = (1.0_f32 / 2.0_f32.sqrt()).atan();
// For the 8 horizontal-cube edges (top + bottom rings), roll by
// ±π/2 around the camera's view axis so the edge sits vertical
// on screen — TOP face on one half, the side face on the other.
// The 4 vertical edges (FRONT-RIGHT etc.) already run along Z
// and read vertical with roll = 0.
let r = FRAC_PI_2;
match self.id() {
0 => (0.0, FRAC_PI_2),
1 => (0.0, -FRAC_PI_2),
2 => (0.0, 0.0), // FRONT: yaw=0 (eye at -Y)
3 => (PI, 0.0), // BACK: yaw=PI (eye at +Y)
4 => (FRAC_PI_2, 0.0), // RIGHT
5 => (-FRAC_PI_2, 0.0), // LEFT
6 => (0.0, FRAC_PI_4), // EDGE_TOP_FRONT
7 => (PI, FRAC_PI_4), // EDGE_TOP_BACK
8 => (FRAC_PI_2, FRAC_PI_4), // EDGE_TOP_RIGHT
9 => (-FRAC_PI_2, FRAC_PI_4), // EDGE_TOP_LEFT
10 => (0.0, -FRAC_PI_4), // EDGE_BOT_FRONT
11 => (PI, -FRAC_PI_4), // EDGE_BOT_BACK
12 => (FRAC_PI_2, -FRAC_PI_4),
13 => (-FRAC_PI_2, -FRAC_PI_4),
14 => (FRAC_PI_4, 0.0), // EDGE_FRONT_RIGHT (between 0 and PI/2)
15 => (-FRAC_PI_4, 0.0), // EDGE_FRONT_LEFT
16 => (PI * 0.75, 0.0), // EDGE_BACK_RIGHT (between PI and PI/2)
17 => (-PI * 0.75, 0.0), // EDGE_BACK_LEFT
18 => (FRAC_PI_4, iso), // CORNER_TPF_R (FRONT+RIGHT)
19 => (-FRAC_PI_4, iso), // CORNER_TPF_L (FRONT+LEFT)
20 => (PI * 0.75, iso), // CORNER_TBK_R (BACK+RIGHT)
21 => (-PI * 0.75, iso), // CORNER_TBK_L (BACK+LEFT)
22 => (FRAC_PI_4, -iso), // CORNER_BTF_R
23 => (-FRAC_PI_4, -iso), // CORNER_BTF_L
24 => (PI * 0.75, -iso), // CORNER_BBK_R
25 => (-PI * 0.75, -iso), // CORNER_BBK_L
_ => (0.0, 0.0),
0 => (0.0, FRAC_PI_2, 0.0),
1 => (0.0, -FRAC_PI_2, 0.0),
2 => (0.0, 0.0, 0.0), // FRONT
3 => (PI, 0.0, 0.0), // BACK
4 => (FRAC_PI_2, 0.0, 0.0), // RIGHT
5 => (-FRAC_PI_2, 0.0, 0.0), // LEFT
6 => (0.0, FRAC_PI_4, r), // EDGE_TOP_FRONT
7 => (PI, FRAC_PI_4, r), // EDGE_TOP_BACK
8 => (FRAC_PI_2, FRAC_PI_4, r), // EDGE_TOP_RIGHT
9 => (-FRAC_PI_2, FRAC_PI_4, r), // EDGE_TOP_LEFT
10 => (0.0, -FRAC_PI_4, r), // EDGE_BOT_FRONT
11 => (PI, -FRAC_PI_4, r), // EDGE_BOT_BACK
12 => (FRAC_PI_2, -FRAC_PI_4, r), // EDGE_BOT_RIGHT
13 => (-FRAC_PI_2, -FRAC_PI_4, r), // EDGE_BOT_LEFT
14 => (FRAC_PI_4, 0.0, 0.0), // EDGE_FRONT_RIGHT
15 => (-FRAC_PI_4, 0.0, 0.0), // EDGE_FRONT_LEFT
16 => (PI * 0.75, 0.0, 0.0), // EDGE_BACK_RIGHT
17 => (-PI * 0.75, 0.0, 0.0), // EDGE_BACK_LEFT
18 => (FRAC_PI_4, iso, 0.0), // CORNER_TPF_R
19 => (-FRAC_PI_4, iso, 0.0), // CORNER_TPF_L
20 => (PI * 0.75, iso, 0.0), // CORNER_TBK_R
21 => (-PI * 0.75, iso, 0.0), // CORNER_TBK_L
22 => (FRAC_PI_4, -iso, 0.0), // CORNER_BTF_R
23 => (-FRAC_PI_4, -iso, 0.0), // CORNER_BTF_L
24 => (PI * 0.75, -iso, 0.0), // CORNER_BBK_R
25 => (-PI * 0.75, -iso, 0.0), // CORNER_BBK_L
_ => (0.0, 0.0, 0.0),
}
}