Skip to content

Commit e35f194

Browse files
Merge pull request #56 from bretttully/gh-50/spatial-predicates
Add spatial predicates API with fixed-scale support
2 parents 8787822 + f170afa commit e35f194

File tree

13 files changed

+2500
-115
lines changed

13 files changed

+2500
-115
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: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use crate::build::builder::{FillStrategy, GraphBuilder, InclusionFilterStrategy};
1+
use crate::build::builder::{GraphBuilder, InclusionFilterStrategy};
2+
use crate::build::sweep::{
3+
EvenOddStrategy, FillStrategy, NegativeStrategy, NonZeroStrategy, PositiveStrategy,
4+
};
25
use crate::core::extract::VisitState;
36
use crate::core::fill_rule::FillRule;
47
use crate::core::graph::OverlayGraph;
@@ -79,11 +82,6 @@ impl GraphBuilder<ShapeCountBoolean, OverlayNode> {
7982
}
8083
}
8184

82-
struct EvenOddStrategy;
83-
struct NonZeroStrategy;
84-
struct PositiveStrategy;
85-
struct NegativeStrategy;
86-
8785
impl FillStrategy<ShapeCountBoolean> for EvenOddStrategy {
8886
#[inline(always)]
8987
fn add_and_fill(this: ShapeCountBoolean, bot: ShapeCountBoolean) -> (ShapeCountBoolean, SegmentFill) {

iOverlay/src/build/builder.rs

Lines changed: 33 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,50 @@
1+
use crate::build::sweep::{FillHandler, FillStrategy, SweepRunner};
12
use crate::core::link::OverlayLink;
23
use crate::core::solver::Solver;
34
use crate::geom::end::End;
45
use crate::geom::id_point::IdPoint;
5-
use crate::geom::v_segment::VSegment;
66
use crate::segm::segment::{NONE, Segment, SegmentFill};
77
use crate::segm::winding::WindingCount;
8-
use crate::util::log::Int;
98
use alloc::vec::Vec;
10-
use i_float::triangle::Triangle;
9+
use core::ops::ControlFlow;
1110
use i_shape::util::reserve::Reserve;
12-
use i_tree::key::exp::KeyExpCollection;
13-
use i_tree::key::list::KeyExpList;
14-
use i_tree::key::tree::KeyExpTree;
15-
16-
pub(super) trait FillStrategy<C> {
17-
fn add_and_fill(this: C, bot: C) -> (C, SegmentFill);
18-
}
1911

2012
pub(super) trait InclusionFilterStrategy {
2113
fn is_included(fill: SegmentFill) -> bool;
2214
}
2315

16+
pub(crate) struct StoreFillsHandler<'a> {
17+
fills: &'a mut Vec<SegmentFill>,
18+
}
19+
20+
impl<'a> StoreFillsHandler<'a> {
21+
#[inline]
22+
pub(crate) fn new(fills: &'a mut Vec<SegmentFill>) -> Self {
23+
Self { fills }
24+
}
25+
}
26+
27+
impl<C> FillHandler<C> for StoreFillsHandler<'_> {
28+
type Output = ();
29+
30+
#[inline(always)]
31+
fn handle(&mut self, index: usize, _segment: &Segment<C>, fill: SegmentFill) -> ControlFlow<()> {
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 };
35+
ControlFlow::Continue(())
36+
}
37+
38+
#[inline(always)]
39+
fn finalize(self) {}
40+
}
41+
2442
pub(crate) trait GraphNode {
2543
fn with_indices(indices: &[usize]) -> Self;
2644
}
2745

2846
pub(crate) struct GraphBuilder<C, N> {
29-
list: Option<KeyExpList<VSegment, i32, C>>,
30-
tree: Option<KeyExpTree<VSegment, i32, C>>,
47+
sweep_runner: SweepRunner<C>,
3148
pub(super) links: Vec<OverlayLink>,
3249
pub(super) nodes: Vec<N>,
3350
pub(super) fills: Vec<SegmentFill>,
@@ -38,8 +55,7 @@ impl<C: WindingCount, N: GraphNode> GraphBuilder<C, N> {
3855
#[inline]
3956
pub(crate) fn new() -> Self {
4057
Self {
41-
list: None,
42-
tree: None,
58+
sweep_runner: SweepRunner::new(),
4359
links: Vec::new(),
4460
nodes: Vec::new(),
4561
fills: Vec::new(),
@@ -53,76 +69,9 @@ impl<C: WindingCount, N: GraphNode> GraphBuilder<C, N> {
5369
solver: &Solver,
5470
segments: &[Segment<C>],
5571
) {
56-
let count = segments.len();
57-
if solver.is_list_fill(segments) {
58-
let capacity = count.log2_sqrt().max(4) * 2;
59-
let mut list = self.take_scan_list(capacity);
60-
self.build_fills::<F, KeyExpList<VSegment, i32, C>>(&mut list, segments);
61-
self.list = Some(list);
62-
} else {
63-
let capacity = count.log2_sqrt().max(8);
64-
let mut tree = self.take_scan_tree(capacity);
65-
self.build_fills::<F, KeyExpTree<VSegment, i32, C>>(&mut tree, segments);
66-
self.tree = Some(tree);
67-
}
68-
}
69-
70-
#[inline]
71-
fn build_fills<F: FillStrategy<C>, S: KeyExpCollection<VSegment, i32, C>>(
72-
&mut self,
73-
scan_list: &mut S,
74-
segments: &[Segment<C>],
75-
) {
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-
}
72+
self.fills.resize(segments.len(), NONE);
73+
self.sweep_runner
74+
.run::<F, _>(solver, segments, StoreFillsHandler::new(&mut self.fills));
12675
}
12776

12877
#[inline]
@@ -155,26 +104,4 @@ impl<C: WindingCount, N: GraphNode> GraphBuilder<C, N> {
155104
));
156105
}
157106
}
158-
159-
#[inline]
160-
fn take_scan_list(&mut self, capacity: usize) -> KeyExpList<VSegment, i32, C> {
161-
if let Some(mut list) = self.list.take() {
162-
list.clear();
163-
list.reserve_capacity(capacity);
164-
list
165-
} else {
166-
KeyExpList::new(capacity)
167-
}
168-
}
169-
170-
#[inline]
171-
fn take_scan_tree(&mut self, capacity: usize) -> KeyExpTree<VSegment, i32, C> {
172-
if let Some(mut tree) = self.tree.take() {
173-
tree.clear();
174-
tree.reserve_capacity(capacity);
175-
tree
176-
} else {
177-
KeyExpTree::new(capacity)
178-
}
179-
}
180107
}

iOverlay/src/build/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ pub(crate) mod builder;
33
mod graph;
44
mod offset;
55
pub(crate) mod string;
6+
pub(crate) mod sweep;
67
mod util;

iOverlay/src/build/offset.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
use crate::build::builder::{FillStrategy, GraphBuilder};
1+
use crate::build::builder::GraphBuilder;
2+
use crate::build::sweep::FillStrategy;
23
use crate::core::graph::OverlayNode;
34
use crate::core::link::OverlayLink;
45
use crate::core::solver::Solver;

iOverlay/src/build/string.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
use crate::build::builder::{FillStrategy, GraphBuilder, InclusionFilterStrategy};
1+
use crate::build::builder::{GraphBuilder, InclusionFilterStrategy};
2+
use crate::build::sweep::FillStrategy;
23
use crate::core::fill_rule::FillRule;
34
use crate::core::solver::Solver;
45
use crate::segm::segment::{CLIP_BOTH, SUBJ_BOTH, Segment, SegmentFill};

0 commit comments

Comments
 (0)