From 012ec3febc9d31e0c04e292dd87437b33ef06a88 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 26 Jun 2026 00:11:40 +0300 Subject: [PATCH] fix(viewcube): land face clicks upright, never upside-down MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A drawing opened with a twisted view made a TOP-face click keep the twist, so north ended up at the bottom. Add Camera::snap_to_face, which snaps to a canonical orientation — north (UCS +Y) up for top/bottom, world (UCS +Z) up for the side elevations — ignoring the current up-sense. Face clicks use it; edges/corners still preserve their up so they spin smoothly around the clicked feature. The Home button routes through the same path, giving a reliable reset for a twisted view. Co-Authored-By: Claude Opus 4.8 --- src/app/update.rs | 22 ++++++++++++++++++---- src/scene/view/camera.rs | 26 +++++++++++++++++++++++--- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/app/update.rs b/src/app/update.rs index 23b81cb46..134b5853c 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -4512,13 +4512,27 @@ impl OpenCADStudio { } 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. + let is_face = matches!(region, scene::CubeRegion::Face(_)); if self.tabs[i].scene.active_viewport.is_some() { - self.tabs[i] - .scene - .snap_active_viewport_to_direction(eye_dir, r_ucs); + if is_face { + self.tabs[i] + .scene + .mutate_active_viewport_camera(|c| c.snap_to_face(eye_dir, r_ucs)); + } else { + self.tabs[i] + .scene + .snap_active_viewport_to_direction(eye_dir, r_ucs); + } } else { let mut cam = self.tabs[i].scene.camera.borrow_mut(); - cam.snap_to_direction(eye_dir, r_ucs); + if is_face { + cam.snap_to_face(eye_dir, r_ucs); + } else { + cam.snap_to_direction(eye_dir, r_ucs); + } } self.tabs[i].scene.camera_generation += 1; self.command_line diff --git a/src/scene/view/camera.rs b/src/scene/view/camera.rs index 01108afcd..984d9b6fe 100644 --- a/src/scene/view/camera.rs +++ b/src/scene/view/camera.rs @@ -390,11 +390,31 @@ impl Camera { self.sync_yaw_pitch(); } - /// Jump to the default "home" view — top-down (looking along −Z), expressed - /// in the active UCS so it lands square to the user's coordinate frame. + /// Snap to a canonical face view: looks along `eye_dir` with a fixed + /// upright orientation — north (UCS +Y) up for top/bottom, world (UCS +Z) + /// up for the side elevations. Unlike [`snap_to_direction`] this ignores + /// the current up-sense, so a face click always lands square and never + /// upside-down, even when the drawing opened with a twisted view. + pub fn snap_to_face(&mut self, eye_dir: Vec3, ucs: glam::Mat4) { + let new_eye = eye_dir.normalize_or(Vec3::Z); + let uy = ucs.transform_vector3(Vec3::Y).normalize_or(Vec3::Y); + let uz = ucs.transform_vector3(Vec3::Z).normalize_or(Vec3::Z); + // Looking along the UCS Z axis (top/bottom) has no "world up" to use, + // so fall back to north (+Y); every side view uses world up. + let up_ref = if new_eye.dot(uz).abs() > 0.9 { uy } else { uz }; + let projected = up_ref - new_eye * up_ref.dot(new_eye); + let new_up = projected.normalize_or(uy); + let new_right = new_up.cross(new_eye).normalize(); + let mat = glam::Mat3::from_cols(new_right, new_up, new_eye); + self.rotation = Quat::from_mat3(&mat).normalize(); + self.sync_yaw_pitch(); + } + + /// Jump to the default "home" view — a canonical top-down view (north up), + /// expressed in the active UCS. Doubles as a "reset" for a twisted view. pub fn home_view(&mut self, ucs: glam::Mat4) { let dir = ucs.transform_vector3(Vec3::Z); - self.snap_to_direction(dir, ucs); + self.snap_to_face(dir, ucs); } /// Roll the camera about its own view axis by `angle` radians. The gaze