From 99905d22866c19c1adfbd5615e0e54d8ac53b731 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 19 Nov 2024 14:46:09 +0100 Subject: [PATCH] :recycle: Move wasm shape proxy impl to frontend --- common/src/app/common/files/changes.cljc | 4 +- common/src/app/common/types/shape.cljc | 54 ++++- common/src/app/common/types/shape/impl.cljc | 185 ----------------- frontend/src/app/main/data/workspace.cljs | 4 +- frontend/src/app/main/features.cljs | 6 +- .../sidebar/options/menus/measures.cljs | 8 +- .../app/main/ui/workspace/viewport_wasm.cljs | 22 +- frontend/src/app/render_wasm.cljs | 194 +----------------- frontend/src/app/render_wasm/api.cljs | 185 +++++++++++++++++ frontend/src/app/render_wasm/shape.cljs | 141 +++++++++++++ 10 files changed, 398 insertions(+), 405 deletions(-) delete mode 100644 common/src/app/common/types/shape/impl.cljc create mode 100644 frontend/src/app/render_wasm/api.cljs create mode 100644 frontend/src/app/render_wasm/shape.cljs diff --git a/common/src/app/common/files/changes.cljc b/common/src/app/common/files/changes.cljc index 3dd63183f..04c11ccae 100644 --- a/common/src/app/common/files/changes.cljc +++ b/common/src/app/common/files/changes.cljc @@ -26,7 +26,6 @@ [app.common.types.pages-list :as ctpl] [app.common.types.shape :as cts] [app.common.types.shape-tree :as ctst] - [app.common.types.shape.impl :as shape.impl] [app.common.types.token :as cto] [app.common.types.token-theme :as ctot] [app.common.types.tokens-lib :as ctob] @@ -541,7 +540,8 @@ (when verify? (check-changes! items)) - (binding [*touched-changes* (volatile! #{}) shape.impl/*wasm-sync* true] + (binding [*touched-changes* (volatile! #{}) + cts/*wasm-sync* true] (let [result (reduce #(or (process-change %1 %2) %1) data items) result (reduce process-touched-change result @*touched-changes*)] ;; Validate result shapes (only on the backend) diff --git a/common/src/app/common/types/shape.cljc b/common/src/app/common/types/shape.cljc index ad9817490..622404c6e 100644 --- a/common/src/app/common/types/shape.cljc +++ b/common/src/app/common/types/shape.cljc @@ -6,6 +6,7 @@ (ns app.common.types.shape (:require + #?(:clj [app.common.fressian :as fres]) [app.common.colors :as clr] [app.common.data :as d] [app.common.geom.matrix :as gmt] @@ -13,15 +14,16 @@ [app.common.geom.proportions :as gpr] [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] + [app.common.record :as cr] [app.common.schema :as sm] [app.common.schema.generators :as sg] + [app.common.transit :as t] [app.common.types.color :as ctc] [app.common.types.grid :as ctg] [app.common.types.plugins :as ctpg] [app.common.types.shape.attrs :refer [default-color]] [app.common.types.shape.blur :as ctsb] [app.common.types.shape.export :as ctse] - [app.common.types.shape.impl :as impl] [app.common.types.shape.interactions :as ctsi] [app.common.types.shape.layout :as ctsl] [app.common.types.shape.path :as ctsp] @@ -31,9 +33,31 @@ [app.common.uuid :as uuid] [clojure.set :as set])) +(defonce ^:dynamic *wasm-sync* false) + +(defonce wasm-enabled? false) +(defonce wasm-create-shape (constantly nil)) + +;; Marker protocol +(defprotocol IShape) + +(cr/defrecord Shape [id name type x y width height rotation selrect points + transform transform-inverse parent-id frame-id flip-x flip-y] + IShape) + (defn shape? [o] - (impl/shape? o)) + #?(:cljs (implements? IShape o) + :clj (instance? Shape o))) + +(defn create-shape + "A low level function that creates a Shape data structure + from a attrs map without performing other transformations" + [attrs] + #?(:cljs (if ^boolean wasm-enabled? + (^function wasm-create-shape attrs) + (map->Shape attrs)) + :clj (map->Shape attrs))) (def stroke-caps-line #{:round :square}) (def stroke-caps-marker #{:line-arrow :triangle-arrow :square-marker :circle-marker :diamond-marker}) @@ -242,7 +266,7 @@ (defn- decode-shape [o] (if (map? o) - (impl/map->Shape o) + (create-shape o) o)) (defn- shape-generator @@ -266,7 +290,7 @@ (= type :bool)) (merge attrs1 shape attrs3) (merge attrs1 shape attrs2 attrs3))))) - (sg/fmap impl/map->Shape))) + (sg/fmap create-shape))) (def schema:shape [:and {:title "Shape" @@ -453,12 +477,6 @@ ;; NOTE: used for create ephimeral shapes for multiple selection :multiple minimal-multiple-attrs)) -(defn create-shape - "A low level function that creates a Shape data structure - from a attrs map without performing other transformations" - [attrs] - (impl/create-shape attrs)) - (defn- make-minimal-shape [type] (let [type (if (= type :curve) :path type) @@ -476,7 +494,7 @@ (assoc :parent-id uuid/zero) (assoc :rotation 0))] - (impl/create-shape attrs))) + (create-shape attrs))) (defn setup-rect "Initializes the selrect and points for a shape." @@ -531,3 +549,17 @@ (assoc :transform-inverse (gmt/matrix))) (gpr/setup-proportions)))) +;; --- SHAPE SERIALIZATION + +(t/add-handlers! + {:id "shape" + :class Shape + :wfn #(into {} %) + :rfn create-shape}) + +#?(:clj + (fres/add-handlers! + {:name "penpot/shape" + :class Shape + :wfn fres/write-map-like + :rfn (comp map->Shape fres/read-map-like)})) diff --git a/common/src/app/common/types/shape/impl.cljc b/common/src/app/common/types/shape/impl.cljc deleted file mode 100644 index 16893e7f0..000000000 --- a/common/src/app/common/types/shape/impl.cljc +++ /dev/null @@ -1,185 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC - -(ns app.common.types.shape.impl - (:require - #?(:clj [app.common.fressian :as fres]) - #?(:cljs [cuerdas.core :as str]) - [app.common.record :as cr] - [app.common.transit :as t] - [clojure.core :as c])) - -(defonce ^:dynamic *wasm-sync* false) -(defonce enabled-wasm-ready-shape false) -(defonce wasm-create-shape (constantly nil)) -(defonce wasm-use-shape (constantly nil)) -(defonce wasm-set-shape-selrect (constantly nil)) -(defonce wasm-set-shape-transform (constantly nil)) -(defonce wasm-set-shape-rotation (constantly nil)) -(defonce wasm-set-shape-fills (constantly nil)) -(defonce wasm-set-shape-blend-mode (constantly nil)) -(defonce wasm-set-shape-children (constantly nil)) - -(cr/defrecord Shape [id name type x y width height rotation selrect points - transform transform-inverse parent-id frame-id flip-x flip-y]) - -(declare ^:private impl-assoc) -(declare ^:private impl-conj) -(declare ^:private impl-dissoc) - -;; TODO: implement lazy MapEntry - -#?(:cljs - (deftype ShapeProxy [delegate] - Object - (toString [coll] - (str "{" (str/join ", " (for [[k v] coll] (str k " " v))) "}")) - - (equiv [this other] - (-equiv this other)) - - IWithMeta - (-with-meta [_ meta] - (ShapeProxy. (with-meta delegate meta))) - - IMeta - (-meta [_] (meta delegate)) - - ICollection - (-conj [coll entry] - (impl-conj coll entry)) - - IEquiv - (-equiv [coll other] - (c/equiv-map coll other)) - - IHash - (-hash [coll] (hash (into {} coll))) - - ISequential - - ISeqable - (-seq [_] - (c/-seq delegate)) - - ICounted - (-count [_] - (+ 1 (count delegate))) - - ILookup - (-lookup [coll k] - (-lookup coll k nil)) - - (-lookup [_ k not-found] - (c/-lookup delegate k not-found)) - - IFind - (-find [_ k] - (c/-find delegate k)) - - IAssociative - (-assoc [coll k v] - (impl-assoc coll k v)) - - (-contains-key? [_ k] - (contains? delegate k)) - - IMap - (-dissoc [coll k] - (impl-dissoc coll k)) - - IFn - (-invoke [coll k] - (-lookup coll k)) - - (-invoke [coll k not-found] - (-lookup coll k not-found)) - - IPrintWithWriter - (-pr-writer [_ writer _] - (-write writer (str "#penpot/shape " (:id delegate)))))) - -(defn shape? - [o] - #?(:clj (instance? Shape o) - :cljs (or (instance? Shape o) - (instance? ShapeProxy o)))) - -;; --- SHAPE IMPL - -#?(:cljs - (defn- impl-assoc - [coll k v] - (when *wasm-sync* - (wasm-use-shape (:id coll)) - (case k - :selrect (wasm-set-shape-selrect v) - :rotation (wasm-set-shape-rotation v) - :transform (wasm-set-shape-transform v) - :fills (wasm-set-shape-fills v) - :blend-mode (wasm-set-shape-blend-mode v) - :shapes (wasm-set-shape-children v) - nil)) - (let [delegate (.-delegate ^ShapeProxy coll) - delegate' (assoc delegate k v)] - (if (identical? delegate' delegate) - coll - (ShapeProxy. delegate'))))) - -#?(:cljs - (defn- impl-dissoc - [coll k] - (let [delegate (.-delegate ^ShapeProxy coll) - delegate' (dissoc delegate k)] - (if (identical? delegate delegate') - coll - (ShapeProxy. delegate'))))) - -#?(:cljs - (defn- impl-conj - [coll entry] - (if (vector? entry) - (-assoc coll (-nth entry 0) (-nth entry 1)) - (loop [ret coll es (seq entry)] - (if (nil? es) - ret - (let [e (first es)] - (if (vector? e) - (recur (-assoc ret (-nth e 0) (-nth e 1)) - (next es)) - (throw (js/Error. "conj on a map takes map entries or seqables of map entries"))))))))) - -(defn create-shape - "Instanciate a shape from a map" - [attrs] - #?(:cljs - (if enabled-wasm-ready-shape - (ShapeProxy. attrs) - (map->Shape attrs)) - - :clj (map->Shape attrs))) - -;; --- SHAPE SERIALIZATION - -(t/add-handlers! - {:id "shape" - :class Shape - :wfn #(into {} %) - :rfn create-shape}) - -#?(:cljs - (t/add-handlers! - {:id "shape" - :class ShapeProxy - :wfn #(into {} %) - :rfn create-shape})) - -#?(:clj - (fres/add-handlers! - {:name "penpot/shape" - :class Shape - :wfn fres/write-map-like - :rfn (comp create-shape fres/read-map-like)})) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 6b923dbb0..f309e7ea5 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -76,7 +76,7 @@ [app.main.repo :as rp] [app.main.streams :as ms] [app.main.worker :as uw] - [app.render-wasm :as render.wasm] + [app.render-wasm :as wasm] [app.util.dom :as dom] [app.util.globals :as ug] [app.util.http :as http] @@ -270,7 +270,7 @@ ;; load. We need to wait the promise to be resolved ;; before continue with the next workspace loading ;; steps - (->> (rx/from render.wasm/module) + (->> (rx/from wasm/module) (rx/ignore)) (->> (rp/cmd! :get-team {:id (:team-id project)}) (rx/mapcat (fn [team] diff --git a/frontend/src/app/main/features.cljs b/frontend/src/app/main/features.cljs index 16c98ba62..16fc4222b 100644 --- a/frontend/src/app/main/features.cljs +++ b/frontend/src/app/main/features.cljs @@ -12,7 +12,7 @@ [app.common.logging :as log] [app.config :as cf] [app.main.store :as st] - [app.render-wasm :as render.wasm] + [app.render-wasm :as wasm] [beicon.v2.core :as rx] [clojure.set :as set] [cuerdas.core :as str] @@ -126,8 +126,8 @@ (effect [_ state _] (let [features (get-team-enabled-features state)] (if (contains? features "render-wasm/v1") - (render.wasm/initialize true) - (render.wasm/initialize false)) + (wasm/initialize true) + (wasm/initialize false)) (log/inf :hint "initialized" :enabled (str/join "," features) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs index fb9dc299b..3be948976 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs @@ -10,7 +10,7 @@ [app.common.data :as d] [app.common.geom.shapes :as gsh] [app.common.logic.shapes :as cls] - [app.common.types.shape.impl :as shape.impl] + [app.common.types.shape :as cts] [app.common.types.shape.layout :as ctl] [app.common.types.shape.radius :as ctsr] [app.common.types.tokens-lib :as ctob] @@ -249,7 +249,7 @@ (fn [value attr] (let [token-value (wtc/maybe-resolve-token-value value) undo-id (js/Symbol)] - (binding [shape.impl/*wasm-sync* true] + (binding [cts/*wasm-sync* true] (if-not design-tokens? (st/emit! (udw/trigger-bounding-box-cloaking ids) (udw/update-dimensions ids attr (or token-value value))) @@ -285,7 +285,7 @@ (mf/deps ids) (fn [value attr] (st/emit! (udw/trigger-bounding-box-cloaking ids)) - (binding [shape.impl/*wasm-sync* true] + (binding [cts/*wasm-sync* true] (doall (map #(do-position-change %1 %2 value attr) shapes frames))))) ;; ROTATION @@ -294,7 +294,7 @@ (mf/use-fn (mf/deps ids) (fn [value] - (binding [shape.impl/*wasm-sync* true] + (binding [cts/*wasm-sync* true] (st/emit! (udw/trigger-bounding-box-cloaking ids) (udw/increase-rotation ids value))))) diff --git a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs index 0143cba50..5ad641afd 100644 --- a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs +++ b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs @@ -12,8 +12,8 @@ [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.impl :as shape.impl] [app.common.types.shape.layout :as ctl] [app.main.data.workspace.modifiers :as dwm] [app.main.features :as features] @@ -49,7 +49,7 @@ [app.main.ui.workspace.viewport.utils :as utils] [app.main.ui.workspace.viewport.viewport-ref :refer [create-viewport-ref]] [app.main.ui.workspace.viewport.widgets :as widgets] - [app.render-wasm :as render.wasm] + [app.render-wasm.api :as wasm.api] [app.util.debug :as dbg] [beicon.v2.core :as rx] [promesa.core :as p] @@ -113,7 +113,7 @@ text-modifiers (mf/deref refs/workspace-text-modifier) objects-modified (mf/with-memo [base-objects text-modifiers modifiers] - (binding [shape.impl/*wasm-sync* true] + (binding [cts/*wasm-sync* true] (apply-modifiers-to-selected selected base-objects text-modifiers modifiers))) selected-shapes (keep (d/getf objects-modified) selected) @@ -275,26 +275,26 @@ (mf/with-effect [] (when-let [canvas (mf/ref-val canvas-ref)] - (->> render.wasm/module + (->> wasm.api/module (p/fmap (fn [ready?] (when ready? (reset! canvas-init? true) - (render.wasm/assign-canvas canvas))))) + (wasm.api/assign-canvas canvas))))) (fn [] - (render.wasm/clear-canvas)))) + (wasm.api/clear-canvas)))) (mf/with-effect [base-objects canvas-init?] (when @canvas-init? - (render.wasm/set-objects base-objects) - (render.wasm/draw-objects zoom vbox))) + (wasm.api/set-objects base-objects) + (wasm.api/draw-objects zoom vbox))) (mf/with-effect [modifiers canvas-init?] (when (and @canvas-init? modifiers) - (render.wasm/draw-objects zoom vbox))) + (wasm.api/draw-objects zoom vbox))) (mf/with-effect [vbox canvas-init?] - (let [frame-id (when @canvas-init? (render.wasm/draw-objects zoom vbox))] - (partial render.wasm/cancel-draw frame-id))) + (let [frame-id (when @canvas-init? (wasm.api/draw-objects zoom vbox))] + (partial wasm.api/cancel-draw frame-id))) (hooks/setup-dom-events zoom disable-paste in-viewport? read-only? drawing-tool drawing-path?) (hooks/setup-viewport-size vport viewport-ref) diff --git a/frontend/src/app/render_wasm.cljs b/frontend/src/app/render_wasm.cljs index ddfd65c30..c0eb40231 100644 --- a/frontend/src/app/render_wasm.cljs +++ b/frontend/src/app/render_wasm.cljs @@ -7,193 +7,13 @@ (ns app.render-wasm "A WASM based render API" (:require - [app.common.data.macros :as dm] - [app.common.types.shape.impl :as ctsi] - [app.common.uuid :as uuid] - [app.config :as cf] - [app.render-wasm.helpers :as h] - [promesa.core :as p])) + [app.common.types.shape :as shape] + [app.render-wasm.api :as api] + [app.render-wasm.shape :as wasm.shape])) + +(def module api/module) (defn initialize [enabled?] - (set! app.common.types.shape.impl/enabled-wasm-ready-shape enabled?)) - -(defonce internal-module #js {}) - -(defn create-shape - [id] - (let [buffer (uuid/get-u32 id)] - (h/call internal-module "_create_shape" - (aget buffer 0) (aget buffer 1) (aget buffer 2) (aget buffer 3)))) - -(defn use-shape - [id] - (let [buffer (uuid/get-u32 id)] - (h/call internal-module "_use_shape" - (aget buffer 0) - (aget buffer 1) - (aget buffer 2) - (aget buffer 3)))) - -(defn set-shape-selrect - [selrect] - (h/call internal-module "_set_shape_selrect" - (dm/get-prop selrect :x1) - (dm/get-prop selrect :y1) - (dm/get-prop selrect :x2) - (dm/get-prop selrect :y2))) - -(defn set-shape-transform - [transform] - (h/call internal-module "_set_shape_transform" - (dm/get-prop transform :a) - (dm/get-prop transform :b) - (dm/get-prop transform :c) - (dm/get-prop transform :d) - (dm/get-prop transform :e) - (dm/get-prop transform :f))) - -(defn set-shape-rotation - [rotation] - (h/call internal-module "_set_shape_rotation" rotation)) - -(defn set-shape-children - [shape-ids] - (h/call internal-module "_clear_shape_children") - (run! (fn [id] - (let [buffer (uuid/get-u32 id)] - (h/call internal-module "_add_shape_child" - (aget buffer 0) - (aget buffer 1) - (aget buffer 2) - (aget buffer 3)))) - shape-ids)) - -(defn set-shape-fills - [fills] - (h/call internal-module "_clear_shape_fills") - (run! (fn [fill] - (let [opacity (:fill-opacity fill) - color (:fill-color fill)] - (when ^boolean color - (let [rgb (js/parseInt (subs color 1) 16) - r (bit-shift-right rgb 16) - g (bit-and (bit-shift-right rgb 8) 255) - b (bit-and rgb 255)] - (h/call internal-module "_add_shape_solid_fill" r g b opacity))))) - fills)) - -(defn- translate-blend-mode - [blend-mode] - (case blend-mode - :normal 3 - :darken 16 - :multiply 24 - :color-burn 19 - :lighten 17 - :screen 14 - :color-dodge 18 - :overlay 15 - :soft-light 21 - :hard-light 20 - :difference 22 - :exclusion 23 - :hue 25 - :saturation 26 - :color 27 - :luminosity 28 - 3)) - -(defn set-shape-blend-mode - [blend-mode] - ;; These values correspond to skia::BlendMode representation - ;; https://rust-skia.github.io/doc/skia_safe/enum.BlendMode.html - (h/call internal-module "_set_shape_blend_mode" (translate-blend-mode blend-mode))) - -(defn set-objects - [objects] - (let [shapes (into [] (vals objects)) - - total-shapes (count shapes)] - (loop [index 0] - (when (< index total-shapes) - (let [shape (nth shapes index) - id (dm/get-prop shape :id) - selrect (dm/get-prop shape :selrect) - rotation (dm/get-prop shape :rotation) - transform (dm/get-prop shape :transform) - fills (dm/get-prop shape :fills) - children (dm/get-prop shape :shapes) - blend-mode (dm/get-prop shape :blend-mode)] - (use-shape id) - (set-shape-selrect selrect) - (set-shape-rotation rotation) - (set-shape-transform transform) - (set-shape-fills fills) - (set-shape-blend-mode blend-mode) - (set-shape-children children) - (recur (inc index))))))) - -(defn draw-objects - [zoom vbox] - (js/requestAnimationFrame - (fn [] - (let [pan-x (- (dm/get-prop vbox :x)) - pan-y (- (dm/get-prop vbox :y))] - (h/call internal-module "_draw_all_shapes" zoom pan-x pan-y))))) - -(defn cancel-draw - [frame-id] - (when (some? frame-id) - (js/cancelAnimationFrame frame-id))) - -(def ^:private canvas-options - #js {:antialias true - :depth true - :stencil true - :alpha true}) - -(defn clear-canvas - [] - ;; TODO: perform corresponding cleaning - ) - -(defn assign-canvas - [canvas] - (let [gl (unchecked-get internal-module "GL") - init-fn (unchecked-get internal-module "_init") - - context (.getContext ^js canvas "webgl2" canvas-options) - - ;; Register the context with emscripten - handle (.registerContext ^js gl context #js {"majorVersion" 2})] - (.makeContextCurrent ^js gl handle) - ;; Initialize Skia - (^function init-fn (.-width ^js canvas) - (.-height ^js canvas)) - (set! (.-width canvas) (.-clientWidth ^js canvas)) - (set! (.-height canvas) (.-clientHeight ^js canvas)))) - -(defonce module - (if (exists? js/dynamicImport) - (let [uri (cf/resolve-static-asset "js/render_wasm.js")] - (->> (js/dynamicImport (str uri)) - (p/mcat (fn [module] - (let [default (unchecked-get module "default")] - (default)))) - (p/fmap (fn [module] - (set! internal-module module) - true)) - (p/merr (fn [cause] - (js/console.error cause) - (p/resolved false))))) - (p/resolved false))) - -(set! app.common.types.shape.impl/wasm-create-shape create-shape) -(set! app.common.types.shape.impl/wasm-use-shape use-shape) -(set! app.common.types.shape.impl/wasm-set-shape-selrect set-shape-selrect) -(set! app.common.types.shape.impl/wasm-set-shape-transform set-shape-transform) -(set! app.common.types.shape.impl/wasm-set-shape-rotation set-shape-rotation) -(set! app.common.types.shape.impl/wasm-set-shape-fills set-shape-fills) -(set! app.common.types.shape.impl/wasm-set-shape-blend-mode set-shape-blend-mode) -(set! app.common.types.shape.impl/wasm-set-shape-children set-shape-children) + (set! app.common.types.shape/wasm-enabled? enabled?) + (set! app.common.types.shape/wasm-create-shape wasm.shape/create-shape)) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs new file mode 100644 index 000000000..06ff6f95b --- /dev/null +++ b/frontend/src/app/render_wasm/api.cljs @@ -0,0 +1,185 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.render-wasm.api + "A WASM based render API" + (:require + [app.common.data.macros :as dm] + [app.common.uuid :as uuid] + [app.config :as cf] + [app.render-wasm.helpers :as h] + [promesa.core :as p])) + +(defonce internal-module #js {}) + +(defn create-shape + [id] + (let [buffer (uuid/get-u32 id)] + (h/call internal-module "_create_shape" + (aget buffer 0) (aget buffer 1) (aget buffer 2) (aget buffer 3)))) + +(defn use-shape + [id] + (let [buffer (uuid/get-u32 id)] + (h/call internal-module "_use_shape" + (aget buffer 0) + (aget buffer 1) + (aget buffer 2) + (aget buffer 3)))) + +(defn set-shape-selrect + [selrect] + (h/call internal-module "_set_shape_selrect" + (dm/get-prop selrect :x1) + (dm/get-prop selrect :y1) + (dm/get-prop selrect :x2) + (dm/get-prop selrect :y2))) + +(defn set-shape-transform + [transform] + (h/call internal-module "_set_shape_transform" + (dm/get-prop transform :a) + (dm/get-prop transform :b) + (dm/get-prop transform :c) + (dm/get-prop transform :d) + (dm/get-prop transform :e) + (dm/get-prop transform :f))) + +(defn set-shape-rotation + [rotation] + (h/call internal-module "_set_shape_rotation" rotation)) + +(defn set-shape-children + [shape-ids] + (h/call internal-module "_clear_shape_children") + (run! (fn [id] + (let [buffer (uuid/get-u32 id)] + (h/call internal-module "_add_shape_child" + (aget buffer 0) + (aget buffer 1) + (aget buffer 2) + (aget buffer 3)))) + shape-ids)) + +(defn set-shape-fills + [fills] + (h/call internal-module "_clear_shape_fills") + (run! (fn [fill] + (let [opacity (:fill-opacity fill) + color (:fill-color fill)] + (when ^boolean color + (let [rgb (js/parseInt (subs color 1) 16) + r (bit-shift-right rgb 16) + g (bit-and (bit-shift-right rgb 8) 255) + b (bit-and rgb 255)] + (h/call internal-module "_add_shape_solid_fill" r g b opacity))))) + fills)) + +(defn- translate-blend-mode + [blend-mode] + (case blend-mode + :normal 3 + :darken 16 + :multiply 24 + :color-burn 19 + :lighten 17 + :screen 14 + :color-dodge 18 + :overlay 15 + :soft-light 21 + :hard-light 20 + :difference 22 + :exclusion 23 + :hue 25 + :saturation 26 + :color 27 + :luminosity 28 + 3)) + +(defn set-shape-blend-mode + [blend-mode] + ;; These values correspond to skia::BlendMode representation + ;; https://rust-skia.github.io/doc/skia_safe/enum.BlendMode.html + (h/call internal-module "_set_shape_blend_mode" (translate-blend-mode blend-mode))) + +(defn set-objects + [objects] + (let [shapes (into [] (vals objects)) + + total-shapes (count shapes)] + (loop [index 0] + (when (< index total-shapes) + (let [shape (nth shapes index) + id (dm/get-prop shape :id) + selrect (dm/get-prop shape :selrect) + rotation (dm/get-prop shape :rotation) + transform (dm/get-prop shape :transform) + fills (dm/get-prop shape :fills) + children (dm/get-prop shape :shapes) + blend-mode (dm/get-prop shape :blend-mode)] + (use-shape id) + (set-shape-selrect selrect) + (set-shape-rotation rotation) + (set-shape-transform transform) + (set-shape-fills fills) + (set-shape-blend-mode blend-mode) + (set-shape-children children) + (recur (inc index))))))) + +(defn draw-objects + [zoom vbox] + (js/requestAnimationFrame + (fn [] + (let [pan-x (- (dm/get-prop vbox :x)) + pan-y (- (dm/get-prop vbox :y))] + (h/call internal-module "_draw_all_shapes" zoom pan-x pan-y))))) + +(defn cancel-draw + [frame-id] + (when (some? frame-id) + (js/cancelAnimationFrame frame-id))) + +(def ^:private canvas-options + #js {:antialias true + :depth true + :stencil true + :alpha true}) + +(defn clear-canvas + [] + ;; TODO: perform corresponding cleaning + ) + +(defn assign-canvas + [canvas] + (let [gl (unchecked-get internal-module "GL") + init-fn (unchecked-get internal-module "_init") + + context (.getContext ^js canvas "webgl2" canvas-options) + + ;; Register the context with emscripten + handle (.registerContext ^js gl context #js {"majorVersion" 2})] + (.makeContextCurrent ^js gl handle) + ;; Initialize Skia + (^function init-fn (.-width ^js canvas) + (.-height ^js canvas)) + (set! (.-width canvas) (.-clientWidth ^js canvas)) + (set! (.-height canvas) (.-clientHeight ^js canvas)))) + +(defonce module + (if (exists? js/dynamicImport) + (let [uri (cf/resolve-static-asset "js/render_wasm.js")] + (->> (js/dynamicImport (str uri)) + (p/mcat (fn [module] + (let [default (unchecked-get module "default")] + (default)))) + (p/fmap (fn [module] + (set! internal-module module) + true)) + (p/merr (fn [cause] + (js/console.error cause) + (p/resolved false))))) + (p/resolved false))) diff --git a/frontend/src/app/render_wasm/shape.cljs b/frontend/src/app/render_wasm/shape.cljs new file mode 100644 index 000000000..a754848f0 --- /dev/null +++ b/frontend/src/app/render_wasm/shape.cljs @@ -0,0 +1,141 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.render-wasm.shape + (:require + [app.common.transit :as t] + [app.common.types.shape :as shape] + [app.render-wasm.api :as api] + [clojure.core :as c] + [cuerdas.core :as str])) + +(declare ^:private impl-assoc) +(declare ^:private impl-conj) +(declare ^:private impl-dissoc) + +(deftype ShapeProxy [delegate] + Object + (toString [coll] + (str "{" (str/join ", " (for [[k v] coll] (str k " " v))) "}")) + + (equiv [this other] + (-equiv this other)) + + ;; Marker protocol + shape/IShape + + IWithMeta + (-with-meta [_ meta] + (ShapeProxy. (with-meta delegate meta))) + + IMeta + (-meta [_] (meta delegate)) + + ICollection + (-conj [coll entry] + (impl-conj coll entry)) + + IEquiv + (-equiv [coll other] + (c/equiv-map coll other)) + + IHash + (-hash [coll] (hash (into {} coll))) + + ISequential + + ISeqable + (-seq [_] + (c/-seq delegate)) + + ICounted + (-count [_] + (+ 1 (count delegate))) + + ILookup + (-lookup [coll k] + (-lookup coll k nil)) + + (-lookup [_ k not-found] + (c/-lookup delegate k not-found)) + + IFind + (-find [_ k] + (c/-find delegate k)) + + IAssociative + (-assoc [coll k v] + (impl-assoc coll k v)) + + (-contains-key? [_ k] + (contains? delegate k)) + + IMap + (-dissoc [coll k] + (impl-dissoc coll k)) + + IFn + (-invoke [coll k] + (-lookup coll k)) + + (-invoke [coll k not-found] + (-lookup coll k not-found)) + + IPrintWithWriter + (-pr-writer [_ writer _] + (-write writer (str "#penpot/shape " (:id delegate))))) + +;; --- SHAPE IMPL + +(defn- impl-assoc + [self k v] + (when ^boolean shape/*wasm-sync* + (api/use-shape (:id self)) + (case k + :selrect (api/set-shape-selrect v) + :rotation (api/set-shape-rotation v) + :transform (api/set-shape-transform v) + :fills (api/set-shape-fills v) + :blend-mode (api/set-shape-blend-mode v) + :shapes (api/set-shape-children v) + nil)) + (let [delegate (.-delegate ^ShapeProxy self) + delegate' (assoc delegate k v)] + (if (identical? delegate' delegate) + self + (ShapeProxy. delegate')))) + +(defn- impl-dissoc + [self k] + (let [delegate (.-delegate ^ShapeProxy self) + delegate' (dissoc delegate k)] + (if (identical? delegate delegate') + self + (ShapeProxy. delegate')))) + +(defn- impl-conj + [self entry] + (if (vector? entry) + (-assoc self (-nth entry 0) (-nth entry 1)) + (loop [ret self es (seq entry)] + (if (nil? es) + ret + (let [e (first es)] + (if (vector? e) + (recur (-assoc ret (-nth e 0) (-nth e 1)) + (next es)) + (throw (js/Error. "conj on a map takes map entries or seqables of map entries")))))))) + +(defn create-shape + "Instanciate a shape from a map" + [attrs] + (ShapeProxy. attrs)) + +(t/add-handlers! + ;; We only add a write handler, read handler uses the dynamic dispatch + {:id "shape" + :class ShapeProxy + :wfn #(into {} %)})