diff --git a/iOverlay/src/split/solver_fragment.rs b/iOverlay/src/split/solver_fragment.rs index 8aff48c..9b622c4 100644 --- a/iOverlay/src/split/solver_fragment.rs +++ b/iOverlay/src/split/solver_fragment.rs @@ -58,7 +58,9 @@ where j += 1; } - let index = buffer.layout.index(x); + // Segments ending at the border are stored in the left group. + // on_border contains only borders with a positive group index. + let index = buffer.layout.index(x) - 1; if let Some(fragments) = buffer.groups.get(index) { self.on_border_split(x, fragments, &mut buffer.on_border[j0..j]); } @@ -312,3 +314,140 @@ where cross.is_round } } + +#[cfg(test)] +mod tests { + use crate::core::solver::Solver; + use crate::geom::x_segment::XSegment; + use crate::segm::boolean::ShapeCountBoolean; + use crate::segm::segment::Segment; + use crate::split::grid_layout::GridLayout; + use crate::split::solver::SplitSolver; + use alloc::vec::Vec; + use i_float::int::point::IntPoint; + + // Exercise the complete splitter so the tests include selection of the border's + // neighboring group, not just on_border_split with an already selected group. + fn assert_border_split(edges: &[[i32; 4]], expected_verticals: &[[i32; 4]]) { + // Keep the x range at [0, 8]. With 4..15 edges the column width is 4. + // These isolated edges do not intersect the geometry under test. + let padding = [[0, 100, 8, 100], [0, 102, 8, 102]]; + for dx in [-11, 0, 7] { + let segment = |&[ax, ay, bx, by]: &[i32; 4]| Segment { + x_segment: XSegment { + a: IntPoint::new(ax + dx, ay), + b: IntPoint::new(bx + dx, by), + }, + count: ShapeCountBoolean::SUBJ_DIRECT, + data: (), + }; + let mut input: Vec<_> = edges.iter().chain(&padding).map(segment).collect(); + input.sort_unstable(); + let layout = GridLayout::new(input.iter().map(|s| s.x_segment), input.len()).unwrap(); + assert_eq!(layout.pos(1), dx + 4); + assert_eq!(layout.pos(2), dx + 8); + + let mut expected: Vec<_> = edges + .iter() + .filter(|e| e[0] != e[2]) + .chain(expected_verticals) + .chain(&padding) + .map(segment) + .collect(); + expected.sort_unstable(); + let expected: Vec<_> = expected.iter().map(|s| (s.x_segment, s.count)).collect(); + + for solver in [Solver::LIST, Solver::TREE, Solver::FRAG] { + for multithreading in [None, solver.multithreading] { + let solver = Solver { + multithreading, + ..solver + }; + let mut actual = input.clone(); + SplitSolver::new().split_segments(&mut actual, &solver); + let actual: Vec<_> = actual.iter().map(|s| (s.x_segment, s.count)).collect(); + assert_eq!( + actual, + expected, + "strategy={:?}, dx={dx}, multithreading={}", + solver.strategy, + multithreading.is_some(), + ); + } + } + } + } + + #[test] + fn border_horizontal_endpoint_from_left() { + // Cover both the first internal border and the rightmost border, where + // the right group contains only the vertical segment. + for x in [4, 8] { + assert_border_split(&[[x, 0, x, 6], [0, 3, x, 3]], &[[x, 0, x, 3], [x, 3, x, 6]]); + } + } + + #[test] + fn border_sloped_endpoints_at_same_point() { + // Rising and falling edges create duplicate marks at (4, 3). + assert_border_split( + &[[4, 0, 4, 6], [0, 0, 4, 3], [0, 6, 4, 3]], + &[[4, 0, 4, 3], [4, 3, 4, 6]], + ); + } + + #[test] + fn border_multiple_points_and_verticals() { + assert_border_split( + &[ + [4, 0, 4, 3], + [4, 5, 4, 8], + [0, 1, 4, 1], + [0, 2, 4, 2], + [0, 6, 4, 6], + [0, 7, 4, 7], + ], + &[ + [4, 0, 4, 1], + [4, 1, 4, 2], + [4, 2, 4, 3], + [4, 5, 4, 6], + [4, 6, 4, 7], + [4, 7, 4, 8], + ], + ); + } + + #[test] + fn border_multiple_columns() { + assert_border_split( + &[[4, 0, 4, 6], [8, 0, 8, 6], [0, 1, 4, 1], [6, 3, 8, 3]], + &[[4, 0, 4, 1], [4, 1, 4, 6], [8, 0, 8, 3], [8, 3, 8, 6]], + ); + } + + #[test] + fn border_shared_ends_and_outside_points_do_not_split() { + assert_border_split( + &[ + [4, 0, 4, 6], + [0, -1, 4, -1], + [0, 0, 4, 0], + [0, 6, 4, 6], + [0, 7, 4, 7], + ], + &[[4, 0, 4, 6]], + ); + } + + #[test] + fn border_endpoint_from_right() { + // This contact is handled inside the right group by bin_split. + assert_border_split(&[[4, 0, 4, 6], [4, 3, 8, 3]], &[[4, 0, 4, 3], [4, 3, 4, 6]]); + } + + #[test] + fn border_first_column_has_no_left_neighbor() { + assert_border_split(&[[0, 0, 0, 6], [0, 3, 8, 3]], &[[0, 0, 0, 3], [0, 3, 0, 6]]); + } +} diff --git a/iOverlay/tests/fragment_tests.rs b/iOverlay/tests/fragment_tests.rs index e3bed8f..15ea690 100644 --- a/iOverlay/tests/fragment_tests.rs +++ b/iOverlay/tests/fragment_tests.rs @@ -11,6 +11,45 @@ mod tests { use i_shape::int::path::IntPath; use i_shape::int::shape::IntContour; + #[test] + fn test_issue_87_frag_preserves_filled_region() { + // https://github.com/iShape-Rust/iOverlay/issues/87 + let subj = vec![ + vec![ + IntPoint::new(0, 0), + IntPoint::new(5, 1), + IntPoint::new(4, 0), + IntPoint::new(4, 2), + ], + vec![IntPoint::new(0, 0), IntPoint::new(1, 0), IntPoint::new(4, 1)], + ]; + let results: Vec<_> = [Solver::LIST, Solver::TREE, Solver::AUTO, Solver::FRAG] + .into_iter() + .map(|solver| { + let shapes = Overlay::with_contours_custom(&subj, &[], Default::default(), solver) + .overlay(OverlayRule::Subject, FillRule::EvenOdd); + let area2: i64 = shapes + .iter() + .flatten() + .map(|contour| { + contour + .iter() + .zip(contour.iter().cycle().skip(1)) + .map(|(a, b)| i64::from(a.x) * i64::from(b.y) - i64::from(b.x) * i64::from(a.y)) + .sum::() + }) + .sum(); + println!("{:?}: area2={area2}, shapes={shapes:?}", solver.strategy); + (solver.strategy, shapes, area2) + }) + .collect(); + + for (strategy, shapes, area2) in &results { + assert_eq!(*area2, 7, "{strategy:?} lost part of the filled region"); + assert_eq!(shapes, &results[0].1, "{strategy:?} differs from List"); + } + } + #[test] fn test_many_squares() { let fill = FillRule::NonZero;