From 1bb337c3dd8d8932e1feedcc8fe03649bd2a250e Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 6 Feb 2025 15:30:31 +0100 Subject: [PATCH] :sparkles: Add support for WASM transforms --- common/src/app/common/uuid.cljc | 5 + .../app/main/data/workspace/modifiers.cljs | 69 ++++++- .../app/main/data/workspace/transforms.cljs | 183 ++++++++++++------ frontend/src/app/main/refs.cljs | 3 + .../main/ui/workspace/viewport/selection.cljs | 15 +- .../app/main/ui/workspace/viewport_wasm.cljs | 5 +- frontend/src/app/render_wasm/api.cljs | 96 +++++++++ render-wasm/_build_env | 5 +- render-wasm/src/main.rs | 69 ++++++- render-wasm/src/matrix.rs | 121 ++++++++++++ render-wasm/src/mem.rs | 43 +++- render-wasm/src/render.rs | 27 ++- render-wasm/src/shapes.rs | 5 +- render-wasm/src/shapes/matrix.rs | 70 ------- render-wasm/src/shapes/transform.rs | 71 +++++++ render-wasm/src/state.rs | 7 +- render-wasm/src/utils.rs | 9 + render-wasm/watch_test | 8 + 18 files changed, 658 insertions(+), 153 deletions(-) create mode 100644 render-wasm/src/matrix.rs delete mode 100644 render-wasm/src/shapes/matrix.rs create mode 100644 render-wasm/src/shapes/transform.rs create mode 100755 render-wasm/watch_test diff --git a/common/src/app/common/uuid.cljc b/common/src/app/common/uuid.cljc index b7b49e2c1..707770fcd 100644 --- a/common/src/app/common/uuid.cljc +++ b/common/src/app/common/uuid.cljc @@ -95,6 +95,11 @@ (impl/getUnsignedParts (.-uuid ^UUID this)))) +#?(:cljs + (defn from-unsigned-parts + [a b c d] + (uuid (impl/fromUnsignedParts a b c d)))) + #?(:cljs (defn get-u32 "A cached variant of get-unsigned-parts" diff --git a/frontend/src/app/main/data/workspace/modifiers.cljs b/frontend/src/app/main/data/workspace/modifiers.cljs index 21fb5487f..de9d58800 100644 --- a/frontend/src/app/main/data/workspace/modifiers.cljs +++ b/frontend/src/app/main/data/workspace/modifiers.cljs @@ -28,6 +28,8 @@ [app.main.data.workspace.guides :as-alias dwg] [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.undo :as dwu] + [app.main.features :as features] + [app.render-wasm.api :as wasm.api] [beicon.v2.core :as rx] [potok.v2.core :as ptk])) @@ -160,8 +162,13 @@ change-to-fixed? (assoc :grow-type :fixed)))) -(defn- clear-local-transform [] +(defn clear-local-transform [] (ptk/reify ::clear-local-transform + ptk/EffectEvent + (effect [_ state _] + (when (features/active-feature? state "render-wasm/v1") + (wasm.api/set-modifiers nil))) + ptk/UpdateEvent (update [_ state] (-> state @@ -391,6 +398,7 @@ (update [_ state] (update state :workspace-modifiers calculate-update-modifiers state ignore-constraints ignore-snap-pixel modif-tree))))) + (defn set-modifiers ([modif-tree] (set-modifiers modif-tree false)) @@ -409,6 +417,37 @@ modifiers (calculate-modifiers state ignore-constraints ignore-snap-pixel modif-tree page-id params)] (assoc state :workspace-modifiers modifiers)))))) +(defn set-wasm-modifiers + ([modif-tree] + (set-wasm-modifiers modif-tree false)) + + ([modif-tree ignore-constraints] + (set-wasm-modifiers modif-tree ignore-constraints false)) + + ([modif-tree ignore-constraints ignore-snap-pixel] + (set-wasm-modifiers modif-tree ignore-constraints ignore-snap-pixel nil)) + + ([modif-tree _ignore-constraints _ignore-snap-pixel _params] + (ptk/reify ::set-wasm-modifiers + ptk/EffectEvent + (effect [_ _ _] + (let [entries + (->> modif-tree + (mapv (fn [[id data]] + {:id id + :transform (ctm/modifiers->transform (:modifiers data))}))) + + modifiers-new + (wasm.api/propagate-modifiers entries)] + (wasm.api/set-modifiers modifiers-new)))))) + +(defn set-selrect-transform + [modifiers] + (ptk/reify ::set-selrect-transform + ptk/UpdateEvent + (update [_ state] + (assoc state :workspace-selrect-transform (ctm/modifiers->transform modifiers))))) + (def ^:private xf-rotation-shape (comp @@ -418,6 +457,33 @@ ;; Rotation use different algorithm to calculate children ;; modifiers (and do not use child constraints). +(defn set-wasm-rotation-modifiers + ([angle shapes] + (set-wasm-rotation-modifiers angle shapes (-> shapes gsh/shapes->rect grc/rect->center))) + + ([angle shapes center] + (ptk/reify ::set-wasm-rotation-modifiers + ptk/EffectEvent + (effect [_ state _] + (let [objects (dsh/lookup-page-objects state) + ids (sequence xf-rotation-shape shapes) + + get-modifier + (fn [shape] + (ctm/rotation-modifiers shape center angle)) + + modif-tree + (-> (build-modif-tree ids objects get-modifier) + (gm/set-objects-modifiers objects)) + + modifiers + (->> modif-tree + (map (fn [[id {:keys [modifiers]}]] + {:id id + :transform (ctm/modifiers->transform modifiers)})))] + + (wasm.api/set-modifiers modifiers)))))) + (defn set-rotation-modifiers ([angle shapes] (set-rotation-modifiers angle shapes (-> shapes gsh/shapes->rect grc/rect->center))) @@ -575,3 +641,4 @@ (if undo-transation? (rx/of (dwu/commit-undo-transaction undo-id)) (rx/empty)))))))) + diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index 5b3988427..d574aa5ba 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -32,6 +32,7 @@ [app.main.data.workspace.modifiers :as dwm] [app.main.data.workspace.selection :as dws] [app.main.data.workspace.undo :as dwu] + [app.main.features :as features] [app.main.snap :as snap] [app.main.streams :as ms] [app.util.array :as array] @@ -132,7 +133,9 @@ (ptk/reify ::finish-transform ptk/UpdateEvent (update [_ state] - (update state :workspace-local dissoc :transform :duplicate-move-started? false)))) + (-> state + (update :workspace-local dissoc :transform :duplicate-move-started?) + (dissoc :workspace-selrect-transform))))) ;; -- Resize -------------------------------------------------------- @@ -217,27 +220,23 @@ (not (mth/close? (dm/get-prop scalev :x) 1)) set-fix-height? - (not (mth/close? (dm/get-prop scalev :y) 1)) + (not (mth/close? (dm/get-prop scalev :y) 1))] - modifiers (cond-> (ctm/empty) - (some? displacement) - (ctm/move displacement) + (cond-> (ctm/empty) + (some? displacement) + (ctm/move displacement) - :always - (ctm/resize scalev resize-origin shape-transform shape-transform-inverse) + :always + (ctm/resize scalev resize-origin shape-transform shape-transform-inverse) - ^boolean set-fix-width? - (ctm/change-property :layout-item-h-sizing :fix) + ^boolean set-fix-width? + (ctm/change-property :layout-item-h-sizing :fix) - ^boolean set-fix-height? - (ctm/change-property :layout-item-v-sizing :fix) + ^boolean set-fix-height? + (ctm/change-property :layout-item-v-sizing :fix) - ^boolean scale-text - (ctm/scale-content (dm/get-prop scalev :x))) - - modif-tree (dwm/create-modif-tree ids modifiers)] - - (rx/of (dwm/set-modifiers modif-tree scale-text)))) + ^boolean scale-text + (ctm/scale-content (dm/get-prop scalev :x))))) ;; Unifies the instantaneous proportion lock modifier ;; activated by Shift key and the shapes own proportion @@ -264,18 +263,43 @@ focus (:workspace-focus-selected state) zoom (dm/get-in state [:workspace-local :zoom] 1) objects (dsh/lookup-page-objects state page-id) - shapes (map (d/getf objects) ids)] + shapes (map (d/getf objects) ids) + + resize-events-stream + (->> ms/mouse-position + (rx/filter some?) + (rx/with-latest-from ms/mouse-position-shift ms/mouse-position-alt) + (rx/map normalize-proportion-lock) + (rx/switch-map + (fn [[point _ _ :as current]] + (->> (snap/closest-snap-point page-id shapes objects layout zoom focus point) + (rx/map #(conj current %))))) + (rx/map #(resize shape initial-position layout %)) + (rx/share))] (rx/concat - (->> ms/mouse-position - (rx/filter some?) - (rx/with-latest-from ms/mouse-position-shift ms/mouse-position-alt) - (rx/map normalize-proportion-lock) - (rx/switch-map (fn [[point _ _ :as current]] - (->> (snap/closest-snap-point page-id shapes objects layout zoom focus point) - (rx/map #(conj current %))))) - (rx/mapcat (partial resize shape initial-position layout)) - (rx/take-until stopper)) + (rx/merge + (->> resize-events-stream + (rx/mapcat + (fn [modifiers] + (let [modif-tree (dwm/create-modif-tree ids modifiers)] + (if (features/active-feature? state "render-wasm/v1") + (rx/of + (dwm/set-selrect-transform modifiers) + (dwm/set-wasm-modifiers modif-tree (contains? layout :scale-text))) + + (rx/of (dwm/set-modifiers modif-tree (contains? layout :scale-text))))))) + (rx/take-until stopper)) + + ;; The last event we need to use the old method so the elements are correctly positioned until + ;; all the logic is implemented in wasm + (if (features/active-feature? state "render-wasm/v1") + (->> resize-events-stream + (rx/take-until stopper) + (rx/last) + (rx/map #(dwm/set-modifiers (dwm/create-modif-tree ids %) (contains? layout :scale-text)))) + (rx/empty))) + (rx/of (dwm/apply-modifiers) (finish-transform)))))))) @@ -371,7 +395,7 @@ (assoc-in [:workspace-local :transform] :rotate))) ptk/WatchEvent - (watch [_ _ stream] + (watch [_ state stream] (let [stopper (mse/drag-stopper stream) group (gsh/shapes->rect shapes) group-center (grc/rect->center group) @@ -390,15 +414,29 @@ angle (if shift? (* (mth/floor (/ angle 15)) 15) angle)] - angle))] + angle)) + + angle-stream + (->> ms/mouse-position + (rx/with-latest-from ms/mouse-position-mod ms/mouse-position-shift) + (rx/map + (fn [[pos mod? shift?]] + (calculate-angle pos mod? shift?))) + (rx/share))] (rx/concat - (->> ms/mouse-position - (rx/with-latest-from ms/mouse-position-mod ms/mouse-position-shift) - (rx/map - (fn [[pos mod? shift?]] - (let [delta-angle (calculate-angle pos mod? shift?)] - (dwm/set-rotation-modifiers delta-angle shapes group-center)))) - (rx/take-until stopper)) + (rx/merge + (->> angle-stream + (rx/map + #(if (features/active-feature? state "render-wasm/v1") + (dwm/set-wasm-rotation-modifiers % shapes group-center) + (dwm/set-rotation-modifiers % shapes group-center))) + (rx/take-until stopper)) + (if (features/active-feature? state "render-wasm/v1") + (->> angle-stream + (rx/take-until stopper) + (rx/last) + (rx/map #(dwm/set-rotation-modifiers % shapes group-center))) + (rx/empty))) (rx/of (dwm/apply-modifiers) (finish-transform))))))) @@ -536,16 +574,17 @@ position (->> ms/mouse-position (rx/map #(gpt/to-vec from-position %))) - snap-delta (rx/concat - ;; We send the nil first so the stream is not waiting for the first value - (rx/of nil) - (->> position - ;; FIXME: performance throttle - (rx/throttle 20) - (rx/switch-map - (fn [pos] - (->> (snap/closest-snap-move page-id shapes objects layout zoom focus pos) - (rx/map #(array pos %)))))))] + snap-delta + (rx/concat + ;; We send the nil first so the stream is not waiting for the first value + (rx/of nil) + (->> position + ;; FIXME: performance throttle + (rx/throttle 20) + (rx/switch-map + (fn [pos] + (->> (snap/closest-snap-move page-id shapes objects layout zoom focus pos) + (rx/map #(array pos %)))))))] (if (empty? shapes) (rx/of (finish-transform)) (let [move-stream @@ -570,29 +609,45 @@ cell-data (when (and grid-layout? (not mod?)) (gslg/get-drop-cell target-frame objects position))] (array move-vector target-frame drop-index cell-data)))) - (rx/take-until stopper))] + (rx/take-until stopper)) + + modifiers-stream + (->> move-stream + (rx/with-latest-from array/conj ms/mouse-position-shift) + (rx/map + (fn [[move-vector target-frame drop-index cell-data shift?]] + (let [x-disp? (> (mth/abs (:x move-vector)) (mth/abs (:y move-vector))) + [move-vector snap-ignore-axis] + (cond + (and shift? x-disp?) + [(assoc move-vector :y 0) :y] + + shift? + [(assoc move-vector :x 0) :x] + + :else + [move-vector nil])] + [(-> (dwm/create-modif-tree ids (ctm/move-modifiers move-vector)) + (dwm/build-change-frame-modifiers objects selected target-frame drop-index cell-data)) + snap-ignore-axis]))) + (rx/share))] (rx/merge ;; Temporary modifiers stream - (->> move-stream - (rx/with-latest-from array/conj ms/mouse-position-shift) + (->> modifiers-stream (rx/map - (fn [[move-vector target-frame drop-index cell-data shift?]] - (let [x-disp? (> (mth/abs (:x move-vector)) (mth/abs (:y move-vector))) - [move-vector snap-ignore-axis] - (cond - (and shift? x-disp?) - [(assoc move-vector :y 0) :y] + (fn [[modifiers snap-ignore-axis]] + (if (features/active-feature? state "render-wasm/v1") + (dwm/set-wasm-modifiers modifiers false false {:snap-ignore-axis snap-ignore-axis}) + (dwm/set-modifiers modifiers false false {:snap-ignore-axis snap-ignore-axis}))))) - shift? - [(assoc move-vector :x 0) :x] - - :else - [move-vector nil])] - - (-> (dwm/create-modif-tree ids (ctm/move-modifiers move-vector)) - (dwm/build-change-frame-modifiers objects selected target-frame drop-index cell-data) - (dwm/set-modifiers false false {:snap-ignore-axis snap-ignore-axis})))))) + (if (features/active-feature? state "render-wasm/v1") + (->> modifiers-stream + (rx/last) + (rx/map + (fn [[modifiers snap-ignore-axis]] + (dwm/set-modifiers modifiers false false {:snap-ignore-axis snap-ignore-axis})))) + (rx/empty)) (->> move-stream (rx/with-latest-from ms/mouse-position-alt) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index deb64480c..b0e5c1642 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -125,6 +125,9 @@ (def workspace-drawing (l/derived :workspace-drawing st/state)) +(def workspace-selrect-transform + (l/derived :workspace-selrect-transform st/state)) + ;; TODO: rename to workspace-selected (?) ;; Don't use directly from components, this is a proxy to improve performance of selected-shapes (def ^:private selected-shapes-data diff --git a/frontend/src/app/main/ui/workspace/viewport/selection.cljs b/frontend/src/app/main/ui/workspace/viewport/selection.cljs index 841661452..98ce5a047 100644 --- a/frontend/src/app/main/ui/workspace/viewport/selection.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/selection.cljs @@ -318,7 +318,13 @@ [{:keys [shape zoom color on-move-selected on-context-menu disable-handlers]}] (let [selrect (dm/get-prop shape :selrect) transform-type (mf/deref refs/current-transform) - transform (gsh/transform-str shape)] + sr-transform (mf/deref refs/workspace-selrect-transform) + + transform + (dm/str + (cond->> (gsh/transform-matrix shape) + (some? sr-transform) + (gmt/multiply sr-transform)))] (when (and (some? selrect) (not (or (= transform-type :move) @@ -336,13 +342,18 @@ {::mf/wrap-props false} [{:keys [shape zoom color on-resize on-rotate disable-handlers]}] (let [transform-type (mf/deref refs/current-transform) + sr-transform (mf/deref refs/workspace-selrect-transform) + read-only? (mf/use-ctx ctx/workspace-read-only?) layout (mf/deref refs/workspace-layout) scale-text? (contains? layout :scale-text) selrect (dm/get-prop shape :selrect) - transform (gsh/transform-matrix shape) + + transform (cond->> (gsh/transform-matrix shape) + (some? sr-transform) + (gmt/multiply sr-transform)) rotation (-> (gpt/point 1 0) (gpt/transform (:transform shape)) diff --git a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs index fc84be920..46e7d0772 100644 --- a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs +++ b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs @@ -12,7 +12,6 @@ [app.common.data.macros :as dm] [app.common.files.helpers :as cfh] [app.common.geom.shapes :as gsh] - [app.common.types.shape :as cts] [app.common.types.shape-tree :as ctt] [app.common.types.shape.layout :as ctl] [app.main.data.workspace.modifiers :as dwm] @@ -112,9 +111,7 @@ text-modifiers (mf/deref refs/workspace-text-modifier) objects-modified (mf/with-memo [base-objects text-modifiers modifiers] - (binding [cts/*wasm-sync* true] - (-> (into selected (keys modifiers)) - (apply-modifiers-to-selected base-objects text-modifiers modifiers)))) + (apply-modifiers-to-selected selected base-objects text-modifiers modifiers)) selected-shapes (keep (d/getf objects-modified) selected) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 6846c0eec..51815eb4a 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -9,6 +9,7 @@ (:require ["react-dom/server" :as rds] [app.common.data.macros :as dm] + [app.common.geom.matrix :as gmt] [app.common.math :as mth] [app.common.svg.path :as path] [app.common.uuid :as uuid] @@ -563,12 +564,107 @@ (rx/reduce conj []) (rx/subs! request-render))))) +(defn uuid->u8 + [id] + (let [buffer (uuid/get-u32 id) + u32-arr (js/Uint32Array. 4)] + (doseq [i (range 0 4)] + (aset u32-arr i (aget buffer i))) + (js/Uint8Array. (.-buffer u32-arr)))) + +(defn matrix->u8 + [{:keys [a b c d e f]}] + (let [f32-arr (js/Float32Array. 6)] + (aset f32-arr 0 a) + (aset f32-arr 1 b) + (aset f32-arr 2 c) + (aset f32-arr 3 d) + (aset f32-arr 4 e) + (aset f32-arr 5 f) + (js/Uint8Array. (.-buffer f32-arr)))) + +(defn data->entry + [data offset] + (let [id1 (.getUint32 data (+ offset 0) true) + id2 (.getUint32 data (+ offset 4) true) + id3 (.getUint32 data (+ offset 8) true) + id4 (.getUint32 data (+ offset 12) true) + + a (.getFloat32 data (+ offset 16) true) + b (.getFloat32 data (+ offset 20) true) + c (.getFloat32 data (+ offset 24) true) + d (.getFloat32 data (+ offset 28) true) + e (.getFloat32 data (+ offset 32) true) + f (.getFloat32 data (+ offset 36) true) + + id (uuid/from-unsigned-parts id1 id2 id3 id4)] + + {:id id + :transform (gmt/matrix a b c d e f)})) + +(defn propagate-modifiers + [entries] + (let [entry-size 40 + ptr (h/call internal-module "_alloc_bytes" (* entry-size (count entries))) + + heap + (js/Uint8Array. + (.-buffer (gobj/get ^js internal-module "HEAPU8")) + ptr + (* entry-size (count entries)))] + + (loop [entries (seq entries) + offset 0] + (when-not (empty? entries) + (let [{:keys [id transform]} (first entries)] + (.set heap (uuid->u8 id) offset) + (.set heap (matrix->u8 transform) (+ offset 16)) + (recur (rest entries) (+ offset entry-size))))) + + (let [result-ptr (h/call internal-module "_propagate_modifiers") + heap (js/DataView. (.-buffer (gobj/get ^js internal-module "HEAPU8"))) + len (.getUint32 heap result-ptr true) + result + (->> (range 0 len) + (mapv #(data->entry heap (+ result-ptr 4 (* % entry-size)))))] + (h/call internal-module "_free_bytes") + + result))) + (defn set-canvas-background [background] (let [rgba (rgba-from-hex background 1)] (h/call internal-module "_set_canvas_background" rgba) (request-render "set-canvas-background"))) +(defn set-modifiers + [modifiers] + (if (empty? modifiers) + (h/call internal-module "_clean_modifiers") + + (let [ENTRY_SIZE 40 + + ptr + (h/call internal-module "_alloc_bytes" (* ENTRY_SIZE (count modifiers))) + + heap + (js/Uint8Array. + (.-buffer (gobj/get ^js internal-module "HEAPU8")) + ptr + (* ENTRY_SIZE (count modifiers)))] + + (loop [entries (seq modifiers) + offset 0] + (when-not (empty? entries) + (let [{:keys [id transform]} (first entries)] + (.set heap (uuid->u8 id) offset) + (.set heap (matrix->u8 transform) (+ offset 16)) + (recur (rest entries) (+ offset ENTRY_SIZE))))) + + (h/call internal-module "_set_modifiers") + + (request-render "set-modifiers")))) + (defn initialize [base-objects zoom vbox background] (let [rgba (rgba-from-hex background 1)] diff --git a/render-wasm/_build_env b/render-wasm/_build_env index 3c7abb24f..b87994ac9 100644 --- a/render-wasm/_build_env +++ b/render-wasm/_build_env @@ -6,7 +6,7 @@ else export _BUILD_MODE=${1:-debug}; fi -export EMCC_CFLAGS="--no-entry \ +EMCC_CFLAGS="--no-entry \ -Os \ -sASSERTIONS=1 \ -sALLOW_TABLE_GROWTH=1 \ @@ -28,8 +28,11 @@ _CARGO_PARAMS="--target=wasm32-unknown-emscripten"; if [ "$_BUILD_MODE" = "release" ]; then _CARGO_PARAMS="--release $_CARGO_PARAMS" +else + EMCC_CFLAGS="$EMCC_CFLAGS -sMALLOC=emmalloc-debug" fi +export EMCC_CFLAGS; export _CARGO_PARAMS; export SKIA_BINARIES_URL="https://github.com/penpot/skia-binaries/releases/download/0.80.1-1/skia-binaries-9e7d2684a17084095aef-wasm32-unknown-emscripten-gl-svg-textlayout.tar.gz" diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 07e8aa75b..fdc05e9ac 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -1,7 +1,11 @@ +use std::collections::HashSet; +use uuid::Uuid; + use skia_safe as skia; mod debug; mod math; +mod matrix; mod mem; mod render; mod shapes; @@ -9,8 +13,8 @@ mod state; mod utils; mod view; -use crate::shapes::{BoolType, Kind, Path}; - +use crate::mem::SerializableResult; +use crate::shapes::{BoolType, Kind, Path, TransformEntry}; use crate::state::State; use crate::utils::uuid_from_u32_quartet; @@ -590,6 +594,67 @@ pub extern "C" fn set_shape_path_attrs(num_attrs: u32) { } } +#[no_mangle] +pub extern "C" fn propagate_modifiers() -> *mut u8 { + let bytes = mem::bytes(); + + let mut entries: Vec<_> = bytes + .chunks(size_of::()) + .map(|data| TransformEntry::from_bytes(data.try_into().unwrap())) + .collect(); + + let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + + let mut processed = HashSet::::new(); + + let mut result = Vec::::new(); + + // Propagate the transform to children + while let Some(entry) = entries.pop() { + if !processed.contains(&entry.id) { + if let Some(shape) = state.shapes.get(&entry.id) { + let mut children: Vec = shape + .children + .iter() + .map(|id| TransformEntry { + id: id.clone(), + transform: entry.transform, + }) + .collect(); + + entries.append(&mut children); + + processed.insert(entry.id); + result.push(entry.clone()); + } + } + } + mem::write_vec(result) +} + +#[no_mangle] +pub extern "C" fn clean_modifiers() { + if let Some(state) = unsafe { STATE.as_mut() } { + state.modifiers.clear(); + } +} + +#[no_mangle] +pub extern "C" fn set_modifiers() { + let bytes = mem::bytes(); + + let entries: Vec<_> = bytes + .chunks(size_of::()) + .map(|data| TransformEntry::from_bytes(data.try_into().unwrap())) + .collect(); + + let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + + for entry in entries { + state.modifiers.insert(entry.id, entry.transform); + } +} + #[no_mangle] pub extern "C" fn add_shape_shadow( raw_color: u32, diff --git a/render-wasm/src/matrix.rs b/render-wasm/src/matrix.rs new file mode 100644 index 000000000..2692e0718 --- /dev/null +++ b/render-wasm/src/matrix.rs @@ -0,0 +1,121 @@ +// allowing dead code so we can have some API's that are not used yet without warnings +#![allow(dead_code)] +use skia_safe as skia; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Matrix { + pub a: f32, + pub b: f32, + pub c: f32, + pub d: f32, + pub e: f32, + pub f: f32, +} + +impl Matrix { + pub fn new(a: f32, b: f32, c: f32, d: f32, e: f32, f: f32) -> Self { + Self { a, b, c, d, e, f } + } + + pub fn translate(x: f32, y: f32) -> Self { + Self::new(0.0, 0.0, 0.0, 0.0, x, y) + } + + pub fn identity() -> Self { + Self { + a: 1., + b: 0., + c: 0., + d: 1., + e: 0., + f: 0., + } + } + + pub fn to_skia_matrix(&self) -> skia::Matrix { + let mut res = skia::Matrix::new_identity(); + + let (translate_x, translate_y) = self.translation(); + let (scale_x, scale_y) = self.scale(); + let (skew_x, skew_y) = self.skew(); + res.set_all( + scale_x, + skew_x, + translate_x, + skew_y, + scale_y, + translate_y, + 0., + 0., + 1., + ); + + res + } + + pub fn no_translation(&self) -> Self { + let mut res = Self::identity(); + res.c = self.c; + res.b = self.b; + res.a = self.a; + res.d = self.d; + res + } + + fn translation(&self) -> (f32, f32) { + (self.e, self.f) + } + + fn scale(&self) -> (f32, f32) { + (self.a, self.d) + } + + fn skew(&self) -> (f32, f32) { + (self.c, self.b) + } + + pub fn product(&self, other: &Matrix) -> Matrix { + let a = self.a * other.a + self.c * other.b; + let b = self.b * other.a + self.d * other.b; + let c = self.a * other.c + self.c * other.d; + let d = self.b * other.c + self.d * other.d; + let e = self.a * other.e + self.c * other.f + self.e; + let f = self.b * other.e + self.d * other.f + self.f; + Matrix::new(a, b, c, d, e, f) + } + + pub fn as_bytes(&self) -> [u8; 24] { + let mut result = [0; 24]; + result[0..4].clone_from_slice(&self.a.to_le_bytes()); + result[4..8].clone_from_slice(&self.b.to_le_bytes()); + result[8..12].clone_from_slice(&self.c.to_le_bytes()); + result[12..16].clone_from_slice(&self.d.to_le_bytes()); + result[16..20].clone_from_slice(&self.e.to_le_bytes()); + result[20..24].clone_from_slice(&self.f.to_le_bytes()); + result + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_product() { + let a = Matrix::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0); + let b = Matrix::new(6.0, 5.0, 4.0, 3.0, 2.0, 1.0); + + assert_eq!( + a.product(&b), + Matrix::new(21.0, 32.0, 13.0, 20.0, 10.0, 14.0) + ); + + let a = Matrix::new(7.0, 4.0, 8.0, 3.0, 9.0, 5.0); + let b = Matrix::new(7.0, 4.0, 8.0, 3.0, 9.0, 5.0); + + assert_eq!( + a.product(&b), + Matrix::new(81.0, 40.0, 80.0, 41.0, 112.0, 56.0) + ); + } +} diff --git a/render-wasm/src/mem.rs b/render-wasm/src/mem.rs index 0fc0eaa83..9670f4574 100644 --- a/render-wasm/src/mem.rs +++ b/render-wasm/src/mem.rs @@ -14,7 +14,20 @@ pub extern "C" fn alloc_bytes(len: usize) -> *mut u8 { return ptr; } -pub fn free_bytes() { +pub fn write_bytes(bytes: Vec) -> *mut u8 { + if unsafe { BUFFERU8.is_some() } { + panic!("Bytes already allocated"); + } + + let mut buffer = Box::new(bytes); + let ptr = buffer.as_mut_ptr(); + + unsafe { BUFFERU8 = Some(buffer) }; + return ptr; +} + +#[no_mangle] +pub extern "C" fn free_bytes() { if unsafe { BUFFERU8.is_some() } { let buffer = unsafe { BUFFERU8.take() }.expect("uninitialized buffer"); std::mem::drop(buffer); @@ -30,3 +43,31 @@ pub fn bytes() -> Vec { let buffer = unsafe { BUFFERU8.take() }.expect("uninitialized buffer"); *buffer } + +pub trait SerializableResult { + type BytesType; + fn from_bytes(bytes: Self::BytesType) -> Self; + fn as_bytes(&self) -> Self::BytesType; + fn clone_to_slice(&self, slice: &mut [u8]); +} + +/* + Returns an array in the heap. The first 4 bytes is always the size + of the array. Then the items are serialized one after the other + by the implementation of SerializableResult trait +*/ +pub fn write_vec(result: Vec) -> *mut u8 { + let elem_size = size_of::(); + let bytes_len = 4 + result.len() * elem_size; + let mut result_bytes = Vec::::with_capacity(bytes_len); + + result_bytes.resize(bytes_len, 0); + result_bytes[0..4].clone_from_slice(&result.len().to_le_bytes()); + + for i in 0..result.len() { + let base = 4 + i * elem_size; + result[i].clone_to_slice(&mut result_bytes[base..base + elem_size]); + } + + write_bytes(result_bytes) +} diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 353de9699..7407beae4 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use uuid::Uuid; use crate::math; +use crate::matrix::Matrix; use crate::view::Viewbox; mod blend; @@ -232,7 +233,18 @@ impl RenderState { .clear(skia::Color::TRANSPARENT); } - pub fn render_shape(&mut self, shape: &mut Shape, clip_bounds: Option) { + pub fn render_shape( + &mut self, + shape: &mut Shape, + modifiers: Option<&Matrix>, + clip_bounds: Option, + ) { + if let Some(modifiers) = modifiers { + self.drawing_surface + .canvas() + .concat(&modifiers.to_skia_matrix()); + } + let transform = shape.transform.to_skia_matrix(); // Check transform-matrix code from common/src/app/common/geom/shapes/transforms.cljc @@ -289,6 +301,7 @@ impl RenderState { pub fn start_render_loop( &mut self, tree: &mut HashMap, + modifiers: &HashMap, timestamp: i32, ) -> Result<(), String> { if self.render_in_progress { @@ -304,7 +317,7 @@ impl RenderState { self.translate(self.viewbox.pan_x, self.viewbox.pan_y); self.pending_nodes = vec![(Uuid::nil(), false, None)]; self.render_in_progress = true; - self.process_animation_frame(tree, timestamp)?; + self.process_animation_frame(tree, modifiers, timestamp)?; Ok(()) } @@ -325,10 +338,11 @@ impl RenderState { pub fn process_animation_frame( &mut self, tree: &mut HashMap, + modifiers: &HashMap, timestamp: i32, ) -> Result<(), String> { if self.render_in_progress { - self.render_shape_tree(tree, timestamp)?; + self.render_shape_tree(tree, modifiers, timestamp)?; if self.render_in_progress { if let Some(frame_id) = self.render_request_id { self.cancel_animation_frame(frame_id); @@ -395,6 +409,7 @@ impl RenderState { pub fn render_shape_tree( &mut self, tree: &HashMap, + modifiers: &HashMap, timestamp: i32, ) -> Result<(), String> { if !self.render_in_progress { @@ -432,7 +447,11 @@ impl RenderState { self.drawing_surface.canvas().save(); if !node_id.is_nil() { - self.render_shape(&mut element.clone(), clip_bounds); + self.render_shape( + &mut element.clone(), + modifiers.get(&element.id), + clip_bounds, + ); } else { self.apply_drawing_to_render_canvas(); } diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index 0ac1843ea..68ca43bb4 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -3,25 +3,26 @@ use skia_safe as skia; use std::collections::HashMap; use uuid::Uuid; +use crate::matrix::Matrix; use crate::render::BlendMode; mod blurs; mod bools; mod fills; -mod matrix; mod paths; mod shadows; mod strokes; mod svgraw; +mod transform; pub use blurs::*; pub use bools::*; pub use fills::*; -use matrix::*; pub use paths::*; pub use shadows::*; pub use strokes::*; pub use svgraw::*; +pub use transform::*; pub type CornerRadius = skia::Point; pub type Corners = [CornerRadius; 4]; diff --git a/render-wasm/src/shapes/matrix.rs b/render-wasm/src/shapes/matrix.rs deleted file mode 100644 index 294b9da06..000000000 --- a/render-wasm/src/shapes/matrix.rs +++ /dev/null @@ -1,70 +0,0 @@ -use skia_safe as skia; - -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Matrix { - pub a: f32, - pub b: f32, - pub c: f32, - pub d: f32, - pub e: f32, - pub f: f32, -} - -impl Matrix { - pub fn new(a: f32, b: f32, c: f32, d: f32, e: f32, f: f32) -> Self { - Self { a, b, c, d, e, f } - } - - pub fn identity() -> Self { - Self { - a: 1., - b: 0., - c: 0., - d: 1., - e: 0., - f: 0., - } - } - - pub fn to_skia_matrix(&self) -> skia::Matrix { - let mut res = skia::Matrix::new_identity(); - - let (translate_x, translate_y) = self.translation(); - let (scale_x, scale_y) = self.scale(); - let (skew_x, skew_y) = self.skew(); - res.set_all( - scale_x, - skew_x, - translate_x, - skew_y, - scale_y, - translate_y, - 0., - 0., - 1., - ); - - res - } - - pub fn no_translation(&self) -> Self { - let mut res = Self::identity(); - res.c = self.c; - res.b = self.b; - res.a = self.a; - res.d = self.d; - res - } - - fn translation(&self) -> (f32, f32) { - (self.e, self.f) - } - - fn scale(&self) -> (f32, f32) { - (self.a, self.d) - } - - fn skew(&self) -> (f32, f32) { - (self.c, self.b) - } -} diff --git a/render-wasm/src/shapes/transform.rs b/render-wasm/src/shapes/transform.rs new file mode 100644 index 000000000..769a7ad38 --- /dev/null +++ b/render-wasm/src/shapes/transform.rs @@ -0,0 +1,71 @@ +use uuid::Uuid; + +use crate::matrix::Matrix; +use crate::mem::SerializableResult; +use crate::utils::{uuid_from_u32_quartet, uuid_to_u32_quartet}; + +#[derive(PartialEq, Debug, Clone)] +#[repr(C)] +pub struct TransformEntry { + pub id: Uuid, + pub transform: Matrix, +} + +impl SerializableResult for TransformEntry { + type BytesType = [u8; size_of::()]; + + fn from_bytes(bytes: Self::BytesType) -> Self { + let id = uuid_from_u32_quartet( + u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]), + u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]), + u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]), + u32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]), + ); + + let transform = Matrix::new( + f32::from_le_bytes([bytes[16], bytes[17], bytes[18], bytes[19]]), + f32::from_le_bytes([bytes[20], bytes[21], bytes[22], bytes[23]]), + f32::from_le_bytes([bytes[24], bytes[25], bytes[26], bytes[27]]), + f32::from_le_bytes([bytes[28], bytes[29], bytes[30], bytes[31]]), + f32::from_le_bytes([bytes[32], bytes[33], bytes[34], bytes[35]]), + f32::from_le_bytes([bytes[36], bytes[37], bytes[38], bytes[39]]), + ); + + TransformEntry { id, transform } + } + + fn as_bytes(&self) -> Self::BytesType { + let mut result: [u8; 40] = [0; 40]; + let (a, b, c, d) = uuid_to_u32_quartet(&self.id); + result[0..4].clone_from_slice(&a.to_le_bytes()); + result[4..8].clone_from_slice(&b.to_le_bytes()); + result[8..12].clone_from_slice(&c.to_le_bytes()); + result[12..16].clone_from_slice(&d.to_le_bytes()); + result[16..40].clone_from_slice(&self.transform.as_bytes()); + result + } + + // The generic trait doesn't know the size of the array. This is why the + // clone needs to be here even if it could be generic. + fn clone_to_slice(&self, slice: &mut [u8]) { + slice.clone_from_slice(&self.as_bytes()); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use uuid::uuid; + + #[test] + fn test_serialization() { + let entry = TransformEntry { + id: uuid!("550e8400-e29b-41d4-a716-446655440000"), + transform: Matrix::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0), + }; + + let bytes = entry.as_bytes(); + + assert_eq!(entry, TransformEntry::from_bytes(bytes)); + } +} diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index 2e92066a9..bd689aaef 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; +use crate::matrix; use skia_safe as skia; use uuid::Uuid; @@ -16,6 +17,7 @@ pub(crate) struct State<'a> { pub current_id: Option, pub current_shape: Option<&'a mut Shape>, pub shapes: HashMap, + pub modifiers: HashMap, } impl<'a> State<'a> { @@ -25,6 +27,7 @@ impl<'a> State<'a> { current_id: None, current_shape: None, shapes: HashMap::with_capacity(capacity), + modifiers: HashMap::new(), } } @@ -38,13 +41,13 @@ impl<'a> State<'a> { pub fn start_render_loop(&mut self, timestamp: i32) -> Result<(), String> { self.render_state - .start_render_loop(&mut self.shapes, timestamp)?; + .start_render_loop(&mut self.shapes, &self.modifiers, timestamp)?; Ok(()) } pub fn process_animation_frame(&mut self, timestamp: i32) -> Result<(), String> { self.render_state - .process_animation_frame(&mut self.shapes, timestamp)?; + .process_animation_frame(&mut self.shapes, &self.modifiers, timestamp)?; Ok(()) } diff --git a/render-wasm/src/utils.rs b/render-wasm/src/utils.rs index 75fb32d33..ae5ff986c 100644 --- a/render-wasm/src/utils.rs +++ b/render-wasm/src/utils.rs @@ -5,3 +5,12 @@ pub fn uuid_from_u32_quartet(a: u32, b: u32, c: u32, d: u32) -> Uuid { let lo: u64 = ((c as u64) << 32) | d as u64; Uuid::from_u64_pair(hi, lo) } + +pub fn uuid_to_u32_quartet(id: &Uuid) -> (u32, u32, u32, u32) { + let (hi, lo) = id.as_u64_pair(); + let hihi32 = (hi >> 32) as u32; + let hilo32 = hi as u32; + let lohi32 = (lo >> 32) as u32; + let lolo32 = lo as u32; + (hihi32, hilo32, lohi32, lolo32) +} diff --git a/render-wasm/watch_test b/render-wasm/watch_test new file mode 100755 index 000000000..398167e2e --- /dev/null +++ b/render-wasm/watch_test @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +_SCRIPT_DIR=$(dirname $0); + +export SKIA_BINARIES_URL="https://github.com/rust-skia/skia-binaries/releases/download/0.80.0/skia-binaries-9e7d2684a17084095aef-x86_64-unknown-linux-gnu-egl-gl-svg-textlayout-vulkan-wayland-webpd-webpe-x11.tar.gz" + +pushd $_SCRIPT_DIR; +cargo watch -x "test --bin render_wasm -- --show-output" +popd