diff --git a/CHANGELOG.md b/CHANGELOG.md index 38928b5d4..9e2de1231 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 [Unreleased]: https://github.com/linebender/vello/compare/v0.5.0...HEAD diff --git a/examples/scenes/src/test_scenes.rs b/examples/scenes/src/test_scenes.rs index 836cadf84..b173784cd 100644 --- a/examples/scenes/src/test_scenes.rs +++ b/examples/scenes/src/test_scenes.rs @@ -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( diff --git a/vello/src/scene.rs b/vello/src/scene.rs index c32ecaf91..078841bf3 100644 --- a/vello/src/scene.rs +++ b/vello/src/scene.rs @@ -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); } @@ -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::>(); - #[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 @@ -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) + } 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::>(); + #[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>, transform: Affine) { let brush = image.into(); diff --git a/vello_tests/snapshots/clip_test.png b/vello_tests/snapshots/clip_test.png index ec78edd78..a2669d1e2 100644 --- a/vello_tests/snapshots/clip_test.png +++ b/vello_tests/snapshots/clip_test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c7ef8db8960774793d3be59d29a9972966994088bacc7d7de3c09aa02b7d561a -size 13198 +oid sha256:d9ce096ce053ca5c27089bbd265beccd44f11dfe78696180094e6b4e19e2d0cb +size 60621 diff --git a/vello_tests/tests/snapshot_test_scenes.rs b/vello_tests/tests/snapshot_test_scenes.rs index 204b8ab55..c3c9a83e8 100644 --- a/vello_tests/tests/snapshot_test_scenes.rs +++ b/vello_tests/tests/snapshot_test_scenes.rs @@ -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); }