forked from 1b-agent/opencadstudio-upstream
fix(snap): Intersection requires a real 3D meeting, not a plan crossing (#335)
seg_intersect_xy found where two segments' XY projections cross and returned the point at z=0, ignoring height. Two lines at different Z whose plans cross therefore fired a real Intersection snap when they only *appear* to cross in top view — that case belongs to Apparent Intersection (view-space). Rename to seg_intersect_3d and require the segments to be at the same height at the crossing: compute Z on each at its crossing parameter and reject when they differ (tolerance scaled to coordinate magnitude). A genuine crossing now snaps at its true height instead of z=0; a plan-only crossing is left to Apparent Intersection. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
a84c1c0bcb
commit
824a508555
1 file changed
+58
-4
+58
-4
@@ -1068,7 +1068,7 @@ impl Snapper {
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if let Some(pt) = seg_intersect_xy(a0, a1, b0, b1) {
|
||||
if let Some(pt) = seg_intersect_3d(a0, a1, b0, b1) {
|
||||
try_pt(pt, SnapType::Intersection);
|
||||
}
|
||||
}
|
||||
@@ -1520,7 +1520,13 @@ fn perp_foot(query: glam::DVec3, p0: glam::DVec3, p1: glam::DVec3) -> Option<gla
|
||||
}
|
||||
|
||||
/// XY-plane segment-segment intersection. Returns `None` if parallel or outside.
|
||||
fn seg_intersect_xy(a0: glam::DVec3, a1: glam::DVec3, b0: glam::DVec3, b1: glam::DVec3) -> Option<glam::DVec3> {
|
||||
/// True 3D intersection of two segments: the point where their plan (XY)
|
||||
/// projections cross **and** both segments are at the same height there. Returns
|
||||
/// `None` when parallel in plan, out of range, or the segments only *appear* to
|
||||
/// cross in plan because they sit at different Z — a real Intersection requires
|
||||
/// the objects to actually meet, so that case is left to Apparent Intersection
|
||||
/// (the view-space crossing). (#335)
|
||||
fn seg_intersect_3d(a0: glam::DVec3, a1: glam::DVec3, b0: glam::DVec3, b1: glam::DVec3) -> Option<glam::DVec3> {
|
||||
let d1x = a1.x - a0.x;
|
||||
let d1y = a1.y - a0.y;
|
||||
let d2x = b1.x - b0.x;
|
||||
@@ -1528,7 +1534,7 @@ fn seg_intersect_xy(a0: glam::DVec3, a1: glam::DVec3, b0: glam::DVec3, b1: glam:
|
||||
let cross = d1x * d2y - d1y * d2x;
|
||||
if cross.abs() < 1e-9 {
|
||||
return None;
|
||||
} // parallel
|
||||
} // parallel in plan
|
||||
let ex = b0.x - a0.x;
|
||||
let ey = b0.y - a0.y;
|
||||
let t = (ex * d2y - ey * d2x) / cross;
|
||||
@@ -1536,7 +1542,16 @@ fn seg_intersect_xy(a0: glam::DVec3, a1: glam::DVec3, b0: glam::DVec3, b1: glam:
|
||||
if t < 0.0 || t > 1.0 || s < 0.0 || s > 1.0 {
|
||||
return None;
|
||||
}
|
||||
Some(glam::DVec3::new(a0.x + t * d1x, a0.y + t * d1y, 0.0))
|
||||
// The plans cross; the segments truly meet only if they are at the same
|
||||
// height at that crossing. Different Z ⇒ they merely overlap in plan, which
|
||||
// is an apparent (view-dependent) intersection, not a real one.
|
||||
let za = a0.z + t * (a1.z - a0.z);
|
||||
let zb = b0.z + s * (b1.z - b0.z);
|
||||
let tol = 1e-6_f64.max(1e-9 * za.abs().max(zb.abs()));
|
||||
if (za - zb).abs() > tol {
|
||||
return None;
|
||||
}
|
||||
Some(glam::DVec3::new(a0.x + t * d1x, a0.y + t * d1y, 0.5 * (za + zb)))
|
||||
}
|
||||
|
||||
/// Intersection of two infinite lines in the XY plane, each given by an origin
|
||||
@@ -1973,4 +1988,43 @@ mod ext_tests {
|
||||
// A point inside the circle has no external tangent.
|
||||
assert!(circle_tangent_points(Vec3::new(1.0, 0.0, 0.0), c, 5.0).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersection_requires_matching_z_not_just_plan_crossing() {
|
||||
use glam::DVec3;
|
||||
// Two segments whose plans cross at the origin, both at z = 0: a real
|
||||
// 3D intersection.
|
||||
let hit = seg_intersect_3d(
|
||||
DVec3::new(-1.0, 0.0, 0.0),
|
||||
DVec3::new(1.0, 0.0, 0.0),
|
||||
DVec3::new(0.0, -1.0, 0.0),
|
||||
DVec3::new(0.0, 1.0, 0.0),
|
||||
)
|
||||
.expect("coplanar crossing is a real intersection");
|
||||
assert!(hit.x.abs() < 1e-9 && hit.y.abs() < 1e-9 && hit.z.abs() < 1e-9);
|
||||
|
||||
// Same plan crossing, but the second segment sits at z = 5: the lines
|
||||
// only *appear* to cross in top view — not a real intersection (#335).
|
||||
assert!(
|
||||
seg_intersect_3d(
|
||||
DVec3::new(-1.0, 0.0, 0.0),
|
||||
DVec3::new(1.0, 0.0, 0.0),
|
||||
DVec3::new(0.0, -1.0, 5.0),
|
||||
DVec3::new(0.0, 1.0, 5.0),
|
||||
)
|
||||
.is_none(),
|
||||
"different-Z plan crossing must not be a real Intersection"
|
||||
);
|
||||
|
||||
// A real 3D crossing above the ground plane: both segments pass through
|
||||
// (0,0,5), so it snaps there at the true height.
|
||||
let hi = seg_intersect_3d(
|
||||
DVec3::new(-1.0, 0.0, 5.0),
|
||||
DVec3::new(1.0, 0.0, 5.0),
|
||||
DVec3::new(0.0, -1.0, 5.0),
|
||||
DVec3::new(0.0, 1.0, 5.0),
|
||||
)
|
||||
.expect("coplanar crossing at z=5");
|
||||
assert!((hi.z - 5.0).abs() < 1e-9, "z should be the true height, got {}", hi.z);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user