From 7c7a127ec84498f83ead2703c5c791bfc5c5cdc1 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Mon, 2 Nov 2026 03:09:08 -0800 Subject: [PATCH] SVG: a thin ellipse far from the origin imports with standard handles An ellipse whose radius is smaller than the point tolerance at its coordinates, but at least two f32 steps, now imports as the four-anchor ellipse instead of usvg's curves with rounded handles. --- crates/svg/src/import/convert.rs | 10 ++++++---- crates/svg/tests/it/basic_shapes.rs | 10 ++++++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/crates/svg/src/import/convert.rs b/crates/svg/src/import/convert.rs index e640880e7..2a777b342 100644 --- a/crates/svg/src/import/convert.rs +++ b/crates/svg/src/import/convert.rs @@ -156,14 +156,16 @@ pub(super) fn drawn_ellipse(p: &usvg::tiny_skia_path::Path) -> Option let (x0, x1) = ends.iter().fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), q| (lo.min(q.x), hi.max(q.x))); let (y0, y1) = ends.iter().fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), q| (lo.min(q.y), hi.max(q.y))); let (c, rx, ry) = (Point::new((x0 + x1) / 2.0, (y0 + y1) / 2.0), (x1 - x0) / 2.0, (y1 - y0) / 2.0); - // usvg works in f32: points match within a few of its steps at the size of the coordinates. - let tol = 16.0 * f64::from(f32::EPSILON) * [x0, x1, y0, y1].iter().fold(1.0f64, |m, v| m.max(v.abs())); + // usvg works in f32: points match within a few of its steps at the size of the coordinates, and + // a radius of fewer than two steps does not survive the conversion. + let step = f64::from(f32::EPSILON) * [x0, x1, y0, y1].iter().fold(1.0f64, |m, v| m.max(v.abs())); + let tol = 16.0 * step; let axes = [Point::new(x1, c.y), Point::new(c.x, y1), Point::new(x0, c.y), Point::new(c.x, y0)]; let off = 1e-3 + 4.0 * tol / rx.min(ry); let on_ellipse = |q: &Point| (((q.x - c.x) / rx).powi(2) + ((q.y - c.y) / ry).powi(2) - 1.0).abs() <= off; if !closed - || rx <= tol - || ry <= tol + || rx < 2.0 * step + || ry < 2.0 * step || ends.first().is_none_or(|s| s.distance(axes[0]) > tol) || !axes.iter().all(|a| ends.iter().any(|q| q.distance(*a) <= tol)) || !ends.iter().all(on_ellipse) diff --git a/crates/svg/tests/it/basic_shapes.rs b/crates/svg/tests/it/basic_shapes.rs index 8d76739b4..da759b197 100644 --- a/crates/svg/tests/it/basic_shapes.rs +++ b/crates/svg/tests/it/basic_shapes.rs @@ -164,9 +164,10 @@ fn circles_and_ellipses_import_as_the_ellipse_drawn() { - "#; + + "#; let back = paths(&import(svg).unwrap()); - assert_eq!(back.len(), 4); + assert_eq!(back.len(), 5); let circle = &back[0].subpaths[0].anchors; let want: Vec = [(100.0, 70.0), (60.0, 110.0), (20.0, 70.0), (60.0, 30.0)].map(|(x, y)| Point::new(x, y)).into(); assert_eq!(circle.len(), 4); @@ -182,6 +183,11 @@ fn circles_and_ellipses_import_as_the_ellipse_drawn() { // A small circle far from the origin too. let far = &back[3].subpaths[0].anchors; assert!(far.len() == 4 && far[0].p.distance(Point::new(40_000_400.0, 0.0)) < 8.0, "{far:?}"); + // A radius of a few of f32's steps at its coordinates, with handles of the standard length. + let thin = &back[4].subpaths[0].anchors; + let handle = thin.first().map(|a| a.h_out - a.p); + assert!(thin.len() == 4 && thin[0].p.distance(Point::new(1_000_036.0, 1_000_000.0)) < 0.25, "{thin:?}"); + assert!(handle.is_some_and(|h| h.x.abs() < 1e-6 && (h.y - shapes::KAPPA).abs() < 1e-6), "{thin:?}"); } #[test]