-
-
Notifications
You must be signed in to change notification settings - Fork 119
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #941 from hannobraun/contains
Implement `Face`/`Point` containment test
- Loading branch information
Showing
6 changed files
with
206 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
use fj_math::Segment; | ||
|
||
use crate::objects::{CurveKind, Edge}; | ||
|
||
use super::CastRay; | ||
|
||
impl CastRay<2> for Edge { | ||
type Hit = <Segment<2> as CastRay<2>>::Hit; | ||
|
||
fn cast_ray( | ||
&self, | ||
ray: super::HorizontalRayToTheRight<2>, | ||
) -> Option<Self::Hit> { | ||
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
//! Ray casting | ||
mod edge; | ||
mod segment; | ||
|
||
pub use self::segment::RaySegmentHit; | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
use fj_math::Point; | ||
|
||
use crate::{ | ||
algorithms::cast_ray::{CastRay, HorizontalRayToTheRight, RaySegmentHit}, | ||
objects::Face, | ||
}; | ||
|
||
use super::Contains; | ||
|
||
impl Contains<Point<2>> 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<Face>, | ||
point: impl Into<Point<2>>, | ||
) { | ||
let face = face.into(); | ||
let point = point.into(); | ||
|
||
assert!(face.contains(&point)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
mod face_point; | ||
|
||
/// Test whether an object or shape contains another | ||
pub trait Contains<T> { | ||
/// 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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters