blob: d02c610087eeaa3b463917050433a9f6f3cbe5fe [file]
// 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, QuadBez, Rect, Segments, Shape, Triangle, segments};
use peniko::kurbo::{self, PathEl};
/// 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 [`Shape`]s from Kurbo that can be exactly mapped to Bézier path elements.
/// To convert a [`Shape`] which requires approximation (such as [`Circle`](kurbo::Circle) or
/// [`RoundedRect`](kurbo::RoundedRect)), you can use [`within`].
/// This however requires you to provide the tolerance.
/// See the docs of `within` for more details.
///
/// It is a requirement of this trait that [`Shape::path_elements`] returns the same iterator
/// as [`ExactPathElements::exact_path_elements`] for any provided tolerance value.
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<Item = PathEl> + '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`]: kurbo::PathSeg
#[inline]
fn exact_path_segments(&self) -> Segments<Self::ExactPathElementsIter<'_>> {
segments(self.exact_path_elements())
}
}
impl<T: ExactPathElements> ExactPathElements for &T {
type ExactPathElementsIter<'iter>
= T::ExactPathElementsIter<'iter>
where
Self: '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.
///
/// *WARNING*: Unlike [`ExactPathElements`], which will produce correct renderings for any scale, this
/// approximation is only valid for a fixed range of transforms.
///
/// 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 4x (for example;
/// you should evaluate the correct value yourself) and remaining within your intended tolerance bound.
/// If the user zoomed past that limit, you would 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`].
/// The resulting path will be within 1/10th of a pixel of the actual shape, which is a negligible
/// difference for rendering.
///
/// This is useful for drawing shapes such as [`Circle`](kurbo::Circle)s and [`RoundedRect`](kurbo::RoundedRect)s.
// TODO: Provide as an extension trait?
pub fn within<S: Shape>(shape: S, tolerance: f64) -> WithTolerance<S> {
WithTolerance { shape, tolerance }
}
/// The type used to implement [`within`].
#[derive(Debug, Clone, Copy)]
pub struct WithTolerance<S: Shape> {
/// The shape to be approximated.
pub shape: S,
/// The tolerance to use when approximating it.
pub tolerance: f64,
}
impl<S: Shape> ExactPathElements for WithTolerance<S> {
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 will not be approximated in Kurbo.
// 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>
where
$ty: '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);