From 481adba66bfabe27ece873fc303ac0fe1abc6b2a Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Mon, 22 Jun 2026 11:27:34 +0300 Subject: [PATCH] fix(ucs): grid axis lines and grid snap follow the active UCS Two leftovers from the UCS grid work: - The coloured X/Y/Z axis lines (draw_axes) were drawn at the world origin along world axes; they now run through the UCS origin along the UCS axes, matching the grid lines. - Grid snap rounded the cursor on the world XYZ lattice; it now rounds in the UCS grid frame (to-UCS, round, back-to-world) so it lands on the grid the user sees. The UCS origin (wire space) + rotation reach the snapper via DocumentTab::ucs_grid_basis; identity outside model space. Co-Authored-By: Claude Opus 4.8 --- src/app/document.rs | 17 +++++++++++++++ src/app/update.rs | 10 ++++++--- src/snap/mod.rs | 28 +++++++++++++++++++----- src/ui/overlay.rs | 53 +++++++++++++++++++++------------------------ 4 files changed, 72 insertions(+), 36 deletions(-) diff --git a/src/app/document.rs b/src/app/document.rs index 288377ac2..b2584528c 100644 --- a/src/app/document.rs +++ b/src/app/document.rs @@ -195,6 +195,23 @@ impl DocumentTab { self.scene.viewcube_ucs = self.ucs_xform().rotation_mat(); } + /// Grid origin (render/wire space) and UCS→world rotation for grid snap and + /// the grid overlay. Identity / origin-at-zero outside model space. + pub(super) fn ucs_grid_basis(&self) -> (glam::Vec3, glam::Mat4) { + if self.scene.current_layout != "Model" { + return (glam::Vec3::ZERO, glam::Mat4::IDENTITY); + } + let xf = self.ucs_xform(); + let (o, ..) = xf.axes(); + let wo = self.scene.world_offset; + let origin = glam::Vec3::new( + o.x - wo[0] as f32, + o.y - wo[1] as f32, + o.z - wo[2] as f32, + ); + (origin, xf.rotation_mat()) + } + pub(super) fn new_drawing(n: usize) -> Self { let mut scene = Scene::new(); linetypes::populate_document(&mut scene.document); diff --git a/src/app/update.rs b/src/app/update.rs index 7601d8fd7..897ff0834 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -2643,7 +2643,9 @@ impl OpenCADStudio { let all_wires = self.tabs[i].scene.hit_test_wires(); // Grip drag has no single rubber-band origin for a perp foot. self.snapper.from_point = None; - let snap_hit = self.snapper.snap(raw, p, &all_wires[..], vp_mat, bounds); + let (go, gr) = self.tabs[i].ucs_grid_basis(); + let snap_hit = + self.snapper.snap(raw, p, &all_wires[..], vp_mat, bounds, go, gr); let mut snapped = snap_hit.map(|s| s.world).unwrap_or(raw); self.tabs[i].snap_result = snap_hit; if let Some(s) = self.tabs[i].snap_result.as_mut() { @@ -2817,9 +2819,10 @@ impl OpenCADStudio { bounds, ) } else { + let (go, gr) = self.tabs[i].ucs_grid_basis(); self.snapper.from_point = self.last_point; self.snapper - .snap(cursor_world, p, &all_wires[..], view_proj, bounds) + .snap(cursor_world, p, &all_wires[..], view_proj, bounds, go, gr) }; // Object Snap Tracking: update dwell, then align the cursor @@ -3445,8 +3448,9 @@ impl OpenCADStudio { self.snapper .snap_tangent_only(raw, p, &all_wires[..], vp_mat, bounds) } else { + let (go, gr) = self.tabs[i].ucs_grid_basis(); self.snapper.from_point = self.last_point; - self.snapper.snap(raw, p, &all_wires[..], vp_mat, bounds) + self.snapper.snap(raw, p, &all_wires[..], vp_mat, bounds, go, gr) }; // snap.world is in paper-space (projected wire coords in MSPACE); // convert to model-space so commands receive consistent coordinates. diff --git a/src/snap/mod.rs b/src/snap/mod.rs index 641bfefab..af5ea443d 100644 --- a/src/snap/mod.rs +++ b/src/snap/mod.rs @@ -428,7 +428,16 @@ impl Snapper { dwell_acquired: false, from_point: None, }; - tmp.snap(cursor_world, cursor_screen, wires, view_proj, bounds) + // Tangent-only: Grid is disabled here, so the grid basis is irrelevant. + tmp.snap( + cursor_world, + cursor_screen, + wires, + view_proj, + bounds, + Vec3::ZERO, + Mat4::IDENTITY, + ) } /// Find the best snap candidate near the cursor. @@ -439,6 +448,10 @@ impl Snapper { wires: &[WireModel], view_proj: Mat4, bounds: Rectangle, + // Grid origin (render/wire space) and UCS→world rotation, so grid snap + // lands on the UCS grid the user sees. `(ZERO, IDENTITY)` = world grid. + grid_origin: Vec3, + grid_rot: Mat4, ) -> Option { if !self.snap_enabled { return None; @@ -727,10 +740,15 @@ impl Snapper { // ── Grid ─────────────────────────────────────────────────────────── if self.is_on(SnapType::Grid) { let s = self.grid_spacing; - let gx = (cursor_world.x / s).round() * s; - let gy = (cursor_world.y / s).round() * s; - let gz = (cursor_world.z / s).round() * s; - try_pt(Vec3::new(gx, gy, gz), SnapType::Grid); + // Round in the UCS grid frame, then map back to world. + let ax = grid_rot.transform_vector3(Vec3::X); + let ay = grid_rot.transform_vector3(Vec3::Y); + let az = grid_rot.transform_vector3(Vec3::Z); + let rel = cursor_world - grid_origin; + let ux = (rel.dot(ax) / s).round() * s; + let uy = (rel.dot(ay) / s).round() * s; + let uz = (rel.dot(az) / s).round() * s; + try_pt(grid_origin + ax * ux + ay * uy + az * uz, SnapType::Grid); } // ── Tangent ──────────────────────────────────────────────────────── diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs index 508e16ad9..3c83b9326 100644 --- a/src/ui/overlay.rs +++ b/src/ui/overlay.rs @@ -981,14 +981,21 @@ fn draw_grid( ); } - // World-space axes always drawn on top of the grid lines. + // 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)); + draw_axes(frame, vp, bounds, extent.max(10.0), grid_origin, grid_axes); } -// ── Coloured world-space axes ───────────────────────────────────────────── +// ── Coloured UCS axes ────────────────────────────────────────────────────── -fn draw_axes(frame: &mut canvas::Frame, vp: Mat4, bounds: iced::Rectangle, extent: f32) { +fn draw_axes( + frame: &mut canvas::Frame, + vp: Mat4, + bounds: iced::Rectangle, + extent: f32, + origin: Vec3, + axes: (Vec3, Vec3, Vec3), +) { let w2s = |world: Vec3| -> Point { let ndc = vp.project_point3(world); Point::new( @@ -997,35 +1004,25 @@ fn draw_axes(frame: &mut canvas::Frame, vp: Mat4, bounds: iced::Rectangle, exten ) }; let e = extent; + let (ax, ay, az) = axes; let axis_stroke = |r: f32, g: f32, b: f32| canvas::Stroke { width: 1.5, style: canvas::Style::Solid(Color { r, g, b, a: 0.85 }), ..Default::default() }; - // X — red - frame.stroke( - &canvas::Path::new(|p| { - p.move_to(w2s(Vec3::new(-e, 0.0, 0.0))); - p.line_to(w2s(Vec3::new(e, 0.0, 0.0))); - }), - axis_stroke(0.90, 0.20, 0.20), - ); - // Y — green - frame.stroke( - &canvas::Path::new(|p| { - p.move_to(w2s(Vec3::new(0.0, -e, 0.0))); - p.line_to(w2s(Vec3::new(0.0, e, 0.0))); - }), - axis_stroke(0.20, 0.85, 0.20), - ); - // Z — blue - frame.stroke( - &canvas::Path::new(|p| { - p.move_to(w2s(Vec3::new(0.0, 0.0, -e))); - p.line_to(w2s(Vec3::new(0.0, 0.0, e))); - }), - axis_stroke(0.20, 0.40, 0.90), - ); + // Axes run through the UCS origin along the UCS axis directions. + 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)); + }), + axis_stroke(r, g, b), + ); + }; + line(ax, 0.90, 0.20, 0.20); // X — red + line(ay, 0.20, 0.85, 0.20); // Y — green + line(az, 0.20, 0.40, 0.90); // Z — blue } // ── UCS icon ──────────────────────────────────────────────────────────────