use super::*; use proptest::prelude::*; use vectorcraft_geom::{Rect, shapes}; /// Sparse crossings add, compare and read like the dense counts they stand for. #[test] fn crossings_are_sparse_counts() { let mut c = Crossings::single(7, true); c += Crossings::single(2, false); c += Crossings::single(7, true); assert_eq!((c.get(14), c.get(5), c.get(15), c.get(0)), (2, 1, 0, 0)); assert_eq!(c.0.len(), 2, "only the slots that aren't 0"); assert_eq!(c.of_tag(7), Crossings(vec![(14, 2)])); assert_eq!(Crossings::single(2, false) + Crossings::single(7, true) + Crossings::single(7, true), c); assert_eq!(Crossings::default() + Crossings::default(), Crossings::default()); } fn line(a: (f64, f64), b: (f64, f64), key: u64) -> Shape { Shape::new(PathData::single(SubPath::polyline(&[Point::new(a.0, a.1), Point::new(b.0, b.1)], false)), FillRule::NonZero, key) } fn rect(x0: f64, y0: f64, x1: f64, y1: f64, key: u64) -> Shape { Shape::new(shapes::rectangle(Rect::new(x0, y0, x1, y1)), FillRule::NonZero, key) } fn area(r: &Region) -> f64 { r.path.to_bezpath().area() } fn at(faces: &[Region], x: f64, y: f64) -> Vec<&Region> { faces.iter().filter(|f| f.contains(Point::new(x, y))).collect() } #[test] fn crossing_lines_enclose_a_face() { // Three lines crossing pairwise, their ends sticking out: one triangle. let s = [line((0.0, 0.0), (100.0, 100.0), 0), line((100.0, 0.0), (0.0, 100.0), 1), line((-10.0, 80.0), (110.0, 80.0), 2)]; let (faces, edges) = live_paint(&s); assert_eq!(faces.len(), 1, "{faces:?}"); let f = &faces[0]; assert!((area(f) - 900.0).abs() < 1e-6, "triangle (20,80) (50,50) (80,80): {}", area(f)); assert!(f.sources.is_empty(), "nothing fills it yet"); assert_eq!(f.path.subpaths.len(), 1); assert_eq!(f.path.subpaths[0].anchors.len(), 3, "the loose ends are not part of it"); // Each line is cut where the others cross it: 3 + 3 + 3 pieces. assert_eq!(edges.len(), 9); assert_eq!(edges.iter().filter(|e| e.key == 2).count(), 3); } fn open(points: &[(f64, f64)], key: u64) -> Shape { let pts: Vec = points.iter().map(|&(x, y)| Point::new(x, y)).collect(); Shape::new(PathData::single(SubPath::polyline(&pts, false)), FillRule::NonZero, key) } #[test] fn gap_detection_closes_a_square_drawn_with_a_gap() { // One open path round a square, its end 2 pt short of its start. let s = [open(&[(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0), (0.0, 2.0)], 0)]; assert!(live_paint(&s).0.is_empty(), "paint leaks through the gap"); let (faces, edges, gaps) = live_paint_with_gaps(&s, 3.0); assert_eq!(gaps.len(), 1, "{gaps:?}"); let g = gaps[0]; assert!(g[0].distance(Point::new(0.0, 0.0)) < 1e-9 && g[1].distance(Point::new(0.0, 2.0)) < 1e-6, "{g:?}"); assert_eq!(faces.len(), 1, "{faces:?}"); assert!((area(&faces[0]) - 10000.0).abs() < 1e-6, "{}", area(&faces[0])); assert_eq!(edges, live_paint(&s).1, "the gap adds no edge"); // A gap wider than the size stays open. let (faces, _, gaps) = live_paint_with_gaps(&s, 1.5); assert!(faces.is_empty() && gaps.is_empty()); } #[test] fn gaps_close_to_curves_and_between_paths() { // A line across a circle, each end 2 pt short of it: the circle splits in two. let circle = Shape::new(shapes::ellipse(Rect::new(50.0, 0.0, 150.0, 100.0)), FillRule::NonZero, 0); let s = [circle, line((52.0, 50.0), (148.0, 50.0), 1)]; assert_eq!(live_paint(&s).0.len(), 1); let (faces, _, gaps) = live_paint_with_gaps(&s, 3.0); assert_eq!(gaps.len(), 2, "{gaps:?}"); for g in &gaps { assert!((g[0].distance(g[1]) - 2.0).abs() < 1e-6, "{g:?}"); } assert_eq!(faces.len(), 2, "{faces:?}"); assert_eq!(at(&faces, 100.0, 25.0).len(), 1); assert_eq!(at(&faces, 100.0, 75.0).len(), 1); // Four lines round a square whose corners miss by 1 pt: the gaps are listed once each. let s = [ line((0.0, 0.0), (99.0, 0.0), 0), line((100.0, 1.0), (100.0, 99.0), 1), line((99.0, 100.0), (1.0, 100.0), 2), line((0.0, 99.0), (0.0, 1.0), 3), ]; let (faces, _, gaps) = live_paint_with_gaps(&s, 3.0); assert_eq!(gaps.len(), 4, "{gaps:?}"); assert_eq!(faces.len(), 1, "{faces:?}"); // One curve that comes back round to 2 pt from its start. let mut bp = BezPath::new(); bp.move_to((0.0, 0.0)); bp.curve_to((100.0, 0.0), (100.0, 100.0), (0.0, 2.0)); let s = [Shape::new(PathData::from_bezpath(&bp), FillRule::NonZero, 0)]; let (faces, _, gaps) = live_paint_with_gaps(&s, 3.0); assert_eq!(gaps.len(), 1, "{gaps:?}"); assert!((gaps[0][0].distance(gaps[0][1]) - 2.0).abs() < 1e-9, "{gaps:?}"); assert_eq!(faces.len(), 1, "{faces:?}"); // A line far longer than the gap, and many lines far from each other. let mut s = vec![line((0.0, 0.0), (100000.0, 0.0), 0), line((50000.0, 2.0), (50000.0, 100.0), 1)]; s.extend((0..2000).map(|k| { let (x, y) = (f64::from(k % 50) * 40.0, 1000.0 + f64::from(k / 50) * 40.0); line((x, y), (x + 10.0, y), 2) })); let gaps = find_gaps(&s, 3.0); assert_eq!(gaps.len(), 1, "{gaps:?}"); assert!(gaps[0][0].distance(Point::new(50000.0, 2.0)) < 1e-9 && gaps[0][1].distance(Point::new(50000.0, 0.0)) < 1e-9, "{gaps:?}"); // Paths far from the origin: one gap between two loose ends, listed once. let s = [line((1e20, 0.0), (1e20, 100.0), 0), line((1e20, 102.0), (1e20, 200.0), 1)]; assert_eq!(find_gaps(&s, 3.0).len(), 1, "{:?}", find_gaps(&s, 3.0)); } #[test] fn overshoots_and_joined_ends_are_no_gaps() { // Ends that cross a line just before they stop, square to it and at 30 degrees. let t = 30f64.to_radians(); let s = [ line((-10.0, 0.0), (110.0, 0.0), 0), line((20.0, 100.0), (20.0, -1.0), 1), line((80.0 + 100.0 * t.cos(), 100.0 * t.sin()), (80.0 - t.cos(), -t.sin()), 2), ]; assert!(find_gaps(&s, 3.0).is_empty(), "{:?}", find_gaps(&s, 3.0)); // An end on another path, or on its own, is joined there. let s = [line((0.0, 0.0), (100.0, 0.0), 0), line((50.0, 0.0), (50.0, 100.0), 1)]; assert!(find_gaps(&s, 3.0).is_empty(), "{:?}", find_gaps(&s, 3.0)); // Another path 2 pt away does not make a gap from the joined end. let s = [line((0.0, 0.0), (100.0, 0.0), 0), line((50.0, 0.0), (50.0, 100.0), 1), line((40.0, -2.0), (60.0, -2.0), 2)]; let gaps = find_gaps(&s, 3.0); assert!(!gaps.is_empty() && gaps.iter().all(|g| g[0].distance(Point::new(50.0, 0.0)) > 1e-9), "{gaps:?}"); let six = [open(&[(0.0, 0.0), (0.0, 100.0), (50.0, 100.0), (50.0, 50.0), (0.0, 50.0)], 0)]; assert!(find_gaps(&six, 3.0).is_empty(), "{:?}", find_gaps(&six, 3.0)); // Sizes that are not a length find nothing. let square = [open(&[(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0), (0.0, 2.0)], 0)]; for size in [0.0, -3.0, f64::NAN, f64::INFINITY] { assert!(find_gaps(&square, size).is_empty(), "{size}"); } } #[test] fn a_grid_of_lines_makes_its_cells() { let mut s = vec![]; for k in 0..4 { let v = f64::from(k) * 10.0; s.push(line((v, -5.0), (v, 35.0), 0)); s.push(line((-5.0, v), (35.0, v), 1)); } let (faces, _) = live_paint(&s); assert_eq!(faces.len(), 9); assert!(faces.iter().all(|f| (area(f) - 100.0).abs() < 1e-6)); assert_eq!(at(&faces, 15.0, 15.0).len(), 1); } #[test] fn a_line_across_a_shape_splits_it() { let s = [rect(0.0, 0.0, 100.0, 50.0, 7), line((40.0, -20.0), (60.0, 70.0), 8)]; let (faces, edges) = live_paint(&s); assert_eq!(faces.len(), 2); assert!(faces.iter().all(|f| f.sources == [0]), "both halves keep the rectangle's fill"); assert!((faces.iter().map(area).sum::() - 5000.0).abs() < 1e-6); assert_eq!(at(&faces, 10.0, 25.0).len(), 1); assert_eq!(at(&faces, 90.0, 25.0).len(), 1); // The rectangle's outline in two pieces, the line in three. assert_eq!(edges.iter().filter(|e| e.key == 7).count(), 2); assert_eq!(edges.iter().filter(|e| e.key == 8).count(), 3); } #[test] fn overlapping_shapes_keep_their_coverage() { let s = [rect(0.0, 0.0, 100.0, 100.0, 0), rect(50.0, 0.0, 150.0, 100.0, 1)]; let (faces, _) = live_paint(&s); assert_eq!(faces.len(), 3); assert_eq!(at(&faces, 25.0, 50.0)[0].sources, [0]); assert_eq!(at(&faces, 75.0, 50.0)[0].sources, [0, 1]); assert_eq!(at(&faces, 125.0, 50.0)[0].sources, [1]); // Same faces as the Pathfinder regions. let regions = crate::regions(&s); assert_eq!(regions.len(), 3); for f in &faces { assert!(regions.iter().any(|r| r.sources == f.sources && (area(r).abs() - area(f)).abs() < 1e-6)); } } #[test] fn a_closed_loop_of_lines_inside_a_shape_is_a_hole_and_a_face() { let mut s = vec![rect(0.0, 0.0, 100.0, 100.0, 0)]; let sq = [(40.0, 40.0), (60.0, 40.0), (60.0, 60.0), (40.0, 60.0), (40.0, 40.0)]; for w in sq.windows(2) { s.push(line(w[0], w[1], 1)); } // A loose line inside: no face of its own, no hole. s.push(line((10.0, 10.0), (20.0, 20.0), 2)); let (faces, _) = live_paint(&s); assert_eq!(faces.len(), 2, "{faces:?}"); let outer = at(&faces, 5.0, 5.0); assert_eq!(outer.len(), 1); assert_eq!(outer[0].path.subpaths.len(), 2, "the inner square is a hole in it"); assert!((area(outer[0]) - 9600.0).abs() < 1e-6); let inner = at(&faces, 50.0, 50.0); assert_eq!(inner.len(), 1, "the hole doesn't count as the outer face"); assert_eq!(inner[0].sources, [0], "the rectangle fills it"); assert!((area(inner[0]) - 400.0).abs() < 1e-6); } #[test] fn an_area_enclosed_by_shapes_is_an_unfilled_face() { // A frame of four bars around a 50 × 50 gap. let s = [rect(0.0, 0.0, 70.0, 10.0, 0), rect(0.0, 60.0, 70.0, 70.0, 0), rect(0.0, 0.0, 10.0, 70.0, 0), rect(60.0, 0.0, 70.0, 70.0, 0)]; let (faces, _) = live_paint(&s); let gap = at(&faces, 35.0, 35.0); assert_eq!(gap.len(), 1); assert!(gap[0].sources.is_empty()); assert!((area(gap[0]) - 2500.0).abs() < 1e-6); } #[test] fn curves_and_a_circle_crossed_by_a_line() { let circle = Shape::new(shapes::ellipse(Rect::new(0.0, 0.0, 100.0, 100.0)), FillRule::NonZero, 0); let (faces, edges) = live_paint(&[circle, line((-10.0, 50.0), (110.0, 50.0), 1)]); assert_eq!(faces.len(), 2); for f in &faces { assert!((area(f) - std::f64::consts::PI * 2500.0 / 2.0).abs() < 2.0, "{}", area(f)); assert!(f.path.subpaths[0].anchors.len() <= 4, "no extra anchors from the sweep's splits"); } assert_eq!(edges.iter().filter(|e| e.key == 0).count(), 2, "two arcs"); } #[test] fn a_line_ending_on_an_edge_splits_the_shape() { let s = [line((90.0, 20.0), (70.0, 10.0), 0), rect(80.0, 10.0, 90.0, 50.0, 1)]; let (faces, _) = live_paint(&s); assert_eq!(faces.len(), 2); assert!((area(&faces[0]) + area(&faces[1]) - 400.0).abs() < 1e-6); for f in &faces { let p = crate::interior_point(&f.path).unwrap(); assert_eq!(at(&faces, p.x, p.y).len(), 1, "{p:?}"); assert_eq!(f.sources, [1]); } } #[test] fn nothing_or_garbage_gives_an_empty_map() { assert_eq!(live_paint(&[]), (vec![], vec![])); assert!(live_paint(&[line((0.0, 0.0), (f64::NAN, 1.0), 0)]).0.is_empty()); let (faces, edges) = live_paint(&[line((0.0, 0.0), (10.0, 10.0), 0)]); assert!(faces.is_empty()); assert_eq!(edges.len(), 1); } /// A strip merged across an axonometric grid of lines (#893, the reporter's file): faces meeting /// at a vertex share its exact coordinates, so the merge is one outline with no edge left inside. #[test] fn faces_share_their_corners_exactly_so_merging_them_leaves_no_seam() { const LINES: &[[f64; 4]] = &[ [416.57797027866155, 268.45362144397683, 416.5779702786616, 811.5463785560231], [243.12292300414873, 511.1134238473054, 713.4550472745127, 782.6598024033286], [836.8770769958512, 511.1134238473054, 366.5449527254871, 782.6598024033286], [457.718646852441, 268.45362144397683, 457.718646852441, 811.5463785560231], [263.6932612910385, 475.4845528055331, 734.0253855614023, 747.0309313615563], [816.3067387089613, 475.4845528055331, 345.9746144385975, 747.0309313615563], [498.85932342622044, 268.45362144397683, 498.85932342622044, 811.5463785560231], [284.26359957792823, 439.8556817637607, 754.5957238482922, 711.4020603197839], [795.7364004220717, 439.8556817637607, 325.40427615170773, 711.4020603197839], [539.9999999999998, 268.45362144397683, 539.9999999999998, 811.5463785560231], [304.83393786481787, 404.22681072198844, 775.1660621351818, 675.7731892780117], [775.1660621351821, 404.22681072198844, 304.833937864818, 675.7731892780117], [581.1406765737793, 268.45362144397683, 581.1406765737793, 811.5463785560231], [325.4042761517076, 368.59793968021614, 795.7364004220715, 640.1443182362394], [754.5957238482922, 368.59793968021614, 284.26359957792835, 640.1443182362394], [622.2813531475588, 268.45362144397683, 622.2813531475588, 811.5463785560231], [345.97461443859737, 332.9690686384438, 816.3067387089613, 604.515447194467], [734.0253855614026, 332.9690686384438, 263.6932612910385, 604.515447194467], [663.422029721338, 268.45362144397683, 663.422029721338, 811.5463785560231], [366.54495272548706, 297.3401975966715, 836.877076995851, 568.8865761526947], [713.4550472745128, 297.3401975966715, 243.12292300414884, 568.8865761526947], ]; let s: Vec = LINES.iter().enumerate().map(|(i, &[x0, y0, x1, y1])| line((x0, y0), (x1, y1), i as u64)).collect(); let (faces, _) = live_paint(&s); let corners: Vec = faces.iter().flat_map(|f| f.path.subpaths.iter().flat_map(|sp| sp.anchors.iter().map(|a| a.p))).collect(); for p in &corners { for q in &corners { assert!(p == q || p.distance(*q) > 1e-6, "one corner, two positions: {p:?} {q:?}"); } } // Down the strip between the lines x ≈ 498.9 and x = 540: 11 triangles, one outline. let mut strip: Vec<&Region> = vec![]; for k in 0..=240 { if let Some(f) = faces.iter().find(|f| f.contains(Point::new(519.43, 420.0 + f64::from(k)))) && !strip.iter().any(|g| std::ptr::eq(*g, f)) { strip.push(f); } } assert_eq!(strip.len(), 11); // As the Shape Builder merges them: in the map, so no sweep can round the corners apart. let merged = crate::FaceMerger::new(&s).merge(&strip); assert_eq!(merged.subpaths.len(), 1, "{merged:?}"); let want: f64 = strip.iter().map(|f| area(f)).sum(); assert!((crate::area(&merged, FillRule::NonZero) - want).abs() < 1e-6 * want); } /// Merging faces of a grid of lines in the map: a ring keeps its hole, cells that only touch at /// a corner stay two outlines, a line's loose end inside the merge leaves no spur. #[test] fn faces_merge_in_the_map_with_holes_corners_and_loose_ends() { let mut s = vec![]; for k in 0..4 { let v = f64::from(k) * 10.0; s.push(line((v, -5.0), (v, 35.0), 0)); s.push(line((-5.0, v), (35.0, v), 1)); } // A loose end poking into the middle cell. s.push(line((15.0, 10.0), (15.0, 15.0), 2)); let (faces, _) = live_paint(&s); let cell = |x: f64, y: f64| at(&faces, x, y)[0]; let merger = crate::FaceMerger::new(&s); let m = map(&s, true).unwrap(); let check = |picked: &[&Region], subpaths: usize, area_want: f64| { // Merged in the map itself, not by the fallback union. let inside: Vec = picked.iter().map(|f| interior_point(&f.path).unwrap()).collect(); assert_eq!(m.merge(&inside, &crate::pathfinder::cover(picked, s.len()), None).as_ref(), Some(&merger.merge(picked))); let m = merger.merge(picked); assert_eq!(m.subpaths.len(), subpaths, "{m:?}"); assert!((crate::area(&m, FillRule::NonZero) - area_want).abs() < 1e-6, "{}", crate::area(&m, FillRule::NonZero)); m }; let all: Vec<&Region> = faces.iter().collect(); let whole = check(&all, 1, 900.0); assert_eq!(whole.subpaths[0].anchors.len(), 4, "one square, no anchors left from the inner lines or the loose end"); let ring: Vec<&Region> = [(5., 5.), (15., 5.), (25., 5.), (5., 15.), (25., 15.), (5., 25.), (15., 25.), (25., 25.)].iter().map(|&(x, y)| cell(x, y)).collect(); check(&ring, 2, 800.0); check(&[cell(5.0, 5.0), cell(15.0, 15.0)], 2, 200.0); } /// Merging in the map takes in a sliver next to the merged faces that only their shapes fill, /// such as the overlap of two squares that overlap by 0.001, and leaves a thin cutout that /// nothing fills as it is. #[test] fn faces_merge_in_the_map_with_the_slivers_their_shapes_fill() { let far = line((400.0, 400.0), (450.0, 450.0), 2); let s = [rect(100.0, 100.0, 200.0, 200.0, 0), rect(199.999, 100.0, 299.999, 200.0, 1), far.clone()]; let regions = shape_builder(&s, false).regions; assert_eq!(regions.len(), 2, "the overlap is a sliver: {regions:?}"); let both: Vec<&Region> = regions.iter().collect(); let merged = crate::FaceMerger::new(&s).merge(&both); assert_eq!(merged.subpaths.len(), 1, "{merged:?}"); assert!((crate::area(&merged, FillRule::NonZero) - 19999.9).abs() < 1e-6, "{}", crate::area(&merged, FillRule::NonZero)); // Live Paint's map does not test which inputs cover the sliver. let covered = |cover: bool| map(&s, cover).unwrap().slivers.into_iter().map(|(_, sources)| sources).collect::>(); assert_eq!(covered(true), [vec![0, 1]]); assert_eq!(covered(false), [Vec::::new()]); // A square with a cutout 0.005 wide. let mut frame = shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0)); frame.subpaths.extend(shapes::rectangle(Rect::new(50.0, 10.0, 50.005, 90.0)).subpaths); let s = [Shape::new(frame, FillRule::EvenOdd, 0), far]; let regions = shape_builder(&s, false).regions; assert_eq!(regions.len(), 1, "{regions:?}"); let merged = crate::FaceMerger::new(&s).merge(&[®ions[0]]); assert_eq!(merged.subpaths.len(), 2, "the cutout stays: {merged:?}"); assert!((crate::area(&merged, FillRule::EvenOdd) - 9999.6).abs() < 1e-6, "{}", crate::area(&merged, FillRule::EvenOdd)); } #[test] fn shape_builder_lines_cut_regions() { // A line across a rectangle: two regions, the line in three pieces keyed by its index. let s = [rect(0.0, 0.0, 100.0, 50.0, 7), line((40.0, -20.0), (60.0, 70.0), 8)]; let BuilderArrangement { regions, lines, .. } = shape_builder(&s, false); assert_eq!(regions.len(), 2); assert!(regions.iter().all(|r| r.sources == [0])); assert_eq!(lines.len(), 3); assert!(lines.iter().all(|l| l.key == 1 && !l.path.is_closed())); // Lines alone enclose a region nothing fills. let s = [line((0.0, 0.0), (100.0, 100.0), 0), line((100.0, 0.0), (0.0, 100.0), 0), line((-10.0, 80.0), (110.0, 80.0), 0)]; let BuilderArrangement { regions, lines, .. } = shape_builder(&s, false); assert_eq!(regions.len(), 1); assert!(regions[0].sources.is_empty() && regions[0].top().is_none()); assert_eq!(lines.len(), 9); } #[test] fn shape_builder_merges_adjacent_gap_closed_faces_without_keeping_bridges() { let outline = Shape::new( PathData::single(SubPath::polyline( &[Point::new(0.0, 4.0), Point::new(0.0, 100.0), Point::new(100.0, 100.0), Point::new(100.0, 0.0), Point::new(0.0, 0.0)], false, )), FillRule::NonZero, 0, ); let shapes = [outline, line((50.0, 0.0), (50.0, 100.0), 1)]; let arr = shape_builder_with_gap(&shapes, true, 6.0); assert_eq!(arr.regions.len(), 2); let faces: Vec<_> = arr.regions.iter().collect(); let merged = crate::FaceMerger::with_gap(&shapes, 6.0).merge(&faces); assert!((crate::area(&merged, FillRule::NonZero).abs() - 10000.0).abs() < 1e-6); assert_eq!(merged.subpaths.len(), 1, "the shared edge disappears"); assert!(arr.lines.iter().chain(&arr.edges).all(|p| p.key < 2), "bridges are not art"); // A request over 72 pt cannot close a gap of 80 pt. let large = [Shape::new( PathData::single(SubPath::polyline( &[Point::new(0.0, 80.0), Point::new(0.0, 200.0), Point::new(200.0, 200.0), Point::new(200.0, 0.0), Point::new(0.0, 0.0)], false, )), FillRule::NonZero, 0, )]; assert!(shape_builder_with_gap(&large, false, 1000.0).regions.is_empty()); } #[test] fn shape_builder_gap_detection_closes_small_gaps() { // Three sides of a 100 x 100 square, the fourth open by 4: no region without Gap Detection. let open = |gap: f64| { let pts = [(0.0, gap), (0.0, 100.0), (100.0, 100.0), (100.0, 0.0), (0.0, 0.0)]; let pts: Vec = pts.iter().map(|&(x, y)| Point::new(x, y)).collect(); vec![Shape::new(PathData::single(SubPath::polyline(&pts, false)), FillRule::NonZero, 0)] }; let s = open(4.0); assert!(shape_builder(&s, false).regions.is_empty()); assert!(shape_builder_with_gap(&s, false, 3.0).regions.is_empty(), "a gap longer than the length stays open"); let arr = shape_builder_with_gap(&s, true, 6.0); assert_eq!(arr.regions.len(), 1); assert!((area(&arr.regions[0]) - 10000.0).abs() < 1e-6); // The bridge is no art: the path comes back as it was, in its own pieces only. assert!(arr.lines.iter().chain(&arr.edges).all(|p| p.key == 0)); let len: f64 = arr.lines.iter().map(|p| p.path.to_bezpath().perimeter(1e-6)).sum(); assert!((len - 396.0).abs() < 1e-6, "{len}"); // Two paths whose ends miss each other: a U and a lid 3 above it. let u = Shape::new( PathData::single(SubPath::polyline( &[Point::new(0.0, 0.0), Point::new(0.0, 100.0), Point::new(100.0, 100.0), Point::new(100.0, 0.0)], false, )), FillRule::NonZero, 0, ); let s = [u, line((0.0, -3.0), (100.0, -3.0), 1)]; assert!(shape_builder_with_gap(&s, false, 2.0).regions.is_empty()); let arr = shape_builder_with_gap(&s, false, 3.5); assert_eq!(arr.regions.len(), 1); assert!((area(&arr.regions[0]) - 10300.0).abs() < 1e-6); // An end close to the side of another path bridges to the nearest point on it. let s = [rect(0.0, 0.0, 100.0, 100.0, 0), line((50.0, 140.0), (50.0, 104.0), 1)]; assert_eq!(shape_builder_with_gap(&s, false, 5.0).regions.len(), 1, "nothing is enclosed, nothing breaks"); // Bad gaps are no gap. let s = open(4.0); for gap in [f64::NAN, f64::INFINITY, -3.0] { assert!(shape_builder_with_gap(&s, false, gap).regions.is_empty()); } } #[test] fn shape_builder_closed_only_or_degenerate_is_the_filled_arrangement() { let s = [rect(0.0, 0.0, 100.0, 100.0, 0), rect(50.0, 0.0, 150.0, 100.0, 1)]; assert_eq!(shape_builder(&s, false), BuilderArrangement { regions: crate::regions(&s), ..Default::default() }); // A broken line can't be swept and fills nothing: the filled areas still give their regions. let s = [rect(0.0, 0.0, 100.0, 100.0, 0), line((0.0, 0.0), (f64::NAN, 1.0), 1)]; assert_eq!(shape_builder(&s, false), BuilderArrangement { regions: crate::regions(&s[..1]), ..Default::default() }); // Zero-length and doubled-back lines don't break it either. let s = [ rect(0.0, 0.0, 100.0, 100.0, 0), line((50.0, 50.0), (50.0, 50.0), 1), line((0.0, 0.0), (100.0, 0.0), 2), line((50.0, 0.0), (50.0, 100.0), 3), ]; let regions = shape_builder(&s, false).regions; assert_eq!(regions.len(), 2); assert!((regions.iter().map(area).sum::() - 10000.0).abs() < 1e-6); } fn circle(cx: f64, cy: f64, r: f64, key: u64) -> Shape { Shape::new(shapes::ellipse(Rect::new(cx - r, cy - r, cx + r, cy + r)), FillRule::NonZero, key) } #[test] fn shape_builder_offers_the_gaps_closed_shapes_enclose() { // Four circles in a 2×2 grid, each touching its neighbours (#1691): the four circles and // the diamond between them, which nothing fills. let s = [circle(50.0, 50.0, 50.0, 0), circle(150.0, 50.0, 50.0, 1), circle(50.0, 150.0, 50.0, 2), circle(150.0, 150.0, 50.0, 3)]; let regions = shape_builder(&s, false).regions; assert_eq!(regions.len(), 5, "{regions:?}"); let gaps: Vec<&Region> = regions.iter().filter(|r| r.sources.is_empty()).collect(); assert_eq!(gaps.len(), 1); assert!(gaps[0].contains(Point::new(100.0, 100.0)) && !gaps[0].contains(Point::new(50.0, 50.0))); let diamond = 10000.0 - std::f64::consts::PI * 2500.0; assert!((area(gaps[0]).abs() - diamond).abs() < 0.01 * diamond, "{}", area(gaps[0])); assert_eq!(shape_builder(&s, true).regions, regions, "the outline edges don't change them"); // A ring of four bars that overlap at the corners: the eight pieces of the bars and the // square they enclose. let s = [rect(0.0, 0.0, 300.0, 100.0, 0), rect(200.0, 0.0, 300.0, 300.0, 1), rect(0.0, 200.0, 300.0, 300.0, 2), rect(0.0, 0.0, 100.0, 300.0, 3)]; let regions = shape_builder(&s, false).regions; assert_eq!(regions.len(), 9, "{regions:?}"); let gaps: Vec<&Region> = regions.iter().filter(|r| r.sources.is_empty()).collect(); assert_eq!(gaps.len(), 1); assert!((area(gaps[0]).abs() - 10000.0).abs() < 1e-6 && gaps[0].contains(Point::new(150.0, 150.0))); // The gap merges with a bar in the planar map: the bar's piece and the square, no seam. let merger = crate::FaceMerger::new(&s); let bar = regions.iter().find(|r| r.sources == [0] && r.contains(Point::new(150.0, 50.0))).unwrap(); let merged = merger.merge(&[bar, gaps[0]]); assert_eq!(merged.subpaths.len(), 1, "{merged:?}"); assert!((crate::area(&merged, FillRule::NonZero) - 20000.0).abs() < 1e-6); // Shapes apart or overlapping without a hole enclose no gap. let s = [circle(50.0, 50.0, 50.0, 0), circle(250.0, 50.0, 50.0, 1), rect(80.0, 0.0, 220.0, 60.0, 2)]; assert!(shape_builder(&s, false).regions.iter().all(|r| !r.sources.is_empty())); } #[test] fn shape_builder_stays_quick_up_to_its_cap() { // A wavy ring of nearly the most segments the map takes, crossed by a few lines. let n = SHAPE_BUILDER_MAX_SEGMENTS - 8; let ring: Vec = (0..n) .map(|k| { let t = k as f64 / n as f64 * std::f64::consts::TAU; let r = 100.0 + 5.0 * (t * 200.0).sin(); Point::new(r * t.cos(), r * t.sin()) }) .collect(); let mut s = vec![Shape::new(PathData::single(SubPath::polyline(&ring, true)), FillRule::NonZero, 0)]; // Closed paths alone are swept for the gaps they enclose. let started = std::time::Instant::now(); assert_eq!(shape_builder(&s, false).regions.len(), 1, "the ring encloses no gap"); assert!(started.elapsed().as_secs_f64() < 5.0, "{:?}", started.elapsed()); for k in 0..4 { let y = -60.0 + 40.0 * f64::from(k); s.push(line((-150.0, y), (150.0, y + 7.0), 1)); } let started = std::time::Instant::now(); let BuilderArrangement { regions, lines, .. } = shape_builder(&s, false); assert!(regions.len() >= 5, "the ring cut in five bands at least (the waves make pockets)"); assert!(lines.len() >= 12, "{}", lines.len()); assert!(started.elapsed().as_secs_f64() < 5.0, "{:?}", started.elapsed()); // Past the cap, only the filled areas count. s.push(line((0.0, 0.0), (1.0, 1.0), 2)); s[0].path.subpaths[0].anchors.extend((0..16).map(|k| vectorcraft_geom::Anchor::corner(Point::new(100.0, f64::from(k))))); assert!(shape_builder(&s, false).lines.is_empty()); } #[test] fn a_traced_outline_of_many_segments_is_cut_by_lines() { // #937: art traced to thousands of segments was past the old cap (4096), so lines stopped // cutting it and the Shape Builder offered the whole shapes. A blob of 20 000 segments // crossed by two long lines (far wider than any of its segments: the index of the // outlines keeps those apart) splits into four regions, each line in four pieces. let n = 20_000; let blob: Vec = (0..n) .map(|k| { let t = k as f64 / n as f64 * std::f64::consts::TAU; let r = 100.0 + 5.0 * (t * 37.0).sin(); Point::new(r * t.cos(), r * t.sin()) }) .collect(); let s = [ Shape::new(PathData::single(SubPath::polyline(&blob, true)), FillRule::NonZero, 0), line((-150.0, 0.0), (150.0, 3.0), 1), line((0.0, -150.0), (3.0, 150.0), 2), ]; let BuilderArrangement { regions, lines, .. } = shape_builder(&s, false); assert_eq!(regions.len(), 4, "{}", regions.len()); assert!(regions.iter().all(|r| r.sources == [0])); let whole = crate::area(&s[0].path, FillRule::NonZero); assert!((regions.iter().map(area).sum::() - whole).abs() < 1e-6 * whole); assert_eq!(lines.iter().filter(|l| l.key == 1).count(), 4, "cut twice by the blob and once by the other line"); assert_eq!(lines.iter().filter(|l| l.key == 2).count(), 4); } #[test] fn a_hatch_of_parallel_lines_is_cut_line_by_line() { // Hundreds of long horizontal lines over a square, all alike in x (the weak axis of an index // by x alone), and one vertical line: every strip is cut in two. let n: u32 = 300; let mut s = vec![rect(0.0, 0.0, 100.0, 100.0, 0)]; for k in 1..n { let y = 100.0 * f64::from(k) / f64::from(n); s.push(line((-10.0, y), (110.0, y), u64::from(k))); } s.push(line((50.0, -10.0), (50.0, 110.0), u64::from(n))); let BuilderArrangement { regions, lines, .. } = shape_builder(&s, false); assert_eq!(regions.len(), 2 * n as usize, "{}", regions.len()); assert!(regions.iter().all(|r| r.sources == [0])); assert!((regions.iter().map(area).sum::() - 10000.0).abs() < 1e-6); // Each horizontal line: a loose end outside, two pieces inside, a loose end outside. assert_eq!(lines.iter().filter(|l| l.key == 1).count(), 4); // The vertical line: cut by the square's sides and every hatch line. assert_eq!(lines.iter().filter(|l| l.key == u64::from(n)).count(), n as usize + 2); } #[test] fn rect_index_finds_every_rectangle_holding_a_point() { // Narrow in x, narrow in y only, and wide both ways: each lookup matches a scan of them all. let mut items = vec![(kurbo::Rect::new(0.0, 0.0, 100.0, 100.0), 0usize)]; for k in 1..50 { let a = f64::from(k as u32) * 2.0; items.push((kurbo::Rect::new(a, 0.0, a + 1.0, 100.0), k)); items.push((kurbo::Rect::new(0.0, a, 100.0, a + 1.0), 100 + k)); } items.push((kurbo::Rect::new(f64::NAN, 0.0, 1.0, 1.0), 999)); let index = RectIndex::new(items.clone()); for p in [Point::new(4.5, 6.5), Point::new(50.0, 50.0), Point::new(-1.0, 3.0), Point::new(99.5, 0.5)] { let mut got: Vec = index.at(p, 0.1).copied().collect(); got.sort_unstable(); let mut want: Vec = items.iter().filter(|(r, _)| r.inflate(0.1, 0.1).contains(p)).map(|(_, k)| *k).collect(); want.sort_unstable(); assert_eq!(got, want, "{p:?}"); } assert_eq!(RectIndex::::new(Vec::new()).at(Point::ZERO, 1.0).count(), 0); } #[test] fn rect_index_reads_a_diagonal_hatch_only_near_the_point() { // 2,000 parallel diagonals a tenth of a point apart: the bounds of every line left of a // point hold it, so an index of the bounds alone handed half the hatch to each lookup // (quadratic in the hatch, #937). Filed by pieces, a lookup gets the lines of its // neighbourhood (a few cells' worth), among them every line through the point. let n = 2000; let items: Vec<_> = (0..n) .map(|k| { let d = f64::from(k) * 0.1; let s = PathSeg::Line(kurbo::Line::new((d, 0.0), (d + 200.0, 200.0))); (s.bounding_box(), (s, k)) }) .collect(); let index = RectIndex::with_footprint(items.clone(), |r, (s, _), cell| segment_footprint(r, s, cell)); for p in [Point::new(100.0, 100.0), Point::new(130.25, 60.0), Point::new(250.0, 150.0), Point::new(399.9, 200.0)] { let got: Vec = index.at(p, 0.1).map(|(_, k)| *k).collect(); assert!(got.len() <= n as usize / 4, "{p:?}: {} candidates", got.len()); let through: Vec = items.iter().filter(|(_, (s, _))| kurbo::ParamCurveNearest::nearest(s, p, 1e-9).distance_sq <= 0.01).map(|(_, (_, k))| *k).collect(); assert!(!through.is_empty() && through.iter().all(|k| got.contains(k)), "{p:?}: {through:?} vs {got:?}"); } // Faces a hatch cuts a square into: thin stripes across the whole grid, each filed under // its own column of cells, so a lookup reads a few of them, not the hatch. let stripes: Vec<_> = (0..n).map(|k| (kurbo::Rect::new(f64::from(k) * 0.1, 0.0, f64::from(k) * 0.1 + 0.1, 200.0), k)).collect(); let index = RectIndex::new(stripes); let got: Vec = index.at(Point::new(100.05, 50.0), 0.0).copied().collect(); assert_eq!(got, [1000]); assert!(index.rest.is_empty()); // A footprint still finds a point on the item but off the grid's cells' edges, and a // degenerate grid (collinear items) is divided along its length. let line = PathSeg::Line(kurbo::Line::new((0.0, 0.0), (1000.0, 0.0))); let items: Vec<_> = (0..10).map(|k| (line.bounding_box(), (line, k))).collect(); let index = RectIndex::with_footprint(items, |r, (s, _), cell| segment_footprint(r, s, cell)); assert_eq!(index.at(Point::new(333.3, 0.0), 0.1).count(), 10); assert_eq!(index.at(Point::new(1000.0, 0.0), 0.1).count(), 10); assert_eq!(index.at(Point::new(-5.0, 0.0), 0.1).count(), 0); } /// The grid's memory stays linear in the item count: pieces of one segment falling in the same /// cell file it once, and coincident segments (each along every cell of a one-column grid) go /// to the scanned list once the grid holds its budget of references, and are still all found. #[test] fn rect_index_holds_a_bounded_number_of_references() { // Ten collinear horizontal lines: a grid of 10 cells a hundred points wide and a hair tall, // so each line's 4096 pieces fall about 400 per cell; filed once per cell. let line = PathSeg::Line(kurbo::Line::new((0.0, 0.0), (1000.0, 0.0))); let items: Vec<_> = (0..10).map(|k| (line.bounding_box(), (line, k))).collect(); let index = RectIndex::with_footprint(items, |r, (s, _), cell| segment_footprint(r, s, cell)); assert!(index.rest.is_empty(), "every line is in the grid, {} scanned", index.rest.len()); assert!(index.cells.iter().all(|c| c.len() <= 10), "{:?}", index.cells.iter().map(Vec::len).collect::>()); assert!(index.cells.iter().map(Vec::len).sum::() >= 10, "the lines are filed"); assert_eq!(index.at(Point::new(500.0, 0.0), 0.1).count(), 10); // 4,096 coincident vertical segments: one column, a row per segment, every segment along // every row would be 16 million references; the budget keeps it to a few hundred per item. let n = 4096u32; let seg = PathSeg::Line(kurbo::Line::new((50.0, 0.0), (50.0, 200.0))); let items: Vec<_> = (0..n).map(|k| (seg.bounding_box(), (seg, k))).collect(); let index = RectIndex::with_footprint(items, |r, (s, _), cell| segment_footprint(r, s, cell)); let references: usize = index.cells.iter().map(Vec::len).sum(); assert!(references <= reference_budget(n as usize, index.cells.len()), "{references} references"); assert!(references < 2_000_000, "{references} references"); assert!(!index.rest.is_empty() && index.rest.len() < n as usize, "{} scanned", index.rest.len()); let mut got: Vec = index.at(Point::new(50.0, 100.0), 0.1).map(|(_, k)| *k).collect(); got.sort_unstable(); assert_eq!(got.len(), n as usize); assert_eq!(got, (0..n).collect::>()); assert_eq!(index.at(Point::new(60.0, 100.0), 0.1).count(), 0); } fn poly(pts: &[(f64, f64)], closed: bool) -> SubPath { SubPath::polyline(&pts.iter().map(|&(x, y)| Point::new(x, y)).collect::>(), closed) } fn ends(sp: &SubPath) -> (Point, Point) { (sp.anchors[0].p, sp.anchors[sp.anchors.len() - 1].p) } /// Each piece cuts out of the input it is keyed by, losing just its own length (what erasing /// it does), and none is a speck. fn assert_pieces_cut_out(shapes: &[Shape], arr: &BuilderArrangement) { for e in arr.edges.iter().chain(&arr.lines) { let len = e.path.length(); assert!(len > 1e-3, "a speck of a piece, keyed {}: {:?}", e.key, e.path); let s = &shapes[e.key as usize]; let cut = cut_out(&s.path, &e.path.subpaths[0], false, 0.05).unwrap_or_else(|| panic!("{:?} not on input {}", e.path, e.key)); let lost = s.path.length() - cut.length(); assert!((lost - len).abs() <= 1e-6 * s.path.length(), "input {} lost {lost} for a piece {len} long", e.key); } } #[test] fn a_line_crossed_where_its_two_passes_part_is_cut_once() { // The shrunk case of `pieces_cut_out_of_their_inputs`: the sweep cut the first line's way // there and its way back by the third line a rounding error (1.2e-6) apart, joined by a // stub. The line came back as a speck keyed by the third line and a piece running from // the crossing out to (10, 70) and back to the other cut, which erasing took twice over. let s = [line((10.0, 70.0), (80.0, 20.0), 0), line((10.0, 40.0), (70.0, 90.0), 0), line((20.0, 80.0), (20.0, 50.0), 0)]; let arr = shape_builder(&s, true); assert_pieces_cut_out(&s, &arr); // The first line cut by both others, the other two once each. let count = |k: u64| arr.lines.iter().filter(|l| l.key == k).count(); assert_eq!((count(0), count(1), count(2)), (3, 2, 2), "{:?}", arr.lines); let at = Point::new(20.0, 70.0 - 50.0 / 7.0); for k in [0, 2] { assert!(arr.lines.iter().filter(|l| l.key == k).all(|l| { let (a, b) = ends(&l.path.subpaths[0]); a.distance(b) > 1.0 && l.path.subpaths[0].anchors.len() == 2 })); assert_eq!( arr.lines .iter() .filter(|l| l.key == k && [ends(&l.path.subpaths[0]).0, ends(&l.path.subpaths[0]).1].iter().any(|p| p.distance(at) < 1e-9)) .count(), 2 ); } } #[test] fn a_run_of_tiny_level_segments_is_not_contracted_as_stubs() { // 2000 segments of 1e-4 (0.2 pt in all), nearly level: each one alone is as short as a // stub the sweep leaves between two cuts, but chained they are real geometry, and the // stub contraction must not pull them into one vertex. let pts: Vec = (0..=2000).map(|k| Point::new(f64::from(k) * 1e-4, f64::from(k) * 1e-8)).collect(); let run = Shape::new(PathData::single(SubPath::polyline(&pts, false)), FillRule::NonZero, 0); let whole = run.path.length(); let s = [run, line((0.1003, -1.0), (0.1003, 1.0), 0)]; let arr = shape_builder(&s, false); let pieces: Vec<&Shape> = arr.lines.iter().filter(|l| l.key == 0).collect(); assert_eq!(pieces.len(), 2, "cut once by the line: {}", pieces.len()); let total: f64 = pieces.iter().map(|l| l.path.length()).sum(); // Where the line crosses, one segment may be taken for a stub (it is as short as one), // moving the cut by less than a stub's reach; the rest of the run stays. assert!((total - whole).abs() <= DEFAULT_PRECISION / 16.0, "{total} of {whole}"); for l in &pieces { let (a, b) = ends(&l.path.subpaths[0]); assert!(a.distance(b) > 0.09, "a piece collapsed: {a:?} {b:?}"); } assert_eq!(arr.lines.iter().filter(|l| l.key == 1).count(), 2); } #[test] fn a_dense_hatch_at_exact_binary_steps_cuts_each_line_once_per_crossing() { // Lines every 0.25 or 0.5 pt (exact in binary) cut a crossing line or a circle where two // passes of each line met it a rounding error apart, adding a speck and a piece there. let hatch = |n: i32, step: f64, f: &dyn Fn(f64) -> Shape| -> Vec { (1..=n).map(|k| f(step * f64::from(k))).collect() }; // Horizontal lines across a diagonal: each in two pieces, the diagonal in n + 1. let mut s = vec![line((-3.0, -2.0), (33.0, 31.0), 0)]; s.extend(hatch(119, 0.25, &|y| line((-5.0, y), (35.0, y), 0))); let arr = shape_builder(&s, false); assert_pieces_cut_out(&s, &arr); assert_eq!(arr.lines.iter().filter(|l| l.key == 0).count(), 120); assert!((1..=119).all(|k| arr.lines.iter().filter(|l| l.key == k).count() == 2)); assert_eq!(arr.lines.len(), 120 + 2 * 119); // Sloping lines across a 45° one. let mut s = vec![line((-3.0, -3.0), (33.0, 33.0), 0)]; s.extend(hatch(59, 0.5, &|v| line((v - 40.0, -5.0), (v + 40.0, 35.0), 0))); let arr = shape_builder(&s, false); assert_pieces_cut_out(&s, &arr); assert_eq!(arr.lines.len(), 60 + 2 * 59, "{}", arr.lines.len()); // Vertical lines across a circle: n + 1 strips, each line in three pieces, the circle's // outline in two per line. let mut s = vec![Shape::new(shapes::ellipse(Rect::new(0.0, 0.0, 30.0, 30.0)), FillRule::NonZero, 0)]; s.extend(hatch(119, 0.25, &|x| line((x, -5.0), (x, 35.0), 0))); let arr = shape_builder(&s, true); assert_pieces_cut_out(&s, &arr); assert_eq!(arr.regions.len(), 120); assert!(arr.regions.iter().all(|r| r.sources == [0])); assert!((1..=119).all(|k| arr.lines.iter().filter(|l| l.key == k).count() == 3)); assert_eq!(arr.lines.len(), 3 * 119); assert_eq!(arr.edges.len(), 2 * 119); } #[test] fn shape_builder_edges_are_the_outlines_cut_where_paths_meet() { // Two overlapping squares: each outline in two pieces, cut where the other crosses it. let s = [rect(0.0, 0.0, 100.0, 100.0, 0), rect(50.0, 50.0, 150.0, 150.0, 1)]; let with = shape_builder(&s, true); assert_eq!(with.regions, shape_builder(&s, false).regions, "the regions don't change"); assert!(with.lines.is_empty() && shape_builder(&s, false).edges.is_empty()); assert_eq!(with.edges.len(), 4); for key in [0, 1] { assert_eq!(with.edges.iter().filter(|e| e.key == key && !e.path.is_closed()).count(), 2); } let inner = with.edges.iter().find(|e| e.key == 0 && e.path.bounds() == Some(Rect::new(50.0, 50.0, 100.0, 100.0))).expect("A inside B"); // Erasing it opens A there. let cut = cut_out(&s[0].path, &inner.path.subpaths[0], false, 1e-3).expect("on A"); assert_eq!(cut.subpaths.len(), 1); let sp = &cut.subpaths[0]; assert!(!sp.closed && sp.anchors.len() == 5, "{sp:?}"); let (a, b) = ends(sp); assert!([a, b].contains(&Point::new(100.0, 50.0)) && [a, b].contains(&Point::new(50.0, 100.0)), "{a:?} {b:?}"); assert!((cut.length() - 300.0).abs() < 1e-6, "{}", cut.length()); // Not on B. assert_eq!(cut_out(&s[1].path, &inner.path.subpaths[0], false, 1e-3), None); // A lone square's edge is its whole outline: erasing it leaves nothing. let lone = shape_builder(&s[..1], true); assert_eq!(lone.edges.len(), 1); assert!(cut_out(&s[0].path, &lone.edges[0].path.subpaths[0], false, 1e-3).unwrap().is_empty()); } #[test] fn cut_out_splits_open_paths_and_keeps_closing_edges_invisible() { let open = PathData::single(poly(&[(0.0, 0.0), (100.0, 0.0)], false)); let cut = cut_out(&open, &poly(&[(60.0, 0.0), (30.0, 0.0)], false), false, 1e-3).unwrap(); let mut parts: Vec<(Point, Point)> = cut.subpaths.iter().map(ends).collect(); parts.sort_by(|a, b| a.0.x.total_cmp(&b.0.x)); assert_eq!(parts, vec![(Point::new(0.0, 0.0), Point::new(30.0, 0.0)), (Point::new(60.0, 0.0), Point::new(100.0, 0.0))]); // An end piece only shortens it. let cut = cut_out(&open, &poly(&[(70.0, 0.0), (100.0, 0.0)], false), false, 1e-3).unwrap(); assert_eq!(cut.subpaths.iter().map(ends).collect::>(), vec![(Point::new(0.0, 0.0), Point::new(70.0, 0.0))]); // A filled open path runs on round its invisible closing edge (100,100) → (0,0). let corner = PathData::single(poly(&[(0.0, 0.0), (100.0, 0.0), (100.0, 100.0)], false)); let piece = poly(&[(100.0, 50.0), (100.0, 100.0), (50.0, 50.0)], false); assert_eq!(cut_out(&corner, &piece, false, 1e-3), None, "without the fill it isn't on the path"); let cut = cut_out(&corner, &piece, true, 1e-3).unwrap(); assert_eq!(cut.subpaths.len(), 1); assert_eq!(cut.subpaths[0].anchors.iter().map(|a| a.p).collect::>(), [(0.0, 0.0), (100.0, 0.0), (100.0, 50.0)].map(Point::from)); // Cut elsewhere, the closing edge stays invisible: two open runs, not one through it. let cut = cut_out(&corner, &poly(&[(20.0, 0.0), (60.0, 0.0)], false), true, 1e-3).unwrap(); assert_eq!(cut.subpaths.len(), 2); assert!(cut.subpaths.iter().all(|sp| !sp.closed)); assert!((cut.length() - 160.0).abs() < 1e-6, "{}", cut.length()); } #[test] fn run_around_stops_at_the_stops_either_side() { // A lone square's edge is its whole outline; with its corners as stops, a point by any side // gives that side. let lone = shape_builder(&[rect(0.0, 0.0, 100.0, 100.0, 0)], true); let edge = &lone.edges[0].path.subpaths[0]; let corners = [(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)].map(Point::from); let side = |at: (f64, f64)| { run_around(edge, &corners, Point::from(at), 1e-3) .map(|sp| (sp.closed, PathData::single(sp.clone()).length(), PathData::single(sp).bounds())) }; assert_eq!(side((50.0, 1.0)), Some((false, 100.0, Some(Rect::new(0.0, 0.0, 100.0, 0.0))))); for at in [(0.5, 50.0), (99.0, 50.0), (50.0, 100.0)] { let (closed, length, _) = side(at).expect("a side"); assert!(!closed && (length - 100.0).abs() < 1e-6, "{at:?}: {length}"); } // Without two stops on it, the run would be all of the outline. assert_eq!(run_around(edge, &corners[..1], Point::new(50.0, 0.0), 1e-3), None); // An open piece's run ends at the piece's own ends where no stop comes first; (40, 5) lies // off the line and does not stop the run. let line = poly(&[(0.0, 0.0), (100.0, 0.0)], false); let run = run_around(&line, &[Point::new(40.0, 0.0), Point::new(40.0, 5.0)], Point::new(70.0, 0.0), 1e-3).unwrap(); assert_eq!(ends(&run), (Point::new(40.0, 0.0), Point::new(100.0, 0.0))); let run = run_around(&line, &[Point::new(40.0, 0.0)], Point::new(10.0, 0.0), 1e-3).unwrap(); assert_eq!(ends(&run), (Point::new(0.0, 0.0), Point::new(40.0, 0.0))); // A stop counts only where it is nearest the piece: the folded line's last anchor lies // 0.02 from its first side and does not stop the run there. let folded = poly(&[(0.0, 0.0), (100.0, 0.0), (50.0, 0.02)], false); let stops = folded.anchors.iter().map(|a| a.p).collect::>(); let run = run_around(&folded, &stops, Point::new(25.0, 0.0), 0.05).unwrap(); assert_eq!(ends(&run), (Point::new(0.0, 0.0), Point::new(100.0, 0.0))); } #[test] fn run_around_a_ring_of_4000_sides_measures_only_the_stops_it_reaches() { // Every anchor of the ring is a stop, so each run is one side. Measuring every stop along // the whole ring would take 16 million nearest-point searches a call. let n = 4000; let pts: Vec<(f64, f64)> = (0..n) .map(|k| { let a = k as f64 * std::f64::consts::TAU / n as f64; (500.0 * a.cos(), 500.0 * a.sin()) }) .collect(); let ring = poly(&pts, true); let stops: Vec = ring.anchors.iter().map(|a| a.p).collect(); let start = std::time::Instant::now(); for k in (0..n).step_by(200) { let run = run_around(&ring, &stops, ring.segment(k).eval(0.5), 1e-3).expect("a side"); assert_eq!(ends(&run), (stops[k], stops[(k + 1) % n]), "side {k}"); } assert!(start.elapsed().as_secs_f64() < 5.0, "20 runs took {:?}", start.elapsed()); } /// Proptest shrank a failure of `pieces_cut_out_of_their_inputs` to this input with linesweeper /// 0.5. The sweep cuts lines 0 and 1 at points of their crossing up to 1.4 `eps` apart, and /// `vertex_id` merges a point only into a vertex within `eps` of it. Line 0's loose end in the /// square is still one piece, and erasing it shortens line 0 by its length only. No face or /// merged outline runs out along it and back. #[test] fn a_loose_end_is_one_piece_when_the_sweep_cuts_its_passes_apart() { let s = [line((30.0, 20.0), (70.0, 0.0), 0), line((40.0, 0.0), (0.0, 90.0), 1), rect(10.0, 0.0, 40.0, 30.0, 2)]; let arr = shape_builder(&s, true); for key in 0..3 { assert_eq!(arr.edges.iter().chain(&arr.lines).filter(|e| e.key == key).count(), 3, "pieces of {key}"); } for e in arr.edges.iter().chain(&arr.lines) { let input = &s[e.key as usize].path; let cut = cut_out(input, &e.path.subpaths[0], false, 0.05).expect("on its input"); assert!((input.length() - cut.length() - e.path.length()).abs() < 1e-6, "{:?}", e.path); } // From (30,20) to the crossing at (220/7, 135/7). let loose = arr.lines.iter().find(|l| l.path.subpaths[0].anchors.iter().any(|a| a.p == Point::new(30.0, 20.0))).expect("the loose end"); assert_eq!(loose.path.subpaths[0].anchors.len(), 2, "{:?}", loose.path); assert!((loose.path.length() - 2000f64.sqrt() / 28.0).abs() < 1e-6, "{}", loose.path.length()); let (faces, _) = live_paint(&s); let left = at(&faces, 15.0, 15.0); assert_eq!(left.len(), 1); assert_eq!(left[0].path.subpaths[0].anchors.len(), 4, "(10,0) (40,0) (26.67,30) (10,30): {:?}", left[0].path); let merged = crate::FaceMerger::new(&s).merge(&faces.iter().collect::>()); assert_eq!(merged.subpaths.len(), 1, "the square: {merged:?}"); assert_eq!(merged.subpaths[0].anchors.len(), 4, "{merged:?}"); } fn arb_segment() -> impl Strategy { let c = || (0..10i32).prop_map(|v| f64::from(v) * 10.0); prop_oneof![ (c(), c(), c(), c()).prop_map(|(a, b, x, y)| line((a, b), (x, y), 0)), (c(), c(), 1..5i32, 1..5i32).prop_map(|(x, y, w, h)| rect(x, y, x + f64::from(w) * 10.0, y + f64::from(h) * 10.0, 0)), (c(), c(), 1..5i32).prop_map(|(x, y, r)| { let r = f64::from(r) * 10.0; Shape::new(shapes::ellipse(Rect::new(x, y, x + r, y + r)), FillRule::EvenOdd, 0) }), ] } proptest! { #![proptest_config(ProptestConfig { cases: 64, ..ProptestConfig::default() })] /// Faces never overlap, and a face's sources are exactly the inputs whose fill holds a /// point inside it. #[test] fn faces_are_disjoint_and_know_what_covers_them(shapes in prop::collection::vec(arb_segment(), 1..7)) { let (faces, _) = live_paint(&shapes); for f in &faces { prop_assert!(area(f) > 0.0); let Some(p) = crate::interior_point(&f.path) else { continue }; prop_assert_eq!(faces.iter().filter(|g| g.contains(p)).count(), 1, "{:?} in more than one face", p); let covering: Vec = shapes .iter() .enumerate() .filter(|(_, s)| s.path.is_closed() && vectorcraft_geom::hit::fill_contains(&s.path.to_bezpath(), s.rule, p)) .map(|(i, _)| i) .collect(); prop_assert_eq!(&f.sources, &covering, "at {:?}", p); } } /// Closed shapes alone: the filled faces cover exactly the Pathfinder regions. #[test] fn filled_faces_are_the_pathfinder_regions(shapes in prop::collection::vec(arb_segment(), 1..6)) { let closed: Vec = shapes.into_iter().filter(|s| s.path.is_closed()).collect(); let (faces, _) = live_paint(&closed); let filled: f64 = faces.iter().filter(|f| !f.sources.is_empty()).map(area).sum(); let regions: f64 = crate::regions(&closed).iter().map(|r| area(r).abs()).sum(); // Both refit split curves to [`DEFAULT_PRECISION`], so curved areas may differ by up // to that much along every outline. let outlines: f64 = closed.iter().map(|s| s.path.to_bezpath().perimeter(1e-6)).sum(); prop_assert!((filled - regions).abs() <= 1e-9 + DEFAULT_PRECISION * outlines, "{} vs {}", filled, regions); } /// Every edge and line piece cuts out of the input it is keyed by, which loses its length /// and nothing else. #[test] fn pieces_cut_out_of_their_inputs(shapes in prop::collection::vec(arb_segment(), 1..6)) { let arr = shape_builder(&shapes, true); for e in arr.edges.iter().chain(&arr.lines) { let (Some(s), Some(piece)) = (shapes.get(e.key as usize), e.path.subpaths.first()) else { continue }; let len = e.path.length(); let cut = cut_out(&s.path, piece, false, 0.05); if len <= 0.05 { // A speck where two paths touch. prop_assert!(cut.is_none()); continue; } prop_assert!(cut.is_some(), "{:?} not on {:?}", piece, s.path); let lost = s.path.length() - cut.map_or(0.0, |c| c.length()); prop_assert!((lost - len).abs() <= 0.05 + 1e-3 * len, "lost {} for a piece {} long", lost, len); } } /// Lines anywhere (not on a grid): faces stay disjoint, and nothing panics. #[test] fn free_lines_make_disjoint_faces(pts in prop::collection::vec((-50.0..150.0f64, -50.0..150.0f64, -50.0..150.0f64, -50.0..150.0f64), 2..9)) { let lines: Vec = pts.into_iter().map(|(a, b, x, y)| line((a, b), (x, y), 0)).collect(); let (faces, edges) = live_paint(&lines); prop_assert!(edges.len() >= lines.len().min(1)); for f in &faces { prop_assert!(f.sources.is_empty()); if let Some(p) = crate::interior_point(&f.path) { prop_assert_eq!(faces.iter().filter(|g| g.contains(p)).count(), 1); } } } } #[test] fn many_lines_stay_quick() { // 60 lines through a 100 pt square: about 900 crossings. let lines: Vec = (0..60) .map(|k| { let t = f64::from(k) * 0.37; line((50.0 + 80.0 * t.cos(), 50.0 + 80.0 * t.sin()), (50.0 - 80.0 * (t + 1.1).cos(), 50.0 - 80.0 * (t + 1.1).sin()), 0) }) .collect(); let started = std::time::Instant::now(); let (faces, _) = live_paint(&lines); assert!(faces.len() > 500, "{}", faces.len()); assert!(started.elapsed().as_secs_f64() < 5.0, "{:?}", started.elapsed()); }