diff --git a/iOverlay/src/core/extract.rs b/iOverlay/src/core/extract.rs index afad0a7..a0d0cc9 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,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, 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,35 +183,29 @@ 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 { - link_id = GraphUtil::next_link( - self.links, - self.nodes, - link_id, - node_id, - clockwise, - &buffer.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 // 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 +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, 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..2ed0f22 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 hole_count_hint = 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; + hole_count_hint += 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 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 { @@ -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(hole_count_hint); + let mut anchors = Vec::with_capacity(hole_count_hint); 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, 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 original_contour_len = 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); + original_contour_len += 1; + } + + // Revisit the contour in reverse; + // all links escape current contour are skipped in `contour_visited`. + + points.reserve_capacity(original_contour_len); + 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_len = 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_len < original_contour_len { + // 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) + }; + + // Self-touch splits can only produce holes inside this contour. + + let hole_start_data = StartPathData::new(clockwise, link, left_top_link); + self.find_contour( + &hole_start_data, + clockwise, + VisitState::HoleVisited, + contour_visited, + points, + ); + + // Hole have to belong 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 + } } diff --git a/iOverlay/tests/ocg_tests.rs b/iOverlay/tests/ocg_tests.rs index 1248645..be52a82 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}; @@ -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]], @@ -47,6 +47,46 @@ 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 @@ -62,7 +102,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 +147,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 +178,262 @@ 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 mut overlay = + Overlay::with_contours_custom(&subj_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 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 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_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 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 ┌───┐ + // │ │ + // 2 ┌───●───●───┐ + // │ │ ░ │ │ + // 1 └───●───●───┘ + // │ │ + // 0 └───┘ + + 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 mut overlay = + Overlay::with_contours_custom(&subj_paths, &[], IntOverlayOptions::ogc(), Default::default()); + + let result = overlay.overlay(OverlayRule::Union, FillRule::EvenOdd); + + assert_eq!(result.len(), 4); + assert_eq!(result[0].len(), 1); + assert_eq!(result[0][0].len(), 4); + 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] + 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]], + ]; + + let mut overlay = + Overlay::with_contours_custom(&subj_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 {