diff --git a/src/app/mod.rs b/src/app/mod.rs index 492c373e..b56087a4 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -314,7 +314,7 @@ pub(super) struct OpenCADStudio { /// Active OTRACK alignment `(tracking_point, unit_direction)` when the /// cursor is on a tracking ray. Lets a typed distance place a point along /// the ray from the tracking point (issue #69). `None` when not aligned. - otrack_active: Option<(glam::Vec3, glam::Vec3)>, + otrack_active: Option<(glam::DVec3, glam::DVec3)>, /// Whether Tangent snap was enabled before a tangent-pick command started. pre_cmd_tangent: Option, /// Drawing defaults captured by ADDSELECTED before it adopts the template diff --git a/src/app/update/command.rs b/src/app/update/command.rs index ac6443e1..e73e7310 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -295,10 +295,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { // (issue #69). if let Some((base, dir)) = self.otrack_active { if let Some(dist) = crate::app::expr_eval::eval_number(text.trim()) { - // Typed distance along the tracking ray: keep the - // magnitude exact in f64 (ray origin/dir are the - // screen-space snapper's f32). - let pt = base.as_dvec3() + dir.as_dvec3() * dist; + let pt = base + dir * dist; self.last_point = Some(pt); self.dyn_user_reshaped = false; self.sync_dyn_fields(); diff --git a/src/app/update/dynamic.rs b/src/app/update/dynamic.rs index 0be8aa4f..fdc097e0 100644 --- a/src/app/update/dynamic.rs +++ b/src/app/update/dynamic.rs @@ -483,10 +483,7 @@ impl OpenCADStudio { .find_map(|f| f.buffer.clone()) { if let Some(dist) = crate::app::expr_eval::eval_number(text.trim()) { - // Distance is typed, so resolve in f64: the tracking ray - // origin/dir come from the screen-space snapper (f32) but - // the magnitude the user entered stays exact. - let pt = base.as_dvec3() + dir.as_dvec3() * dist; + let pt = base + dir * dist; self.last_point = Some(pt); for f in self.tabs[i].dyn_fields.iter_mut() { f.buffer = None; diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index c7705037..7f5be604 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -981,7 +981,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task { // to a tracking ray (and store the alignment so a typed // distance can place a point along it — issue #69). let otrack_hit = { - let snap_world = self.tabs[i].snap_result.map(|s| s.world.as_vec3()); + let snap_world = self.tabs[i].snap_result.map(|s| s.world); self.snapper.update_otrack_dwell( snap_world, &all_wires[..], @@ -1009,16 +1009,17 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task { } else { None }; - let ucs = self.tabs[i].scene.viewcube_ucs_mat(); + let (_, ucs_x, ucs_y, _) = self.tabs[i].ucs_xform().axes(); self.snapper.otrack_snap( - cursor_world.as_vec3(), + cursor_world, view_rot, eye, bounds, step, - self.last_point.map(|p| p.as_vec3()), + self.last_point, self.ortho_mode && !is_window_corner, - ucs, + ucs_x, + ucs_y, ) } else { None @@ -1041,7 +1042,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task { if let (Some(base), Some((dir, _))) = (self.last_point, self.snapper.parallel_ref) { - self.otrack_active = Some((base.as_vec3(), dir)); + self.otrack_active = Some((base, dir.as_dvec3())); } self.tabs[i].snap_result = Some(par); } @@ -1051,7 +1052,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task { let mut pt: glam::DVec3 = if let Some(h) = otrack_hit { // Tracking alignment wins over the free cursor; // ortho/polar don't re-constrain it. - h.aligned.as_dvec3() + h.aligned } else { // Snap runs in model space (viewport camera or the // model/paper view), so the result is already model. @@ -1364,39 +1365,21 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task { // point along the aligned direction (issue #69). if let Some(h) = otrack_hit { if !needs_entity { - let far = 1e5_f32; + let far = 1e5_f64; let far_pos = h.base + h.dir * far; let far_neg = h.base - h.dir * far; - previews.push(crate::scene::WireModel { - taper_widths: Vec::new(), - world_width: 0.0, - depth_override: None, - fill_is_3d: false, - pick_tris: Vec::new(), - pick_tris_low: Vec::new(), - dash_from_start: false, - dash_align_end: None, - text_verts: Vec::new(), - name: "__otrack_guide__".into(), - points: vec![ + let mut guide = crate::scene::WireModel::solid_f64( + "__otrack_guide__".into(), + vec![ [far_neg.x, far_neg.y, far_neg.z], [far_pos.x, far_pos.y, far_pos.z], ], - points_low: Vec::new(), - color: [0.2, 0.9, 0.5, 0.6], - selected: false, - aci: 0, - pattern_length: 0.8, - pattern: [0.5, -0.3, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - line_weight_px: 1.0, - snap_pts: vec![], - tangent_geoms: vec![], - key_vertices: vec![], - aabb: crate::scene::WireModel::UNBOUNDED_AABB, - plinegen: true, - fill_tris: vec![], - fill_tris_low: Vec::new(), - }); + [0.2, 0.9, 0.5, 0.6], + false, + ); + guide.pattern_length = 0.8; + guide.pattern = [0.5, -0.3, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]; + previews.push(guide); } } self.tabs[i].scene.set_preview_wires(previews); @@ -2056,14 +2039,23 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task { } else { None }; - let ucs = self.tabs[i].scene.viewcube_ucs_mat(); - self.snapper - .otrack_snap(raw.as_vec3(), view_rot, eye, bounds, step, self.last_point.map(|p| p.as_vec3()), self.ortho_mode && !is_window_corner, ucs) + let (_, ucs_x, ucs_y, _) = self.tabs[i].ucs_xform().axes(); + self.snapper.otrack_snap( + raw, + view_rot, + eye, + bounds, + step, + self.last_point, + self.ortho_mode && !is_window_corner, + ucs_x, + ucs_y, + ) } else { None }; if let Some(h) = otrack { - pt = h.aligned.as_dvec3(); + pt = h.aligned; if self.tabs[i].active_ucs.is_none() { pt.z = 0.0; } diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index e1bbbd9f..31c4b9cb 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -418,8 +418,8 @@ impl OpenCADStudio { None }; let ost_project = - |w: glam::Vec3, view_rot: glam::Mat4, eye: glam::DVec3, ob: iced::Rectangle| { - let ndc = view_rot.project_point3((w.as_dvec3() - eye).as_vec3()); + |w: glam::DVec3, view_rot: glam::Mat4, eye: glam::DVec3, ob: iced::Rectangle| { + let ndc = view_rot.project_point3((w - eye).as_vec3()); iced::Point::new( ob.x + (ndc.x + 1.0) * 0.5 * ob.width, ob.y + (1.0 - ndc.y) * 0.5 * ob.height, @@ -446,7 +446,7 @@ impl OpenCADStudio { match (otrack_proj, self.otrack_active) { (Some((view_rot, eye, ob)), Some((base, _dir))) => { let b = ost_project(base, view_rot, eye, ob); - let a = ost_project(tab.last_cursor_world.as_vec3(), view_rot, eye, ob); + let a = ost_project(tab.last_cursor_world, view_rot, eye, ob); (b.x.is_finite() && a.x.is_finite()).then_some((b, a)) } _ => None, @@ -455,7 +455,7 @@ impl OpenCADStudio { let parallel_ref_marker: Option = match (otrack_proj, self.snapper.parallel_ref) { (Some((view_rot, eye, ob)), Some((_, pt))) => { - let s = ost_project(pt, view_rot, eye, ob); + let s = ost_project(pt.as_dvec3(), view_rot, eye, ob); (s.x.is_finite() && s.y.is_finite()).then_some(s) } _ => None, diff --git a/src/snap.rs b/src/snap.rs index 024121d8..36e675bb 100644 --- a/src/snap.rs +++ b/src/snap.rs @@ -4,7 +4,7 @@ //! Endpoint, Midpoint, Center, Node, Quadrant, Intersection, //! Extension, Insertion, Perpendicular, Nearest, ApparentIntersection, Grid, Tangent -use glam::{Mat4, Vec3}; +use glam::{DVec3, Mat4, Vec3}; use iced::time::Instant; use iced::{Point, Rectangle}; @@ -80,11 +80,11 @@ pub struct SnapResult { #[derive(Debug, Clone, Copy)] pub struct OtrackHit { /// Cursor projected onto the tracking ray. - pub aligned: Vec3, + pub aligned: DVec3, /// Unit ray direction toward the cursor side (for typed-distance entry). - pub dir: Vec3, + pub dir: DVec3, /// The tracking point the ray emanates from. - pub base: Vec3, + pub base: DVec3, } // ── Snapper ─────────────────────────────────────────────────────────────── @@ -108,16 +108,16 @@ pub struct Snapper { /// Object Snap Tracking on/off (F11). pub otrack_enabled: bool, /// Acquired OST points (world XZ, Y=0 plane). - pub tracking_points: Vec, + pub tracking_points: Vec, /// Edge directions at each acquired point (parallel to `tracking_points`): /// the line direction of every wire segment meeting at that corner, so /// OTRACK can offer an alignment ray along a segment's extension, not only /// the ortho/polar axes. Pulling the cursor along an acquired corner's edge /// then locks to that line (#219). Empty for a point that is not a segment /// endpoint (e.g. a midpoint or centre acquisition). - pub tracking_dirs: Vec>, + pub tracking_dirs: Vec>, /// Last snap world position (for dwell detection). - pub last_snap_world: Option, + pub last_snap_world: Option, /// When the cursor first rested near `last_snap_world`. pub dwell_since: Option, /// Whether the current dwell already acquired/removed a point (fire once). @@ -231,14 +231,11 @@ impl Snapper { if self.tracking_points.is_empty() { return false; } - let pf = p.as_vec3(); - let tol = 1e-4_f32.max(4e-7 * pf.x.abs().max(pf.y.abs())); + let tol = 1e-8_f64.max(2e-12 * p.x.abs().max(p.y.abs())); let tol2 = tol * tol; - self.tracking_points.iter().any(|t| { - let dx = t.x - pf.x; - let dy = t.y - pf.y; - dx * dx + dy * dy < tol2 - }) + self.tracking_points + .iter() + .any(|t| (*t - p).length_squared() < tol2) } pub fn toggle_global(&mut self) { @@ -283,7 +280,7 @@ impl Snapper { /// `snap_world` is the current snap result world point (if any). pub fn update_otrack_dwell( &mut self, - snap_world: Option, + snap_world: Option, wires: &[WireModel], view_rot: glam::Mat4, eye: glam::DVec3, @@ -325,8 +322,8 @@ impl Snapper { Some(p) => { // Convert to screen to measure pixel distance. let is_same = if let Some(prev) = self.last_snap_world { - let dp = world_to_screen(p.as_dvec3(), view_rot, eye, bounds); - let dp2 = world_to_screen(prev.as_dvec3(), view_rot, eye, bounds); + let dp = world_to_screen(p, view_rot, eye, bounds); + let dp2 = world_to_screen(prev, view_rot, eye, bounds); let dx = dp.x - dp2.x; let dy = dp.y - dp2.y; (dx * dx + dy * dy).sqrt() < DWELL_PX @@ -343,7 +340,7 @@ impl Snapper { // otherwise acquire it. let existing = self.tracking_points.iter().position(|t| { let d = (*t - p).length(); - d < self.grid_spacing * 0.1 + d < self.grid_spacing as f64 * 0.1 }); match existing { Some(idx) => { @@ -372,11 +369,11 @@ impl Snapper { /// it is already tracked; drops the oldest when the 4-point cap is reached. /// Edge directions are scanned once here, at acquisition, so OTRACK can align /// to a segment's extension without rescanning geometry per move (#219). - fn acquire_tracking_point(&mut self, p: Vec3, wires: &[WireModel], endpoints_only: bool) { + fn acquire_tracking_point(&mut self, p: DVec3, wires: &[WireModel], endpoints_only: bool) { if self .tracking_points .iter() - .any(|t| (*t - p).length() < self.grid_spacing * 0.1) + .any(|t| (*t - p).length() < self.grid_spacing as f64 * 0.1) { return; } @@ -440,38 +437,40 @@ impl Snapper { /// cursor side, used for typed-distance entry), and the originating point. pub fn otrack_snap( &self, - cursor_world: Vec3, + cursor_world: DVec3, view_rot: glam::Mat4, eye: glam::DVec3, bounds: iced::Rectangle, polar_step_deg: Option, - last_point: Option, + last_point: Option, // Ortho on: the axis from `last_point` is a hard lock. Only crossings of // an acquired ray with that axis lock; single tracking rays are // suppressed so the cursor can't leave the ortho axis. (#218) ortho: bool, - // UCS→world rotation: tracking rays run along the UCS axes, matching - // ortho/polar. Identity = world-aligned rays. - ucs: glam::Mat4, + // UCS axes in world space: tracking rays follow the active UCS while + // their world geometry stays in f64. + ucs_x: DVec3, + ucs_y: DVec3, ) -> Option { if !self.otrack_enabled || self.tracking_points.is_empty() { return None; } - let cursor_screen = world_to_screen(cursor_world.as_dvec3(), view_rot, eye, bounds); + let cursor_screen = world_to_screen(cursor_world, view_rot, eye, bounds); // Use the same aperture as OSNAP so the catch distance is uniform. let r = self.osnap_radius_px; - let screen_dist = |w: Vec3| { - let s = world_to_screen(w.as_dvec3(), view_rot, eye, bounds); + let screen_dist = |w: DVec3| { + let s = world_to_screen(w, view_rot, eye, bounds); ((s.x - cursor_screen.x).powi(2) + (s.y - cursor_screen.y).powi(2)).sqrt() }; // Candidate angles in [0,180); each ray extends both ways via the // signed projection `t`, so 0°/90° cover horizontal/vertical. - let mut angles: Vec = Vec::new(); + let mut angles: Vec = Vec::new(); match polar_step_deg.filter(|s| *s > 1e-3) { Some(step) => { - let mut a = 0.0_f32; + let step = step as f64; + let mut a = 0.0_f64; while a < 180.0 - 1e-3 { angles.push(a); a += step; @@ -487,8 +486,8 @@ impl Snapper { // origin (a parallel pencil that only meets at that origin) are never // intersected with each other. struct Ray { - origin: Vec3, - dir: Vec3, + origin: DVec3, + dir: DVec3, group: usize, } let mut rays: Vec = Vec::new(); @@ -497,7 +496,7 @@ impl Snapper { let ar = adeg.to_radians(); rays.push(Ray { origin: tp, - dir: ucs.transform_vector3(Vec3::new(ar.cos(), ar.sin(), 0.0)), + dir: ucs_x * ar.cos() + ucs_y * ar.sin(), group: gi, }); } @@ -523,11 +522,11 @@ impl Snapper { if let Some(bp) = last_point { let d = tp - bp; let len2 = d.x * d.x + d.y * d.y; - if len2 > 1e-12 { + if len2 > 1e-24 { let inv = 1.0 / len2.sqrt(); rays.push(Ray { origin: bp, - dir: Vec3::new(d.x * inv, d.y * inv, 0.0), + dir: DVec3::new(d.x * inv, d.y * inv, 0.0), group: gi, }); } @@ -540,12 +539,13 @@ impl Snapper { const POLAR_GROUP: usize = usize::MAX; const ORTHO_GROUP: usize = usize::MAX - 1; if let (Some(step), Some(lp)) = (polar_step_deg.filter(|s| *s > 1e-3), last_point) { - let mut a = 0.0_f32; + let step = step as f64; + let mut a = 0.0_f64; while a < 180.0 - 1e-3 { let ar = a.to_radians(); rays.push(Ray { origin: lp, - dir: ucs.transform_vector3(Vec3::new(ar.cos(), ar.sin(), 0.0)), + dir: ucs_x * ar.cos() + ucs_y * ar.sin(), group: POLAR_GROUP, }); a += step; @@ -555,11 +555,11 @@ impl Snapper { // ortho axis locks on-axis (the useful corner-finding case). (#218) let ortho_lock = ortho && last_point.is_some(); if let (true, Some(lp)) = (ortho_lock, last_point) { - for &adeg in &[0.0_f32, 90.0] { + for &adeg in &[0.0_f64, 90.0] { let ar = adeg.to_radians(); rays.push(Ray { origin: lp, - dir: ucs.transform_vector3(Vec3::new(ar.cos(), ar.sin(), 0.0)), + dir: ucs_x * ar.cos() + ucs_y * ar.sin(), group: ORTHO_GROUP, }); } @@ -621,7 +621,7 @@ impl Snapper { for ray in rays.iter().take(otrack_ray_count) { let t = (cursor_world.x - ray.origin.x) * ray.dir.x + (cursor_world.y - ray.origin.y) * ray.dir.y; - let aligned = Vec3::new( + let aligned = DVec3::new( ray.origin.x + ray.dir.x * t, ray.origin.y + ray.dir.y * t, ray.origin.z, @@ -1507,17 +1507,13 @@ fn snap_priority(t: SnapType) -> u8 { /// alignment ray along each so the cursor can track a segment's extension, not /// just the ortho/polar axes (#219). Scanned once, at acquisition — not per /// move. Empty when `p` is not a segment endpoint (midpoint / centre / node). -fn edge_dirs_at(p: Vec3, wires: &[WireModel]) -> Vec { - // The acquired point is an f32 truncation of the true (f64) vertex, so its - // error grows with coordinate magnitude (~1 ULP ≈ 1.2e-7·mag). Scale the - // endpoint-match tolerance to a few multiples of that, with a tight floor - // near the origin — a fixed fraction would span metres at UTM scale and - // match unrelated vertices, while a magnitude-blind floor would miss the - // corner once f32 rounding exceeds it. - let tol = 1e-4_f32.max(4e-7 * p.x.abs().max(p.y.abs())); - let tol2 = (tol * tol) as f64; - let pd = p.as_dvec3(); - let mut dirs: Vec = Vec::new(); +fn edge_dirs_at(p: DVec3, wires: &[WireModel]) -> Vec { + // Acquired points and reconstructed wire vertices are both f64. Keep a + // small scale-aware window for double-single reconstruction residuals + // without allowing unrelated UTM-scale vertices to match. + let tol = 1e-8_f64.max(2e-12 * p.x.abs().max(p.y.abs())); + let tol2 = tol * tol; + let mut dirs: Vec = Vec::new(); 'outer: for wire in wires { let n = wire.points.len(); if n < 2 { @@ -1529,7 +1525,7 @@ fn edge_dirs_at(p: Vec3, wires: &[WireModel]) -> Vec { if !a.x.is_finite() || !b.x.is_finite() { continue; // NaN sentinel separates sub-paths } - if (a - pd).length_squared() >= tol2 && (b - pd).length_squared() >= tol2 { + if (a - p).length_squared() >= tol2 && (b - p).length_squared() >= tol2 { continue; // neither endpoint is the acquired corner } let seg = b - a; @@ -1537,7 +1533,7 @@ fn edge_dirs_at(p: Vec3, wires: &[WireModel]) -> Vec { if l < 1e-9 { continue; } - let d = Vec3::new((seg.x / l) as f32, (seg.y / l) as f32, 0.0); + let d = DVec3::new(seg.x / l, seg.y / l, 0.0); // Skip a direction already present (parallel within ~0.5°); the ray // is bidirectional, so opposite signs are the same alignment line. if dirs.iter().any(|e| (e.x * d.x + e.y * d.y).abs() > 0.99996) { @@ -1632,15 +1628,15 @@ fn seg_intersect_3d(a0: glam::DVec3, a1: glam::DVec3, b0: glam::DVec3, b1: glam: /// Intersection of two infinite lines in the XY plane, each given by an origin /// and a direction. Returns `None` when the lines are parallel. -fn line_intersect_xy(o1: Vec3, d1: Vec3, o2: Vec3, d2: Vec3) -> Option { +fn line_intersect_xy(o1: DVec3, d1: DVec3, o2: DVec3, d2: DVec3) -> Option { let cross = d1.x * d2.y - d1.y * d2.x; - if cross.abs() < 1e-9 { + if cross.abs() < 1e-15 { return None; } let ex = o2.x - o1.x; let ey = o2.y - o1.y; let t = (ex * d2.y - ey * d2.x) / cross; - Some(Vec3::new(o1.x + d1.x * t, o1.y + d1.y * t, o1.z)) + Some(DVec3::new(o1.x + d1.x * t, o1.y + d1.y * t, o1.z)) } /// Screen-space 2D segment intersection. Returns `(t, s)` parameters if found. @@ -1948,14 +1944,9 @@ mod ext_tests { // Nothing acquired → no endpoint is a live extension source (#262). assert!(!s.is_tracked_endpoint(glam::DVec3::new(10.0, 0.0, 0.0))); // Acquire an endpoint → only that vertex tracks. - s.tracking_points.push(Vec3::new(10.0, 0.0, 0.0)); + s.tracking_points.push(DVec3::new(10.0, 0.0, 0.0)); assert!(s.is_tracked_endpoint(glam::DVec3::new(10.0, 0.0, 0.0))); assert!(!s.is_tracked_endpoint(glam::DVec3::new(5.0, 0.0, 0.0))); - // The match tolerance scales with coordinate magnitude, so an acquired - // vertex at UTM scale still matches its f32-truncated tracking point. - let big = 1_234_567.0_f64; - s.tracking_points.push(Vec3::new(big as f32, 0.0, 0.0)); - assert!(s.is_tracked_endpoint(glam::DVec3::new(big, 0.0, 0.0))); } #[test] @@ -1973,13 +1964,13 @@ mod ext_tests { // Extension-driven acquisition (endpoints_only): a midpoint — like an // extension foot the cursor paused on — is ignored, so it can't fill the // buffer and evict the real endpoint (#262). - s.acquire_tracking_point(Vec3::new(5.0, 0.0, 0.0), &wires, true); + s.acquire_tracking_point(DVec3::new(5.0, 0.0, 0.0), &wires, true); assert!(s.tracking_points.is_empty()); // The genuine endpoint is acquired. - s.acquire_tracking_point(Vec3::new(10.0, 0.0, 0.0), &wires, true); + s.acquire_tracking_point(DVec3::new(10.0, 0.0, 0.0), &wires, true); assert_eq!(s.tracking_points.len(), 1); // OTRACK (endpoints_only = false) still acquires any snap point. - s.acquire_tracking_point(Vec3::new(5.0, 0.0, 0.0), &wires, false); + s.acquire_tracking_point(DVec3::new(5.0, 0.0, 0.0), &wires, false); assert_eq!(s.tracking_points.len(), 2); } @@ -1990,13 +1981,13 @@ mod ext_tests { s.osnap_radius_px = 10.0; // An acquired corner of an existing line, and the base point (first // point) of the line currently being drawn. - let corner = Vec3::new(4.0, 2.0, 0.0); + let corner = DVec3::new(8_000.0, 6_000.0, 0.0); s.tracking_points.push(corner); s.tracking_dirs.push(Vec::new()); - let base = Vec3::new(0.0, 0.0, 0.0); + let base = DVec3::new(0.0, 0.0, 0.0); - // A plain orthographic camera: world * 0.1 → NDC, eye at the origin. - let view_rot = Mat4::from_scale(Vec3::splat(0.1)); + // A plain orthographic camera: world * 0.0001 → NDC. + let view_rot = Mat4::from_scale(Vec3::splat(0.0001)); let eye = glam::DVec3::ZERO; let bounds = Rectangle { x: 0.0, @@ -2005,12 +1996,11 @@ mod ext_tests { height: 1000.0, }; - // Cursor past the corner along base→corner (≈26.57°, not an ortho/polar - // axis), nudged a hair off the line — only the new base→corner ray can - // lock it. + // Cursor 5000 units along the base→corner ray (3-4-5 direction), nudged + // a hair off it. The OTRACK result must retain that exact distance. let dir = (corner - base).normalize(); - let perp = Vec3::new(-dir.y, dir.x, 0.0); - let cursor = base + dir * 6.0 + perp * 0.05; + let perp = DVec3::new(-dir.y, dir.x, 0.0); + let cursor = base + dir * 5_000.0 + perp * 0.05; let hit = s .otrack_snap( @@ -2021,23 +2011,33 @@ mod ext_tests { None, Some(base), false, - Mat4::IDENTITY, + DVec3::X, + DVec3::Y, ) .expect("base→corner alignment should lock"); // The aligned point lies on the base→corner line, and the reported base // is the command's base point (so typed-distance runs from there). let off = hit.aligned - base; let cross = off.x * dir.y - off.y * dir.x; + assert!((off.length() - 5_000.0).abs() < 1e-10); assert!( - cross.abs() < 1e-3, + cross.abs() < 1e-12, "aligned point off the base→corner line: {:?}", hit.aligned ); - assert!((hit.base - base).length() < 1e-3, "ray base is not the command base"); + assert!((hit.base - base).length() < 1e-12, "ray base is not the command base"); // Without a base point the alignment does not exist (nothing to join). let none = s.otrack_snap( - cursor, view_rot, eye, bounds, None, None, false, Mat4::IDENTITY, + cursor, + view_rot, + eye, + bounds, + None, + None, + false, + DVec3::X, + DVec3::Y, ); assert!(none.is_none(), "no base point → no base→corner alignment"); }