Skip to content
13 changes: 7 additions & 6 deletions iOverlay/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "i_overlay"
version = "7.0.3"
version = "8.0.0"
authors = ["Nail Sharipov <nailxsharipov@gmail.com>"]
edition = "2024"
rust-version = "1.88"
Expand All @@ -12,11 +12,12 @@ readme = "README.md"
categories = ["algorithms", "graphics", "science::geo", "mathematics", "no-std"]

[dependencies]
i_float = { version = "^3.0.0" }
i_shape = { version = "^3.0.0" }
i_tree = { version = "^0.19.0" }
i_key_sort = { version = "^0.11.0" }

i_float = { version = "^4.0.0"}
i_shape = { version = "^4.0.0"}

#i_float = { path = "../../iFloat"}
#i_shape = { path = "../../iShape"}
#i_tree = { path = "../../iTree" }
Expand All @@ -36,7 +37,7 @@ allow_multithreading = ["dep:rayon", "i_key_sort/allow_multithreading"]
serde = { version = "^1.0", features = ["derive"] }
serde_json = "^1.0"
rand = { version = "~0.10", features = ["alloc"] }
i_float = { version = "^4.0.0", features = ["serde"] }
i_shape = { version = "^4.0.0", features = ["serde"] }
#i_float = { path = "../../iFloat", features = ["serde"] }
#i_shape = { path = "../../iShape", features = ["serde"] }
i_float = { version = "^3.0.0", features = ["serde"] }
i_shape = { version = "^3.0.0", features = ["serde"] }
#i_shape = { path = "../../iShape", features = ["serde"] }
31 changes: 19 additions & 12 deletions iOverlay/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ iOverlay powers polygon boolean operations in [geo](https://github.com/georust/g
- [Slicing a Polygon with a Polyline](#slicing-a-polygon-with-a-polyline)
- [Clipping a Polyline by a Polygon](#clipping-a-polyline-by-a-polygon)
- [Buffering](#buffering)
- [Offseting a Path](#offseting-a-path)
- [Offseting a Polygon](#offseting-a-polygon)
- [Offsetting a Path](#offsetting-a-path)
- [Offsetting a Polygon](#offsetting-a-polygon)
- [LineCap](#linecap)
- [LineJoin](#linejoin)
- [FAQ](#faq)
Expand All @@ -56,7 +56,7 @@ iOverlay powers polygon boolean operations in [geo](https://github.com/georust/g
- **Simplification**: removes degenerate vertices and merges collinear edges.
- **Buffering**: offsets paths and polygons.
- **Fill Rules**: even-odd, non-zero, positive and negative.
- **Data Types**: Supports `i16`/`i32`/`i64` integer APIs and `f32`/`f64` floating-point APIs.
- **Data Types**: supports `i16`/`i32`/`i64` integer APIs and `f32`/`f64` floating-point APIs.

&nbsp;
## Demo
Expand All @@ -72,8 +72,13 @@ iOverlay powers polygon boolean operations in [geo](https://github.com/georust/g

iOverlay supports:

- `i16`/`i32`/`i64` math solvers
- `on`/`off` multithreading feature
- `i16`/`i32`/`i64` integer engines
- Optional multithreading for large inputs through the `allow_multithreading` Cargo feature

```toml
[dependencies]
i_overlay = { version = "^8.0", features = ["allow_multithreading"] }
```

<img src="readme/average_relative_time.svg" alt="Average relative time for iOverlay Rust solvers" style="max-width:860px;width:100%;">

Expand All @@ -87,7 +92,7 @@ See the detailed reports: [Performance Comparison](https://ishape-rust.github.io
Add the following to your Cargo.toml:
```toml
[dependencies]
i_overlay = "^7.0"
i_overlay = "^8.0"
```

Read full [documentation](https://ishape-rust.github.io/iShape-js/overlay/doc.html)
Expand Down Expand Up @@ -175,13 +180,13 @@ The result is a vec of shapes:
```
&nbsp;

The `overlay` function returns a `Vec<Shapes>`:
The `overlay` function returns `Shapes<P>`, which is an alias for `Vec<Shape<P>>`:

- `Vec<Shape>`: A collection of shapes.
- `Shape`: Represents a shape made up of:
- `Vec<Contour>`: A list of contours.
- `Shapes<P>`: a collection of shapes.
- `Shape<P>`: a shape made up of:
- `Vec<Contour<P>>`: a list of contours.
- The first contour is the outer boundary (counterclockwise), and subsequent contours represent holes (clockwise).
- `Contour`: A sequence of points (`Vec<P: FloatPointCompatible>`) forming a closed contour.
- `Contour<P>`: a sequence of points (`Vec<P>`) forming a closed contour, where `P` implements `FloatPointCompatible`.

**Note**: By default, outer boundaries are counterclockwise and holes are clockwise—unless `main_direction` is set. [More information](https://ishape-rust.github.io/iShape-js/overlay/contours/contours.html) about contours.

Expand Down Expand Up @@ -216,7 +221,9 @@ enum EdgeKind {
}

impl OverlayEdgeData for EdgeKind {
fn merge(ctx: EdgeDataMerge<ShapeCountBoolean, Self>) -> Self {
type Store = ();

fn merge(ctx: EdgeDataMerge<ShapeCountBoolean, Self>, _store: &mut Self::Store) -> Self {
match (ctx.lhs_data, ctx.rhs_data) {
(EdgeKind::Red, EdgeKind::Red) => EdgeKind::Red,
(EdgeKind::Green, EdgeKind::Green) => EdgeKind::Green,
Expand Down
15 changes: 7 additions & 8 deletions iOverlay/src/build/boolean.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ use crate::segm::winding::WindingCount;
use alloc::vec::Vec;
use i_float::int::number::int::IntNumber;
use i_key_sort::sort::key::SortKey;
use i_shape::util::reserve::Reserve;
use i_tree::Expiration;

impl<I, D> GraphBuilder<ShapeCountBoolean, OverlayNode, I, D>
Expand Down Expand Up @@ -303,7 +302,7 @@ impl<I: IntNumber, D> OverlayLinkFilter for [OverlayLink<I, D>] {
#[inline]
fn filter_subject_into<I: IntNumber, D>(links: &[OverlayLink<I, D>], buffer: &mut Vec<VisitState>) {
buffer.clear();
buffer.reserve_capacity(links.len());
buffer.reserve(links.len());
for link in links.iter() {
buffer.push(VisitState::new(!link.fill.is_subject()));
}
Expand All @@ -312,7 +311,7 @@ fn filter_subject_into<I: IntNumber, D>(links: &[OverlayLink<I, D>], buffer: &mu
#[inline]
fn filter_clip_into<I: IntNumber, D>(links: &[OverlayLink<I, D>], buffer: &mut Vec<VisitState>) {
buffer.clear();
buffer.reserve_capacity(links.len());
buffer.reserve(links.len());
for link in links.iter() {
buffer.push(VisitState::new(!link.fill.is_clip()));
}
Expand All @@ -321,7 +320,7 @@ fn filter_clip_into<I: IntNumber, D>(links: &[OverlayLink<I, D>], buffer: &mut V
#[inline]
fn filter_intersect_into<I: IntNumber, D>(links: &[OverlayLink<I, D>], buffer: &mut Vec<VisitState>) {
buffer.clear();
buffer.reserve_capacity(links.len());
buffer.reserve(links.len());
for link in links.iter() {
buffer.push(VisitState::new(!link.fill.is_intersect()));
}
Expand All @@ -330,7 +329,7 @@ fn filter_intersect_into<I: IntNumber, D>(links: &[OverlayLink<I, D>], buffer: &
#[inline]
fn filter_union_into<I: IntNumber, D>(links: &[OverlayLink<I, D>], buffer: &mut Vec<VisitState>) {
buffer.clear();
buffer.reserve_capacity(links.len());
buffer.reserve(links.len());
for link in links.iter() {
buffer.push(VisitState::new(!link.fill.is_union()));
}
Expand All @@ -339,7 +338,7 @@ fn filter_union_into<I: IntNumber, D>(links: &[OverlayLink<I, D>], buffer: &mut
#[inline]
fn filter_difference_into<I: IntNumber, D>(links: &[OverlayLink<I, D>], buffer: &mut Vec<VisitState>) {
buffer.clear();
buffer.reserve_capacity(links.len());
buffer.reserve(links.len());
for link in links.iter() {
buffer.push(VisitState::new(!link.fill.is_difference()));
}
Expand All @@ -351,7 +350,7 @@ fn filter_inverse_difference_into<I: IntNumber, D>(
buffer: &mut Vec<VisitState>,
) {
buffer.clear();
buffer.reserve_capacity(links.len());
buffer.reserve(links.len());
for link in links.iter() {
buffer.push(VisitState::new(!link.fill.is_inverse_difference()));
}
Expand All @@ -360,7 +359,7 @@ fn filter_inverse_difference_into<I: IntNumber, D>(
#[inline]
fn filter_xor_into<I: IntNumber, D>(links: &[OverlayLink<I, D>], buffer: &mut Vec<VisitState>) {
buffer.clear();
buffer.reserve_capacity(links.len());
buffer.reserve(links.len());
for link in links.iter() {
buffer.push(VisitState::new(!link.fill.is_xor()));
}
Expand Down
5 changes: 2 additions & 3 deletions iOverlay/src/build/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ use crate::segm::winding::WindingCount;
use alloc::vec::Vec;
use core::ops::ControlFlow;
use i_float::int::number::int::IntNumber;
use i_shape::util::reserve::Reserve;
use i_tree::Expiration;

pub(super) trait InclusionFilterStrategy {
Expand Down Expand Up @@ -89,7 +88,7 @@ where
segments: &[Segment<C, I, D>],
) {
self.links.clear();
self.links.reserve_capacity(segments.len());
self.links.reserve(segments.len());

for (segment, &fill) in segments.iter().zip(&self.fills) {
if !F::is_included(fill) {
Expand All @@ -107,7 +106,7 @@ where
#[inline]
pub(super) fn build_links_all(&mut self, segments: &[Segment<C, I, D>]) {
self.links.clear();
self.links.reserve_capacity(segments.len());
self.links.reserve(segments.len());

for (segment, &fill) in segments.iter().zip(&self.fills) {
self.links.push(OverlayLink::new_with_data(
Expand Down
105 changes: 105 additions & 0 deletions iOverlay/src/build/string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,3 +228,108 @@ impl StringFillFilter for SegmentFill {
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::build::builder::GraphBuilder;
use crate::core::solver::Solver;
use crate::string::overlay::StringOverlay;
use alloc::vec;
use i_float::int::point::IntPoint;

fn clip_rule(invert: bool) -> ClipRule {
ClipRule {
invert,
boundary_included: false,
}
}

fn clip_path(shape: &[IntPoint<i32>], path: &[IntPoint<i32>], invert: bool) -> Vec<Vec<IntPoint<i32>>> {
let mut overlay = StringOverlay::with_shape_contour(shape);
overlay.add_string_path(path);
overlay.clip_string_lines(FillRule::NonZero, clip_rule(invert))
}

#[test]
fn empty_segment_set_builds_empty_graph_for_every_fill_rule() {
let solver = Solver::default();
let segments: Vec<Segment<ShapeCountString, i32>> = Vec::new();

for fill_rule in [
FillRule::EvenOdd,
FillRule::NonZero,
FillRule::Positive,
FillRule::Negative,
] {
let mut builder = GraphBuilder::new();
let graph = builder.build_string_all(fill_rule, &solver, &segments);

assert!(graph.nodes.is_empty());
assert!(graph.links.is_empty());
}
}

#[test]
fn minimal_string_inputs_are_rejected() {
let point = IntPoint::new(1, 1);
let mut overlay = StringOverlay::<i32>::new(0);

overlay.add_string_path(&[]);
overlay.add_string_path(&[point]);
overlay.add_string_line([point, point]);
overlay.add_string_path(&[point, point, point]);
overlay.add_string_contour(&[point, point]);

assert!(overlay.build_graph_view(FillRule::NonZero).is_none());
}

#[test]
fn repeated_string_points_are_normalized_consistently() {
let shape = [
IntPoint::new(0, 0),
IntPoint::new(10, 0),
IntPoint::new(10, 10),
IntPoint::new(0, 10),
];
let clean = [IntPoint::new(-5, 5), IntPoint::new(5, 5), IntPoint::new(15, 5)];
let repeated = [
IntPoint::new(-5, 5),
IntPoint::new(-5, 5),
IntPoint::new(5, 5),
IntPoint::new(5, 5),
IntPoint::new(15, 5),
IntPoint::new(15, 5),
];

let expected = clip_path(&shape, &clean, false);
let result = clip_path(&shape, &repeated, false);

assert_eq!(result, expected);
assert_eq!(
result,
vec![vec![
IntPoint::new(0, 5),
IntPoint::new(5, 5),
IntPoint::new(10, 5),
]]
);
}

#[test]
fn zero_area_subject_contours_do_not_affect_clipping() {
let line = [IntPoint::new(-5, 0), IntPoint::new(15, 0)];
let expected_outside = vec![line.to_vec()];
let contours = [
vec![IntPoint::new(0, 0)],
vec![IntPoint::new(0, 0), IntPoint::new(0, 0)],
vec![IntPoint::new(0, 0), IntPoint::new(10, 0)],
vec![IntPoint::new(0, 0), IntPoint::new(5, 0), IntPoint::new(10, 0)],
];

for contour in contours {
assert!(clip_path(&contour, &line, false).is_empty());
assert_eq!(clip_path(&contour, &line, true), expected_outside);
}
}
}
9 changes: 4 additions & 5 deletions iOverlay/src/core/edge_overlay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::core::edge_data::OverlayEdgeData;
use crate::core::extract::BooleanExtractionBuffer;
use crate::core::fill_rule::FillRule;
use crate::core::graph::OverlayNode;
use crate::core::integer::OverlayInt;
use crate::core::overlay::{IntOverlayOptions, ShapeType};
use crate::core::overlay_rule::OverlayRule;
use crate::core::solver::Solver;
Expand All @@ -13,8 +14,6 @@ use crate::vector::edge::{DataVectorEdge, DataVectorShape};
use alloc::vec::Vec;
use i_float::int::number::int::IntNumber;
use i_float::int::point::IntPoint;
use i_key_sort::sort::key::SortKey;
use i_tree::{Expiration, LayoutNumber};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InputEdge<I: IntNumber, D> {
Expand All @@ -23,7 +22,7 @@ pub struct InputEdge<I: IntNumber, D> {
pub data: D,
}

pub struct EdgeOverlay<I: IntNumber + Expiration, D: OverlayEdgeData> {
pub struct EdgeOverlay<I: OverlayInt, D: OverlayEdgeData> {
pub solver: Solver,
pub options: IntOverlayOptions<I::WideUInt>,
pub boolean_buffer: Option<BooleanExtractionBuffer<I>>,
Expand All @@ -35,7 +34,7 @@ pub struct EdgeOverlay<I: IntNumber + Expiration, D: OverlayEdgeData> {

impl<I, D> EdgeOverlay<I, D>
where
I: IntNumber + Expiration + LayoutNumber + SortKey,
I: OverlayInt,
D: OverlayEdgeData,
{
pub fn new(capacity: usize) -> Self {
Expand Down Expand Up @@ -133,7 +132,7 @@ where

impl<I, D> EdgeOverlay<I, D>
where
I: IntNumber + Expiration + LayoutNumber + SortKey,
I: OverlayInt,
D: OverlayEdgeData,
{
pub fn edges(&self) -> impl Iterator<Item = [IntPoint<I>; 2]> + '_ {
Expand Down
Loading
Loading