feat(otrack): track the direction from the base point to an acquired corner

Once the first point of a segment is placed, OTRACK now offers an
alignment along the line joining that base point and an acquired
tracking point — the direction *between* them, not only the ortho/polar
axes or the corner's own edges. Acquire an existing corner while drawing
and the cursor locks onto the base→corner line, with typed-distance
measured from the base point.

Adds a base→corner ray per acquired point in otrack_snap, grouped with
that corner so it never self-intersects with the corner's other rays.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-07-08 10:34:31 +03:00
1 parent 4164ef7111
commit 5f64a5eb1b
1 file changed
+77
+77
View File
@@ -486,6 +486,24 @@ impl Snapper {
});
}
}
// Ray from the command's base point toward this acquired corner:
// once the first point is placed, the cursor can lock onto the line
// joining that base point and an existing corner — the direction
// *between* them, not only the ortho/polar axes or the corner's own
// edges. Grouped with the corner so it never self-intersects with
// that corner's other rays (they meet only at the corner).
if let Some(bp) = last_point {
let d = tp - bp;
let len2 = d.x * d.x + d.y * d.y;
if len2 > 1e-12 {
let inv = 1.0 / len2.sqrt();
rays.push(Ray {
origin: bp,
dir: Vec3::new(d.x * inv, d.y * inv, 0.0),
group: gi,
});
}
}
}
// OTRACK rays come first; the auxiliary rays appended below (polar from
// last_point, ortho axis from last_point) only participate in
@@ -1851,6 +1869,65 @@ mod ext_tests {
assert_eq!(s.tracking_points.len(), 2);
}
#[test]
fn base_to_corner_direction_is_tracked() {
let mut s = Snapper::default();
s.otrack_enabled = true;
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);
s.tracking_points.push(corner);
s.tracking_dirs.push(Vec::new());
let base = Vec3::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));
let eye = glam::DVec3::ZERO;
let bounds = Rectangle {
x: 0.0,
y: 0.0,
width: 1000.0,
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.
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 hit = s
.otrack_snap(
cursor,
view_rot,
eye,
bounds,
None,
Some(base),
false,
Mat4::IDENTITY,
)
.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!(
cross.abs() < 1e-3,
"aligned point off the base→corner line: {:?}",
hit.aligned
);
assert!((hit.base - base).length() < 1e-3, "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,
);
assert!(none.is_none(), "no base point → no base→corner alignment");
}
#[test]
fn tangent_points_are_perpendicular_to_the_radius() {
let c = Vec3::new(0.0, 0.0, 0.0);