From c73dfc95ffd0ec470e80d8956de1bf04a6685737 Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Fri, 23 Jan 2026 16:22:28 +0000 Subject: [PATCH 01/14] Add an `ExactPathElements` trait This represents shapes which can be turned into exact paths Co-Authored-By: Tom Churchman --- sparse_strips/vello_api/src/exact.rs | 137 +++++++++++++++++++++++++++ sparse_strips/vello_api/src/lib.rs | 1 + 2 files changed, 138 insertions(+) create mode 100644 sparse_strips/vello_api/src/exact.rs diff --git a/sparse_strips/vello_api/src/exact.rs b/sparse_strips/vello_api/src/exact.rs new file mode 100644 index 000000000..f6ddd5ba2 --- /dev/null +++ b/sparse_strips/vello_api/src/exact.rs @@ -0,0 +1,137 @@ +// Copyright 2025 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Turning shapes into Bézier paths without approximation. + +use kurbo::{BezPath, CubicBez, Line, PathEl, QuadBez, Rect, Segments, Shape, Triangle, segments}; +use peniko::kurbo; + +/// A generic trait for shapes that can be mapped exactly to Bézier path elements (i.e., without +/// approximation). +/// +/// The methods on [`PaintScene`](crate::PaintScene) use this trait ensure that consistent behaviour +/// is maintained when Vello API is used to render content which might be rescaled. +/// +/// This is implemented for several [`Shape`]s from Kurbo. +/// To convert a [`Shape`] which requires approximation (such as [`Circle`](kurbo::Circle) or +/// [`RoundedRect`](kurbo::RoundedRect)), you can use [`within`]. +/// Note that the returned shape will use. +/// This however requires you to provide the tolerance. +pub trait ExactPathElements { + /// The iterator returned by the [`Self::exact_path_elements`] method. + /// + /// In many cases, this will be the same iterator as [`Shape::path_elements`] + type ExactPathElementsIter<'iter>: Iterator + 'iter + where + Self: 'iter; + + /// Returns an iterator over this shape expressed as exact [`PathEl`]s; + /// that is, as exact Bézier path _elements_. + /// + /// These path elements are exact, in the sense that no approximation is required + /// to calculate them. This is not possible for all shapes, but is possible for all + /// finite polygons and other piecewise-cubic parametric curves. Some shapes will + /// need to be approximated, which [`Shape::path_elements`] does instead. + /// The [`within`] helper function allows the approximated shape to be used + /// where an exact value is required, by providing the tolerance used. + /// + /// In many cases, shapes are able to iterate their elements without + /// allocating; however creating a [`BezPath`] object always allocates. + /// If you need an owned [`BezPath`] you can use [`BezPath::from_iter`] (or + /// [`Iterator::collect`]). + fn exact_path_elements(&self) -> Self::ExactPathElementsIter<'_>; + + /// Returns an iterator over this shape expressed as exact Bézier path + /// _segments_ ([`PathSeg`]s). + /// + /// The allocation behaviour is the same as for [`ExactPathElements::exact_path_elements`]. + /// + /// [`PathSeg`]: crate::PathSeg + #[inline] + fn exact_path_segments(&self) -> Segments> { + segments(self.exact_path_elements()) + } +} + +impl<'a, T: ExactPathElements> ExactPathElements for &'a T { + type ExactPathElementsIter<'iter> + = T::ExactPathElementsIter<'iter> + where + T: 'iter, + 'a: 'iter; + + #[inline] + fn exact_path_elements(&self) -> Self::ExactPathElementsIter<'_> { + (*self).exact_path_elements() + } +} + +/// Use an approximated shape where an [`ExactPathElements`] is required, by approximating it to +/// within the given tolerance. +/// +/// This is useful for drawing shapes such as [`Circle`](kurbo::Circle)s and [`RoundedRect`](kurbo::RoundedRect)s +/// to renderers which use Vello API. +/// +/// As the user of this function, you are responsible for determining the correct tolerance for your use case. +/// A reasonable approach might be to select a tolerance which allows scaling up ("zooming in") by 8x to be +/// within your intended tolernace bound, then recomputing the [`Scene`](crate::Scene) from your base data +/// representation once that is exceeded. +/// If you know that the shape will not be scaled, you can use [`UNSCALED_TOLERANCE`]. +// TODO: Ideally, we would say something like: "the resulting path will be within a 1/10th +// of a pixel of the actual shape, which is a negligible difference for rendering" +// but we probably can't, because Kurbo doesn't document the properties of their tolerance +// (at least, I've not found where it is documented...) +// TODO: Provide as an extension trait? +pub fn within(shape: S, tolerance: f64) -> WithTolerance { + WithTolerance { shape, tolerance } +} + +/// The type used to implement [`within`]. +#[derive(Debug)] +pub struct WithTolerance { + /// The shape to be approximated. + pub shape: S, + /// The tolerance to use when approximating it. + pub tolerance: f64, +} + +impl ExactPathElements for WithTolerance { + type ExactPathElementsIter<'iter> + = S::PathElementsIter<'iter> + where + S: 'iter; + + fn exact_path_elements(&self) -> Self::ExactPathElementsIter<'_> { + self.shape.path_elements(self.tolerance) + } +} + +/// The recommended tolerance for approximating shapes which won't be rescaled as Bezier paths. +/// +/// This can be used as the tolerance parameter to [`within`], when you know that +/// a shape will be drawn at its natural size, without scaling or skew. +pub const UNSCALED_TOLERANCE: f64 = 0.1; + +// TODO: Provide an `fn ideal_tolerance(transform: Affine) -> f64` + +/// Implement `ExactPathElements` for an existing [`Shape`], which we know +// In theory, the impl is the wrong way around; instead the `Shape` impl should be in terms of `ExactPathElements`. +macro_rules! passthrough { + ($ty: ty) => { + impl ExactPathElements for $ty { + type ExactPathElementsIter<'iter> = <$ty as Shape>::PathElementsIter<'iter>; + + fn exact_path_elements(&self) -> Self::ExactPathElementsIter<'_> { + // We use a tolerance of zero here because we know this to be exact. + self.path_elements(0.) + } + } + }; +} +passthrough!(BezPath); + +passthrough!(CubicBez); +passthrough!(Line); +passthrough!(QuadBez); +passthrough!(Rect); +passthrough!(Triangle); diff --git a/sparse_strips/vello_api/src/lib.rs b/sparse_strips/vello_api/src/lib.rs index 24b1e9a3f..27fb3463b 100644 --- a/sparse_strips/vello_api/src/lib.rs +++ b/sparse_strips/vello_api/src/lib.rs @@ -137,6 +137,7 @@ extern crate alloc; mod painter; +pub mod exact; pub mod paths; pub mod scene; pub mod texture; From bba5ce774af89288fd6987b73de2fb9f0aa189a1 Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Fri, 23 Jan 2026 16:30:22 +0000 Subject: [PATCH 02/14] Use `ExactPathElements` in Vello API --- sparse_strips/vello_api/src/exact.rs | 38 ++++++++++++++++++++++---- sparse_strips/vello_api/src/painter.rs | 16 +++++++---- sparse_strips/vello_api/src/paths.rs | 19 ++++++------- sparse_strips/vello_api/src/scene.rs | 12 ++++++-- sparse_strips/vello_cpu/src/api.rs | 5 ++-- 5 files changed, 65 insertions(+), 25 deletions(-) diff --git a/sparse_strips/vello_api/src/exact.rs b/sparse_strips/vello_api/src/exact.rs index f6ddd5ba2..2349936de 100644 --- a/sparse_strips/vello_api/src/exact.rs +++ b/sparse_strips/vello_api/src/exact.rs @@ -17,7 +17,7 @@ use peniko::kurbo; /// [`RoundedRect`](kurbo::RoundedRect)), you can use [`within`]. /// Note that the returned shape will use. /// This however requires you to provide the tolerance. -pub trait ExactPathElements { +pub trait ExactPathElements: Shape { /// The iterator returned by the [`Self::exact_path_elements`] method. /// /// In many cases, this will be the same iterator as [`Shape::path_elements`] @@ -77,10 +77,8 @@ impl<'a, T: ExactPathElements> ExactPathElements for &'a T { /// within your intended tolernace bound, then recomputing the [`Scene`](crate::Scene) from your base data /// representation once that is exceeded. /// If you know that the shape will not be scaled, you can use [`UNSCALED_TOLERANCE`]. -// TODO: Ideally, we would say something like: "the resulting path will be within a 1/10th -// of a pixel of the actual shape, which is a negligible difference for rendering" -// but we probably can't, because Kurbo doesn't document the properties of their tolerance -// (at least, I've not found where it is documented...) +/// The resulting path will be within 1/10th of a pixel of the actual shape, which is a negligible +/// difference for rendering. // TODO: Provide as an extension trait? pub fn within(shape: S, tolerance: f64) -> WithTolerance { WithTolerance { shape, tolerance } @@ -106,6 +104,36 @@ impl ExactPathElements for WithTolerance { } } +impl Shape for WithTolerance { + type PathElementsIter<'iter> + = ::ExactPathElementsIter<'iter> + where + S: 'iter; + + fn path_elements(&self, _tolerance: f64) -> Self::PathElementsIter<'_> { + self.exact_path_elements() + } + + fn area(&self) -> f64 { + self.shape.area() + } + + fn perimeter(&self, accuracy: f64) -> f64 { + // TODO: Is this correct? Should it instead be based on the result of evaluating with the stored tolerance? + self.shape.perimeter(accuracy) + } + + fn winding(&self, pt: kurbo::Point) -> i32 { + // TODO: Is this correct? + self.shape.winding(pt) + } + + fn bounding_box(&self) -> Rect { + // TODO: Is this correct? + self.shape.bounding_box() + } +} + /// The recommended tolerance for approximating shapes which won't be rescaled as Bezier paths. /// /// This can be used as the tolerance parameter to [`within`], when you know that diff --git a/sparse_strips/vello_api/src/painter.rs b/sparse_strips/vello_api/src/painter.rs index 0e2134a0e..09afa5a81 100644 --- a/sparse_strips/vello_api/src/painter.rs +++ b/sparse_strips/vello_api/src/painter.rs @@ -3,9 +3,10 @@ use core::any::Any; -use peniko::kurbo::{Affine, BezPath, Rect, Shape, Stroke}; +use peniko::kurbo::{Affine, BezPath, Rect, Stroke}; use peniko::{BlendMode, Brush, Color, Fill, ImageBrush}; +use crate::exact::ExactPathElements; use crate::scene::Scene; use crate::texture::TextureId; @@ -86,13 +87,18 @@ pub trait PaintScene: Any { /// Both `shape` and the current brush will be transformed using the 2d affine `transform`. /// See the documentation on [`Fill`]'s variants for details on when you might choose a given fill rule. // It would be really nice to have images of nested paths here, to explain winding numbers. - fn fill_path(&mut self, transform: Affine, fill_rule: Fill, path: impl Shape); + fn fill_path(&mut self, transform: Affine, fill_rule: Fill, path: impl ExactPathElements); /// Stroke along `path` with the current brush, following the given stroke parameters. /// /// Both the stroked area and the current brush will be transformed using the 2d affine `transform`. /// Dashes configured in the stroke parameter will be expanded. - fn stroke_path(&mut self, transform: Affine, stroke_params: &Stroke, path: impl Shape); + fn stroke_path( + &mut self, + transform: Affine, + stroke_params: &Stroke, + path: impl ExactPathElements, + ); /// Set the current brush to `brush`. /// @@ -198,7 +204,7 @@ pub trait PaintScene: Any { fn push_layer( &mut self, clip_transform: Affine, - clip_path: Option, + clip_path: Option, blend_mode: Option, opacity: Option, ); @@ -212,7 +218,7 @@ pub trait PaintScene: Any { /// /// **However, the transforms are *not* saved or modified by the layer stack.** /// That is, the `transform` argument to this function only applies a transform to the `clip` shape. - fn push_clip_layer(&mut self, clip_transform: Affine, path: impl Shape) { + fn push_clip_layer(&mut self, clip_transform: Affine, path: impl ExactPathElements) { self.push_layer(clip_transform, Some(path), None, None); } diff --git a/sparse_strips/vello_api/src/paths.rs b/sparse_strips/vello_api/src/paths.rs index b75965ad1..00b4e2c54 100644 --- a/sparse_strips/vello_api/src/paths.rs +++ b/sparse_strips/vello_api/src/paths.rs @@ -15,10 +15,9 @@ //! for each path). use alloc::vec::Vec; -use peniko::{ - Style, - kurbo::{PathEl, Shape}, -}; +use peniko::{Style, kurbo::PathEl}; + +use crate::exact::ExactPathElements; /// The id for a single path within a given [`PathSet`]. /// This is an index into the [`meta`](PathSet::meta) field. @@ -87,13 +86,13 @@ impl PathSet { /// See the docs on [`append`](PathSet::append) for how this changes when path sets are combined. /// /// This method is generally only expected to be used by [`Scene`](crate::Scene). - pub fn prepare_shape(&mut self, shape: &impl Shape, style: impl Into