Skip to content

Commit b15a9a8

Browse files
committed
gh-50 Add spatial predicates API with fixed-scale support
Add FloatPredicateOverlay and FloatRelate trait for efficient spatial relationship testing (intersects, touches, within, covers, disjoint). Features: - PredicateOverlay for integer-space predicate evaluation with early-exit - FloatPredicateOverlay wrapper handling float-to-int conversion - FloatRelate trait for ergonomic predicate methods on ShapeResource types - FixedScaleFloatRelate trait for fixed-scale precision predicates - with_adapter() and with_adapter_custom() constructors for custom adapters - with_subj_and_clip_fixed_scale() for fixed-scale precision
1 parent 8787822 commit b15a9a8

File tree

9 files changed

+1420
-53
lines changed

9 files changed

+1420
-53
lines changed

iOverlay/README.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ iOverlay powers polygon boolean operations in [geo](https://github.com/georust/g
2323
- [Boolean Operations](#boolean-operations)
2424
- [Simple Example](#simple-example)
2525
- [Overlay Rules](#overlay-rules)
26+
- [Spatial Predicates](#spatial-predicates)
2627
- [Custom Point Type Support](#custom-point-type-support)
2728
- [Slicing & Clipping](#slicing--clipping)
2829
- [Slicing a Polygon with a Polyline](#slicing-a-polygon-with-a-polyline)
@@ -49,6 +50,7 @@ iOverlay powers polygon boolean operations in [geo](https://github.com/georust/g
4950
## Features
5051

5152
- **Boolean Operations**: union, intersection, difference, and exclusion.
53+
- **Spatial Predicates**: `intersects`, `disjoint`, `interiors_intersect`, `touches`, `within`, `covers` with early-exit optimization.
5254
- **Polyline Operations**: clip and slice.
5355
- **Polygons**: with holes, self-intersections, and multiple contours.
5456
- **Simplification**: removes degenerate vertices and merges collinear edges.
@@ -183,6 +185,78 @@ The `overlay` function returns a `Vec<Shapes>`:
183185
|---------|---------------|----------------------|----------------|--------------------|----------------|
184186
| <img src="readme/ab.svg" alt="AB" style="width:100px;"> | <img src="readme/union.svg" alt="Union" style="width:100px;"> | <img src="readme/intersection.svg" alt="Intersection" style="width:100px;"> | <img src="readme/difference_ab.svg" alt="Difference" style="width:100px;"> | <img src="readme/difference_ba.svg" alt="Inverse Difference" style="width:100px;"> | <img src="readme/exclusion.svg" alt="Exclusion" style="width:100px;"> |
185187

188+
&nbsp;
189+
## Spatial Predicates
190+
191+
When you only need to know *whether* two shapes have a spatial relationship—not compute their intersection geometry—use spatial predicates for better performance:
192+
193+
```rust
194+
use i_overlay::float::relate::FloatRelate;
195+
196+
let outer = vec![[0.0, 0.0], [0.0, 20.0], [20.0, 20.0], [20.0, 0.0]];
197+
let inner = vec![[5.0, 5.0], [5.0, 15.0], [15.0, 15.0], [15.0, 5.0]];
198+
let adjacent = vec![[20.0, 0.0], [20.0, 10.0], [30.0, 10.0], [30.0, 0.0]];
199+
let distant = vec![[100.0, 100.0], [100.0, 110.0], [110.0, 110.0], [110.0, 100.0]];
200+
201+
// intersects: shapes share any point (interior or boundary)
202+
assert!(outer.intersects(&inner));
203+
assert!(outer.intersects(&adjacent)); // edge contact counts
204+
205+
// disjoint: shapes share no points (negation of intersects)
206+
assert!(outer.disjoint(&distant));
207+
208+
// interiors_intersect: interiors overlap (stricter than intersects)
209+
assert!(outer.interiors_intersect(&inner));
210+
assert!(!outer.interiors_intersect(&adjacent)); // edge-only contact
211+
212+
// touches: boundaries intersect but interiors don't
213+
assert!(outer.touches(&adjacent));
214+
assert!(!outer.touches(&inner)); // interiors overlap
215+
216+
// within: first shape completely inside second
217+
assert!(inner.within(&outer));
218+
assert!(!outer.within(&inner));
219+
220+
// covers: first shape completely contains second
221+
assert!(outer.covers(&inner));
222+
assert!(!inner.covers(&outer));
223+
```
224+
225+
These methods use early-exit optimization, returning as soon as the predicate can be determined without processing remaining segments.
226+
227+
### Fixed-Scale Predicates
228+
229+
For consistent precision across operations, use `FixedScaleFloatRelate`:
230+
231+
```rust
232+
use i_overlay::float::scale::FixedScaleFloatRelate;
233+
234+
let square = vec![[0.0, 0.0], [0.0, 10.0], [10.0, 10.0], [10.0, 0.0]];
235+
let other = vec![[5.0, 5.0], [5.0, 15.0], [15.0, 15.0], [15.0, 5.0]];
236+
237+
let scale = 1000.0; // or 1.0 / grid_size
238+
239+
let result = square.intersects_with_fixed_scale(&other, scale);
240+
assert!(result.unwrap());
241+
```
242+
243+
For more control, use `FloatPredicateOverlay` directly with a custom adapter:
244+
245+
```rust
246+
use i_overlay::float::relate::FloatPredicateOverlay;
247+
use i_float::adapter::FloatPointAdapter;
248+
249+
let square = vec![[0.0, 0.0], [0.0, 10.0], [10.0, 10.0], [10.0, 0.0]];
250+
let clip = vec![[5.0, 5.0], [5.0, 15.0], [15.0, 15.0], [15.0, 5.0]];
251+
252+
// Use fixed-scale constructor
253+
let mut overlay = FloatPredicateOverlay::with_subj_and_clip_fixed_scale(
254+
&square, &clip, 1000.0
255+
).unwrap();
256+
257+
assert!(overlay.intersects());
258+
```
259+
186260
&nbsp;
187261
## Custom Point Type Support
188262
`iOverlay` allows users to define custom point types, as long as they implement the `FloatPointCompatible` trait.

iOverlay/src/build/boolean.rs

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use crate::build::builder::{FillStrategy, GraphBuilder, InclusionFilterStrategy};
1+
use crate::build::builder::{
2+
FillHandler, FillStrategy, GraphBuilder, InclusionFilterStrategy, sweep_with_handler,
3+
};
24
use crate::core::extract::VisitState;
35
use crate::core::fill_rule::FillRule;
46
use crate::core::graph::OverlayGraph;
@@ -8,14 +10,18 @@ use crate::core::link::OverlayLinkFilter;
810
use crate::core::overlay::IntOverlayOptions;
911
use crate::core::overlay_rule::OverlayRule;
1012
use crate::core::solver::Solver;
13+
use crate::geom::v_segment::VSegment;
1114
use crate::segm::boolean::ShapeCountBoolean;
1215
use crate::segm::segment::{
1316
ALL, BOTH_BOTTOM, BOTH_TOP, CLIP_BOTH, CLIP_BOTTOM, CLIP_TOP, SUBJ_BOTH, SUBJ_BOTTOM, SUBJ_TOP, Segment,
1417
SegmentFill,
1518
};
1619
use crate::segm::winding::WindingCount;
20+
use crate::util::log::Int;
1721
use alloc::vec::Vec;
1822
use i_shape::util::reserve::Reserve;
23+
use i_tree::key::list::KeyExpList;
24+
use i_tree::key::tree::KeyExpTree;
1925

2026
impl GraphBuilder<ShapeCountBoolean, OverlayNode> {
2127
#[inline]
@@ -79,6 +85,54 @@ impl GraphBuilder<ShapeCountBoolean, OverlayNode> {
7985
}
8086
}
8187

88+
/// Sweeps segments with a fill handler, dispatching on fill rule.
89+
/// This is used by PredicateOverlay for early-exit predicate evaluation.
90+
#[inline]
91+
pub(crate) fn sweep_boolean<H: FillHandler>(
92+
fill_rule: FillRule,
93+
solver: &Solver,
94+
segments: &[Segment<ShapeCountBoolean>],
95+
handler: H,
96+
) -> H::Output {
97+
let count = segments.len();
98+
if solver.is_list_fill(segments) {
99+
let capacity = count.log2_sqrt().max(4) * 2;
100+
let mut list: KeyExpList<VSegment, i32, ShapeCountBoolean> = KeyExpList::new(capacity);
101+
sweep_boolean_with_scan(fill_rule, &mut list, segments, handler)
102+
} else {
103+
let capacity = count.log2_sqrt().max(8);
104+
let mut tree: KeyExpTree<VSegment, i32, ShapeCountBoolean> = KeyExpTree::new(capacity);
105+
sweep_boolean_with_scan(fill_rule, &mut tree, segments, handler)
106+
}
107+
}
108+
109+
#[inline]
110+
fn sweep_boolean_with_scan<S, H>(
111+
fill_rule: FillRule,
112+
scan: &mut S,
113+
segments: &[Segment<ShapeCountBoolean>],
114+
handler: H,
115+
) -> H::Output
116+
where
117+
S: i_tree::key::exp::KeyExpCollection<VSegment, i32, ShapeCountBoolean>,
118+
H: FillHandler,
119+
{
120+
match fill_rule {
121+
FillRule::EvenOdd => {
122+
sweep_with_handler::<ShapeCountBoolean, EvenOddStrategy, S, H>(scan, segments, handler)
123+
}
124+
FillRule::NonZero => {
125+
sweep_with_handler::<ShapeCountBoolean, NonZeroStrategy, S, H>(scan, segments, handler)
126+
}
127+
FillRule::Positive => {
128+
sweep_with_handler::<ShapeCountBoolean, PositiveStrategy, S, H>(scan, segments, handler)
129+
}
130+
FillRule::Negative => {
131+
sweep_with_handler::<ShapeCountBoolean, NegativeStrategy, S, H>(scan, segments, handler)
132+
}
133+
}
134+
}
135+
82136
struct EvenOddStrategy;
83137
struct NonZeroStrategy;
84138
struct PositiveStrategy;

iOverlay/src/build/builder.rs

Lines changed: 98 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,115 @@ use crate::segm::segment::{NONE, Segment, SegmentFill};
77
use crate::segm::winding::WindingCount;
88
use crate::util::log::Int;
99
use alloc::vec::Vec;
10+
use core::ops::ControlFlow;
1011
use i_float::triangle::Triangle;
1112
use i_shape::util::reserve::Reserve;
1213
use i_tree::key::exp::KeyExpCollection;
1314
use i_tree::key::list::KeyExpList;
1415
use i_tree::key::tree::KeyExpTree;
1516

16-
pub(super) trait FillStrategy<C> {
17+
pub(crate) trait FillStrategy<C> {
1718
fn add_and_fill(this: C, bot: C) -> (C, SegmentFill);
1819
}
1920

2021
pub(super) trait InclusionFilterStrategy {
2122
fn is_included(fill: SegmentFill) -> bool;
2223
}
2324

25+
/// Handler for processing segment fills during sweep.
26+
/// Enables early-exit for predicates like `intersects()`.
27+
pub(crate) trait FillHandler {
28+
type Output;
29+
fn handle(&mut self, index: usize, fill: SegmentFill) -> ControlFlow<Self::Output>;
30+
fn finalize(self) -> Self::Output;
31+
}
32+
33+
/// Handler that stores fills in a Vec (used by GraphBuilder).
34+
pub(crate) struct StoreFillsHandler<'a> {
35+
fills: &'a mut Vec<SegmentFill>,
36+
}
37+
38+
impl<'a> StoreFillsHandler<'a> {
39+
#[inline]
40+
pub(crate) fn new(fills: &'a mut Vec<SegmentFill>) -> Self {
41+
Self { fills }
42+
}
43+
}
44+
45+
impl FillHandler for StoreFillsHandler<'_> {
46+
type Output = ();
47+
48+
#[inline(always)]
49+
fn handle(&mut self, index: usize, fill: SegmentFill) -> ControlFlow<()> {
50+
self.fills[index] = fill;
51+
ControlFlow::Continue(())
52+
}
53+
54+
#[inline(always)]
55+
fn finalize(self) {}
56+
}
57+
58+
/// Core sweep-line algorithm that computes segment fills and passes them to a handler.
59+
/// This is the shared implementation used by both graph building and predicate evaluation.
60+
#[inline]
61+
pub(crate) fn sweep_with_handler<C, F, S, H>(
62+
scan: &mut S,
63+
segments: &[Segment<C>],
64+
mut handler: H,
65+
) -> H::Output
66+
where
67+
C: WindingCount,
68+
F: FillStrategy<C>,
69+
S: KeyExpCollection<VSegment, i32, C>,
70+
H: FillHandler,
71+
{
72+
let mut node = Vec::with_capacity(4);
73+
let n = segments.len();
74+
let mut i = 0;
75+
76+
while i < n {
77+
let p = segments[i].x_segment.a;
78+
79+
node.push(End {
80+
index: i,
81+
point: segments[i].x_segment.b,
82+
});
83+
i += 1;
84+
85+
while i < n && segments[i].x_segment.a == p {
86+
node.push(End {
87+
index: i,
88+
point: segments[i].x_segment.b,
89+
});
90+
i += 1;
91+
}
92+
93+
if node.len() > 1 {
94+
node.sort_by(|s0, s1| Triangle::clock_order_point(p, s1.point, s0.point));
95+
}
96+
97+
let mut sum_count = scan.first_less_or_equal_by(p.x, C::new(0, 0), |s| s.is_under_point_order(p));
98+
99+
for se in node.iter() {
100+
let sid = unsafe { segments.get_unchecked(se.index) };
101+
let (new_sum, fill) = F::add_and_fill(sid.count, sum_count);
102+
sum_count = new_sum;
103+
104+
if let ControlFlow::Break(result) = handler.handle(se.index, fill) {
105+
return result;
106+
}
107+
108+
if sid.x_segment.is_not_vertical() {
109+
scan.insert(sid.x_segment.into(), sum_count, p.x);
110+
}
111+
}
112+
113+
node.clear();
114+
}
115+
116+
handler.finalize()
117+
}
118+
24119
pub(crate) trait GraphNode {
25120
fn with_indices(indices: &[usize]) -> Self;
26121
}
@@ -73,56 +168,8 @@ impl<C: WindingCount, N: GraphNode> GraphBuilder<C, N> {
73168
scan_list: &mut S,
74169
segments: &[Segment<C>],
75170
) {
76-
let mut node = Vec::with_capacity(4);
77-
78-
let n = segments.len();
79-
80-
self.fills.resize(n, NONE);
81-
82-
let mut i = 0;
83-
84-
while i < n {
85-
let p = segments[i].x_segment.a;
86-
87-
node.push(End {
88-
index: i,
89-
point: segments[i].x_segment.b,
90-
});
91-
i += 1;
92-
93-
while i < n && segments[i].x_segment.a == p {
94-
node.push(End {
95-
index: i,
96-
point: segments[i].x_segment.b,
97-
});
98-
i += 1;
99-
}
100-
101-
if node.len() > 1 {
102-
node.sort_by(|s0, s1| Triangle::clock_order_point(p, s1.point, s0.point));
103-
}
104-
105-
let mut sum_count =
106-
scan_list.first_less_or_equal_by(p.x, C::new(0, 0), |s| s.is_under_point_order(p));
107-
let mut fill: SegmentFill;
108-
109-
for se in node.iter() {
110-
let sid = unsafe {
111-
// SAFETY: `se.index` was produced from `i` while iterating i ∈ [0, n) over `segments`
112-
segments.get_unchecked(se.index)
113-
};
114-
(sum_count, fill) = F::add_and_fill(sid.count, sum_count);
115-
unsafe {
116-
// SAFETY: `se.index` was produced from `i` while iterating i ∈ [0, n) over `segments` and segments.len == self.fills.len
117-
*self.fills.get_unchecked_mut(se.index) = fill
118-
}
119-
if sid.x_segment.is_not_vertical() {
120-
scan_list.insert(sid.x_segment.into(), sum_count, p.x);
121-
}
122-
}
123-
124-
node.clear();
125-
}
171+
self.fills.resize(segments.len(), NONE);
172+
sweep_with_handler::<C, F, S, _>(scan_list, segments, StoreFillsHandler::new(&mut self.fills));
126173
}
127174

128175
#[inline]

iOverlay/src/core/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,7 @@ pub(crate) mod link;
77
pub(crate) mod nearest_vector;
88
pub mod overlay;
99
pub mod overlay_rule;
10+
pub mod predicate;
11+
pub mod relate;
1012
pub mod simplify;
1113
pub mod solver;

0 commit comments

Comments
 (0)