diff --git a/iOverlay/README.md b/iOverlay/README.md index 7e9abe5..4dfcdb6 100644 --- a/iOverlay/README.md +++ b/iOverlay/README.md @@ -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) @@ -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. @@ -183,6 +185,78 @@ The `overlay` function returns a `Vec`: |---------|---------------|----------------------|----------------|--------------------|----------------| | AB | Union | Intersection | Difference | Inverse Difference | Exclusion | +  +## 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()); +``` +   ## Custom Point Type Support `iOverlay` allows users to define custom point types, as long as they implement the `FloatPointCompatible` trait. diff --git a/iOverlay/src/build/boolean.rs b/iOverlay/src/build/boolean.rs index 2b8cb05..8cc136e 100644 --- a/iOverlay/src/build/boolean.rs +++ b/iOverlay/src/build/boolean.rs @@ -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; @@ -79,11 +82,6 @@ impl GraphBuilder { } } -struct EvenOddStrategy; -struct NonZeroStrategy; -struct PositiveStrategy; -struct NegativeStrategy; - impl FillStrategy for EvenOddStrategy { #[inline(always)] fn add_and_fill(this: ShapeCountBoolean, bot: ShapeCountBoolean) -> (ShapeCountBoolean, SegmentFill) { diff --git a/iOverlay/src/build/builder.rs b/iOverlay/src/build/builder.rs index aecbd89..db12a52 100644 --- a/iOverlay/src/build/builder.rs +++ b/iOverlay/src/build/builder.rs @@ -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 { - 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, +} + +impl<'a> StoreFillsHandler<'a> { + #[inline] + pub(crate) fn new(fills: &'a mut Vec) -> Self { + Self { fills } + } +} + +impl FillHandler for StoreFillsHandler<'_> { + type Output = (); + + #[inline(always)] + fn handle(&mut self, index: usize, _segment: &Segment, 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 { - list: Option>, - tree: Option>, + sweep_runner: SweepRunner, pub(super) links: Vec, pub(super) nodes: Vec, pub(super) fills: Vec, @@ -38,8 +55,7 @@ impl GraphBuilder { #[inline] pub(crate) fn new() -> Self { Self { - list: None, - tree: None, + sweep_runner: SweepRunner::new(), links: Vec::new(), nodes: Vec::new(), fills: Vec::new(), @@ -53,76 +69,9 @@ impl GraphBuilder { solver: &Solver, segments: &[Segment], ) { - 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::>(&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::>(&mut tree, segments); - self.tree = Some(tree); - } - } - - #[inline] - fn build_fills, S: KeyExpCollection>( - &mut self, - scan_list: &mut S, - segments: &[Segment], - ) { - 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::(solver, segments, StoreFillsHandler::new(&mut self.fills)); } #[inline] @@ -155,26 +104,4 @@ impl GraphBuilder { )); } } - - #[inline] - fn take_scan_list(&mut self, capacity: usize) -> KeyExpList { - 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 { - if let Some(mut tree) = self.tree.take() { - tree.clear(); - tree.reserve_capacity(capacity); - tree - } else { - KeyExpTree::new(capacity) - } - } } diff --git a/iOverlay/src/build/mod.rs b/iOverlay/src/build/mod.rs index ade5934..c757ecb 100644 --- a/iOverlay/src/build/mod.rs +++ b/iOverlay/src/build/mod.rs @@ -3,4 +3,5 @@ pub(crate) mod builder; mod graph; mod offset; pub(crate) mod string; +pub(crate) mod sweep; mod util; diff --git a/iOverlay/src/build/offset.rs b/iOverlay/src/build/offset.rs index 8d3d535..6b1b2e9 100644 --- a/iOverlay/src/build/offset.rs +++ b/iOverlay/src/build/offset.rs @@ -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; diff --git a/iOverlay/src/build/string.rs b/iOverlay/src/build/string.rs index 3258654..157637e 100644 --- a/iOverlay/src/build/string.rs +++ b/iOverlay/src/build/string.rs @@ -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}; diff --git a/iOverlay/src/build/sweep.rs b/iOverlay/src/build/sweep.rs new file mode 100644 index 0000000..0bf0286 --- /dev/null +++ b/iOverlay/src/build/sweep.rs @@ -0,0 +1,165 @@ +use crate::core::fill_rule::FillRule; +use crate::core::solver::Solver; +use crate::geom::end::End; +use crate::geom::v_segment::VSegment; +use crate::segm::segment::{Segment, SegmentFill}; +use crate::segm::winding::WindingCount; +use crate::util::log::Int; +use alloc::vec::Vec; +use core::ops::ControlFlow; +use i_float::triangle::Triangle; +use i_tree::key::exp::KeyExpCollection; +use i_tree::key::list::KeyExpList; +use i_tree::key::tree::KeyExpTree; + +pub(crate) trait FillStrategy { + fn add_and_fill(this: C, bot: C) -> (C, SegmentFill); +} + +pub(crate) trait FillHandler { + type Output; + fn handle(&mut self, index: usize, segment: &Segment, fill: SegmentFill) -> ControlFlow; + fn finalize(self) -> Self::Output; +} + +#[inline] +fn sweep_with_handler(scan: &mut S, segments: &[Segment], mut handler: H) -> H::Output +where + C: WindingCount, + F: FillStrategy, + S: KeyExpCollection, + H: FillHandler, +{ + let mut node = Vec::with_capacity(4); + let n = segments.len(); + 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.first_less_or_equal_by(p.x, C::new(0, 0), |s| s.is_under_point_order(p)); + + for se in node.iter() { + let sid = unsafe { segments.get_unchecked(se.index) }; + let (new_sum, fill) = F::add_and_fill(sid.count, sum_count); + sum_count = new_sum; + + if let ControlFlow::Break(result) = handler.handle(se.index, sid, fill) { + return result; + } + + if sid.x_segment.is_not_vertical() { + scan.insert(sid.x_segment.into(), sum_count, p.x); + } + } + + node.clear(); + } + + handler.finalize() +} + +pub(crate) struct SweepRunner { + list: Option>, + tree: Option>, +} + +impl SweepRunner { + #[inline] + pub(crate) fn new() -> Self { + Self { + list: None, + tree: None, + } + } + + #[inline] + pub(crate) fn run(&mut self, solver: &Solver, segments: &[Segment], handler: H) -> H::Output + where + F: FillStrategy, + H: FillHandler, + { + 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); + let result = sweep_with_handler::(&mut list, segments, handler); + self.list = Some(list); + result + } else { + let capacity = count.log2_sqrt().max(8); + let mut tree = self.take_scan_tree(capacity); + let result = sweep_with_handler::(&mut tree, segments, handler); + self.tree = Some(tree); + result + } + } + + #[inline] + pub(crate) fn run_with_fill_rule( + &mut self, + fill_rule: FillRule, + solver: &Solver, + segments: &[Segment], + handler: H, + ) -> H::Output + where + H: FillHandler, + EvenOddStrategy: FillStrategy, + NonZeroStrategy: FillStrategy, + PositiveStrategy: FillStrategy, + NegativeStrategy: FillStrategy, + { + match fill_rule { + FillRule::EvenOdd => self.run::(solver, segments, handler), + FillRule::NonZero => self.run::(solver, segments, handler), + FillRule::Positive => self.run::(solver, segments, handler), + FillRule::Negative => self.run::(solver, segments, handler), + } + } + + #[inline] + fn take_scan_list(&mut self, capacity: usize) -> KeyExpList { + 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 { + if let Some(mut tree) = self.tree.take() { + tree.clear(); + tree.reserve_capacity(capacity); + tree + } else { + KeyExpTree::new(capacity) + } + } +} + +pub(crate) struct EvenOddStrategy; +pub(crate) struct NonZeroStrategy; +pub(crate) struct PositiveStrategy; +pub(crate) struct NegativeStrategy; diff --git a/iOverlay/src/core/mod.rs b/iOverlay/src/core/mod.rs index 0877a7c..c8ae4d9 100644 --- a/iOverlay/src/core/mod.rs +++ b/iOverlay/src/core/mod.rs @@ -7,5 +7,7 @@ pub(crate) mod link; pub(crate) mod nearest_vector; pub mod overlay; pub mod overlay_rule; +pub mod predicate; +pub mod relate; pub mod simplify; pub mod solver; diff --git a/iOverlay/src/core/predicate.rs b/iOverlay/src/core/predicate.rs new file mode 100644 index 0000000..2bab0ab --- /dev/null +++ b/iOverlay/src/core/predicate.rs @@ -0,0 +1,597 @@ +use crate::build::sweep::FillHandler; +use crate::segm::boolean::ShapeCountBoolean; +use crate::segm::segment::{ + BOTH_BOTTOM, BOTH_TOP, CLIP_BOTH, CLIP_BOTTOM, CLIP_TOP, SUBJ_BOTH, SUBJ_BOTTOM, SUBJ_TOP, Segment, + SegmentFill, +}; +use alloc::vec::Vec; +use core::ops::ControlFlow; +use i_float::int::point::IntPoint; +use i_key_sort::sort::two_keys::TwoKeysSort; + +/// Collects segment endpoints and checks for coincidence between subject and clip. +/// +/// Uses optimized algorithm: collect into separate Vecs, sort with `sort_by_two_keys`, +/// dedup, then binary search from shorter into longer array. +pub(crate) struct PointCoincidenceChecker { + subj_points: Vec, + clip_points: Vec, +} + +impl PointCoincidenceChecker { + /// Create a new checker with pre-allocated capacity. + /// + /// `capacity` is the number of segments; each segment contributes 2 endpoints. + #[inline] + pub(crate) fn new(capacity: usize) -> Self { + Self { + subj_points: Vec::with_capacity(capacity * 2), + clip_points: Vec::with_capacity(capacity * 2), + } + } + + /// Add a segment's endpoints based on its count and fill. + /// + /// Uses fill to skip inner segments that can't contribute to boundary coincidence: + /// - Segments entirely inside subject (SUBJ_BOTH, no clip contribution) with no + /// clip in the segment are skipped for clip collection + /// - Similarly for clip-only interior segments + #[inline] + pub(crate) fn add_segment(&mut self, segment: &Segment, fill: SegmentFill) { + // Skip inner segments optimization: + // If segment is entirely inside one shape's interior (filled on both sides) + // and has no contribution from the other shape, it's not on a boundary + // where coincidence could occur. + let subj_interior = (fill & SUBJ_BOTH) == SUBJ_BOTH; + let clip_interior = (fill & CLIP_BOTH) == CLIP_BOTH; + + if subj_interior || clip_interior || fill == 0 { + return; + } + + let is_subj = fill & SUBJ_BOTH != 0; + let is_clip = fill & CLIP_BOTH != 0; + if is_subj && is_clip { + // Segment belongs to both shapes (boundary contact) - this is a shared edge, not a point coincidence. + return; + } + if is_subj { + self.subj_points.push(segment.x_segment.a); + self.subj_points.push(segment.x_segment.b); + } else { + debug_assert!(is_clip); + self.clip_points.push(segment.x_segment.a); + self.clip_points.push(segment.x_segment.b); + } + } + + /// Check if any subject point coincides with any clip point. + /// + /// Consumes self and returns true if coincidence found. + /// + /// Optimization: Only sort/dedup the shorter array, then iterate the longer + /// array doing binary searches into the shorter. This minimizes total work: + /// O(n log n) sort + O(m log n) searches, where n ≤ m. + #[inline] + pub(crate) fn has_coincidence(mut self) -> bool { + if self.subj_points.is_empty() || self.clip_points.is_empty() { + return false; + } + + // Determine shorter/longer by pre-dedup size (good estimate of post-dedup) + let (shorter, longer) = if self.subj_points.len() <= self.clip_points.len() { + (&mut self.subj_points, &self.clip_points) + } else { + (&mut self.clip_points, &self.subj_points) + }; + + // Sort and dedup only the shorter array (binary search target) + shorter.sort_by_two_keys(false, |p| p.x, |p| p.y); + shorter.dedup(); + + // Iterate longer (unsorted) and binary search into shorter + longer.iter().any(|p| shorter.binary_search(p).is_ok()) + } +} + +/// Handler that checks if subject and clip shapes intersect (share any point). +/// +/// Returns `true` on the first segment where both shapes contribute fill, +/// indicating the geometries share at least one point (interior overlap or boundary contact). +/// This matches the DE-9IM definition of `intersects`. +/// +/// This handler is designed for early-exit optimization - it breaks out of the sweep +/// loop as soon as an intersection is detected, avoiding processing of remaining segments. +/// +/// Also collects endpoint information for point coincidence check in finalize. +pub(crate) struct IntersectsHandler { + point_checker: PointCoincidenceChecker, +} + +impl IntersectsHandler { + pub(crate) fn new(capacity: usize) -> Self { + Self { + point_checker: PointCoincidenceChecker::new(capacity), + } + } +} + +impl FillHandler for IntersectsHandler { + type Output = bool; + + #[inline(always)] + fn handle( + &mut self, + _index: usize, + segment: &Segment, + fill: SegmentFill, + ) -> ControlFlow { + // Shapes intersect if both contribute to any segment (interior overlap or boundary contact) + let has_subj = (fill & SUBJ_BOTH) != 0; + let has_clip = (fill & CLIP_BOTH) != 0; + if has_subj && has_clip { + ControlFlow::Break(true) + } else { + self.point_checker.add_segment(segment, fill); + ControlFlow::Continue(()) + } + } + + #[inline(always)] + fn finalize(self) -> bool { + self.point_checker.has_coincidence() + } +} + +/// Handler that checks if the interiors of subject and clip shapes overlap. +/// +/// Returns `true` when both shapes have fill on the same side of a segment, +/// indicating their interiors share area. This is stricter than `intersects` +/// which also returns true for boundary-only contact. +/// +/// Early-exits `true` on first interior overlap. +pub(crate) struct InteriorsIntersectHandler; + +impl FillHandler for InteriorsIntersectHandler { + type Output = bool; + + #[inline(always)] + fn handle( + &mut self, + _index: usize, + _segment: &Segment, + fill: SegmentFill, + ) -> ControlFlow { + // Interiors intersect if both shapes fill the same side + if (fill & BOTH_TOP) == BOTH_TOP || (fill & BOTH_BOTTOM) == BOTH_BOTTOM { + ControlFlow::Break(true) + } else { + ControlFlow::Continue(()) + } + } + + #[inline(always)] + fn finalize(self) -> bool { + false + } +} + +/// Handler that checks if subject and clip shapes touch (boundaries intersect but interiors don't). +/// +/// Returns `true` if boundaries contact without interior overlap. +/// Early-exits with `false` on first interior overlap since that definitively means +/// the shapes don't just touch. +/// +/// Also collects endpoint information for point coincidence check in finalize. +pub(crate) struct TouchesHandler { + has_boundary_contact: bool, + point_checker: PointCoincidenceChecker, +} + +impl TouchesHandler { + pub(crate) fn new(capacity: usize) -> Self { + Self { + has_boundary_contact: false, + point_checker: PointCoincidenceChecker::new(capacity), + } + } +} + +impl FillHandler for TouchesHandler { + type Output = bool; + + #[inline(always)] + fn handle( + &mut self, + _index: usize, + segment: &Segment, + fill: SegmentFill, + ) -> ControlFlow { + // Interior overlap = not a touch (early exit false) + if (fill & BOTH_TOP) == BOTH_TOP || (fill & BOTH_BOTTOM) == BOTH_BOTTOM { + return ControlFlow::Break(false); + } + // Track boundary contact + if (fill & SUBJ_BOTH) != 0 && (fill & CLIP_BOTH) != 0 { + self.has_boundary_contact = true; + } + self.point_checker.add_segment(segment, fill); + ControlFlow::Continue(()) + } + + #[inline(always)] + fn finalize(self) -> bool { + self.has_boundary_contact || self.point_checker.has_coincidence() + } +} + +/// Handler that checks if subject and clip shapes intersect by point coincidence only. +/// +/// Returns `true` if shapes share boundary vertices but NOT edges. +/// - Returns `false` if there's interior overlap (early exit) +/// - Returns `false` if there's edge/boundary contact (shared segments, early exit) +/// - Returns `true` ONLY if shapes touch by point coincidence without any edge overlap +pub(crate) struct PointIntersectsHandler { + point_checker: PointCoincidenceChecker, +} + +impl PointIntersectsHandler { + pub(crate) fn new(capacity: usize) -> Self { + Self { + point_checker: PointCoincidenceChecker::new(capacity), + } + } +} + +impl FillHandler for PointIntersectsHandler { + type Output = bool; + + #[inline(always)] + fn handle( + &mut self, + _index: usize, + segment: &Segment, + fill: SegmentFill, + ) -> ControlFlow { + // Interior overlap = not a point-only intersection (early exit false) + if (fill & BOTH_TOP) == BOTH_TOP || (fill & BOTH_BOTTOM) == BOTH_BOTTOM { + return ControlFlow::Break(false); + } + // Boundary contact (edge sharing) = not point-only (early exit false) + if (fill & SUBJ_BOTH) != 0 && (fill & CLIP_BOTH) != 0 { + return ControlFlow::Break(false); + } + self.point_checker.add_segment(segment, fill); + ControlFlow::Continue(()) + } + + #[inline(always)] + fn finalize(self) -> bool { + self.point_checker.has_coincidence() + } +} + +/// Handler that checks if subject is completely within clip. +/// +/// Returns `true` if everywhere the subject has fill, the clip also has fill +/// on the same side. Early-exits `false` on first violation. +pub(crate) struct WithinHandler { + subj_present: bool, +} + +impl WithinHandler { + pub(crate) fn new() -> Self { + Self { subj_present: false } + } +} + +impl FillHandler for WithinHandler { + type Output = bool; + + #[inline(always)] + fn handle( + &mut self, + _index: usize, + _segment: &Segment, + fill: SegmentFill, + ) -> ControlFlow { + let subj_top = (fill & SUBJ_TOP) != 0; + let subj_bot = (fill & SUBJ_BOTTOM) != 0; + let clip_top = (fill & CLIP_TOP) != 0; + let clip_bot = (fill & CLIP_BOTTOM) != 0; + + if subj_top || subj_bot { + self.subj_present = true; + } + + // Subject filled where clip isn't = not within + if (subj_top && !clip_top) || (subj_bot && !clip_bot) { + ControlFlow::Break(false) + } else { + ControlFlow::Continue(()) + } + } + + #[inline(always)] + fn finalize(self) -> bool { + // Empty subject is not within anything + self.subj_present + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::geom::x_segment::XSegment; + + fn make_segment(ax: i32, ay: i32, bx: i32, by: i32, subj: i32, clip: i32) -> Segment { + Segment { + x_segment: XSegment { + a: IntPoint::new(ax, ay), + b: IntPoint::new(bx, by), + }, + count: ShapeCountBoolean { subj, clip }, + } + } + + #[test] + fn test_point_coincidence_no_points() { + let checker = PointCoincidenceChecker::new(10); + assert!(!checker.has_coincidence()); + } + + #[test] + fn test_point_coincidence_subj_only() { + let mut checker = PointCoincidenceChecker::new(10); + checker.add_segment(&make_segment(0, 0, 10, 0, 1, 0), SUBJ_TOP); + assert!(!checker.has_coincidence()); + } + + #[test] + fn test_point_coincidence_coincident_point() { + let mut checker = PointCoincidenceChecker::new(10); + // Subject segment with endpoint at (10, 10) + checker.add_segment(&make_segment(0, 0, 10, 10, 1, 0), SUBJ_TOP); + // Clip segment with endpoint at (10, 10) + checker.add_segment(&make_segment(10, 10, 20, 20, 0, 1), CLIP_TOP); + assert!(checker.has_coincidence()); + } + + #[test] + fn test_point_coincidence_no_coincidence() { + let mut checker = PointCoincidenceChecker::new(10); + checker.add_segment(&make_segment(0, 0, 5, 5, 1, 0), SUBJ_TOP); + checker.add_segment(&make_segment(10, 10, 20, 20, 0, 1), CLIP_TOP); + assert!(!checker.has_coincidence()); + } + + #[test] + fn test_point_coincidence_shared_segment_is_line_not_point() { + let mut checker = PointCoincidenceChecker::new(10); + // Segment with both SUBJ and CLIP fill is a shared edge (line intersection), + // not a point coincidence. Only one array gets populated, so no coincidence. + checker.add_segment(&make_segment(0, 0, 10, 10, 1, 1), SUBJ_TOP | CLIP_BOTTOM); + assert!(!checker.has_coincidence()); + } + + #[test] + fn test_point_coincidence_dedup_works() { + let mut checker = PointCoincidenceChecker::new(10); + // Two subject segments sharing endpoint (5, 5) + checker.add_segment(&make_segment(0, 0, 5, 5, 1, 0), SUBJ_TOP); + checker.add_segment(&make_segment(5, 5, 10, 10, 1, 0), SUBJ_TOP); + // Clip at (5, 5) + checker.add_segment(&make_segment(5, 5, 15, 15, 0, 1), CLIP_TOP); + assert!(checker.has_coincidence()); + } + + #[test] + fn test_intersects_handler_both_top() { + let seg = make_segment(0, 0, 10, 0, 1, 1); + let mut handler = IntersectsHandler::new(10); + let fill = SUBJ_TOP | CLIP_TOP; + let result = handler.handle(0, &seg, fill); + assert!(matches!(result, ControlFlow::Break(true))); + } + + #[test] + fn test_intersects_handler_both_bottom() { + let seg = make_segment(0, 0, 10, 0, 1, 1); + let mut handler = IntersectsHandler::new(10); + let fill = SUBJ_BOTTOM | CLIP_BOTTOM; + let result = handler.handle(0, &seg, fill); + assert!(matches!(result, ControlFlow::Break(true))); + } + + #[test] + fn test_intersects_handler_boundary_contact() { + // Boundary contact (edge sharing) is still an intersection per DE-9IM + let seg = make_segment(0, 0, 10, 0, 1, 1); + let mut handler = IntersectsHandler::new(10); + let fill = SUBJ_TOP | CLIP_BOTTOM; + let result = handler.handle(0, &seg, fill); + assert!(matches!(result, ControlFlow::Break(true))); + } + + #[test] + fn test_intersects_handler_no_intersection() { + // Only subject contributes - no intersection + let seg = make_segment(0, 0, 10, 0, 1, 0); + let mut handler = IntersectsHandler::new(10); + let fill = SUBJ_TOP; + let result = handler.handle(0, &seg, fill); + assert!(matches!(result, ControlFlow::Continue(()))); + + // Only clip contributes - no intersection + let seg = make_segment(0, 0, 10, 0, 0, 1); + let fill = CLIP_BOTTOM; + let result = handler.handle(0, &seg, fill); + assert!(matches!(result, ControlFlow::Continue(()))); + } + + #[test] + fn test_intersects_handler_finalize_with_coincidence() { + let mut handler = IntersectsHandler::new(10); + // Add segments that don't trigger early exit but have point coincidence + let seg1 = make_segment(0, 0, 10, 10, 1, 0); + let seg2 = make_segment(10, 10, 20, 20, 0, 1); + let _ = handler.handle(0, &seg1, SUBJ_TOP); + let _ = handler.handle(1, &seg2, CLIP_TOP); + assert!(handler.finalize()); + } + + #[test] + fn test_interiors_intersect_handler_both_top() { + let seg = make_segment(0, 0, 10, 0, 1, 1); + let mut handler = InteriorsIntersectHandler; + let fill = SUBJ_TOP | CLIP_TOP; + let result = handler.handle(0, &seg, fill); + assert!(matches!(result, ControlFlow::Break(true))); + } + + #[test] + fn test_interiors_intersect_handler_both_bottom() { + let seg = make_segment(0, 0, 10, 0, 1, 1); + let mut handler = InteriorsIntersectHandler; + let fill = SUBJ_BOTTOM | CLIP_BOTTOM; + let result = handler.handle(0, &seg, fill); + assert!(matches!(result, ControlFlow::Break(true))); + } + + #[test] + fn test_interiors_intersect_handler_boundary_only() { + // Boundary contact without interior overlap + let seg = make_segment(0, 0, 10, 0, 1, 1); + let mut handler = InteriorsIntersectHandler; + let fill = SUBJ_TOP | CLIP_BOTTOM; + let result = handler.handle(0, &seg, fill); + assert!(matches!(result, ControlFlow::Continue(()))); + assert!(!handler.finalize()); + } + + #[test] + fn test_touches_handler_boundary_only() { + let seg = make_segment(0, 0, 10, 0, 1, 1); + let mut handler = TouchesHandler::new(10); + let fill = SUBJ_TOP | CLIP_BOTTOM; + let result = handler.handle(0, &seg, fill); + assert!(matches!(result, ControlFlow::Continue(()))); + assert!(handler.finalize()); // boundary contact, no interior overlap + } + + #[test] + fn test_touches_handler_interior_overlap() { + let seg = make_segment(0, 0, 10, 0, 1, 1); + let mut handler = TouchesHandler::new(10); + let fill = SUBJ_TOP | CLIP_TOP; + let result = handler.handle(0, &seg, fill); + assert!(matches!(result, ControlFlow::Break(false))); // early exit on interior overlap + } + + #[test] + fn test_touches_handler_no_contact() { + let seg = make_segment(0, 0, 10, 0, 1, 0); + let mut handler = TouchesHandler::new(10); + let fill = SUBJ_TOP; + let result = handler.handle(0, &seg, fill); + assert!(matches!(result, ControlFlow::Continue(()))); + assert!(!handler.finalize()); // no boundary contact, no interior overlap + } + + #[test] + fn test_touches_handler_point_coincidence() { + let mut handler = TouchesHandler::new(10); + // Add segments that don't touch via fill but have point coincidence + let seg1 = make_segment(0, 0, 10, 10, 1, 0); + let seg2 = make_segment(10, 10, 20, 20, 0, 1); + let _ = handler.handle(0, &seg1, SUBJ_TOP); + let _ = handler.handle(1, &seg2, CLIP_TOP); + assert!(handler.finalize()); + } + + #[test] + fn test_within_handler_subject_inside_clip() { + let seg = make_segment(0, 0, 10, 0, 1, 1); + let mut handler = WithinHandler::new(); + // Subject has top fill, clip also has top fill - subject is within + let fill = SUBJ_TOP | CLIP_TOP; + let result = handler.handle(0, &seg, fill); + assert!(matches!(result, ControlFlow::Continue(()))); + assert!(handler.finalize()); + } + + #[test] + fn test_within_handler_subject_outside_clip() { + let seg = make_segment(0, 0, 10, 0, 1, 0); + let mut handler = WithinHandler::new(); + // Subject has top fill but clip doesn't - subject is outside + let fill = SUBJ_TOP; + let result = handler.handle(0, &seg, fill); + assert!(matches!(result, ControlFlow::Break(false))); + } + + #[test] + fn test_within_handler_empty_subject() { + let handler = WithinHandler::new(); + // Empty subject is not within anything + assert!(!handler.finalize()); + } + + #[test] + fn test_within_handler_clip_only() { + let seg = make_segment(0, 0, 10, 0, 0, 1); + let mut handler = WithinHandler::new(); + // Only clip contributes - ok, but need subject present + let fill = CLIP_TOP; + let result = handler.handle(0, &seg, fill); + assert!(matches!(result, ControlFlow::Continue(()))); + assert!(!handler.finalize()); + } + + #[test] + fn test_point_intersects_handler_point_only() { + let mut handler = PointIntersectsHandler::new(10); + // Subject segment ending at (10, 10) + let seg1 = make_segment(0, 0, 10, 10, 1, 0); + // Clip segment starting at (10, 10) + let seg2 = make_segment(10, 10, 20, 20, 0, 1); + let _ = handler.handle(0, &seg1, SUBJ_TOP); + let _ = handler.handle(1, &seg2, CLIP_TOP); + // Point coincidence without edge contact → true + assert!(handler.finalize()); + } + + #[test] + fn test_point_intersects_handler_edge_contact() { + // Segment belongs to both subject and clip (shared edge) + let seg = make_segment(0, 0, 10, 0, 1, 1); + let mut handler = PointIntersectsHandler::new(10); + // Both shapes have fill on opposite sides (boundary contact) + let fill = SUBJ_TOP | CLIP_BOTTOM; + let result = handler.handle(0, &seg, fill); + // Early exit false on boundary contact (edge sharing) + assert!(matches!(result, ControlFlow::Break(false))); + } + + #[test] + fn test_point_intersects_handler_interior_overlap() { + let seg = make_segment(0, 0, 10, 0, 1, 1); + let mut handler = PointIntersectsHandler::new(10); + // Interior overlap (both shapes fill the same side) + let fill = SUBJ_TOP | CLIP_TOP; + let result = handler.handle(0, &seg, fill); + // Early exit false on interior overlap + assert!(matches!(result, ControlFlow::Break(false))); + } + + #[test] + fn test_point_intersects_handler_no_contact() { + let seg1 = make_segment(0, 0, 5, 5, 1, 0); + let seg2 = make_segment(10, 10, 20, 20, 0, 1); + let mut handler = PointIntersectsHandler::new(10); + let _ = handler.handle(0, &seg1, SUBJ_TOP); + let _ = handler.handle(1, &seg2, CLIP_TOP); + // No contact at all → false + assert!(!handler.finalize()); + } +} diff --git a/iOverlay/src/core/relate.rs b/iOverlay/src/core/relate.rs new file mode 100644 index 0000000..b54380a --- /dev/null +++ b/iOverlay/src/core/relate.rs @@ -0,0 +1,572 @@ +use crate::build::sweep::{FillHandler, SweepRunner}; +use crate::core::fill_rule::FillRule; +use crate::core::overlay::ShapeType; +use crate::core::predicate::{ + InteriorsIntersectHandler, IntersectsHandler, PointIntersectsHandler, TouchesHandler, WithinHandler, +}; +use crate::core::solver::Solver; +use crate::segm::boolean::ShapeCountBoolean; +use crate::segm::build::BuildSegments; +use crate::segm::segment::Segment; +use crate::split::solver::SplitSolver; +use alloc::vec::Vec; +use i_float::int::point::IntPoint; +use i_shape::int::shape::{IntContour, IntShape}; + +/// Overlay structure optimized for spatial predicate evaluation. +/// +/// `PredicateOverlay` provides efficient spatial relationship testing between +/// two polygon sets without computing full boolean operation results. It is +/// designed for cases where you only need to know *whether* shapes intersect, +/// not *what* the intersection looks like. +/// +/// # Example +/// +/// ```ignore +/// use i_overlay::core::relate::PredicateOverlay; +/// use i_overlay::core::overlay::ShapeType; +/// use i_overlay::segm::build::BuildSegments; +/// +/// let mut overlay = PredicateOverlay::new(16); +/// // Add subject and clip segments... +/// let intersects = overlay.intersects(); +/// ``` +/// +/// For float coordinates, prefer using [`FloatPredicateOverlay`](crate::float::relate::FloatPredicateOverlay) +/// or the [`FloatRelate`](crate::float::relate::FloatRelate) trait. +pub struct PredicateOverlay { + /// Solver configuration for segment operations. + pub solver: Solver, + /// Fill rule for determining polygon interiors. + pub fill_rule: FillRule, + pub(crate) segments: Vec>, + pub(crate) split_solver: SplitSolver, + sweep_runner: SweepRunner, +} + +impl PredicateOverlay { + #[inline] + pub fn new(capacity: usize) -> Self { + Self { + solver: Default::default(), + fill_rule: FillRule::EvenOdd, + segments: Vec::with_capacity(capacity), + split_solver: SplitSolver::new(), + sweep_runner: SweepRunner::new(), + } + } + + fn evaluate>(&mut self, handler: H) -> T { + if self.segments.is_empty() { + return T::default(); + } + self.split_solver.split_segments(&mut self.segments, &self.solver); + if self.segments.is_empty() { + return T::default(); + } + self.sweep_runner + .run_with_fill_rule(self.fill_rule, &self.solver, &self.segments, handler) + } + + /// Returns `true` if the subject and clip shapes intersect (share any point). + /// + /// This includes both interior overlap and boundary contact (including single-point touches). + #[inline] + pub fn intersects(&mut self) -> bool { + let capacity = self.segments.len(); + self.evaluate(IntersectsHandler::new(capacity)) + } + + /// Returns `true` if the interiors of subject and clip shapes overlap. + /// + /// Unlike `intersects()`, this returns `false` for shapes that only share + /// boundary points (edges or vertices) without interior overlap. + #[inline] + pub fn interiors_intersect(&mut self) -> bool { + self.evaluate(InteriorsIntersectHandler) + } + + /// Returns `true` if subject and clip shapes touch (boundaries intersect but interiors don't). + /// + /// This returns `true` when shapes share boundary points (edges or vertices) + /// but their interiors don't overlap. This includes single-point vertex touches. + #[inline] + pub fn touches(&mut self) -> bool { + let capacity = self.segments.len(); + self.evaluate(TouchesHandler::new(capacity)) + } + + /// Returns `true` if subject and clip shapes intersect by point coincidence only. + /// + /// This returns `true` when shapes share boundary vertices but NOT edges. + /// Unlike `touches()`, this returns `false` for shapes that share edges. + #[inline] + pub fn point_intersects(&mut self) -> bool { + let capacity = self.segments.len(); + self.evaluate(PointIntersectsHandler::new(capacity)) + } + + /// Returns `true` if subject is completely within clip. + /// + /// Subject is within clip if everywhere the subject has fill, the clip + /// also has fill on the same side. + #[inline] + pub fn within(&mut self) -> bool { + self.evaluate(WithinHandler::new()) + } + + /// Adds a path to the overlay using an iterator, allowing for more flexible path input. + /// This function is particularly useful when working with dynamically generated paths or + /// when paths are not directly stored in a collection. + /// - `iter`: An iterator over references to `IntPoint` that defines the path. + /// - `shape_type`: Specifies the role of the added path in the overlay operation, either as `Subject` or `Clip`. + #[inline] + pub fn add_path_iter>(&mut self, iter: I, shape_type: ShapeType) { + self.segments.append_path_iter(iter, shape_type, false); + } + + /// Adds a single path to the overlay as either subject or clip paths. + /// - `contour`: An array of points that form a closed path. + /// - `shape_type`: Specifies the role of the added path in the overlay operation, either as `Subject` or `Clip`. + #[inline] + pub fn add_contour(&mut self, contour: &[IntPoint], shape_type: ShapeType) { + self.segments + .append_path_iter(contour.iter().copied(), shape_type, false); + } + + /// Adds multiple paths to the overlay as either subject or clip paths. + /// - `contours`: An array of `IntContour` instances to be added to the overlay. + /// - `shape_type`: Specifies the role of the added paths in the overlay operation, either as `Subject` or `Clip`. + #[inline] + pub fn add_contours(&mut self, contours: &[IntContour], shape_type: ShapeType) { + for contour in contours.iter() { + self.add_contour(contour, shape_type); + } + } + + /// Adds a single shape to the overlay as either a subject or clip shape. + /// - `shape`: A reference to a `IntShape` instance to be added. + /// - `shape_type`: Specifies the role of the added shape in the overlay operation, either as `Subject` or `Clip`. + #[inline] + pub fn add_shape(&mut self, shape: &IntShape, shape_type: ShapeType) { + self.add_contours(shape, shape_type); + } + + /// Adds multiple shapes to the overlay as either subject or clip shapes. + /// - `shapes`: An array of `IntShape` instances to be added to the overlay. + /// - `shape_type`: Specifies the role of the added shapes in the overlay operation, either as `Subject` or `Clip`. + #[inline] + pub fn add_shapes(&mut self, shapes: &[IntShape], shape_type: ShapeType) { + for shape in shapes.iter() { + self.add_contours(shape, shape_type); + } + } + + #[inline] + pub fn clear(&mut self) { + self.segments.clear(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec; + + fn square(x: i32, y: i32, size: i32) -> Vec { + vec![ + IntPoint::new(x, y), + IntPoint::new(x, y + size), + IntPoint::new(x + size, y + size), + IntPoint::new(x + size, y), + ] + } + + #[test] + fn test_add_contour_intersects() { + let mut overlay = PredicateOverlay::new(16); + overlay.add_contour(&square(0, 0, 10), ShapeType::Subject); + overlay.add_contour(&square(5, 5, 10), ShapeType::Clip); + assert!(overlay.intersects()); + } + + #[test] + fn test_add_contour_disjoint() { + let mut overlay = PredicateOverlay::new(16); + overlay.add_contour(&square(0, 0, 10), ShapeType::Subject); + overlay.add_contour(&square(20, 20, 10), ShapeType::Clip); + assert!(!overlay.intersects()); + } + + #[test] + fn test_add_contour_touches() { + let mut overlay = PredicateOverlay::new(16); + overlay.add_contour(&square(0, 0, 10), ShapeType::Subject); + overlay.add_contour(&square(10, 0, 10), ShapeType::Clip); + assert!(overlay.touches()); + + overlay.clear(); + overlay.add_contour(&square(0, 0, 10), ShapeType::Subject); + overlay.add_contour(&square(10, 0, 10), ShapeType::Clip); + assert!(!overlay.interiors_intersect()); + } + + #[test] + fn test_add_contour_within() { + let mut overlay = PredicateOverlay::new(16); + overlay.add_contour(&square(5, 5, 10), ShapeType::Subject); + overlay.add_contour(&square(0, 0, 20), ShapeType::Clip); + assert!(overlay.within()); + } + + #[test] + fn test_add_contours() { + let mut overlay = PredicateOverlay::new(16); + let contours = vec![square(0, 0, 5), square(10, 10, 5)]; + overlay.add_contours(&contours, ShapeType::Subject); + overlay.add_contour(&square(2, 2, 3), ShapeType::Clip); + assert!(overlay.intersects()); + } + + #[test] + fn test_add_shape() { + let mut overlay = PredicateOverlay::new(16); + let shape = vec![square(0, 0, 10)]; + overlay.add_shape(&shape, ShapeType::Subject); + overlay.add_contour(&square(5, 5, 10), ShapeType::Clip); + assert!(overlay.intersects()); + } + + #[test] + fn test_add_shapes() { + let mut overlay = PredicateOverlay::new(16); + let shapes = vec![vec![square(0, 0, 5)], vec![square(20, 20, 5)]]; + overlay.add_shapes(&shapes, ShapeType::Subject); + overlay.add_contour(&square(2, 2, 3), ShapeType::Clip); + assert!(overlay.intersects()); + } + + #[test] + fn test_add_path_iter() { + let mut overlay = PredicateOverlay::new(16); + let points = square(0, 0, 10); + overlay.add_path_iter(points.into_iter(), ShapeType::Subject); + overlay.add_contour(&square(5, 5, 10), ShapeType::Clip); + assert!(overlay.intersects()); + } + + #[test] + fn test_point_touch_intersects() { + // Two squares touching at a single corner point (10, 10) + let mut overlay = PredicateOverlay::new(16); + overlay.add_contour(&square(0, 0, 10), ShapeType::Subject); + overlay.add_contour(&square(10, 10, 10), ShapeType::Clip); + assert!(overlay.intersects(), "point-to-point should intersect"); + } + + #[test] + fn test_point_touch_touches() { + // Two squares touching at a single corner point (10, 10) + let mut overlay = PredicateOverlay::new(16); + overlay.add_contour(&square(0, 0, 10), ShapeType::Subject); + overlay.add_contour(&square(10, 10, 10), ShapeType::Clip); + assert!(overlay.touches(), "point-to-point should touch"); + } + + #[test] + fn test_point_touch_no_interior_intersect() { + // Two squares touching at a single corner point (10, 10) + let mut overlay = PredicateOverlay::new(16); + overlay.add_contour(&square(0, 0, 10), ShapeType::Subject); + overlay.add_contour(&square(10, 10, 10), ShapeType::Clip); + assert!( + !overlay.interiors_intersect(), + "point touch has no interior intersection" + ); + } + + /// Test that intersects() detects when an edge passes through another polygon's interior. + /// + /// Shape A is a quadrilateral: (0,1), (0,0), (3,0), (3,8) + /// Shape B is a box: (0,3) to (1,4) + /// + /// The edge from (0,1) to (3,8) passes through (1, 10/3 ≈ 3.33), which is inside B. + /// Therefore intersects() should return true. + #[test] + fn test_intersects_edge_through_interior() { + // Shape A: quadrilateral with edge passing through B's interior + let shape_a = vec![ + IntPoint::new(0, 1), + IntPoint::new(0, 0), + IntPoint::new(3, 0), + IntPoint::new(3, 8), + ]; + + // Shape B: box from (0,3) to (1,4) + let shape_b = vec![ + IntPoint::new(0, 3), + IntPoint::new(1, 3), + IntPoint::new(1, 4), + IntPoint::new(0, 4), + ]; + + let mut overlay = PredicateOverlay::new(16); + overlay.add_contour(&shape_a, ShapeType::Subject); + overlay.add_contour(&shape_b, ShapeType::Clip); + + // The edge from (0,1) to (3,8) has parametric form: + // P(t) = (0,1) + t*(3,7) = (3t, 1+7t) for t in [0,1] + // At x=1: t=1/3, y = 1 + 7/3 = 10/3 ≈ 3.333 + // Point (1, 3.333) is strictly inside B (x in [0,1], y in [3,4]) + assert!( + overlay.intersects(), + "Edge (0,1)->(3,8) passes through box interior at (1, 3.33); should intersect" + ); + } + + #[test] + fn test_segment_end_to_start_touch() { + // Triangle where subject's segment endpoint touches clip's segment startpoint + // Subject: triangle at (0,0), (10,0), (5,10) + // Clip: triangle at (10,0), (20,0), (15,10) + // They touch at exactly one point: (10,0) + let subj = vec![IntPoint::new(0, 0), IntPoint::new(10, 0), IntPoint::new(5, 10)]; + let clip = vec![IntPoint::new(10, 0), IntPoint::new(20, 0), IntPoint::new(15, 10)]; + + let mut overlay = PredicateOverlay::new(16); + overlay.add_contour(&subj, ShapeType::Subject); + overlay.add_contour(&clip, ShapeType::Clip); + assert!( + overlay.intersects(), + "segment b touching segment a should intersect" + ); + + overlay.clear(); + overlay.add_contour(&subj, ShapeType::Subject); + overlay.add_contour(&clip, ShapeType::Clip); + assert!(overlay.touches(), "segment b touching segment a should touch"); + + overlay.clear(); + overlay.add_contour(&subj, ShapeType::Subject); + overlay.add_contour(&clip, ShapeType::Clip); + assert!( + !overlay.interiors_intersect(), + "segment b touching segment a should not have interior intersection" + ); + } + + /// Creates a square with a hole (doughnut shape). + /// Outer: counter-clockwise, Inner hole: clockwise + fn doughnut( + outer_x: i32, + outer_y: i32, + outer_size: i32, + hole_x: i32, + hole_y: i32, + hole_size: i32, + ) -> Vec> { + vec![ + // Outer boundary (counter-clockwise) + vec![ + IntPoint::new(outer_x, outer_y), + IntPoint::new(outer_x, outer_y + outer_size), + IntPoint::new(outer_x + outer_size, outer_y + outer_size), + IntPoint::new(outer_x + outer_size, outer_y), + ], + // Inner hole (clockwise) + vec![ + IntPoint::new(hole_x, hole_y), + IntPoint::new(hole_x + hole_size, hole_y), + IntPoint::new(hole_x + hole_size, hole_y + hole_size), + IntPoint::new(hole_x, hole_y + hole_size), + ], + ] + } + + /// Creates a diamond (rotated square) with corners at midpoints of a bounding box. + fn diamond(cx: i32, cy: i32, radius: i32) -> Vec { + vec![ + IntPoint::new(cx, cy - radius), // top + IntPoint::new(cx + radius, cy), // right + IntPoint::new(cx, cy + radius), // bottom + IntPoint::new(cx - radius, cy), // left + ] + } + + #[test] + fn test_doughnut_with_diamond_touching_hole_intersects() { + // Subject: square doughnut with outer (0,0)-(30,30) and hole (10,10)-(20,20) + // Clip: diamond inside the hole with corners touching the hole boundary + // Diamond centered at (15,15) with corners at (15,10), (20,15), (15,20), (10,15) + // + // This tests that inner segments of the hole (which have SUBJ_BOTH fill) + // are still correctly tracked for point coincidence detection. + let mut overlay = PredicateOverlay::new(32); + let doughnut_shape = doughnut(0, 0, 30, 10, 10, 10); + overlay.add_shape(&doughnut_shape, ShapeType::Subject); + overlay.add_contour(&diamond(15, 15, 5), ShapeType::Clip); + assert!( + overlay.intersects(), + "diamond touching hole boundary should intersect" + ); + } + + #[test] + fn test_doughnut_with_diamond_touching_hole_touches() { + // Same setup: diamond corners touch the hole boundary but don't overlap + let mut overlay = PredicateOverlay::new(32); + let doughnut_shape = doughnut(0, 0, 30, 10, 10, 10); + overlay.add_shape(&doughnut_shape, ShapeType::Subject); + overlay.add_contour(&diamond(15, 15, 5), ShapeType::Clip); + assert!(overlay.touches(), "diamond touching hole boundary should touch"); + } + + #[test] + fn test_doughnut_with_diamond_touching_hole_no_interior_intersect() { + // Same setup: diamond only touches at boundary points, interiors don't overlap + let mut overlay = PredicateOverlay::new(32); + let doughnut_shape = doughnut(0, 0, 30, 10, 10, 10); + overlay.add_shape(&doughnut_shape, ShapeType::Subject); + overlay.add_contour(&diamond(15, 15, 5), ShapeType::Clip); + assert!( + !overlay.interiors_intersect(), + "diamond touching hole boundary should not have interior intersection" + ); + } + + #[test] + fn test_doughnut_with_diamond_inside_hole_disjoint() { + // Diamond fully inside the hole, not touching any boundary + // Diamond centered at (15,15) with radius 2 (corners at 13,15,17,15 etc) + let mut overlay = PredicateOverlay::new(32); + let doughnut_shape = doughnut(0, 0, 30, 10, 10, 10); + overlay.add_shape(&doughnut_shape, ShapeType::Subject); + overlay.add_contour(&diamond(15, 15, 2), ShapeType::Clip); + assert!(!overlay.intersects(), "diamond inside hole should not intersect"); + assert!(!overlay.touches(), "diamond inside hole should not touch"); + } + + #[test] + fn test_doughnut_with_diamond_touching_single_corner() { + // Diamond inside the hole, touching only one corner: (10, 10) + // The hole is at (10,10)-(20,20), so a diamond inside it touching the + // bottom-left corner (10,10) needs all other points inside the hole. + // Diamond: (10,10), (12,10), (12,12), (10,12) - this is a small square + // in the corner of the hole, with one corner touching the hole corner. + let diamond_touching_corner = vec![ + IntPoint::new(10, 10), // touches hole corner (also doughnut boundary) + IntPoint::new(12, 10), // on hole bottom edge + IntPoint::new(12, 12), // inside hole + IntPoint::new(10, 12), // on hole left edge + ]; + + let mut overlay = PredicateOverlay::new(32); + overlay.add_shape(&doughnut(0, 0, 30, 10, 10, 10), ShapeType::Subject); + overlay.add_contour(&diamond_touching_corner, ShapeType::Clip); + + assert!( + overlay.intersects(), + "diamond touching hole corner should intersect" + ); + assert!(overlay.touches(), "diamond touching hole corner should touch"); + assert!( + !overlay.interiors_intersect(), + "diamond touching hole corner should not have interior intersection" + ); + } + + #[test] + fn test_outer_diamond_touching_doughnut_corner() { + // Diamond outside the doughnut, touching the outer corner at (0, 0) + // This tests point coincidence for outer boundary segments. + let diamond_outside = vec![ + IntPoint::new(0, 0), // touches doughnut outer corner + IntPoint::new(-5, 3), // outside + IntPoint::new(-5, -3), // outside + ]; + + let mut overlay = PredicateOverlay::new(32); + overlay.add_shape(&doughnut(0, 0, 30, 10, 10, 10), ShapeType::Subject); + overlay.add_contour(&diamond_outside, ShapeType::Clip); + + assert!( + overlay.intersects(), + "triangle touching outer corner should intersect" + ); + assert!(overlay.touches(), "triangle touching outer corner should touch"); + assert!( + !overlay.interiors_intersect(), + "triangle touching outer corner should not have interior intersection" + ); + } + + #[test] + fn test_point_intersects_corner_to_corner() { + // Two squares touching at a single corner point (10, 10) + let mut overlay = PredicateOverlay::new(16); + overlay.add_contour(&square(0, 0, 10), ShapeType::Subject); + overlay.add_contour(&square(10, 10, 10), ShapeType::Clip); + assert!( + overlay.point_intersects(), + "corner-to-corner should be point-only intersection" + ); + } + + #[test] + fn test_point_intersects_edge_sharing() { + // Two squares sharing an edge (not point-only) + let mut overlay = PredicateOverlay::new(16); + overlay.add_contour(&square(0, 0, 10), ShapeType::Subject); + overlay.add_contour(&square(10, 0, 10), ShapeType::Clip); + // touches() is true for edge sharing + assert!(overlay.touches()); + + overlay.clear(); + overlay.add_contour(&square(0, 0, 10), ShapeType::Subject); + overlay.add_contour(&square(10, 0, 10), ShapeType::Clip); + // point_intersects() is false for edge sharing + assert!( + !overlay.point_intersects(), + "edge sharing is not point-only intersection" + ); + } + + #[test] + fn test_point_intersects_overlapping() { + // Overlapping squares (not point-only) + let mut overlay = PredicateOverlay::new(16); + overlay.add_contour(&square(0, 0, 10), ShapeType::Subject); + overlay.add_contour(&square(5, 5, 10), ShapeType::Clip); + assert!( + !overlay.point_intersects(), + "overlapping shapes are not point-only intersection" + ); + } + + #[test] + fn test_point_intersects_disjoint() { + // Disjoint squares (no contact at all) + let mut overlay = PredicateOverlay::new(16); + overlay.add_contour(&square(0, 0, 10), ShapeType::Subject); + overlay.add_contour(&square(20, 20, 10), ShapeType::Clip); + assert!( + !overlay.point_intersects(), + "disjoint shapes have no point intersection" + ); + } + + #[test] + fn test_point_intersects_triangle_vertex() { + // Two triangles touching at a single vertex + let tri1 = vec![IntPoint::new(0, 0), IntPoint::new(10, 0), IntPoint::new(5, 10)]; + let tri2 = vec![IntPoint::new(10, 0), IntPoint::new(20, 0), IntPoint::new(15, 10)]; + + let mut overlay = PredicateOverlay::new(16); + overlay.add_contour(&tri1, ShapeType::Subject); + overlay.add_contour(&tri2, ShapeType::Clip); + assert!( + overlay.point_intersects(), + "triangles touching at vertex should be point-only intersection" + ); + } +} diff --git a/iOverlay/src/float/mod.rs b/iOverlay/src/float/mod.rs index 80652dc..4a3c14a 100644 --- a/iOverlay/src/float/mod.rs +++ b/iOverlay/src/float/mod.rs @@ -1,6 +1,7 @@ pub mod clip; pub mod graph; pub mod overlay; +pub mod relate; pub mod scale; pub mod simplify; pub mod single; diff --git a/iOverlay/src/float/relate.rs b/iOverlay/src/float/relate.rs new file mode 100644 index 0000000..7e5727f --- /dev/null +++ b/iOverlay/src/float/relate.rs @@ -0,0 +1,665 @@ +use crate::core::fill_rule::FillRule; +use crate::core::overlay::ShapeType; +use crate::core::relate::PredicateOverlay; +use crate::core::solver::Solver; +use i_float::adapter::FloatPointAdapter; +use i_float::float::compatible::FloatPointCompatible; +use i_float::float::number::FloatNumber; +use i_shape::source::resource::ShapeResource; + +/// Float-coordinate wrapper for spatial predicate evaluation. +/// +/// `FloatPredicateOverlay` handles conversion from floating-point coordinates to +/// the internal integer representation, then delegates to [`PredicateOverlay`](crate::core::relate::PredicateOverlay) +/// for efficient predicate evaluation. +/// +/// # Example +/// +/// ``` +/// use i_overlay::float::relate::FloatPredicateOverlay; +/// +/// let subject = 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]]; +/// +/// let mut overlay = FloatPredicateOverlay::with_subj_and_clip(&subject, &clip); +/// assert!(overlay.intersects()); +/// ``` +/// +/// For a more ergonomic API, see the [`FloatRelate`] trait which provides +/// methods directly on shape types. +pub struct FloatPredicateOverlay, T: FloatNumber> { + pub(crate) overlay: PredicateOverlay, + pub(crate) adapter: FloatPointAdapter, +} + +impl, T: FloatNumber> FloatPredicateOverlay { + /// Creates a new predicate overlay with a pre-configured adapter. + /// + /// Use this when you need fixed-scale precision via `FloatPointAdapter::with_scale()`. + /// + /// # Arguments + /// * `adapter` - A `FloatPointAdapter` instance for coordinate conversion. + /// * `capacity` - Initial capacity for storing segments. + #[inline] + pub fn with_adapter(adapter: FloatPointAdapter, capacity: usize) -> Self { + Self { + overlay: PredicateOverlay::new(capacity), + adapter, + } + } + + /// Creates a new predicate overlay with a pre-configured adapter, fill rule, and solver. + /// + /// Use this when you need fixed-scale precision with custom overlay settings. + /// + /// # Arguments + /// * `adapter` - A `FloatPointAdapter` instance for coordinate conversion. + /// * `fill_rule` - Fill rule to determine filled areas. + /// * `solver` - Type of solver to use. + /// * `capacity` - Initial capacity for storing segments. + #[inline] + pub fn with_adapter_custom( + adapter: FloatPointAdapter, + fill_rule: FillRule, + solver: Solver, + capacity: usize, + ) -> Self { + let mut overlay = PredicateOverlay::new(capacity); + overlay.fill_rule = fill_rule; + overlay.solver = solver; + Self { overlay, adapter } + } + + /// Creates a new predicate overlay from subject and clip shapes. + pub fn with_subj_and_clip(subj: &R0, clip: &R1) -> Self + where + R0: ShapeResource + ?Sized, + R1: ShapeResource + ?Sized, + { + let iter = subj.iter_paths().chain(clip.iter_paths()).flatten(); + let adapter = FloatPointAdapter::with_iter(iter); + let subj_capacity = subj.iter_paths().fold(0, |s, c| s + c.len()); + let clip_capacity = clip.iter_paths().fold(0, |s, c| s + c.len()); + + let mut result = Self { + overlay: PredicateOverlay::new(subj_capacity + clip_capacity), + adapter, + }; + result.add_source(subj, ShapeType::Subject); + result.add_source(clip, ShapeType::Clip); + result + } + + /// Creates a new predicate overlay with custom solver and fill rule. + pub fn with_subj_and_clip_custom( + subj: &R0, + clip: &R1, + fill_rule: FillRule, + solver: Solver, + ) -> Self + where + R0: ShapeResource + ?Sized, + R1: ShapeResource + ?Sized, + { + let iter = subj.iter_paths().chain(clip.iter_paths()).flatten(); + let adapter = FloatPointAdapter::with_iter(iter); + let subj_capacity = subj.iter_paths().fold(0, |s, c| s + c.len()); + let clip_capacity = clip.iter_paths().fold(0, |s, c| s + c.len()); + + let mut overlay = PredicateOverlay::new(subj_capacity + clip_capacity); + overlay.fill_rule = fill_rule; + overlay.solver = solver; + + let mut result = Self { overlay, adapter }; + result.add_source(subj, ShapeType::Subject); + result.add_source(clip, ShapeType::Clip); + result + } + + /// Adds a shape resource as subject or clip. + /// + /// # Arguments + /// * `resource` - A `ShapeResource` specifying the geometry to add. + /// * `shape_type` - Whether to add as `Subject` or `Clip`. + pub fn add_source + ?Sized>(&mut self, resource: &R, shape_type: ShapeType) { + for contour in resource.iter_paths() { + self.overlay + .add_path_iter(contour.iter().map(|p| self.adapter.float_to_int(p)), shape_type); + } + } + + /// Clears segments for reuse with new geometry. + #[inline] + pub fn clear(&mut self) { + self.overlay.clear(); + } + + /// Returns `true` if the subject and clip shapes intersect. + /// + /// Uses early-exit optimization - returns immediately when the first + /// intersection is found. + #[inline] + pub fn intersects(&mut self) -> bool { + self.overlay.intersects() + } + + /// Returns `true` if the interiors of subject and clip shapes overlap. + #[inline] + pub fn interiors_intersect(&mut self) -> bool { + self.overlay.interiors_intersect() + } + + /// Returns `true` if subject and clip shapes touch (boundaries intersect but interiors don't). + #[inline] + pub fn touches(&mut self) -> bool { + self.overlay.touches() + } + + /// Returns `true` if subject and clip shapes intersect by point coincidence only. + #[inline] + pub fn point_intersects(&mut self) -> bool { + self.overlay.point_intersects() + } + + /// Returns `true` if subject is completely within clip. + #[inline] + pub fn within(&mut self) -> bool { + self.overlay.within() + } +} + +/// Ergonomic trait for spatial predicate operations on shape resources. +/// +/// This trait provides convenient methods for testing spatial relationships +/// directly on contours, shapes, and shape collections without explicit +/// overlay construction. +/// +/// # Example +/// +/// ``` +/// use i_overlay::float::relate::FloatRelate; +/// +/// 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]]; +/// +/// // Overlapping shapes +/// assert!(square.intersects(&other)); +/// assert!(square.interiors_intersect(&other)); +/// assert!(!square.touches(&other)); +/// +/// let distant = vec![[100.0, 100.0], [100.0, 110.0], [110.0, 110.0], [110.0, 100.0]]; +/// +/// // Non-overlapping shapes (fast bounding-box rejection) +/// assert!(!square.intersects(&distant)); +/// assert!(square.disjoint(&distant)); +/// ``` +/// +/// # Supported Types +/// +/// This trait is implemented for any type implementing `ShapeResource`, including: +/// - `Vec<[f64; 2]>` - single contour +/// - `Vec>` - multiple contours (shape with holes) +/// - `Vec>>` - multiple shapes +pub trait FloatRelate +where + R1: ShapeResource + ?Sized, + P: FloatPointCompatible, + T: FloatNumber, +{ + /// Returns `true` if this shape intersects with another (shares any point). + /// + /// This method uses bounding-box rejection for fast negative results and + /// early-exit for fast positive results. It's significantly more efficient + /// than computing a full intersection when you only need a boolean answer. + /// + /// Matches the DE-9IM definition: returns `true` for both interior + /// overlap and boundary contact (shapes sharing an edge). + fn intersects(&self, other: &R1) -> bool; + + /// Returns `true` if the interiors of this shape and another overlap. + /// + /// Unlike `intersects()`, this returns `false` for shapes that only share + /// boundary points (edges or vertices) without interior overlap. + fn interiors_intersect(&self, other: &R1) -> bool; + + /// Returns `true` if this shape touches another (boundaries intersect but interiors don't). + /// + /// Returns `true` when shapes share boundary points but their interiors don't overlap. + fn touches(&self, other: &R1) -> bool; + + /// Returns `true` if this shape intersects another by point coincidence only. + fn point_intersects(&self, other: &R1) -> bool; + + /// Returns `true` if this shape is completely within another. + /// + /// Subject is within clip if everywhere the subject has fill, the clip + /// also has fill on the same side. + fn within(&self, other: &R1) -> bool; + + /// Returns `true` if this shape does not intersect with another (no shared points). + /// + /// This is the negation of `intersects()`. + fn disjoint(&self, other: &R1) -> bool; + + /// Returns `true` if this shape completely covers another. + /// + /// `covers(A, B)` is equivalent to `within(B, A)`. + fn covers(&self, other: &R1) -> bool; +} + +impl FloatRelate for R0 +where + R0: ShapeResource + ?Sized, + R1: ShapeResource + ?Sized, + P: FloatPointCompatible, + T: FloatNumber, +{ + #[inline] + fn intersects(&self, other: &R1) -> bool { + FloatPredicateOverlay::with_subj_and_clip(self, other).intersects() + } + + #[inline] + fn interiors_intersect(&self, other: &R1) -> bool { + FloatPredicateOverlay::with_subj_and_clip(self, other).interiors_intersect() + } + + #[inline] + fn touches(&self, other: &R1) -> bool { + FloatPredicateOverlay::with_subj_and_clip(self, other).touches() + } + + #[inline] + fn point_intersects(&self, other: &R1) -> bool { + FloatPredicateOverlay::with_subj_and_clip(self, other).point_intersects() + } + + #[inline] + fn within(&self, other: &R1) -> bool { + FloatPredicateOverlay::with_subj_and_clip(self, other).within() + } + + #[inline] + fn disjoint(&self, other: &R1) -> bool { + !self.intersects(other) + } + + #[inline] + fn covers(&self, other: &R1) -> bool { + other.within(self) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec; + use alloc::vec::Vec; + + #[test] + fn test_intersects_overlapping() { + 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]]; + + assert!(square.intersects(&other)); + assert!(other.intersects(&square)); + } + + #[test] + fn test_intersects_disjoint() { + let square = vec![[0.0, 0.0], [0.0, 10.0], [10.0, 10.0], [10.0, 0.0]]; + let other = vec![[20.0, 20.0], [20.0, 30.0], [30.0, 30.0], [30.0, 20.0]]; + + assert!(!square.intersects(&other)); + assert!(!other.intersects(&square)); + } + + #[test] + fn test_intersects_contained() { + 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]]; + + assert!(outer.intersects(&inner)); + assert!(inner.intersects(&outer)); + } + + #[test] + fn test_intersects_touching_edge() { + let left = vec![[0.0, 0.0], [0.0, 10.0], [10.0, 10.0], [10.0, 0.0]]; + let right = vec![[10.0, 0.0], [10.0, 10.0], [20.0, 10.0], [20.0, 0.0]]; + + // Shapes sharing an edge intersect (boundary contact) per DE-9IM + assert!(left.intersects(&right)); + assert!(right.intersects(&left)); + } + + #[test] + fn test_intersects_empty() { + let square = vec![[0.0, 0.0], [0.0, 10.0], [10.0, 10.0], [10.0, 0.0]]; + let empty: Vec<[f64; 2]> = vec![]; + + assert!(!square.intersects(&empty)); + assert!(!empty.intersects(&square)); + } + + #[test] + fn test_disjoint() { + let square = vec![[0.0, 0.0], [0.0, 10.0], [10.0, 10.0], [10.0, 0.0]]; + let other = vec![[20.0, 20.0], [20.0, 30.0], [30.0, 30.0], [30.0, 20.0]]; + + assert!(square.disjoint(&other)); + assert!(other.disjoint(&square)); + + let overlapping = vec![[5.0, 5.0], [5.0, 15.0], [15.0, 15.0], [15.0, 5.0]]; + assert!(!square.disjoint(&overlapping)); + } + + #[test] + fn test_interiors_intersect_overlapping() { + 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]]; + + assert!(square.interiors_intersect(&other)); + assert!(other.interiors_intersect(&square)); + } + + #[test] + fn test_interiors_intersect_touching_edge() { + let left = vec![[0.0, 0.0], [0.0, 10.0], [10.0, 10.0], [10.0, 0.0]]; + let right = vec![[10.0, 0.0], [10.0, 10.0], [20.0, 10.0], [20.0, 0.0]]; + + // Shapes sharing an edge don't have interior overlap + assert!(!left.interiors_intersect(&right)); + assert!(!right.interiors_intersect(&left)); + } + + #[test] + fn test_interiors_intersect_disjoint() { + let square = vec![[0.0, 0.0], [0.0, 10.0], [10.0, 10.0], [10.0, 0.0]]; + let other = vec![[20.0, 20.0], [20.0, 30.0], [30.0, 30.0], [30.0, 20.0]]; + + assert!(!square.interiors_intersect(&other)); + } + + #[test] + fn test_touches_edge_sharing() { + let left = vec![[0.0, 0.0], [0.0, 10.0], [10.0, 10.0], [10.0, 0.0]]; + let right = vec![[10.0, 0.0], [10.0, 10.0], [20.0, 10.0], [20.0, 0.0]]; + + // Shapes sharing an edge touch + assert!(left.touches(&right)); + assert!(right.touches(&left)); + } + + #[test] + fn test_touches_overlapping() { + 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]]; + + // Overlapping shapes don't touch (interiors intersect) + assert!(!square.touches(&other)); + assert!(!other.touches(&square)); + } + + #[test] + fn test_touches_disjoint() { + let square = vec![[0.0, 0.0], [0.0, 10.0], [10.0, 10.0], [10.0, 0.0]]; + let other = vec![[20.0, 20.0], [20.0, 30.0], [30.0, 30.0], [30.0, 20.0]]; + + // Disjoint shapes don't touch + assert!(!square.touches(&other)); + } + + #[test] + fn test_within_contained() { + 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]]; + + assert!(inner.within(&outer)); + assert!(!outer.within(&inner)); + } + + #[test] + fn test_within_overlapping() { + 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]]; + + // Overlapping shapes - neither is within the other + assert!(!square.within(&other)); + assert!(!other.within(&square)); + } + + #[test] + fn test_within_disjoint() { + let square = vec![[0.0, 0.0], [0.0, 10.0], [10.0, 10.0], [10.0, 0.0]]; + let other = vec![[20.0, 20.0], [20.0, 30.0], [30.0, 30.0], [30.0, 20.0]]; + + assert!(!square.within(&other)); + assert!(!other.within(&square)); + } + + #[test] + fn test_within_empty() { + let square = vec![[0.0, 0.0], [0.0, 10.0], [10.0, 10.0], [10.0, 0.0]]; + let empty: Vec<[f64; 2]> = vec![]; + + // Empty is not within anything + assert!(!empty.within(&square)); + // Non-empty is not within empty + assert!(!square.within(&empty)); + } + + #[test] + fn test_covers_contained() { + 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]]; + + assert!(outer.covers(&inner)); + assert!(!inner.covers(&outer)); + } + + #[test] + fn test_covers_overlapping() { + 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]]; + + // Neither covers the other + assert!(!square.covers(&other)); + assert!(!other.covers(&square)); + } + + #[test] + fn test_covers_empty() { + let square = vec![[0.0, 0.0], [0.0, 10.0], [10.0, 10.0], [10.0, 0.0]]; + let empty: Vec<[f64; 2]> = vec![]; + + // Anything covers empty (empty is within anything) + // But empty.within(square) is false, so square.covers(empty) = empty.within(square) = false + assert!(!square.covers(&empty)); + } + + #[test] + fn test_predicate_consistency() { + // Test that predicates are consistent for various scenarios + + // Overlapping squares + 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]]; + + assert!(square.intersects(&other)); + assert!(!square.disjoint(&other)); + assert!(square.interiors_intersect(&other)); + assert!(!square.touches(&other)); + assert!(!square.within(&other)); + assert!(!square.covers(&other)); + + // Disjoint squares + let distant = vec![[100.0, 100.0], [100.0, 110.0], [110.0, 110.0], [110.0, 100.0]]; + + assert!(!square.intersects(&distant)); + assert!(square.disjoint(&distant)); + assert!(!square.interiors_intersect(&distant)); + assert!(!square.touches(&distant)); + assert!(!square.within(&distant)); + assert!(!square.covers(&distant)); + + // Contained + 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]]; + + assert!(outer.intersects(&inner)); + assert!(!outer.disjoint(&inner)); + assert!(outer.interiors_intersect(&inner)); + assert!(!outer.touches(&inner)); + assert!(!outer.within(&inner)); + assert!(outer.covers(&inner)); + + assert!(inner.intersects(&outer)); + assert!(inner.interiors_intersect(&outer)); + assert!(inner.within(&outer)); + assert!(!inner.covers(&outer)); + + // Edge-sharing only + let left = vec![[0.0, 0.0], [0.0, 10.0], [10.0, 10.0], [10.0, 0.0]]; + let right = vec![[10.0, 0.0], [10.0, 10.0], [20.0, 10.0], [20.0, 0.0]]; + + assert!(left.intersects(&right)); + assert!(!left.disjoint(&right)); + assert!(!left.interiors_intersect(&right)); + assert!(left.touches(&right)); + assert!(!left.within(&right)); + assert!(!left.covers(&right)); + } + + #[test] + fn test_predicate_overlay_with_adapter() { + use crate::core::overlay::ShapeType; + 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 other = vec![[5.0, 5.0], [5.0, 15.0], [15.0, 15.0], [15.0, 5.0]]; + + let iter = square.iter().chain(other.iter()); + let adapter = FloatPointAdapter::with_iter(iter); + + let mut overlay = FloatPredicateOverlay::with_adapter(adapter, 16); + overlay.add_source(&square, ShapeType::Subject); + overlay.add_source(&other, ShapeType::Clip); + assert!(overlay.intersects()); + } + + #[test] + fn test_predicate_overlay_with_adapter_custom() { + use crate::core::fill_rule::FillRule; + use crate::core::overlay::ShapeType; + use crate::core::solver::Solver; + 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 other = vec![[5.0, 5.0], [5.0, 15.0], [15.0, 15.0], [15.0, 5.0]]; + + let iter = square.iter().chain(other.iter()); + let adapter = FloatPointAdapter::with_iter(iter); + + let mut overlay = + FloatPredicateOverlay::with_adapter_custom(adapter, FillRule::NonZero, Solver::default(), 16); + overlay.add_source(&square, ShapeType::Subject); + overlay.add_source(&other, ShapeType::Clip); + assert!(overlay.intersects()); + } + + #[test] + fn test_predicate_overlay_with_subj_and_clip_custom() { + use crate::core::fill_rule::FillRule; + use crate::core::solver::Solver; + + 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 mut overlay = FloatPredicateOverlay::with_subj_and_clip_custom( + &square, + &other, + FillRule::NonZero, + Solver::default(), + ); + assert!(overlay.intersects()); + } + + #[test] + fn test_predicate_overlay_clear() { + use crate::core::overlay::ShapeType; + + 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 mut overlay = FloatPredicateOverlay::with_subj_and_clip(&square, &other); + assert!(overlay.intersects()); + + overlay.clear(); + + // Use coordinates within the original adapter bounds + let touching = vec![[10.0, 0.0], [10.0, 10.0], [15.0, 10.0], [15.0, 0.0]]; + overlay.add_source(&square, ShapeType::Subject); + overlay.add_source(&touching, ShapeType::Clip); + // After clear and re-add, shapes touch but don't overlap interiors + assert!(overlay.intersects()); + } + + #[test] + fn test_predicate_overlay_all_predicates() { + 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 mut overlay = FloatPredicateOverlay::with_subj_and_clip(&inner, &outer); + assert!(overlay.intersects()); + + overlay.clear(); + overlay.add_source(&inner, crate::core::overlay::ShapeType::Subject); + overlay.add_source(&outer, crate::core::overlay::ShapeType::Clip); + assert!(overlay.interiors_intersect()); + + overlay.clear(); + overlay.add_source(&inner, crate::core::overlay::ShapeType::Subject); + overlay.add_source(&outer, crate::core::overlay::ShapeType::Clip); + assert!(!overlay.touches()); + + overlay.clear(); + overlay.add_source(&inner, crate::core::overlay::ShapeType::Subject); + overlay.add_source(&outer, crate::core::overlay::ShapeType::Clip); + assert!(overlay.within()); + } + + #[test] + fn test_point_intersects_trait() { + // Two squares touching at a single corner point (10, 10) + let square1 = vec![[0.0, 0.0], [0.0, 10.0], [10.0, 10.0], [10.0, 0.0]]; + let square2 = vec![[10.0, 10.0], [10.0, 20.0], [20.0, 20.0], [20.0, 10.0]]; + + // Point-only intersection → true + assert!(square1.point_intersects(&square2)); + assert!(square2.point_intersects(&square1)); + + // Edge-sharing squares (not point-only) + let square3 = vec![[10.0, 0.0], [10.0, 10.0], [20.0, 10.0], [20.0, 0.0]]; + assert!( + !square1.point_intersects(&square3), + "edge sharing is not point-only" + ); + // But they do touch + assert!(square1.touches(&square3)); + + // Overlapping squares (not point-only) + let square4 = vec![[5.0, 5.0], [5.0, 15.0], [15.0, 15.0], [15.0, 5.0]]; + assert!( + !square1.point_intersects(&square4), + "overlapping is not point-only" + ); + + // Disjoint squares (no intersection) + let square5 = vec![[100.0, 100.0], [100.0, 110.0], [110.0, 110.0], [110.0, 100.0]]; + assert!( + !square1.point_intersects(&square5), + "disjoint has no point intersection" + ); + } +} diff --git a/iOverlay/src/float/scale.rs b/iOverlay/src/float/scale.rs index b098e34..6e7d562 100644 --- a/iOverlay/src/float/scale.rs +++ b/iOverlay/src/float/scale.rs @@ -3,6 +3,7 @@ use crate::core::overlay::ShapeType; use crate::core::overlay_rule::OverlayRule; use crate::core::solver::Solver; use crate::float::overlay::{FloatOverlay, OverlayOptions}; +use crate::float::relate::FloatPredicateOverlay; use i_float::adapter::FloatPointAdapter; use i_float::float::compatible::FloatPointCompatible; use i_float::float::number::FloatNumber; @@ -178,12 +179,184 @@ impl, T: FloatNumber> FloatOverlay { } } +impl, T: FloatNumber> FloatPredicateOverlay { + /// Creates a new predicate overlay with subject and clip shapes using fixed-scale precision. + /// + /// This variant uses a fixed float-to-integer scale instead of auto-scaling. + /// It validates that the requested scale fits the input bounds and returns an error if not. + /// + /// `scale = 1.0 / grid_size` if you want a grid-size style parameter. + /// + /// # Arguments + /// * `subj` - A `ShapeResource` defining the subject geometry. + /// * `clip` - A `ShapeResource` defining the clip geometry. + /// * `scale` - Fixed float-to-integer scale factor. + pub fn with_subj_and_clip_fixed_scale( + subj: &R0, + clip: &R1, + scale: T, + ) -> Result + where + R0: ShapeResource + ?Sized, + R1: ShapeResource + ?Sized, + { + let s = FixedScaleOverlayError::validate_scale(scale)?; + + let iter = subj.iter_paths().chain(clip.iter_paths()).flatten(); + let mut adapter = FloatPointAdapter::with_iter(iter); + if adapter.dir_scale < scale { + return Err(FixedScaleOverlayError::ScaleTooLarge); + } + + adapter.dir_scale = scale; + adapter.inv_scale = T::from_float(1.0 / s); + + let subj_capacity = subj.iter_paths().fold(0, |s, c| s + c.len()); + let clip_capacity = clip.iter_paths().fold(0, |s, c| s + c.len()); + + let mut result = Self::with_adapter(adapter, subj_capacity + clip_capacity); + result.add_source(subj, ShapeType::Subject); + result.add_source(clip, ShapeType::Clip); + Ok(result) + } + + /// Creates a new predicate overlay with subject and clip shapes using fixed-scale precision + /// and custom fill rule and solver. + /// + /// # Arguments + /// * `subj` - A `ShapeResource` defining the subject geometry. + /// * `clip` - A `ShapeResource` defining the clip geometry. + /// * `fill_rule` - Fill rule to determine filled areas. + /// * `solver` - Type of solver to use. + /// * `scale` - Fixed float-to-integer scale factor. + pub fn with_subj_and_clip_fixed_scale_custom( + subj: &R0, + clip: &R1, + fill_rule: FillRule, + solver: Solver, + scale: T, + ) -> Result + where + R0: ShapeResource + ?Sized, + R1: ShapeResource + ?Sized, + { + let s = FixedScaleOverlayError::validate_scale(scale)?; + + let iter = subj.iter_paths().chain(clip.iter_paths()).flatten(); + let mut adapter = FloatPointAdapter::with_iter(iter); + if adapter.dir_scale < scale { + return Err(FixedScaleOverlayError::ScaleTooLarge); + } + + adapter.dir_scale = scale; + adapter.inv_scale = T::from_float(1.0 / s); + + let subj_capacity = subj.iter_paths().fold(0, |s, c| s + c.len()); + let clip_capacity = clip.iter_paths().fold(0, |s, c| s + c.len()); + + let mut result = Self::with_adapter_custom(adapter, fill_rule, solver, subj_capacity + clip_capacity); + result.add_source(subj, ShapeType::Subject); + result.add_source(clip, ShapeType::Clip); + Ok(result) + } +} + +/// Trait for spatial predicate operations with fixed-scale precision. +/// +/// This trait provides methods for testing spatial relationships using a fixed +/// float-to-integer scale, which is useful when you need consistent precision +/// across multiple operations or when working with known coordinate bounds. +/// +/// # Example +/// +/// ``` +/// 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]]; +/// +/// // Use fixed scale of 1000.0 for consistent precision +/// let result = square.intersects_with_fixed_scale(&other, 1000.0); +/// assert!(result.unwrap()); +/// ``` +pub trait FixedScaleFloatRelate +where + R1: ShapeResource + ?Sized, + P: FloatPointCompatible, + T: FloatNumber, +{ + /// Returns `true` if shapes intersect, using fixed-scale precision. + fn intersects_with_fixed_scale(&self, other: &R1, scale: T) -> Result; + + /// Returns `true` if interiors of shapes overlap, using fixed-scale precision. + fn interiors_intersect_with_fixed_scale( + &self, + other: &R1, + scale: T, + ) -> Result; + + /// Returns `true` if shapes touch (boundaries intersect but interiors don't), using fixed-scale precision. + fn touches_with_fixed_scale(&self, other: &R1, scale: T) -> Result; + + /// Returns `true` if this shape is completely within another, using fixed-scale precision. + fn within_with_fixed_scale(&self, other: &R1, scale: T) -> Result; + + /// Returns `true` if shapes do not intersect, using fixed-scale precision. + fn disjoint_with_fixed_scale(&self, other: &R1, scale: T) -> Result; + + /// Returns `true` if this shape completely covers another, using fixed-scale precision. + fn covers_with_fixed_scale(&self, other: &R1, scale: T) -> Result; +} + +impl FixedScaleFloatRelate for R0 +where + R0: ShapeResource + ?Sized, + R1: ShapeResource + ?Sized, + P: FloatPointCompatible, + T: FloatNumber, +{ + #[inline] + fn intersects_with_fixed_scale(&self, other: &R1, scale: T) -> Result { + Ok(FloatPredicateOverlay::with_subj_and_clip_fixed_scale(self, other, scale)?.intersects()) + } + + #[inline] + fn interiors_intersect_with_fixed_scale( + &self, + other: &R1, + scale: T, + ) -> Result { + Ok(FloatPredicateOverlay::with_subj_and_clip_fixed_scale(self, other, scale)?.interiors_intersect()) + } + + #[inline] + fn touches_with_fixed_scale(&self, other: &R1, scale: T) -> Result { + Ok(FloatPredicateOverlay::with_subj_and_clip_fixed_scale(self, other, scale)?.touches()) + } + + #[inline] + fn within_with_fixed_scale(&self, other: &R1, scale: T) -> Result { + Ok(FloatPredicateOverlay::with_subj_and_clip_fixed_scale(self, other, scale)?.within()) + } + + #[inline] + fn disjoint_with_fixed_scale(&self, other: &R1, scale: T) -> Result { + Ok(!FloatPredicateOverlay::with_subj_and_clip_fixed_scale(self, other, scale)?.intersects()) + } + + #[inline] + fn covers_with_fixed_scale(&self, other: &R1, scale: T) -> Result { + Ok(FloatPredicateOverlay::with_subj_and_clip_fixed_scale(other, self, scale)?.within()) + } +} + #[cfg(test)] mod tests { use crate::core::fill_rule::FillRule; use crate::core::overlay_rule::OverlayRule; use crate::float::overlay::FloatOverlay; - use crate::float::scale::FixedScaleFloatOverlay; + use crate::float::relate::FloatPredicateOverlay; + use crate::float::scale::{FixedScaleFloatOverlay, FixedScaleFloatRelate}; use alloc::vec; #[test] @@ -264,4 +437,212 @@ mod tests { FloatOverlay::with_subj_and_clip_fixed_scale(&left_rect, &right_rect, f64::INFINITY).is_err() ); } + + #[test] + fn test_intersects_with_fixed_scale() { + 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 result = square.intersects_with_fixed_scale(&other, 1000.0); + assert!(result.unwrap()); + } + + #[test] + fn test_intersects_with_fixed_scale_disjoint() { + let square = vec![[0.0, 0.0], [0.0, 10.0], [10.0, 10.0], [10.0, 0.0]]; + let other = vec![[20.0, 20.0], [20.0, 30.0], [30.0, 30.0], [30.0, 20.0]]; + + let result = square.intersects_with_fixed_scale(&other, 1000.0); + assert!(!result.unwrap()); + } + + #[test] + fn test_interiors_intersect_with_fixed_scale() { + 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 result = square.interiors_intersect_with_fixed_scale(&other, 1000.0); + assert!(result.unwrap()); + } + + #[test] + fn test_touches_with_fixed_scale() { + let left = vec![[0.0, 0.0], [0.0, 10.0], [10.0, 10.0], [10.0, 0.0]]; + let right = vec![[10.0, 0.0], [10.0, 10.0], [20.0, 10.0], [20.0, 0.0]]; + + let result = left.touches_with_fixed_scale(&right, 1000.0); + assert!(result.unwrap()); + } + + #[test] + fn test_within_with_fixed_scale() { + 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 result = inner.within_with_fixed_scale(&outer, 1000.0); + assert!(result.unwrap()); + } + + #[test] + fn test_predicate_overlay_with_fixed_scale() { + 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 mut overlay = + FloatPredicateOverlay::with_subj_and_clip_fixed_scale(&square, &other, 1000.0).unwrap(); + assert!(overlay.intersects()); + } + + #[test] + fn test_predicate_fixed_scale_invalid() { + 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]]; + + assert!(FloatPredicateOverlay::with_subj_and_clip_fixed_scale(&square, &other, -1.0).is_err()); + assert!(FloatPredicateOverlay::with_subj_and_clip_fixed_scale(&square, &other, 0.0).is_err()); + assert!(FloatPredicateOverlay::with_subj_and_clip_fixed_scale(&square, &other, f64::NAN).is_err()); + assert!( + FloatPredicateOverlay::with_subj_and_clip_fixed_scale(&square, &other, f64::INFINITY).is_err() + ); + } + + #[test] + fn test_disjoint_with_fixed_scale() { + let square = vec![[0.0, 0.0], [0.0, 10.0], [10.0, 10.0], [10.0, 0.0]]; + let other = vec![[20.0, 20.0], [20.0, 30.0], [30.0, 30.0], [30.0, 20.0]]; + + let result = square.disjoint_with_fixed_scale(&other, 1000.0); + assert!(result.unwrap()); + + let overlapping = vec![[5.0, 5.0], [5.0, 15.0], [15.0, 15.0], [15.0, 5.0]]; + let result = square.disjoint_with_fixed_scale(&overlapping, 1000.0); + assert!(!result.unwrap()); + } + + #[test] + fn test_covers_with_fixed_scale() { + 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 result = outer.covers_with_fixed_scale(&inner, 1000.0); + assert!(result.unwrap()); + + let result = inner.covers_with_fixed_scale(&outer, 1000.0); + assert!(!result.unwrap()); + } + + #[test] + fn test_fixed_scale_custom_overlay() { + use crate::core::solver::Solver; + + let left_rect = vec![[0.0, 0.0], [0.0, 1.0], [1.0, 1.0], [1.0, 0.0]]; + let right_rect = vec![[1.0, 0.0], [1.0, 1.0], [2.0, 1.0], [2.0, 0.0]]; + + let shapes = FloatOverlay::with_subj_and_clip_fixed_scale_custom( + &left_rect, + &right_rect, + Default::default(), + Solver::default(), + 10.0, + ) + .unwrap() + .overlay(OverlayRule::Union, FillRule::EvenOdd); + + assert_eq!(shapes.len(), 1); + assert_eq!(shapes[0].len(), 1); + assert_eq!(shapes[0][0].len(), 4); + } + + #[test] + fn test_fixed_scale_custom_overlay_invalid() { + use crate::core::solver::Solver; + + let left_rect = vec![[0.0, 0.0], [0.0, 1.0], [1.0, 1.0], [1.0, 0.0]]; + let right_rect = vec![[1.0, 0.0], [1.0, 1.0], [2.0, 1.0], [2.0, 0.0]]; + + let result = FloatOverlay::with_subj_and_clip_fixed_scale_custom( + &left_rect, + &right_rect, + Default::default(), + Solver::default(), + -1.0, + ); + assert!(result.is_err()); + } + + #[test] + fn test_fixed_scale_custom_overlay_scale_too_large() { + use crate::core::solver::Solver; + + let left_rect = vec![[0.0, 0.0], [0.0, 1.0], [1.0, 1.0], [1.0, 0.0]]; + let right_rect = vec![[1.0, 0.0], [1.0, 1.0], [2.0, 1.0], [2.0, 0.0]]; + + let scale = (1u64 << 32) as f64; + let result = FloatOverlay::with_subj_and_clip_fixed_scale_custom( + &left_rect, + &right_rect, + Default::default(), + Solver::default(), + scale, + ); + assert!(result.is_err()); + } + + #[test] + fn test_predicate_overlay_with_fixed_scale_custom() { + use crate::core::solver::Solver; + + 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 mut overlay = FloatPredicateOverlay::with_subj_and_clip_fixed_scale_custom( + &square, + &other, + FillRule::NonZero, + Solver::default(), + 1000.0, + ) + .unwrap(); + assert!(overlay.intersects()); + } + + #[test] + fn test_predicate_overlay_with_fixed_scale_custom_invalid() { + use crate::core::solver::Solver; + + 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 result = FloatPredicateOverlay::with_subj_and_clip_fixed_scale_custom( + &square, + &other, + FillRule::NonZero, + Solver::default(), + -1.0, + ); + assert!(result.is_err()); + + let scale = (1u64 << 32) as f64; + let result = FloatPredicateOverlay::with_subj_and_clip_fixed_scale_custom( + &square, + &other, + FillRule::NonZero, + Solver::default(), + scale, + ); + assert!(result.is_err()); + } + + #[test] + fn test_fixed_scale_relate_invalid_scale() { + 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]]; + + assert!(square.intersects_with_fixed_scale(&other, -1.0).is_err()); + assert!(square.interiors_intersect_with_fixed_scale(&other, -1.0).is_err()); + assert!(square.touches_with_fixed_scale(&other, -1.0).is_err()); + assert!(square.within_with_fixed_scale(&other, -1.0).is_err()); + assert!(square.disjoint_with_fixed_scale(&other, -1.0).is_err()); + assert!(square.covers_with_fixed_scale(&other, -1.0).is_err()); + } }