diff --git a/src/app/update.rs b/src/app/update.rs index cc6e808d9..9f0c2a339 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -3281,8 +3281,8 @@ impl OpenCADStudio { bounds, ) } else { - let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds); - find_hit_grip(p, &self.tabs[i].selected_grips, vp_mat, bounds) + let cam = self.tabs[i].scene.camera.borrow(); + find_hit_grip(p, &self.tabs[i].selected_grips, &cam, bounds) }; if let Some((grip_id, is_translate, world)) = grip_hit { // The visibility (lookup) grip opens a state @@ -8275,8 +8275,8 @@ impl OpenCADStudio { bounds, ) } else { - let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds); - find_hit_grip(p, &self.tabs[i].selected_grips, vp_mat, bounds) + let cam = self.tabs[i].scene.camera.borrow(); + find_hit_grip(p, &self.tabs[i].selected_grips, &cam, bounds) }; match hit { Some((grip_id, _, _)) => { diff --git a/src/app/view.rs b/src/app/view.rs index 29d49359e..9f4cc748e 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -98,8 +98,8 @@ impl OpenCADStudio { drop(cam); grips_to_screen_paper(&tab.selected_grips, tx, ty, half_w, half_h, bounds) } else { - let vp_mat = tab.scene.camera.borrow().view_proj(bounds); - grips_to_screen(&tab.selected_grips, vp_mat, bounds) + let cam = tab.scene.camera.borrow(); + grips_to_screen(&tab.selected_grips, &cam, bounds) }; screen_grips .into_iter() @@ -140,17 +140,11 @@ impl OpenCADStudio { // enumeration for both, shared with the renderer. // Align the model grid to the active UCS (origin in wire space, world // axis directions); paper space stays plain WCS. - let (grid_origin, grid_axes) = if is_paper { - (glam::Vec3::ZERO, (glam::Vec3::X, glam::Vec3::Y, glam::Vec3::Z)) + let (grid_origin, grid_axes): (glam::DVec3, _) = if is_paper { + (glam::DVec3::ZERO, (glam::Vec3::X, glam::Vec3::Y, glam::Vec3::Z)) } else { let (o, ux, uy, uz) = tab.ucs_xform().axes(); - let wo = [0.0_f64; 3]; - let o_wire = glam::Vec3::new( - o.x - wo[0] as f32, - o.y - wo[1] as f32, - o.z - wo[2] as f32, - ); - (o_wire, (ux, uy, uz)) + (o.as_dvec3(), (ux, uy, uz)) }; let grid: Vec = tab .scene @@ -159,7 +153,8 @@ impl OpenCADStudio { .map(|(bounds, cam)| { let plane = grid_plane_from_camera(cam.pitch, cam.yaw); overlay::GridParams { - view_proj: cam.view_proj(bounds), + view_rot: cam.view_proj_rte(bounds), + eye: cam.eye_f64(), bounds, plane, origin: grid_origin, diff --git a/src/scene/pick/grip.rs b/src/scene/pick/grip.rs index b31684c53..306d55ad7 100644 --- a/src/scene/pick/grip.rs +++ b/src/scene/pick/grip.rs @@ -1,7 +1,7 @@ //! OpenCADStudio-style grip editing. use acadrust::Handle; -use glam::{Mat4, Vec3}; +use glam::Vec3; use iced::{Point, Rectangle}; use crate::scene::model::object::{GripDef, GripShape}; @@ -34,17 +34,19 @@ pub struct GripEdit { /// Returns `(grip_id, screen_pos, is_midpoint, shape)` for each grip. pub fn grips_to_screen( grips: &[GripDef], - view_proj: Mat4, + camera: &crate::scene::view::camera::Camera, bounds: Rectangle, ) -> Vec<(usize, Point, bool, GripShape, Option<[f32; 2]>)> { grips .iter() .map(|g| { - let ndc = view_proj.project_point3(g.world.as_vec3()); - let screen = Point::new( - bounds.x + (ndc.x + 1.0) * 0.5 * bounds.width, - bounds.y + (1.0 - ndc.y) * 0.5 * bounds.height, - ); + // Project from the f64 grip position via the relative-to-eye path so + // the grip stays glued to the wire at UTM-scale coordinates (an + // `as_vec3` cast first would quantize it ~0.5 m off at high zoom). + let screen = match camera.project_f64(g.world, bounds) { + Some(p) => Point::new(bounds.x + p.x, bounds.y + p.y), + None => Point::new(f32::NAN, f32::NAN), + }; (g.id, screen, g.is_midpoint, g.shape, g.dir) }) .collect() @@ -106,18 +108,17 @@ pub fn find_hit_grip_paper( pub fn find_hit_grip( cursor: Point, grips: &[GripDef], - view_proj: Mat4, + camera: &crate::scene::view::camera::Camera, bounds: Rectangle, ) -> Option<(usize, bool, Vec3)> { let mut best_dist = GRIP_THRESHOLD_PX; let mut best: Option<(usize, bool, Vec3)> = None; for g in grips { - let ndc = view_proj.project_point3(g.world.as_vec3()); - let screen = Point::new( - (ndc.x + 1.0) * 0.5 * bounds.width, - (1.0 - ndc.y) * 0.5 * bounds.height, - ); + let Some(screen) = camera.project_f64(g.world, bounds) else { + continue; + }; + let screen = Point::new(screen.x, screen.y); let dx = screen.x - cursor.x; let dy = screen.y - cursor.y; let d = (dx * dx + dy * dy).sqrt(); diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs index 3c83b9326..f49f23ff4 100644 --- a/src/ui/overlay.rs +++ b/src/ui/overlay.rs @@ -48,13 +48,18 @@ pub enum GridPlane { /// Passed to the canvas when the GRID display is active. #[derive(Clone)] pub struct GridParams { - pub view_proj: Mat4, + /// Rotation-only view-projection (Camera::view_proj_rte). Grid points are + /// made relative to `eye` in f64 before projecting, so the grid stays + /// precise / jitter-free at UTM-scale absolute coordinates. + pub view_rot: Mat4, + /// Camera eye in absolute world f64 — subtracted from each grid point. + pub eye: glam::DVec3, pub bounds: iced::Rectangle, pub plane: GridPlane, - /// Grid origin in render (wire) space and the active UCS axis directions. + /// Grid origin in absolute world f64 and the active UCS axis directions. /// The grid rules along these instead of world X/Y/Z, so it aligns to the /// user's coordinate system. Plain WCS passes `(ZERO, X, Y, Z)`. - pub origin: Vec3, + pub origin: glam::DVec3, pub axes: (Vec3, Vec3, Vec3), } @@ -314,7 +319,7 @@ impl canvas::Program for SelectionCanvas { // Project with the full viewport rect `gb` so the grid stays aligned; // only the clip is clamped. frame.with_clip(clip, |f| { - draw_grid(f, g.view_proj, g.plane, gb, g.origin, g.axes) + draw_grid(f, g.view_rot, g.eye, g.plane, gb, g.origin, g.axes) }); } @@ -864,17 +869,18 @@ const MIN_GRID_PX: f32 = 20.0; fn draw_grid( frame: &mut canvas::Frame, - vp: Mat4, + view_rot: Mat4, + eye: glam::DVec3, plane: GridPlane, bounds: iced::Rectangle, - grid_origin: Vec3, + grid_origin: glam::DVec3, grid_axes: (Vec3, Vec3, Vec3), ) { - // World → canvas screen: include bounds origin so the grid lands in the - // active tile's rectangle (the screen → world unproject below stays - // tile-local, which is what feeds the visible-extent computation). - let w2s = |world: Vec3| -> Point { - let ndc = vp.project_point3(world); + // World → canvas screen via relative-to-eye: subtract the f64 eye first so + // grid points near the camera stay precise at UTM-scale absolute coords. + let w2s = |world: glam::DVec3| -> Point { + let rel = (world - eye).as_vec3(); + let ndc = view_rot.project_point3(rel); Point::new( bounds.x + (ndc.x + 1.0) * 0.5 * bounds.width, bounds.y + (1.0 - ndc.y) * 0.5 * bounds.height, @@ -893,8 +899,8 @@ fn draw_grid( // Adaptive spacing: measure pixels per 1-unit step along each axis, // then find the smallest power-of-5 multiple that gives ≥ MIN_GRID_PX. let o = w2s(grid_origin); - let a1s = w2s(grid_origin + axis1); - let a2s = w2s(grid_origin + axis2); + let a1s = w2s(grid_origin + axis1.as_dvec3()); + let a2s = w2s(grid_origin + axis2.as_dvec3()); let px1 = ((a1s.x - o.x).powi(2) + (a1s.y - o.y).powi(2)).sqrt(); let px2 = ((a2s.x - o.x).powi(2) + (a2s.y - o.y).powi(2)).sqrt(); let px_per_unit = px1.max(px2); @@ -912,11 +918,12 @@ fn draw_grid( // Visible world extent: unproject screen corners (mid-depth approximation) // and project them onto the grid axes. - let inv = vp.inverse(); - let unproject = |sx: f32, sy: f32| -> Vec3 { + let inv = view_rot.inverse(); + let unproject = |sx: f32, sy: f32| -> glam::DVec3 { let ndc_x = (sx / bounds.width) * 2.0 - 1.0; let ndc_y = 1.0 - (sy / bounds.height) * 2.0; - inv.project_point3(Vec3::new(ndc_x, ndc_y, 0.5)) + // Unproject in eye-relative space, then add the f64 eye back. + eye + inv.project_point3(Vec3::new(ndc_x, ndc_y, 0.5)).as_dvec3() }; let corners = [ unproject(0.0, 0.0), @@ -925,7 +932,10 @@ fn draw_grid( unproject(bounds.width, bounds.height), ]; let range = |ax: Vec3| -> (f32, f32) { - let vals: Vec = corners.iter().map(|p| (*p - grid_origin).dot(ax)).collect(); + let vals: Vec = corners + .iter() + .map(|p| (*p - grid_origin).as_vec3().dot(ax)) + .collect(); ( vals.iter().cloned().fold(f32::INFINITY, f32::min), vals.iter().cloned().fold(f32::NEG_INFINITY, f32::max), @@ -957,8 +967,8 @@ fn draw_grid( // Lines parallel to axis2 (varying axis1 position) for i in n1_s..=n1_e { let v = i as f32 * s; - let p0 = w2s(grid_origin + axis1 * v + axis2 * (min2 - s)); - let p1 = w2s(grid_origin + axis1 * v + axis2 * (max2 + s)); + let p0 = w2s(grid_origin + (axis1 * v + axis2 * (min2 - s)).as_dvec3()); + let p1 = w2s(grid_origin + (axis1 * v + axis2 * (max2 + s)).as_dvec3()); frame.stroke( &canvas::Path::new(|b| { b.move_to(p0); @@ -970,8 +980,8 @@ fn draw_grid( // Lines parallel to axis1 (varying axis2 position) for i in n2_s..=n2_e { let v = i as f32 * s; - let p0 = w2s(grid_origin + axis2 * v + axis1 * (min1 - s)); - let p1 = w2s(grid_origin + axis2 * v + axis1 * (max1 + s)); + let p0 = w2s(grid_origin + (axis2 * v + axis1 * (min1 - s)).as_dvec3()); + let p1 = w2s(grid_origin + (axis2 * v + axis1 * (max1 + s)).as_dvec3()); frame.stroke( &canvas::Path::new(|b| { b.move_to(p0); @@ -983,21 +993,22 @@ fn draw_grid( // Coloured axes drawn on top of the grid lines, along the same UCS basis. let extent = (min1.abs().max(max1.abs()).max(min2.abs()).max(max2.abs()) + s) * 1.5; - draw_axes(frame, vp, bounds, extent.max(10.0), grid_origin, grid_axes); + draw_axes(frame, view_rot, eye, bounds, extent.max(10.0), grid_origin, grid_axes); } // ── Coloured UCS axes ────────────────────────────────────────────────────── fn draw_axes( frame: &mut canvas::Frame, - vp: Mat4, + view_rot: Mat4, + eye: glam::DVec3, bounds: iced::Rectangle, extent: f32, - origin: Vec3, + origin: glam::DVec3, axes: (Vec3, Vec3, Vec3), ) { - let w2s = |world: Vec3| -> Point { - let ndc = vp.project_point3(world); + let w2s = |world: glam::DVec3| -> Point { + let ndc = view_rot.project_point3((world - eye).as_vec3()); Point::new( bounds.x + (ndc.x + 1.0) * 0.5 * bounds.width, bounds.y + (1.0 - ndc.y) * 0.5 * bounds.height, @@ -1014,8 +1025,8 @@ fn draw_axes( let mut line = |dir: Vec3, r: f32, g: f32, b: f32| { frame.stroke( &canvas::Path::new(|p| { - p.move_to(w2s(origin - dir * e)); - p.line_to(w2s(origin + dir * e)); + p.move_to(w2s(origin - (dir * e).as_dvec3())); + p.line_to(w2s(origin + (dir * e).as_dvec3())); }), axis_stroke(r, g, b), );