Skip to content

Commit 0b94dcb

Browse files
committed
gh-50 Fix point-to-point touch detection and optimize fill indexing
- Use unsafe unchecked indexing in StoreFillsHandler for performance - Add point coincidence detection for single-vertex touches between shapes - Refactor TouchesHandler to return (boundary_contact, interior_overlap) tuple with early-exit on interior overlap - Make evaluate() generic over output type to support tuple returns - Add tests for point-to-point and segment endpoint touch scenarios
1 parent a238171 commit 0b94dcb

File tree

3 files changed

+133
-22
lines changed

3 files changed

+133
-22
lines changed

iOverlay/src/build/builder.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ impl FillHandler for StoreFillsHandler<'_> {
2929

3030
#[inline(always)]
3131
fn handle(&mut self, index: usize, fill: SegmentFill) -> ControlFlow<()> {
32-
self.fills[index] = fill;
32+
// fills is pre-allocated to segments.len() and index is guaranteed
33+
// to be in range by the sweep algorithm
34+
unsafe { *self.fills.get_unchecked_mut(index) = fill };
3335
ControlFlow::Continue(())
3436
}
3537

iOverlay/src/core/predicate.rs

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -65,39 +65,39 @@ impl FillHandler for InteriorsIntersectHandler {
6565

6666
/// Handler that checks if subject and clip shapes touch (boundaries intersect but interiors don't).
6767
///
68-
/// Returns `true` if the shapes share boundary points but their interiors don't overlap.
69-
/// Early-exits `false` on first interior overlap.
68+
/// Returns `(boundary_contact, interior_overlap)`. Early-exits with `(_, true)` on first
69+
/// interior overlap since that definitively means the shapes don't just touch.
7070
pub(crate) struct TouchesHandler {
71-
has_intersection: bool,
71+
has_boundary_contact: bool,
7272
}
7373

7474
impl TouchesHandler {
7575
pub(crate) fn new() -> Self {
7676
Self {
77-
has_intersection: false,
77+
has_boundary_contact: false,
7878
}
7979
}
8080
}
8181

8282
impl FillHandler for TouchesHandler {
83-
type Output = bool;
83+
type Output = (bool, bool); // (boundary_contact, interior_overlap)
8484

8585
#[inline(always)]
86-
fn handle(&mut self, _index: usize, fill: SegmentFill) -> ControlFlow<bool> {
87-
// Check interior overlap - if found, they don't just touch
86+
fn handle(&mut self, _index: usize, fill: SegmentFill) -> ControlFlow<(bool, bool)> {
87+
// Check interior overlap - early exit since this means they don't just touch
8888
if (fill & BOTH_TOP) == BOTH_TOP || (fill & BOTH_BOTTOM) == BOTH_BOTTOM {
89-
return ControlFlow::Break(false);
89+
return ControlFlow::Break((self.has_boundary_contact, true));
9090
}
91-
// Check boundary contact (both shapes contribute but on opposite sides)
91+
// Check boundary contact (both shapes contribute)
9292
if (fill & SUBJ_BOTH) != 0 && (fill & CLIP_BOTH) != 0 {
93-
self.has_intersection = true;
93+
self.has_boundary_contact = true;
9494
}
9595
ControlFlow::Continue(())
9696
}
9797

9898
#[inline(always)]
99-
fn finalize(self) -> bool {
100-
self.has_intersection
99+
fn finalize(self) -> (bool, bool) {
100+
(self.has_boundary_contact, false)
101101
}
102102
}
103103

@@ -225,15 +225,15 @@ mod tests {
225225
let fill = SUBJ_TOP | CLIP_BOTTOM;
226226
let result = handler.handle(0, fill);
227227
assert!(matches!(result, ControlFlow::Continue(())));
228-
assert!(handler.finalize());
228+
assert_eq!(handler.finalize(), (true, false)); // boundary contact, no interior overlap
229229
}
230230

231231
#[test]
232232
fn test_touches_handler_interior_overlap() {
233233
let mut handler = TouchesHandler::new();
234234
let fill = SUBJ_TOP | CLIP_TOP;
235235
let result = handler.handle(0, fill);
236-
assert!(matches!(result, ControlFlow::Break(false)));
236+
assert!(matches!(result, ControlFlow::Break((_, true)))); // early exit on interior overlap
237237
}
238238

239239
#[test]
@@ -242,7 +242,7 @@ mod tests {
242242
let fill = SUBJ_TOP;
243243
let result = handler.handle(0, fill);
244244
assert!(matches!(result, ControlFlow::Continue(())));
245-
assert!(!handler.finalize());
245+
assert_eq!(handler.finalize(), (false, false)); // no boundary contact, no interior overlap
246246
}
247247

248248
#[test]

iOverlay/src/core/relate.rs

Lines changed: 115 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,45 @@ use alloc::vec::Vec;
1111
use i_float::int::point::IntPoint;
1212
use i_shape::int::shape::{IntContour, IntShape};
1313

14+
/// Checks if any vertex from a subject segment coincides with any vertex from a clip segment.
15+
///
16+
/// Collects all endpoints, sorts by (x, y), then scans for adjacent points from different shapes.
17+
fn has_point_coincidence(segments: &[Segment<ShapeCountBoolean>]) -> bool {
18+
// Collect all endpoints with a flag indicating if they're from subj (true) or clip (false)
19+
let mut points: Vec<(i32, i32, bool, bool)> = Vec::with_capacity(segments.len() * 2);
20+
21+
for seg in segments {
22+
let is_subj = seg.count.subj != 0;
23+
let is_clip = seg.count.clip != 0;
24+
points.push((seg.x_segment.a.x, seg.x_segment.a.y, is_subj, is_clip));
25+
points.push((seg.x_segment.b.x, seg.x_segment.b.y, is_subj, is_clip));
26+
}
27+
28+
// Sort by (x, y)
29+
points.sort_unstable_by(|a, b| (a.0, a.1).cmp(&(b.0, b.1)));
30+
31+
// Scan for adjacent points at the same location from different shapes
32+
let mut i = 0;
33+
while i < points.len() {
34+
let (x, y, _, _) = points[i];
35+
let mut has_subj = false;
36+
let mut has_clip = false;
37+
38+
// Collect all points at this location
39+
while i < points.len() && points[i].0 == x && points[i].1 == y {
40+
has_subj |= points[i].2;
41+
has_clip |= points[i].3;
42+
i += 1;
43+
}
44+
45+
if has_subj && has_clip {
46+
return true;
47+
}
48+
}
49+
50+
false
51+
}
52+
1453
/// Overlay structure optimized for spatial predicate evaluation.
1554
///
1655
/// `PredicateOverlay` provides efficient spatial relationship testing between
@@ -54,22 +93,25 @@ impl PredicateOverlay {
5493
}
5594
}
5695

57-
fn evaluate<H: FillHandler<Output = bool>>(&mut self, handler: H) -> bool {
96+
fn evaluate<T: Default, H: FillHandler<Output = T>>(&mut self, handler: H) -> T {
5897
if self.segments.is_empty() {
59-
return false;
98+
return T::default();
6099
}
61100
self.split_solver.split_segments(&mut self.segments, &self.solver);
62101
if self.segments.is_empty() {
63-
return false;
102+
return T::default();
64103
}
65104
self.sweep_runner
66105
.run_with_fill_rule(self.fill_rule, &self.solver, &self.segments, handler)
67106
}
68107

69108
/// Returns `true` if the subject and clip shapes intersect (share any point).
109+
///
110+
/// This includes both interior overlap and boundary contact (including single-point touches).
70111
#[inline]
71112
pub fn intersects(&mut self) -> bool {
72-
self.evaluate(IntersectsHandler)
113+
// Check sweep first (handles most cases), then fall back to point coincidence
114+
self.evaluate(IntersectsHandler) || has_point_coincidence(&self.segments)
73115
}
74116

75117
/// Returns `true` if the interiors of subject and clip shapes overlap.
@@ -84,10 +126,17 @@ impl PredicateOverlay {
84126
/// Returns `true` if subject and clip shapes touch (boundaries intersect but interiors don't).
85127
///
86128
/// This returns `true` when shapes share boundary points (edges or vertices)
87-
/// but their interiors don't overlap.
129+
/// but their interiors don't overlap. This includes single-point vertex touches.
88130
#[inline]
89131
pub fn touches(&mut self) -> bool {
90-
self.evaluate(TouchesHandler::new())
132+
let (boundary_contact, interior_overlap) = self.evaluate(TouchesHandler::new());
133+
if interior_overlap {
134+
return false;
135+
}
136+
if boundary_contact {
137+
return true;
138+
}
139+
has_point_coincidence(&self.segments)
91140
}
92141

93142
/// Returns `true` if subject is completely within clip.
@@ -238,4 +287,64 @@ mod tests {
238287
overlay.add_contour(&square(5, 5, 10), ShapeType::Clip);
239288
assert!(overlay.intersects());
240289
}
290+
291+
#[test]
292+
fn test_point_touch_intersects() {
293+
// Two squares touching at a single corner point (10, 10)
294+
let mut overlay = PredicateOverlay::new(16);
295+
overlay.add_contour(&square(0, 0, 10), ShapeType::Subject);
296+
overlay.add_contour(&square(10, 10, 10), ShapeType::Clip);
297+
assert!(overlay.intersects(), "point-to-point should intersect");
298+
}
299+
300+
#[test]
301+
fn test_point_touch_touches() {
302+
// Two squares touching at a single corner point (10, 10)
303+
let mut overlay = PredicateOverlay::new(16);
304+
overlay.add_contour(&square(0, 0, 10), ShapeType::Subject);
305+
overlay.add_contour(&square(10, 10, 10), ShapeType::Clip);
306+
assert!(overlay.touches(), "point-to-point should touch");
307+
}
308+
309+
#[test]
310+
fn test_point_touch_no_interior_intersect() {
311+
// Two squares touching at a single corner point (10, 10)
312+
let mut overlay = PredicateOverlay::new(16);
313+
overlay.add_contour(&square(0, 0, 10), ShapeType::Subject);
314+
overlay.add_contour(&square(10, 10, 10), ShapeType::Clip);
315+
assert!(!overlay.interiors_intersect(), "point touch has no interior intersection");
316+
}
317+
318+
#[test]
319+
fn test_segment_end_to_start_touch() {
320+
// Triangle where subject's segment endpoint touches clip's segment startpoint
321+
// Subject: triangle at (0,0), (10,0), (5,10)
322+
// Clip: triangle at (10,0), (20,0), (15,10)
323+
// They touch at exactly one point: (10,0)
324+
let subj = vec![
325+
IntPoint::new(0, 0),
326+
IntPoint::new(10, 0),
327+
IntPoint::new(5, 10),
328+
];
329+
let clip = vec![
330+
IntPoint::new(10, 0),
331+
IntPoint::new(20, 0),
332+
IntPoint::new(15, 10),
333+
];
334+
335+
let mut overlay = PredicateOverlay::new(16);
336+
overlay.add_contour(&subj, ShapeType::Subject);
337+
overlay.add_contour(&clip, ShapeType::Clip);
338+
assert!(overlay.intersects(), "segment b touching segment a should intersect");
339+
340+
overlay.clear();
341+
overlay.add_contour(&subj, ShapeType::Subject);
342+
overlay.add_contour(&clip, ShapeType::Clip);
343+
assert!(overlay.touches(), "segment b touching segment a should touch");
344+
345+
overlay.clear();
346+
overlay.add_contour(&subj, ShapeType::Subject);
347+
overlay.add_contour(&clip, ShapeType::Clip);
348+
assert!(!overlay.interiors_intersect(), "segment b touching segment a should not have interior intersection");
349+
}
241350
}

0 commit comments

Comments
 (0)