Files
vectorcraft/crates/pathops/src/planar/tests.rs
T

1022 lines
53 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<Point> = 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::<f64>() - 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<Shape> = 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<Point> = 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<Point> = 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::<Vec<_>>();
assert_eq!(covered(true), [vec![0, 1]]);
assert_eq!(covered(false), [Vec::<usize>::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(&[&regions[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<Point> = 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::<f64>() - 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<Point> = (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<Point> = (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::<f64>() - 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::<f64>() - 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<usize> = index.at(p, 0.1).copied().collect();
got.sort_unstable();
let mut want: Vec<usize> = 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::<usize>::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<u32> = index.at(p, 0.1).map(|(_, k)| *k).collect();
assert!(got.len() <= n as usize / 4, "{p:?}: {} candidates", got.len());
let through: Vec<u32> =
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<u32> = 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::<Vec<_>>());
assert!(index.cells.iter().map(Vec::len).sum::<usize>() >= 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<u32> = 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::<Vec<_>>());
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::<Vec<_>>(), 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<Point> = (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<Shape> { (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<_>>(), 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::<Vec<_>>(), [(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::<Vec<_>>();
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<Point> = 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::<Vec<_>>());
assert_eq!(merged.subpaths.len(), 1, "the square: {merged:?}");
assert_eq!(merged.subpaths[0].anchors.len(), 4, "{merged:?}");
}
fn arb_segment() -> impl Strategy<Value = Shape> {
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<usize> = 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<Shape> = 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<Shape> = 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<Shape> = (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());
}