fix(snap): correct active intersection snapping

This commit is contained in:
Hakan Seven 2026-08-09 13:27:51 +03:00
commit f57c258ada
3 changed files with 118 additions and 232 deletions

View file

@ -865,7 +865,7 @@ impl Snapper {
// rotated or isometric grid the user sees.
grid_origin: Vec3,
grid_axes: (Vec3, Vec3, Vec3),
construction_cursor: Option<glam::DVec3>,
construction_ray: Option<(glam::DVec3, glam::DVec3)>,
) -> Option<SnapResult> {
// Object-snap selection is priority-then-distance, NOT nearest-wins.
// "Continuous" snaps (Nearest, Perpendicular, …) sit on the geometry
@ -1217,42 +1217,45 @@ impl Snapper {
}
}
// ── Intersection with active construction segment (#704) ──────────
//
// The existing Intersection pass below only compares document
// entities with each other. Also compare the rubber-band currently
// being drawn against existing geometry.
if self.is_on(SnapType::Intersection) {
if let (Some(from), Some(to)) = (self.from_point, construction_cursor) {
let from = from.as_dvec3();
if (to - from).length_squared() > 1e-18 {
// Active/deferred Intersection uses the actual nearby wires directly.
// The interaction candidate set may contain wires while its optional
// segment index is empty.
for wire in wires.iter() {
if !wire_in_range(wire) {
continue;
let mut try_ray_intersections = |origin: glam::DVec3, through: glam::DVec3| {
if (through - origin).length_squared() <= 1e-18 {
return;
}
if let Some(segments) = &local_segments {
for segment in segments {
if let Some(point) =
ray_segment_intersect_3d(origin, through, segment.a, segment.b)
{
if (point - origin).length_squared() > 1e-18 {
try_pt(point, SnapType::Intersection);
}
for index in 0..wire.points.len().saturating_sub(1) {
let b0 = wp_f64(wire, index);
let b1 = wp_f64(wire, index + 1);
if let Some(pt) = ray_segment_intersect_3d(from, to, b0, b1) {
if (pt - from).length_squared() > 1e-18 {
try_pt(pt, SnapType::Intersection);
}
}
}
} else {
for wire in wires.iter().filter(|wire| wire_in_range(wire)) {
for index in 0..wire.points.len().saturating_sub(1) {
if let Some(point) = ray_segment_intersect_3d(
origin,
through,
wp_f64(wire, index),
wp_f64(wire, index + 1),
) {
if (point - origin).length_squared() > 1e-18 {
try_pt(point, SnapType::Intersection);
}
}
}
}
}
};
if self.is_on(SnapType::Intersection) {
if let Some((origin, through)) = construction_ray {
try_ray_intersections(origin, through);
}
}
// ── Intersection with active perpendicular OTRACK ray (#704) ──────
//
// A perpendicular tracking ray is not part of the command's ordinary
// ORTHO/POLAR construction cursor. When the cursor is actually tracking
// one of those rays, allow Intersection to stop it on nearby geometry.
// Check engaged perpendicular tracking rays against nearby geometry.
if self.is_on(SnapType::Intersection)
&& self.otrack_enabled
&& self.is_on(SnapType::Perpendicular)
@ -1263,50 +1266,23 @@ impl Snapper {
};
for &dir in perp_dirs {
// Project the cursor onto this perpendicular tracking line.
let rel = cursor_world - origin;
let t = rel.x * dir.x + rel.y * dir.y;
let aligned = glam::DVec3::new(
origin.x + dir.x * t,
origin.y + dir.y * t,
origin.z,
);
// Only consider the ray when the cursor is genuinely tracking it.
// This prevents Intersection from behaving like Nearest.
let aligned_screen = world_to_screen(aligned, view_rot, eye, bounds);
if dist2(aligned_screen, cursor_screen) > radius2 {
continue;
}
// Perpendicular tracking is an infinite line, but once the cursor
// chooses a side we only want intersections forward on that side.
let dir_to_cursor = if t >= 0.0 { dir } else { -dir };
let through = origin + dir_to_cursor;
for wire in wires.iter() {
if !wire_in_range(wire) {
continue;
}
for index in 0..wire.points.len().saturating_sub(1) {
let b0 = wp_f64(wire, index);
let b1 = wp_f64(wire, index + 1);
if let Some(pt) =
ray_segment_intersect_3d(origin, through, b0, b1)
{
// Do not re-offer the tracking origin itself.
if (pt - origin).length_squared() > 1e-18 {
try_pt(pt, SnapType::Intersection);
}
}
}
}
try_ray_intersections(origin, origin + dir_to_cursor);
}
}
}
drop(try_ray_intersections);
// ── Intersection — segment-segment intersections (pairwise, gated) ──
if self.is_on(SnapType::Intersection)
@ -2103,13 +2079,7 @@ fn perp_foot(query: glam::DVec3, p0: glam::DVec3, p1: glam::DVec3) -> Option<gla
))
}
/// Intersection between a forward construction ray and an existing segment.
///
/// The ray starts at `ray_origin` and points toward `ray_through`, but extends
/// indefinitely forward. The existing geometry remains a finite segment.
///
/// Like `seg_intersect_3d`, this is a true 3D intersection: the XY projections
/// must cross and both geometries must have the same Z at the crossing.
/// True 3D intersection of a forward ray and a finite segment.
fn ray_segment_intersect_3d(
ray_origin: glam::DVec3,
ray_through: glam::DVec3,
@ -2132,8 +2102,6 @@ fn ray_segment_intersect_3d(
let t = (ex * d2y - ey * d2x) / cross;
let s = (ex * d1y - ey * d1x) / cross;
// `t >= 0`: only forward along the construction ray.
// `s` must remain inside the existing finite segment.
if t < 0.0 || s < 0.0 || s > 1.0 {
return None;
}