Improve setting svg attrs in wasm

This commit is contained in:
Alejandro Alonso 2025-10-23 14:22:22 +02:00 committed by Alonso Torres
parent dba718b850
commit 479ce99b32
14 changed files with 1554 additions and 73 deletions

File diff suppressed because it is too large Load Diff

View File

@ -211,3 +211,20 @@ test("Renders a file with a closed path shape with multiple segments using strok
await expect(workspace.canvas).toHaveScreenshot();
});
test("Renders a file with paths and svg attrs", async ({
page,
}) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockGetFile("render-wasm/get-file-svg-attrs.json");
await workspace.goToWorkspace({
id: "4732f3e3-7a1a-807e-8006-ff76066e631d",
pageId: "4732f3e3-7a1a-807e-8006-ff76066e631e",
});
await workspace.waitForFirstRender();
await expect(workspace.canvas).toHaveScreenshot();
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -370,12 +370,11 @@
(dissoc :style)
(merge style)
(select-keys allowed-keys))
str (sr/serialize-path-attrs attrs)
size (count str)]
(when (pos? size)
(let [offset (mem/alloc size)]
(h/call wasm/internal-module "stringToUTF8" str offset size)
(h/call wasm/internal-module "_set_shape_path_attrs" (count attrs))))))
fill-rule (-> attrs :fillRule sr/translate-fill-rule)
stroke-linecap (-> attrs :strokeLinecap sr/translate-stroke-linecap)
stroke-linejoin (-> attrs :strokeLinejoin sr/translate-stroke-linejoin)
fill-none (= "none" (-> attrs :fill))]
(h/call wasm/internal-module "_set_shape_svg_attrs" fill-rule stroke-linecap stroke-linejoin fill-none)))
(defn set-shape-path-content
"Upload path content in chunks to WASM."
@ -1160,7 +1159,10 @@
:text-direction (unchecked-get module "RawTextDirection")
:text-decoration (unchecked-get module "RawTextDecoration")
:text-transform (unchecked-get module "RawTextTransform")
:segment-data (unchecked-get module "RawSegmentData")}]
:segment-data (unchecked-get module "RawSegmentData")
:stroke-linecap (unchecked-get module "RawStrokeLineCap")
:stroke-linejoin (unchecked-get module "RawStrokeLineJoin")
:fill-rule (unchecked-get module "RawFillRule")}]
(set! wasm/serializers serializers)
(default))))
(p/fmap (fn [default]

View File

@ -63,6 +63,24 @@
default (unchecked-get values "rect")]
(d/nilv (unchecked-get values (d/name type)) default)))
(defn translate-stroke-linecap
[stroke-linecap]
(let [values (unchecked-get wasm/serializers "stroke-linecap")
default (unchecked-get values "butt")]
(d/nilv (unchecked-get values (d/name stroke-linecap)) default)))
(defn translate-stroke-linejoin
[stroke-linejoin]
(let [values (unchecked-get wasm/serializers "stroke-linejoin")
default (unchecked-get values "miter")]
(d/nilv (unchecked-get values (d/name stroke-linejoin)) default)))
(defn translate-fill-rule
[fill-rule]
(let [values (unchecked-get wasm/serializers "fill-rule")
default (unchecked-get values "nonzero")]
(d/nilv (unchecked-get values (d/name fill-rule)) default)))
(defn translate-stroke-style
[stroke-style]
(let [values (unchecked-get wasm/serializers "stroke-style")

View File

@ -160,6 +160,38 @@ Stroke styles are serialized as `u8`:
| 3 | Mixed |
| \_ | Solid |
## Fill rules
Fill rules are serialized as `u8`
| Value | Field |
| ----- | ------ |
| 0 | Nonzero |
| 1 | Evenodd |
| \_ | Nonzero |
## Stroke linecaps
Stroke linecaps are serialized as `u8`
| Value | Field |
| ----- | ------ |
| 0 | Butt |
| 1 | Round |
| 2 | Square |
| \_ | Butt |
## Stroke linejoins
Stroke linejoins are serialized as `u8`
| Value | Field |
| ----- | ------ |
| 0 | Miter |
| 1 | Round |
| 2 | Bevel |
| \_ | Miter |
## Bool Operations
Bool operations (`bool-type`) are serialized as `u8`:

View File

@ -764,14 +764,9 @@ impl RenderState {
&shape
};
let has_fill_none = matches!(
shape.svg_attrs.get("fill").map(String::as_str),
Some("none")
);
if shape.fills.is_empty()
&& !matches!(shape.shape_type, Type::Group(_))
&& !has_fill_none
&& !shape.svg_attrs.fill_none
{
if let Some(fills_to_render) = self.nested_fills.last() {
let fills_to_render = fills_to_render.clone();

View File

@ -1,8 +1,8 @@
use std::collections::HashMap;
use crate::math::{Matrix, Point, Rect};
use crate::shapes::{Corners, Fill, ImageFill, Path, Shape, Stroke, StrokeCap, StrokeKind, Type};
use crate::shapes::{
Corners, Fill, ImageFill, Path, Shape, Stroke, StrokeCap, StrokeKind, SvgAttrs, Type,
};
use skia_safe::{self as skia, ImageFilter, RRect};
use super::{RenderState, SurfaceId};
@ -17,7 +17,7 @@ fn draw_stroke_on_rect(
rect: &Rect,
selrect: &Rect,
corners: &Option<Corners>,
svg_attrs: &HashMap<String, String>,
svg_attrs: &SvgAttrs,
scale: f32,
shadow: Option<&ImageFilter>,
blur: Option<&ImageFilter>,
@ -53,7 +53,7 @@ fn draw_stroke_on_circle(
stroke: &Stroke,
rect: &Rect,
selrect: &Rect,
svg_attrs: &HashMap<String, String>,
svg_attrs: &SvgAttrs,
scale: f32,
shadow: Option<&ImageFilter>,
blur: Option<&ImageFilter>,
@ -130,7 +130,7 @@ pub fn draw_stroke_on_path(
path: &Path,
selrect: &Rect,
path_transform: Option<&Matrix>,
svg_attrs: &HashMap<String, String>,
svg_attrs: &SvgAttrs,
scale: f32,
shadow: Option<&ImageFilter>,
blur: Option<&ImageFilter>,
@ -217,7 +217,7 @@ fn handle_stroke_caps(
selrect: &Rect,
canvas: &skia::Canvas,
is_open: bool,
svg_attrs: &HashMap<String, String>,
svg_attrs: &SvgAttrs,
scale: f32,
blur: Option<&ImageFilter>,
antialias: bool,

View File

@ -21,6 +21,7 @@ mod rects;
mod shadows;
mod shape_to_path;
mod strokes;
mod svg_attrs;
mod svgraw;
mod text;
pub mod text_paths;
@ -41,6 +42,7 @@ pub use rects::*;
pub use shadows::*;
pub use shape_to_path::*;
pub use strokes::*;
pub use svg_attrs::*;
pub use svgraw::*;
pub use text::*;
pub use transform::*;
@ -174,7 +176,7 @@ pub struct Shape {
pub opacity: f32,
pub hidden: bool,
pub svg: Option<skia::svg::Dom>,
pub svg_attrs: HashMap<String, String>,
pub svg_attrs: SvgAttrs,
pub shadows: Vec<Shadow>,
pub layout_item: Option<LayoutItem>,
pub extrect: OnceCell<math::Rect>,
@ -201,7 +203,7 @@ impl Shape {
hidden: false,
blur: None,
svg: None,
svg_attrs: HashMap::new(),
svg_attrs: SvgAttrs::default(),
shadows: Vec::with_capacity(1),
layout_item: None,
extrect: OnceCell::new(),
@ -566,15 +568,6 @@ impl Shape {
};
}
pub fn set_path_attr(&mut self, name: String, value: String) {
match self.shape_type {
Type::Path(_) | Type::Bool(_) => {
self.set_svg_attr(name, value);
}
_ => unreachable!("This shape should have path attrs"),
};
}
pub fn set_svg_raw_content(&mut self, content: String) -> Result<(), String> {
self.shape_type = Type::SVGRaw(SVGRaw::from_content(content));
Ok(())
@ -607,10 +600,6 @@ impl Shape {
self.svg = Some(svg);
}
pub fn set_svg_attr(&mut self, name: String, value: String) {
self.svg_attrs.insert(name, value);
}
pub fn blend_mode(&self) -> BlendMode {
self.blend_mode
}
@ -1104,7 +1093,7 @@ impl Shape {
if let Some(path_transform) = self.to_path_transform() {
skia_path.transform(&path_transform);
}
if let Some("evenodd") = self.svg_attrs.get("fill-rule").map(String::as_str) {
if self.svg_attrs.fill_rule == FillRule::Evenodd {
skia_path.set_fill_type(skia::PathFillType::EvenOdd);
}
Some(skia_path)

View File

@ -1,8 +1,10 @@
use crate::shapes::fills::{Fill, SolidColor};
use skia_safe::{self as skia, Rect};
use std::collections::HashMap;
use super::Corners;
use super::StrokeLineCap;
use super::StrokeLineJoin;
use super::SvgAttrs;
#[derive(Debug, Clone, PartialEq, Copy)]
pub enum StrokeStyle {
@ -159,7 +161,7 @@ impl Stroke {
pub fn to_paint(
&self,
rect: &Rect,
svg_attrs: &HashMap<String, String>,
svg_attrs: &SvgAttrs,
scale: f32,
antialias: bool,
) -> skia::Paint {
@ -175,11 +177,11 @@ impl Stroke {
paint.set_stroke_width(width);
paint.set_anti_alias(antialias);
if let Some("round") = svg_attrs.get("stroke-linecap").map(String::as_str) {
if svg_attrs.stroke_linecap == StrokeLineCap::Round {
paint.set_stroke_cap(skia::paint::Cap::Round);
}
if let Some("round") = svg_attrs.get("stroke-linejoin").map(String::as_str) {
if svg_attrs.stroke_linejoin == StrokeLineJoin::Round {
paint.set_stroke_join(skia::paint::Join::Round);
}
@ -225,7 +227,7 @@ impl Stroke {
&self,
is_open: bool,
rect: &Rect,
svg_attrs: &HashMap<String, String>,
svg_attrs: &SvgAttrs,
scale: f32,
antialias: bool,
) -> skia::Paint {
@ -249,7 +251,7 @@ impl Stroke {
&self,
is_open: bool,
rect: &Rect,
svg_attrs: &HashMap<String, String>,
svg_attrs: &SvgAttrs,
scale: f32,
antialias: bool,
) -> skia::Paint {

View File

@ -0,0 +1,49 @@
#[derive(Debug, Clone, PartialEq, Copy, Default)]
pub enum FillRule {
#[default]
Nonzero,
Evenodd,
}
#[derive(Debug, Clone, PartialEq, Copy, Default)]
pub enum StrokeLineCap {
#[default]
Butt,
Round,
Square,
}
#[derive(Debug, Clone, PartialEq, Copy, Default)]
pub enum StrokeLineJoin {
#[default]
Miter,
Round,
Bevel,
}
#[derive(Debug, Clone, PartialEq, Copy)]
pub struct SvgAttrs {
pub fill_rule: FillRule,
pub stroke_linecap: StrokeLineCap,
pub stroke_linejoin: StrokeLineJoin,
/// Indicates that this shape has an explicit `fill="none"` attribute.
///
/// In SVG, the `fill` attribute is inheritable from container elements like `<g>`.
/// However, when a shape explicitly sets `fill="none"`, it breaks the color
/// inheritance chain - the shape will not inherit fill colors from parent containers.
///
/// This is different from having an empty fills array, as it explicitly signals
/// the intention to have no fill, preventing inheritance.
pub fill_none: bool,
}
impl Default for SvgAttrs {
fn default() -> Self {
Self {
fill_rule: FillRule::Nonzero,
stroke_linecap: StrokeLineCap::Butt,
stroke_linejoin: StrokeLineJoin::Miter,
fill_none: false,
}
}
}

View File

@ -7,4 +7,5 @@ pub mod paths;
pub mod shadows;
pub mod shapes;
pub mod strokes;
pub mod svg_attrs;
pub mod text;

View File

@ -225,37 +225,6 @@ pub extern "C" fn current_to_path() -> *mut u8 {
mem::write_vec(result)
}
// Extracts a string from the bytes slice until the next null byte (0) and returns the result as a `String`.
// Updates the `start` index to the end of the extracted string.
fn extract_string(start: &mut usize, bytes: &[u8]) -> String {
match bytes[*start..].iter().position(|&b| b == 0) {
Some(pos) => {
let end = *start + pos;
let slice = &bytes[*start..end];
*start = end + 1; // Move the `start` pointer past the null byte
// Call to unsafe function within an unsafe block
unsafe { String::from_utf8_unchecked(slice.to_vec()) }
}
None => {
*start = bytes.len(); // Move `start` to the end if no null byte is found
String::new()
}
}
}
#[no_mangle]
pub extern "C" fn set_shape_path_attrs(num_attrs: u32) {
with_current_shape_mut!(state, |shape: &mut Shape| {
let bytes = mem::bytes();
let mut start = 0;
for _ in 0..num_attrs {
let name = extract_string(&mut start, &bytes);
let value = extract_string(&mut start, &bytes);
shape.set_path_attr(name, value);
}
});
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -0,0 +1,95 @@
use macros::ToJs;
use crate::shapes::{FillRule, StrokeLineCap, StrokeLineJoin};
use crate::{with_current_shape_mut, STATE};
#[derive(PartialEq, ToJs)]
#[repr(u8)]
#[allow(dead_code)]
pub enum RawFillRule {
Nonzero = 0,
Evenodd = 1,
}
impl From<u8> for RawFillRule {
fn from(value: u8) -> Self {
unsafe { std::mem::transmute(value) }
}
}
impl From<RawFillRule> for FillRule {
fn from(value: RawFillRule) -> Self {
match value {
RawFillRule::Nonzero => FillRule::Nonzero,
RawFillRule::Evenodd => FillRule::Evenodd,
}
}
}
#[derive(PartialEq, ToJs)]
#[repr(u8)]
#[allow(dead_code)]
pub enum RawStrokeLineCap {
Butt = 0,
Round = 1,
Square = 2,
}
impl From<u8> for RawStrokeLineCap {
fn from(value: u8) -> Self {
unsafe { std::mem::transmute(value) }
}
}
impl From<RawStrokeLineCap> for StrokeLineCap {
fn from(value: RawStrokeLineCap) -> Self {
match value {
RawStrokeLineCap::Butt => StrokeLineCap::Butt,
RawStrokeLineCap::Round => StrokeLineCap::Round,
RawStrokeLineCap::Square => StrokeLineCap::Square,
}
}
}
#[derive(PartialEq, ToJs)]
#[repr(u8)]
#[allow(dead_code)]
pub enum RawStrokeLineJoin {
Miter = 0,
Round = 1,
Bevel = 2,
}
impl From<u8> for RawStrokeLineJoin {
fn from(value: u8) -> Self {
unsafe { std::mem::transmute(value) }
}
}
impl From<RawStrokeLineJoin> for StrokeLineJoin {
fn from(value: RawStrokeLineJoin) -> Self {
match value {
RawStrokeLineJoin::Miter => StrokeLineJoin::Miter,
RawStrokeLineJoin::Round => StrokeLineJoin::Round,
RawStrokeLineJoin::Bevel => StrokeLineJoin::Bevel,
}
}
}
#[no_mangle]
pub extern "C" fn set_shape_svg_attrs(
fill_rule: u8,
stroke_linecap: u8,
stroke_linejoin: u8,
fill_none: bool,
) {
with_current_shape_mut!(state, |shape: &mut Shape| {
let fill_rule = RawFillRule::from(fill_rule);
shape.svg_attrs.fill_rule = fill_rule.into();
let stroke_linecap = RawStrokeLineCap::from(stroke_linecap);
shape.svg_attrs.stroke_linecap = stroke_linecap.into();
let stroke_linejoin = RawStrokeLineJoin::from(stroke_linejoin);
shape.svg_attrs.stroke_linejoin = stroke_linejoin.into();
shape.svg_attrs.fill_none = fill_none;
});
}