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
9 changes: 7 additions & 2 deletions crates/anyrender/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,10 +187,13 @@ pub trait PaintScene: RenderContext {
fn reset(&mut self);

/// Pushes a new layer clipped by the specified shape and composed with previous layers using the specified blend mode.
/// The fill rule determines which parts of the clip shape are inside the clipping region.
/// Every drawing command after this call will be clipped by the shape until the layer is popped.
/// However, the transforms are not saved or modified by the layer stack.
#[allow(clippy::too_many_arguments)]
fn push_layer(
&mut self,
fill: Fill,
blend: impl Into<BlendMode>,
alpha: f32,
transform: Affine,
Expand All @@ -200,9 +203,10 @@ pub trait PaintScene: RenderContext {
);

/// Pushes a new clip layer clipped by the specified shape.
/// The fill rule determines which parts of the clip shape are inside the clipping region.
/// Every drawing command after this call will be clipped by the shape until the layer is popped.
/// However, the transforms are not saved or modified by the layer stack.
fn push_clip_layer(&mut self, transform: Affine, clip: &impl Shape);
fn push_clip_layer(&mut self, fill: Fill, transform: Affine, clip: &impl Shape);

/// Pops the current layer.
fn pop_layer(&mut self);
Expand Down Expand Up @@ -261,6 +265,7 @@ pub trait PaintScene: RenderContext {
for cmd in scene.commands {
match cmd {
RenderCommand::PushLayer(cmd) => self.push_layer(
cmd.fill,
cmd.blend,
cmd.alpha,
scene_transform * cmd.transform,
Expand All @@ -269,7 +274,7 @@ pub trait PaintScene: RenderContext {
cmd.backdrop_filter,
),
RenderCommand::PushClipLayer(cmd) => {
self.push_clip_layer(scene_transform * cmd.transform, &cmd.clip)
self.push_clip_layer(cmd.fill, scene_transform * cmd.transform, &cmd.clip)
}
RenderCommand::PopLayer => self.pop_layer(),
RenderCommand::Stroke(cmd) => self.stroke(
Expand Down
9 changes: 8 additions & 1 deletion crates/anyrender/src/null_backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ impl PaintScene for NullScenePainter {

fn push_layer(
&mut self,
_fill: peniko::Fill,
_blend: impl Into<peniko::BlendMode>,
_alpha: f32,
_transform: kurbo::Affine,
Expand All @@ -107,7 +108,13 @@ impl PaintScene for NullScenePainter {
) {
}

fn push_clip_layer(&mut self, _transform: kurbo::Affine, _clip: &impl kurbo::Shape) {}
fn push_clip_layer(
&mut self,
_fill: peniko::Fill,
_transform: kurbo::Affine,
_clip: &impl kurbo::Shape,
) {
}

fn pop_layer(&mut self) {}

Expand Down
14 changes: 12 additions & 2 deletions crates/anyrender/src/recording.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ impl RenderCommand {
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct LayerCommand {
#[cfg_attr(feature = "serde", serde(default))]
pub fill: Fill,
pub blend: BlendMode,
pub alpha: f32,
pub transform: Affine,
Expand All @@ -70,6 +72,8 @@ pub struct LayerCommand {
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ClipCommand {
#[cfg_attr(feature = "serde", serde(default))]
pub fill: Fill,
pub transform: Affine,
#[cfg_attr(feature = "serde", serde(with = "svg_path"))]
pub clip: BezPath, // TODO: more shape options
Expand Down Expand Up @@ -178,6 +182,7 @@ impl PaintScene for Scene {

fn push_layer(
&mut self,
fill: Fill,
blend: impl Into<BlendMode>,
alpha: f32,
transform: Affine,
Expand All @@ -188,6 +193,7 @@ impl PaintScene for Scene {
let blend = blend.into();
let clip = clip.into_path(self.tolerance);
let layer = LayerCommand {
fill,
blend,
alpha,
transform,
Expand All @@ -198,9 +204,13 @@ impl PaintScene for Scene {
self.commands.push(RenderCommand::PushLayer(layer));
}

fn push_clip_layer(&mut self, transform: Affine, clip: &impl Shape) {
fn push_clip_layer(&mut self, fill: Fill, transform: Affine, clip: &impl Shape) {
let clip = clip.into_path(self.tolerance);
let layer = ClipCommand { transform, clip };
let layer = ClipCommand {
fill,
transform,
clip,
};
self.commands.push(RenderCommand::PushClipLayer(layer));
}

Expand Down
41 changes: 39 additions & 2 deletions crates/anyrender_serialize/tests/serialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,45 @@ fn test_empty_scene_roundtrip() {
assert_scene_roundtrip(&Scene::new());
}

#[test]
fn legacy_clips_default_to_nonzero() {
let mut scene = Scene::new();
let clip = Rect::new(0.0, 0.0, 100.0, 100.0);
scene.push_layer(
Fill::NonZero,
Mix::Normal,
1.0,
Affine::IDENTITY,
&clip,
None,
None,
);
scene.push_clip_layer(Fill::NonZero, Affine::IDENTITY, &clip);
let archive = SceneArchive::from_scene(&scene, &default_config()).unwrap();
for command in &archive.commands {
let mut value = serde_json::to_value(command).unwrap();
let fields = value
.as_object_mut()
.unwrap()
.values_mut()
.next()
.unwrap()
.as_object_mut()
.unwrap();
assert!(fields.remove("fill").is_some());
let restored: SerializableRenderCommand = serde_json::from_value(value).unwrap();
assert_eq!(&restored, command);
}
}

/// Tests that all non-image and non-font command types survive a roundtrip.
#[test]
fn test_all_command_types_roundtrip() {
let mut scene = Scene::new();

// Layer with blend mode
scene.push_layer(
Fill::EvenOdd,
Mix::Multiply,
0.75,
Affine::translate((5.0, 5.0)),
Expand Down Expand Up @@ -71,7 +103,11 @@ fn test_all_command_types_roundtrip() {
);

// Clip layer
scene.push_clip_layer(Affine::scale(1.5), &Rect::new(50.0, 50.0, 150.0, 150.0));
scene.push_clip_layer(
Fill::EvenOdd,
Affine::scale(1.5),
&Rect::new(50.0, 50.0, 150.0, 150.0),
);
scene.fill(
Fill::NonZero,
Affine::IDENTITY,
Expand All @@ -83,6 +119,7 @@ fn test_all_command_types_roundtrip() {

// Layer with compose blend mode
scene.push_layer(
Fill::NonZero,
Compose::SrcOver,
1.0,
Affine::IDENTITY,
Expand Down Expand Up @@ -450,7 +487,7 @@ fn assert_glyph_run_preserved(restored: &Scene) {
match &restored.commands[0] {
RenderCommand::GlyphRun(glyph_run) => {
assert_eq!(glyph_run.font_size, 16.0);
assert_eq!(glyph_run.hint, false);
assert!(!glyph_run.hint);
assert_eq!(glyph_run.brush_alpha, 1.0);
assert_eq!(glyph_run.transform, Affine::translate((10.0, 50.0)));
assert_eq!(glyph_run.glyph_transform, None);
Expand Down
19 changes: 13 additions & 6 deletions crates/anyrender_skia/src/scene.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,15 +113,16 @@ impl SkiaScenePainter<'_> {
self.inner.concat(&sk_kurbo::matrix_from_affine(transform));
}

fn clip(&self, shape: &impl kurbo::Shape) {
fn clip(&self, fill: peniko::Fill, shape: &impl kurbo::Shape) {
if let Some(rect) = shape.as_rect() {
self.inner.clip_rect(sk_kurbo::rect_from(rect), None, true);
} else if let Some(rrect) = shape.as_rounded_rect() {
self.inner
.clip_rrect(sk_kurbo::rrect_from(rrect), None, true);
} else {
self.inner
.clip_path(&sk_kurbo::path_from_shape(shape), None, true);
let mut path = sk_kurbo::path_from_shape(shape);
path.set_fill_type(sk_peniko::path_fill_type_from_fill(fill));
self.inner.clip_path(&path, None, true);
}
}

Expand Down Expand Up @@ -426,6 +427,7 @@ impl PaintScene for SkiaScenePainter<'_> {

fn push_layer(
&mut self,
fill: peniko::Fill,
blend: impl Into<peniko::BlendMode>,
alpha: f32,
transform: kurbo::Affine,
Expand All @@ -446,7 +448,7 @@ impl PaintScene for SkiaScenePainter<'_> {
self.inner.save();

self.set_matrix(transform);
self.clip(clip);
self.clip(fill, clip);

let backdrop_filter = backdrop_filter.as_ref().and_then(|f| convert_filter(f));
let mut save_layer_rec = SaveLayerRec::default().paint(&self.cache.paint);
Expand All @@ -457,11 +459,16 @@ impl PaintScene for SkiaScenePainter<'_> {
self.inner.save_layer(&save_layer_rec);
}

fn push_clip_layer(&mut self, transform: kurbo::Affine, clip: &impl kurbo::Shape) {
fn push_clip_layer(
&mut self,
fill: peniko::Fill,
transform: kurbo::Affine,
clip: &impl kurbo::Shape,
) {
self.inner.save(); // we need to do two saves because of pop_layer

self.set_matrix(transform);
self.clip(clip);
self.clip(fill, clip);
self.inner.save();
}

Expand Down
58 changes: 55 additions & 3 deletions crates/anyrender_svg/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,60 @@ pub fn render_svg_tree_with<S: PaintScene, F: FnMut(&mut S, &usvg::Node)>(

#[cfg(test)]
mod tests {
// CI will fail unless cargo nextest can execute at least one test per workspace.
// Delete this dummy test once we have an actual real test.
use super::*;
use anyrender::recording::{RenderCommand, Scene};
use peniko::Fill;

fn clip_rule(clip_attrs: &str, path_attrs: &str) -> Fill {
let svg = format!(
r##"<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<defs><clipPath id="clip" {clip_attrs}>
<path {path_attrs} d="M0 0H100V100H0Z M25 25H75V75H25Z"/>
</clipPath></defs>
<rect width="100" height="100" clip-path="url(#clip)"/>
</svg>"##
);
let mut scene = Scene::new();
render_svg_str(&mut scene, &svg, Affine::IDENTITY).unwrap();
scene
.commands
.iter()
.find_map(|command| match command {
RenderCommand::PushLayer(layer) => Some(layer.fill),
_ => None,
})
.expect("SVG should produce a clipping layer")
}

#[test]
fn clip_rule_attribute_is_preserved() {
assert_eq!(clip_rule("", ""), Fill::NonZero);
assert_eq!(clip_rule("", r#"clip-rule="nonzero""#), Fill::NonZero);
assert_eq!(clip_rule("", r#"clip-rule="evenodd""#), Fill::EvenOdd);
}

#[test]
fn clip_rule_style_overrides_attribute_and_inherits() {
assert_eq!(
clip_rule("", r#"clip-rule="nonzero" style="clip-rule: evenodd""#),
Fill::EvenOdd
);
assert_eq!(
clip_rule(r#"style="clip-rule: evenodd""#, ""),
Fill::EvenOdd
);
assert_eq!(
clip_rule(r#"clip-rule="evenodd""#, r#"style="clip-rule: nonzero""#),
Fill::NonZero
);
}

#[test]
fn dummy_test_until_we_have_a_real_test() {}
fn fill_rule_does_not_control_clipping() {
assert_eq!(clip_rule("", r#"fill-rule="evenodd""#), Fill::NonZero);
assert_eq!(
clip_rule("", r#"fill-rule="nonzero" clip-rule="evenodd""#),
Fill::EvenOdd
);
}
}
5 changes: 5 additions & 0 deletions crates/anyrender_svg/src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ pub(crate) fn render_group<S: PaintScene, F: FnMut(&mut S, &usvg::Node)>(
Some(usvg::Node::Path(clip_path)) => {
let local_path = util::to_bez_path(clip_path);
scene.push_layer(
match clip_path.fill().map(|fill| fill.rule()) {
Some(usvg::FillRule::EvenOdd) => Fill::EvenOdd,
_ => Fill::NonZero,
},
BlendMode {
mix,
compose: peniko::Compose::SrcOver,
Expand Down Expand Up @@ -61,6 +65,7 @@ pub(crate) fn render_group<S: PaintScene, F: FnMut(&mut S, &usvg::Node)>(
.transform_rect_bbox(rect)
.inflate(2.0, 2.0);
scene.push_layer(
Fill::NonZero,
BlendMode {
mix,
compose: peniko::Compose::SrcOver,
Expand Down
8 changes: 4 additions & 4 deletions crates/anyrender_vello/src/scene.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,19 +68,19 @@ impl PaintScene for VelloScenePainter<'_, '_> {

fn push_layer(
&mut self,
fill: Fill,
blend: impl Into<BlendMode>,
alpha: f32,
transform: Affine,
clip: &impl Shape,
_filter: Option<Arc<Filter>>,
_backdrop_filter: Option<Arc<Filter>>,
) {
self.inner
.push_layer(Fill::NonZero, blend, alpha, transform, clip);
self.inner.push_layer(fill, blend, alpha, transform, clip);
}

fn push_clip_layer(&mut self, transform: Affine, clip: &impl Shape) {
self.inner.push_clip_layer(Fill::NonZero, transform, clip);
fn push_clip_layer(&mut self, fill: Fill, transform: Affine, clip: &impl Shape) {
self.inner.push_clip_layer(fill, transform, clip);
}

fn pop_layer(&mut self) {
Expand Down
Loading
Loading