0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-06 14:50:20 -05:00

🎉 Basic wasm support for svg attrs and svg defs

This commit is contained in:
Alejandro Alonso 2024-12-26 13:16:37 +01:00
parent c0cfa8dc42
commit f509edc151
12 changed files with 564 additions and 74 deletions

View file

@ -128,11 +128,13 @@
(defn svg-raw-wrapper-factory (defn svg-raw-wrapper-factory
[objects] [objects]
(let [shape-wrapper (shape-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 (mf/fnc svg-raw-wrapper
[{:keys [shape] :as props}] [{:keys [shape] :as props}]
(let [childs (mapv #(get objects %) (:shapes shape))] (let [childs (mapv #(get objects %) (:shapes shape))]
(if (and (map? (:content 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])) (or (= :svg (get-in shape [:content :tag]))
(contains? shape :svg-attrs))) (contains? shape :svg-attrs)))
[:> shape-container {:shape shape} [:> shape-container {:shape shape}

View file

@ -13,15 +13,25 @@
[app.main.ui.components.title-bar :refer [title-bar]] [app.main.ui.components.title-bar :refer [title-bar]]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.functions :as uf]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(mf/defc attribute-value [{:keys [attr value on-change on-delete] :as props}] (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/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] (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 handle-delete
(mf/use-fn (mf/use-fn
@ -35,7 +45,7 @@
[:div {:class (stl/css :attr-content)} [:div {:class (stl/css :attr-content)}
[:span {:class (stl/css :attr-name)} label] [:span {:class (stl/css :attr-name)} label]
[:div {:class (stl/css :attr-input)} [:div {:class (stl/css :attr-input)}
[:input {:value value [:input {:value @last-value
:on-change handle-change}]] :on-change handle-change}]]
[:div {:class (stl/css :attr-actions)} [:div {:class (stl/css :attr-actions)}
[:button {:class (stl/css :attr-action-btn) [:button {:class (stl/css :attr-action-btn)

View file

@ -7,19 +7,26 @@
(ns app.render-wasm.api (ns app.render-wasm.api
"A WASM based render API" "A WASM based render API"
(:require (:require
["react-dom/server" :as rds]
[app.common.colors :as cc]
[app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.math :as mth] [app.common.math :as mth]
[app.common.svg.path :as path] [app.common.svg.path :as path]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cf] [app.config :as cf]
[app.main.refs :as refs]
[app.main.render :as render]
[app.render-wasm.helpers :as h] [app.render-wasm.helpers :as h]
[app.util.debug :as dbg] [app.util.debug :as dbg]
[app.util.functions :as fns] [app.util.functions :as fns]
[app.util.http :as http] [app.util.http :as http]
[app.util.webapi :as wapi] [app.util.webapi :as wapi]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[cuerdas.core :as str]
[goog.object :as gobj] [goog.object :as gobj]
[promesa.core :as p])) [promesa.core :as p]
[rumext.v2 :as mf]))
(defonce internal-frame-id nil) (defonce internal-frame-id nil)
(defonce internal-module #js {}) (defonce internal-module #js {})
@ -28,6 +35,54 @@
(def dpr (def dpr
(if use-dpr? js/window.devicePixelRatio 1.0)) (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 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 ;; 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). ;; the window started rendering elements so it could be useful to measure time between frames).
@ -237,16 +292,52 @@
(store-image id)))))) (store-image id))))))
fills)) 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 (defn set-shape-path-content
[content] [content]
(let [buffer (path/content->buffer content) (let [buffer (path/content->buffer content)
size (.-byteLength buffer) size (.-byteLength buffer)
ptr (h/call internal-module "_alloc_bytes" size) ptr (h/call internal-module "_alloc_bytes" size)
heap (gobj/get ^js internal-module "HEAPU8") heap (gobj/get ^js internal-module "HEAPU8")
mem (js/Uint8Array. (.-buffer heap) ptr size)] mem (js/Uint8Array. (.-buffer heap) ptr size)]
(.set mem (js/Uint8Array. buffer)) (.set mem (js/Uint8Array. buffer))
(h/call internal-module "_set_shape_path_content"))) (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 (defn- translate-blend-mode
[blend-mode] [blend-mode]
(case blend-mode (case blend-mode
@ -290,6 +381,11 @@
(h/call internal-module "_navigate") (h/call internal-module "_navigate")
(debounce-render-without-cache)) (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 (defn set-objects
[objects] [objects]
(let [shapes (into [] (vals objects)) (let [shapes (into [] (vals objects))
@ -310,7 +406,8 @@
blend-mode (dm/get-prop shape :blend-mode) blend-mode (dm/get-prop shape :blend-mode)
opacity (dm/get-prop shape :opacity) opacity (dm/get-prop shape :opacity)
hidden (dm/get-prop shape :hidden) 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) (use-shape id)
(set-shape-type type) (set-shape-type type)
@ -319,13 +416,21 @@
(set-shape-rotation rotation) (set-shape-rotation rotation)
(set-shape-transform transform) (set-shape-transform transform)
(set-shape-blend-mode blend-mode) (set-shape-blend-mode blend-mode)
(set-shape-children children)
(set-shape-opacity opacity) (set-shape-opacity opacity)
(set-shape-hidden hidden) (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))] (let [pending-fills (doall (set-shape-fills fills))]
(recur (inc index) (into pending pending-fills)))) (recur (inc index) (into pending pending-fills))))
pending))] pending))]
(request-render) (request-render)
(when-let [pending (seq pending)] (when-let [pending (seq pending)]
(->> (rx/from pending) (->> (rx/from pending)
@ -340,9 +445,9 @@
:alpha true}) :alpha true})
(defn clear-canvas (defn clear-canvas
[] [])
;; TODO: perform corresponding cleaning ;; TODO: Perform the corresponding cleanup."
)
(defn resize-viewbox (defn resize-viewbox
[width height] [width height]

View file

@ -121,7 +121,14 @@
:opacity (api/set-shape-opacity v) :opacity (api/set-shape-opacity v)
:hidden (api/set-shape-hidden v) :hidden (api/set-shape-hidden v)
:shapes (api/set-shape-children 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) nil)
;; when something synced with wasm ;; when something synced with wasm
;; is modified, we need to request ;; is modified, we need to request

135
render-wasm/Cargo.lock generated
View file

@ -8,6 +8,19 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 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]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.1.3" version = "1.1.3"
@ -93,12 +106,35 @@ dependencies = [
"cfg-if", "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]] [[package]]
name = "either" name = "either"
version = "1.13.0" version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 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]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.1" version = "1.0.1"
@ -293,12 +329,24 @@ dependencies = [
"minimal-lexical", "minimal-lexical",
] ]
[[package]]
name = "once_cell"
version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.1" version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pin-project-lite"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
[[package]] [[package]]
name = "prettyplease" name = "prettyplease"
version = "0.2.24" version = "0.2.24"
@ -318,6 +366,15 @@ dependencies = [
"unicode-ident", "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]] [[package]]
name = "quote" name = "quote"
version = "1.0.37" version = "1.0.37"
@ -369,6 +426,7 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
name = "render" name = "render"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"edit-xml",
"gl", "gl",
"skia-safe", "skia-safe",
"uuid", "uuid",
@ -509,6 +567,26 @@ dependencies = [
"xattr", "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]] [[package]]
name = "toml" name = "toml"
version = "0.8.19" version = "0.8.19"
@ -543,6 +621,37 @@ dependencies = [
"winnow", "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]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.13" version = "1.0.13"
@ -558,6 +667,12 @@ dependencies = [
"getrandom", "getrandom",
] ]
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.0+wasi-snapshot-preview1" version = "0.11.0+wasi-snapshot-preview1"
@ -671,3 +786,23 @@ name = "xml-rs"
version = "0.8.22" version = "0.8.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af4e2e2f7cba5a093896c1e150fbfe177d1883e7448200efb81d40b9d339ef26" 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",
]

View file

@ -11,6 +11,7 @@ name = "render_wasm"
path = "src/main.rs" path = "src/main.rs"
[dependencies] [dependencies]
edit-xml = "0.1.0"
gl = "0.14.0" gl = "0.14.0"
skia-safe = { version = "0.80.1", default-features = false, features = ["gl", "svg", "textlayout", "binary-cache"]} skia-safe = { version = "0.80.1", default-features = false, features = ["gl", "svg", "textlayout", "binary-cache"]}
uuid = { version = "1.11.0", features = ["v4"] } uuid = { version = "1.11.0", features = ["v4"] }

View file

@ -16,7 +16,7 @@ export EMCC_CFLAGS="--no-entry \
-sMAX_WEBGL_VERSION=2 \ -sMAX_WEBGL_VERSION=2 \
-sMODULARIZE=1 \ -sMODULARIZE=1 \
-sEXPORT_NAME=createRustSkiaModule \ -sEXPORT_NAME=createRustSkiaModule \
-sEXPORTED_RUNTIME_METHODS=GL \ -sEXPORTED_RUNTIME_METHODS=GL,stringToUTF8 \
-sEXPORT_ES6=1" -sEXPORT_ES6=1"
EMSDK_QUIET=1 . /usr/local/emsdk/emsdk_env.sh; EMSDK_QUIET=1 . /usr/local/emsdk/emsdk_env.sh;

View file

@ -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] #[no_mangle]
pub extern "C" fn set_shape_blend_mode(mode: i32) { pub extern "C" fn set_shape_blend_mode(mode: i32) {
let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); 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() { fn main() {
init_gl(); init_gl();
} }

View file

@ -1,7 +1,6 @@
use std::collections::HashMap;
use skia::Contains; use skia::Contains;
use skia_safe as skia; use skia_safe as skia;
use std::collections::HashMap;
use uuid::Uuid; use uuid::Uuid;
use crate::math; use crate::math;
@ -31,6 +30,7 @@ pub trait Renderable {
fn hidden(&self) -> bool; fn hidden(&self) -> bool;
fn clip(&self) -> bool; fn clip(&self) -> bool;
fn children_ids(&self) -> Vec<Uuid>; fn children_ids(&self) -> Vec<Uuid>;
fn is_recursive(&self) -> bool;
} }
pub(crate) struct CachedSurfaceImage { pub(crate) struct CachedSurfaceImage {
@ -188,6 +188,7 @@ impl RenderState {
skia::SamplingOptions::new(skia::FilterMode::Linear, skia::MipmapMode::Nearest), skia::SamplingOptions::new(skia::FilterMode::Linear, skia::MipmapMode::Nearest),
Some(&paint), Some(&paint),
); );
self.drawing_surface self.drawing_surface
.canvas() .canvas()
.clear(skia::Color::TRANSPARENT); .clear(skia::Color::TRANSPARENT);
@ -318,43 +319,49 @@ impl RenderState {
// Returns a boolean indicating if the viewbox contains the rendered shapes // Returns a boolean indicating if the viewbox contains the rendered shapes
fn render_shape_tree(&mut self, root_id: &Uuid, tree: &HashMap<Uuid, impl Renderable>) -> bool { fn render_shape_tree(&mut self, root_id: &Uuid, tree: &HashMap<Uuid, impl Renderable>) -> bool {
let element = tree.get(&root_id).unwrap(); if let Some(element) = tree.get(&root_id) {
let mut is_complete = self.viewbox.area.contains(element.bounds()); let mut is_complete = self.viewbox.area.contains(element.bounds());
if !root_id.is_nil() { if !root_id.is_nil() {
if !element.bounds().intersects(self.viewbox.area) || element.hidden() { if !element.bounds().intersects(self.viewbox.area) || element.hidden() {
self.render_debug_element(element, false); self.render_debug_element(element, false);
// TODO: This means that not all the shapes are renderer so we // TODO: This means that not all the shapes are renderer so we
// need to call a render_all on the zoom out. // need to call a render_all on the zoom out.
return is_complete; // TODO return is_complete or return false?? return is_complete; // TODO return is_complete or return false??
} else { } else {
self.render_debug_element(element, true); self.render_debug_element(element, true);
}
} }
}
// This is needed so the next non-children shape does not carry this shape's transform // This is needed so the next non-children shape does not carry this shape's transform
self.final_surface.canvas().save(); self.final_surface.canvas().save();
self.drawing_surface.canvas().save(); self.drawing_surface.canvas().save();
if !root_id.is_nil() { if !root_id.is_nil() {
self.render_single_element(element); self.render_single_element(element);
if element.clip() { if element.clip() {
self.drawing_surface.canvas().clip_rect( self.drawing_surface.canvas().clip_rect(
element.bounds(), element.bounds(),
skia::ClipOp::Intersect, skia::ClipOp::Intersect,
true, 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;
} }
} }

View file

@ -1,5 +1,6 @@
use crate::math; use crate::math;
use skia_safe as skia; use skia_safe as skia;
use std::collections::HashMap;
use uuid::Uuid; use uuid::Uuid;
use crate::render::{BlendMode, Renderable}; use crate::render::{BlendMode, Renderable};
@ -9,17 +10,20 @@ mod images;
mod matrix; mod matrix;
mod paths; mod paths;
mod renderable; mod renderable;
mod svgraw;
pub use fills::*; pub use fills::*;
pub use images::*; pub use images::*;
use matrix::*; use matrix::*;
pub use paths::*; pub use paths::*;
pub use svgraw::*;
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum Kind { pub enum Kind {
Rect(math::Rect), Rect(math::Rect),
Circle(math::Rect), Circle(math::Rect),
Path(Path), Path(Path),
SVGRaw(SVGRaw),
} }
pub type Color = skia::Color; pub type Color = skia::Color;
@ -38,6 +42,7 @@ pub struct Shape {
blend_mode: BlendMode, blend_mode: BlendMode,
opacity: f32, opacity: f32,
hidden: bool, hidden: bool,
svg_attrs: HashMap<String, String>,
} }
impl Shape { impl Shape {
@ -54,6 +59,7 @@ impl Shape {
blend_mode: BlendMode::default(), blend_mode: BlendMode::default(),
opacity: 1., opacity: 1.,
hidden: false, hidden: false,
svg_attrs: HashMap::new(),
} }
} }
@ -135,10 +141,28 @@ impl Shape {
Ok(()) 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) { pub fn set_blend_mode(&mut self, mode: BlendMode) {
self.blend_mode = mode; 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<skia::Matrix> { fn to_path_transform(&self) -> Option<skia::Matrix> {
match self.kind { match self.kind {
Kind::Path(_) => { Kind::Path(_) => {

View file

@ -1,4 +1,6 @@
use edit_xml::Document;
use skia_safe as skia; use skia_safe as skia;
use std::collections::HashMap;
use uuid::Uuid; use uuid::Uuid;
use super::{Fill, Image, Kind, Shape}; use super::{Fill, Image, Kind, Shape};
@ -23,21 +25,38 @@ impl Renderable for Shape {
surface.canvas().concat(&matrix); surface.canvas().concat(&matrix);
for fill in self.fills().rev() { match &self.kind {
render_fill( Kind::SVGRaw(sr) => render_svg(&sr.content.to_string(), surface, font_provider),
surface, _ => {
images, // let svg_canvas_required =
fill, // matches!(&self.kind, Kind::Path(_)) && !self.svg_attrs.is_empty();
self.selrect, // if svg_canvas_required {
&self.kind, // let svg_canvas = build_svg_canvas(self.selrect);
self.to_path_transform().as_ref(), // render_fills_for_kind(
); // self,
} // &svg_canvas,
// images,
let mut paint = skia::Paint::default(); // self.to_path_transform().as_ref(),
paint.set_blend_mode(self.blend_mode.into()); // );
paint.set_alpha_f(self.opacity); // 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(()) Ok(())
} }
@ -64,22 +83,68 @@ impl Renderable for Shape {
fn children_ids(&self) -> Vec<Uuid> { fn children_ids(&self) -> Vec<Uuid> {
self.children.clone() 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<String, String>,
) {
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( fn render_fill(
surface: &mut skia::Surface, canvas: &skia::Canvas,
images: &ImageStore, images: &ImageStore,
fill: &Fill, fill: &Fill,
selrect: Rect, selrect: Rect,
kind: &Kind, kind: &Kind,
path_transform: Option<&skia::Matrix>, path_transform: Option<&skia::Matrix>,
svg_attrs: &HashMap<String, String>,
) { ) {
match (fill, kind) { match (fill, kind) {
(Fill::Image(image_fill), kind) => { (Fill::Image(image_fill), kind) => {
let image = images.get(&image_fill.id()); let image = images.get(&image_fill.id());
if let Some(image) = image { if let Some(image) = image {
draw_image_in_container( draw_image_in_container(
surface.canvas(), canvas,
&image, &image,
image_fill.size(), image_fill.size(),
kind, kind,
@ -90,16 +155,22 @@ fn render_fill(
} }
} }
(_, Kind::Rect(rect)) => { (_, Kind::Rect(rect)) => {
surface.canvas().draw_rect(rect, &fill.to_paint(&selrect)); canvas.draw_rect(rect, &fill.to_paint(&selrect));
} }
(_, Kind::Circle(rect)) => { (_, Kind::Circle(rect)) => {
surface.canvas().draw_oval(rect, &fill.to_paint(&selrect)); canvas.draw_oval(rect, &fill.to_paint(&selrect));
} }
(_, Kind::Path(path)) => { (_, Kind::Path(path)) => {
surface.canvas().draw_path( let mut skia_path = &mut path.to_skia_path();
&path.to_skia_path().transform(path_transform.unwrap()), skia_path = skia_path.transform(path_transform.unwrap());
&fill.to_paint(&selrect), 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, true,
); );
} }
Kind::SVGRaw(_) => {
canvas.clip_rect(container, skia::ClipOp::Intersect, true);
}
} }
// Draw the image with the calculated destination rectangle // 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 // Restore the canvas to remove the clipping
canvas.restore(); 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<String, String>,
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());
}

View file

@ -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 }
}
}