mirror of
https://github.com/penpot/penpot.git
synced 2026-11-05 01:20:38 +00:00
* ⚡ Avoid decoding the opened page twice for its frame index The frame index was built over a with-meta copy, so pages got decoded again. Read it from the page objects themselves and fix the with-meta identity check. * ⚡ Intern keywords when decoding transit on the frontend Each shape is decoded on its own, so every shape had its own key keywords. Cache them in the read handler so all decoded data shares one instance. * ⚡ Use an array map as the wasm shape proxy delegate A hash map with ~35 keys costs ~3 KB of nodes per shape on large pages. An array map holds the same entries in one array, at a small lookup cost.
462 lines
12 KiB
Clojure
462 lines
12 KiB
Clojure
;; 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 SUBSIDIARY SL
|
|
|
|
(ns app.render-wasm.shape
|
|
(:require
|
|
[app.common.data :as d]
|
|
[app.common.data.macros :as dm]
|
|
[app.common.files.helpers :as cfh]
|
|
[app.common.render-wasm.api.select :as wselect]
|
|
[app.common.render-wasm.svg-derived :as svg-derived]
|
|
[app.common.transit :as t]
|
|
[app.common.types.shape :as shape]
|
|
[app.common.types.shape.layout :as ctl]
|
|
[app.main.refs :as refs]
|
|
[app.render-wasm.api :as api]
|
|
[beicon.v2.core :as rx]
|
|
[cljs.core :as c]
|
|
[cuerdas.core :as str]))
|
|
|
|
(declare ^:private impl-assoc)
|
|
(declare ^:private impl-conj)
|
|
(declare ^:private impl-dissoc)
|
|
|
|
(defn shape-in-current-page?
|
|
"Check if a shape is in the current page by looking up the current page objects"
|
|
[shape-id]
|
|
(let [objects (deref refs/workspace-page-objects)]
|
|
(contains? objects shape-id)))
|
|
|
|
(defn map-entry
|
|
[k v]
|
|
(cljs.core/MapEntry. k v nil))
|
|
|
|
(deftype ShapeProxy [id type delegate]
|
|
Object
|
|
(toString [coll]
|
|
(str "{" (str/join ", " (for [[k v] coll] (str k " " v))) "}"))
|
|
|
|
(equiv [this other]
|
|
(-equiv this other))
|
|
|
|
;; Marker protocol
|
|
shape/IShape
|
|
|
|
c/IWithMeta
|
|
(-with-meta [_ meta]
|
|
(ShapeProxy. id type (with-meta delegate meta)))
|
|
|
|
c/IMeta
|
|
(-meta [_] (meta delegate))
|
|
|
|
c/ICollection
|
|
(-conj [coll entry]
|
|
(impl-conj coll entry))
|
|
|
|
c/IEmptyableCollection
|
|
(-empty [_]
|
|
(ShapeProxy. nil nil nil))
|
|
|
|
c/IEquiv
|
|
(-equiv [coll other]
|
|
(c/equiv-map coll other))
|
|
|
|
c/IHash
|
|
(-hash [coll]
|
|
(hash (into {} coll)))
|
|
|
|
c/ISequential
|
|
c/ISeqable
|
|
(-seq [_]
|
|
(cons (map-entry :id id)
|
|
(cons (map-entry :type type)
|
|
(c/-seq delegate))))
|
|
|
|
c/ICounted
|
|
(-count [_]
|
|
(+ 1 (count delegate)))
|
|
|
|
c/IKVReduce
|
|
(-kv-reduce [_ f init]
|
|
(let [acc (f init :id id)]
|
|
(if (reduced? acc)
|
|
@acc
|
|
(let [acc (f acc :type type)]
|
|
(if (reduced? acc)
|
|
@acc
|
|
(reduce-kv f acc delegate))))))
|
|
|
|
c/ILookup
|
|
(-lookup [coll k]
|
|
(-lookup coll k nil))
|
|
|
|
(-lookup [_ k not-found]
|
|
(case k
|
|
:id id
|
|
:type type
|
|
(c/-lookup delegate k not-found)))
|
|
|
|
c/IFind
|
|
(-find [_ k]
|
|
(case k
|
|
:id
|
|
(map-entry :id id)
|
|
:type
|
|
(map-entry :type type)
|
|
(c/-find delegate k)))
|
|
|
|
c/IAssociative
|
|
(-assoc [coll k v]
|
|
(impl-assoc coll k v))
|
|
|
|
(-contains-key? [_ k]
|
|
(or (= k :id)
|
|
(= k :type)
|
|
(contains? delegate k)))
|
|
|
|
c/IMap
|
|
(-dissoc [coll k]
|
|
(impl-dissoc coll k))
|
|
|
|
c/IFn
|
|
(-invoke [coll k]
|
|
(-lookup coll k nil))
|
|
|
|
(-invoke [coll k not-found]
|
|
(-lookup coll k not-found))
|
|
|
|
c/IPrintWithWriter
|
|
(-pr-writer [_ writer _]
|
|
(-write writer (str "#penpot/shape " (:id delegate)))))
|
|
|
|
;; --- SHAPE IMPL
|
|
;; When an attribute is sent to WASM it could still be pending some side operations
|
|
;; for example: font loading when changing a text, this is an async operation that will
|
|
;; resolve eventually.
|
|
;; The `set-wasm-attr!` can return a list of callbacks to be executed in a second pass.
|
|
(defn- set-wasm-attr!
|
|
[shape k]
|
|
(when (api/initialized?)
|
|
(let [shape (case k
|
|
:svg-attrs (svg-derived/apply-svg-derived (assoc shape :svg-attrs (get shape :svg-attrs)))
|
|
(:fills :blur :shadow) (svg-derived/apply-svg-derived shape)
|
|
shape)
|
|
v (get shape k)
|
|
id (get shape :id)]
|
|
(case k
|
|
:parent-id
|
|
(api/set-parent-id v)
|
|
|
|
:type
|
|
(do
|
|
(api/set-shape-type v)
|
|
(when (or (= v :path) (= v :bool))
|
|
(api/set-shape-path-content (:content shape))))
|
|
|
|
:bool-type
|
|
(api/set-shape-bool-type v)
|
|
|
|
:selrect
|
|
(do
|
|
(api/set-shape-selrect v)
|
|
(when (cfh/svg-raw-shape? shape)
|
|
(api/set-shape-svg-raw-content (api/get-static-markup shape))))
|
|
|
|
:show-content
|
|
(if (cfh/frame-shape? shape)
|
|
(api/set-shape-clip-content (not v))
|
|
(api/set-shape-clip-content false))
|
|
|
|
:rotation
|
|
(api/set-shape-rotation v)
|
|
|
|
:transform
|
|
(api/set-shape-transform v)
|
|
|
|
:fills
|
|
(api/set-shape-fills id v false)
|
|
|
|
:strokes
|
|
(into [] (api/set-shape-strokes id v false))
|
|
|
|
:blend-mode
|
|
(api/set-shape-blend-mode v)
|
|
|
|
:opacity
|
|
(api/set-shape-opacity v)
|
|
|
|
:hidden
|
|
(api/set-shape-hidden v)
|
|
|
|
:shapes
|
|
(api/set-shape-children v)
|
|
|
|
:blur
|
|
(api/set-shape-blur v)
|
|
|
|
:background-blur
|
|
(api/set-shape-background-blur v)
|
|
|
|
:shadow
|
|
(api/set-shape-shadows v)
|
|
|
|
:constraints-h
|
|
(api/set-constraints-h v)
|
|
|
|
:constraints-v
|
|
(api/set-constraints-v v)
|
|
|
|
:r1
|
|
(api/set-shape-corners
|
|
[v
|
|
(dm/get-prop shape :r2)
|
|
(dm/get-prop shape :r3)
|
|
(dm/get-prop shape :r4)])
|
|
|
|
:r2
|
|
(api/set-shape-corners
|
|
[(dm/get-prop shape :r1)
|
|
v
|
|
(dm/get-prop shape :r3)
|
|
(dm/get-prop shape :r4)])
|
|
|
|
:r3
|
|
(api/set-shape-corners
|
|
[(dm/get-prop shape :r1)
|
|
(dm/get-prop shape :r2)
|
|
v
|
|
(dm/get-prop shape :r4)])
|
|
|
|
:r4
|
|
(api/set-shape-corners
|
|
[(dm/get-prop shape :r1)
|
|
(dm/get-prop shape :r2)
|
|
(dm/get-prop shape :r3)
|
|
v])
|
|
|
|
:svg-attrs
|
|
(do
|
|
(api/set-shape-svg-attrs v)
|
|
;; Always update fills/blur/shadow to clear previous state if filters disappear
|
|
(api/set-shape-fills id (:fills shape) false)
|
|
(api/set-shape-blur (:blur shape))
|
|
(api/set-shape-background-blur (:background-blur shape))
|
|
(api/set-shape-shadows (:shadow shape)))
|
|
|
|
:masked-group
|
|
(when (cfh/group-shape? shape)
|
|
(api/set-masked (boolean (:masked-group shape))))
|
|
|
|
:content
|
|
(cond
|
|
(or (cfh/path-shape? shape)
|
|
(cfh/bool-shape? shape))
|
|
(api/set-shape-path-content v)
|
|
|
|
(cfh/svg-raw-shape? shape)
|
|
(api/set-shape-svg-raw-content (api/get-static-markup shape))
|
|
|
|
(cfh/text-shape? shape)
|
|
(let [text-content-pending (api/set-shape-text-content id v)
|
|
pending-thumbnails (vec text-content-pending)
|
|
pending-full (vec (api/set-shape-text-images id v))
|
|
text-font-state (api/text-font-state-for-shape shape)]
|
|
;; FIXME: this is a hack to process the pending tasks
|
|
;; asynchronously we should probably modify set-wasm-attr!
|
|
;; to return a list of callbacks to be executed in a
|
|
;; second pass.
|
|
(api/process-pending [shape] pending-thumbnails pending-full text-font-state api/noop-fn)
|
|
nil))
|
|
|
|
:grow-type
|
|
(api/set-shape-grow-type v)
|
|
|
|
(:layout-item-align-self
|
|
:layout-item-margin
|
|
:layout-item-margin-type
|
|
:layout-item-h-sizing
|
|
:layout-item-v-sizing
|
|
:layout-item-max-h
|
|
:layout-item-min-h
|
|
:layout-item-max-w
|
|
:layout-item-min-w
|
|
:layout-item-absolute
|
|
:layout-item-z-index)
|
|
(api/set-layout-data shape)
|
|
|
|
:layout-grid-rows
|
|
(api/set-grid-layout-rows v)
|
|
|
|
:layout-grid-columns
|
|
(api/set-grid-layout-columns v)
|
|
|
|
:layout-grid-cells
|
|
(api/set-grid-layout-cells v)
|
|
|
|
:layout
|
|
(do
|
|
(api/clear-layout)
|
|
(cond
|
|
(ctl/grid-layout? shape)
|
|
(api/set-grid-layout shape)
|
|
|
|
(ctl/flex-layout? shape)
|
|
(api/set-flex-layout shape))
|
|
(api/set-layout-data shape))
|
|
|
|
(:layout-flex-dir
|
|
:layout-gap-type
|
|
:layout-gap
|
|
:layout-align-items
|
|
:layout-align-content
|
|
:layout-justify-items
|
|
:layout-justify-content
|
|
:layout-wrap-type
|
|
:layout-padding-type
|
|
:layout-padding)
|
|
(cond
|
|
(ctl/grid-layout? shape)
|
|
(api/set-grid-layout-data shape)
|
|
|
|
(ctl/flex-layout? shape)
|
|
(api/set-flex-layout shape))
|
|
|
|
;; Property not in WASM
|
|
nil))))
|
|
|
|
(defn process-shape!
|
|
[shape properties]
|
|
(let [shape-id (dm/get-prop shape :id)]
|
|
(if (shape-in-current-page? shape-id)
|
|
(do
|
|
(wselect/use-shape shape-id)
|
|
(->> properties
|
|
(mapcat #(set-wasm-attr! shape %))
|
|
(d/index-by :key :callback)
|
|
(vals)
|
|
(rx/from)
|
|
(rx/mapcat (fn [callback] (callback)))
|
|
(rx/reduce conj [])))
|
|
(rx/empty))))
|
|
|
|
(defn process-shape-changes!
|
|
[objects shape-changes]
|
|
(when (api/initialized?)
|
|
(let [shape-changes
|
|
(->> shape-changes
|
|
;; We don't need to update the model for shapes not in the current page
|
|
(filter (fn [[shape-id _]] (shape-in-current-page? shape-id))))]
|
|
(when (d/not-empty? shape-changes)
|
|
(->> (rx/from shape-changes)
|
|
(rx/mapcat (fn [[shape-id props]] (process-shape! (get objects shape-id) props)))
|
|
(rx/reduce conj [])
|
|
(rx/subs! (fn [_] (api/request-render "set-wasm-attrs"))))))))
|
|
|
|
;; `conj` empty set initialization
|
|
(def conj* (fnil conj (d/ordered-set)))
|
|
|
|
(defn- track-change!
|
|
[self k]
|
|
(when-let [changes shape/*shape-changes*]
|
|
(let [id (:id self)]
|
|
(.set changes id (conj* (.get changes id) k)))))
|
|
|
|
(defn- assoc-delegate
|
|
"Keeps an array map delegate as an array map when adding a new key."
|
|
[delegate k v]
|
|
(if (and (instance? PersistentArrayMap delegate)
|
|
(not (contains? delegate k)))
|
|
(PersistentArrayMap. (meta delegate)
|
|
(inc (count delegate))
|
|
(doto (aclone (.-arr ^PersistentArrayMap delegate))
|
|
(.push k v))
|
|
nil)
|
|
(assoc delegate k v)))
|
|
|
|
(defn- impl-assoc
|
|
[self k v]
|
|
(track-change! self k)
|
|
|
|
(case k
|
|
:id
|
|
(ShapeProxy. v
|
|
(.-type ^ShapeProxy self)
|
|
(.-delegate ^ShapeProxy self))
|
|
:type
|
|
(ShapeProxy. (.-id ^ShapeProxy self)
|
|
v
|
|
(.-delegate ^ShapeProxy self))
|
|
|
|
(let [delegate (.-delegate ^ShapeProxy self)
|
|
delegate' (assoc-delegate delegate k v)]
|
|
(if (identical? delegate' delegate)
|
|
self
|
|
(ShapeProxy. (.-id ^ShapeProxy self)
|
|
(.-type ^ShapeProxy self)
|
|
delegate')))))
|
|
|
|
(def ^:private base-fields
|
|
"Base fields of the `Shape` record this proxy stands in for: `cr/defrecord`
|
|
nils them on dissoc instead of removing them, and the schema requires them."
|
|
#{:name :x :y :width :height :rotation :selrect :points
|
|
:transform :transform-inverse :parent-id :frame-id :flip-x :flip-y})
|
|
|
|
(defn- impl-dissoc
|
|
[self k]
|
|
(track-change! self k)
|
|
|
|
(case k
|
|
:id
|
|
(ShapeProxy. nil
|
|
(.-type ^ShapeProxy self)
|
|
(.-delegate ^ShapeProxy self))
|
|
:type
|
|
(ShapeProxy. (.-id ^ShapeProxy self)
|
|
nil
|
|
(.-delegate ^ShapeProxy self))
|
|
(let [delegate (.-delegate ^ShapeProxy self)
|
|
delegate' (if (contains? base-fields k)
|
|
(assoc delegate k nil)
|
|
(dissoc delegate k))]
|
|
(if (identical? delegate delegate')
|
|
self
|
|
(ShapeProxy. (.-id ^ShapeProxy self)
|
|
(.-type ^ShapeProxy self)
|
|
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
|
|
"Instantiate a shape from a map. The delegate is an array map because
|
|
it takes far less memory than a hash map for shape-sized maps."
|
|
[attrs]
|
|
(let [arr (reduce-kv (fn [arr k v]
|
|
(when-not (or (= k :id) (= k :type))
|
|
(.push arr k v))
|
|
arr)
|
|
#js []
|
|
attrs)]
|
|
(ShapeProxy. (:id attrs)
|
|
(:type attrs)
|
|
(PersistentArrayMap. nil (/ (alength arr) 2) arr nil))))
|
|
|
|
(t/add-handlers!
|
|
;; We only add a write handler, read handler uses the dynamic dispatch
|
|
{:id "shape"
|
|
:class ShapeProxy
|
|
:wfn #(into {} %)})
|