diff --git a/.gitignore b/.gitignore index 474e79de8af..b710bf22b01 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ docs/reference/src/language/builtins/structs.md .env .envrc __pycache__ + +.idea \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 60683837a3d..837d6bcc032 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -156,6 +156,7 @@ spin_on = { version = "0.1" } strum = { version = "0.26.1", default-features = false, features = ["derive"] } toml_edit = { version = "0.22.7" } ttf-parser = { version = "0.21" } +zeno = { version = "0.3.1" } raw-window-handle-06 = { package = "raw-window-handle", version = "0.6", features = ["alloc"] } diff --git a/internal/core/Cargo.toml b/internal/core/Cargo.toml index 2b9736ee5f4..0cc6d7e4f68 100644 --- a/internal/core/Cargo.toml +++ b/internal/core/Cargo.toml @@ -24,7 +24,7 @@ libm = ["num-traits/libm", "euclid/libm"] # Allow the viewer to query at runtime information about item types rtti = [] # Use the standard library -std = ["euclid/std", "once_cell/std", "scoped-tls-hkt", "lyon_path", "lyon_algorithms", "lyon_geom", "lyon_extra", "dep:web-time", "image-decoders", "svg", "raw-window-handle-06?/std", "chrono/std", "chrono/wasmbind", "chrono/clock"] +std = ["euclid/std", "once_cell/std", "scoped-tls-hkt", "lyon_path", "lyon_algorithms", "lyon_geom", "lyon_extra", "dep:web-time", "image-decoders", "svg", "raw-window-handle-06?/std", "chrono/std", "chrono/wasmbind", "chrono/clock", "tiny-skia/std", "path"] # Unsafe feature meaning that there is only one core running and all thread_local are static. # You can only enable this feature if you are sure that any API of this crate is only called # from a single core, and not in a interrupt or signal handler. @@ -38,6 +38,8 @@ software-renderer = ["bytemuck"] image-decoders = ["dep:image", "dep:clru"] svg = ["dep:resvg", "shared-fontdb"] +path = ["dep:zeno"] + box-shadow-cache = [] shared-fontdb = ["i-slint-common/shared-fontdb"] @@ -85,6 +87,7 @@ clru = { workspace = true, optional = true } resvg = { workspace = true, optional = true } fontdb = { workspace = true, optional = true } serde = { workspace = true, optional = true } +zeno = { workspace = true, optional = true } raw-window-handle-06 = { workspace = true, optional = true } bitflags = { version = "2.4.2"} diff --git a/internal/core/software_renderer.rs b/internal/core/software_renderer.rs index 5b3de6ed299..fddc91fe864 100644 --- a/internal/core/software_renderer.rs +++ b/internal/core/software_renderer.rs @@ -18,11 +18,8 @@ use crate::graphics::{ BorderRadius, PixelFormat, Rgba8Pixel, SharedImageBuffer, SharedPixelBuffer, }; use crate::item_rendering::{CachedRenderingData, DirtyRegion, RenderBorderRectangle, RenderImage}; -use crate::items::{ItemRc, TextOverflow, TextWrap}; -use crate::lengths::{ - LogicalBorderRadius, LogicalLength, LogicalPoint, LogicalRect, LogicalSize, LogicalVector, - PhysicalPx, PointLengths, RectLengths, ScaleFactor, SizeLengths, -}; +use crate::items::{ItemRc, TextOverflow, TextWrap, FillRule}; +use crate::lengths::{LogicalBorderRadius, LogicalLength, LogicalPoint, LogicalRect, LogicalSize, LogicalVector, PhysicalPx, PointLengths, RectLengths, ScaleFactor, SizeLengths}; use crate::renderer::{Renderer, RendererSealed}; use crate::textlayout::{AbstractFont, FontMetrics, TextParagraphLayout}; use crate::window::{WindowAdapter, WindowInner}; @@ -33,11 +30,11 @@ use alloc::{vec, vec::Vec}; use core::cell::{Cell, RefCell}; use core::pin::Pin; use euclid::Length; +use lyon_path::Event; use fixed::Fixed; #[allow(unused)] use num_traits::Float; use num_traits::NumCast; - pub use draw_functions::{PremultipliedRgbaColor, Rgb565Pixel, TargetPixel}; type PhysicalLength = euclid::Length; @@ -974,6 +971,15 @@ fn render_window_frame_by_line( extra_left_clip, ); } + SceneCommand::ZenoPath { zenopath_index } => { + let cmd = &scene.vectors.zeno_paths[zenopath_index as usize]; + draw_functions::draw_zeno_path_line( + &PhysicalRect { origin: span.pos, size: span.size }, + scene.current_line, + cmd, + range_buffer, + ) + } } } }, @@ -993,6 +999,7 @@ struct SceneVectors { rounded_rectangles: Vec, shared_buffers: Vec, gradients: Vec, + zeno_paths: Vec, } struct Scene { @@ -1248,10 +1255,14 @@ enum SceneCommand { RoundedRectangle { rectangle_index: u16, }, - /// rectangle_index is an index in the [`SceneVectors::rounded_gradients`] array + /// gradient_index is an index in the [`SceneVectors::rounded_gradients`] array Gradient { gradient_index: u16, }, + /// zenopath_index is an index in the [`SceneVectors::zeno_paths`] array + ZenoPath { + zenopath_index: u16, + } } struct SceneTexture<'a> { @@ -1398,6 +1409,16 @@ struct GradientCommand { bottom_clip: PhysicalLength, } +#[derive(Debug)] +struct ZenoPathCommand { + stroke_mask: Option>, + stroke_brush: Brush, + + fill_mask: Option>, + fill_brush: Brush, +} + + fn prepare_scene( window: &WindowInner, size: PhysicalSize, @@ -1478,6 +1499,7 @@ trait ProcessScene { fn process_rounded_rectangle(&mut self, geometry: PhysicalRect, data: RoundedRectangle); fn process_shared_image_buffer(&mut self, geometry: PhysicalRect, buffer: SharedBufferCommand); fn process_gradient(&mut self, geometry: PhysicalRect, gradient: GradientCommand); + fn process_path(&mut self, geometry: PhysicalRect, path: ZenoPathCommand); } struct RenderToBuffer<'a, TargetPixel> { @@ -1579,6 +1601,17 @@ impl<'a, T: TargetPixel> ProcessScene for RenderToBuffer<'a, T> { ); }); } + + fn process_path(&mut self, geometry: PhysicalRect, path: ZenoPathCommand) { + self.foreach_ranges(&geometry, |line, buffer, _extra_left_clip, _extra_right_clip| { + draw_functions::draw_zeno_path_line( + &geometry, + PhysicalLength::new(line), + &path, + buffer, + ) + }); + } } #[derive(Default)] @@ -1652,6 +1685,20 @@ impl ProcessScene for PrepareScene { }); } } + + fn process_path(&mut self, geometry: PhysicalRect, path: ZenoPathCommand) { + let size = geometry.size; + if !size.is_empty() { + let zenopath_index = self.vectors.zeno_paths.len() as u16; + self.vectors.zeno_paths.push(path); + self.items.push(SceneItem { + pos: geometry.origin, + size, + z: self.items.len() as u16, + command: SceneCommand::ZenoPath { zenopath_index }, + }) + } + } } struct SceneBuilder<'a, T> { @@ -2495,9 +2542,100 @@ impl<'a, T: ProcessScene> crate::item_rendering::ItemRenderer for SceneBuilder<' } } - #[cfg(feature = "std")] - fn draw_path(&mut self, _path: Pin<&crate::items::Path>, _: &ItemRc, _size: LogicalSize) { - // TODO + #[cfg(feature = "path")] + fn draw_path(&mut self, path: Pin<&crate::items::Path>, item: &ItemRc, size: LogicalSize) { + use zeno::PathBuilder; + let geom = LogicalRect::from(size); + + let clipped = match geom.intersection(&self.current_state.clip) { + Some(geom) => geom, + None => return, + }; + + let geometry = (clipped.translate(self.current_state.offset.to_vector()).cast() + * self.scale_factor) + .round() + .cast() + .transformed(self.rotation); + + let path_props = path.as_ref(); + let mut zeno_pb: Vec = Vec::new(); + let (logical_offset, path_events2) = path.fitted_path_events(item).unwrap(); + + for event in path_events2.iter() { + match event { + Event::Begin { at } => { + zeno_pb.move_to([at.x, at.y]); + } + Event::Line { from: _, to } => { + zeno_pb.line_to([to.x, to.y]); + } + Event::Quadratic { from: _, ctrl, to } => { + zeno_pb.quad_to( + [ctrl.x, ctrl.y], + [to.x, to.y] + ); + } + Event::Cubic { from: _, ctrl1, ctrl2, to } => { + zeno_pb.curve_to( + [ctrl1.x, ctrl1.y], + [ctrl2.x, ctrl2.y], + [to.x, to.y], + ); + } + Event::End { last: _, first: _, close } => { + if close { + zeno_pb.close(); + } + } + } + } + + let transform = Some(zeno::Transform::translation(logical_offset.x, logical_offset.y)); + + let fill_mask = if !path_props.fill().is_transparent() { + let mut mask = Vec::new(); + mask.resize((geometry.size.width * geometry.size.height) as usize, 0u8); + + let fill_rule = match path_props.fill_rule() { + FillRule::Evenodd => zeno::Fill::EvenOdd, + FillRule::Nonzero => zeno::Fill::NonZero, + }; + + zeno::Mask::new(&zeno_pb) + .transform(transform) + .style(fill_rule) + .size(geometry.size.width, geometry.size.height) + .render_into(&mut mask, None); + + Some(mask) + } else { None }; + + let stroke_mask = if !path_props.stroke().is_transparent() { + let mut mask = Vec::new(); + mask.resize((geometry.size.width * geometry.size.height) as usize, 0u8); + + let stroke_width = path_props.stroke_width().0; + + zeno::Mask::new(&zeno_pb) + .transform(transform) + .style( + zeno::Stroke::new(stroke_width) + .cap(zeno::Cap::Butt) + .join(zeno::Join::Miter) + ) + .size(geometry.size.width, geometry.size.height) + .render_into(&mut mask, None); + + Some(mask) + } else { None }; + + self.processor.process_path(geometry.cast(), ZenoPathCommand { + stroke_mask, + stroke_brush: path_props.stroke(), + fill_mask, + fill_brush: path_props.fill(), + }); } fn draw_box_shadow( @@ -2659,6 +2797,71 @@ impl<'a, T: ProcessScene> crate::item_rendering::ItemRenderer for SceneBuilder<' } } +// impl From for tiny_skia::Color { +// fn from(value: Color) -> Self { +// Self::from_rgba8( +// value.red(), +// value.green(), +// value.blue(), +// value.alpha() +// ) +// } +// } +// +// fn brush_to_paint(brush: Brush, path: tiny_skia::Path) -> Option> { +// if brush.is_transparent() { +// return None; +// } +// +// let mut paint = tiny_skia::Paint::default(); +// paint.anti_alias = true; +// +// match brush { +// Brush::SolidColor(color) => { +// paint.set_color(tiny_skia::Color::from(color)); +// } +// Brush::LinearGradient(gradient) => { +// let stops = gradient.stops().map(|stop| { +// tiny_skia::GradientStop::new(stop.position, tiny_skia::Color::from(stop.color)) +// }).collect::>(); +// +// let path_bounds = path.bounds(); +// let (start, end) = crate::graphics::line_for_angle( +// gradient.angle(), +// [path_bounds.width(), path_bounds.height()].into(), +// ); +// +// let gradient = tiny_skia::LinearGradient::new( +// tiny_skia::Point::from_xy(start.x, start.y), +// tiny_skia::Point::from_xy(end.x, end.y), +// stops, +// tiny_skia::SpreadMode::Pad, +// tiny_skia::Transform::default(), +// ).expect("could not create linear gradient shader"); +// paint.shader = gradient +// } +// Brush::RadialGradient(gradient) => { +// let stops = gradient.stops().map(|stop| { +// tiny_skia::GradientStop::new(stop.position, tiny_skia::Color::from(stop.color)) +// }).collect::>(); +// +// let path_bounds = path.bounds(); +// +// let gradient = tiny_skia::RadialGradient::new( +// tiny_skia::Point::from_xy(0.0, 0.0), +// tiny_skia::Point::from_xy(path_bounds.width(), path_bounds.height()), // TODO: fix points +// 0., // TODO: fix angle +// stops, +// tiny_skia::SpreadMode::Pad, +// tiny_skia::Transform::default(), +// ).expect("could not create radial gradient shader"); +// paint.shader = gradient +// } +// } +// +// Some(paint) +// } + /// This is a minimal adapter for a Window that doesn't have any other feature than rendering /// using the software renderer. pub struct MinimalSoftwareWindow { diff --git a/internal/core/software_renderer/draw_functions.rs b/internal/core/software_renderer/draw_functions.rs index 946a16e0c34..e0fb8aa0331 100644 --- a/internal/core/software_renderer/draw_functions.rs +++ b/internal/core/software_renderer/draw_functions.rs @@ -6,7 +6,7 @@ //! This is the module for the functions that are drawing the pixels //! on the line buffer -use super::{PhysicalLength, PhysicalRect}; +use super::{PhysicalLength, PhysicalRect, ZenoPathCommand}; use crate::graphics::{PixelFormat, Rgb8Pixel}; use crate::lengths::{PointLengths, SizeLengths}; use crate::software_renderer::fixed::Fixed; @@ -411,8 +411,8 @@ pub(super) fn draw_rounded_rectangle_line( .saturating_sub((rr.left_clip.get() + extra_left_clip) as u32) .min(width as u32) as usize ..x3.floor() - .saturating_sub((rr.left_clip.get() + extra_left_clip) as u32) - .min(width as u32) as usize], + .saturating_sub((rr.left_clip.get() + extra_left_clip) as u32) + .min(width as u32) as usize], rr.border_color, ) } @@ -612,6 +612,54 @@ pub(super) fn draw_gradient_line( } } +/// Draws a path using two mask buffers and brushes (for fill and stroke) +#[cfg(feature = "path")] +pub(super) fn draw_zeno_path_line( + span: &PhysicalRect, + line: PhysicalLength, + cmd: &ZenoPathCommand, + line_buffer: &mut [impl TargetPixel], +) { + let y = line.0; + let pixmap_y = (y - span.origin.y) as usize; + let y_idx = pixmap_y * span.size.width as usize; + + let fill_mask = cmd.fill_mask.as_ref(); + let stroke_mask = cmd.stroke_mask.as_ref(); + + let fill_color = cmd.fill_brush.color(); + let fill_color_alpha = fill_color.alpha() as f32 / 255.0; + + let stroke_color = cmd.stroke_brush.color(); + let stroke_color_alpha = stroke_color.alpha() as f32 / 255.0; + + for (pixmap_x, pix) in line_buffer.iter_mut().enumerate() { + let pixel_idx = y_idx + pixmap_x; + + if let Some(fill_mask) = fill_mask { + if let Some(&pixel) = fill_mask.get(pixel_idx) { + if pixel != 0 { + // TODO: render other type of brushes + let fill_alpha = ((pixel as f32) / 255.0) * fill_color_alpha; + let color = fill_color.with_alpha(fill_alpha); + pix.blend(PremultipliedRgbaColor::premultiply(color)); + } + } + } + + if let Some(stroke_mask) = stroke_mask { + if let Some(&pixel) = stroke_mask.get(pixel_idx) { + if pixel != 0 { + // TODO: render other type of brushes + let stroke_alpha = ((pixel as f32) / 255.0) * stroke_color_alpha; + let color = stroke_color.with_alpha(stroke_alpha); + pix.blend(PremultipliedRgbaColor::premultiply(color)); + } + } + } + } +} + /// A color whose component have been pre-multiplied by alpha /// /// The renderer operates faster on pre-multiplied color since it diff --git a/tests/cases/path/animation.slint b/tests/cases/path/animation.slint new file mode 100644 index 00000000000..61867985ffb --- /dev/null +++ b/tests/cases/path/animation.slint @@ -0,0 +1,40 @@ +export component MultiplePathTest inherits Window { + background: #222; + width: 400px; + height: 400px; + property progress; + animate progress { + duration: 1000ms; + iteration-count: -1; + } + private property radius: 0.5; + + init => { + root.progress = 1.0; + } + + path := Path { + private property progress: clamp(root.progress * 1turn, 0, 0.99999turn); + viewbox-width: 1; + viewbox-height: 1; + width: 100%; + height: 100%; + + stroke: blue; + stroke-width: 10px; + + MoveTo { + x: 0.5; + y: 0; + } + + ArcTo { + radius-x: radius; + radius-y: radius; + x: 0.5 - radius * sin(-(path.progress)); + y: 0.5 - radius * cos(-(path.progress)); + sweep: root.progress > 0; + large-arc: root.progress > 0.5; + } + } +} \ No newline at end of file diff --git a/tests/cases/path/multiple.slint b/tests/cases/path/multiple.slint new file mode 100644 index 00000000000..c6593d3e92f --- /dev/null +++ b/tests/cases/path/multiple.slint @@ -0,0 +1,136 @@ +export component MultiplePathTest inherits Window { + background: #222; + width: 400px; + height: 400px; + + VerticalLayout { + HorizontalLayout { + padding-left: 10px; + padding-right: 10px; + Path { + stroke: red; + stroke-width: 4px; + MoveTo { + x: 50; + y: 30; + } + + LineTo { + x: 80; + y: 80; + } + + LineTo { + x: 90; + y: 15; + } + + LineTo { + x: 10; + y: 70; + } + } + + Path { + stroke: hotpink; + fill: @linear-gradient(45deg, red, blue); + stroke-width: 4px; + MoveTo { + x: 50; + y: 30; + } + + LineTo { + x: 80; + y: 80; + } + + LineTo { + x: 90; + y: 15; + } + + LineTo { + x: 40; + y: 70; + } + + Close { } + } + + Path { + stroke: green; + fill: rgba(0, 255, 100, 0.2); + stroke-width: 5px; + commands: "150,15 258,77 258,202 150,265 42,202 42,77 Z"; + } + } + + HorizontalLayout { + Path { + width: 120px; + height: 70px; + stroke: darkcyan; + stroke-width: 8px; + commands: "M 100 350 q 150 -300 300 0"; + } + + Path { + width: 100px; + height: 50px; + viewbox-width: 50; + viewbox-height: 50; + viewbox-x: 0; + viewbox-y: 0; + clip: true; + stroke: yellow; + stroke-width: 4px; + MoveTo { + x: 10; + y: 10; + } + + LineTo { + x: 80; + y: 30; + } + + LineTo { + x: 30; + y: 75; + } + + LineTo { + x: 50; + y: 2; + } + } + } + } + + Path { + x: 300px; + y: 300px; + width: 80px; + height: 80px; + fill: rgba(255, 0, 255, 0.1); + stroke: @linear-gradient(10deg, blue, green, red); + stroke-width: 10px; + MoveTo { + x: 10; + y: 10; + } + + LineTo { + x: 80; + y: 30; + } + + LineTo { + x: 30; + y: 75; + } + + Close { } + } +} diff --git a/tests/screenshots/cases/software/basic/path-arc.slint b/tests/screenshots/cases/software/basic/path-arc.slint new file mode 100644 index 00000000000..7ecec18fcb6 --- /dev/null +++ b/tests/screenshots/cases/software/basic/path-arc.slint @@ -0,0 +1,18 @@ +export component TestCase inherits Window { + width: 64px; + height: 64px; + background: lightblue; + + Path { + stroke: darkcyan; + stroke-width: 8px; + + ArcTo { + x: 10; + y: 70; + radius-x: 60; + radius-y: 60; + large-arc: true; + } + } +} diff --git a/tests/screenshots/cases/software/basic/path-basic.slint b/tests/screenshots/cases/software/basic/path-basic.slint new file mode 100644 index 00000000000..6e96d33ce40 --- /dev/null +++ b/tests/screenshots/cases/software/basic/path-basic.slint @@ -0,0 +1,14 @@ +export component TestCase inherits Window { + width: 64px; + height: 64px; + background: lightblue; + + Path { + stroke: red; + stroke-width: 4px; + MoveTo { x: 10; y: 10; } + LineTo { x: 80; y: 30; } + LineTo { x: 30; y: 75; } + LineTo { x: 50; y: 2; } + } +} \ No newline at end of file diff --git a/tests/screenshots/cases/software/basic/path-command-bezier.slint b/tests/screenshots/cases/software/basic/path-command-bezier.slint new file mode 100644 index 00000000000..f0c6e8435a5 --- /dev/null +++ b/tests/screenshots/cases/software/basic/path-command-bezier.slint @@ -0,0 +1,11 @@ +export component TestCase inherits Window { + width: 64px; + height: 64px; + background: lightblue; + + Path { + stroke: darkcyan; + stroke-width: 8px; + commands: "M 100 350 q 150 -300 300 0"; + } +} diff --git a/tests/screenshots/cases/software/basic/path-command.slint b/tests/screenshots/cases/software/basic/path-command.slint new file mode 100644 index 00000000000..7e76612a9c8 --- /dev/null +++ b/tests/screenshots/cases/software/basic/path-command.slint @@ -0,0 +1,11 @@ +export component TestCase inherits Window { + width: 64px; + height: 64px; + background: lightblue; + + Path { + stroke: green; + stroke-width: 8px; + commands: "150,15 258,77 258,202 150,265 42,202 42,77 Z"; + } +} diff --git a/tests/screenshots/cases/software/basic/path-fill.slint b/tests/screenshots/cases/software/basic/path-fill.slint new file mode 100644 index 00000000000..b4e2a3e8f86 --- /dev/null +++ b/tests/screenshots/cases/software/basic/path-fill.slint @@ -0,0 +1,29 @@ +export component TestCase inherits Window { + width: 64px; + height: 64px; + background: lightblue; + Path { + fill: @linear-gradient(10deg, blue, red); + stroke: black; + stroke-width: 4px; + MoveTo { + x: 10; + y: 10; + } + + LineTo { + x: 80; + y: 30; + } + + LineTo { + x: 30; + y: 75; + } + + LineTo { + x: 40; + y: 30; + } + } +} diff --git a/tests/screenshots/cases/software/basic/path-position.slint b/tests/screenshots/cases/software/basic/path-position.slint new file mode 100644 index 00000000000..4b37e897e2e --- /dev/null +++ b/tests/screenshots/cases/software/basic/path-position.slint @@ -0,0 +1,29 @@ +export component TestCase inherits Window { + width: 64px; + height: 64px; + background: lightblue; + + Path { + x: 28px; + width: 32px; + fill: cyan; + stroke: crimson; + stroke-width: 4px; + MoveTo { + x: 10; + y: 10; + } + + LineTo { + x: 80; + y: 30; + } + + LineTo { + x: 30; + y: 75; + } + + Close { } + } +} diff --git a/tests/screenshots/cases/software/basic/path-viewbox.slint b/tests/screenshots/cases/software/basic/path-viewbox.slint new file mode 100644 index 00000000000..31827cc3f49 --- /dev/null +++ b/tests/screenshots/cases/software/basic/path-viewbox.slint @@ -0,0 +1,35 @@ +export component TestCase inherits Window { + width: 64px; + height: 64px; + background: lightblue; + Path { + width: 32px; + height: 32px; + viewbox-width: 50; + viewbox-height: 50; + viewbox-x: 0; + viewbox-y: 0; + clip: true; + stroke: yellow; + stroke-width: 4px; + MoveTo { + x: 10; + y: 10; + } + + LineTo { + x: 80; + y: 30; + } + + LineTo { + x: 30; + y: 75; + } + + LineTo { + x: 50; + y: 2; + } + } +} diff --git a/tests/screenshots/references/software/basic/path-arc.png b/tests/screenshots/references/software/basic/path-arc.png new file mode 100644 index 00000000000..9c5e1727a02 Binary files /dev/null and b/tests/screenshots/references/software/basic/path-arc.png differ diff --git a/tests/screenshots/references/software/basic/path-basic.png b/tests/screenshots/references/software/basic/path-basic.png new file mode 100644 index 00000000000..d5cd0588dd1 Binary files /dev/null and b/tests/screenshots/references/software/basic/path-basic.png differ diff --git a/tests/screenshots/references/software/basic/path-command-bezier.png b/tests/screenshots/references/software/basic/path-command-bezier.png new file mode 100644 index 00000000000..e339fbe9034 Binary files /dev/null and b/tests/screenshots/references/software/basic/path-command-bezier.png differ diff --git a/tests/screenshots/references/software/basic/path-command.png b/tests/screenshots/references/software/basic/path-command.png new file mode 100644 index 00000000000..5b8444559e8 Binary files /dev/null and b/tests/screenshots/references/software/basic/path-command.png differ diff --git a/tests/screenshots/references/software/basic/path-fill.png b/tests/screenshots/references/software/basic/path-fill.png new file mode 100644 index 00000000000..3a1a4ecee77 Binary files /dev/null and b/tests/screenshots/references/software/basic/path-fill.png differ diff --git a/tests/screenshots/references/software/basic/path-position.png b/tests/screenshots/references/software/basic/path-position.png new file mode 100644 index 00000000000..aed792ffbed Binary files /dev/null and b/tests/screenshots/references/software/basic/path-position.png differ diff --git a/tests/screenshots/references/software/basic/path-viewbox.png b/tests/screenshots/references/software/basic/path-viewbox.png new file mode 100644 index 00000000000..b825a2b7f87 Binary files /dev/null and b/tests/screenshots/references/software/basic/path-viewbox.png differ