From d78ed60582a589dc488a1f1166edd12bc9803575 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Mon, 22 Jun 2026 10:10:47 +0300 Subject: [PATCH] feat(ucs): orient the ViewCube to the active UCS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ViewCube showed world Front/Top/Right. It now composes the camera rotation with the active UCS (scene.viewcube_ucs, synced from the tab's active_ucs), so its faces follow the user's coordinate system in model space. Render uniform, hover/click hit-test, and the click→snap direction all go through scene.viewcube_ucs_mat() so they stay in lock-step; identity outside model space leaves paper-viewport navigation unchanged. Co-Authored-By: Claude Opus 4.8 --- src/app/commands.rs | 2 ++ src/app/document.rs | 13 +++++++++++++ src/app/update.rs | 15 +++++++++------ src/scene/mod.rs | 18 +++++++++++++++++- src/scene/view/render.rs | 4 ++-- 5 files changed, 43 insertions(+), 9 deletions(-) diff --git a/src/app/commands.rs b/src/app/commands.rs index 75a231378..e8c9b59a4 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -3822,6 +3822,8 @@ impl OpenCADStudio { } } } + // Keep the scene's ViewCube UCS in lock-step with active_ucs. + self.tabs[i].sync_ucs_to_scene(); } // ── Named Views (VIEW command) ──────────────────────────────── diff --git a/src/app/document.rs b/src/app/document.rs index c1672c135..c3471310d 100644 --- a/src/app/document.rs +++ b/src/app/document.rs @@ -186,6 +186,19 @@ impl DocumentTab { } else { Some(u) }; + self.sync_ucs_to_scene(); + } + + /// Push the active UCS rotation into the scene so the ViewCube composes with + /// it. Call after any change to `active_ucs`. + pub(super) fn sync_ucs_to_scene(&mut self) { + let (_, x, y, z) = self.ucs_xform().axes(); + self.scene.viewcube_ucs = glam::Mat4::from_cols( + x.extend(0.0), + y.extend(0.0), + z.extend(0.0), + glam::Vec4::W, + ); } pub(super) fn new_drawing(n: usize) -> Self { diff --git a/src/app/update.rs b/src/app/update.rs index da0965677..47d3a6b72 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -3175,10 +3175,9 @@ impl OpenCADStudio { let (vw, vh) = vp_size; if vw > 1.0 && vh > 1.0 { - let cam = self.tabs[i].scene.camera.borrow(); - if scene::hit_test(p.x, p.y, vw, vh, cam.view_rotation_mat(), VIEWCUBE_PX) - .is_some() - { + let rot = self.tabs[i].scene.camera.borrow().view_rotation_mat() + * self.tabs[i].scene.viewcube_ucs_mat(); + if scene::hit_test(p.x, p.y, vw, vh, rot, VIEWCUBE_PX).is_some() { return Task::none(); } } @@ -4330,9 +4329,13 @@ impl OpenCADStudio { Message::ViewCubeSnap(region) => { let i = self.active_tab; let mut region = region; + // The cube is oriented in the active UCS, so its face directions + // are UCS-frame — rotate them into world before comparing / + // snapping (identity outside model space). + let r_ucs = self.tabs[i].scene.viewcube_ucs_mat(); // "Already there → flip to opposite" check: compare the // current gaze direction with the region's target gaze. - let target_dir = region.snap_direction(); + let target_dir = r_ucs.transform_vector3(region.snap_direction()); let cur_dir = { let cam = self.tabs[i].scene.camera.borrow(); cam.rotation * glam::Vec3::Z @@ -4340,7 +4343,7 @@ impl OpenCADStudio { if cur_dir.dot(target_dir) > 0.9999 { region = region.opposite(); } - let eye_dir = region.snap_direction(); + let eye_dir = r_ucs.transform_vector3(region.snap_direction()); if self.tabs[i].scene.active_viewport.is_some() { self.tabs[i] diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 6f777adfb..381ea617b 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -803,6 +803,10 @@ pub struct Scene { paper_projected_cache: RefCell)>>, /// Active layout name — "Model" or a paper space layout name. pub current_layout: String, + /// UCS→world rotation for the ViewCube, kept in sync with the tab's active + /// UCS by `DocumentTab::sync_ucs_to_scene`. Identity = WCS. Applied only in + /// model space so the cube's faces follow the user's coordinate system. + pub viewcube_ucs: glam::Mat4, /// GPU render data for hatch fills, keyed by the DXF entity Handle. pub hatches: HashMap, /// GPU render data for solid meshes (truck Shell/Solid tessellation). @@ -954,6 +958,7 @@ impl Scene { paper_sheet_cache: RefCell::new(None), paper_projected_cache: RefCell::new(HashMap::default()), current_layout: "Model".to_string(), + viewcube_ucs: glam::Mat4::IDENTITY, hatches: HashMap::default(), meshes: HashMap::default(), block_meshes: HashMap::default(), @@ -3794,7 +3799,18 @@ impl Scene { return cam.view_rotation_mat(); } } - self.camera.borrow().view_rotation_mat() + self.camera.borrow().view_rotation_mat() * self.viewcube_ucs_mat() + } + + /// The UCS→world rotation the ViewCube should compose with the camera — + /// the active UCS in model space, identity everywhere else. Render, + /// hit-test, and click-snap all go through this so they stay in lock-step. + pub fn viewcube_ucs_mat(&self) -> glam::Mat4 { + if self.current_layout == "Model" { + self.viewcube_ucs + } else { + glam::Mat4::IDENTITY + } } /// Return the handle of the user viewport whose bounding rectangle contains diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index 649d34a63..6647bd6ce 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -794,7 +794,7 @@ impl Scene { images, meshes: self.meshes_arc(), uniforms, - cam_rotation: inst.camera.view_rotation_mat(), + cam_rotation: inst.camera.view_rotation_mat() * self.viewcube_ucs_mat(), // Only the active viewport gets the hovered-region highlight. hover_region: if inst.active { hover_region } else { None }, show_viewcube: inst.active, @@ -826,7 +826,7 @@ impl Scene { cursor: mouse::Cursor, ) { let pos = cursor.position_in(bounds); - let cam_rotation = self.camera.borrow().view_rotation_mat(); + let cam_rotation = self.camera.borrow().view_rotation_mat() * self.viewcube_ucs_mat(); if let Some(p) = pos { let tile = self.active_model_tile_bounds(bounds.width, bounds.height); state.hover_region = hover_id(