Skip to content

Commit 17f4b2f

Browse files
committed
gh-50 Add PointIntersectsHandler for point-only intersection detection
Add a new spatial predicate that returns true only when shapes touch by vertex coincidence without any edge overlap. This fills the gap between touches() (which includes edge contact) and interiors_intersect(). - Add PointIntersectsHandler with early exit on interior/boundary overlap - Add point_intersects() to PredicateOverlay and FloatPredicateOverlay - Add point_intersects() to FloatRelate trait
1 parent 643daab commit 17f4b2f

File tree

3 files changed

+224
-1
lines changed

3 files changed

+224
-1
lines changed

iOverlay/src/core/predicate.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,52 @@ impl FillHandler<ShapeCountBoolean> for TouchesHandler {
224224
}
225225
}
226226

227+
/// Handler that checks if subject and clip shapes intersect by point coincidence only.
228+
///
229+
/// Returns `true` if shapes share boundary vertices but NOT edges.
230+
/// - Returns `false` if there's interior overlap (early exit)
231+
/// - Returns `false` if there's edge/boundary contact (shared segments, early exit)
232+
/// - Returns `true` ONLY if shapes touch by point coincidence without any edge overlap
233+
pub(crate) struct PointIntersectsHandler {
234+
point_checker: PointCoincidenceChecker,
235+
}
236+
237+
impl PointIntersectsHandler {
238+
pub(crate) fn new(capacity: usize) -> Self {
239+
Self {
240+
point_checker: PointCoincidenceChecker::new(capacity),
241+
}
242+
}
243+
}
244+
245+
impl FillHandler<ShapeCountBoolean> for PointIntersectsHandler {
246+
type Output = bool;
247+
248+
#[inline(always)]
249+
fn handle(
250+
&mut self,
251+
_index: usize,
252+
segment: &Segment<ShapeCountBoolean>,
253+
fill: SegmentFill,
254+
) -> ControlFlow<bool> {
255+
// Interior overlap = not a point-only intersection (early exit false)
256+
if (fill & BOTH_TOP) == BOTH_TOP || (fill & BOTH_BOTTOM) == BOTH_BOTTOM {
257+
return ControlFlow::Break(false);
258+
}
259+
// Boundary contact (edge sharing) = not point-only (early exit false)
260+
if (fill & SUBJ_BOTH) != 0 && (fill & CLIP_BOTH) != 0 {
261+
return ControlFlow::Break(false);
262+
}
263+
self.point_checker.add_segment(segment, fill);
264+
ControlFlow::Continue(())
265+
}
266+
267+
#[inline(always)]
268+
fn finalize(self) -> bool {
269+
self.point_checker.has_coincidence()
270+
}
271+
}
272+
227273
/// Handler that checks if subject is completely within clip.
228274
///
229275
/// Returns `true` if everywhere the subject has fill, the clip also has fill
@@ -499,4 +545,51 @@ mod tests {
499545
assert!(matches!(result, ControlFlow::Continue(())));
500546
assert!(!handler.finalize());
501547
}
548+
549+
#[test]
550+
fn test_point_intersects_handler_point_only() {
551+
let mut handler = PointIntersectsHandler::new(10);
552+
// Subject segment ending at (10, 10)
553+
let seg1 = make_segment(0, 0, 10, 10, 1, 0);
554+
// Clip segment starting at (10, 10)
555+
let seg2 = make_segment(10, 10, 20, 20, 0, 1);
556+
let _ = handler.handle(0, &seg1, SUBJ_TOP);
557+
let _ = handler.handle(1, &seg2, CLIP_TOP);
558+
// Point coincidence without edge contact → true
559+
assert!(handler.finalize());
560+
}
561+
562+
#[test]
563+
fn test_point_intersects_handler_edge_contact() {
564+
// Segment belongs to both subject and clip (shared edge)
565+
let seg = make_segment(0, 0, 10, 0, 1, 1);
566+
let mut handler = PointIntersectsHandler::new(10);
567+
// Both shapes have fill on opposite sides (boundary contact)
568+
let fill = SUBJ_TOP | CLIP_BOTTOM;
569+
let result = handler.handle(0, &seg, fill);
570+
// Early exit false on boundary contact (edge sharing)
571+
assert!(matches!(result, ControlFlow::Break(false)));
572+
}
573+
574+
#[test]
575+
fn test_point_intersects_handler_interior_overlap() {
576+
let seg = make_segment(0, 0, 10, 0, 1, 1);
577+
let mut handler = PointIntersectsHandler::new(10);
578+
// Interior overlap (both shapes fill the same side)
579+
let fill = SUBJ_TOP | CLIP_TOP;
580+
let result = handler.handle(0, &seg, fill);
581+
// Early exit false on interior overlap
582+
assert!(matches!(result, ControlFlow::Break(false)));
583+
}
584+
585+
#[test]
586+
fn test_point_intersects_handler_no_contact() {
587+
let seg1 = make_segment(0, 0, 5, 5, 1, 0);
588+
let seg2 = make_segment(10, 10, 20, 20, 0, 1);
589+
let mut handler = PointIntersectsHandler::new(10);
590+
let _ = handler.handle(0, &seg1, SUBJ_TOP);
591+
let _ = handler.handle(1, &seg2, CLIP_TOP);
592+
// No contact at all → false
593+
assert!(!handler.finalize());
594+
}
502595
}

