Files
penpot/frontend/src/app/plugins/shape.cljs
2024-06-04 10:15:32 +02:00

538 lines
22 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.plugins.shape
"RPC for plugins runtime."
(:require
[app.common.colors :as clr]
[app.common.data :as d]
[app.common.files.helpers :as cfh]
[app.common.record :as crc]
[app.common.spec :as us]
[app.common.text :as txt]
[app.common.types.shape :as cts]
[app.common.types.shape.layout :as ctl]
[app.common.types.shape.radius :as ctsr]
[app.common.uuid :as uuid]
[app.main.data.workspace :as udw]
[app.main.data.workspace.selection :as dws]
[app.main.data.workspace.shape-layout :as dwsl]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.texts :as dwt]
[app.main.store :as st]
[app.plugins.flex :as flex]
[app.plugins.grid :as grid]
[app.plugins.utils :as utils :refer [locate-objects locate-shape proxy->shape array-to-js]]
[app.util.object :as obj]
[app.util.text-editor :as ted]))
(declare shape-proxy)
(defn text-props
[shape]
(d/merge
(dwt/current-root-values {:shape shape :attrs txt/root-attrs})
(dwt/current-paragraph-values {:shape shape :attrs txt/paragraph-attrs})
(dwt/current-text-values {:shape shape :attrs txt/text-node-attrs})))
(deftype ShapeProxy [$file $page $id]
Object
(resize
[_ width height]
(st/emit! (udw/update-dimensions [$id] :width width)
(udw/update-dimensions [$id] :height height)))
(clone
[_]
(let [ret-v (atom nil)]
(st/emit! (dws/duplicate-shapes #{$id} :change-selection? false :return-ref ret-v))
(shape-proxy (deref ret-v))))
(remove
[_]
(st/emit! (dwsh/delete-shapes #{$id})))
;; Only for frames + groups + booleans
(getChildren
[_]
(apply array (->> (locate-shape $file $page $id)
:shapes
(map #(shape-proxy $file $page %)))))
(appendChild
[_ child]
(let [child-id (obj/get child "$id")]
(st/emit! (udw/relocate-shapes #{child-id} $id 0))))
(insertChild
[_ index child]
(let [child-id (obj/get child "$id")]
(st/emit! (udw/relocate-shapes #{child-id} $id index))))
;; Only for frames
(addFlexLayout
[_]
(st/emit! (dwsl/create-layout-from-id $id :flex :from-frame? true :calculate-params? false))
(grid/grid-layout-proxy $file $page $id))
(addGridLayout
[_]
(st/emit! (dwsl/create-layout-from-id $id :grid :from-frame? true :calculate-params? false))
(grid/grid-layout-proxy $file $page $id)))
(crc/define-properties!
ShapeProxy
{:name js/Symbol.toStringTag
:get (fn [] (str "ShapeProxy"))})
(defn shape-proxy
([id]
(shape-proxy (:current-file-id @st/state) (:current-page-id @st/state) id))
([page-id id]
(shape-proxy (:current-file-id @st/state) page-id id))
([file-id page-id id]
(assert (uuid? file-id))
(assert (uuid? page-id))
(assert (uuid? id))
(let [data (locate-shape file-id page-id id)]
(-> (ShapeProxy. file-id page-id id)
(crc/add-properties!
{:name "$id" :enumerable false :get (constantly id)}
{:name "$file" :enumerable false :get (constantly file-id)}
{:name "$page" :enumerable false :get (constantly page-id)}
{:name "id"
:get #(-> % proxy->shape :id str)}
{:name "type"
:get #(-> % proxy->shape :type name)}
{:name "name"
:get #(-> % proxy->shape :name)
:set (fn [self value]
(let [id (obj/get self "$id")]
(st/emit! (dwsh/update-shapes [id] #(assoc % :name value)))))}
{:name "blocked"
:get #(-> % proxy->shape :blocked boolean)
:set (fn [self value]
(let [id (obj/get self "$id")]
(st/emit! (dwsh/update-shapes [id] #(assoc % :blocked value)))))}
{:name "hidden"
:get #(-> % proxy->shape :hidden boolean)
:set (fn [self value]
(let [id (obj/get self "$id")]
(st/emit! (dwsh/update-shapes [id] #(assoc % :hidden value)))))}
{:name "proportionLock"
:get #(-> % proxy->shape :proportion-lock boolean)
:set (fn [self value]
(let [id (obj/get self "$id")]
(st/emit! (dwsh/update-shapes [id] #(assoc % :proportion-lock value)))))}
{:name "constraintsHorizontal"
:get #(-> % proxy->shape :constraints-h d/name)
:set (fn [self value]
(let [id (obj/get self "$id")
value (keyword value)]
(when (contains? cts/horizontal-constraint-types value)
(st/emit! (dwsh/update-shapes [id] #(assoc % :constraints-h value))))))}
{:name "constraintsVertical"
:get #(-> % proxy->shape :constraints-v d/name)
:set (fn [self value]
(let [id (obj/get self "$id")
value (keyword value)]
(when (contains? cts/vertical-constraint-types value)
(st/emit! (dwsh/update-shapes [id] #(assoc % :constraints-v value))))))}
{:name "borderRadius"
:get #(-> % proxy->shape :rx)
:set (fn [self value]
(let [id (obj/get self "$id")
shape (proxy->shape self)]
(when (us/safe-int? value)
(when (or (not (ctsr/has-radius? shape)) (ctsr/radius-4? shape))
(st/emit! (dwsh/update-shapes [id] ctsr/switch-to-radius-1)))
(st/emit! (dwsh/update-shapes [id] #(ctsr/set-radius-1 % value))))))}
{:name "borderRadiusTopLeft"
:get #(-> % proxy->shape :r1)
:set (fn [self value]
(let [id (obj/get self "$id")
shape (proxy->shape self)]
(when (us/safe-int? value)
(when (or (not (ctsr/has-radius? shape)) (not (ctsr/radius-4? shape)))
(st/emit! (dwsh/update-shapes [id] ctsr/switch-to-radius-4)))
(st/emit! (dwsh/update-shapes [id] #(ctsr/set-radius-4 % :r1 value))))))}
{:name "borderRadiusTopRight"
:get #(-> % proxy->shape :r2)
:set (fn [self value]
(let [id (obj/get self "$id")
shape (proxy->shape self)]
(when (us/safe-int? value)
(when (or (not (ctsr/has-radius? shape)) (not (ctsr/radius-4? shape)))
(st/emit! (dwsh/update-shapes [id] ctsr/switch-to-radius-4)))
(st/emit! (dwsh/update-shapes [id] #(ctsr/set-radius-4 % :r2 value))))))}
{:name "borderRadiusBottomRight"
:get #(-> % proxy->shape :r3)
:set (fn [self value]
(let [id (obj/get self "$id")
shape (proxy->shape self)]
(when (us/safe-int? value)
(when (or (not (ctsr/has-radius? shape)) (not (ctsr/radius-4? shape)))
(st/emit! (dwsh/update-shapes [id] ctsr/switch-to-radius-4)))
(st/emit! (dwsh/update-shapes [id] #(ctsr/set-radius-4 % :r3 value))))))}
{:name "borderRadiusBottomLeft"
:get #(-> % proxy->shape :r4)
:set (fn [self value]
(let [id (obj/get self "$id")
shape (proxy->shape self)]
(when (us/safe-int? value)
(when (or (not (ctsr/has-radius? shape)) (not (ctsr/radius-4? shape)))
(st/emit! (dwsh/update-shapes [id] ctsr/switch-to-radius-4)))
(st/emit! (dwsh/update-shapes [id] #(ctsr/set-radius-4 % :r4 value))))))}
{:name "opacity"
:get #(-> % proxy->shape :opacity)
:set (fn [self value]
(let [id (obj/get self "$id")]
(when (and (us/safe-number? value) (>= value 0) (<= value 1))
(st/emit! (dwsh/update-shapes [id] #(assoc % :opacity value))))))}
{:name "blendMode"
:get #(-> % proxy->shape :blend-mode (d/nilv :normal) d/name)
:set (fn [self value]
(let [id (obj/get self "$id")
value (keyword value)]
(when (contains? cts/blend-modes value)
(st/emit! (dwsh/update-shapes [id] #(assoc % :blend-mode value))))))}
{:name "shadows"
:get #(-> % proxy->shape :shadow array-to-js)
:set (fn [self value]
(let [id (obj/get self "$id")
value (mapv (fn [val]
;; Merge default shadow properties
(d/patch-object
{:id (uuid/next)
:style :drop-shadow
:color {:color clr/black :opacity 0.2}
:offset-x 4
:offset-y 4
:blur 4
:spread 0
:hidden false}
(utils/from-js val #{:style :type})))
value)]
(st/emit! (dwsh/update-shapes [id] #(assoc % :shadow value)))))}
{:name "blur"
:get #(-> % proxy->shape :blur utils/to-js)
:set (fn [self value]
(if (nil? value)
(st/emit! (dwsh/update-shapes [id] #(dissoc % :blur)))
(let [id (obj/get self "$id")
value
(d/patch-object
{:id (uuid/next)
:type :layer-blur
:value 4
:hidden false}
(utils/from-js value))]
(st/emit! (dwsh/update-shapes [id] #(assoc % :blur value))))))}
{:name "exports"
:get #(-> % proxy->shape :exports array-to-js)
:set (fn [self value]
(let [id (obj/get self "$id")
value (mapv #(utils/from-js %) value)]
(st/emit! (dwsh/update-shapes [id] #(assoc % :exports value)))))}
;; Geometry properties
{:name "x"
:get #(-> % proxy->shape :x)
:set
(fn [self value]
(let [id (obj/get self "$id")]
(st/emit! (udw/update-position id {:x value}))))}
{:name "y"
:get #(-> % proxy->shape :y)
:set
(fn [self value]
(let [id (obj/get self "$id")]
(st/emit! (udw/update-position id {:y value}))))}
{:name "parentX"
:get (fn [self]
(let [shape (proxy->shape self)
parent-id (:parent-id shape)
parent (locate-shape (obj/get self "$file") (obj/get self "$page") parent-id)]
(- (:x shape) (:x parent))))
:set
(fn [self value]
(let [id (obj/get self "$id")
parent-id (-> self proxy->shape :parent-id)
parent (locate-shape (obj/get self "$file") (obj/get self "$page") parent-id)
parent-x (:x parent)]
(st/emit! (udw/update-position id {:x (+ parent-x value)}))))}
{:name "parentY"
:get (fn [self]
(let [shape (proxy->shape self)
parent-id (:parent-id shape)
parent (locate-shape (obj/get self "$file") (obj/get self "$page") parent-id)
parent-y (:y parent)]
(- (:y shape) parent-y)))
:set
(fn [self value]
(let [id (obj/get self "$id")
parent-id (-> self proxy->shape :parent-id)
parent (locate-shape (obj/get self "$file") (obj/get self "$page") parent-id)
parent-y (:y parent)]
(st/emit! (udw/update-position id {:y (+ parent-y value)}))))}
{:name "frameX"
:get (fn [self]
(let [shape (proxy->shape self)
frame-id (:parent-id shape)
frame (locate-shape (obj/get self "$file") (obj/get self "$page") frame-id)
frame-x (:x frame)]
(- (:x shape) frame-x)))
:set
(fn [self value]
(let [id (obj/get self "$id")
frame-id (-> self proxy->shape :frame-id)
frame (locate-shape (obj/get self "$file") (obj/get self "$page") frame-id)
frame-x (:x frame)]
(st/emit! (udw/update-position id {:x (+ frame-x value)}))))}
{:name "frameY"
:get (fn [self]
(let [shape (proxy->shape self)
frame-id (:parent-id shape)
frame (locate-shape (obj/get self "$file") (obj/get self "$page") frame-id)
frame-y (:y frame)]
(- (:y shape) frame-y)))
:set
(fn [self value]
(let [id (obj/get self "$id")
frame-id (-> self proxy->shape :frame-id)
frame (locate-shape (obj/get self "$file") (obj/get self "$page") frame-id)
frame-y (:y frame)]
(st/emit! (udw/update-position id {:y (+ frame-y value)}))))}
{:name "width"
:get #(-> % proxy->shape :width)}
{:name "height"
:get #(-> % proxy->shape :height)}
{:name "flipX"
:get #(-> % proxy->shape :flip-x)}
{:name "flipY"
:get #(-> % proxy->shape :flip-y)}
;; Strokes and fills
{:name "fills"
:get #(if (cfh/text-shape? data)
(-> % proxy->shape text-props :fills array-to-js)
(-> % proxy->shape :fills array-to-js))
:set (fn [self value]
(let [id (obj/get self "$id")
value (mapv #(utils/from-js %) value)]
(st/emit! (dwsh/update-shapes [id] #(assoc % :fills value)))))}
{:name "strokes"
:get #(-> % proxy->shape :strokes array-to-js)
:set (fn [self value]
(let [id (obj/get self "$id")
value (mapv #(utils/from-js % #{:stroke-style :stroke-alignment}) value)]
(st/emit! (dwsh/update-shapes [id] #(assoc % :strokes value)))))}
{:name "layoutChild"
:get
(fn [self]
(let [file-id (obj/get self "$file")
page-id (obj/get self "$page")
id (obj/get self "$id")
objects (locate-objects file-id page-id)]
(when (ctl/any-layout-immediate-child-id? objects id)
(flex/layout-child-proxy file-id page-id id))))}
{:name "layoutCell"
:get
(fn [self]
(let [file-id (obj/get self "$file")
page-id (obj/get self "$page")
id (obj/get self "$id")
objects (locate-objects file-id page-id)]
(when (ctl/grid-layout-immediate-child-id? objects id)
(grid/layout-cell-proxy file-id page-id id))))})
(cond-> (or (cfh/frame-shape? data) (cfh/group-shape? data) (cfh/svg-raw-shape? data) (cfh/bool-shape? data))
(crc/add-properties!
{:name "children"
:enumerable false
:get #(.getChildren ^js %)}))
(cond-> (not (or (cfh/frame-shape? data) (cfh/group-shape? data) (cfh/svg-raw-shape? data) (cfh/bool-shape? data)))
(-> (obj/unset! "appendChild")
(obj/unset! "insertChild")
(obj/unset! "getChildren")))
(cond-> (cfh/frame-shape? data)
(-> (crc/add-properties!
{:name "grid"
:get
(fn [self]
(let [layout (-> self proxy->shape :layout)
file-id (obj/get self "$file")
page-id (obj/get self "$page")
id (obj/get self "$id")]
(when (= :grid layout)
(grid/grid-layout-proxy file-id page-id id))))}
{:name "flex"
:get
(fn [self]
(let [layout (-> self proxy->shape :layout)
file-id (obj/get self "$file")
page-id (obj/get self "$page")
id (obj/get self "$id")]
(when (= :flex layout)
(flex/flex-layout-proxy file-id page-id id))))}
{:name "guides"
:get #(-> % proxy->shape :grids array-to-js)
:set (fn [self value]
(let [id (obj/get self "$id")
value (mapv #(utils/from-js %) value)]
(st/emit! (dwsh/update-shapes [id] #(assoc % :grids value)))))}
{:name "horizontalSizing"
:get #(-> % proxy->shape :layout-item-h-sizing (d/nilv :fix) d/name)
:set
(fn [self value]
(let [id (obj/get self "$id")
value (keyword value)]
(when (contains? #{:fix :auto} value)
(st/emit! (dwsl/update-layout #{id} {:layout-item-h-sizing value})))))}
{:name "verticalSizing"
:get #(-> % proxy->shape :layout-item-v-sizing (d/nilv :fix) d/name)
:set
(fn [self value]
(let [id (obj/get self "$id")
value (keyword value)]
(when (contains? #{:fix :auto} value)
(st/emit! (dwsl/update-layout #{id} {:layout-item-v-sizing value})))))})))
(cond-> (not (cfh/frame-shape? data))
(-> (obj/unset! "addGridLayout")
(obj/unset! "addFlexLayout")))
(cond-> (cfh/text-shape? data)
(crc/add-properties!
{:name "characters"
:get #(-> % proxy->shape :content txt/content->text)
:set
(fn [self value]
(let [id (obj/get self "$id")]
;; The user is currently editing the text. We need to update the
;; editor as well
(when (contains? (:workspace-editor-state @st/state) id)
(let [shape (proxy->shape self)
editor
(-> shape
(txt/change-text value)
:content
ted/import-content
ted/create-editor-state)]
(st/emit! (dwt/update-editor-state shape editor))))
(st/emit! (dwsh/update-shapes [id] #(txt/change-text % value)))))}
{:name "growType"
:get #(-> % proxy->shape :grow-type d/name)
:set
(fn [self value]
(let [id (obj/get self "$id")
value (keyword value)]
(when (contains? #{:auto-width :auto-height :fixed} value)
(st/emit! (dwsh/update-shapes [id] #(assoc % :grow-type value))))))}
{:name "fontId"
:get #(-> % proxy->shape text-props :font-id)
:set
(fn [self value]
(let [id (obj/get self "$id")]
(st/emit! (dwt/update-attrs id {:font-id value}))))}
{:name "fontFamily"
:get #(-> % proxy->shape text-props :font-family)
:set
(fn [self value]
(let [id (obj/get self "$id")]
(st/emit! (dwt/update-attrs id {:font-id value}))))}
{:name "fontVariantId"
:get #(-> % proxy->shape text-props :font-variant-id)
:set
(fn [self value]
(let [id (obj/get self "$id")]
(st/emit! (dwt/update-attrs id {:font-id value}))))}
{:name "fontSize"
:get #(-> % proxy->shape text-props :font-size)
:set
(fn [self value]
(let [id (obj/get self "$id")]
(st/emit! (dwt/update-attrs id {:font-id value}))))}
{:name "fontWeight"
:get #(-> % proxy->shape text-props :font-weight)
:set
(fn [self value]
(let [id (obj/get self "$id")]
(st/emit! (dwt/update-attrs id {:font-id value}))))}
{:name "fontStyle"
:get #(-> % proxy->shape text-props :font-style)
:set
(fn [self value]
(let [id (obj/get self "$id")]
(st/emit! (dwt/update-attrs id {:font-style value}))))}
{:name "lineHeight"
:get #(-> % proxy->shape text-props :line-height)
:set
(fn [self value]
(let [id (obj/get self "$id")]
(st/emit! (dwt/update-attrs id {:line-height value}))))}
{:name "letterSpacing"
:get #(-> % proxy->shape text-props :letter-spacing)
:set
(fn [self value]
(let [id (obj/get self "$id")]
(st/emit! (dwt/update-attrs id {:letter-spacing value}))))}
{:name "textTransform"
:get #(-> % proxy->shape text-props :text-transform)
:set
(fn [self value]
(let [id (obj/get self "$id")]
(st/emit! (dwt/update-attrs id {:text-transform value}))))}))))))