forked from 1b-agent/opencadstudio-upstream
feat(otrack): draw both vectors of an intersection lock
An OTRACK crossing is the meeting of two tracking vectors, but
`OtrackHit` carried only the one a typed distance is measured along, so
the overlay had a single guide to draw. Snapping to the crossing of two
vectors worked and looked like an unexplained point on one line.
Carry the crossed ray out of `otrack_snap` as `OtrackHit::cross`, keep it
on the app beside `otrack_active`, and let the overlay take a list of
guides rather than one. `otrack_active` still drives typed-distance
entry on its own, so distance entry is unchanged.
Fixes HakanSeven12/OpenCADStudio#1313
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TQzYZsXBwPZ5HwBaGitEUV
(cherry picked from commit 2f6caacece)
This commit is contained in:
1 parent
41101efaa5
commit
033319a4e3
7 files changed
+183
-23
No files matched your search
+144
-5
@@ -177,6 +177,12 @@ pub struct OtrackHit {
|
||||
pub dir: DVec3,
|
||||
/// The tracking point the ray emanates from.
|
||||
pub base: DVec3,
|
||||
/// The second ray of an intersection lock, as `(base, outward direction)`.
|
||||
/// A crossing is the meeting of two tracking vectors, and the user has to
|
||||
/// see both of them to read what the lock means; `base`/`dir` above carry
|
||||
/// only the one a typed distance is measured along. `None` for a
|
||||
/// single-ray alignment, which has no second vector. (#1313)
|
||||
pub cross: Option<(DVec3, DVec3)>,
|
||||
|
||||
pub kind: TrackingKind,
|
||||
}
|
||||
@@ -917,19 +923,35 @@ impl Snapper {
|
||||
if sd < r && best_x.as_ref().map_or(true, |(bd, _)| sd < *bd) {
|
||||
// Report an acquired tracking ray (not an auxiliary
|
||||
// last_point ray) as base/dir for typed-distance entry.
|
||||
let ot = if rays[i].group != POLAR_GROUP && rays[i].group != ORTHO_GROUP {
|
||||
&rays[i]
|
||||
let (ot, other) = if rays[i].group != POLAR_GROUP
|
||||
&& rays[i].group != ORTHO_GROUP
|
||||
{
|
||||
(&rays[i], &rays[j])
|
||||
} else {
|
||||
&rays[j]
|
||||
(&rays[j], &rays[i])
|
||||
};
|
||||
let t = (x.x - ot.origin.x) * ot.dir.x + (x.y - ot.origin.y) * ot.dir.y;
|
||||
let dir_out = if t >= 0.0 { ot.dir } else { -ot.dir };
|
||||
// Point each ray the way the crossing lies from its own
|
||||
// origin, so the guide drawn for it runs through the lock
|
||||
// rather than away from it.
|
||||
let outward = |ray: &Ray| {
|
||||
let t = (x.x - ray.origin.x) * ray.dir.x
|
||||
+ (x.y - ray.origin.y) * ray.dir.y;
|
||||
if t >= 0.0 {
|
||||
ray.dir
|
||||
} else {
|
||||
-ray.dir
|
||||
}
|
||||
};
|
||||
let dir_out = outward(ot);
|
||||
best_x = Some((
|
||||
sd,
|
||||
OtrackHit {
|
||||
aligned: x,
|
||||
dir: dir_out,
|
||||
base: ot.origin,
|
||||
// The vector the reported one crosses. Both are
|
||||
// drawn, so the intersection reads as one. (#1313)
|
||||
cross: Some((other.origin, outward(other))),
|
||||
kind: ot.kind,
|
||||
}
|
||||
));
|
||||
@@ -970,6 +992,8 @@ impl Snapper {
|
||||
aligned,
|
||||
dir: dir_out,
|
||||
base: ray.origin,
|
||||
// A single-ray alignment has no second vector.
|
||||
cross: None,
|
||||
kind: ray.kind,
|
||||
},
|
||||
));
|
||||
@@ -3660,6 +3684,121 @@ mod ext_tests {
|
||||
assert!(none.is_none(), "no base point → no base→corner alignment");
|
||||
}
|
||||
|
||||
/// #1313: an intersection lock must report both of the vectors it is the
|
||||
/// crossing of, so the overlay can draw both. Reporting only the one a
|
||||
/// typed distance runs along leaves the user with a single guide and no
|
||||
/// sign of what the point actually is.
|
||||
#[test]
|
||||
fn intersection_lock_reports_both_crossing_vectors() {
|
||||
let mut s = Snapper::default();
|
||||
s.otrack_enabled = true;
|
||||
s.osnap_radius_px = 10.0;
|
||||
// Two acquired corners. With no polar step each offers a horizontal and
|
||||
// a vertical ray, so their rays cross at (10, 0) and at (0, 5).
|
||||
let first = DVec3::new(0.0, 0.0, 0.0);
|
||||
let second = DVec3::new(10.0, 5.0, 0.0);
|
||||
for corner in [first, second] {
|
||||
s.tracking_points.push(corner);
|
||||
s.tracking_dirs.push(Vec::new());
|
||||
}
|
||||
|
||||
let view_rot = Mat4::from_scale(Vec3::splat(0.0001));
|
||||
let eye = glam::DVec3::ZERO;
|
||||
let bounds = Rectangle {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
width: 1000.0,
|
||||
height: 1000.0,
|
||||
};
|
||||
|
||||
// Cursor a hair off the crossing of the first corner's horizontal ray
|
||||
// and the second corner's vertical one.
|
||||
let crossing = DVec3::new(10.0, 0.0, 0.0);
|
||||
let hit = s
|
||||
.otrack_snap(
|
||||
crossing + DVec3::new(0.02, 0.02, 0.0),
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
DVec3::X,
|
||||
DVec3::Y,
|
||||
)
|
||||
.expect("the two rays cross inside the aperture");
|
||||
assert!(
|
||||
(hit.aligned - crossing).length() < 1e-9,
|
||||
"locked off the crossing: {:?}",
|
||||
hit.aligned
|
||||
);
|
||||
|
||||
let (cross_base, cross_dir) = hit.cross.expect("a crossing reports its second vector");
|
||||
let bases = [hit.base, cross_base];
|
||||
for corner in [first, second] {
|
||||
assert!(
|
||||
bases.iter().any(|b| (*b - corner).length() < 1e-9),
|
||||
"{corner:?} is not one of the two reported vectors: {bases:?}"
|
||||
);
|
||||
}
|
||||
|
||||
// Each vector runs from its own corner through the crossing, pointing
|
||||
// at it — the guides are drawn along these.
|
||||
for (base, dir) in [(hit.base, hit.dir), (cross_base, cross_dir)] {
|
||||
let off = crossing - base;
|
||||
assert!(
|
||||
(off.x * dir.y - off.y * dir.x).abs() < 1e-9,
|
||||
"the crossing is off the vector from {base:?} along {dir:?}"
|
||||
);
|
||||
assert!(
|
||||
off.dot(dir) > 0.0,
|
||||
"vector from {base:?} points away from the crossing"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The second vector belongs to a crossing alone: a plain single-ray
|
||||
/// alignment has nothing to cross, and must not draw a second guide.
|
||||
#[test]
|
||||
fn single_ray_alignment_reports_no_crossing_vector() {
|
||||
let mut s = Snapper::default();
|
||||
s.otrack_enabled = true;
|
||||
s.osnap_radius_px = 10.0;
|
||||
let corner = DVec3::new(10.0, 5.0, 0.0);
|
||||
s.tracking_points.push(corner);
|
||||
s.tracking_dirs.push(Vec::new());
|
||||
|
||||
let view_rot = Mat4::from_scale(Vec3::splat(0.0001));
|
||||
let eye = glam::DVec3::ZERO;
|
||||
let bounds = Rectangle {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
width: 1000.0,
|
||||
height: 1000.0,
|
||||
};
|
||||
|
||||
let hit = s
|
||||
.otrack_snap(
|
||||
DVec3::new(60.0, 5.02, 0.0),
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
DVec3::X,
|
||||
DVec3::Y,
|
||||
)
|
||||
.expect("the corner's horizontal ray catches the cursor");
|
||||
assert!(
|
||||
hit.cross.is_none(),
|
||||
"a single-ray alignment reported a crossing vector: {:?}",
|
||||
hit.cross
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tangent_points_are_perpendicular_to_the_radius() {
|
||||
let c = Vec3::new(0.0, 0.0, 0.0);
|
||||
|
||||
Reference in new issue
Block a user