From 88bf2e57dcc08c95471d0e922c2e9a801ddc130a Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Mon, 22 Jun 2026 10:03:35 +0300 Subject: [PATCH] feat(ucs): align the model grid to the active UCS The grid ruled along world X/Y/Z through the world origin. It now rules along the active UCS axes through the UCS origin (passed in render/wire space), so it lines up with the user's coordinate system. Paper space stays plain WCS; identity UCS reproduces the old world-aligned grid. Co-Authored-By: Claude Opus 4.8 --- src/app/view.rs | 16 ++++++++++++++++ src/ui/overlay.rs | 44 ++++++++++++++++++++++++++++++-------------- 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/src/app/view.rs b/src/app/view.rs index 137639afe..7ed573625 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -138,6 +138,20 @@ impl OpenCADStudio { // stay independent: model tiles in model space; the sheet plus each // floating viewport (clipped to its rectangle) in paper space. One // 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)) + } else { + let (o, ux, uy, uz) = tab.ucs_xform().axes(); + let wo = tab.scene.world_offset; + 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)) + }; let grid: Vec = tab .scene .grid_views(vw, vh) @@ -148,6 +162,8 @@ impl OpenCADStudio { view_proj: cam.view_proj(bounds), bounds, plane, + origin: grid_origin, + axes: grid_axes, } }) .collect(); diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs index c6431907b..508e16ad9 100644 --- a/src/ui/overlay.rs +++ b/src/ui/overlay.rs @@ -51,6 +51,11 @@ pub struct GridParams { pub view_proj: Mat4, pub bounds: iced::Rectangle, pub plane: GridPlane, + /// Grid origin in render (wire) space 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 axes: (Vec3, Vec3, Vec3), } /// Compute the adaptive grid step size (world units) that the grid renderer @@ -308,7 +313,9 @@ 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)); + frame.with_clip(clip, |f| { + draw_grid(f, g.view_proj, g.plane, gb, g.origin, g.axes) + }); } if let (Some(a), Some(b)) = (self.selection.box_anchor, self.selection.box_current) { @@ -855,7 +862,14 @@ impl canvas::Program for SelectionCanvas { /// Minimum pixel gap between adjacent grid lines before stepping up to next spacing. const MIN_GRID_PX: f32 = 20.0; -fn draw_grid(frame: &mut canvas::Frame, vp: Mat4, plane: GridPlane, bounds: iced::Rectangle) { +fn draw_grid( + frame: &mut canvas::Frame, + vp: Mat4, + plane: GridPlane, + bounds: iced::Rectangle, + grid_origin: Vec3, + 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). @@ -867,18 +881,20 @@ fn draw_grid(frame: &mut canvas::Frame, vp: Mat4, plane: GridPlane, bounds: iced ) }; - // Plane-tangent axes: axis1 and axis2 span the grid plane. + // Plane-tangent axes: axis1 and axis2 span the grid plane, taken from the + // active UCS basis so the grid aligns to the user's coordinate system. + let (gx, gy, gz) = grid_axes; let (axis1, axis2) = match plane { - GridPlane::Xz => (Vec3::X, Vec3::Z), - GridPlane::Xy => (Vec3::X, Vec3::Y), - GridPlane::Yz => (Vec3::Y, Vec3::Z), + GridPlane::Xz => (gx, gz), + GridPlane::Xy => (gx, gy), + GridPlane::Yz => (gy, gz), }; // 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(Vec3::ZERO); - let a1s = w2s(axis1); - let a2s = w2s(axis2); + let o = w2s(grid_origin); + let a1s = w2s(grid_origin + axis1); + let a2s = w2s(grid_origin + axis2); 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); @@ -909,7 +925,7 @@ fn draw_grid(frame: &mut canvas::Frame, vp: Mat4, plane: GridPlane, bounds: iced unproject(bounds.width, bounds.height), ]; let range = |ax: Vec3| -> (f32, f32) { - let vals: Vec = corners.iter().map(|p| p.dot(ax)).collect(); + let vals: Vec = corners.iter().map(|p| (*p - grid_origin).dot(ax)).collect(); ( vals.iter().cloned().fold(f32::INFINITY, f32::min), vals.iter().cloned().fold(f32::NEG_INFINITY, f32::max), @@ -941,8 +957,8 @@ fn draw_grid(frame: &mut canvas::Frame, vp: Mat4, plane: GridPlane, bounds: iced // Lines parallel to axis2 (varying axis1 position) for i in n1_s..=n1_e { let v = i as f32 * s; - let p0 = w2s(axis1 * v + axis2 * (min2 - s)); - let p1 = w2s(axis1 * v + axis2 * (max2 + s)); + let p0 = w2s(grid_origin + axis1 * v + axis2 * (min2 - s)); + let p1 = w2s(grid_origin + axis1 * v + axis2 * (max2 + s)); frame.stroke( &canvas::Path::new(|b| { b.move_to(p0); @@ -954,8 +970,8 @@ fn draw_grid(frame: &mut canvas::Frame, vp: Mat4, plane: GridPlane, bounds: iced // Lines parallel to axis1 (varying axis2 position) for i in n2_s..=n2_e { let v = i as f32 * s; - let p0 = w2s(axis2 * v + axis1 * (min1 - s)); - let p1 = w2s(axis2 * v + axis1 * (max1 + s)); + let p0 = w2s(grid_origin + axis2 * v + axis1 * (min1 - s)); + let p1 = w2s(grid_origin + axis2 * v + axis1 * (max1 + s)); frame.stroke( &canvas::Path::new(|b| { b.move_to(p0);