Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
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
120 changes: 66 additions & 54 deletions vello/src/scene.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,45 +212,41 @@ impl Scene {
) {
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) => {
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, 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);
}
} 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 @@ -382,35 +378,7 @@ impl Scene {

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, shape);
if encode_result {
if let Some(brush_transform) = brush_transform
&& self
Expand All @@ -432,6 +400,50 @@ 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 and shape, including dashing, but not, e.g., the
/// shape transform. If applicable, that should be handled by the caller.
///
/// Returns `true` if a non-zero number of segments were encoded.
fn stroke_gpu_inner(&mut self, style: &Stroke, shape: &impl Shape) -> bool {
// See the note about tolerances in `Self::stroke`.
const SHAPE_TOLERANCE: f64 = 0.01;

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)
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)
};

encode_result
}

/// 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
Loading