mirror of
https://github.com/penpot/penpot.git
synced 2025-01-04 13:50:12 -05:00
🎉 Basic wasm support for svg attrs and svg defs
This commit is contained in:
parent
c0cfa8dc42
commit
f509edc151
12 changed files with 564 additions and 74 deletions
|
@ -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}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
135
render-wasm/Cargo.lock
generated
135
render-wasm/Cargo.lock
generated
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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<Uuid>;
|
||||
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<Uuid, impl Renderable>) -> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String, String>,
|
||||
}
|
||||
|
||||
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<skia::Matrix> {
|
||||
match self.kind {
|
||||
Kind::Path(_) => {
|
||||
|
|
|
@ -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<Uuid> {
|
||||
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(
|
||||
surface: &mut skia::Surface,
|
||||
canvas: &skia::Canvas,
|
||||
images: &ImageStore,
|
||||
fill: &Fill,
|
||||
selrect: Rect,
|
||||
kind: &Kind,
|
||||
path_transform: Option<&skia::Matrix>,
|
||||
svg_attrs: &HashMap<String, String>,
|
||||
) {
|
||||
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<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());
|
||||
}
|
||||
|
|
10
render-wasm/src/shapes/svgraw.rs
Normal file
10
render-wasm/src/shapes/svgraw.rs
Normal 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 }
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue