From b4e5ec2628f88b47f5cc264cd506a35cb986a6de Mon Sep 17 00:00:00 2001 From: "Surkov, Kirill" Date: Sun, 27 Jul 2025 05:35:06 +0300 Subject: [PATCH 1/7] Add 2D segment intersection --- crates/bevy_math/src/primitives/dim2.rs | 49 +++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index 76da9555fa5f3..8f533c95413bb 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -1521,6 +1521,32 @@ impl Segment2d { let t = projection_scaled / length_squared; self.vertices[0] + t * segment_vector } + + /// Returns the point of intersection between this [`Segment2d`] and another, if it exists. + #[inline(always)] + pub fn intersect(&self, other: Self) -> Option { + let p = self.point1(); + let q = other.point1(); + let r = self.scaled_direction(); + let s = other.scaled_direction(); + + let r_cross_s = r.perp_dot(s); + let q_minus_p = q - p; + + if r_cross_s != 0.0 { + let t = q_minus_p.perp_dot(s / r_cross_s); + let u = q_minus_p.perp_dot(r / r_cross_s); + + let t_in_range = (0.0..=1.0).contains(&t); + let u_in_range = (0.0..=1.0).contains(&u); + + if t_in_range && u_in_range { + return Some(p + t * r); + } + } + + None + } } impl From<[Vec2; 2]> for Segment2d { @@ -2375,6 +2401,29 @@ mod tests { } } + #[test] + fn segment_intersect() { + let isec = Segment2d::new(Vec2::new(0.0, 0.0), Vec2::new(2.0, 2.0)) + .intersect(Segment2d::new(Vec2::new(0.0, 2.0), Vec2::new(2.0, 0.0))); + assert_eq!(isec, Some(Vec2::new(1.0, 1.0))); + + let isec = Segment2d::new(Vec2::new(0.0, 0.0), Vec2::new(2.0, 0.0)) + .intersect(Segment2d::new(Vec2::new(0.0, 1.0), Vec2::new(2.0, 1.0))); + assert_eq!(isec, None); + + let isec = Segment2d::new(Vec2::new(0.0, 0.0), Vec2::new(1.0, 1.0)) + .intersect(Segment2d::new(Vec2::new(2.0, 2.0), Vec2::new(3.0, 3.0))); + assert_eq!(isec, None); + + let isec = Segment2d::new(Vec2::new(0.0, 0.0), Vec2::new(1.0, 1.0)) + .intersect(Segment2d::new(Vec2::new(1.0, 1.0), Vec2::new(2.0, 0.0))); + assert_eq!(isec, Some(Vec2::new(1.0, 1.0))); + + let isec = Segment2d::new(Vec2::new(0.0, 0.0), Vec2::new(1.0, 0.0)) + .intersect(Segment2d::new(Vec2::new(2.0, 0.0), Vec2::new(3.0, 0.0))); + assert_eq!(isec, None); + } + #[test] fn circle_math() { let circle = Circle { radius: 3.0 }; From 94bb21d407132367f029399597555d335a868d1c Mon Sep 17 00:00:00 2001 From: Surkov Kirill Date: Mon, 28 Jul 2025 00:26:35 +0300 Subject: [PATCH 2/7] Reduce floating point ops Co-authored-by: IQuick 143 --- crates/bevy_math/src/primitives/dim2.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index 8f533c95413bb..68f6784659431 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -1534,8 +1534,8 @@ impl Segment2d { let q_minus_p = q - p; if r_cross_s != 0.0 { - let t = q_minus_p.perp_dot(s / r_cross_s); - let u = q_minus_p.perp_dot(r / r_cross_s); + let t = q_minus_p.perp_dot(s) / r_cross_s; + let u = q_minus_p.perp_dot(r) / r_cross_s; let t_in_range = (0.0..=1.0).contains(&t); let u_in_range = (0.0..=1.0).contains(&u); From d1f5c3f7364d30df688e61c002d62a9ccbbbac96 Mon Sep 17 00:00:00 2001 From: Surkov Kirill Date: Tue, 12 Aug 2025 07:03:23 +0300 Subject: [PATCH 3/7] Support collinear and degenerate segments in intersection --- crates/bevy_math/src/primitives/dim2.rs | 150 +++++++++++++++++++----- 1 file changed, 121 insertions(+), 29 deletions(-) diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index 68f6784659431..564ff0b468ac9 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -1523,29 +1523,57 @@ impl Segment2d { } /// Returns the point of intersection between this [`Segment2d`] and another, if it exists. + /// + /// If two segments are collinear and intersecting, returns the intersection point closest to the start of `self`. #[inline(always)] - pub fn intersect(&self, other: Self) -> Option { + pub fn intersect(&self, other: &Self) -> Option { let p = self.point1(); let q = other.point1(); let r = self.scaled_direction(); let s = other.scaled_direction(); + let pq = q - p; + let pq_cross_r = pq.perp_dot(r); + let pq_cross_s = pq.perp_dot(s); let r_cross_s = r.perp_dot(s); - let q_minus_p = q - p; if r_cross_s != 0.0 { - let t = q_minus_p.perp_dot(s) / r_cross_s; - let u = q_minus_p.perp_dot(r) / r_cross_s; - + // non parallel + let t = pq_cross_s / r_cross_s; + let u = pq_cross_r / r_cross_s; let t_in_range = (0.0..=1.0).contains(&t); let u_in_range = (0.0..=1.0).contains(&u); - - if t_in_range && u_in_range { - return Some(p + t * r); + (t_in_range && u_in_range).then_some(p + r * t) + } else if pq_cross_r == 0.0 || pq_cross_s == 0.0 { + // collinear + let r_dot_r = r.dot(r); + let s_dot_s = s.dot(s); + match (r_dot_r == 0.0, s_dot_s == 0.0) { + // point point + (true, true) => (p == q).then_some(p), + // segment point + (false, true) if pq_cross_r == 0.0 => { + let t = pq.dot(r) / r_dot_r; + (0.0..=1.0).contains(&t).then_some(q) + } + // point segment + (true, false) if pq_cross_s == 0.0 => { + let t = -pq.dot(s) / s_dot_s; + (0.0..=1.0).contains(&t).then_some(p) + } + // segment segment + (false, false) => { + let t0 = pq.dot(r) / r_dot_r; + let t1 = t0 + s.dot(r) / r_dot_r; + let (t_min, t_max) = if t0 < t1 { (t0, t1) } else { (t1, t0) }; + (t_max >= 0.0 && t_min <= 1.0).then_some(p + r * t_min.clamp(0.0, 1.0)) + } + _ => None, } + } else { + // parallel + None } - - None } } @@ -2403,25 +2431,89 @@ mod tests { #[test] fn segment_intersect() { - let isec = Segment2d::new(Vec2::new(0.0, 0.0), Vec2::new(2.0, 2.0)) - .intersect(Segment2d::new(Vec2::new(0.0, 2.0), Vec2::new(2.0, 0.0))); - assert_eq!(isec, Some(Vec2::new(1.0, 1.0))); - - let isec = Segment2d::new(Vec2::new(0.0, 0.0), Vec2::new(2.0, 0.0)) - .intersect(Segment2d::new(Vec2::new(0.0, 1.0), Vec2::new(2.0, 1.0))); - assert_eq!(isec, None); - - let isec = Segment2d::new(Vec2::new(0.0, 0.0), Vec2::new(1.0, 1.0)) - .intersect(Segment2d::new(Vec2::new(2.0, 2.0), Vec2::new(3.0, 3.0))); - assert_eq!(isec, None); - - let isec = Segment2d::new(Vec2::new(0.0, 0.0), Vec2::new(1.0, 1.0)) - .intersect(Segment2d::new(Vec2::new(1.0, 1.0), Vec2::new(2.0, 0.0))); - assert_eq!(isec, Some(Vec2::new(1.0, 1.0))); - - let isec = Segment2d::new(Vec2::new(0.0, 0.0), Vec2::new(1.0, 0.0)) - .intersect(Segment2d::new(Vec2::new(2.0, 0.0), Vec2::new(3.0, 0.0))); - assert_eq!(isec, None); + // non parallel with intersection + let segment1 = Segment2d::new(Vec2::new(0.0, 0.0), Vec2::new(2.0, 2.0)); + let segment2 = Segment2d::new(Vec2::new(0.0, 2.0), Vec2::new(2.0, 0.0)); + assert_eq!(segment1.intersect(&segment2), Some(Vec2::new(1.0, 1.0))); + assert_eq!(segment2.intersect(&segment1), Some(Vec2::new(1.0, 1.0))); + + // non parallel touching + let segment1 = Segment2d::new(Vec2::new(0.0, 0.0), Vec2::new(1.0, 1.0)); + let segment2 = Segment2d::new(Vec2::new(1.0, 1.0), Vec2::new(2.0, 0.0)); + assert_eq!(segment1.intersect(&segment2), Some(Vec2::new(1.0, 1.0))); + assert_eq!(segment2.intersect(&segment1), Some(Vec2::new(1.0, 1.0))); + + // non parallel without intersection + let segment1 = Segment2d::new(Vec2::new(0.0, 0.0), Vec2::new(2.0, 2.0)); + let segment2 = Segment2d::new(Vec2::new(0.0, 6.0), Vec2::new(6.0, 0.0)); + assert_eq!(segment1.intersect(&segment2), None); + assert_eq!(segment2.intersect(&segment1), None); + + // parallel non collinear + let segment1 = Segment2d::new(Vec2::new(0.0, 0.0), Vec2::new(2.0, 2.0)); + let segment2 = Segment2d::new(Vec2::new(0.0, 1.0), Vec2::new(2.0, 3.0)); + assert_eq!(segment1.intersect(&segment2), None); + assert_eq!(segment2.intersect(&segment1), None); + + // collinear without intersection + let segment1 = Segment2d::new(Vec2::new(0.0, 0.0), Vec2::new(2.0, 2.0)); + let segment2 = Segment2d::new(Vec2::new(3.0, 3.0), Vec2::new(4.0, 4.0)); + assert_eq!(segment1.intersect(&segment2), None); + assert_eq!(segment2.intersect(&segment1), None); + + // collinear overlapping + let segment1 = Segment2d::new(Vec2::new(0.0, 0.0), Vec2::new(2.0, 2.0)); + let segment2 = Segment2d::new(Vec2::new(1.0, 1.0), Vec2::new(3.0, 3.0)); + assert_eq!(segment1.intersect(&segment2), Some(Vec2::new(1.0, 1.0))); + assert_eq!(segment2.intersect(&segment1), Some(Vec2::new(1.0, 1.0))); + + // collinear touching + let segment1 = Segment2d::new(Vec2::new(0.0, 0.0), Vec2::new(1.0, 1.0)); + let segment2 = Segment2d::new(Vec2::new(1.0, 1.0), Vec2::new(2.0, 2.0)); + assert_eq!(segment1.intersect(&segment2), Some(Vec2::new(1.0, 1.0))); + assert_eq!(segment2.intersect(&segment1), Some(Vec2::new(1.0, 1.0))); + + // collinear with full overlap + let segment1 = Segment2d::new(Vec2::new(0.0, 0.0), Vec2::new(1.0, 1.0)); + let segment2 = Segment2d::new(Vec2::new(0.0, 0.0), Vec2::new(1.0, 1.0)); + assert_eq!(segment1.intersect(&segment2), Some(Vec2::new(0.0, 0.0))); + assert_eq!(segment2.intersect(&segment1), Some(Vec2::new(0.0, 0.0))); + + // collinear opposite directions + let segment1 = Segment2d::new(Vec2::new(0.0, 0.0), Vec2::new(2.0, 2.0)); + let segment2 = Segment2d::new(Vec2::new(3.0, 3.0), Vec2::new(1.0, 1.0)); + assert_eq!(segment1.intersect(&segment2), Some(Vec2::new(1.0, 1.0))); + assert_eq!(segment2.intersect(&segment1), Some(Vec2::new(2.0, 2.0))); + + // segment and dot with intersection + let segment1 = Segment2d::new(Vec2::new(0.0, 0.0), Vec2::new(2.0, 2.0)); + let segment2 = Segment2d::new(Vec2::new(1.0, 1.0), Vec2::new(1.0, 1.0)); + assert_eq!(segment1.intersect(&segment2), Some(Vec2::new(1.0, 1.0))); + assert_eq!(segment2.intersect(&segment1), Some(Vec2::new(1.0, 1.0))); + + // segment and dot touching + let segment1 = Segment2d::new(Vec2::new(0.0, 0.0), Vec2::new(2.0, 2.0)); + let segment2 = Segment2d::new(Vec2::new(2.0, 2.0), Vec2::new(2.0, 2.0)); + assert_eq!(segment1.intersect(&segment2), Some(Vec2::new(2.0, 2.0))); + assert_eq!(segment2.intersect(&segment1), Some(Vec2::new(2.0, 2.0))); + + // segment and dot without intersection + let segment1 = Segment2d::new(Vec2::new(0.0, 0.0), Vec2::new(2.0, 2.0)); + let segment2 = Segment2d::new(Vec2::new(1.0, 2.0), Vec2::new(1.0, 2.0)); + assert_eq!(segment1.intersect(&segment2), None); + assert_eq!(segment2.intersect(&segment1), None); + + // dot and dot with intersection + let segment1 = Segment2d::new(Vec2::new(1.0, 1.0), Vec2::new(1.0, 1.0)); + let segment2 = Segment2d::new(Vec2::new(1.0, 1.0), Vec2::new(1.0, 1.0)); + assert_eq!(segment1.intersect(&segment2), Some(Vec2::new(1.0, 1.0))); + assert_eq!(segment2.intersect(&segment1), Some(Vec2::new(1.0, 1.0))); + + // dot and dot without intersection + let segment1 = Segment2d::new(Vec2::new(1.0, 1.0), Vec2::new(1.0, 1.0)); + let segment2 = Segment2d::new(Vec2::new(2.0, 2.0), Vec2::new(2.0, 2.0)); + assert_eq!(segment1.intersect(&segment2), None); + assert_eq!(segment2.intersect(&segment1), None); } #[test] From 5fff30cfed7505d32c28fa16f94daac7865570f4 Mon Sep 17 00:00:00 2001 From: Surkov Kirill Date: Tue, 12 Aug 2025 23:48:33 +0300 Subject: [PATCH 4/7] Add epsilon to floating point comparisons --- crates/bevy_math/src/primitives/dim2.rs | 27 +++++++++++++++---------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index 564ff0b468ac9..2def7fdee2222 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -1537,36 +1537,41 @@ impl Segment2d { let pq_cross_s = pq.perp_dot(s); let r_cross_s = r.perp_dot(s); - if r_cross_s != 0.0 { + if r_cross_s.abs() > f32::EPSILON { // non parallel let t = pq_cross_s / r_cross_s; let u = pq_cross_r / r_cross_s; - let t_in_range = (0.0..=1.0).contains(&t); - let u_in_range = (0.0..=1.0).contains(&u); + let t_in_range = (-f32::EPSILON..=1.0 + f32::EPSILON).contains(&t); + let u_in_range = (-f32::EPSILON..=1.0 + f32::EPSILON).contains(&u); (t_in_range && u_in_range).then_some(p + r * t) - } else if pq_cross_r == 0.0 || pq_cross_s == 0.0 { + } else if pq_cross_r.abs() < f32::EPSILON || pq_cross_s.abs() < f32::EPSILON { // collinear let r_dot_r = r.dot(r); let s_dot_s = s.dot(s); - match (r_dot_r == 0.0, s_dot_s == 0.0) { + match (r_dot_r.abs() < f32::EPSILON, s_dot_s.abs() < f32::EPSILON) { // point point - (true, true) => (p == q).then_some(p), + (true, true) => (pq.length_squared() < f32::EPSILON).then_some(p), // segment point - (false, true) if pq_cross_r == 0.0 => { + (false, true) if pq_cross_r.abs() < f32::EPSILON => { let t = pq.dot(r) / r_dot_r; - (0.0..=1.0).contains(&t).then_some(q) + (-f32::EPSILON..=1.0 + f32::EPSILON) + .contains(&t) + .then_some(q) } // point segment - (true, false) if pq_cross_s == 0.0 => { + (true, false) if pq_cross_s.abs() < f32::EPSILON => { let t = -pq.dot(s) / s_dot_s; - (0.0..=1.0).contains(&t).then_some(p) + (-f32::EPSILON..=1.0 + f32::EPSILON) + .contains(&t) + .then_some(p) } // segment segment (false, false) => { let t0 = pq.dot(r) / r_dot_r; let t1 = t0 + s.dot(r) / r_dot_r; let (t_min, t_max) = if t0 < t1 { (t0, t1) } else { (t1, t0) }; - (t_max >= 0.0 && t_min <= 1.0).then_some(p + r * t_min.clamp(0.0, 1.0)) + (t_max >= -f32::EPSILON && t_min <= 1.0 + f32::EPSILON) + .then_some(p + r * t_min.clamp(0.0, 1.0)) } _ => None, } From 9a3a23cce8df7a5276b7d5c672ab615b953978a9 Mon Sep 17 00:00:00 2001 From: Surkov Kirill Date: Tue, 12 Aug 2025 23:52:27 +0300 Subject: [PATCH 5/7] Improve variable naming for squared vector lengths --- crates/bevy_math/src/primitives/dim2.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index 2def7fdee2222..c11db4bbdd62c 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -1546,29 +1546,29 @@ impl Segment2d { (t_in_range && u_in_range).then_some(p + r * t) } else if pq_cross_r.abs() < f32::EPSILON || pq_cross_s.abs() < f32::EPSILON { // collinear - let r_dot_r = r.dot(r); - let s_dot_s = s.dot(s); - match (r_dot_r.abs() < f32::EPSILON, s_dot_s.abs() < f32::EPSILON) { + let r_len2 = r.length_squared(); + let s_len2 = s.length_squared(); + match (r_len2.abs() < f32::EPSILON, s_len2.abs() < f32::EPSILON) { // point point (true, true) => (pq.length_squared() < f32::EPSILON).then_some(p), // segment point (false, true) if pq_cross_r.abs() < f32::EPSILON => { - let t = pq.dot(r) / r_dot_r; + let t = pq.dot(r) / r_len2; (-f32::EPSILON..=1.0 + f32::EPSILON) .contains(&t) .then_some(q) } // point segment (true, false) if pq_cross_s.abs() < f32::EPSILON => { - let t = -pq.dot(s) / s_dot_s; + let t = -pq.dot(s) / s_len2; (-f32::EPSILON..=1.0 + f32::EPSILON) .contains(&t) .then_some(p) } // segment segment (false, false) => { - let t0 = pq.dot(r) / r_dot_r; - let t1 = t0 + s.dot(r) / r_dot_r; + let t0 = pq.dot(r) / r_len2; + let t1 = t0 + s.dot(r) / r_len2; let (t_min, t_max) = if t0 < t1 { (t0, t1) } else { (t1, t0) }; (t_max >= -f32::EPSILON && t_min <= 1.0 + f32::EPSILON) .then_some(p + r * t_min.clamp(0.0, 1.0)) From db06f267addedf9085d8d9af9dffab685a01e9bc Mon Sep 17 00:00:00 2001 From: Surkov Kirill Date: Tue, 12 Aug 2025 23:58:23 +0300 Subject: [PATCH 6/7] Use ops::abs instead of f32::abs --- crates/bevy_math/src/primitives/dim2.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index c11db4bbdd62c..9ee52449b1db7 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -1537,29 +1537,32 @@ impl Segment2d { let pq_cross_s = pq.perp_dot(s); let r_cross_s = r.perp_dot(s); - if r_cross_s.abs() > f32::EPSILON { + if ops::abs(r_cross_s) > f32::EPSILON { // non parallel let t = pq_cross_s / r_cross_s; let u = pq_cross_r / r_cross_s; let t_in_range = (-f32::EPSILON..=1.0 + f32::EPSILON).contains(&t); let u_in_range = (-f32::EPSILON..=1.0 + f32::EPSILON).contains(&u); (t_in_range && u_in_range).then_some(p + r * t) - } else if pq_cross_r.abs() < f32::EPSILON || pq_cross_s.abs() < f32::EPSILON { + } else if ops::abs(pq_cross_r) < f32::EPSILON || ops::abs(pq_cross_s) < f32::EPSILON { // collinear let r_len2 = r.length_squared(); let s_len2 = s.length_squared(); - match (r_len2.abs() < f32::EPSILON, s_len2.abs() < f32::EPSILON) { + match ( + ops::abs(r_len2) < f32::EPSILON, + ops::abs(s_len2) < f32::EPSILON, + ) { // point point (true, true) => (pq.length_squared() < f32::EPSILON).then_some(p), // segment point - (false, true) if pq_cross_r.abs() < f32::EPSILON => { + (false, true) if ops::abs(pq_cross_r) < f32::EPSILON => { let t = pq.dot(r) / r_len2; (-f32::EPSILON..=1.0 + f32::EPSILON) .contains(&t) .then_some(q) } // point segment - (true, false) if pq_cross_s.abs() < f32::EPSILON => { + (true, false) if ops::abs(pq_cross_s) < f32::EPSILON => { let t = -pq.dot(s) / s_len2; (-f32::EPSILON..=1.0 + f32::EPSILON) .contains(&t) From 6a600d8dc3b35776db41c40b440d99327c8dbec8 Mon Sep 17 00:00:00 2001 From: Surkov Kirill Date: Wed, 13 Aug 2025 01:02:30 +0300 Subject: [PATCH 7/7] Remove unnecessary ops::abs --- crates/bevy_math/src/primitives/dim2.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index 9ee52449b1db7..528d96bb7cf8e 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -1548,10 +1548,7 @@ impl Segment2d { // collinear let r_len2 = r.length_squared(); let s_len2 = s.length_squared(); - match ( - ops::abs(r_len2) < f32::EPSILON, - ops::abs(s_len2) < f32::EPSILON, - ) { + match (r_len2 < f32::EPSILON, s_len2 < f32::EPSILON) { // point point (true, true) => (pq.length_squared() < f32::EPSILON).then_some(p), // segment point