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
137 changes: 31 additions & 106 deletions iOverlay/src/build/builder.rs
Original file line number Diff line number Diff line change
@@ -1,33 +1,48 @@
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 FillHandler for StoreFillsHandler<'_> {
type Output = ();

#[inline(always)]
fn handle(&mut self, index: usize, fill: SegmentFill) -> ControlFlow<()> {
self.fills[index] = fill;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better to use unsafe here.
The compiler doesn't see this as safe and won't optimize it. But the algorithm guarantees that it return an index less than the vector's length.

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 +53,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 +67,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 +102,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