mirror of
https://github.com/penpot/penpot.git
synced 2026-05-11 02:58:25 +00:00
418 lines
11 KiB
Clojure
418 lines
11 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 INC
|
|
|
|
(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.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]
|
|
[app.render-wasm.svg-filters :as svg-filters]
|
|
[app.render-wasm.wasm :as wasm]
|
|
[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/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 wasm/context-initialized?
|
|
(let [shape (case k
|
|
:svg-attrs (svg-filters/apply-svg-derived (assoc shape :svg-attrs (get shape :svg-attrs)))
|
|
(:fills :blur :shadow) (svg-filters/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)
|
|
|
|
: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-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 [pending-thumbnails (into [] (concat (api/set-shape-text-content id v)))
|
|
pending-full (into [] (concat (api/set-shape-text-images id v)))]
|
|
;; 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 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
|
|
(api/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/tap
|
|
(fn []
|
|
(when (cfh/text-shape? shape)
|
|
(api/update-text-rect! (:id shape)))))))
|
|
(rx/empty))))
|
|
|
|
(defn process-shape-changes!
|
|
[objects shape-changes]
|
|
(->> (rx/from shape-changes)
|
|
(rx/mapcat (fn [[shape-id props]] (process-shape! (get objects shape-id) props)))
|
|
(rx/subs! #(api/request-render "set-wasm-attrs"))))
|
|
|
|
;; `conj` empty set initialization
|
|
(def conj* (fnil conj (d/ordered-set)))
|
|
|
|
(defn- impl-assoc
|
|
[self k v]
|
|
(when shape/*shape-changes*
|
|
(vswap! shape/*shape-changes* update (:id self) conj* 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 k v)]
|
|
(if (identical? delegate' delegate)
|
|
self
|
|
(ShapeProxy. (.-id ^ShapeProxy self)
|
|
(.-type ^ShapeProxy self)
|
|
delegate')))))
|
|
|
|
(defn- impl-dissoc
|
|
[self k]
|
|
(when shape/*shape-changes*
|
|
(vswap! shape/*shape-changes* update (:id self) conj* 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' (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"))))))))
|
|
|
|
(def ^:private xf:without-id-and-type
|
|
(remove (fn [kvpair]
|
|
(let [k (key kvpair)]
|
|
(or (= k :id)
|
|
(= k :type))))))
|
|
|
|
(defn create-shape
|
|
"Instanciate a shape from a map"
|
|
[attrs]
|
|
(ShapeProxy. (:id attrs)
|
|
(:type attrs)
|
|
(into {} xf:without-id-and-type attrs)))
|
|
|
|
(t/add-handlers!
|
|
;; We only add a write handler, read handler uses the dynamic dispatch
|
|
{:id "shape"
|
|
:class ShapeProxy
|
|
:wfn #(into {} %)})
|