Files
vectorcraft/crates/pathops/tests/ops.rs
T
bflatastic cb9d93b647 Fix #1462: Split Into Grid has its row and column sizes, gutters, totals, Add Guides and Preview
Object > Path > Split Into Grid took a number of rows and columns and one gutter, and split each
selected path on its own. The dialog now has the documented options, the rows beside the columns:
number, height or width, gutter and total, with Add Guides and Preview. Several selected paths
become one grid over them all, painted as the topmost one, as documented.

object.path.splitIntoGrid takes rowGutter and columnGutter (gutter still sets both), totalHeight
and totalWidth, height and width (the size of each row and column, which sets the total) and
addGuides, which adds ruler guides along the row and column edges on the grid's layer.

Behaviour reference:
https://helpx.adobe.com/illustrator/desktop/manage-objects/edit-objects/divide-or-split-objects.html
2026-10-13 02:48:36 +07:00

487 lines
18 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.
// Integration tests: unwrapping and panicking on failure is fine here, unlike in shipped code (AGENTS.md › Robustness).
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use std::f64::consts::PI;
use kurbo::{ParamCurve, Point, Rect, Shape as _};
use vectorcraft_geom::{FillRule, PathData, SubPath};
use vectorcraft_pathops::*;
const NZ: FillRule = FillRule::NonZero;
fn rect(x: f64, y: f64, w: f64, h: f64) -> PathData {
PathData::from_bezpath(&Rect::new(x, y, x + w, y + h).to_path(1e-6))
}
fn circle(cx: f64, cy: f64, r: f64) -> PathData {
PathData::from_bezpath(&kurbo::Circle::new((cx, cy), r).to_path(1e-4))
}
fn ar(p: &PathData) -> f64 {
area(p, NZ)
}
fn close(a: f64, b: f64, rel: f64) -> bool {
(a - b).abs() <= rel * a.abs().max(b.abs()).max(1.0)
}
fn shape(p: PathData, key: u64) -> Shape {
Shape::new(p, NZ, key)
}
fn has_curves(p: &PathData) -> bool {
p.anchors().any(|(_, _, a)| a.has_in() || a.has_out())
}
#[test]
fn union_of_rects() {
let u = boolean(&rect(0.0, 0.0, 10.0, 10.0), NZ, &rect(5.0, 5.0, 10.0, 10.0), NZ, BoolOp::Union);
assert!(close(ar(&u), 175.0, 1e-9));
assert_eq!(u.subpaths.len(), 1);
assert_eq!(u.anchor_count(), 8);
}
#[test]
fn intersect_of_rects() {
let i = boolean(&rect(0.0, 0.0, 10.0, 10.0), NZ, &rect(5.0, 5.0, 10.0, 10.0), NZ, BoolOp::Intersect);
assert!(close(ar(&i), 25.0, 1e-9));
assert_eq!(i.anchor_count(), 4);
}
#[test]
fn difference_of_rects() {
let d = boolean(&rect(0.0, 0.0, 10.0, 10.0), NZ, &rect(5.0, 5.0, 10.0, 10.0), NZ, BoolOp::Difference);
assert!(close(ar(&d), 75.0, 1e-9));
assert_eq!(d.anchor_count(), 6);
}
#[test]
fn xor_of_rects() {
let x = boolean(&rect(0.0, 0.0, 10.0, 10.0), NZ, &rect(5.0, 5.0, 10.0, 10.0), NZ, BoolOp::Xor);
assert!(close(ar(&x), 150.0, 1e-9));
}
#[test]
fn disjoint_union_keeps_both() {
let u = boolean(&rect(0.0, 0.0, 1.0, 1.0), NZ, &rect(5.0, 5.0, 1.0, 1.0), NZ, BoolOp::Union);
assert_eq!(u.subpaths.len(), 2);
assert!(close(ar(&u), 2.0, 1e-9));
}
#[test]
fn a_minus_a_is_empty() {
let c = circle(3.0, 4.0, 7.0);
assert!(boolean(&c, NZ, &c, NZ, BoolOp::Difference).is_empty());
}
#[test]
fn union_idempotent_rect() {
let r = rect(1.0, 2.0, 3.0, 4.0);
let u = boolean(&r, NZ, &r, NZ, BoolOp::Union);
assert_eq!(u.anchor_count(), 4);
assert!(close(ar(&u), 12.0, 1e-9));
}
#[test]
fn circle_union_is_curve_preserving_and_compact() {
let u = boolean(&circle(0.0, 0.0, 10.0), NZ, &circle(10.0, 0.0, 10.0), NZ, BoolOp::Union);
assert!(has_curves(&u));
assert!(u.anchor_count() <= 12, "anchors: {}", u.anchor_count());
// Lens area: 2 r² acos(d/2r) − (d/2)√(4r² − d²)
let (r, d): (f64, f64) = (10.0, 10.0);
let lens = 2.0 * r * r * (d / (2.0 * r)).acos() - d / 2.0 * (4.0 * r * r - d * d).sqrt();
assert!(close(ar(&u), 2.0 * PI * r * r - lens, 1e-3));
}
#[test]
fn circle_intersect_rect_keeps_curve() {
let i = boolean(&circle(0.0, 0.0, 10.0), NZ, &rect(0.0, -20.0, 20.0, 40.0), NZ, BoolOp::Intersect);
assert!(has_curves(&i));
assert!(close(ar(&i), PI * 50.0, 1e-3));
assert!(i.anchor_count() <= 5, "anchors: {}", i.anchor_count());
}
#[test]
fn hole_from_difference() {
let d = boolean(&rect(0.0, 0.0, 10.0, 10.0), NZ, &rect(3.0, 3.0, 4.0, 4.0), NZ, BoolOp::Difference);
assert_eq!(d.subpaths.len(), 2);
assert!(close(ar(&d), 84.0, 1e-9));
// Consistently oriented: works with both fill rules.
assert!(close(area(&d, FillRule::EvenOdd), 84.0, 1e-9));
}
#[test]
fn fill_rules_differ_for_nested_same_direction() {
let mut p = rect(0.0, 0.0, 10.0, 10.0);
p.subpaths.extend(rect(3.0, 3.0, 4.0, 4.0).subpaths);
assert!(close(area(&p, FillRule::NonZero), 100.0, 1e-9));
assert!(close(area(&p, FillRule::EvenOdd), 84.0, 1e-9));
let u = boolean(&p, FillRule::EvenOdd, &rect(20.0, 0.0, 1.0, 1.0), NZ, BoolOp::Union);
assert!(close(ar(&u), 85.0, 1e-9));
}
#[test]
fn open_paths_are_closed_for_fill() {
let tri = PathData::single(SubPath::polyline(&[Point::new(0.0, 0.0), Point::new(10.0, 0.0), Point::new(0.0, 10.0)], false));
assert!(close(ar(&tri), 50.0, 1e-9));
}
#[test]
fn non_finite_input_errors() {
let bad = PathData::single(SubPath::polyline(&[Point::new(f64::NAN, 0.0), Point::new(1.0, 0.0), Point::new(0.0, 1.0)], true));
let r = try_boolean(&bad, NZ, &rect(0.0, 0.0, 1.0, 1.0), NZ, BoolOp::Union, DEFAULT_PRECISION);
assert_eq!(r, Err(PathOpsError::NonFinite));
}
#[test]
fn normalize_figure_eight() {
let p = PathData::single(SubPath::polyline(&[Point::new(0.0, 0.0), Point::new(10.0, 10.0), Point::new(10.0, 0.0), Point::new(0.0, 10.0)], true));
let n = normalize(&p, NZ);
assert_eq!(n.subpaths.len(), 2);
assert!(close(ar(&n), 50.0, 1e-9));
}
fn stack() -> Vec<Shape> {
vec![shape(rect(0.0, 0.0, 10.0, 10.0), 1), shape(rect(5.0, 5.0, 10.0, 10.0), 2)]
}
#[test]
fn pf_unite_takes_front_key() {
let r = pathfinder(PathfinderOp::Unite, &stack());
assert_eq!(r.len(), 1);
assert_eq!(r[0].key, 2);
assert!(close(ar(&r[0].path), 175.0, 1e-9));
}
#[test]
fn pf_minus_front_and_back() {
let r = pathfinder(PathfinderOp::MinusFront, &stack());
assert_eq!(r[0].key, 1);
assert!(close(ar(&r[0].path), 75.0, 1e-9));
let r = pathfinder(PathfinderOp::MinusBack, &stack());
assert_eq!(r[0].key, 2);
assert!(close(ar(&r[0].path), 75.0, 1e-9));
}
#[test]
fn pf_intersect_and_exclude_three() {
let s = vec![shape(rect(0.0, 0.0, 10.0, 10.0), 1), shape(rect(5.0, 0.0, 10.0, 10.0), 2), shape(rect(8.0, 0.0, 10.0, 10.0), 3)];
let i = pathfinder(PathfinderOp::Intersect, &s);
assert!(close(ar(&i[0].path), 20.0, 1e-9));
// Odd coverage: [0,5)=1, [5,8)=2, [8,10)=3, [10,15)=2, [15,18)=1 → 5+2+3 = 10 wide.
let x = pathfinder(PathfinderOp::Exclude, &s);
assert!(close(ar(&x[0].path), 100.0, 1e-9));
}
#[test]
fn pf_divide_faces() {
let r = pathfinder(PathfinderOp::Divide, &stack());
assert_eq!(r.len(), 3);
let total: f64 = r.iter().map(|s| ar(&s.path)).sum();
assert!(close(total, 175.0, 1e-9));
let overlap = r.iter().find(|s| close(ar(&s.path), 25.0, 1e-9)).unwrap();
assert_eq!(overlap.key, 2);
}
#[test]
fn pf_trim_removes_hidden() {
let r = pathfinder(PathfinderOp::Trim, &stack());
assert_eq!(r.len(), 2);
assert!(close(ar(&r[0].path), 75.0, 1e-9));
assert!(close(ar(&r[1].path), 100.0, 1e-9));
}
#[test]
fn pf_merge_same_key() {
let mut s = stack();
s[1].key = 1;
let r = pathfinder(PathfinderOp::Merge, &s);
assert_eq!(r.len(), 1);
assert!(close(ar(&r[0].path), 175.0, 1e-9));
let r = pathfinder(PathfinderOp::Merge, &stack());
assert_eq!(r.len(), 2);
}
#[test]
fn pf_crop() {
let s = vec![shape(rect(0.0, 0.0, 10.0, 10.0), 1), shape(rect(10.0, 0.0, 10.0, 10.0), 2), shape(rect(5.0, 0.0, 10.0, 5.0), 3)];
let r = pathfinder(PathfinderOp::Crop, &s);
assert_eq!(r.len(), 2);
assert!(r.iter().all(|x| close(ar(&x.path), 25.0, 1e-9)));
assert_eq!(r[0].key, 1);
}
#[test]
fn pf_outline_edges() {
let r = pathfinder(PathfinderOp::Outline, &stack());
assert_eq!(r.len(), 4, "{r:?}");
assert!(r.iter().all(|s| s.path.subpaths.iter().all(|sp| !sp.closed)));
let len: f64 = r.iter().map(|s| s.path.length()).sum();
assert!(close(len, 80.0, 1e-9));
}
#[test]
fn pf_outline_dedups_shared_edges() {
// Two squares sharing an edge exactly.
let s = vec![shape(rect(0.0, 0.0, 10.0, 10.0), 1), shape(rect(10.0, 0.0, 10.0, 10.0), 2)];
let r = pathfinder(PathfinderOp::Outline, &s);
let len: f64 = r.iter().map(|s| s.path.length()).sum();
assert!(close(len, 70.0, 1e-9), "len {len}");
}
#[test]
fn regions_and_region_at() {
let s = stack();
let rs = regions(&s);
assert_eq!(rs.len(), 3);
let hit = region_at(&s, Point::new(7.0, 7.0)).unwrap();
assert_eq!(hit.sources, vec![0, 1]);
assert!(close(ar(&hit.path), 25.0, 1e-9));
assert!(region_at(&s, Point::new(100.0, 100.0)).is_none());
let refs: Vec<&Region> = rs.iter().collect();
assert!(close(ar(&merge_regions(&refs)), 175.0, 1e-9));
}
#[test]
fn offset_circle_grows() {
let o = offset_path(&circle(0.0, 0.0, 10.0), 3.0, Join::Round, 4.0);
assert!(close(ar(&o), PI * 169.0, 2e-3), "{}", ar(&o));
assert!(o.anchor_count() <= 16, "anchors {}", o.anchor_count());
}
#[test]
fn offset_circle_insets() {
let o = offset_path(&circle(0.0, 0.0, 10.0), -4.0, Join::Round, 4.0);
assert!(close(ar(&o), PI * 36.0, 2e-3), "{}", ar(&o));
}
#[test]
fn offset_rect_joins() {
let r = rect(0.0, 0.0, 20.0, 10.0);
assert!(close(ar(&offset_path(&r, 2.0, Join::Miter, 4.0)), 24.0 * 14.0, 1e-6));
assert_eq!(offset_path(&r, 2.0, Join::Miter, 4.0).anchor_count(), 4);
assert!(close(ar(&offset_path(&r, 2.0, Join::Bevel, 4.0)), 24.0 * 14.0 - 4.0 * 2.0, 1e-6));
assert!(close(ar(&offset_path(&r, 2.0, Join::Round, 4.0)), 200.0 + 2.0 * 2.0 * 30.0 + PI * 4.0, 1e-3));
assert!(close(ar(&offset_path(&r, -2.0, Join::Miter, 4.0)), 16.0 * 6.0, 1e-6));
assert!(offset_path(&r, -6.0, Join::Miter, 4.0).is_empty());
}
#[test]
fn outline_stroke_of_line() {
let l = PathData::single(SubPath::polyline(&[Point::new(0.0, 0.0), Point::new(30.0, 40.0)], false));
let o = outline_stroke(&l, 4.0, Cap::Butt, Join::Miter, 4.0);
assert!(close(ar(&o), 200.0, 1e-6));
let o = outline_stroke(&l, 4.0, Cap::Square, Join::Miter, 4.0);
assert!(close(ar(&o), 216.0, 1e-6));
}
#[test]
fn outline_stroke_of_square_is_ring() {
let o = outline_stroke(&rect(0.0, 0.0, 10.0, 10.0), 2.0, Cap::Butt, Join::Miter, 10.0);
assert_eq!(o.subpaths.len(), 2);
assert!(close(ar(&o), 144.0 - 64.0, 1e-6));
}
#[test]
fn outline_stroke_self_overlap_is_cleaned() {
// A zig-zag whose stroke overlaps itself at the joins.
let z = PathData::single(SubPath::polyline(&[Point::new(0.0, 0.0), Point::new(10.0, 0.0), Point::new(0.0, 1.0), Point::new(10.0, 2.0)], false));
let o = outline_stroke(&z, 3.0, Cap::Round, Join::Round, 4.0);
assert!(!o.is_empty());
// Normalised output: its signed area equals its fill area.
let signed: f64 = o.subpaths.iter().map(|s| s.area()).sum::<f64>().abs();
assert!(close(signed, ar(&o), 1e-6));
}
fn dense_circle(r: f64, n: usize) -> PathData {
let pts: Vec<Point> =
(0..n).map(|i| Point::new(r * (i as f64 / n as f64 * 2.0 * PI).cos(), r * (i as f64 / n as f64 * 2.0 * PI).sin())).collect();
PathData::single(SubPath::polyline(&pts, true))
}
fn max_deviation(orig: &PathData, simp: &PathData) -> f64 {
let bp = orig.to_bezpath();
let mut worst: f64 = 0.0;
for seg in bp.segments() {
for i in 0..=8 {
let p = seg.eval(i as f64 / 8.0);
worst = worst.max(simp.nearest(p).unwrap().4);
}
}
worst
}
#[test]
fn simplify_dense_circle() {
let c = dense_circle(50.0, 200);
let s = simplify(&c, 0.5);
assert!(s.anchor_count() <= 12, "anchors {}", s.anchor_count());
assert!(max_deviation(&c, &s) <= 0.5 + 1e-6);
assert!(s.subpaths[0].closed);
}
#[test]
fn simplify_keeps_square_corners() {
let mut pts = Vec::new();
for (a, b) in [((0.0, 0.0), (10.0, 0.0)), ((10.0, 0.0), (10.0, 10.0)), ((10.0, 10.0), (0.0, 10.0)), ((0.0, 10.0), (0.0, 0.0))] {
for i in 0..5 {
let t = i as f64 / 5.0;
pts.push(Point::new(a.0 + (b.0 - a.0) * t, a.1 + (b.1 - a.1) * t));
}
}
let sq = PathData::single(SubPath::polyline(&pts, true));
let s = simplify(&sq, 0.1);
assert_eq!(s.anchor_count(), 4);
assert!(!has_curves(&s));
}
#[test]
fn simplify_straight_lines_option() {
let c = dense_circle(50.0, 200);
let s = simplify_with(&c, &SimplifyOptions { tolerance: 1.0, straight_lines: true, ..Default::default() });
assert!(!has_curves(&s));
assert!(s.anchor_count() < 40);
assert!(max_deviation(&c, &s) <= 1.0 + 1e-6);
}
#[test]
fn smooth_rounds_corners() {
let s = smooth(&rect(0.0, 0.0, 10.0, 10.0), 1.0);
assert!(s.anchors().all(|(_, _, a)| a.has_in() && a.has_out()));
let same = smooth(&rect(0.0, 0.0, 10.0, 10.0), 0.0);
assert_eq!(same, rect(0.0, 0.0, 10.0, 10.0));
}
#[test]
fn remove_redundant_collinear_and_curve() {
let p = PathData::single(SubPath::polyline(
&[Point::new(0.0, 0.0), Point::new(5.0, 0.0), Point::new(10.0, 0.0), Point::new(10.0, 10.0), Point::new(0.0, 10.0)],
true,
));
assert_eq!(remove_redundant_points(&p, 1e-6).anchor_count(), 4);
let c = add_anchor_points(&circle(0.0, 0.0, 10.0));
let n0 = c.anchor_count();
let r = remove_redundant_points(&c, 1e-3);
assert!(r.anchor_count() < n0, "{} vs {}", r.anchor_count(), n0);
assert!(close(ar(&r), ar(&c), 1e-4));
}
#[test]
fn add_anchor_points_doubles() {
let c = circle(0.0, 0.0, 10.0);
let a = add_anchor_points(&c);
assert_eq!(a.anchor_count(), 2 * c.anchor_count());
assert!(close(ar(&a), ar(&c), 1e-9));
let r = add_anchor_points(&rect(0.0, 0.0, 10.0, 10.0));
assert!(r.anchors().any(|(_, _, a)| a.p == Point::new(5.0, 0.0)));
}
#[test]
fn average_axes() {
let p = PathData::single(SubPath::polyline(&[Point::new(0.0, 0.0), Point::new(10.0, 4.0), Point::new(5.0, 8.0)], false));
let h = average(&p, &[(0, 0), (0, 1)], AverageAxis::Horizontal);
assert_eq!(h.subpaths[0].anchors[0].p, Point::new(0.0, 2.0));
assert_eq!(h.subpaths[0].anchors[1].p, Point::new(10.0, 2.0));
let b = average(&p, &[(0, 0), (0, 1), (0, 2)], AverageAxis::Both);
assert!(b.subpaths[0].anchors.iter().all(|a| a.p.distance(Point::new(5.0, 4.0)) < 1e-12));
let v = average(&p, &[(0, 0), (0, 1)], AverageAxis::Vertical);
assert_eq!(v.subpaths[0].anchors[0].p.x, 5.0);
}
#[test]
fn join_two_open_paths() {
let a = PathData::single(SubPath::polyline(&[Point::new(0.0, 0.0), Point::new(10.0, 0.0)], false));
let b = PathData::single(SubPath::polyline(&[Point::new(20.0, 5.0), Point::new(10.0, 0.0)], false));
let j = join(&[a.clone(), b], 1e-6);
assert_eq!(j.subpaths.len(), 1);
assert_eq!(j.anchor_count(), 3);
assert!(!j.subpaths[0].closed);
let c = PathData::single(SubPath::polyline(&[Point::new(12.0, 0.0), Point::new(20.0, 0.0)], false));
let j = join(&[a, c], 1e-6);
assert_eq!(j.anchor_count(), 4);
}
#[test]
fn join_single_path_closes() {
let a = PathData::single(SubPath::polyline(&[Point::new(0.0, 0.0), Point::new(10.0, 0.0), Point::new(5.0, 5.0)], false));
let j = join(&[a], 1e-6);
assert!(j.subpaths[0].closed);
assert_eq!(j.anchor_count(), 3);
let b = PathData::single(SubPath::polyline(&[Point::new(0.0, 0.0), Point::new(10.0, 0.0), Point::new(5.0, 5.0), Point::new(0.0, 0.0)], false));
assert_eq!(join(&[b], 1e-6).anchor_count(), 3);
}
#[test]
fn grid_split() {
let g = split_into_grid(Rect::new(0.0, 0.0, 100.0, 50.0), 2, 3, (7.0, 5.0));
assert_eq!(g.len(), 6);
let w = (100.0 - 10.0) / 3.0;
let h = (50.0 - 7.0) / 2.0;
assert!(g.iter().all(|c| close(ar(c), w * h, 1e-9)));
assert_eq!(g[5].bounds().unwrap().x1, 100.0);
assert!(split_into_grid(Rect::new(0.0, 0.0, 10.0, 10.0), 0, 3, (1.0, 1.0)).is_empty());
assert!(split_into_grid(Rect::new(0.0, 0.0, 10.0, f64::NAN), 2, 3, (1.0, 1.0)).is_empty());
}
#[test]
#[ignore]
fn timing_unite_1000_circles() {
let mut seed = 0x1234_5678_u64;
let mut rnd = || {
seed ^= seed << 13;
seed ^= seed >> 7;
seed ^= seed << 17;
(seed % 1_000_000) as f64 / 1_000_000.0
};
let circles: Vec<Shape> = (0..1000).map(|i| shape(circle(rnd() * 1000.0, rnd() * 1000.0, 5.0 + rnd() * 30.0), i)).collect();
let t = std::time::Instant::now();
let r = pathfinder(PathfinderOp::Unite, &circles);
println!("unite 1000 circles: {} ms, {} anchors", t.elapsed().as_millis(), r[0].path.anchor_count());
assert!(!r.is_empty());
}
#[test]
fn remove_anchor_refits_the_curve_round_it() {
use vectorcraft_geom::Anchor;
let (p0, p3) = (Point::new(0.0, 0.0), Point::new(100.0, 0.0));
let original =
SubPath::new(vec![Anchor::with_handles(p0, p0, Point::new(30.0, 80.0)), Anchor::with_handles(p3, Point::new(70.0, 80.0), p3)], false);
let mut sp = original.clone();
let i = sp.insert_anchor(0, 0.4);
assert!(remove_anchor(&mut sp, i));
assert_eq!(sp.anchors.len(), 2);
assert!(sp.anchors[0].h_out.distance(original.anchors[0].h_out) < 0.5, "{:?}", sp.anchors[0].h_out);
assert!(sp.anchors[1].h_in.distance(original.anchors[1].h_in) < 0.5, "{:?}", sp.anchors[1].h_in);
// A circle stays closed and round with one anchor fewer.
let mut c = circle(0.0, 0.0, 50.0).subpaths.remove(0);
let n = c.anchors.len();
assert!(remove_anchor(&mut c, 1));
assert!(c.closed && c.anchors.len() == n - 1);
let a = ar(&PathData::single(c));
assert!(close(a, PI * 2500.0, 0.03), "{a}");
}
#[test]
fn remove_anchor_between_straight_sides_draws_a_straight_one() {
let mut sp = rect(0.0, 0.0, 100.0, 80.0).subpaths.remove(0);
let n = sp.anchors.len();
assert!(remove_anchor(&mut sp, 0));
assert!(sp.closed && sp.anchors.len() == n - 1);
assert!(!sp.anchors.iter().any(|a| a.has_in() || a.has_out()), "{sp:?}");
let mut line = SubPath::polyline(&[Point::new(0.0, 0.0), Point::new(40.0, 30.0), Point::new(100.0, 0.0)], false);
assert!(remove_anchor(&mut line, 1));
assert!(line.anchors.len() == 2 && line.segment_is_line(0));
}
#[test]
fn remove_anchor_at_an_end_drops_its_segment_and_rejects_bad_input() {
let mut sp = SubPath::polyline(&[Point::new(0.0, 0.0), Point::new(10.0, 0.0), Point::new(20.0, 5.0)], false);
assert!(!remove_anchor(&mut sp, 3));
assert!(remove_anchor(&mut sp, 0));
assert_eq!(sp.anchors[0].p, Point::new(10.0, 0.0));
// Non-finite points never panic.
let mut bad = SubPath::polyline(&[Point::new(0.0, 0.0), Point::new(f64::NAN, 1.0), Point::new(f64::INFINITY, 0.0)], false);
bad.anchors[0].h_out = Point::new(5.0, f64::NAN);
assert!(remove_anchor(&mut bad, 1));
assert_eq!(bad.anchors.len(), 2);
}