Skip to content

Commit

Permalink
Merge pull request #941 from hannobraun/contains
Browse files Browse the repository at this point in the history
Implement `Face`/`Point` containment test
  • Loading branch information
hannobraun authored Aug 10, 2022
2 parents 9920ef8 + a73cb24 commit 78e3fb8
Show file tree
Hide file tree
Showing 6 changed files with 206 additions and 0 deletions.
29 changes: 29 additions & 0 deletions crates/fj-kernel/src/algorithms/cast_ray/edge.rs
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)
}
}
1 change: 1 addition & 0 deletions crates/fj-kernel/src/algorithms/cast_ray/mod.rs
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;
Expand Down
156 changes: 156 additions & 0 deletions crates/fj-kernel/src/algorithms/contains/face_point.rs
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));
}
}
11 changes: 11 additions & 0 deletions crates/fj-kernel/src/algorithms/contains/mod.rs
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;
}
2 changes: 2 additions & 0 deletions crates/fj-kernel/src/algorithms/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//! on their respective purpose.
mod approx;
mod contains;
mod reverse;
mod sweep;
mod transform;
Expand All @@ -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},
Expand Down
7 changes: 7 additions & 0 deletions crates/fj-kernel/src/algorithms/triangulate/polygon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,13 @@ impl Polygon {
contains
}

/// Check whether the polygon contains a point
///
/// # Implementation Note
///
/// This code is being duplicated by the `Contains<Point<2>>` 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<Point<2>>,
Expand Down

0 comments on commit 78e3fb8

Please sign in to comment.