Merge remote-tracking branch 'origin/main' into codex/craft-eps-fontscan

This commit is contained in:
Dylan M. Taylor committed 2026-11-13 20:02:18 +00:00
commit 142102fc94
1 file changed
+539 -1
+539 -1
View File
@@ -55,7 +55,11 @@ pub fn import(bytes: &[u8]) -> Result<Imported, Error> {
if estimated_elements(&entity.kind) > remaining {
return Err(Error::TooManySegments);
}
let converted = entity_paths(&entity.kind, entity.common.extrusion);
let converted = if let crate::model::EntityKind::Spline(spline) = &entity.kind {
spline_path(spline)?.into_iter().collect()
} else {
entity_paths(&entity.kind, entity.common.extrusion)
};
if converted.iter().flat_map(|path| path.elements()).any(|element| {
let finite = |p: Point| p.x.is_finite() && p.y.is_finite();
match *element {
@@ -128,6 +132,9 @@ fn estimated_elements(kind: &crate::model::EntityKind) -> usize {
polyline_count(&vertices, poly.closed())
}
E::Ellipse { start, end, .. } => 1usize.saturating_add(arc_count(end - start)),
// `spline_path` already caps subdivision at MAX_SPLINE_ELEMENTS. Its actual output is
// checked against the drawing-wide budget after conversion.
E::Spline(_) => 0,
_ => 0,
}
}
@@ -191,6 +198,278 @@ fn entity_paths(kind: &crate::model::EntityKind, normal: crate::model::Point) ->
}
}
const MAX_SPLINE_ELEMENTS: usize = 200_000;
const SPLINE_TOLERANCE: f64 = 1e-6;
const MAX_SPLINE_DEPTH: u32 = 12;
const MAX_SPLINE_DEGREE: usize = 11;
const MIN_RELATIVE_WEIGHT: f64 = 1e-100;
#[derive(Clone, Copy)]
struct Homogeneous {
x: f64,
y: f64,
w: f64,
}
impl Homogeneous {
fn lerp(self, other: Self, t: f64) -> Self {
let s = 1.0 - t;
Self { x: self.x * s + other.x * t, y: self.y * s + other.y * t, w: self.w * s + other.w * t }
}
fn point(self) -> Option<Point> {
(self.w.abs() > 1e-300).then(|| Point::new(self.x / self.w, self.y / self.w)).filter(|point| point.x.is_finite() && point.y.is_finite())
}
}
// Keep the NURBS conversion semantics of the previous importer: exact polynomial Bezier
// spans through cubic degree, tolerance-bounded rational/high-degree spans, invalid-weight
// fallback, and fit-point fallback when control data is malformed.
fn spline_path(spline: &crate::model::Spline) -> Result<Option<BezPath>, Error> {
let weights = (spline.flags & 4 != 0 && spline.weights.len() == spline.control.len()).then_some(spline.weights.as_slice());
let mut limit_hit = false;
let converted = usize::try_from(spline.degree).ok().and_then(|degree| nurbs(degree, &spline.knots, &spline.control, weights, &mut limit_hit));
let path = converted
.map(|mut path| {
if spline.flags & 1 != 0 {
path.close_path();
}
path
})
.or_else(|| {
let closed = spline.flags & 1 != 0;
// `through` emits one move, n-1 (open) or n (closed) cubics, and
// an additional close element for closed paths. Check before allocating
// its converted point vector.
let max_points = MAX_SPLINE_ELEMENTS.saturating_sub(if closed { 2 } else { 0 });
if spline.fit.len() > max_points {
limit_hit = true;
return None;
}
through(&spline.fit.iter().map(|p| Point::new(p[0], p[1])).collect::<Vec<_>>(), closed)
});
if limit_hit {
return Err(Error::TooManySegments);
}
Ok(path)
}
fn nurbs(degree: usize, knots: &[f64], control: &[[f64; 3]], weights: Option<&[f64]>, limit_hit: &mut bool) -> Option<BezPath> {
let n = control.len();
if !(1..=MAX_SPLINE_DEGREE).contains(&degree)
|| n <= degree
|| knots.len() != n + degree + 1
|| knots.windows(2).any(|w| w[1] < w[0])
|| control.iter().any(|p| !p[0].is_finite() || !p[1].is_finite())
|| knots.iter().any(|k| !k.is_finite())
{
return None;
}
let raw = |i: usize| weights.and_then(|w| w.get(i)).copied().filter(|w| *w > 0.0 && w.is_finite()).unwrap_or(1.0);
let max_w = (0..n).map(raw).fold(0.0, f64::max);
if max_w == 0.0 {
return None;
}
let weight = |i: usize| (raw(i) / max_w).max(MIN_RELATIVE_WEIGHT);
let rational = (0..n).any(|i| (weight(i) - weight(0)).abs() > 1e-12);
let (lo, hi) = control.iter().fold((Point::new(f64::MAX, f64::MAX), Point::new(f64::MIN, f64::MIN)), |(lo, hi), p| {
(Point::new(lo.x.min(p[0]), lo.y.min(p[1])), Point::new(hi.x.max(p[0]), hi.y.max(p[1])))
});
let tolerance = ((hi - lo).hypot() * SPLINE_TOLERANCE).max(1e-12);
let mut path = BezPath::new();
for span in degree..n {
let (Some(&t0), Some(&t1)) = (knots.get(span), knots.get(span + 1)) else { continue };
if t1 <= t0 {
continue;
}
let bezier: Vec<Homogeneous> = (0..=degree)
.map(|j| {
let args: Vec<_> = (0..degree).map(|k| if k < degree - j { t0 } else { t1 }).collect();
blossom(degree, knots, control, span, &args, &weight)
})
.collect::<Option<_>>()?;
let start = bezier.first()?.point()?;
let needs_new_subpath = path_endpoint(&path).is_none_or(|end| end.distance(start) > 1e-12 * (1.0 + start.to_vec2().hypot()));
if needs_new_subpath {
path.move_to(start);
}
// A rational degree-one span changes its parameterization with its weights, but its locus
// is still the straight segment between its endpoints. Preserve that exact geometry.
let polynomial: Option<Vec<Point>> = if rational && degree > 1 { None } else { bezier.iter().map(|p| p.point()).collect() };
match polynomial.as_deref() {
Some([_, p]) => path.line_to(*p),
Some([_, q, p]) => path.quad_to(*q, *p),
Some([_, a, b, p]) => path.curve_to(*a, *b, *p),
_ => fit_spline(&mut path, &bezier, tolerance, 0, limit_hit)?,
}
if path.elements().len() > MAX_SPLINE_ELEMENTS {
*limit_hit = true;
return None;
}
}
(path.elements().len() > 1).then_some(path)
}
fn path_endpoint(path: &BezPath) -> Option<Point> {
match path.elements().last()? {
kurbo::PathEl::MoveTo(p) | kurbo::PathEl::LineTo(p) => Some(*p),
kurbo::PathEl::QuadTo(_, p) => Some(*p),
kurbo::PathEl::CurveTo(_, _, p) => Some(*p),
kurbo::PathEl::ClosePath => None,
}
}
fn blossom(degree: usize, knots: &[f64], control: &[[f64; 3]], span: usize, args: &[f64], weight: &impl Fn(usize) -> f64) -> Option<Homogeneous> {
// Only the active degree+1 control points are needed for one span. Convert this small window
// here instead of making a second vector proportional to an untrusted entity's full controls.
let first = span.checked_sub(degree)?;
let mut points: Vec<_> = control
.get(first..=span)?
.iter()
.enumerate()
.map(|(offset, p)| {
let w = weight(first + offset);
Homogeneous { x: p[0] * w, y: p[1] * w, w }
})
.collect();
for r in 1..=degree {
let u = *args.get(r - 1)?;
for j in (r..=degree).rev() {
let i = span - degree + j;
let (a, b) = (*knots.get(i)?, *knots.get(i + degree + 1 - r)?);
let alpha = if b - a > 1e-300 { (u - a) / (b - a) } else { 0.0 };
let previous = *points.get(j - 1)?;
let current = points.get_mut(j)?;
*current = previous.lerp(*current, alpha);
}
}
points.get(degree).copied()
}
fn eval_spline_bezier(bezier: &[Homogeneous], t: f64) -> Option<(Point, kurbo::Vec2)> {
let mut points = bezier.to_vec();
let n = points.len().checked_sub(1)?;
for level in (1..=n).rev() {
if level == 1 {
let (a, b) = (*points.first()?, *points.get(1)?);
let p = a.lerp(b, t);
let point = p.point()?;
let k = n as f64;
let (dx, dy, dw) = (k * (b.x - a.x), k * (b.y - a.y), k * (b.w - a.w));
let w2 = p.w * p.w;
return Some((point, kurbo::Vec2::new((dx * p.w - p.x * dw) / w2, (dy * p.w - p.y * dw) / w2)));
}
for i in 0..level {
let next = *points.get(i + 1)?;
let current = points.get_mut(i)?;
*current = current.lerp(next, t);
}
}
Some((points.first()?.point()?, kurbo::Vec2::ZERO))
}
fn fit_spline(path: &mut BezPath, bezier: &[Homogeneous], tolerance: f64, depth: u32, limit_hit: &mut bool) -> Option<()> {
let (p0, d0) = eval_spline_bezier(bezier, 0.0)?;
let (p3, d3) = eval_spline_bezier(bezier, 1.0)?;
let (c1, c2) = (p0 + d0 / 3.0, p3 - d3 / 3.0);
if ![c1, c2].iter().all(|p| p.x.is_finite() && p.y.is_finite()) {
path.line_to(p3);
return Some(());
}
let candidate = [p0, c1, c2, p3];
if cubic_error_bound(bezier, &candidate).is_some_and(|error| error <= tolerance) {
if path.elements().len() >= MAX_SPLINE_ELEMENTS {
*limit_hit = true;
return None;
}
path.curve_to(c1, c2, p3);
return Some(());
}
if depth >= MAX_SPLINE_DEPTH || path.elements().len() >= MAX_SPLINE_ELEMENTS {
*limit_hit = true;
return None;
}
let (left, right) = split_homogeneous_bezier(bezier)?;
fit_spline(path, &left, tolerance, depth + 1, limit_hit)?;
fit_spline(path, &right, tolerance, depth + 1, limit_hit)
}
fn split_homogeneous_bezier(bezier: &[Homogeneous]) -> Option<(Vec<Homogeneous>, Vec<Homogeneous>)> {
let count = bezier.len();
if count < 2 {
return None;
}
let mut row = bezier.to_vec();
let mut left = vec![*bezier.first()?; count];
let mut right = vec![*bezier.last()?; count];
left[0] = row[0];
right[count - 1] = row[count - 1];
for level in 1..count {
for index in 0..count - level {
row[index] = row[index].lerp(row[index + 1], 0.5);
}
left[level] = row[0];
right[count - 1 - level] = row[count - 1 - level];
}
Some((left, right))
}
// Bound the rational curve/cubic difference over the whole parameter interval. In Bernstein
// form, X(t)-W(t)C(t) has degree p+3, so its norm is bounded by the largest coefficient norm;
// W(t) is a convex combination of positive weights and is at least their minimum.
fn cubic_error_bound(bezier: &[Homogeneous], cubic: &[Point; 4]) -> Option<f64> {
let degree = bezier.len().checked_sub(1)?;
let total = degree.checked_add(3)?;
let min_weight = bezier.iter().map(|point| point.w).fold(f64::INFINITY, f64::min);
if !(min_weight > 0.0 && min_weight.is_finite()) {
return None;
}
let mut max_error_numerator: f64 = 0.0;
for k in 0..=total {
let mut x = 0.0;
let mut y = 0.0;
for (i, point) in bezier.iter().enumerate() {
let Some(j) = k.checked_sub(i).filter(|j| *j < 4) else { continue };
let factor = binomial(degree, i) * binomial(3, j) / binomial(total, k);
x += (point.x - point.w * cubic[j].x) * factor;
y += (point.y - point.w * cubic[j].y) * factor;
}
max_error_numerator = max_error_numerator.max(x.hypot(y));
}
Some(max_error_numerator / min_weight)
}
fn binomial(n: usize, k: usize) -> f64 {
if k > n {
return 0.0;
}
let k = k.min(n - k);
(1..=k).fold(1.0, |value, i| value * (n + 1 - i) as f64 / i as f64)
}
fn through(points: &[Point], closed: bool) -> Option<BezPath> {
let n = points.len();
let first = *points.first()?;
if n < 2 {
return None;
}
let at = |i: isize| {
let len = n as isize;
let j = if closed { i.rem_euclid(len) } else { i.clamp(0, len - 1) };
points.get(j as usize).copied().unwrap_or(first)
};
let mut path = BezPath::new();
path.move_to(first);
let segments = if closed { n } else { n - 1 };
for i in 0..segments as isize {
let (p0, p1, p2, p3) = (at(i - 1), at(i), at(i + 1), at(i + 2));
path.curve_to(p1 + (p2 - p0) / 6.0, p2 - (p3 - p1) / 6.0, p2);
}
if closed {
path.close_path();
}
Some(path)
}
fn map_ocs_path(path: BezPath, normal: crate::model::Point, z: f64) -> BezPath {
let [x, y, n] = crate::model::ocs_axes(normal);
let transform = kurbo::Affine::new([x[0], x[1], y[0], y[1], n[0] * z, n[1] * z]);
@@ -319,3 +598,262 @@ fn ellipse(center: crate::model::Point, major: crate::model::Point, normal: crat
fn cross(a: crate::model::Point, b: crate::model::Point) -> crate::model::Point {
[a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
}
#[cfg(test)]
mod spline_tests {
use super::*;
use crate::model::Spline;
use kurbo::ParamCurveNearest;
// The previous importer accepted cubic fits based on seven fixed probes, then bisected the
// parameter interval. Keep this test-only reference to compare real legacy output with the
// conservative fit used by craft-dxf.
fn prior_fit(path: &mut BezPath, bezier: &[Homogeneous], t0: f64, t1: f64, tolerance: f64, depth: u32) -> Option<()> {
let (p0, d0) = eval_spline_bezier(bezier, t0)?;
let (p3, d3) = eval_spline_bezier(bezier, t1)?;
let h = (t1 - t0) / 3.0;
let (c1, c2) = (p0 + d0 * h, p3 - d3 * h);
if ![c1, c2].iter().all(|point| point.x.is_finite() && point.y.is_finite()) {
path.line_to(p3);
return Some(());
}
let cubic = kurbo::CubicBez::new(p0, c1, c2, p3);
let close = (1..8).all(|i| {
let t = f64::from(i) / 8.0;
eval_spline_bezier(bezier, t0 + (t1 - t0) * t).is_some_and(|(point, _)| kurbo::ParamCurve::eval(&cubic, t).distance(point) <= tolerance)
});
if close || depth >= MAX_SPLINE_DEPTH || path.elements().len() >= MAX_SPLINE_ELEMENTS {
path.curve_to(c1, c2, p3);
return Some(());
}
let midpoint = (t0 + t1) / 2.0;
prior_fit(path, bezier, t0, midpoint, tolerance, depth + 1)?;
prior_fit(path, bezier, midpoint, t1, tolerance, depth + 1)
}
fn legacy_path(bezier: &[Homogeneous], tolerance: f64) -> BezPath {
let mut path = BezPath::new();
let start = eval_spline_bezier(bezier, 0.0).expect("legacy start point").0;
path.move_to(start);
prior_fit(&mut path, bezier, 0.0, 1.0, tolerance, 0).expect("legacy fit succeeds");
path
}
fn sampled_path_distance(from: &BezPath, to: &BezPath, tolerance: f64) -> f64 {
let target: Vec<_> = to.segments().collect();
from.segments()
.flat_map(|segment| (0..=32).map(move |i| kurbo::ParamCurve::eval(&segment, f64::from(i) / 32.0)))
.map(|point| target.iter().map(|segment| segment.nearest(point, tolerance / 100.0).distance_sq.sqrt()).fold(f64::INFINITY, f64::min))
.fold(0.0_f64, f64::max)
}
fn cubic() -> Spline {
Spline {
degree: 3,
flags: 8,
knots: vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0],
control: vec![[0.0, 0.0, 0.0], [1.0, 2.0, 0.0], [2.0, 2.0, 0.0], [3.0, 0.0, 0.0]],
weights: Vec::new(),
fit: Vec::new(),
start_tangent: None,
end_tangent: None,
fit_tolerance: 0.0,
}
}
fn xyz(points: &[Point]) -> Vec<[f64; 3]> {
points.iter().map(|point| [point.x, point.y, 0.0]).collect()
}
#[test]
fn spline_path_follows_clamped_control_curve_and_bounds_work() {
let path = spline_path(&cubic()).unwrap().expect("valid cubic spline");
assert_eq!(path.elements().len(), 2);
assert!(matches!(path.elements()[0], kurbo::PathEl::MoveTo(p) if p == Point::new(0.0, 0.0)));
assert!(matches!(path.elements()[1], kurbo::PathEl::CurveTo(a, b, p)
if a == Point::new(1.0, 2.0) && b == Point::new(2.0, 2.0) && p == Point::new(3.0, 0.0)));
assert_eq!(estimated_elements(&crate::model::EntityKind::Spline(cubic())), 0);
}
#[test]
fn rational_closed_periodic_and_fit_point_splines_keep_compatibility() {
let mut rational_line = Spline {
degree: 1,
flags: 4,
knots: vec![0.0, 0.0, 1.0, 1.0],
control: vec![[2.0, 3.0, 0.0], [5.0, 7.0, 0.0]],
weights: vec![1.0, 100.0],
fit: Vec::new(),
start_tangent: None,
end_tangent: None,
fit_tolerance: 0.0,
};
rational_line.flags |= 8;
let line = spline_path(&rational_line).unwrap().expect("rational degree-one spline");
assert!(matches!(line.elements(), [kurbo::PathEl::MoveTo(a), kurbo::PathEl::LineTo(b)]
if *a == Point::new(2.0, 3.0) && *b == Point::new(5.0, 7.0)));
let mut rational = cubic();
rational.flags |= 4;
rational.weights = vec![1.0, 0.5, 0.5, 1.0];
let rational_path = spline_path(&rational).unwrap().expect("rational spline");
assert!(rational_path.elements().iter().any(|el| matches!(el, kurbo::PathEl::CurveTo(..))));
let mut closed = cubic();
closed.flags |= 1;
assert!(matches!(spline_path(&closed).unwrap().unwrap().elements().last(), Some(kurbo::PathEl::ClosePath)));
let mut periodic = cubic();
periodic.flags |= 2;
assert!(!matches!(spline_path(&periodic).unwrap().unwrap().elements().last(), Some(kurbo::PathEl::ClosePath)));
let mut fit = cubic();
fit.knots.clear();
fit.fit = vec![[0.0, 0.0, 0.0], [1.0, 2.0, 0.0], [3.0, 0.0, 0.0]];
let path = spline_path(&fit).unwrap().expect("fit-point fallback");
assert!(matches!(path.elements()[0], kurbo::PathEl::MoveTo(p) if p == Point::new(0.0,0.0)));
assert!(matches!(path.elements().last(), Some(kurbo::PathEl::CurveTo(_, _, p)) if *p == Point::new(3.0,0.0)));
let mut negative = cubic();
negative.degree = -1;
negative.fit = fit.fit;
assert!(spline_path(&negative).unwrap().is_some(), "invalid degree reaches the prior fit-point fallback");
}
#[test]
fn spline_path_rejects_malformed_knots_and_defaults_invalid_weights() {
let mut spline = cubic();
spline.knots[4] = f64::NAN;
assert!(spline_path(&spline).unwrap().is_none());
let mut spline = cubic();
spline.flags |= 4;
spline.weights = vec![1.0, 0.0, 1.0, 1.0];
assert!(spline_path(&spline).unwrap().is_some(), "invalid weights historically fall back to unit weights");
}
#[test]
fn ascii_document_import_keeps_spline_as_supported_geometry() {
let bytes = b"0\nSECTION\n2\nENTITIES\n0\nSPLINE\n8\n0\n70\n8\n71\n3\n72\n8\n73\n4\n40\n0\n40\n0\n40\n0\n40\n0\n40\n1\n40\n1\n40\n1\n40\n1\n10\n0\n20\n0\n10\n1\n20\n2\n10\n2\n20\n2\n10\n3\n20\n0\n0\nENDSEC\n0\nEOF\n";
let imported = import(bytes).expect("valid spline drawing");
assert_eq!(imported.paths.len(), 1);
assert!(imported.warnings.is_empty());
let bounds = imported.bounds.expect("spline bounds");
assert!((bounds.x0 - 0.0).abs() < 1e-12 && (bounds.x1 - 3.0).abs() < 1e-12);
assert!((bounds.y0 - 0.0).abs() < 1e-12 && (bounds.y1 - 1.5).abs() < 1e-12);
}
#[test]
fn discontinuous_nurbs_spans_remain_separate_subpaths() {
let mut limit_hit = false;
let path = nurbs(
1,
&[0.0, 0.0, 0.5, 0.5, 1.0, 1.0],
&xyz(&[Point::new(0.0, 0.0), Point::new(1.0, 0.0), Point::new(5.0, 5.0), Point::new(6.0, 5.0)]),
None,
&mut limit_hit,
)
.expect("piecewise linear spline");
assert!(!limit_hit);
assert!(matches!(path.elements(), [kurbo::PathEl::MoveTo(a), kurbo::PathEl::LineTo(b), kurbo::PathEl::MoveTo(c), kurbo::PathEl::LineTo(d)]
if *a == Point::new(0.0,0.0) && *b == Point::new(1.0,0.0) && *c == Point::new(5.0,5.0) && *d == Point::new(6.0,5.0)));
}
#[test]
fn positive_knot_spans_at_large_offsets_are_kept() {
let start = 1.0e12;
let delta = 1.0e-3;
let mut limit_hit = false;
let path = nurbs(1, &[start, start, start + delta, start + delta], &xyz(&[Point::new(2.0, 3.0), Point::new(5.0, 7.0)]), None, &mut limit_hit)
.expect("positive-width span");
assert!(!limit_hit);
assert!(matches!(path.elements(), [kurbo::PathEl::MoveTo(a), kurbo::PathEl::LineTo(b)]
if *a == Point::new(2.0, 3.0) && *b == Point::new(5.0, 7.0)));
}
#[test]
fn fit_point_fallback_checks_element_budget_before_collecting() {
let mut spline = cubic();
spline.degree = -1;
spline.fit = vec![[0.0, 0.0, 0.0]; MAX_SPLINE_ELEMENTS + 1];
assert!(matches!(spline_path(&spline), Err(Error::TooManySegments)));
}
#[test]
fn bernstein_error_bound_covers_rational_and_high_degree_deviations() {
for control in [
vec![Homogeneous { x: 0.0, y: 0.0, w: 1.0 }, Homogeneous { x: 0.2, y: 2.0, w: 0.5 }, Homogeneous { x: 1.0, y: 0.0, w: 1.0 }],
vec![
Homogeneous { x: 0.0, y: 0.0, w: 1.0 },
Homogeneous { x: 0.2, y: 2.0, w: 0.7 },
Homogeneous { x: 0.5, y: -2.0, w: 0.9 },
Homogeneous { x: 0.8, y: 2.0, w: 1.2 },
Homogeneous { x: 1.0, y: 0.0, w: 1.0 },
],
] {
let degree = control.len() - 1;
let bezier = [Point::new(0.0, 0.0), Point::new(0.25, 1.0), Point::new(0.75, 1.0), Point::new(1.0, 0.0)];
let bound = cubic_error_bound(&control, &bezier).unwrap();
let cubic = kurbo::CubicBez::new(bezier[0], bezier[1], bezier[2], bezier[3]);
for sample in 0..=1024 {
let t = sample as f64 / 1024.0;
let mut points = control.clone();
for level in (1..=degree).rev() {
for i in 0..level {
points[i] = points[i].lerp(points[i + 1], t);
}
}
let source = points[0].point().unwrap();
let approximation = kurbo::ParamCurve::eval(&cubic, t);
assert!(source.distance(approximation) <= bound * (1.0 + 1e-12), "sample {t}: bound {bound}");
}
}
}
#[test]
fn rational_quadratic_circle_matches_the_prior_importer_tolerance() {
let weight = std::f64::consts::FRAC_1_SQRT_2;
let control = xyz(&[(1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (-1.0, 1.0), (-1.0, 0.0), (-1.0, -1.0), (0.0, -1.0), (1.0, -1.0), (1.0, 0.0)]
.map(|(x, y)| Point::new(200.0 + x * 100.0, 200.0 + y * 100.0)));
let knots = [0.0, 0.0, 0.0, 1.0, 1.0, 2.0, 2.0, 3.0, 3.0, 4.0, 4.0, 4.0];
let weights = [1.0, weight, 1.0, weight, 1.0, weight, 1.0, weight, 1.0];
let mut limit_hit = false;
let path = nurbs(2, &knots, &control, Some(&weights), &mut limit_hit).expect("circle NURBS");
assert!(!limit_hit);
let center = Point::new(200.0, 200.0);
let max_radial_error = path
.segments()
.flat_map(|segment| (0..=32).map(move |i| kurbo::ParamCurve::eval(&segment, i as f64 / 32.0)))
.map(|point| (point.distance(center) - 100.0).abs())
.fold(0.0_f64, f64::max);
assert!(max_radial_error < 0.01, "prior importer tolerance was 0.01; got {max_radial_error}");
}
#[test]
fn bounded_spline_fit_matches_prior_rational_and_high_degree_geometry() {
let weight = std::f64::consts::FRAC_1_SQRT_2;
let curves = [
vec![Homogeneous { x: 1.0, y: 0.0, w: 1.0 }, Homogeneous { x: weight, y: weight, w: weight }, Homogeneous { x: 0.0, y: 1.0, w: 1.0 }],
vec![
Homogeneous { x: 0.0, y: 0.0, w: 1.0 },
Homogeneous { x: 0.2, y: 2.0, w: 0.7 },
Homogeneous { x: 0.5, y: -2.0, w: 0.9 },
Homogeneous { x: 0.8, y: 2.0, w: 1.2 },
Homogeneous { x: 1.0, y: 0.0, w: 1.0 },
],
];
for bezier in curves {
let points: Vec<_> = bezier.iter().map(|point| point.point().expect("finite source point")).collect();
let (lo, hi) = points.iter().fold((points[0], points[0]), |(lo, hi), point| {
(Point::new(lo.x.min(point.x), lo.y.min(point.y)), Point::new(hi.x.max(point.x), hi.y.max(point.y)))
});
let tolerance = ((hi - lo).hypot() * SPLINE_TOLERANCE).max(1e-12);
let mut current = BezPath::new();
current.move_to(points[0]);
let mut limit_hit = false;
fit_spline(&mut current, &bezier, tolerance, 0, &mut limit_hit).expect("bounded fit succeeds");
assert!(!limit_hit);
let prior = legacy_path(&bezier, tolerance);
let difference = sampled_path_distance(&prior, &current, tolerance).max(sampled_path_distance(&current, &prior, tolerance));
assert!(difference <= 2.1 * tolerance, "legacy/new Hausdorff sample distance {difference} exceeds 2.1 × tolerance {tolerance}");
}
}
}