-
Notifications
You must be signed in to change notification settings - Fork 218
Avoid hardcoding a tolerance in Vello API, instead require the user to provide it #1376
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c73dfc9
bba5ce7
159457b
651818a
a69f47a
8228d5b
5b4e851
098ec2e
4904c24
8bfd288
d3b9a7e
882164e
32501e6
08ce9de
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,141 @@ | ||
| // 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.) | ||
DJMcNab marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
| }; | ||
| } | ||
|
|
||
| passthrough!(BezPath); | ||
| passthrough!(CubicBez); | ||
| passthrough!(Line); | ||
| passthrough!(QuadBez); | ||
| passthrough!(Rect); | ||
| passthrough!(Triangle); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -137,6 +137,7 @@ extern crate alloc; | |
|
|
||
| mod painter; | ||
|
|
||
| pub mod exact; | ||
| pub mod paths; | ||
| pub mod scene; | ||
| pub mod texture; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,12 +12,13 @@ | |
|
|
||
| use vello_api::{ | ||
| PaintScene, Scene, | ||
| exact::ExactPathElements, | ||
| peniko::Style, | ||
| scene::{RenderCommand, extract_integer_translation}, | ||
| texture::TextureId, | ||
| }; | ||
| use vello_common::{ | ||
| kurbo::{self, Affine, BezPath, Shape}, | ||
| kurbo::{self, Affine, BezPath}, | ||
| paint::{ImageId, ImageSource}, | ||
| peniko::{BlendMode, Brush, Color, Fill, ImageBrush}, | ||
| }; | ||
|
|
@@ -86,6 +87,7 @@ impl PaintScene for CPUScenePainter { | |
| .set_transform(push_layer_command.clip_transform); | ||
| let clip_path = if let Some(path_id) = push_layer_command.clip_path { | ||
| let path = &input_paths.meta[usize::try_from(path_id.0).unwrap()]; | ||
| // TODO: Also correctly support the case where the meta has a `Style::Stroke` | ||
| let path_end = &input_paths | ||
| .meta | ||
| .get(usize::try_from(path_id.0).unwrap() + 1) | ||
|
|
@@ -133,17 +135,27 @@ impl PaintScene for CPUScenePainter { | |
| Ok(()) | ||
| } | ||
|
|
||
| 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) { | ||
| self.render_context.set_transform(transform); | ||
| self.render_context.set_fill_rule(fill_rule); | ||
| // TODO: Tweak inner `fill_path` API to either take a `Shape` or an &[PathEl] | ||
| self.render_context.fill_path(&path.into_path(0.1)); | ||
| // However, using `to_path` avoids allocation in some cases. | ||
| // TODO: Tweak inner API to accept an `ExactPathElements` (or at least, the resultant iterator) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We probably want to just pass the iterator so we don't monomorphise across every trait impl
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe it would still in most cases be a separate impl for each |
||
| // That would avoid the superfluous allocation here. | ||
| self.render_context | ||
| .fill_path(&path.exact_path_elements().collect()); | ||
| } | ||
|
|
||
| fn stroke_path(&mut self, transform: Affine, stroke_params: &kurbo::Stroke, path: impl Shape) { | ||
| fn stroke_path( | ||
| &mut self, | ||
| transform: Affine, | ||
| stroke_params: &kurbo::Stroke, | ||
| path: &impl ExactPathElements, | ||
| ) { | ||
| self.render_context.set_transform(transform); | ||
| self.render_context.set_stroke(stroke_params.clone()); | ||
| self.render_context.stroke_path(&path.into_path(0.1)); | ||
| // TODO: As in `fill_path` | ||
| self.render_context | ||
| .stroke_path(&path.exact_path_elements().collect()); | ||
| } | ||
|
|
||
| fn set_brush( | ||
|
|
@@ -198,7 +210,7 @@ impl PaintScene for CPUScenePainter { | |
| fn push_layer( | ||
| &mut self, | ||
| clip_transform: Affine, | ||
| clip_path: Option<impl Shape>, | ||
| clip_path: Option<&impl ExactPathElements>, | ||
| blend_mode: Option<BlendMode>, | ||
| opacity: Option<f32>, | ||
| // mask: Option<Mask>, | ||
|
|
@@ -208,21 +220,23 @@ impl PaintScene for CPUScenePainter { | |
| self.render_context.set_fill_rule(Fill::NonZero); | ||
| self.render_context.set_transform(clip_transform); | ||
| self.render_context.push_layer( | ||
| clip_path.map(|it| it.into_path(0.1)).as_ref(), | ||
| // TODO: As in `fill_path` | ||
| clip_path | ||
| .map(|it| it.exact_path_elements().collect()) | ||
| .as_ref(), | ||
| blend_mode, | ||
| opacity, | ||
| None, | ||
| None, | ||
| ); | ||
| } | ||
|
|
||
| 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.render_context.set_fill_rule(Fill::NonZero); | ||
| self.render_context.set_transform(clip_transform); | ||
| self.render_context.push_clip_layer( | ||
| // TODO: Not allocate | ||
| &path.into_path(0.1), | ||
| ); | ||
| // TODO: As in `fill_path` | ||
| self.render_context | ||
| .push_clip_layer(&path.exact_path_elements().collect()); | ||
| } | ||
|
|
||
| fn pop_layer(&mut self) { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Edited by @DJMcNab to make suggestion take up less space.
Already applied suggestion
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have one concern here, which is the 0.00001 bound. I believe that would support a$$10^4$$ times zoom, rather than a 4x zoom, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've removed the
0.00001for now; we can always restore it in a follow-up.