diff --git a/crates/fj-kernel/src/algorithms/cast_ray/edge.rs b/crates/fj-kernel/src/algorithms/cast_ray/edge.rs new file mode 100644 index 000000000..e7e88a62a --- /dev/null +++ b/crates/fj-kernel/src/algorithms/cast_ray/edge.rs @@ -0,0 +1,29 @@ +use fj_math::Segment; + +use crate::objects::{CurveKind, Edge}; + +use super::CastRay; + +impl CastRay<2> for Edge { + type Hit = as CastRay<2>>::Hit; + + fn cast_ray( + &self, + ray: super::HorizontalRayToTheRight<2>, + ) -> Option { + let line = match self.curve().kind() { + CurveKind::Line(line) => line, + CurveKind::Circle(_) => { + todo!("Casting rays against circles is not supported yet") + } + }; + + let points = self.vertices().expect_vertices().map(|vertex| { + let point = vertex.position(); + line.point_from_line_coords(point) + }); + let segment = Segment::from_points(points); + + segment.cast_ray(ray) + } +} diff --git a/crates/fj-kernel/src/algorithms/cast_ray/mod.rs b/crates/fj-kernel/src/algorithms/cast_ray/mod.rs index 7496fc786..7324ff289 100644 --- a/crates/fj-kernel/src/algorithms/cast_ray/mod.rs +++ b/crates/fj-kernel/src/algorithms/cast_ray/mod.rs @@ -1,5 +1,6 @@ //! Ray casting +mod edge; mod segment; pub use self::segment::RaySegmentHit; diff --git a/crates/fj-kernel/src/algorithms/contains/face_point.rs b/crates/fj-kernel/src/algorithms/contains/face_point.rs new file mode 100644 index 000000000..25b5a875b --- /dev/null +++ b/crates/fj-kernel/src/algorithms/contains/face_point.rs @@ -0,0 +1,156 @@ +use fj_math::Point; + +use crate::{ + algorithms::cast_ray::{CastRay, HorizontalRayToTheRight, RaySegmentHit}, + objects::Face, +}; + +use super::Contains; + +impl Contains> for Face { + fn contains(&self, point: &Point<2>) -> bool { + let ray = HorizontalRayToTheRight { origin: *point }; + + let mut num_hits = 0; + + for cycle in self.all_cycles() { + // We need to properly detect the ray passing the boundary at the + // "seam" of the polygon, i.e. the vertex between the last and the + // first segment. The logic in the loop properly takes care of that, + // as long as we initialize the `previous_hit` variable with the + // result of the last segment. + let mut previous_hit = cycle + .edges() + .last() + .copied() + .and_then(|edge| edge.cast_ray(ray)); + + for edge in cycle.edges() { + let hit = edge.cast_ray(ray); + + let count_hit = match (hit, previous_hit) { + (Some(RaySegmentHit::Segment), _) => { + // We're hitting a segment right-on. Clear case. + true + } + ( + Some(RaySegmentHit::UpperVertex), + Some(RaySegmentHit::LowerVertex), + ) + | ( + Some(RaySegmentHit::LowerVertex), + Some(RaySegmentHit::UpperVertex), + ) => { + // If we're hitting a vertex, only count it if we've hit + // the other kind of vertex right before. + // + // That means, we're passing through the polygon + // boundary at where two edges touch. Depending on the + // order in which edges are checked, we're seeing this + // as a hit to one edge's lower/upper vertex, then the + // other edge's opposite vertex. + // + // If we're seeing two of the same vertices in a row, + // we're not actually passing through the polygon + // boundary. Then we're just touching a vertex without + // passing through anything. + true + } + (Some(RaySegmentHit::Parallel), _) => { + // A parallel edge must be completely ignored. Its + // presence won't change anything, so we can treat it as + // if it wasn't there, and its neighbors were connected + // to each other. + continue; + } + _ => { + // Any other case is not a valid hit. + false + } + }; + + if count_hit { + num_hits += 1; + } + + previous_hit = hit; + } + } + + num_hits % 2 == 1 + } +} + +#[cfg(test)] +mod tests { + use fj_math::Point; + + use crate::{ + algorithms::Contains, + objects::{Face, Surface}, + }; + + #[test] + fn ray_hits_vertex_while_passing_outside() { + let face = Face::build(Surface::xy_plane()).polygon_from_points([ + [0., 0.], + [2., 1.], + [0., 2.], + ]); + + assert_contains_point(face, [1., 1.]); + } + + #[test] + fn ray_hits_vertex_at_cycle_seam() { + let face = Face::build(Surface::xy_plane()) + .polygon_from_points([[4., 2.], [0., 4.], [0., 0.]]) + .with_hole([[1., 1.], [2., 1.], [1., 3.]]); + + assert_contains_point(face, [1., 2.]); + } + + #[test] + fn ray_hits_vertex_while_staying_inside() { + let face = Face::build(Surface::xy_plane()).polygon_from_points([ + [0., 0.], + [2., 1.], + [3., 0.], + [3., 4.], + ]); + + assert_contains_point(face, [1., 1.]); + } + + #[test] + fn ray_hits_parallel_edge() { + // Ray passes face boundary at a vertex. + let face = Face::build(Surface::xy_plane()).polygon_from_points([ + [0., 0.], + [2., 1.], + [3., 1.], + [0., 2.], + ]); + assert_contains_point(face, [1., 1.]); + + // Ray hits a vertex, but doesn't pass face boundary there. + let face = Face::build(Surface::xy_plane()).polygon_from_points([ + [0., 0.], + [2., 1.], + [3., 1.], + [4., 0.], + [4., 5.], + ]); + assert_contains_point(face, [1., 1.]); + } + + fn assert_contains_point( + face: impl Into, + point: impl Into>, + ) { + let face = face.into(); + let point = point.into(); + + assert!(face.contains(&point)); + } +} diff --git a/crates/fj-kernel/src/algorithms/contains/mod.rs b/crates/fj-kernel/src/algorithms/contains/mod.rs new file mode 100644 index 000000000..e7ed67a27 --- /dev/null +++ b/crates/fj-kernel/src/algorithms/contains/mod.rs @@ -0,0 +1,11 @@ +mod face_point; + +/// Test whether an object or shape contains another +pub trait Contains { + /// Test whether an object or shape contains another + /// + /// Returns `true`, if `self` fully contains `other`, `false` otherwise. A + /// negative return value could mean that `other` is completely outside of + /// `self`, or that they intersect. + fn contains(&self, object: &T) -> bool; +} diff --git a/crates/fj-kernel/src/algorithms/mod.rs b/crates/fj-kernel/src/algorithms/mod.rs index 889f66288..29bde36fe 100644 --- a/crates/fj-kernel/src/algorithms/mod.rs +++ b/crates/fj-kernel/src/algorithms/mod.rs @@ -4,6 +4,7 @@ //! on their respective purpose. mod approx; +mod contains; mod reverse; mod sweep; mod transform; @@ -14,6 +15,7 @@ pub mod intersection; pub use self::{ approx::{CycleApprox, FaceApprox, InvalidTolerance, Tolerance}, + contains::Contains, reverse::reverse_face, sweep::sweep, transform::{transform_faces, TransformObject}, diff --git a/crates/fj-kernel/src/algorithms/triangulate/polygon.rs b/crates/fj-kernel/src/algorithms/triangulate/polygon.rs index b53aca5a7..081f076d0 100644 --- a/crates/fj-kernel/src/algorithms/triangulate/polygon.rs +++ b/crates/fj-kernel/src/algorithms/triangulate/polygon.rs @@ -131,6 +131,13 @@ impl Polygon { contains } + /// Check whether the polygon contains a point + /// + /// # Implementation Note + /// + /// This code is being duplicated by the `Contains>` implementation + /// for `Face`. It would be nice to be able to consolidate the duplication, + /// but this has turned out to be difficult. pub fn contains_point( &self, point: impl Into>,