Files
vectorcraft/crates/pathops/tests/it/ops.rs
T
dotCooCoo 6026772765 Fix #3773: Offset Path offsets a path whose ends meet as a closed shape
Offset Path outlined an open path on both sides, so a shape whose last
anchor sits on its first came out as a ring with a hole. Such a path is
now offset as a closed shape: its last anchor merges into its first,
which takes the last anchor's in-handle, with the same 1e-6 test that
Object › Path › Join uses for one path (crates/engine/src/cmd/path.rs).
The Offset Path effect calls the same function.
2026-10-29 09:57:34 +07:00

566 lines
22 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 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));
}
/// #3870: a circle whose anchors sit on the square's edge it crosses gives the four edges of the
/// arrangement, meeting at those anchors, and no sliver between an anchor and the crossing
/// computed a hair from it.
#[test]
fn pf_outline_has_no_sliver_where_an_anchor_is_the_crossing() {
let s = vec![shape(rect(0.0, 0.0, 100.0, 100.0), 1), shape(circle(100.0, 100.0, 50.0), 2)];
let r = pathfinder(PathfinderOp::Outline, &s);
assert_eq!(r.len(), 4, "{r:?}");
assert!(r.iter().all(|s| s.path.length() > 1.0), "no sliver: {r:?}");
}
#[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 regions_split_an_island_inside_a_hole_from_the_frame_around_it() {
// A frame (0..200 minus 50..150) with an island (80..120) in its hole, as Minus Front leaves
// a rectangle behind a ring (#3132): the frame and the island are separate faces.
let mut p = rect(0.0, 0.0, 200.0, 200.0);
p.subpaths.extend(rect(50.0, 50.0, 100.0, 100.0).subpaths);
p.subpaths.extend(rect(80.0, 80.0, 40.0, 40.0).subpaths);
let s = vec![Shape::new(p, FillRule::EvenOdd, 1)];
let rs = regions(&s);
let mut filled: Vec<(f64, Rect)> = rs.iter().filter(|r| r.sources == vec![0]).map(|r| (ar(&r.path).abs(), r.path.bounds().unwrap())).collect();
filled.sort_by(|a, b| b.0.total_cmp(&a.0));
assert_eq!(filled.len(), 2, "{filled:?}");
assert!(close(filled[0].0, 30000.0, 1e-9), "{filled:?}");
assert_eq!(filled[0].1, Rect::new(0.0, 0.0, 200.0, 200.0));
assert!(close(filled[1].0, 1600.0, 1e-9), "{filled:?}");
assert_eq!(filled[1].1, Rect::new(80.0, 80.0, 120.0, 120.0));
let frame = region_at(&s, Point::new(10.0, 10.0)).unwrap();
assert!(!frame.contains(Point::new(100.0, 100.0)));
}
#[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 offset_of_an_open_path_whose_ends_meet_is_the_closed_shape_offset() {
// Issue #3773: an open path whose last anchor sits on its first offsets like the closed shape,
// not as a ring around its outline.
let corners = [Point::new(0.0, 0.0), Point::new(20.0, 0.0), Point::new(20.0, 10.0), Point::new(0.0, 10.0), Point::new(0.0, 0.0)];
let r = PathData::single(SubPath::polyline(&corners, false));
let grown = offset_path(&r, 2.0, Join::Miter, 4.0);
assert!(close(ar(&grown), 24.0 * 14.0, 1e-6), "{}", ar(&grown));
assert_eq!(grown.subpaths.len(), 1);
assert_eq!(grown.anchor_count(), 4);
assert!(close(ar(&offset_path(&r, -2.0, Join::Miter, 4.0)), 16.0 * 6.0, 1e-6));
// A curved outline: the last anchor's in-handle shapes the closing curve.
let mut sp = circle(0.0, 0.0, 10.0).subpaths.remove(0);
let first = sp.anchors[0];
sp.anchors.push(vectorcraft_geom::Anchor::with_handles(first.p, first.h_in, first.p));
sp.anchors[0].h_in = first.p;
sp.closed = false;
let o = offset_path(&PathData::single(sp), 3.0, Join::Round, 4.0);
assert!(close(ar(&o), PI * 169.0, 2e-3), "{}", ar(&o));
assert_eq!(o.subpaths.len(), 1);
}
#[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_straight_line_with_round_caps_is_compact() {
let l = PathData::single(SubPath::polyline(&[Point::new(0.0, 50.0), Point::new(100.0, 50.0)], false));
let o = outline_stroke(&l, 10.0, Cap::Round, Join::Round, 4.0);
assert!(close(ar(&o), 100.0 * 10.0 + PI * 5.0 * 5.0, 1e-3), "{}", ar(&o));
let anchors = o.subpaths.iter().map(|s| s.anchors.len()).sum::<usize>();
assert!(anchors <= 8, "round-cap expand should stay compact, got {anchors} anchors");
let dense: Vec<Point> = (0..=20).map(|i| Point::new(i as f64 * 5.0, 50.0)).collect();
let dense_path = PathData::single(SubPath::polyline(&dense, false));
let dense_o = outline_stroke(&dense_path, 10.0, Cap::Round, Join::Round, 4.0);
let dense_anchors = dense_o.subpaths.iter().map(|s| s.anchors.len()).sum::<usize>();
assert_eq!(dense_anchors, anchors, "colinear spine points should not add outline anchors");
}
#[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);
}
/// The Simplify slider's levels: 1 pt at 50, doubled for every 10 levels down and halved for every
/// 10 up; Auto-Simplify takes the lowest level whose tolerance is at most 1/400 of the size, and
/// level 100 for a size under 12.5 pt.
#[test]
fn simplify_levels_and_auto_simplify() {
assert_eq!(simplify_level_tolerance(SIMPLIFY_LEVEL), 1.0);
assert_eq!((simplify_level_tolerance(0.0), simplify_level_tolerance(100.0)), (32.0, 1.0 / 32.0));
assert_eq!((simplify_level_tolerance(-5.0), simplify_level_tolerance(500.0), simplify_level_tolerance(f64::NAN)), (32.0, 1.0 / 32.0, 1.0));
assert_eq!((auto_simplify_level(400.0), auto_simplify_level(100.0), auto_simplify_level(300.0)), (50.0, 70.0, 55.0));
assert!(simplify_level_tolerance(auto_simplify_level(300.0)) <= 300.0 / 400.0);
assert_eq!((auto_simplify_level(12.5), auto_simplify_level(10.0)), (100.0, 100.0));
assert_eq!((auto_simplify_level(0.0), auto_simplify_level(f64::INFINITY), auto_simplify_level(1e9)), (100.0, 100.0, 0.0));
}
#[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);
}