forked from 1b-agent/opencadstudio-upstream
fix(viewcube): deterministic horizon for obliques, generic opposite
Top Front Right/Left kept Y (north) as up when starting from Top, tilting the base because orbit is turntable (never banks). Make snap_to_direction deterministic: world +Z horizon except near +-Z where north +Y is used, sign-preserved only for intentionally inverted views. Expand opposite() from faces-only to all 26 regions so already-there flip works for edges/corners.
This commit is contained in:
1 parent
5ba72ec3e4
commit
4fdef48502
3 files changed
+50
-49
No files matched your search
@@ -5511,9 +5511,10 @@ properties={:.1}ms picked={}",
|
||||
}
|
||||
let eye_dir = r_ucs.transform_vector3(region.snap_direction());
|
||||
|
||||
// Faces snap to a canonical upright orientation (never upside
|
||||
// down); edges/corners keep the current up-sense so they spin
|
||||
// smoothly around the clicked feature.
|
||||
// Faces and edges/corners all snap deterministically (world +Z
|
||||
// horizon, north +Y for top/bottom) so a cube click is repeatable
|
||||
// and matches the turntable orbit; snap_to_direction preserves an
|
||||
// intentional upside-down sense by sign-flipping only.
|
||||
let is_face = matches!(region, scene::CubeRegion::Face(_));
|
||||
if self.tabs[i].scene.active_viewport.is_some() {
|
||||
if is_face {
|
||||
|
||||
@@ -142,24 +142,39 @@ impl CubeRegion {
|
||||
|
||||
/// 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.
|
||||
/// / `snap_to_face` which derive a deterministic horizon (world +Z,
|
||||
/// except top/bottom where north +Y is used) so every cube click
|
||||
/// lands repeatably.
|
||||
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)
|
||||
}
|
||||
|
||||
pub fn opposite(self) -> CubeRegion {
|
||||
match self {
|
||||
CubeRegion::Face(FACE_TOP) => CubeRegion::Face(FACE_BOTTOM),
|
||||
CubeRegion::Face(FACE_BOTTOM) => CubeRegion::Face(FACE_TOP),
|
||||
CubeRegion::Face(FACE_FRONT) => CubeRegion::Face(FACE_BACK),
|
||||
CubeRegion::Face(FACE_BACK) => CubeRegion::Face(FACE_FRONT),
|
||||
CubeRegion::Face(FACE_RIGHT) => CubeRegion::Face(FACE_LEFT),
|
||||
CubeRegion::Face(FACE_LEFT) => CubeRegion::Face(FACE_RIGHT),
|
||||
other => other,
|
||||
// Generic opposite: the region whose centroid is nearest to -self.
|
||||
// Covers faces, edges and corners so "already there → flip" works
|
||||
// for every cube click, not just faces.
|
||||
let centroids = region_centroids();
|
||||
let c = centroids[self.id()];
|
||||
let neg = [-c[0], -c[1], -c[2]];
|
||||
let mut best = self.id();
|
||||
let mut best_d = f32::MAX;
|
||||
for (i, cc) in centroids.iter().enumerate() {
|
||||
let dx = cc[0] - neg[0];
|
||||
let dy = cc[1] - neg[1];
|
||||
let dz = cc[2] - neg[2];
|
||||
let d = dx * dx + dy * dy + dz * dz;
|
||||
if d < best_d {
|
||||
best_d = d;
|
||||
best = i;
|
||||
}
|
||||
}
|
||||
if best < 6 {
|
||||
CubeRegion::Face(best)
|
||||
} else if best < 18 {
|
||||
CubeRegion::Edge(best)
|
||||
} else {
|
||||
CubeRegion::Corner(best)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+19
-34
@@ -645,44 +645,29 @@ impl Camera {
|
||||
/// Snap to a canonical view direction (called by ViewCubeSnap).
|
||||
/// `eye_dir` is the unit vector from the target toward the camera.
|
||||
///
|
||||
/// Up vector resolution:
|
||||
/// 1. Take the current up.
|
||||
/// 2. Pick the world axis (±X, ±Y, ±Z) whose dot product with the
|
||||
/// current up is highest — skipping any axis (anti-)parallel to
|
||||
/// the new gaze direction.
|
||||
/// 3. Project that axis onto the plane ⊥ `new_eye` and use that as
|
||||
/// the new up.
|
||||
///
|
||||
/// Result: small tilts collapse onto the nearest world axis (so the
|
||||
/// view always lands cleanly aligned), while genuine flips of the
|
||||
/// up-sense (e.g. orbited upside-down) are preserved.
|
||||
/// Deterministic horizon: when looking close to ±Z (top/bottom) the
|
||||
/// up is north (+Y), otherwise world up (+Z), projected onto the
|
||||
/// plane ⊥ `new_eye`. This matches the turntable `orbit` (which never
|
||||
/// banks) so a cube corner always lands with the same roll and the
|
||||
/// base never appears rotated. The sign is flipped only if the
|
||||
/// current view is intentionally upside-down, preserving that sense.
|
||||
pub fn snap_to_direction(&mut self, eye_dir: Vec3, ucs: glam::Mat4) {
|
||||
let new_eye = eye_dir.normalize_or(Vec3::Z);
|
||||
let cur_up = self.rotation * Vec3::Y;
|
||||
// Candidate up axes are the UCS axes, not world X/Y/Z, so a face snap
|
||||
// lands the view square to the user's coordinate system (in-plane roll
|
||||
// included). Identity `ucs` reproduces the world-aligned snap.
|
||||
let ux = ucs.transform_vector3(Vec3::X).normalize_or(Vec3::X);
|
||||
let uy = ucs.transform_vector3(Vec3::Y).normalize_or(Vec3::Y);
|
||||
let uz = ucs.transform_vector3(Vec3::Z).normalize_or(Vec3::Z);
|
||||
let cardinals = [ux, -ux, uy, -uy, uz, -uz];
|
||||
let mut best_score = f32::NEG_INFINITY;
|
||||
let mut best_up = uz;
|
||||
for axis in cardinals {
|
||||
// Skip axes (nearly) collinear with the new gaze — they can't
|
||||
// serve as up because the projection onto the plane would
|
||||
// vanish.
|
||||
if axis.dot(new_eye).abs() > 0.999 {
|
||||
continue;
|
||||
}
|
||||
let score = axis.dot(cur_up);
|
||||
if score > best_score {
|
||||
best_score = score;
|
||||
best_up = axis;
|
||||
}
|
||||
}
|
||||
// Project the chosen axis onto the plane ⊥ new_eye and normalize.
|
||||
let projected = best_up - new_eye * best_up.dot(new_eye);
|
||||
// Deterministic horizon: top/bottom views (eye ≈ ±Z) use north (+Y)
|
||||
// as up; every other direction uses world up (+Z). This keeps the
|
||||
// cube's "Top Front Right" etc. repeatable and aligned with the
|
||||
// turntable orbit (which never banks), and avoids the previous
|
||||
// adaptive choice that picked the nearest cardinal to the current
|
||||
// up — from a top view that was +Y, so an oblique top corner kept
|
||||
// Y as up and tilted the base (roll) instead of keeping the horizon
|
||||
// level. Preserve the sign only to keep an intentionally inverted
|
||||
// (upside-down) view inverted.
|
||||
let cur_up = self.rotation * Vec3::Y;
|
||||
let raw_ref = if new_eye.dot(uz).abs() > 0.9 { uy } else { uz };
|
||||
let up_ref = if cur_up.dot(raw_ref) < 0.0 { -raw_ref } else { raw_ref };
|
||||
let projected = up_ref - new_eye * up_ref.dot(new_eye);
|
||||
let new_up = projected.normalize_or(if new_eye.dot(uz).abs() < 0.99 {
|
||||
(uz - new_eye * uz.dot(new_eye)).normalize()
|
||||
} else {
|
||||
|
||||
Reference in new issue
Block a user