mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 11:55:23 +00:00
revolve_curve_samples counted a profile's contours but never compared the count with the limit, so a profile made only of empty or one-anchor subpaths passed, although each contour takes one profile point. HoleSearch::new then built one entry for each subpath without a limit, and mesh export skips revolve_curve_samples when curve_samples is given. Both now refuse a profile with more than 16,384 contours as too complex.
151 lines
7.1 KiB
Rust
151 lines
7.1 KiB
Rust
use crate::*;
|
|
use vectorcraft_geom::{Anchor, SubPath};
|
|
|
|
/// A closed star of `points` tips: twice as many straight edges.
|
|
fn star(points: usize) -> PathData {
|
|
let anchors = (0..points * 2)
|
|
.map(|i| {
|
|
let (s, c) = (i as f64 * std::f64::consts::PI / points as f64).sin_cos();
|
|
let r = if i % 2 == 0 { 100.0 } else { 60.0 };
|
|
Anchor::corner(Point::new(200.0 + r * c, 200.0 + r * s))
|
|
})
|
|
.collect();
|
|
PathData::new(vec![SubPath::new(anchors, true)])
|
|
}
|
|
|
|
/// An open wave of `anchors` smooth anchors: one curved segment between each two.
|
|
fn wave(anchors: usize) -> PathData {
|
|
let anchors = (0..anchors)
|
|
.map(|i| {
|
|
let p = Point::new(100.0 + if i % 2 == 0 { 0.0 } else { 20.0 }, i as f64);
|
|
Anchor::smooth(p, p + Vec2::new(6.0, 0.3))
|
|
})
|
|
.collect();
|
|
PathData::new(vec![SubPath::new(anchors, false)])
|
|
}
|
|
|
|
fn revolved(path: &PathData, options: Revolve) -> Result<Vec<Face>, String> {
|
|
revolve(path, path.bounds().unwrap(), options)
|
|
}
|
|
|
|
#[test]
|
|
fn a_star_of_600_points_revolves_at_the_default_segments() {
|
|
let faces = revolved(&star(600), Revolve::default()).unwrap();
|
|
// The tip on the axis gives two edges with a side of no length.
|
|
assert!((1198 * 64 * 2..=1200 * 64 * 2).contains(&faces.len()), "{}", faces.len());
|
|
let refused = revolved(&star(600), Revolve { segments: 128, ..Revolve::default() }).unwrap_err();
|
|
assert!(refused.contains("reduce Segments"), "{refused}");
|
|
}
|
|
|
|
#[test]
|
|
fn a_curved_profile_past_the_old_limit_takes_fewer_samples_for_each_curve() {
|
|
let wave = wave(200);
|
|
assert!(revolved(&wave, Revolve::default()).is_ok());
|
|
let samples = revolve_curve_samples([&wave], Revolve::default()).unwrap();
|
|
assert!((MIN_CURVE_SAMPLES..CURVE_SAMPLES).contains(&samples), "{samples}");
|
|
}
|
|
|
|
#[test]
|
|
fn a_profile_within_the_limits_keeps_every_sample() {
|
|
assert_eq!(revolve_curve_samples([&wave(50)], Revolve::default()), Ok(CURVE_SAMPLES));
|
|
assert_eq!(revolve_curve_samples([&star(5)], Revolve::default()), Ok(CURVE_SAMPLES));
|
|
}
|
|
|
|
#[test]
|
|
fn two_thousand_curves_fit_at_low_segments_and_are_refused_before_any_face_at_high() {
|
|
let wave = wave(2001);
|
|
let samples = revolve_curve_samples([&wave], Revolve { segments: 8, ..Revolve::default() }).unwrap();
|
|
assert!((MIN_CURVE_SAMPLES..CURVE_SAMPLES).contains(&samples), "{samples}");
|
|
assert!(revolved(&wave, Revolve { segments: 8, ..Revolve::default() }).is_ok());
|
|
let refused = revolve_curve_samples([&wave], Revolve { segments: 128, ..Revolve::default() }).unwrap_err();
|
|
assert!(refused.contains("reduce Segments"), "{refused}");
|
|
assert_eq!(revolved(&wave, Revolve { segments: 128, ..Revolve::default() }), Err(refused));
|
|
}
|
|
|
|
#[test]
|
|
fn too_many_anchors_are_refused_as_a_profile() {
|
|
let refused = revolve_curve_samples([&wave(MAX_PROFILE_POINTS + 1)], Revolve { segments: 8, ..Revolve::default() }).unwrap_err();
|
|
assert_eq!(refused, TOO_COMPLEX_PROFILE);
|
|
}
|
|
|
|
#[test]
|
|
fn every_contour_takes_a_point_even_an_empty_one() {
|
|
// Contours of one anchor and of none, with no segment between them.
|
|
let lone = |i: usize| SubPath::new(if i.is_multiple_of(2) { vec![] } else { vec![Anchor::corner(Point::new(100.0, i as f64))] }, false);
|
|
let path = PathData::new((0..=MAX_PROFILE_POINTS).map(lone).collect());
|
|
assert_eq!(revolve_curve_samples([&path], Revolve::default()), Err(TOO_COMPLEX_PROFILE.to_string()));
|
|
assert_eq!(hole_segments(&path, FillRule::NonZero), Err(TOO_COMPLEX_PROFILE.to_string()), "before any contour is built");
|
|
let given = Revolve { curve_samples: Some(CURVE_SAMPLES), ..Revolve::default() };
|
|
assert_eq!(revolve_mesh(&path, FillRule::NonZero, path.bounds().unwrap(), given).map(|_| ()), Err(TOO_COMPLEX_PROFILE.to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn a_profile_at_the_point_limit_is_taken_and_one_past_it_is_refused() {
|
|
let straight = |anchors: usize| {
|
|
let anchors = (0..anchors).map(|i| Anchor::corner(Point::new(100.0 + (i % 2) as f64, i as f64))).collect();
|
|
PathData::new(vec![SubPath::new(anchors, false)])
|
|
};
|
|
let narrow = Revolve { angle: 1.0, cap_ends: false, segments: 8, ..Revolve::default() };
|
|
assert_eq!(revolve_curve_samples([&straight(MAX_PROFILE_POINTS)], narrow), Ok(CURVE_SAMPLES));
|
|
assert_eq!(revolve_curve_samples([&straight(MAX_PROFILE_POINTS + 1)], narrow), Err(TOO_COMPLEX_PROFILE.to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn the_profiles_of_one_object_share_one_budget() {
|
|
let (one, other) = (star(600), star(600));
|
|
let o = Revolve::default();
|
|
assert!(revolve_curve_samples([&one], o).is_ok());
|
|
assert!(revolve_curve_samples([&other], o).is_ok());
|
|
let refused = revolve_curve_samples([&one, &other], o).unwrap_err();
|
|
assert!(refused.contains("reduce Segments"), "{refused}");
|
|
// Contours of one profile count together as well.
|
|
let both = PathData::new(one.subpaths.iter().chain(&other.subpaths).cloned().collect());
|
|
assert_eq!(revolve_curve_samples([&both], o), Err(refused));
|
|
}
|
|
|
|
#[test]
|
|
fn deeply_nested_contours_stop_the_search_for_holes() {
|
|
// Concentric triangles: each tests every contour outside it.
|
|
let triangle = |r: f64| {
|
|
let anchors = [(0.0, -r), (r, r), (-r, r)].iter().map(|(x, y)| Anchor::corner(Point::new(200_000.0 + x, 200_000.0 + y))).collect();
|
|
SubPath::new(anchors, true)
|
|
};
|
|
let nested = PathData::new((1..=4000).map(|i| triangle(i as f64)).collect());
|
|
assert_eq!(hole_segments(&nested, FillRule::NonZero), Err(TOO_COMPLEX_PROFILE.to_string()));
|
|
let few = PathData::new((1..=4).map(|i| triangle(i as f64)).collect());
|
|
assert_eq!(hole_segments(&few, FillRule::EvenOdd).map(|h| h.len()), Ok(4));
|
|
}
|
|
|
|
#[test]
|
|
fn a_point_is_tested_only_against_the_contours_near_it() {
|
|
// 3,000 separate squares: each segment's middle lies in the bounds of its own square only.
|
|
let squares = (0..3000)
|
|
.map(|i| {
|
|
let (x, y) = ((i % 60) as f64 * 20.0, (i / 60) as f64 * 20.0);
|
|
let corners = [(x, y), (x + 10.0, y), (x + 10.0, y + 10.0), (x, y + 10.0)];
|
|
SubPath::new(corners.iter().map(|(x, y)| Anchor::corner(Point::new(*x, *y))).collect(), true)
|
|
})
|
|
.collect();
|
|
let (holes, work) = hole_segments_counted(&PathData::new(squares), FillRule::NonZero).unwrap();
|
|
assert!(holes.iter().flatten().all(|hole| !hole));
|
|
assert!(work < 200_000, "{work}");
|
|
}
|
|
|
|
#[test]
|
|
fn overlapping_contours_are_holes_only_where_they_overlap() {
|
|
let square = |x: f64, y: f64, side: f64| {
|
|
let corners = [(x, y), (x + side, y), (x + side, y + side), (x, y + side)];
|
|
SubPath::new(corners.iter().map(|(x, y)| Anchor::corner(Point::new(*x, *y))).collect(), true)
|
|
};
|
|
let overlapping = PathData::new(vec![square(0.0, 0.0, 100.0), square(40.0, 40.0, 100.0)]);
|
|
let expected = vec![vec![false, true, true, false], vec![true, false, false, true]];
|
|
assert_eq!(hole_segments(&overlapping, FillRule::EvenOdd), Ok(expected));
|
|
}
|
|
|
|
#[test]
|
|
fn given_samples_are_used_for_each_curve() {
|
|
let wave = wave(4);
|
|
let at = |samples| revolved(&wave, Revolve { curve_samples: Some(samples), cap_ends: false, segments: 8, ..Revolve::default() }).unwrap().len();
|
|
assert_eq!(at(4) * 2, at(8));
|
|
}
|