iOverlay/src/core/relate.rs

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
use crate::build::sweep::{FillHandler, SweepRunner};
22
use crate::core::fill_rule::FillRule;
33
use crate::core::overlay::ShapeType;
4-
use crate::core::predicate::{InteriorsIntersectHandler, IntersectsHandler, TouchesHandler, WithinHandler};
4+
use crate::core::predicate::{
5+
InteriorsIntersectHandler, IntersectsHandler, PointIntersectsHandler, TouchesHandler, WithinHandler,
6+
};
57
use crate::core::solver::Solver;
68
use crate::segm::boolean::ShapeCountBoolean;
79
use crate::segm::build::BuildSegments;
@@ -94,6 +96,16 @@ impl PredicateOverlay {
9496
self.evaluate(TouchesHandler::new(capacity))
9597
}
9698

99+
/// Returns `true` if subject and clip shapes intersect by point coincidence only.
100+
///
101+
/// This returns `true` when shapes share boundary vertices but NOT edges.
102+
/// Unlike `touches()`, this returns `false` for shapes that share edges.
103+
#[inline]
104+
pub fn point_intersects(&mut self) -> bool {
105+
let capacity = self.segments.len();
106+
self.evaluate(PointIntersectsHandler::new(capacity))
107+
}
108+
97109
/// Returns `true` if subject is completely within clip.
98110
///
99111
/// Subject is within clip if everywhere the subject has fill, the clip
@@ -487,4 +499,74 @@ mod tests {
487499
"triangle touching outer corner should not have interior intersection"
488500
);
489501
}
502+
503+
#[test]
504+
fn test_point_intersects_corner_to_corner() {
505+
// Two squares touching at a single corner point (10, 10)
506+
let mut overlay = PredicateOverlay::new(16);
507+
overlay.add_contour(&square(0, 0, 10), ShapeType::Subject);
508+
overlay.add_contour(&square(10, 10, 10), ShapeType::Clip);
509+
assert!(
510+
overlay.point_intersects(),
511+
"corner-to-corner should be point-only intersection"
512+
);
513+
}
514+
515+
#[test]
516+
fn test_point_intersects_edge_sharing() {
517+
// Two squares sharing an edge (not point-only)
518+
let mut overlay = PredicateOverlay::new(16);
519+
overlay.add_contour(&square(0, 0, 10), ShapeType::Subject);
520+
overlay.add_contour(&square(10, 0, 10), ShapeType::Clip);
521+
// touches() is true for edge sharing
522+
assert!(overlay.touches());
523+
524+
overlay.clear();
525+
overlay.add_contour(&square(0, 0, 10), ShapeType::Subject);
526+
overlay.add_contour(&square(10, 0, 10), ShapeType::Clip);
527+
// point_intersects() is false for edge sharing
528+
assert!(
529+
!overlay.point_intersects(),
530+
"edge sharing is not point-only intersection"
531+
);
532+
}
533+
534+
#[test]
535+
fn test_point_intersects_overlapping() {
536+
// Overlapping squares (not point-only)
537+
let mut overlay = PredicateOverlay::new(16);
538+
overlay.add_contour(&square(0, 0, 10), ShapeType::Subject);
539+
overlay.add_contour(&square(5, 5, 10), ShapeType::Clip);
540+
assert!(
541+
!overlay.point_intersects(),
542+
"overlapping shapes are not point-only intersection"
543+
);
544+
}
545+
546+
#[test]
547+
fn test_point_intersects_disjoint() {
548+
// Disjoint squares (no contact at all)
549+
let mut overlay = PredicateOverlay::new(16);
550+
overlay.add_contour(&square(0, 0, 10), ShapeType::Subject);
551+
overlay.add_contour(&square(20, 20, 10), ShapeType::Clip);
552+
assert!(
553+
!overlay.point_intersects(),
554+
"disjoint shapes have no point intersection"
555+
);
556+
}
557+
558+
#[test]
559+
fn test_point_intersects_triangle_vertex() {
560+
// Two triangles touching at a single vertex
561+
let tri1 = vec![IntPoint::new(0, 0), IntPoint::new(10, 0), IntPoint::new(5, 10)];
562+
let tri2 = vec![IntPoint::new(10, 0), IntPoint::new(20, 0), IntPoint::new(15, 10)];
563+
564+
let mut overlay = PredicateOverlay::new(16);
565+
overlay.add_contour(&tri1, ShapeType::Subject);
566+
overlay.add_contour(&tri2, ShapeType::Clip);
567+
assert!(
568+
overlay.point_intersects(),
569+
"triangles touching at vertex should be point-only intersection"
570+
);
571+
}
490572
}

