From 6f1f810cbf61074a7e50b7bd26e9b80295db2a92 Mon Sep 17 00:00:00 2001 From: Matheus Dias Date: Tue, 3 Sep 2024 23:07:39 -0300 Subject: [PATCH 1/7] software-renderer: initial path rendering implementation --- internal/core/Cargo.toml | 2 +- internal/core/software_renderer.rs | 133 ++++++++++++++++-- .../core/software_renderer/draw_functions.rs | 39 ++++- .../cases/software/basic/path-basic.slint | 14 ++ .../references/software/basic/path-basic.png | Bin 0 -> 2776 bytes 5 files changed, 178 insertions(+), 10 deletions(-) create mode 100644 tests/screenshots/cases/software/basic/path-basic.slint create mode 100644 tests/screenshots/references/software/basic/path-basic.png diff --git a/internal/core/Cargo.toml b/internal/core/Cargo.toml index 76d01a993a6..e17ccb76cb8 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"] # 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. diff --git a/internal/core/software_renderer.rs b/internal/core/software_renderer.rs index 5b3de6ed299..8b763a50e68 100644 --- a/internal/core/software_renderer.rs +++ b/internal/core/software_renderer.rs @@ -19,25 +19,25 @@ use crate::graphics::{ }; 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::lengths::{LogicalBorderRadius, LogicalLength, LogicalPoint, LogicalPx, LogicalRect, LogicalSize, LogicalVector, PhysicalPx, PointLengths, RectLengths, ScaleFactor, SizeLengths}; use crate::renderer::{Renderer, RendererSealed}; use crate::textlayout::{AbstractFont, FontMetrics, TextParagraphLayout}; use crate::window::{WindowAdapter, WindowInner}; -use crate::{Brush, Color, Coord, ImageInner, StaticTextures}; +use crate::{Brush, Color, Coord, ImageInner, PathData, StaticTextures}; use alloc::rc::{Rc, Weak}; #[cfg(not(feature = "std"))] use alloc::{vec, vec::Vec}; use core::cell::{Cell, RefCell}; use core::pin::Pin; use euclid::Length; +use lyon_path::Event; +use lyon_path::math::Point; use fixed::Fixed; #[allow(unused)] use num_traits::Float; use num_traits::NumCast; - +#[cfg(feature = "std")] +use resvg::{tiny_skia::{FillRule, Stroke, Paint, Pixmap, PathBuilder, Transform as TinySkiaTransform}}; pub use draw_functions::{PremultipliedRgbaColor, Rgb565Pixel, TargetPixel}; type PhysicalLength = euclid::Length; @@ -974,6 +974,15 @@ fn render_window_frame_by_line( extra_left_clip, ); } + SceneCommand::SkiaPixmap { pixmap_index } => { + let cmd = &scene.vectors.skia_pixmaps[pixmap_index as usize]; + draw_functions::draw_skia_pixmap_line( + &PhysicalRect { origin: span.pos, size: span.size }, + scene.current_line, + cmd, + range_buffer, + ) + } } } }, @@ -993,6 +1002,7 @@ struct SceneVectors { rounded_rectangles: Vec, shared_buffers: Vec, gradients: Vec, + skia_pixmaps: Vec, } struct Scene { @@ -1252,6 +1262,10 @@ enum SceneCommand { Gradient { gradient_index: u16, }, + /// rectangle_index is an index in the [`SceneVectors::skia_pixmap`] array + SkiaPixmap { + pixmap_index: u16, + }, } struct SceneTexture<'a> { @@ -1398,6 +1412,12 @@ struct GradientCommand { bottom_clip: PhysicalLength, } +#[derive(Debug)] +struct SkiaPixmapCommand { + pixmap: Pixmap +} + + fn prepare_scene( window: &WindowInner, size: PhysicalSize, @@ -1478,6 +1498,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, pixmap_command: SkiaPixmapCommand); } struct RenderToBuffer<'a, TargetPixel> { @@ -1579,6 +1600,17 @@ impl<'a, T: TargetPixel> ProcessScene for RenderToBuffer<'a, T> { ); }); } + + fn process_path(&mut self, geometry: PhysicalRect, pixmap_command: SkiaPixmapCommand) { + self.foreach_ranges(&geometry, |line, buffer, extra_left_clip, extra_right_clip| { + draw_functions::draw_skia_pixmap_line( + &geometry, + PhysicalLength::new(line), + &pixmap_command, + buffer, + ) + }) + } } #[derive(Default)] @@ -1652,6 +1684,20 @@ impl ProcessScene for PrepareScene { }); } } + + fn process_path(&mut self, geometry: PhysicalRect, pixmap_command: SkiaPixmapCommand) { + let size = geometry.size; + if !size.is_empty() { + let pixmap_index = self.vectors.gradients.len() as u16; + self.vectors.skia_pixmaps.push(pixmap_command); + self.items.push(SceneItem { + pos: geometry.origin, + size, + z: self.items.len() as u16, + command: SceneCommand::SkiaPixmap { pixmap_index }, + }) + } + } } struct SceneBuilder<'a, T> { @@ -2496,8 +2542,79 @@ 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 + fn draw_path(&mut self, path: Pin<&crate::items::Path>, item: &ItemRc, size: LogicalSize) { + let geom = LogicalRect::from(size); + let phys_size = geom.size_length().cast() * self.scale_factor; + + let color = path.as_ref().stroke().color(); + println!("color: {}", color); + let mut paint = Paint::default(); + paint.set_color_rgba8( + color.red(), + color.green(), + color.blue(), + color.alpha(), + ); + // paint.set_color_rgba8( + // 255, + // 255, + // 0, + // 255, + // ); + paint.anti_alias = true; + + let mut pb = PathBuilder::new(); + + let (logical_offset, path_events) = path.fitted_path_events(item).unwrap(); + + for event in path_events.iter() { + match event { + Event::Begin { at } => { + println!("event: begin"); + pb.move_to(at.x, at.y) + } + Event::Line { from: _, to } => { + println!("event: line"); + pb.line_to(to.x, to.y) + } + Event::Quadratic { from: _, ctrl, to } => { + pb.quad_to( + ctrl.x, ctrl.y, + to.x, to.y + ) + } + Event::Cubic { from: _, ctrl1, ctrl2, to } => { + pb.cubic_to( + ctrl1.x, ctrl1.y, + ctrl2.x, ctrl2.y, + to.x, to.y, + ) + } + Event::End { last: _, first: _, close } => { + if close { + pb.close() + } + } + } + } + + let mut stroke = Stroke::default(); + stroke.width = 2.0; + + let resolved_path = pb.finish().unwrap(); + + let mut pixmap = Pixmap::new(phys_size.width as u32, phys_size.width as u32).unwrap(); + pixmap.stroke_path( + &resolved_path, + &paint, + &stroke, + TinySkiaTransform::identity(), + None + ); + + self.processor.process_path(PhysicalRect::from_size(phys_size.cast()), SkiaPixmapCommand { + pixmap + }) } fn draw_box_shadow( diff --git a/internal/core/software_renderer/draw_functions.rs b/internal/core/software_renderer/draw_functions.rs index 946a16e0c34..6c37c9c38d9 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, SkiaPixmapCommand}; use crate::graphics::{PixelFormat, Rgb8Pixel}; use crate::lengths::{PointLengths, SizeLengths}; use crate::software_renderer::fixed::Fixed; @@ -612,6 +612,43 @@ pub(super) fn draw_gradient_line( } } +#[cfg(feature = "std")] +pub(super) fn draw_skia_pixmap_line( + span: &PhysicalRect, + line: PhysicalLength, + cmd: &super::SkiaPixmapCommand, + line_buffer: &mut [impl TargetPixel], +) { + let y = line.0; + let min_y = span.origin.y; + let max_y = span.origin.y + span.size.height; + + if y > min_y && y < max_y { + for (index, pix) in line_buffer.iter_mut().enumerate() { + let pixmap_x = index as u32 - span.origin.x as u32; + let pixmap_y = y as u32 - span.origin.y as u32; + + if pixmap_x > 0 && pixmap_x < span.size.width as u32 { + let pixmap = cmd.pixmap.pixel(pixmap_x, pixmap_y); + match pixmap { + Some(pixmap_pix) => { + pix.blend(PremultipliedRgbaColor { + red: pixmap_pix.red(), + green: pixmap_pix.green(), + blue: pixmap_pix.blue(), + alpha: pixmap_pix.alpha(), + }) + } + None => {} + } + } + } + } + + println!("line: {}", line.0); + println!("line_buffer sz: {}", line_buffer.len()); +} + /// A color whose component have been pre-multiplied by alpha /// /// The renderer operates faster on pre-multiplied color since it 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/references/software/basic/path-basic.png b/tests/screenshots/references/software/basic/path-basic.png new file mode 100644 index 0000000000000000000000000000000000000000..8c7674e15fd6d5d7b600f7bc658e1b12c241af79 GIT binary patch literal 2776 zcmcImZBSEZ7DhzsQmxLiEGn8zYaO;QE6X?*9WYldYt~kcR%oWHk==m5r+>EPjv z^Co}JdCxh|dCqg*6FYZ^7sf1$iHL|;xb+X2@5BE_{>+br-&L}xdb^kxUBK~Trl#h~|yJbseSxfEdnBHx< zQf0AHDe2`yZrjV8NVMMM_C|5H@&dPnhb&^ky^Qo+r5n#q>YUv=_Vs*f98cxmI!7m_ zGyAZpI?YX~1DRLNZ8Hfts#ZBzP}E*oBk%$)<8N;hrr!$F|s$D@(jv|3}yQFkE zWzkGk)I+Ls-;KWA7oY z!)7c1bq{+I>306CG*Xsh6ZM69|9Pc>P0*>2O4nZ>&6P!^(VZ!CM`WYf0ZAEZ`-ReT zq@Mj6{L|3Dl#T{}QB`O{6ujAEv|Sivf{IdCocw6KvzQ1j+r^p42vpXm~Xn`;AU@ z<0Crxe+Qq}>`25OIS^ltt7IT8XjjnSy(?f&r{2| z3|j|B)j^?_ONK%y;EDy0s#AX_9Y*4VJae$Jd)Op6bf)N__zbqgRDchN@&1R>(g!Va zuCmu)yWrMEG99rl8*(2oMxHdHk5UPDbw=CFjlPR!%wDK>CyCPry0IlmTwhh*^+VQ& zbIy2vBHbsO4mBpA_8JLyODMo>_B?;3+z1da?SG-cvMe^X4w8uE0Y3vdZh*RCKDQQx zMiHhx0cC1>fs_L52C^JZhE0v{?;l)GeW@=okK>+eQdMrG9Fli_q=+gz%|fA&aU0Mwn_UHvlv=ivMHY)OHXkO%eT?usc)(3Me>0zs0^o=~ zC}j5Xqeh}UNxszI@Ro=9C>(o{YbCRgEMmbdBukiSZ(U`29%eUVtA-MF`SDyHKSX>` z5kGPC39TJ%(V~|dTSr2~^i?t{qMF;u7y3nujH>i&^MO@9p5vDQfRw;d-J$U4j`nH! zxjeP++esnl0%#1_9i8!1`0vPPS|2Z`*JdQZg=Vf(AE7i1*1ClweR3{N%LMfi0`3!G z$glOQ&djam*6~`zu!DUI(*(TyY~mFsxC!w!wCS*6=>VkGQMl5tEqpBD7SE_nSsr=! zwx&zW^RdYKGwV&E&szqk>s6w2wCNga#+EjN=$yiu!0xlw5sMHwb8?Rb4{4Bs(bSY4 zuh@a~oJK-9L)K(t5+@oM{LZZEZ9-sNmNZ?(s1RzqnKi-9XHI6YghX=$P4qDfD z;_a7B9K+v8h14zD5+|u4Bq!5Ns!z_*udD}dL|H*Ax?g7Ik6#RC448$5m>B!Jj{XZD zn53xiz^Dl-78uKZn#Hc_7xL5=rA=Phgy%!QE7!L%$c^jFnVr2*t>y3g$JO(jcj><#wW?3-3LRFe`^# zW-NzY)kHtiw$hF00+21V9QWJw!$>rQ9mni7FcDnkp>G6?Pwwg0b1_36;q+xKcU(Jy zEU3oHVa$kDZxvt_W{uM!ax@61b|W!#ARc|NUlH0ti{J1o*Fop!hdV#-$@FT2wXt;S z&*+%g^!)_F-Oyj3*iW3`KF(rS;k`L+F(h< z35H(A;KE&jG)TTa@Bhu3YhacKbyR#FPik^)hJR&tiRVyP7rLo^+sKLxKY3Sz(v> z%b{^foJ?H3;qlFYev3m+@*{OAbShpwn}uQ{SiqY9bO>HWsh2|MOza@VbU^x?07C5G z9tsN{x*7zbXVYw1JhR}P6m@uCt1q2?X1*hftq#AndR5~}e? Date: Thu, 5 Sep 2024 22:39:34 -0300 Subject: [PATCH 2/7] software-renderer: add path rendering test case --- tests/cases/path/multiple.slint | 136 ++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 tests/cases/path/multiple.slint diff --git a/tests/cases/path/multiple.slint b/tests/cases/path/multiple.slint new file mode 100644 index 00000000000..793d555aa64 --- /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, red, green, blue); + stroke-width: 10px; + MoveTo { + x: 10; + y: 10; + } + + LineTo { + x: 80; + y: 30; + } + + LineTo { + x: 30; + y: 75; + } + + Close { } + } +} From 8450dc4e110ecd479da4db804c1c3daaca78a2f6 Mon Sep 17 00:00:00 2001 From: Matheus Dias Date: Sat, 7 Sep 2024 16:51:23 -0300 Subject: [PATCH 3/7] software-renderer: change path rendering to zeno builder --- Cargo.toml | 1 + internal/core/Cargo.toml | 5 +- internal/core/software_renderer.rs | 239 ++++++++++++------ .../core/software_renderer/draw_functions.rs | 69 ++--- 4 files changed, 207 insertions(+), 107 deletions(-) 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 e17ccb76cb8..3c05a3c058e 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", "tiny-skia/std"] +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 8b763a50e68..55d7c872f32 100644 --- a/internal/core/software_renderer.rs +++ b/internal/core/software_renderer.rs @@ -18,12 +18,12 @@ 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, LogicalPx, 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}; -use crate::{Brush, Color, Coord, ImageInner, PathData, StaticTextures}; +use crate::{Brush, Color, Coord, ImageInner, StaticTextures}; use alloc::rc::{Rc, Weak}; #[cfg(not(feature = "std"))] use alloc::{vec, vec::Vec}; @@ -31,13 +31,10 @@ use core::cell::{Cell, RefCell}; use core::pin::Pin; use euclid::Length; use lyon_path::Event; -use lyon_path::math::Point; use fixed::Fixed; #[allow(unused)] use num_traits::Float; use num_traits::NumCast; -#[cfg(feature = "std")] -use resvg::{tiny_skia::{FillRule, Stroke, Paint, Pixmap, PathBuilder, Transform as TinySkiaTransform}}; pub use draw_functions::{PremultipliedRgbaColor, Rgb565Pixel, TargetPixel}; type PhysicalLength = euclid::Length; @@ -974,9 +971,9 @@ fn render_window_frame_by_line( extra_left_clip, ); } - SceneCommand::SkiaPixmap { pixmap_index } => { - let cmd = &scene.vectors.skia_pixmaps[pixmap_index as usize]; - draw_functions::draw_skia_pixmap_line( + 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, @@ -1002,7 +999,7 @@ struct SceneVectors { rounded_rectangles: Vec, shared_buffers: Vec, gradients: Vec, - skia_pixmaps: Vec, + zeno_paths: Vec, } struct Scene { @@ -1258,14 +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, }, - /// rectangle_index is an index in the [`SceneVectors::skia_pixmap`] array - SkiaPixmap { - pixmap_index: u16, - }, + /// zenopath_index is an index in the [`SceneVectors::zeno_paths`] array + ZenoPath { + zenopath_index: u16, + } } struct SceneTexture<'a> { @@ -1413,8 +1410,12 @@ struct GradientCommand { } #[derive(Debug)] -struct SkiaPixmapCommand { - pixmap: Pixmap +struct ZenoPathCommand { + stroke_mask: Option>, + stroke_brush: Brush, + + fill_mask: Option>, + fill_brush: Brush, } @@ -1498,7 +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, pixmap_command: SkiaPixmapCommand); + fn process_path(&mut self, geometry: PhysicalRect, path: ZenoPathCommand); } struct RenderToBuffer<'a, TargetPixel> { @@ -1601,15 +1602,15 @@ impl<'a, T: TargetPixel> ProcessScene for RenderToBuffer<'a, T> { }); } - fn process_path(&mut self, geometry: PhysicalRect, pixmap_command: SkiaPixmapCommand) { - self.foreach_ranges(&geometry, |line, buffer, extra_left_clip, extra_right_clip| { - draw_functions::draw_skia_pixmap_line( + 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), - &pixmap_command, + &path, buffer, ) - }) + }); } } @@ -1685,16 +1686,16 @@ impl ProcessScene for PrepareScene { } } - fn process_path(&mut self, geometry: PhysicalRect, pixmap_command: SkiaPixmapCommand) { + fn process_path(&mut self, geometry: PhysicalRect, path: ZenoPathCommand) { let size = geometry.size; if !size.is_empty() { - let pixmap_index = self.vectors.gradients.len() as u16; - self.vectors.skia_pixmaps.push(pixmap_command); + 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::SkiaPixmap { pixmap_index }, + command: SceneCommand::ZenoPath { zenopath_index }, }) } } @@ -2541,80 +2542,99 @@ impl<'a, T: ProcessScene> crate::item_rendering::ItemRenderer for SceneBuilder<' } } - #[cfg(feature = "std")] + #[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 phys_size = geom.size_length().cast() * self.scale_factor; - - let color = path.as_ref().stroke().color(); - println!("color: {}", color); - let mut paint = Paint::default(); - paint.set_color_rgba8( - color.red(), - color.green(), - color.blue(), - color.alpha(), - ); - // paint.set_color_rgba8( - // 255, - // 255, - // 0, - // 255, - // ); - paint.anti_alias = true; - let mut pb = PathBuilder::new(); + 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 (logical_offset, path_events) = path.fitted_path_events(item).unwrap(); + 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_events.iter() { + for event in path_events2.iter() { match event { Event::Begin { at } => { - println!("event: begin"); - pb.move_to(at.x, at.y) + zeno_pb.move_to([at.x, at.y]); } Event::Line { from: _, to } => { - println!("event: line"); - pb.line_to(to.x, to.y) + zeno_pb.line_to([to.x, to.y]); } Event::Quadratic { from: _, ctrl, to } => { - pb.quad_to( - ctrl.x, ctrl.y, - to.x, to.y - ) + zeno_pb.quad_to( + [ctrl.x, ctrl.y], + [to.x, to.y] + ); } Event::Cubic { from: _, ctrl1, ctrl2, to } => { - pb.cubic_to( - ctrl1.x, ctrl1.y, - ctrl2.x, ctrl2.y, - to.x, to.y, - ) + zeno_pb.curve_to( + [ctrl1.x, ctrl1.y], + [ctrl2.x, ctrl2.y], + [to.x, to.y], + ); } Event::End { last: _, first: _, close } => { if close { - pb.close() + zeno_pb.close(); } } } } - let mut stroke = Stroke::default(); - stroke.width = 2.0; + let transform = Some(zeno::Transform::translation(logical_offset.x, logical_offset.y)); - let resolved_path = pb.finish().unwrap(); + 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 mut pixmap = Pixmap::new(phys_size.width as u32, phys_size.width as u32).unwrap(); - pixmap.stroke_path( - &resolved_path, - &paint, - &stroke, - TinySkiaTransform::identity(), - None - ); + let fill_rule = match path_props.fill_rule() { + FillRule::Evenodd => zeno::Fill::EvenOdd, + FillRule::Nonzero => zeno::Fill::NonZero, + }; - self.processor.process_path(PhysicalRect::from_size(phys_size.cast()), SkiaPixmapCommand { - pixmap - }) + 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::Square) + ) + .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( @@ -2776,6 +2796,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 6c37c9c38d9..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, SkiaPixmapCommand}; +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,41 +612,52 @@ pub(super) fn draw_gradient_line( } } -#[cfg(feature = "std")] -pub(super) fn draw_skia_pixmap_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: &super::SkiaPixmapCommand, + cmd: &ZenoPathCommand, line_buffer: &mut [impl TargetPixel], ) { let y = line.0; - let min_y = span.origin.y; - let max_y = span.origin.y + span.size.height; - - if y > min_y && y < max_y { - for (index, pix) in line_buffer.iter_mut().enumerate() { - let pixmap_x = index as u32 - span.origin.x as u32; - let pixmap_y = y as u32 - span.origin.y as u32; - - if pixmap_x > 0 && pixmap_x < span.size.width as u32 { - let pixmap = cmd.pixmap.pixel(pixmap_x, pixmap_y); - match pixmap { - Some(pixmap_pix) => { - pix.blend(PremultipliedRgbaColor { - red: pixmap_pix.red(), - green: pixmap_pix.green(), - blue: pixmap_pix.blue(), - alpha: pixmap_pix.alpha(), - }) - } - None => {} + 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)); } } } - } - println!("line: {}", line.0); - println!("line_buffer sz: {}", line_buffer.len()); + 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 From 65a80e35682320e09679a254332c8b556026e828 Mon Sep 17 00:00:00 2001 From: Matheus Dias Date: Sat, 7 Sep 2024 16:52:13 -0300 Subject: [PATCH 4/7] software-renderer: create path test cases --- tests/cases/path/animation.slint | 40 ++++++++++++++++++++++++++++++++ tests/cases/path/multiple.slint | 10 ++++---- 2 files changed, 45 insertions(+), 5 deletions(-) create mode 100644 tests/cases/path/animation.slint 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 index 793d555aa64..c6593d3e92f 100644 --- a/tests/cases/path/multiple.slint +++ b/tests/cases/path/multiple.slint @@ -107,30 +107,30 @@ export component MultiplePathTest inherits Window { } } } - + Path { x: 300px; y: 300px; width: 80px; height: 80px; fill: rgba(255, 0, 255, 0.1); - stroke: @linear-gradient(10deg, red, green, blue); + 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 { } } } From 4d87d831c3e459495686f539dba6d9d10076b85c Mon Sep 17 00:00:00 2001 From: Matheus Dias Date: Sat, 7 Sep 2024 16:53:59 -0300 Subject: [PATCH 5/7] software-renderer: create path screenshots tests --- .../cases/software/basic/path-arc.slint | 18 +++++++++ .../software/basic/path-command-bezier.slint | 11 ++++++ .../cases/software/basic/path-command.slint | 11 ++++++ .../cases/software/basic/path-fill.slint | 29 +++++++++++++++ .../cases/software/basic/path-position.slint | 29 +++++++++++++++ .../cases/software/basic/path-viewbox.slint | 35 ++++++++++++++++++ .../references/software/basic/path-arc.png | Bin 0 -> 2517 bytes .../references/software/basic/path-basic.png | Bin 2776 -> 2938 bytes .../software/basic/path-command-bezier.png | Bin 0 -> 1843 bytes .../software/basic/path-command.png | Bin 0 -> 2262 bytes .../references/software/basic/path-fill.png | Bin 0 -> 2446 bytes .../software/basic/path-position.png | Bin 0 -> 1649 bytes .../software/basic/path-viewbox.png | Bin 0 -> 1162 bytes 13 files changed, 133 insertions(+) create mode 100644 tests/screenshots/cases/software/basic/path-arc.slint create mode 100644 tests/screenshots/cases/software/basic/path-command-bezier.slint create mode 100644 tests/screenshots/cases/software/basic/path-command.slint create mode 100644 tests/screenshots/cases/software/basic/path-fill.slint create mode 100644 tests/screenshots/cases/software/basic/path-position.slint create mode 100644 tests/screenshots/cases/software/basic/path-viewbox.slint create mode 100644 tests/screenshots/references/software/basic/path-arc.png create mode 100644 tests/screenshots/references/software/basic/path-command-bezier.png create mode 100644 tests/screenshots/references/software/basic/path-command.png create mode 100644 tests/screenshots/references/software/basic/path-fill.png create mode 100644 tests/screenshots/references/software/basic/path-position.png create mode 100644 tests/screenshots/references/software/basic/path-viewbox.png 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-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 0000000000000000000000000000000000000000..9c5e1727a0229d4fa2ed282081760a5b46245bef GIT binary patch literal 2517 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 b^h(pxT>)n2+>7yLNGjTQprGy56X*X62b4p3 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5b8444559e8af9d0b0971aeb18303ccc0cf9e985 GIT binary patch literal 2262 zcmcImZB!Fy7DlUxT1kbqDa(l)EzwLeYgJO@BW+zpKBp!?5JD!CKqix!%xn_&NB!9!d(U}i z&Y5@SzV|-&xz8QVmd%BW7W{faTwL6uqCXV;DRTdPbS2D=Tvsd{)x^amG!_-C-zJ+i z`O~b^=|yMKi;}co)SC+n3d+jLc5IKBG-^cW|HBSNT?>WLQ_9LVZ7MiwK3l)Q{#Kb# zdRQtI*rP|j^_Q?FW8fZC-6W%KccF5VB(JgcHNQ3k%adty@T zzKFWFVrZR#eQ7Miu%F8=W~NSw83Pg;P$o)7#0LgPYtV_^`Tivm#V+Q#^G4aXr8Jf6 zbU2ZTmi!6KL#dh9MZSg}nNc4;S8|Hq0CA_11tu-jY?&r`Og})~Xab=#^u$~|o}(nB za*bfXU>rsh!grHB4zXm96X1`S!C}nmtLSs0L7PZ)r#84C{t@~85fcX-kFC$i>m9*o(>#=kz5qCLy z-Jc-Z7RwH(u9^1e=&+FXeTd||2Pcdk_D`l!|CHdLapr&?4H`;jy{|VPxvPD7rG)-~ z0Gqh1=}K=EvwP2(}+lT_U<$p6Z8oGN{HhZV;YR17gUM;hS@5pz6S)}m)FryfbW?|(nWgL8?s79LyM5;~TlF7)5k?%eUB z#Q(8Q)@)Pl7mvafj~@8SjovOkX=>peXxEv^Dqp;RHN!P!7|aWa>AcW6r*KRpPMjS!h4uzH1XMcN_vS4Ogre@Imo zAD2m(O)!z7)VVh9KLCc6@PC@DZ8hkQB(_qQ8~$2670+kQ_Fy~4P5`E>w`!sG)meES z$M}#_yE<+Q*)z6_*`Z@jx60kMUE}d=Rl4O-4eF_@RDEbv&^@d>*((2T36NRM?QI^U zcZjJngz5&!`PXjUhN|VQcoslvVh{IZBazoKf$G{uSvw)`wW~Ypg~NQbzUyp$;Gy;D zAG@t~DKk=@Kj}byz{|45s`gao$6ej-$TLP{@P&V^KT? z;65P#X->%+ruh0f5Z9pi6FxgmcO;1B)H*L=!43gy5`$sHOc10XASD>a7WSp8Y+`lW z6ImqUI&SZzQb4;=g-sYq6P>ukry8!D4eE}dwJIgs;8 zn*S9E^_XLPxQJPPx#^-&HPFL$I+6FL8la{3bJRgwRWDq*G*NE=aqNWr!kK{S8btRC zSUg#~SF)C=VALR+!uQ`ph~Y#dGiwN7*V>YXp4}$=Y}gx#g{ah!sfSUs!9W_NOUF8=` z{oAEpRZL#toy5`sJP!)5g;X}c`^inYYb!<^FcL{U)^&C}I!I9{&NWt1L&@?V;QL(? zMg)ZG%hyO8GYS#Z(}Q`Cw-K67X)RUE1{!`|B@?Qi*9sA0b-*}47Q+>lYXf71veypb zd&PCotQjRezngm4q{2AtpnmBIGn}xlYbX+BXpJts-z&)XOhBVJ`kJCkyR$imiS~oU zdzDzZHK@N3oeuDK0U;M0!>sNeLr7iZp2gDLqo2 z>v8k^JlDN$_&#NEyZB>wcLjUa$7Da7yweYQl+5@+`zmZ0b^RQ0XyJ^X=IQJ7QyX+A zihAw4cuS9szQXhWSrY!3)A(>MyJ=>sTV>ZuZsMy P#1(DaT=3$c392a=$0%vIOj7-Lr0g?RRc}ob$fl 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b825a2b7f87f69f259181dc4ae16bd4376831c6b GIT binary patch literal 1162 zcmcIkZA@EL7;d*2<*Q5+sg8`rY?B2I{h`o|jj=(Y!(oA0*T{g*C3YIH-T1nN2ClM^ zra@#gAssDJmu}gzI&0D(Ef=_9laNt5bQyHeFF<-b?b_Q9YFoIy*L$7*@P~mmb@#OMx&*bALpDT&!g0OVH>&cPeBwk+K%dS&N04}?pvW>-dFzB zzVerPZx2FT4o4&soqC@XfRgCk{~;~Z(*gm7A`+FAaV|mX!Q}Z0kpO4~fXaCavf^w) zk!_zQ&hl|U8-gpKYU>72UBv0?T3*^%AzNa z`-L@yh*N9b+%jnH!`r3^npKQ5&Nt#Q?l308TW?{2?bkg;`g8O%bMO9nh1$ZSCfCwE zfk!Bxg>5=}EDO>pjnP0W>)ZN_ZtzA!J}BRE02?)!w<;c8@@u4tMM*6j$?QLvSSUHv zg|0R@WU;Ym*5re+uaVN7HN*8-QWS~?Mq<`$A+*_nNSffur_S;4TWl2j!s6fDj~&A#_{KSSE8&Sg)biE;NGc;Xcu4HSZOd^?&>r#D> zd@>2J<+L3e9wHXByiQBxH2eT&PJtJV-*=e8X2Cjr(YJ%h;N$9AL-3FtWufthr&$x; z`*C3%846W2w5p}0_2YTojHPDcor|Wq_z56Khk}rD~IU0&EVzY-IQI0$zH^m1%}}B*A(X87t@72c5vuR3;sE{yPGKftDa9r z+IcsbU`)k^(})5-oJkk literal 0 HcmV?d00001 From 25de8ce38e60e8c6cd0371a6a50c49f7d0bd693e Mon Sep 17 00:00:00 2001 From: Matheus Dias Date: Sat, 7 Sep 2024 17:12:47 -0300 Subject: [PATCH 6/7] software-renderer: change stroke join --- internal/core/software_renderer.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/core/software_renderer.rs b/internal/core/software_renderer.rs index 55d7c872f32..fddc91fe864 100644 --- a/internal/core/software_renderer.rs +++ b/internal/core/software_renderer.rs @@ -2621,7 +2621,8 @@ impl<'a, T: ProcessScene> crate::item_rendering::ItemRenderer for SceneBuilder<' .transform(transform) .style( zeno::Stroke::new(stroke_width) - .cap(zeno::Cap::Square) + .cap(zeno::Cap::Butt) + .join(zeno::Join::Miter) ) .size(geometry.size.width, geometry.size.height) .render_into(&mut mask, None); From af28639f16a71cbac47fd875cb50c0edd5c28e6b Mon Sep 17 00:00:00 2001 From: Matheus Dias Date: Sat, 7 Sep 2024 17:15:50 -0300 Subject: [PATCH 7/7] chore: add .idea (rust rover) folder to gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) 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