From b7552736fce50a108c96748bd8d6eb04a09e1056 Mon Sep 17 00:00:00 2001 From: Nail Sharipov Date: Sun, 8 Feb 2026 17:30:06 +0300 Subject: [PATCH 01/10] traverse contour second time --- iOverlay/src/core/extract.rs | 20 ++-- iOverlay/src/core/extract_ogc.rs | 186 +++++++++++++++++++++++++++---- 2 files changed, 178 insertions(+), 28 deletions(-) diff --git a/iOverlay/src/core/extract.rs b/iOverlay/src/core/extract.rs index afad0a7..e04aafc 100644 --- a/iOverlay/src/core/extract.rs +++ b/iOverlay/src/core/extract.rs @@ -31,6 +31,7 @@ pub(crate) enum VisitState { pub struct BooleanExtractionBuffer { pub(crate) points: Vec, pub(crate) visited: Vec, + pub(crate) contour_visited: Option>, } impl OverlayGraph<'_> { @@ -120,7 +121,7 @@ impl OverlayGraph<'_> { let direction = is_hole == clockwise; let start_data = StartPathData::new(direction, link, left_top_link); - self.find_contour(&start_data, direction, visited_state, buffer); + self.find_contour(&start_data, direction, visited_state, &mut buffer.visited, &mut buffer.points); let (is_valid, is_modified) = buffer.points.validate( self.options.min_output_area, self.options.preserve_output_collinear, @@ -176,15 +177,16 @@ impl OverlayGraph<'_> { start_data: &StartPathData, clockwise: bool, visited_state: VisitState, - buffer: &mut BooleanExtractionBuffer, + visited: &mut Vec, + points: &mut Vec, ) { let mut link_id = start_data.link_id; let mut node_id = start_data.node_id; let last_node_id = start_data.last_node_id; - buffer.visited.visit_edge(link_id, visited_state); - buffer.points.clear(); - buffer.points.push(start_data.begin); + visited.visit_edge(link_id, visited_state); + points.clear(); + points.push(start_data.begin); // Find a closed tour while node_id != last_node_id { @@ -194,7 +196,7 @@ impl OverlayGraph<'_> { link_id, node_id, clockwise, - &buffer.visited, + &visited, ); let link = unsafe { @@ -202,9 +204,9 @@ impl OverlayGraph<'_> { // from `find_left_top_link`, so it remains in `0..self.links.len()`. self.links.get_unchecked(link_id) }; - node_id = buffer.points.push_node_and_get_other(link, node_id); + node_id = points.push_node_and_get_other(link, node_id); - buffer.visited.visit_edge(link_id, visited_state); + visited.visit_edge(link_id, visited_state); } } @@ -242,7 +244,7 @@ impl OverlayGraph<'_> { let direction = is_hole == clockwise; let start_data = StartPathData::new(direction, link, left_top_link); - self.find_contour(&start_data, direction, visited_state, buffer); + self.find_contour(&start_data, direction, visited_state, &mut buffer.visited, &mut buffer.points); let (is_valid, _) = buffer.points.validate( self.options.min_output_area, self.options.preserve_output_collinear, diff --git a/iOverlay/src/core/extract_ogc.rs b/iOverlay/src/core/extract_ogc.rs index 3b3b26f..e4b0eb4 100644 --- a/iOverlay/src/core/extract_ogc.rs +++ b/iOverlay/src/core/extract_ogc.rs @@ -9,7 +9,8 @@ use crate::core::overlay_rule::OverlayRule; use crate::geom::v_segment::VSegment; use alloc::vec; use alloc::vec::Vec; -use i_shape::int::shape::IntShapes; +use i_float::int::point::IntPoint; +use i_shape::int::shape::{IntShape, IntShapes}; use i_shape::util::reserve::Reserve; impl OverlayGraph<'_> { @@ -20,10 +21,21 @@ impl OverlayGraph<'_> { ) -> IntShapes { let is_main_dir_cw = self.options.output_direction == ContourDirection::Clockwise; + let mut contour_visited = if let Some(mut visited) = buffer.contour_visited.take() { + let target_len = buffer.visited.len(); + if visited.len() != target_len { + visited.resize(target_len, VisitState::Skipped); + } + visited.fill(VisitState::Skipped); + visited + } else { + vec![VisitState::Skipped; buffer.visited.len()] + }; + let mut shapes = Vec::new(); buffer.points.reserve_capacity(buffer.visited.len()); - let mut avg_holes_count = 0; + let mut min_holes_count = 0; let mut link_index = 0; while link_index < buffer.visited.len() { @@ -58,26 +70,24 @@ impl OverlayGraph<'_> { visited_state, &mut buffer.visited, ); - avg_holes_count += 1; + min_holes_count += 1; continue; } - self.find_contour(&start_data, traversal_direction, visited_state, buffer); - let (is_valid, _) = buffer.points.validate( - self.options.min_output_area, - self.options.preserve_output_collinear, - ); - - if !is_valid { + if let Some(shape) = self.collect_shape( + &start_data, + traversal_direction, + &mut buffer.visited, + &mut contour_visited, + &mut buffer.points, + ) { + shapes.push(shape); + } else { link_index += 1; - continue; - } - - let contour = buffer.points.as_slice().to_vec(); - shapes.push(vec![contour]); + }; } - if avg_holes_count > 0 { + if min_holes_count > 0 { // Keep only hole edges; skip everything else for the second pass. for state in buffer.visited.iter_mut() { *state = match *state { @@ -86,8 +96,8 @@ impl OverlayGraph<'_> { }; } - let mut holes = Vec::with_capacity(avg_holes_count); - let mut anchors = Vec::with_capacity(avg_holes_count); + let mut holes = Vec::with_capacity(min_holes_count); + let mut anchors = Vec::with_capacity(min_holes_count); let mut anchors_already_sorted = true; link_index = 0; @@ -112,7 +122,14 @@ impl OverlayGraph<'_> { let start_data = StartPathData::new(is_main_dir_cw, link, left_top_link); - self.find_contour(&start_data, is_main_dir_cw, VisitState::HullVisited, buffer); + self.find_contour( + &start_data, + is_main_dir_cw, + VisitState::HullVisited, + &mut buffer.visited, + &mut buffer.points, + ); + let (is_valid, is_modified) = buffer.points.validate( self.options.min_output_area, self.options.preserve_output_collinear, @@ -157,6 +174,8 @@ impl OverlayGraph<'_> { shapes.join_sorted_holes(holes, anchors, is_main_dir_cw); } + buffer.contour_visited = Some(contour_visited); + shapes } @@ -192,4 +211,133 @@ impl OverlayGraph<'_> { visited.visit_edge(link_id, visited_state); } } + + fn collect_shape( + &self, + start_data: &StartPathData, + clockwise: bool, + global_visited: &mut Vec, + contour_visited: &mut Vec, + points: &mut Vec, + ) -> Option { + let mut link_id = start_data.link_id; + let mut node_id = start_data.node_id; + let last_node_id = start_data.last_node_id; + + // first find all edges belong to contour + + let mut end_link_id = start_data.link_id; + + global_visited.visit_edge(link_id, VisitState::HullVisited); + contour_visited.visit_edge(link_id, VisitState::Unvisited); + + let mut count = 1; + + // Find a closed tour + while node_id != last_node_id { + link_id = GraphUtil::next_link( + self.links, + self.nodes, + link_id, + node_id, + clockwise, + global_visited, + ); + + let link = unsafe { + // Safety: `link_id` is always derived from a previous in-bounds index or + // from `find_left_top_link`, so it remains in `0..self.links.len()`. + self.links.get_unchecked(link_id) + }; + + node_id = if link.a.id == node_id { + link.b.id + } else { + link.a.id + }; + end_link_id = end_link_id.max(link_id); + contour_visited.visit_edge(link_id, VisitState::Unvisited); + global_visited.visit_edge(link_id, VisitState::HullVisited); + count += 1; + } + + // revisit all links in reverse direction + // all links escape current contour are unvisited in contour_visited + + points.reserve_capacity(count); + self.find_contour( + &start_data, + !clockwise, + VisitState::HullVisited, + contour_visited, + points, + ); + + let (is_valid, _) = points.validate( + self.options.min_output_area, + self.options.preserve_output_collinear, + ); + + let contour_count = points.len(); + + let mut shape = if is_valid { + let mut shape = vec![]; + let contour = points.as_slice().to_vec(); + shape.push(contour); + Some(shape) + } else { + None + }; + + if contour_count < count { + // contour has self touches + let mut link_index = start_data.link_id; + while link_index <= end_link_id { + if contour_visited.is_visited(link_index) { + link_index += 1; + continue; + } + + let left_top_link = unsafe { + // Safety: `link_index` walks 0..buffer.visited.len(), and buffer.visited.len() <= self.links.len(). + GraphUtil::find_left_top_link(self.links, self.nodes, link_index, contour_visited) + }; + + let link = unsafe { + // Safety: `left_top_link` originates from `find_left_top_link`, which only returns + // indices in 0..self.links.len(), so this lookup cannot go out of bounds. + self.links.get_unchecked(left_top_link) + }; + + // only holes are possible! + + let hole_start_data = StartPathData::new(clockwise, link, left_top_link); + self.find_contour( + &hole_start_data, + clockwise, + VisitState::HoleVisited, + contour_visited, + points, + ); + + // hole belong exact to this shape + if let Some(shape) = shape.as_mut() { + let (is_valid, _) = points.validate( + self.options.min_output_area, + self.options.preserve_output_collinear, + ); + + if !is_valid { + link_index += 1; + continue; + } + + let contour = points.as_slice().to_vec(); + shape.push(contour); + } + } + } + + shape + } } From 1c824ccc7b9f578ec5f6dd323139245c5fdbcb38 Mon Sep 17 00:00:00 2001 From: Nail Sharipov Date: Sun, 8 Feb 2026 20:43:15 +0300 Subject: [PATCH 02/10] add tests --- iOverlay/tests/ocg_tests.rs | 100 ++++++++++++++++++++++++++++++++++-- 1 file changed, 97 insertions(+), 3 deletions(-) diff --git a/iOverlay/tests/ocg_tests.rs b/iOverlay/tests/ocg_tests.rs index 1248645..07a3d17 100644 --- a/iOverlay/tests/ocg_tests.rs +++ b/iOverlay/tests/ocg_tests.rs @@ -23,7 +23,7 @@ mod tests { // OGC Simple Feature Specification (ISO 19125-1) states: // "The interior of every Surface is a connected point set." - let subj_paths = int_shape![[[0, 0], [5, 0], [5, 5], [0, 5],]]; + let subj_paths = int_shape![[[0, 0], [5, 0], [5, 5], [0, 5]]]; let clip_paths = int_shape![ [[1, 2], [1, 4], [3, 4], [3, 3], [2, 3], [2, 2]], @@ -62,7 +62,7 @@ mod tests { // │ │ // 0 └───────────────────┘ - let subj_paths = int_shape![[[0, 0], [5, 0], [5, 5], [0, 5],]]; + let subj_paths = int_shape![[[0, 0], [5, 0], [5, 5], [0, 5]]]; let clip_paths = int_shape![ [[1, 2], [1, 3], [2, 3], [2, 2]], @@ -107,7 +107,7 @@ mod tests { // │ │ // 0 └───────────────────────────┘ - let subj_paths = int_shape![[[0, 0], [7, 0], [7, 7], [0, 7],]]; + let subj_paths = int_shape![[[0, 0], [7, 0], [7, 7], [0, 7]]]; let clip_paths = int_shape![ [[1, 3], [1, 4], [2, 4], [2, 3]], @@ -138,6 +138,100 @@ mod tests { assert_eq!(result[4].len(), 1); } + #[test] + fn test_3() { + // 0 1 2 3 + // 3 ┌───────┐ + // │ │ + // 2 │ ┌───●───┐ + // │ │ ░ │ │ + // 1 │ └───┘ │ + // │ │ + // 0 └───────────┘ + let subj_paths = int_shape![[[0, 3], [0, 0], [3, 0], [3, 2], [1, 2], [1, 1], [2, 1], [2, 3]]]; + + let clip_paths = int_shape![]; + + let mut overlay = Overlay::with_contours_custom( + &subj_paths, + &clip_paths, + IntOverlayOptions::ogc(), + Default::default(), + ); + + let result = overlay.overlay(OverlayRule::Union, FillRule::EvenOdd); + + assert_eq!(result.len(), 1); + assert_eq!(result[0].len(), 2); + assert_eq!(result[0][0].len(), 6); + assert_eq!(result[0][1].len(), 4); + } + + #[test] + fn test_4() { + // 0 1 2 3 + // 3 ┌───┐ + // │ │ + // 2 ┌───●───●───┐ + // │ │ ░ │ │ + // 1 └───●───●───┘ + // │ │ + // 0 └───┘ + + let subj_paths = int_shape![[[0, 0], [5, 0], [5, 5], [0, 5]]]; + + let clip_paths = int_shape![ + [[1, 2], [1, 3], [2, 3], [2, 2]], + [[2, 1], [2, 2], [3, 2], [3, 1]], + [[2, 3], [2, 4], [3, 4], [3, 3]], + [[3, 2], [3, 3], [4, 3], [4, 2]], + ]; + + let mut overlay = Overlay::with_contours_custom( + &subj_paths, + &clip_paths, + IntOverlayOptions::ogc(), + Default::default(), + ); + + let result = overlay.overlay(OverlayRule::Difference, FillRule::EvenOdd); + + assert_eq!(result.len(), 2); + assert_eq!(result[0].len(), 2); + assert_eq!(result[0][0].len(), 4); + assert_eq!(result[0][1].len(), 12); + assert_eq!(result[1].len(), 1); + assert_eq!(result[1][0].len(), 4); + } + + #[test] + fn test_5() { + let subj_paths = int_shape![ + [[-3, 0], [-3, -3], [0, -3], [0, 0], [3, 0], [3, 3], [0, 3], [0, 0]], + [[-1, -2], [-2, -1], [0, 0], [1, 2], [2, 1], [0, 0]], + ]; + + let clip_paths = int_shape![]; + + let mut overlay = Overlay::with_contours_custom( + &subj_paths, + &clip_paths, + IntOverlayOptions::ogc(), + Default::default(), + ); + + let result = overlay.overlay(OverlayRule::Union, FillRule::EvenOdd); + + assert_eq!(result.len(), 2); + assert_eq!(result[0].len(), 2); + assert_eq!(result[0][0].len(), 4); + assert_eq!(result[0][1].len(), 3); + + assert_eq!(result[1].len(), 2); + assert_eq!(result[1][0].len(), 4); + assert_eq!(result[1][1].len(), 3); + } + #[test] fn test_checkerboard_a() { for n in 4..50 { From 494783acfeb3dd951e31e3fd2cd8338fe83da3b4 Mon Sep 17 00:00:00 2001 From: Nail Sharipov Date: Sun, 8 Feb 2026 20:56:24 +0300 Subject: [PATCH 03/10] fix test 4 --- iOverlay/tests/ocg_tests.rs | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/iOverlay/tests/ocg_tests.rs b/iOverlay/tests/ocg_tests.rs index 07a3d17..d8f6f87 100644 --- a/iOverlay/tests/ocg_tests.rs +++ b/iOverlay/tests/ocg_tests.rs @@ -178,15 +178,15 @@ mod tests { // │ │ // 0 └───┘ - let subj_paths = int_shape![[[0, 0], [5, 0], [5, 5], [0, 5]]]; - - let clip_paths = int_shape![ - [[1, 2], [1, 3], [2, 3], [2, 2]], - [[2, 1], [2, 2], [3, 2], [3, 1]], - [[2, 3], [2, 4], [3, 4], [3, 3]], - [[3, 2], [3, 3], [4, 3], [4, 2]], + let subj_paths = int_shape![ + [[0, 2], [0, 1], [1, 1], [1, 2]], + [[2, 2], [2, 1], [3, 1], [3, 2]], + [[1, 1], [1, 0], [2, 0], [2, 1]], + [[1, 3], [1, 2], [2, 2], [2, 3]], ]; + let clip_paths = int_shape![]; + let mut overlay = Overlay::with_contours_custom( &subj_paths, &clip_paths, @@ -196,12 +196,15 @@ mod tests { let result = overlay.overlay(OverlayRule::Difference, FillRule::EvenOdd); - assert_eq!(result.len(), 2); - assert_eq!(result[0].len(), 2); + assert_eq!(result.len(), 4); + assert_eq!(result[0].len(), 1); assert_eq!(result[0][0].len(), 4); - assert_eq!(result[0][1].len(), 12); assert_eq!(result[1].len(), 1); assert_eq!(result[1][0].len(), 4); + assert_eq!(result[2].len(), 1); + assert_eq!(result[2][0].len(), 4); + assert_eq!(result[3].len(), 1); + assert_eq!(result[3][0].len(), 4); } #[test] From 7a15fab727c103bd1dca13df7ce73d1e2cb217c8 Mon Sep 17 00:00:00 2001 From: Nail Sharipov Date: Sun, 8 Feb 2026 20:59:36 +0300 Subject: [PATCH 04/10] overlay rule --- iOverlay/tests/ocg_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iOverlay/tests/ocg_tests.rs b/iOverlay/tests/ocg_tests.rs index d8f6f87..07aa3c3 100644 --- a/iOverlay/tests/ocg_tests.rs +++ b/iOverlay/tests/ocg_tests.rs @@ -194,7 +194,7 @@ mod tests { Default::default(), ); - let result = overlay.overlay(OverlayRule::Difference, FillRule::EvenOdd); + let result = overlay.overlay(OverlayRule::Union, FillRule::EvenOdd); assert_eq!(result.len(), 4); assert_eq!(result[0].len(), 1); From cf844194bd0269a830929cab930a404aa071bd08 Mon Sep 17 00:00:00 2001 From: Nail Sharipov Date: Sun, 8 Feb 2026 21:14:54 +0300 Subject: [PATCH 05/10] fix comments --- iOverlay/src/core/extract_ogc.rs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/iOverlay/src/core/extract_ogc.rs b/iOverlay/src/core/extract_ogc.rs index e4b0eb4..2ed0f22 100644 --- a/iOverlay/src/core/extract_ogc.rs +++ b/iOverlay/src/core/extract_ogc.rs @@ -35,7 +35,7 @@ impl OverlayGraph<'_> { let mut shapes = Vec::new(); buffer.points.reserve_capacity(buffer.visited.len()); - let mut min_holes_count = 0; + let mut hole_count_hint = 0; let mut link_index = 0; while link_index < buffer.visited.len() { @@ -70,7 +70,7 @@ impl OverlayGraph<'_> { visited_state, &mut buffer.visited, ); - min_holes_count += 1; + hole_count_hint += 1; continue; } @@ -87,7 +87,7 @@ impl OverlayGraph<'_> { }; } - if min_holes_count > 0 { + if hole_count_hint > 0 { // Keep only hole edges; skip everything else for the second pass. for state in buffer.visited.iter_mut() { *state = match *state { @@ -96,8 +96,8 @@ impl OverlayGraph<'_> { }; } - let mut holes = Vec::with_capacity(min_holes_count); - let mut anchors = Vec::with_capacity(min_holes_count); + let mut holes = Vec::with_capacity(hole_count_hint); + let mut anchors = Vec::with_capacity(hole_count_hint); let mut anchors_already_sorted = true; link_index = 0; @@ -224,14 +224,14 @@ impl OverlayGraph<'_> { let mut node_id = start_data.node_id; let last_node_id = start_data.last_node_id; - // first find all edges belong to contour + // First, mark all edges that belong to the contour. let mut end_link_id = start_data.link_id; global_visited.visit_edge(link_id, VisitState::HullVisited); contour_visited.visit_edge(link_id, VisitState::Unvisited); - let mut count = 1; + let mut original_contour_len = 1; // Find a closed tour while node_id != last_node_id { @@ -258,13 +258,13 @@ impl OverlayGraph<'_> { end_link_id = end_link_id.max(link_id); contour_visited.visit_edge(link_id, VisitState::Unvisited); global_visited.visit_edge(link_id, VisitState::HullVisited); - count += 1; + original_contour_len += 1; } - // revisit all links in reverse direction - // all links escape current contour are unvisited in contour_visited + // Revisit the contour in reverse; + // all links escape current contour are skipped in `contour_visited`. - points.reserve_capacity(count); + points.reserve_capacity(original_contour_len); self.find_contour( &start_data, !clockwise, @@ -278,7 +278,7 @@ impl OverlayGraph<'_> { self.options.preserve_output_collinear, ); - let contour_count = points.len(); + let contour_len = points.len(); let mut shape = if is_valid { let mut shape = vec![]; @@ -289,7 +289,7 @@ impl OverlayGraph<'_> { None }; - if contour_count < count { + if contour_len < original_contour_len { // contour has self touches let mut link_index = start_data.link_id; while link_index <= end_link_id { @@ -309,7 +309,7 @@ impl OverlayGraph<'_> { self.links.get_unchecked(left_top_link) }; - // only holes are possible! + // Self-touch splits can only produce holes inside this contour. let hole_start_data = StartPathData::new(clockwise, link, left_top_link); self.find_contour( @@ -320,7 +320,7 @@ impl OverlayGraph<'_> { points, ); - // hole belong exact to this shape + // Hole have to belong to this shape. if let Some(shape) = shape.as_mut() { let (is_valid, _) = points.validate( self.options.min_output_area, From 98ad21ec4f2e562c14f0bd7ee187e34bf29cd2c0 Mon Sep 17 00:00:00 2001 From: Nail Sharipov Date: Sun, 8 Feb 2026 21:21:17 +0300 Subject: [PATCH 06/10] add one more test --- iOverlay/tests/ocg_tests.rs | 39 ++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/iOverlay/tests/ocg_tests.rs b/iOverlay/tests/ocg_tests.rs index 07aa3c3..f57ecf2 100644 --- a/iOverlay/tests/ocg_tests.rs +++ b/iOverlay/tests/ocg_tests.rs @@ -169,6 +169,43 @@ mod tests { #[test] fn test_4() { + // 0 1 2 3 4 + // 4 ┌───────────┐ + // │ │ + // 3 │ ┌───●───┐ + // │ │ ░ │ │ + // 2 │ ┌───●───┘ │ + // │ │ ░ │ │ + // 1 │ └───┘ │ + // │ │ + // 0 └───────────────┘ + let subj_paths = int_shape![ + [[0, 4], [0, 0], [4, 0], [4, 3], [3, 3], [3, 4]] + ]; + + let clip_paths = int_shape![ + [[1, 2], [1, 1], [2, 1], [2, 2]], + [[2, 3], [2, 2], [3, 2], [3, 3]], + ]; + + let mut overlay = Overlay::with_contours_custom( + &subj_paths, + &clip_paths, + IntOverlayOptions::ogc(), + Default::default(), + ); + + let result = overlay.overlay(OverlayRule::Difference, FillRule::EvenOdd); + + assert_eq!(result.len(), 1); + assert_eq!(result[0].len(), 3); + assert_eq!(result[0][0].len(), 6); + assert_eq!(result[0][1].len(), 4); + assert_eq!(result[0][2].len(), 4); + } + + #[test] + fn test_5() { // 0 1 2 3 // 3 ┌───┐ // │ │ @@ -208,7 +245,7 @@ mod tests { } #[test] - fn test_5() { + fn test_6() { let subj_paths = int_shape![ [[-3, 0], [-3, -3], [0, -3], [0, 0], [3, 0], [3, 3], [0, 3], [0, 0]], [[-1, -2], [-2, -1], [0, 0], [1, 2], [2, 1], [0, 0]], From 6538c1ac62f0116fd7311cb2a947e441293bc96e Mon Sep 17 00:00:00 2001 From: Nail Sharipov Date: Sun, 8 Feb 2026 21:23:51 +0300 Subject: [PATCH 07/10] add more tests --- iOverlay/tests/ocg_tests.rs | 42 ++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/iOverlay/tests/ocg_tests.rs b/iOverlay/tests/ocg_tests.rs index f57ecf2..d1a5f15 100644 --- a/iOverlay/tests/ocg_tests.rs +++ b/iOverlay/tests/ocg_tests.rs @@ -148,6 +148,7 @@ mod tests { // 1 │ └───┘ │ // │ │ // 0 └───────────┘ + let subj_paths = int_shape![[[0, 3], [0, 0], [3, 0], [3, 2], [1, 2], [1, 1], [2, 1], [2, 3]]]; let clip_paths = int_shape![]; @@ -179,6 +180,7 @@ mod tests { // 1 │ └───┘ │ // │ │ // 0 └───────────────┘ + let subj_paths = int_shape![ [[0, 4], [0, 0], [4, 0], [4, 3], [3, 3], [3, 4]] ]; @@ -206,6 +208,44 @@ mod tests { #[test] fn test_5() { + // 0 1 2 3 4 + // 4 ┌───────────────┐ + // │ │ + // 3 │ ┌───┐ │ + // │ │ ░ │ │ + // 2 │ ┌───●───┘ │ + // │ │ ░ │ │ + // 1 │ └───┘ │ + // │ │ + // 0 └───────────────┘ + + let subj_paths = int_shape![ + [[0, 4], [0, 0], [4, 0], [4, 4]] + ]; + + let clip_paths = int_shape![ + [[1, 2], [1, 1], [2, 1], [2, 2]], + [[2, 3], [2, 2], [3, 2], [3, 3]], + ]; + + let mut overlay = Overlay::with_contours_custom( + &subj_paths, + &clip_paths, + IntOverlayOptions::ogc(), + Default::default(), + ); + + let result = overlay.overlay(OverlayRule::Difference, FillRule::EvenOdd); + + assert_eq!(result.len(), 1); + assert_eq!(result[0].len(), 3); + assert_eq!(result[0][0].len(), 4); + assert_eq!(result[0][1].len(), 4); + assert_eq!(result[0][2].len(), 4); + } + + #[test] + fn test_6() { // 0 1 2 3 // 3 ┌───┐ // │ │ @@ -245,7 +285,7 @@ mod tests { } #[test] - fn test_6() { + fn test_7() { let subj_paths = int_shape![ [[-3, 0], [-3, -3], [0, -3], [0, 0], [3, 0], [3, 3], [0, 3], [0, 0]], [[-1, -2], [-2, -1], [0, 0], [1, 2], [2, 1], [0, 0]], From bd40c435ab76b64a552ac5f6b9acf6284ad57679 Mon Sep 17 00:00:00 2001 From: Nail Sharipov Date: Sun, 8 Feb 2026 21:44:11 +0300 Subject: [PATCH 08/10] add invert tests --- iOverlay/tests/ocg_tests.rs | 100 ++++++++++++++++++++++++++++++++---- 1 file changed, 90 insertions(+), 10 deletions(-) diff --git a/iOverlay/tests/ocg_tests.rs b/iOverlay/tests/ocg_tests.rs index d1a5f15..2a241dc 100644 --- a/iOverlay/tests/ocg_tests.rs +++ b/iOverlay/tests/ocg_tests.rs @@ -1,7 +1,7 @@ #[cfg(test)] mod tests { use i_overlay::core::fill_rule::FillRule; - use i_overlay::core::overlay::{IntOverlayOptions, Overlay}; + use i_overlay::core::overlay::{ContourDirection, IntOverlayOptions, Overlay}; use i_overlay::core::overlay_rule::OverlayRule; use i_shape::{int_path, int_shape}; @@ -47,6 +47,51 @@ mod tests { assert_eq!(result[1][0].len(), 4); } + #[test] + fn test_0_invert() { + // 0 1 2 3 4 5 + // 5 ┌───────────────────┐ + // │ │ + // 4 │ ┌───────┐ │ + // │ │ ░ ░ │ │ Two L-shaped holes share vertices at (2,2) and (3,3) + // 3 │ │ ┌───●───┐ │ + // │ │ ░ │ │ ░ │ │ ░ = holes + // 2 │ └───●───┘ │ │ + // │ │ ░ ░ │ │ The shared edge disconnects the interior + // 1 │ └───────┘ │ + // │ │ + // 0 └───────────────────┘ + // + // OGC Simple Feature Specification (ISO 19125-1) states: + // "The interior of every Surface is a connected point set." + + let subj_paths = int_shape![[[0, 0], [5, 0], [5, 5], [0, 5]]]; + + let clip_paths = int_shape![ + [[1, 2], [1, 4], [3, 4], [3, 3], [2, 3], [2, 2]], + [[2, 1], [2, 2], [3, 2], [3, 3], [4, 3], [4, 1]], + ]; + + let mut opts = IntOverlayOptions::ogc(); + opts.output_direction = ContourDirection::Clockwise; + + let mut overlay = Overlay::with_contours_custom( + &subj_paths, + &clip_paths, + opts, + Default::default(), + ); + + let result = overlay.overlay(OverlayRule::Difference, FillRule::EvenOdd); + + assert_eq!(result.len(), 2); + assert_eq!(result[0].len(), 2); + assert_eq!(result[0][0].len(), 4); + assert_eq!(result[0][1].len(), 8); + assert_eq!(result[1].len(), 1); + assert_eq!(result[1][0].len(), 4); + } + #[test] fn test_1() { // 0 1 2 3 4 5 @@ -151,11 +196,9 @@ mod tests { let subj_paths = int_shape![[[0, 3], [0, 0], [3, 0], [3, 2], [1, 2], [1, 1], [2, 1], [2, 3]]]; - let clip_paths = int_shape![]; - let mut overlay = Overlay::with_contours_custom( &subj_paths, - &clip_paths, + &[], IntOverlayOptions::ogc(), Default::default(), ); @@ -244,6 +287,47 @@ mod tests { assert_eq!(result[0][2].len(), 4); } + #[test] + fn test_5_invert() { + // 0 1 2 3 4 + // 4 ┌───────────────┐ + // │ │ + // 3 │ ┌───┐ │ + // │ │ ░ │ │ + // 2 │ ┌───●───┘ │ + // │ │ ░ │ │ + // 1 │ └───┘ │ + // │ │ + // 0 └───────────────┘ + + let subj_paths = int_shape![ + [[0, 4], [0, 0], [4, 0], [4, 4]] + ]; + + let clip_paths = int_shape![ + [[1, 2], [1, 1], [2, 1], [2, 2]], + [[2, 3], [2, 2], [3, 2], [3, 3]], + ]; + + let mut opts = IntOverlayOptions::ogc(); + opts.output_direction = ContourDirection::Clockwise; + + let mut overlay = Overlay::with_contours_custom( + &subj_paths, + &clip_paths, + opts, + Default::default(), + ); + + let result = overlay.overlay(OverlayRule::Difference, FillRule::EvenOdd); + + assert_eq!(result.len(), 1); + assert_eq!(result[0].len(), 3); + assert_eq!(result[0][0].len(), 4); + assert_eq!(result[0][1].len(), 4); + assert_eq!(result[0][2].len(), 4); + } + #[test] fn test_6() { // 0 1 2 3 @@ -262,11 +346,9 @@ mod tests { [[1, 3], [1, 2], [2, 2], [2, 3]], ]; - let clip_paths = int_shape![]; - let mut overlay = Overlay::with_contours_custom( &subj_paths, - &clip_paths, + &[], IntOverlayOptions::ogc(), Default::default(), ); @@ -291,11 +373,9 @@ mod tests { [[-1, -2], [-2, -1], [0, 0], [1, 2], [2, 1], [0, 0]], ]; - let clip_paths = int_shape![]; - let mut overlay = Overlay::with_contours_custom( &subj_paths, - &clip_paths, + &[], IntOverlayOptions::ogc(), Default::default(), ); From baa3be1ec6ea40bce51c95dd8fe1f672b90c68c7 Mon Sep 17 00:00:00 2001 From: Nail Sharipov Date: Sun, 8 Feb 2026 21:47:15 +0300 Subject: [PATCH 09/10] fmt --- iOverlay/src/core/extract.rs | 25 ++++++++------ iOverlay/tests/ocg_tests.rs | 65 ++++++++---------------------------- 2 files changed, 29 insertions(+), 61 deletions(-) diff --git a/iOverlay/src/core/extract.rs b/iOverlay/src/core/extract.rs index e04aafc..a0d0cc9 100644 --- a/iOverlay/src/core/extract.rs +++ b/iOverlay/src/core/extract.rs @@ -121,7 +121,13 @@ impl OverlayGraph<'_> { let direction = is_hole == clockwise; let start_data = StartPathData::new(direction, link, left_top_link); - self.find_contour(&start_data, direction, visited_state, &mut buffer.visited, &mut buffer.points); + self.find_contour( + &start_data, + direction, + visited_state, + &mut buffer.visited, + &mut buffer.points, + ); let (is_valid, is_modified) = buffer.points.validate( self.options.min_output_area, self.options.preserve_output_collinear, @@ -190,14 +196,7 @@ impl OverlayGraph<'_> { // Find a closed tour while node_id != last_node_id { - link_id = GraphUtil::next_link( - self.links, - self.nodes, - link_id, - node_id, - clockwise, - &visited, - ); + link_id = GraphUtil::next_link(self.links, self.nodes, link_id, node_id, clockwise, &visited); let link = unsafe { // Safety: `link_id` is always derived from a previous in-bounds index or @@ -244,7 +243,13 @@ impl OverlayGraph<'_> { let direction = is_hole == clockwise; let start_data = StartPathData::new(direction, link, left_top_link); - self.find_contour(&start_data, direction, visited_state, &mut buffer.visited, &mut buffer.points); + self.find_contour( + &start_data, + direction, + visited_state, + &mut buffer.visited, + &mut buffer.points, + ); let (is_valid, _) = buffer.points.validate( self.options.min_output_area, self.options.preserve_output_collinear, diff --git a/iOverlay/tests/ocg_tests.rs b/iOverlay/tests/ocg_tests.rs index 2a241dc..674523c 100644 --- a/iOverlay/tests/ocg_tests.rs +++ b/iOverlay/tests/ocg_tests.rs @@ -75,12 +75,7 @@ mod tests { let mut opts = IntOverlayOptions::ogc(); opts.output_direction = ContourDirection::Clockwise; - let mut overlay = Overlay::with_contours_custom( - &subj_paths, - &clip_paths, - opts, - Default::default(), - ); + let mut overlay = Overlay::with_contours_custom(&subj_paths, &clip_paths, opts, Default::default()); let result = overlay.overlay(OverlayRule::Difference, FillRule::EvenOdd); @@ -196,12 +191,8 @@ mod tests { let subj_paths = int_shape![[[0, 3], [0, 0], [3, 0], [3, 2], [1, 2], [1, 1], [2, 1], [2, 3]]]; - let mut overlay = Overlay::with_contours_custom( - &subj_paths, - &[], - IntOverlayOptions::ogc(), - Default::default(), - ); + let mut overlay = + Overlay::with_contours_custom(&subj_paths, &[], IntOverlayOptions::ogc(), Default::default()); let result = overlay.overlay(OverlayRule::Union, FillRule::EvenOdd); @@ -224,14 +215,9 @@ mod tests { // │ │ // 0 └───────────────┘ - let subj_paths = int_shape![ - [[0, 4], [0, 0], [4, 0], [4, 3], [3, 3], [3, 4]] - ]; + let subj_paths = int_shape![[[0, 4], [0, 0], [4, 0], [4, 3], [3, 3], [3, 4]]]; - let clip_paths = int_shape![ - [[1, 2], [1, 1], [2, 1], [2, 2]], - [[2, 3], [2, 2], [3, 2], [3, 3]], - ]; + let clip_paths = int_shape![[[1, 2], [1, 1], [2, 1], [2, 2]], [[2, 3], [2, 2], [3, 2], [3, 3]],]; let mut overlay = Overlay::with_contours_custom( &subj_paths, @@ -262,14 +248,9 @@ mod tests { // │ │ // 0 └───────────────┘ - let subj_paths = int_shape![ - [[0, 4], [0, 0], [4, 0], [4, 4]] - ]; + let subj_paths = int_shape![[[0, 4], [0, 0], [4, 0], [4, 4]]]; - let clip_paths = int_shape![ - [[1, 2], [1, 1], [2, 1], [2, 2]], - [[2, 3], [2, 2], [3, 2], [3, 3]], - ]; + let clip_paths = int_shape![[[1, 2], [1, 1], [2, 1], [2, 2]], [[2, 3], [2, 2], [3, 2], [3, 3]],]; let mut overlay = Overlay::with_contours_custom( &subj_paths, @@ -300,24 +281,14 @@ mod tests { // │ │ // 0 └───────────────┘ - let subj_paths = int_shape![ - [[0, 4], [0, 0], [4, 0], [4, 4]] - ]; + let subj_paths = int_shape![[[0, 4], [0, 0], [4, 0], [4, 4]]]; - let clip_paths = int_shape![ - [[1, 2], [1, 1], [2, 1], [2, 2]], - [[2, 3], [2, 2], [3, 2], [3, 3]], - ]; + let clip_paths = int_shape![[[1, 2], [1, 1], [2, 1], [2, 2]], [[2, 3], [2, 2], [3, 2], [3, 3]],]; let mut opts = IntOverlayOptions::ogc(); opts.output_direction = ContourDirection::Clockwise; - let mut overlay = Overlay::with_contours_custom( - &subj_paths, - &clip_paths, - opts, - Default::default(), - ); + let mut overlay = Overlay::with_contours_custom(&subj_paths, &clip_paths, opts, Default::default()); let result = overlay.overlay(OverlayRule::Difference, FillRule::EvenOdd); @@ -346,12 +317,8 @@ mod tests { [[1, 3], [1, 2], [2, 2], [2, 3]], ]; - let mut overlay = Overlay::with_contours_custom( - &subj_paths, - &[], - IntOverlayOptions::ogc(), - Default::default(), - ); + let mut overlay = + Overlay::with_contours_custom(&subj_paths, &[], IntOverlayOptions::ogc(), Default::default()); let result = overlay.overlay(OverlayRule::Union, FillRule::EvenOdd); @@ -373,12 +340,8 @@ mod tests { [[-1, -2], [-2, -1], [0, 0], [1, 2], [2, 1], [0, 0]], ]; - let mut overlay = Overlay::with_contours_custom( - &subj_paths, - &[], - IntOverlayOptions::ogc(), - Default::default(), - ); + let mut overlay = + Overlay::with_contours_custom(&subj_paths, &[], IntOverlayOptions::ogc(), Default::default()); let result = overlay.overlay(OverlayRule::Union, FillRule::EvenOdd); From 24dd18e83a67490d8a161798a8bd50133eadcd02 Mon Sep 17 00:00:00 2001 From: Nail Sharipov Date: Sun, 8 Feb 2026 22:11:30 +0300 Subject: [PATCH 10/10] more tests --- iOverlay/tests/ocg_tests.rs | 81 ++++++++++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/iOverlay/tests/ocg_tests.rs b/iOverlay/tests/ocg_tests.rs index 674523c..be52a82 100644 --- a/iOverlay/tests/ocg_tests.rs +++ b/iOverlay/tests/ocg_tests.rs @@ -301,6 +301,36 @@ mod tests { #[test] fn test_6() { + // 0 1 2 3 4 5 + // 3 ┌───────┐ ┌───────┐ + // │ │ │ │ + // 2 │ ┌───●───●───┐ │ + // │ │ ░ │ │ ░ │ │ + // 1 │ └───┘ └───┘ │ + // │ │ + // 0 └───────────────────┘ + + let subj_paths = int_shape![[[0, 3], [0, 0], [5, 0], [5, 3], [3, 3], [3, 2], [2, 2], [2, 3]],]; + let clip_paths = int_shape![[[1, 2], [1, 1], [2, 1], [2, 2]], [[3, 2], [3, 1], [4, 1], [4, 2]],]; + + let mut overlay = Overlay::with_contours_custom( + &subj_paths, + &clip_paths, + IntOverlayOptions::ogc(), + Default::default(), + ); + + let result = overlay.overlay(OverlayRule::Difference, FillRule::EvenOdd); + + assert_eq!(result.len(), 1); + assert_eq!(result[0].len(), 3); + assert_eq!(result[0][0].len(), 8); + assert_eq!(result[0][1].len(), 4); + assert_eq!(result[0][2].len(), 4); + } + + #[test] + fn test_7() { // 0 1 2 3 // 3 ┌───┐ // │ │ @@ -334,7 +364,56 @@ mod tests { } #[test] - fn test_7() { + fn test_8() { + // 0 1 2 3 4 5 + // 4 ┌───────┐ ┌───────┐ + // │ │ │ │ + // 3 │ ┌───●───●───┐ │ + // │ │ ░ │ │ ░ │ │ + // 2 │ └───●───●───┘ │ + // │ │ ░ │ │ + // 1 │ └───┘ │ + // │ │ + // 0 └───────────────────┘ + + let subj_paths = int_shape![[ + [0, 4], + [0, 0], + [5, 0], + [5, 4], + [3, 4], + [3, 3], + [4, 3], + [4, 2], + [3, 2], + [3, 1], + [2, 1], + [2, 2], + [1, 2], + [1, 3], + [2, 3], + [2, 4] + ]]; + let clip_paths = int_shape![[[2, 3], [2, 2], [3, 2], [3, 3]]]; + + let mut overlay = Overlay::with_contours_custom( + &subj_paths, + &clip_paths, + IntOverlayOptions::ogc(), + Default::default(), + ); + + let result = overlay.overlay(OverlayRule::Union, FillRule::EvenOdd); + + assert_eq!(result.len(), 2); + assert_eq!(result[0].len(), 1); + assert_eq!(result[0][0].len(), 16); + assert_eq!(result[1].len(), 1); + assert_eq!(result[1][0].len(), 4); + } + + #[test] + fn test_9() { let subj_paths = int_shape![ [[-3, 0], [-3, -3], [0, -3], [0, 0], [3, 0], [3, 3], [0, 3], [0, 0]], [[-1, -2], [-2, -1], [0, 0], [1, 2], [2, 1], [0, 0]],