iOverlay/src/float/relate.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,12 @@ impl<P: FloatPointCompatible<T>, T: FloatNumber> FloatPredicateOverlay<P, T> {
155155
self.overlay.touches()
156156
}
157157

158+
/// Returns `true` if subject and clip shapes intersect by point coincidence only.
159+
#[inline]
160+
pub fn point_intersects(&mut self) -> bool {
161+
self.overlay.point_intersects()
162+
}
163+
158164
/// Returns `true` if subject is completely within clip.
159165
#[inline]
160166
pub fn within(&mut self) -> bool {
@@ -221,6 +227,9 @@ where
221227
/// Returns `true` when shapes share boundary points but their interiors don't overlap.
222228
fn touches(&self, other: &R1) -> bool;
223229

230+
/// Returns `true` if this shape intersects another by point coincidence only.
231+
fn point_intersects(&self, other: &R1) -> bool;
232+
224233
/// Returns `true` if this shape is completely within another.
225234
///
226235
/// Subject is within clip if everywhere the subject has fill, the clip
@@ -260,6 +269,11 @@ where
260269
FloatPredicateOverlay::with_subj_and_clip(self, other).touches()
261270
}
262271

272+
#[inline]
273+
fn point_intersects(&self, other: &R1) -> bool {
274+
FloatPredicateOverlay::with_subj_and_clip(self, other).point_intersects()
275+
}
276+
263277
#[inline]
264278
fn within(&self, other: &R1) -> bool {
265279
FloatPredicateOverlay::with_subj_and_clip(self, other).within()
@@ -614,4 +628,38 @@ mod tests {
614628
overlay.add_source(&outer, crate::core::overlay::ShapeType::Clip);
615629
assert!(overlay.within());
616630
}
631+
632+
#[test]
633+
fn test_point_intersects_trait() {
634+
// Two squares touching at a single corner point (10, 10)
635+
let square1 = vec![[0.0, 0.0], [0.0, 10.0], [10.0, 10.0], [10.0, 0.0]];
636+
let square2 = vec![[10.0, 10.0], [10.0, 20.0], [20.0, 20.0], [20.0, 10.0]];
637+
638+
// Point-only intersection → true
639+
assert!(square1.point_intersects(&square2));
640+
assert!(square2.point_intersects(&square1));
641+
642+
// Edge-sharing squares (not point-only)
643+
let square3 = vec![[10.0, 0.0], [10.0, 10.0], [20.0, 10.0], [20.0, 0.0]];
644+
assert!(
645+
!square1.point_intersects(&square3),
646+
"edge sharing is not point-only"
647+
);
648+
// But they do touch
649+
assert!(square1.touches(&square3));
650+
651+
// Overlapping squares (not point-only)
652+
let square4 = vec![[5.0, 5.0], [5.0, 15.0], [15.0, 15.0], [15.0, 5.0]];
653+
assert!(
654+
!square1.point_intersects(&square4),
655+
"overlapping is not point-only"
656+
);
657+
658+
// Disjoint squares (no intersection)
659+
let square5 = vec![[100.0, 100.0], [100.0, 110.0], [110.0, 110.0], [110.0, 100.0]];
660+
assert!(
661+
!square1.point_intersects(&square5),
662+
"disjoint has no point intersection"
663+
);
664+
}
617665
}

0 commit comments

Comments
 (0)