486 lines
18 KiB
Rust
486 lines
18 KiB
Rust
// 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, 5.0);
|
||
assert_eq!(g.len(), 6);
|
||
let w = (100.0 - 10.0) / 3.0;
|
||
let h = (50.0 - 5.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).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);
|
||
}
|