Skip to content
74 changes: 74 additions & 0 deletions iOverlay/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ iOverlay powers polygon boolean operations in [geo](https://github.com/georust/g
- [Boolean Operations](#boolean-operations)
- [Simple Example](#simple-example)
- [Overlay Rules](#overlay-rules)
- [Spatial Predicates](#spatial-predicates)
- [Custom Point Type Support](#custom-point-type-support)
- [Slicing & Clipping](#slicing--clipping)
- [Slicing a Polygon with a Polyline](#slicing-a-polygon-with-a-polyline)
Expand All @@ -49,6 +50,7 @@ iOverlay powers polygon boolean operations in [geo](https://github.com/georust/g
## Features

- **Boolean Operations**: union, intersection, difference, and exclusion.
- **Spatial Predicates**: `intersects`, `disjoint`, `interiors_intersect`, `touches`, `within`, `covers` with early-exit optimization.
- **Polyline Operations**: clip and slice.
- **Polygons**: with holes, self-intersections, and multiple contours.
- **Simplification**: removes degenerate vertices and merges collinear edges.
Expand Down Expand Up @@ -183,6 +185,78 @@ The `overlay` function returns a `Vec<Shapes>`:
|---------|---------------|----------------------|----------------|--------------------|----------------|
| <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;"> |

&nbsp;
## Spatial Predicates

When you only need to know *whether* two shapes have a spatial relationship—not compute their intersection geometry—use spatial predicates for better performance:

```rust
use i_overlay::float::relate::FloatRelate;

let outer = vec![[0.0, 0.0], [0.0, 20.0], [20.0, 20.0], [20.0, 0.0]];
let inner = vec![[5.0, 5.0], [5.0, 15.0], [15.0, 15.0], [15.0, 5.0]];
let adjacent = vec![[20.0, 0.0], [20.0, 10.0], [30.0, 10.0], [30.0, 0.0]];
let distant = vec![[100.0, 100.0], [100.0, 110.0], [110.0, 110.0], [110.0, 100.0]];

// intersects: shapes share any point (interior or boundary)
assert!(outer.intersects(&inner));
assert!(outer.intersects(&adjacent)); // edge contact counts

// disjoint: shapes share no points (negation of intersects)
assert!(outer.disjoint(&distant));

// interiors_intersect: interiors overlap (stricter than intersects)
assert!(outer.interiors_intersect(&inner));
assert!(!outer.interiors_intersect(&adjacent)); // edge-only contact

// touches: boundaries intersect but interiors don't
assert!(outer.touches(&adjacent));
assert!(!outer.touches(&inner)); // interiors overlap

// within: first shape completely inside second
assert!(inner.within(&outer));
assert!(!outer.within(&inner));

// covers: first shape completely contains second
assert!(outer.covers(&inner));
assert!(!inner.covers(&outer));
```

These methods use early-exit optimization, returning as soon as the predicate can be determined without processing remaining segments.

### Fixed-Scale Predicates

For consistent precision across operations, use `FixedScaleFloatRelate`:

```rust
use i_overlay::float::scale::FixedScaleFloatRelate;

let square = vec![[0.0, 0.0], [0.0, 10.0], [10.0, 10.0], [10.0, 0.0]];
let other = vec![[5.0, 5.0], [5.0, 15.0], [15.0, 15.0], [15.0, 5.0]];

let scale = 1000.0; // or 1.0 / grid_size

let result = square.intersects_with_fixed_scale(&other, scale);
assert!(result.unwrap());
```

For more control, use `FloatPredicateOverlay` directly with a custom adapter:

```rust
use i_overlay::float::relate::FloatPredicateOverlay;
use i_float::adapter::FloatPointAdapter;

let square = vec![[0.0, 0.0], [0.0, 10.0], [10.0, 10.0], [10.0, 0.0]];
let clip = vec![[5.0, 5.0], [5.0, 15.0], [15.0, 15.0], [15.0, 5.0]];

// Use fixed-scale constructor
let mut overlay = FloatPredicateOverlay::with_subj_and_clip_fixed_scale(
&square, &clip, 1000.0
).unwrap();

assert!(overlay.intersects());
```

&nbsp;
## Custom Point Type Support
`iOverlay` allows users to define custom point types, as long as they implement the `FloatPointCompatible` trait.
Expand Down
10 changes: 4 additions & 6 deletions iOverlay/src/build/boolean.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use crate::build::builder::{FillStrategy, GraphBuilder, InclusionFilterStrategy};
use crate::build::builder::{GraphBuilder, InclusionFilterStrategy};
use crate::build::sweep::{
EvenOddStrategy, FillStrategy, NegativeStrategy, NonZeroStrategy, PositiveStrategy,
};
use crate::core::extract::VisitState;
use crate::core::fill_rule::FillRule;
use crate::core::graph::OverlayGraph;
Expand Down Expand Up @@ -79,11 +82,6 @@ impl GraphBuilder<ShapeCountBoolean, OverlayNode> {
}
}

struct EvenOddStrategy;
struct NonZeroStrategy;
struct PositiveStrategy;
struct NegativeStrategy;

impl FillStrategy<ShapeCountBoolean> for EvenOddStrategy {
#[inline(always)]
fn add_and_fill(this: ShapeCountBoolean, bot: ShapeCountBoolean) -> (ShapeCountBoolean, SegmentFill) {
Expand Down
139 changes: 33 additions & 106 deletions iOverlay/src/build/builder.rs
Original file line number Diff line number Diff line change
@@ -1,33 +1,50 @@
use crate::build::sweep::{FillHandler, FillStrategy, SweepRunner};
use crate::core::link::OverlayLink;
use crate::core::solver::Solver;
use crate::geom::end::End;
use crate::geom::id_point::IdPoint;
use crate::geom::v_segment::VSegment;
use crate::segm::segment::{NONE, Segment, SegmentFill};
use crate::segm::winding::WindingCount;
use crate::util::log::Int;
use alloc::vec::Vec;
use i_float::triangle::Triangle;
use core::ops::ControlFlow;
use i_shape::util::reserve::Reserve;
use i_tree::key::exp::KeyExpCollection;
use i_tree::key::list::KeyExpList;
use i_tree::key::tree::KeyExpTree;

pub(super) trait FillStrategy<C> {
fn add_and_fill(this: C, bot: C) -> (C, SegmentFill);
}

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

pub(crate) struct StoreFillsHandler<'a> {
fills: &'a mut Vec<SegmentFill>,
}

impl<'a> StoreFillsHandler<'a> {
#[inline]
pub(crate) fn new(fills: &'a mut Vec<SegmentFill>) -> Self {
Self { fills }
}
}

impl<C> FillHandler<C> for StoreFillsHandler<'_> {
type Output = ();

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

#[inline(always)]
fn finalize(self) {}
}

pub(crate) trait GraphNode {
fn with_indices(indices: &[usize]) -> Self;
}

pub(crate) struct GraphBuilder<C, N> {
list: Option<KeyExpList<VSegment, i32, C>>,
tree: Option<KeyExpTree<VSegment, i32, C>>,
sweep_runner: SweepRunner<C>,
pub(super) links: Vec<OverlayLink>,
pub(super) nodes: Vec<N>,
pub(super) fills: Vec<SegmentFill>,
Expand All @@ -38,8 +55,7 @@ impl<C: WindingCount, N: GraphNode> GraphBuilder<C, N> {
#[inline]
pub(crate) fn new() -> Self {
Self {
list: None,
tree: None,
sweep_runner: SweepRunner::new(),
links: Vec::new(),
nodes: Vec::new(),
fills: Vec::new(),
Expand All @@ -53,76 +69,9 @@ impl<C: WindingCount, N: GraphNode> GraphBuilder<C, N> {
solver: &Solver,
segments: &[Segment<C>],
) {
let count = segments.len();
if solver.is_list_fill(segments) {
let capacity = count.log2_sqrt().max(4) * 2;
let mut list = self.take_scan_list(capacity);
self.build_fills::<F, KeyExpList<VSegment, i32, C>>(&mut list, segments);
self.list = Some(list);
} else {
let capacity = count.log2_sqrt().max(8);
let mut tree = self.take_scan_tree(capacity);
self.build_fills::<F, KeyExpTree<VSegment, i32, C>>(&mut tree, segments);
self.tree = Some(tree);
}
}

#[inline]
fn build_fills<F: FillStrategy<C>, S: KeyExpCollection<VSegment, i32, C>>(
&mut self,
scan_list: &mut S,
segments: &[Segment<C>],
) {
let mut node = Vec::with_capacity(4);

let n = segments.len();

self.fills.resize(n, NONE);

let mut i = 0;

while i < n {
let p = segments[i].x_segment.a;

node.push(End {
index: i,
point: segments[i].x_segment.b,
});
i += 1;

while i < n && segments[i].x_segment.a == p {
node.push(End {
index: i,
point: segments[i].x_segment.b,
});
i += 1;
}

if node.len() > 1 {
node.sort_by(|s0, s1| Triangle::clock_order_point(p, s1.point, s0.point));
}

let mut sum_count =
scan_list.first_less_or_equal_by(p.x, C::new(0, 0), |s| s.is_under_point_order(p));
let mut fill: SegmentFill;

for se in node.iter() {
let sid = unsafe {
// SAFETY: `se.index` was produced from `i` while iterating i ∈ [0, n) over `segments`
segments.get_unchecked(se.index)
};
(sum_count, fill) = F::add_and_fill(sid.count, sum_count);
unsafe {
// SAFETY: `se.index` was produced from `i` while iterating i ∈ [0, n) over `segments` and segments.len == self.fills.len
*self.fills.get_unchecked_mut(se.index) = fill
}
if sid.x_segment.is_not_vertical() {
scan_list.insert(sid.x_segment.into(), sum_count, p.x);
}
}

node.clear();
}
self.fills.resize(segments.len(), NONE);
self.sweep_runner
.run::<F, _>(solver, segments, StoreFillsHandler::new(&mut self.fills));
}

#[inline]
Expand Down Expand Up @@ -155,26 +104,4 @@ impl<C: WindingCount, N: GraphNode> GraphBuilder<C, N> {
));
}
}

#[inline]
fn take_scan_list(&mut self, capacity: usize) -> KeyExpList<VSegment, i32, C> {
if let Some(mut list) = self.list.take() {
list.clear();
list.reserve_capacity(capacity);
list
} else {
KeyExpList::new(capacity)
}
}

#[inline]
fn take_scan_tree(&mut self, capacity: usize) -> KeyExpTree<VSegment, i32, C> {
if let Some(mut tree) = self.tree.take() {
tree.clear();
tree.reserve_capacity(capacity);
tree
} else {
KeyExpTree::new(capacity)
}
}
}
1 change: 1 addition & 0 deletions iOverlay/src/build/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ pub(crate) mod builder;
mod graph;
mod offset;
pub(crate) mod string;
pub(crate) mod sweep;
mod util;
3 changes: 2 additions & 1 deletion iOverlay/src/build/offset.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::build::builder::{FillStrategy, GraphBuilder};
use crate::build::builder::GraphBuilder;
use crate::build::sweep::FillStrategy;
use crate::core::graph::OverlayNode;
use crate::core::link::OverlayLink;
use crate::core::solver::Solver;
Expand Down
3 changes: 2 additions & 1 deletion iOverlay/src/build/string.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::build::builder::{FillStrategy, GraphBuilder, InclusionFilterStrategy};
use crate::build::builder::{GraphBuilder, InclusionFilterStrategy};
use crate::build::sweep::FillStrategy;
use crate::core::fill_rule::FillRule;
use crate::core::solver::Solver;
use crate::segm::segment::{CLIP_BOTH, SUBJ_BOTH, Segment, SegmentFill};
Expand Down
Loading