diff --git a/frontend/src/app/main/render.cljs b/frontend/src/app/main/render.cljs index f0a9d14a0..b916688d0 100644 --- a/frontend/src/app/main/render.cljs +++ b/frontend/src/app/main/render.cljs @@ -128,11 +128,13 @@ (defn svg-raw-wrapper-factory [objects] (let [shape-wrapper (shape-wrapper-factory objects) - svg-raw-shape (svg-raw/svg-raw-shape shape-wrapper)] + svg-raw-shape (svg-raw/svg-raw-shape shape-wrapper)] (mf/fnc svg-raw-wrapper [{:keys [shape] :as props}] (let [childs (mapv #(get objects %) (:shapes shape))] (if (and (map? (:content shape)) + ;; tspan shouldn't be contained in a group or have svg defs + (not= :tspan (get-in shape [:content :tag])) (or (= :svg (get-in shape [:content :tag])) (contains? shape :svg-attrs))) [:> shape-container {:shape shape} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs index 1d6d00966..a1a451c8f 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs @@ -13,15 +13,25 @@ [app.main.ui.components.title-bar :refer [title-bar]] [app.main.ui.icons :as i] [app.util.dom :as dom] + [app.util.functions :as uf] [app.util.i18n :refer [tr]] [rumext.v2 :as mf])) (mf/defc attribute-value [{:keys [attr value on-change on-delete] :as props}] - (let [handle-change + (let [last-value (mf/use-state value) + + handle-change* (mf/use-fn - (mf/deps attr on-change) + (uf/debounce (fn [val] + (on-change attr val)) + 300)) + + handle-change + (mf/use-fn + (mf/deps attr on-change handle-change*) (fn [event] - (on-change attr (dom/get-target-val event)))) + (reset! last-value (dom/get-target-val event)) + (handle-change* (dom/get-target-val event)))) handle-delete (mf/use-fn @@ -35,7 +45,7 @@ [:div {:class (stl/css :attr-content)} [:span {:class (stl/css :attr-name)} label] [:div {:class (stl/css :attr-input)} - [:input {:value value + [:input {:value @last-value :on-change handle-change}]] [:div {:class (stl/css :attr-actions)} [:button {:class (stl/css :attr-action-btn) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index c2d42f25a..04f3d1d0d 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -7,19 +7,26 @@ (ns app.render-wasm.api "A WASM based render API" (:require + ["react-dom/server" :as rds] + [app.common.colors :as cc] + [app.common.data :as d] [app.common.data.macros :as dm] [app.common.math :as mth] [app.common.svg.path :as path] [app.common.uuid :as uuid] [app.config :as cf] + [app.main.refs :as refs] + [app.main.render :as render] [app.render-wasm.helpers :as h] [app.util.debug :as dbg] [app.util.functions :as fns] [app.util.http :as http] [app.util.webapi :as wapi] [beicon.v2.core :as rx] + [cuerdas.core :as str] [goog.object :as gobj] - [promesa.core :as p])) + [promesa.core :as p] + [rumext.v2 :as mf])) (defonce internal-frame-id nil) (defonce internal-module #js {}) @@ -28,6 +35,54 @@ (def dpr (if use-dpr? js/window.devicePixelRatio 1.0)) +;; (mf/defc svg-raw-element +;; {::mf/props :obj} +;; [{:keys [tag attrs content] :as props}] +;; [:& (name tag) attrs +;; (for [child content] +;; (if (string? child) +;; child +;; [:& svg-raw-element child]))]) + +;; (mf/defc svg-raw +;; {::mf/props :obj} +;; [{:keys [shape] :as props}] +;; (let [content (:content shape)] +;; [:svg {:version "1.1" +;; :xmlns "http://www.w3.org/2000/svg" +;; :xmlnsXlink "http://www.w3.org/1999/xlink" +;; :fill "none"} +;; (if (string? content) +;; content +;; (let [svg-attrs (:svg-attrs shape) +;; content (-> +;; (:content shape) +;; (update :attrs merge svg-attrs))] +;; (println "content" content) +;; (println "svg-attrs" svg-attrs) +;; [:& svg-raw-element content]))])) + +;; Based on app.main.render/object-svg +(mf/defc object-svg + {::mf/props :obj} + [{:keys [shape] :as props}] + (let [objects (mf/deref refs/workspace-page-objects) + shape-wrapper + (mf/with-memo [shape] + (render/shape-wrapper-factory objects))] + + [:svg {:version "1.1" + :xmlns "http://www.w3.org/2000/svg" + :xmlnsXlink "http://www.w3.org/1999/xlink" + :fill "none"} + [:& shape-wrapper {:shape shape}]])) + +(defn get-static-markup + [shape] + (-> + (mf/element object-svg #js {:shape shape}) + (rds/renderToStaticMarkup))) + ;; This should never be called from the outside. ;; This function receives a "time" parameter that we're not using but maybe in the future could be useful (it is the time since ;; the window started rendering elements so it could be useful to measure time between frames). @@ -237,16 +292,52 @@ (store-image id)))))) fills)) +;; (defn serialize-path-style +;; [style] +;; (reduce +;; (fn [acc [key value]] +;; (str/concat acc (str/kebab key) ": " value ";")) +;; "" +;; style)) + +(defn serialize-path-attrs + [svg-attrs] + (reduce + (fn [acc [key value]] + (str/concat + acc + (str/kebab key) "\0" + value "\0")) "" svg-attrs)) + +(defn set-shape-path-attrs + [attrs] + (let [style (:style attrs) + attrs (-> attrs + (dissoc :style) + (merge style)) + str (serialize-path-attrs attrs) + size (count str) + ptr (h/call internal-module "_alloc_bytes" size)] + (h/call internal-module "stringToUTF8" str ptr size) + (h/call internal-module "_set_shape_path_attrs" (count attrs)))) + (defn set-shape-path-content [content] - (let [buffer (path/content->buffer content) - size (.-byteLength buffer) - ptr (h/call internal-module "_alloc_bytes" size) + (let [buffer (path/content->buffer content) + size (.-byteLength buffer) + ptr (h/call internal-module "_alloc_bytes" size) heap (gobj/get ^js internal-module "HEAPU8") mem (js/Uint8Array. (.-buffer heap) ptr size)] (.set mem (js/Uint8Array. buffer)) (h/call internal-module "_set_shape_path_content"))) +(defn set-shape-svg-raw-content + [content] + (let [size (get-string-length content) + ptr (h/call internal-module "_alloc_bytes" size)] + (h/call internal-module "stringToUTF8" content ptr size) + (h/call internal-module "_set_shape_svg_raw_content"))) + (defn- translate-blend-mode [blend-mode] (case blend-mode @@ -290,6 +381,11 @@ (h/call internal-module "_navigate") (debounce-render-without-cache)) +(defn add-svg-fill + [svg-data] + {:fill-color (:fill svg-data) + :fill-opacity (-> svg-data (:fill-opacity "1") d/parse-double)}) + (defn set-objects [objects] (let [shapes (into [] (vals objects)) @@ -310,7 +406,8 @@ blend-mode (dm/get-prop shape :blend-mode) opacity (dm/get-prop shape :opacity) hidden (dm/get-prop shape :hidden) - content (dm/get-prop shape :content)] + content (dm/get-prop shape :content) + svg-attrs (dm/get-prop shape :svg-attrs)] (use-shape id) (set-shape-type type) @@ -319,13 +416,21 @@ (set-shape-rotation rotation) (set-shape-transform transform) (set-shape-blend-mode blend-mode) - (set-shape-children children) (set-shape-opacity opacity) (set-shape-hidden hidden) - (when (and (some? content) (= type :path)) (set-shape-path-content content)) + (set-shape-children children) + + (when (= :path type) + (set-shape-path-attrs svg-attrs) + (set-shape-path-content content)) + + (when (= :svg-raw type) + (set-shape-svg-raw-content (get-static-markup shape))) + (let [pending-fills (doall (set-shape-fills fills))] (recur (inc index) (into pending pending-fills)))) pending))] + (request-render) (when-let [pending (seq pending)] (->> (rx/from pending) @@ -340,9 +445,9 @@ :alpha true}) (defn clear-canvas - [] - ;; TODO: perform corresponding cleaning - ) + []) + ;; TODO: Perform the corresponding cleanup." + (defn resize-viewbox [width height] diff --git a/frontend/src/app/render_wasm/shape.cljs b/frontend/src/app/render_wasm/shape.cljs index 582c61103..de7752d45 100644 --- a/frontend/src/app/render_wasm/shape.cljs +++ b/frontend/src/app/render_wasm/shape.cljs @@ -121,7 +121,14 @@ :opacity (api/set-shape-opacity v) :hidden (api/set-shape-hidden v) :shapes (api/set-shape-children v) - :content (api/set-shape-path-content v) + :svg-attrs (when (= (:type self) :path) + (api/set-shape-path-attrs v)) + :content (cond + (= (:type self) :path) + (api/set-shape-path-content v) + + (= (:type self) :svg-raw) + (api/set-shape-svg-raw-content (api/get-static-markup self))) nil) ;; when something synced with wasm ;; is modified, we need to request diff --git a/render-wasm/Cargo.lock b/render-wasm/Cargo.lock index 16afbba53..c2729e0d7 100644 --- a/render-wasm/Cargo.lock +++ b/render-wasm/Cargo.lock @@ -8,6 +8,19 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -93,12 +106,35 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "edit-xml" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f955ed8607e62368a0ff8c4235d8a9b5ebb8d7dbf0139a457cf324ce5ed5d6a" +dependencies = [ + "ahash", + "encoding_rs", + "memchr", + "quick-xml", + "thiserror", + "tracing", +] + [[package]] name = "either" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -293,12 +329,24 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pin-project-lite" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" + [[package]] name = "prettyplease" version = "0.2.24" @@ -318,6 +366,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-xml" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.37" @@ -369,6 +426,7 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" name = "render" version = "0.1.0" dependencies = [ + "edit-xml", "gl", "skia-safe", "uuid", @@ -509,6 +567,26 @@ dependencies = [ "xattr", ] +[[package]] +name = "thiserror" +version = "1.0.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "toml" version = "0.8.19" @@ -543,6 +621,37 @@ dependencies = [ "winnow", ] +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] + [[package]] name = "unicode-ident" version = "1.0.13" @@ -558,6 +667,12 @@ dependencies = [ "getrandom", ] +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -671,3 +786,23 @@ name = "xml-rs" version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af4e2e2f7cba5a093896c1e150fbfe177d1883e7448200efb81d40b9d339ef26" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/render-wasm/Cargo.toml b/render-wasm/Cargo.toml index 97e8b4247..076f7d184 100644 --- a/render-wasm/Cargo.toml +++ b/render-wasm/Cargo.toml @@ -11,6 +11,7 @@ name = "render_wasm" path = "src/main.rs" [dependencies] +edit-xml = "0.1.0" gl = "0.14.0" skia-safe = { version = "0.80.1", default-features = false, features = ["gl", "svg", "textlayout", "binary-cache"]} uuid = { version = "1.11.0", features = ["v4"] } diff --git a/render-wasm/build b/render-wasm/build index 1f8c7c6e7..355d4e584 100755 --- a/render-wasm/build +++ b/render-wasm/build @@ -16,7 +16,7 @@ export EMCC_CFLAGS="--no-entry \ -sMAX_WEBGL_VERSION=2 \ -sMODULARIZE=1 \ -sEXPORT_NAME=createRustSkiaModule \ - -sEXPORTED_RUNTIME_METHODS=GL \ + -sEXPORTED_RUNTIME_METHODS=GL,stringToUTF8 \ -sEXPORT_ES6=1" EMSDK_QUIET=1 . /usr/local/emsdk/emsdk_env.sh; diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 6abf6b465..1960ba070 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -327,6 +327,21 @@ pub extern "C" fn clear_shape_fills() { } } +#[no_mangle] +pub extern "C" fn set_shape_svg_raw_content() { + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); + if let Some(shape) = state.current_shape() { + let bytes = mem::bytes(); + let svg_raw_content = String::from_utf8(bytes) + .unwrap() + .trim_end_matches('\0') + .to_string(); + shape + .set_svg_raw_content(svg_raw_content) + .expect("Failed to set svg raw content"); + } +} + #[no_mangle] pub extern "C" fn set_shape_blend_mode(mode: i32) { let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); @@ -367,6 +382,39 @@ pub extern "C" fn set_shape_path_content() { } } +// 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) { + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); + + if let Some(shape) = state.current_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); + } + } +} + fn main() { init_gl(); } diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index c768d3e24..a3c6b1124 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -1,7 +1,6 @@ -use std::collections::HashMap; - use skia::Contains; use skia_safe as skia; +use std::collections::HashMap; use uuid::Uuid; use crate::math; @@ -31,6 +30,7 @@ pub trait Renderable { fn hidden(&self) -> bool; fn clip(&self) -> bool; fn children_ids(&self) -> Vec; + fn is_recursive(&self) -> bool; } pub(crate) struct CachedSurfaceImage { @@ -188,6 +188,7 @@ impl RenderState { skia::SamplingOptions::new(skia::FilterMode::Linear, skia::MipmapMode::Nearest), Some(&paint), ); + self.drawing_surface .canvas() .clear(skia::Color::TRANSPARENT); @@ -318,43 +319,49 @@ impl RenderState { // Returns a boolean indicating if the viewbox contains the rendered shapes fn render_shape_tree(&mut self, root_id: &Uuid, tree: &HashMap) -> bool { - let element = tree.get(&root_id).unwrap(); - let mut is_complete = self.viewbox.area.contains(element.bounds()); + if let Some(element) = tree.get(&root_id) { + let mut is_complete = self.viewbox.area.contains(element.bounds()); - if !root_id.is_nil() { - if !element.bounds().intersects(self.viewbox.area) || element.hidden() { - self.render_debug_element(element, false); - // TODO: This means that not all the shapes are renderer so we - // need to call a render_all on the zoom out. - return is_complete; // TODO return is_complete or return false?? - } else { - self.render_debug_element(element, true); + if !root_id.is_nil() { + if !element.bounds().intersects(self.viewbox.area) || element.hidden() { + self.render_debug_element(element, false); + // TODO: This means that not all the shapes are renderer so we + // need to call a render_all on the zoom out. + return is_complete; // TODO return is_complete or return false?? + } else { + self.render_debug_element(element, true); + } } - } - // This is needed so the next non-children shape does not carry this shape's transform - self.final_surface.canvas().save(); - self.drawing_surface.canvas().save(); + // This is needed so the next non-children shape does not carry this shape's transform + self.final_surface.canvas().save(); + self.drawing_surface.canvas().save(); - if !root_id.is_nil() { - self.render_single_element(element); - if element.clip() { - self.drawing_surface.canvas().clip_rect( - element.bounds(), - skia::ClipOp::Intersect, - true, - ); + if !root_id.is_nil() { + self.render_single_element(element); + if element.clip() { + self.drawing_surface.canvas().clip_rect( + element.bounds(), + skia::ClipOp::Intersect, + true, + ); + } } + + // draw all the children shapes + if element.is_recursive() { + for id in element.children_ids() { + is_complete = self.render_shape_tree(&id, tree) && is_complete; + } + } + + self.final_surface.canvas().restore(); + self.drawing_surface.canvas().restore(); + + return is_complete; + } else { + eprintln!("Error: Element with root_id {root_id} not found in the tree."); + return false; } - - // draw all the children shapes - for id in element.children_ids() { - is_complete = self.render_shape_tree(&id, tree) && is_complete; - } - - self.final_surface.canvas().restore(); - self.drawing_surface.canvas().restore(); - - return is_complete; } } diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index 1b0cb4529..03f9aacab 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -1,5 +1,6 @@ use crate::math; use skia_safe as skia; +use std::collections::HashMap; use uuid::Uuid; use crate::render::{BlendMode, Renderable}; @@ -9,17 +10,20 @@ mod images; mod matrix; mod paths; mod renderable; +mod svgraw; pub use fills::*; pub use images::*; use matrix::*; pub use paths::*; +pub use svgraw::*; #[derive(Debug, Clone, PartialEq)] pub enum Kind { Rect(math::Rect), Circle(math::Rect), Path(Path), + SVGRaw(SVGRaw), } pub type Color = skia::Color; @@ -38,6 +42,7 @@ pub struct Shape { blend_mode: BlendMode, opacity: f32, hidden: bool, + svg_attrs: HashMap, } impl Shape { @@ -54,6 +59,7 @@ impl Shape { blend_mode: BlendMode::default(), opacity: 1., hidden: false, + svg_attrs: HashMap::new(), } } @@ -135,10 +141,28 @@ impl Shape { Ok(()) } + pub fn set_path_attr(&mut self, name: String, value: String) { + match &mut self.kind { + Kind::Path(_) => { + self.set_svg_attr(name, value); + } + Kind::Rect(_) | Kind::Circle(_) | Kind::SVGRaw(_) => todo!(), + }; + } + + pub fn set_svg_raw_content(&mut self, content: String) -> Result<(), String> { + self.kind = Kind::SVGRaw(SVGRaw::from_content(content)); + Ok(()) + } + pub fn set_blend_mode(&mut self, mode: BlendMode) { self.blend_mode = mode; } + pub fn set_svg_attr(&mut self, name: String, value: String) { + self.svg_attrs.insert(name, value); + } + fn to_path_transform(&self) -> Option { match self.kind { Kind::Path(_) => { diff --git a/render-wasm/src/shapes/renderable.rs b/render-wasm/src/shapes/renderable.rs index 8aaf9ced4..15be13ca4 100644 --- a/render-wasm/src/shapes/renderable.rs +++ b/render-wasm/src/shapes/renderable.rs @@ -1,4 +1,6 @@ +use edit_xml::Document; use skia_safe as skia; +use std::collections::HashMap; use uuid::Uuid; use super::{Fill, Image, Kind, Shape}; @@ -23,21 +25,38 @@ impl Renderable for Shape { surface.canvas().concat(&matrix); - for fill in self.fills().rev() { - render_fill( - surface, - images, - fill, - self.selrect, - &self.kind, - self.to_path_transform().as_ref(), - ); - } - - let mut paint = skia::Paint::default(); - paint.set_blend_mode(self.blend_mode.into()); - paint.set_alpha_f(self.opacity); - + match &self.kind { + Kind::SVGRaw(sr) => render_svg(&sr.content.to_string(), surface, font_provider), + _ => { + // let svg_canvas_required = + // matches!(&self.kind, Kind::Path(_)) && !self.svg_attrs.is_empty(); + // if svg_canvas_required { + // let svg_canvas = build_svg_canvas(self.selrect); + // render_fills_for_kind( + // self, + // &svg_canvas, + // images, + // self.to_path_transform().as_ref(), + // ); + // render_svg_path_attrs( + // svg_canvas, + // &self.svg_attrs, + // self.selrect, + // surface, + // font_provider, + // ); + // } else { + let canvas = surface.canvas(); + render_fills_for_kind( + self, + &canvas, + images, + self.to_path_transform().as_ref(), + &self.svg_attrs, + ); + // } + } + }; Ok(()) } @@ -64,22 +83,68 @@ impl Renderable for Shape { fn children_ids(&self) -> Vec { self.children.clone() } + + fn is_recursive(&self) -> bool { + !matches!(self.kind, Kind::SVGRaw(_)) + } +} + +fn render_fills_for_kind( + shape: &Shape, + canvas: &skia::Canvas, + images: &ImageStore, + path_transform: Option<&skia::Matrix>, + svg_attrs: &HashMap, +) { + for fill in shape.fills().rev() { + render_fill( + canvas, + images, + fill, + shape.selrect, + &shape.kind, + path_transform, + svg_attrs, + ); + } + + //TODO: remove when strokes are implemented, this is just for testing paths with no fills + if shape.fills().len() == 0 { + if let Kind::Path(ref path) = shape.kind { + let mut p = skia::Paint::default(); + p.set_style(skia_safe::PaintStyle::Stroke); + p.set_stroke_width(2.0); + p.set_anti_alias(true); + p.set_blend_mode(skia::BlendMode::SrcOver); + + if let Some("round") = svg_attrs.get("stroke-linecap").map(String::as_str) { + p.set_stroke_cap(skia::paint::Cap::Round); + } + if let Some("round") = svg_attrs.get("stroke-linejoin").map(String::as_str) { + p.set_stroke_join(skia::paint::Join::Round); + } + let mut skia_path = &mut path.to_skia_path(); + skia_path = skia_path.transform(path_transform.unwrap()); + canvas.draw_path(&skia_path, &p); + } + } } fn render_fill( - surface: &mut skia::Surface, + canvas: &skia::Canvas, images: &ImageStore, fill: &Fill, selrect: Rect, kind: &Kind, path_transform: Option<&skia::Matrix>, + svg_attrs: &HashMap, ) { match (fill, kind) { (Fill::Image(image_fill), kind) => { let image = images.get(&image_fill.id()); if let Some(image) = image { draw_image_in_container( - surface.canvas(), + canvas, &image, image_fill.size(), kind, @@ -90,16 +155,22 @@ fn render_fill( } } (_, Kind::Rect(rect)) => { - surface.canvas().draw_rect(rect, &fill.to_paint(&selrect)); + canvas.draw_rect(rect, &fill.to_paint(&selrect)); } (_, Kind::Circle(rect)) => { - surface.canvas().draw_oval(rect, &fill.to_paint(&selrect)); + canvas.draw_oval(rect, &fill.to_paint(&selrect)); } (_, Kind::Path(path)) => { - surface.canvas().draw_path( - &path.to_skia_path().transform(path_transform.unwrap()), - &fill.to_paint(&selrect), - ); + let mut skia_path = &mut path.to_skia_path(); + skia_path = skia_path.transform(path_transform.unwrap()); + if let Some("evenodd") = svg_attrs.get("fill-rule").map(String::as_str) { + skia_path.set_fill_type(skia::PathFillType::EvenOdd); + } + + canvas.draw_path(&skia_path, &fill.to_paint(&selrect)); + } + (_, Kind::SVGRaw(_sr)) => { + // NOOP } } } @@ -162,6 +233,9 @@ pub fn draw_image_in_container( true, ); } + Kind::SVGRaw(_) => { + canvas.clip_rect(container, skia::ClipOp::Intersect, true); + } } // Draw the image with the calculated destination rectangle @@ -170,3 +244,70 @@ pub fn draw_image_in_container( // Restore the canvas to remove the clipping canvas.restore(); } + +fn render_svg( + svg: &str, + surface: &mut skia::Surface, + font_provider: &skia::textlayout::TypefaceFontProvider, +) { + let font_manager = skia::FontMgr::from(font_provider.clone()); + let dom_result = skia::svg::Dom::from_str(svg, font_manager); + + match dom_result { + Ok(dom) => { + dom.render(surface.canvas()); + } + Err(e) => { + eprintln!("Error parsing SVG. Error: {}", e); + } + } +} + +fn build_svg_canvas(selrect: Rect) -> skia::svg::Canvas { + let canvas = skia::svg::Canvas::new( + skia::Rect::from_size(( + selrect.right - selrect.left + 1., + selrect.bottom - selrect.top + 1., + )), + None, + ); + // SVG canvas needs positive sizes + canvas.concat(&skia::Matrix::translate(skia::Point::new( + -selrect.left, + -selrect.top, + ))); + + canvas +} + +fn render_svg_path_attrs( + canvas: skia::svg::Canvas, + svg_attrs: &HashMap, + selrect: Rect, + surface: &mut skia_safe::Surface, + font_provider: &skia::textlayout::TypefaceFontProvider, +) { + let svg_data = canvas.end(); + let svg = String::from_utf8_lossy(svg_data.as_bytes()); + + let mut doc = Document::parse_str(&svg).unwrap(); + let root = doc.root_element().unwrap(); + + for path in root.find_all(&doc, "path") { + for (name, value) in svg_attrs { + path.set_attribute(&mut doc, name, value); + } + } + + let svg_mod = doc.write_str().unwrap(); + let dom = + skia::svg::Dom::from_str(svg_mod, skia::FontMgr::from(font_provider.clone())).unwrap(); + + surface + .canvas() + .concat(&skia::Matrix::translate(skia::Point::new( + selrect.left, + selrect.top, + ))); + dom.render(surface.canvas()); +} diff --git a/render-wasm/src/shapes/svgraw.rs b/render-wasm/src/shapes/svgraw.rs new file mode 100644 index 000000000..408a92d2a --- /dev/null +++ b/render-wasm/src/shapes/svgraw.rs @@ -0,0 +1,10 @@ +#[derive(Debug, Clone, PartialEq)] +pub struct SVGRaw { + pub content: String, +} + +impl SVGRaw { + pub fn from_content(svg: String) -> SVGRaw { + SVGRaw { content: svg } + } +}