Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ This release has an [MSRV][] of 1.88.
### Changed

- Breaking change: wgpu has been updated to wgpu 27. ([#1280][] by [@theoparis][])
- Breaking change: Make `Scene` clip / layers honor fill rule (even-odd clips). ([#1332][] by [@waywardmonkeys][])
When pushing a layer, you should use `Fill::NonZero` as the clip fill rule to achieve the same behavior as previous versions.
- Breaking change: Allow setting `Scene` layer clip shape drawing style, adding even-odd filled path clipping and stroked path clipping to the various scene layer methods (`Scene::{push_layer, push_luminance_mask_layer, push_clip_layer}`). ([#1332][] by [@waywardmonkeys][], [#1342][] by [@tomcur][])
When pushing a layer, you should use `Fill::NonZero` as the clip draw style to achieve the same behavior as previous versions.

### Fixed

Expand Down Expand Up @@ -386,6 +386,7 @@ This release has an [MSRV][] of 1.75.
[#1280]: https://github.com/linebender/vello/pull/1280
[#1323]: https://github.com/linebender/vello/pull/1323
[#1332]: https://github.com/linebender/vello/pull/1332
[#1342]: https://github.com/linebender/vello/pull/1342

<!-- Note that this still comparing against 0.5.0, because 0.5.1 is a cherry-picked patch -->
[Unreleased]: https://github.com/linebender/vello/compare/v0.5.0...HEAD
Expand Down
40 changes: 40 additions & 0 deletions examples/scenes/src/test_scenes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1673,6 +1673,46 @@ mod impls {
);
scene.pop_layer();

// Dashed stroke clip demo: clip to the stroked outline of a path.
let stroke_demo_rect = Rect::new(250.0, 460.0, 450.0, 660.0);
scene.fill(
Fill::NonZero,
Affine::IDENTITY,
palette::css::LIGHT_GREEN,
None,
&stroke_demo_rect,
);
let mut stroke_star = BezPath::new();
let center = Point::new(350.0, 560.0);
let outer_r = 85.0;
let start_angle = -std::f64::consts::FRAC_PI_2;
let pts: [Point; 5] = core::array::from_fn(|i| {
let a = start_angle + (i as f64) * (2.0 * std::f64::consts::PI / 5.0);
center + Vec2::new(a.cos() * outer_r, a.sin() * outer_r)
});
let order = [0_usize, 2, 4, 1, 3];
stroke_star.move_to(pts[order[0]]);
for &idx in &order[1..] {
stroke_star.line_to(pts[idx]);
}
stroke_star.close_path();
let mut stroke = Stroke::new(5.0);
stroke.dash_pattern = [10.].into_iter().collect();
stroke.join = Join::Round;
stroke.start_cap = Cap::Round;
stroke.end_cap = Cap::Round;
scene.push_clip_layer(&stroke, Affine::IDENTITY, &stroke_star);
let grad = Gradient::new_linear((250.0, 460.0), (450.0, 660.0))
.with_stops([palette::css::MAGENTA, palette::css::CYAN]);
scene.fill(
Fill::NonZero,
Affine::IDENTITY,
&grad,
None,
&stroke_demo_rect,
);
scene.pop_layer();

let large_background_rect = Rect::new(-1000.0, -1000.0, 2000.0, 2000.0);
let inside_clip_rect = Rect::new(11.0, 13.399999999999999, 59.0, 56.6);
let outside_clip_rect = Rect::new(
Expand Down
131 changes: 71 additions & 60 deletions vello/src/scene.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,47 +210,43 @@ impl Scene {
transform: Affine,
clip: &impl Shape,
) {
let t = Transform::from_kurbo(&transform);
self.encoding.encode_transform(t);
let (is_fill, stroke_for_estimate) = match clip_style {
// The logic for encoding the clip shape differs between fill and stroke style clips, but
// the logic is otherwise similar.
//
// `encoded_result` will be `true` if and only if a valid path has been encoded. If it is
// `false`, we will need to explicitly encode a valid empty path.
let encoded_result = match clip_style {
StyleRef::Fill(fill) => {
let t = Transform::from_kurbo(&transform);
self.encoding.encode_transform(t);
self.encoding.encode_fill_style(fill);
(true, None)
#[cfg(feature = "bump_estimate")]
self.estimator.count_path(clip.path_elements(0.1), &t, None);
self.encoding.encode_shape(clip, true)
}
StyleRef::Stroke(stroke) => {
let encoded_stroke = self.encoding.encode_stroke_style(stroke);
(false, encoded_stroke.then_some(stroke))
if stroke.width == 0. {
// If the stroke has zero width, encode a fill style and indicate no path was
// encoded.
self.encoding.encode_fill_style(Fill::NonZero);
false
} else {
self.stroke_gpu_inner(stroke, transform, clip)
}
}
};
if stroke_for_estimate.is_none() && matches!(clip_style, StyleRef::Stroke(_)) {
// If the stroke has zero width, encode a valid empty path. This suppresses
// all drawing until the layer is popped.
self.encoding.encode_fill_style(Fill::NonZero);
self.encoding.encode_empty_shape();
#[cfg(feature = "bump_estimate")]
{
use peniko::kurbo::PathEl;
let path = [PathEl::MoveTo(Point::ZERO), PathEl::LineTo(Point::ZERO)];
self.estimator.count_path(path.into_iter(), &t, None);
}
self.encoding.encode_begin_clip(parameters);
return;
}

if !self.encoding.encode_shape(clip, is_fill) {
// If the layer shape is invalid, encode a valid empty path. This suppresses
// all drawing until the layer is popped.
if !encoded_result {
// If the layer shape is invalid or a zero-width stroke, encode a valid empty path.
// This suppresses all drawing until the layer is popped.
self.encoding.encode_empty_shape();
#[cfg(feature = "bump_estimate")]
{
use peniko::kurbo::PathEl;
let path = [PathEl::MoveTo(Point::ZERO), PathEl::LineTo(Point::ZERO)];
self.estimator.count_path(path.into_iter(), &t, None);
self.estimator
.count_path(path.into_iter(), &Transform::IDENTITY, None);
}
} else {
#[cfg(feature = "bump_estimate")]
self.estimator
.count_path(clip.path_elements(0.1), &t, stroke_for_estimate);
}
self.encoding.encode_begin_clip(parameters);
}
Expand Down Expand Up @@ -379,38 +375,7 @@ impl Scene {
if style.width == 0. {
return;
}

let t = Transform::from_kurbo(&transform);
self.encoding.encode_transform(t);
let encoded_stroke = self.encoding.encode_stroke_style(style);
debug_assert!(encoded_stroke, "Stroke width is non-zero");

// We currently don't support dashing on the GPU. If the style has a dash pattern, then
// we convert it into stroked paths on the CPU and encode those as individual draw
// objects.
let encode_result = if style.dash_pattern.is_empty() {
#[cfg(feature = "bump_estimate")]
self.estimator
.count_path(shape.path_elements(SHAPE_TOLERANCE), &t, Some(style));
self.encoding.encode_shape(shape, false)
} else {
// TODO: We currently collect the output of the dash iterator because
// `encode_path_elements` wants to consume the iterator. We want to avoid calling
// `dash` twice when `bump_estimate` is enabled because it internally allocates.
// Bump estimation will move to resolve time rather than scene construction time,
// so we can revert this back to not collecting when that happens.
let dashed = peniko::kurbo::dash(
shape.path_elements(SHAPE_TOLERANCE),
style.dash_offset,
&style.dash_pattern,
)
.collect::<Vec<_>>();
#[cfg(feature = "bump_estimate")]
self.estimator
.count_path(dashed.iter().copied(), &t, Some(style));
self.encoding
.encode_path_elements(dashed.into_iter(), false)
};
let encode_result = self.stroke_gpu_inner(style, transform, shape);
if encode_result {
if let Some(brush_transform) = brush_transform
&& self
Expand All @@ -432,6 +397,52 @@ impl Scene {
}
}

/// Encodes the stroke of a shape using the specified style. The stroke style must have
/// non-zero width.
///
/// This handles encoding the stroke style (including dashing), transform, and shape.
///
/// Returns `true` if a non-zero number of segments were encoded.
fn stroke_gpu_inner(&mut self, style: &Stroke, transform: Affine, shape: &impl Shape) -> bool {
// See the note about tolerances in `Self::stroke`.
const SHAPE_TOLERANCE: f64 = 0.01;

let t = Transform::from_kurbo(&transform);
self.encoding.encode_transform(t);
let encoded_stroke = self.encoding.encode_stroke_style(style);
debug_assert!(encoded_stroke, "Stroke width is non-zero");

// We currently don't support dashing on the GPU. If the style has a dash pattern, then
// we convert it into stroked paths on the CPU and encode those as individual draw
// objects.
//
// Note both branches return a boolean indicating whether a non-zero number of segments
// were encoded.
if style.dash_pattern.is_empty() {
#[cfg(feature = "bump_estimate")]
self.estimator
.count_path(shape.path_elements(SHAPE_TOLERANCE), &t, Some(style));
self.encoding.encode_shape(shape, false)
Comment on lines +422 to +425
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't want to tackle (or think about) tolerances here, and this is just lifted directly from the code in main, but I'm not entirely sure this is correct... The Estimator::count_path call gets SHAPE_TOLERANCE passed in (with a value of 0.01), but the Encoding::encode_shape call has a hard-coded 0.1 tolerance.

Additionally, as the note about tolerances in Scene::stroke says, this does nothing for handling tolerance under scaling.

} else {
// TODO: We currently collect the output of the dash iterator because
// `encode_path_elements` wants to consume the iterator. We want to avoid calling
// `dash` twice when `bump_estimate` is enabled because it internally allocates.
// Bump estimation will move to resolve time rather than scene construction time,
// so we can revert this back to not collecting when that happens.
let dashed = peniko::kurbo::dash(
shape.path_elements(SHAPE_TOLERANCE),
style.dash_offset,
&style.dash_pattern,
)
.collect::<Vec<_>>();
#[cfg(feature = "bump_estimate")]
self.estimator
.count_path(dashed.iter().copied(), &t, Some(style));
self.encoding
.encode_path_elements(dashed.into_iter(), false)
}
}

/// Draws an image at its natural size with the given transform.
pub fn draw_image<'b>(&mut self, image: impl Into<ImageBrushRef<'b>>, transform: Affine) {
let brush = image.into();
Expand Down
4 changes: 2 additions & 2 deletions vello_tests/snapshots/clip_test.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion vello_tests/tests/snapshot_test_scenes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ fn snapshot_many_clips() {
#[cfg_attr(skip_gpu_tests, ignore)]
fn snapshot_clip_test() {
let test_scene = test_scenes::clip_test();
let params = TestParams::new("clip_test", 512, 512);
let params = TestParams::new("clip_test", 512, 768);
snapshot_test_scene(test_scene, params);
}

Expand Down