From ac58a5b8faaffe8a98894d1671f9c1f155b60bdd Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 27 Jun 2024 10:11:48 +0200 Subject: [PATCH] :sparkles: Improved transformation from and to JS for plugins --- frontend/src/app/plugins/api.cljs | 27 +- frontend/src/app/plugins/fonts.cljs | 3 +- frontend/src/app/plugins/format.cljs | 405 +++++++++++- frontend/src/app/plugins/grid.cljs | 11 +- frontend/src/app/plugins/library.cljs | 47 +- frontend/src/app/plugins/page.cljs | 29 +- frontend/src/app/plugins/parser.cljs | 308 ++++++++- frontend/src/app/plugins/public_utils.cljs | 3 +- frontend/src/app/plugins/shape.cljs | 625 ++++-------------- frontend/src/app/plugins/text.cljs | 434 ++++++++++++ frontend/src/app/plugins/user.cljs | 3 +- frontend/src/app/plugins/utils.cljs | 55 -- frontend/src/app/plugins/viewport.cljs | 3 +- frontend/src/app/util/object.cljs | 5 + .../plugins/context_shapes_test.cljs | 23 +- 15 files changed, 1359 insertions(+), 622 deletions(-) create mode 100644 frontend/src/app/plugins/text.cljs diff --git a/frontend/src/app/plugins/api.cljs b/frontend/src/app/plugins/api.cljs index a645bb791..2cd3938a7 100644 --- a/frontend/src/app/plugins/api.cljs +++ b/frontend/src/app/plugins/api.cljs @@ -27,6 +27,7 @@ [app.plugins.events :as events] [app.plugins.file :as file] [app.plugins.fonts :as fonts] + [app.plugins.format :as format] [app.plugins.library :as library] [app.plugins.page :as page] [app.plugins.parser :as parser] @@ -101,26 +102,12 @@ shapes (->> shapes (map #(obj/get % "$id")) (mapcat #(cfh/get-children-with-self objects %))) - file-id (:current-file-id @st/state) - shared-libs (:workspace-libraries @st/state) + shared-libs (:workspace-libraries @st/state)] - format-entry - (fn [{:keys [prop shape-id index]}] - #js {:property (d/name prop) - :index index - :shapeId (str shape-id)}) - format-result - (fn [[color attrs]] - (let [shapes-info (apply array (map format-entry attrs)) - color (u/to-js color)] - (obj/set! color "shapeInfo" shapes-info) - color))] - (apply - array - (->> (ctc/extract-all-colors shapes file-id shared-libs) - (group-by :attrs) - (map format-result)))))) + (->> (ctc/extract-all-colors shapes file-id shared-libs) + (group-by :attrs) + (format/format-array format/format-color-result))))) (replaceColor [_ shapes old-color new-color] @@ -188,8 +175,8 @@ (p/create (fn [resolve reject] (->> (dwm/upload-media-url name file-id url) - (rx/map u/to-js) (rx/take 1) + (rx/map format/format-image) (rx/subs! resolve reject))))))) (uploadMediaData @@ -205,7 +192,7 @@ :on-image identity :on-svg identity}) (rx/take 1) - (rx/map u/to-js) + (rx/map format/format-image) (rx/subs! resolve reject)))))) (group diff --git a/frontend/src/app/plugins/fonts.cljs b/frontend/src/app/plugins/fonts.cljs index 4902277bc..74631e1a4 100644 --- a/frontend/src/app/plugins/fonts.cljs +++ b/frontend/src/app/plugins/fonts.cljs @@ -12,6 +12,7 @@ [app.main.fonts :as fonts] [app.main.store :as st] [app.plugins.shape :as shape] + [app.plugins.text :as text] [app.plugins.utils :as u] [app.util.object :as obj] [cuerdas.core :as str])) @@ -39,7 +40,7 @@ (applyToRange [_ range variant] (cond - (not (shape/text-range? range)) + (not (text/text-range? range)) (u/display-not-valid :applyToRange range) ;; TODO: Check variant inside font variants diff --git a/frontend/src/app/plugins/format.cljs b/frontend/src/app/plugins/format.cljs index 4b2a44c09..9df43773b 100644 --- a/frontend/src/app/plugins/format.cljs +++ b/frontend/src/app/plugins/format.cljs @@ -4,5 +4,408 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.plugins.format) +(ns app.plugins.format + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.util.object :as obj])) +(defn format-id + [id] + (when id (dm/str id))) + +(defn format-key + [kw] + (when kw (d/name kw))) + +(defn format-array + [format-fn coll] + (when (some? coll) + (apply array (keep format-fn coll)))) + +;; export type PenpotPoint = { x: number; y: number }; +(defn format-point + [{:keys [x y] :as point}] + (when (some? point) + (obj/clear-empty + #js {:x x :y y}))) + +;;export type PenpotBounds = { +;; x: number; +;; y: number; +;; width: number; +;; height: number; +;;}; +(defn format-bounds + [{:keys [x y width height] :as bounds}] + (when (some? bounds) + (obj/clear-empty + #js {:x x :y y :width width :height height}))) + +;; export interface PenpotColorShapeInfoEntry { +;; readonly property: string; +;; readonly index?: number; +;; readonly shapeId: string; +;; } +(defn format-shape-info + [{:keys [prop shape-id index] :as info}] + (when (some? info) + (obj/clear-empty + #js {:property (d/name prop) + :index index + :shapeId (dm/str shape-id)}))) + +;; export type PenpotGradient = { +;; type: 'linear' | 'radial'; +;; startX: number; +;; startY: number; +;; endX: number; +;; endY: number; +;; width: number; +;; stops: Array<{ color: string; opacity?: number; offset: number }>; +;; }; +(defn format-stop + [{:keys [color opacity offset] :as stop}] + (when (some? stop) + (obj/clear-empty #js {:color color :opacity opacity :offset offset}))) + +(defn format-gradient + [{:keys [type start-x start-y end-x end-y width stops] :as gradient}] + (when (some? gradient) + (obj/clear-empty + #js {:type (format-key type) + :startX start-x + :startY start-y + :endX end-x + :endY end-y + :width width + :stops (format-array format-stop stops)}))) + +;; export type PenpotImageData = { +;; name?: string; +;; width: number; +;; height: number; +;; mtype?: string; +;; id: string; +;; keepApectRatio?: boolean; +;; }; +(defn format-image + [{:keys [name width height mtype id keep-aspect-ratio] :as image}] + (when (some? image) + (obj/clear-empty + #js {:name name + :width width + :height height + :mtype mtype + :id (format-id id) + :keepAspectRatio keep-aspect-ratio}))) + +;; export interface PenpotColor { +;; id?: string; +;; name?: string; +;; path?: string; +;; color?: string; +;; opacity?: number; +;; refId?: string; +;; refFile?: string; +;; gradient?: PenpotGradient; +;; image?: PenpotImageData; +;; } +(defn format-color + [{:keys [id name path color opacity ref-id ref-file gradient image] :as color-data}] + (when (some? color-data) + (obj/clear-empty + #js {:id (format-id id) + :name name + :path path + :color color + :opacity opacity + :refId (format-id ref-id) + :refFile (format-id ref-file) + :gradient (format-gradient gradient) + :image (format-image image)}))) + +;; PenpotColor & PenpotColorShapeInfo +(defn format-color-result + [[color attrs]] + (let [shapes-info (apply array (map format-shape-info attrs)) + color (format-color color)] + (obj/set! color "shapeInfo" shapes-info) + color)) + + +;; export interface PenpotShadow { +;; id?: string; +;; style?: 'drop-shadow' | 'inner-shadow'; +;; offsetX?: number; +;; offsetY?: number; +;; blur?: number; +;; spread?: number; +;; hidden?: boolean; +;; color?: PenpotColor; +;; } +(defn format-shadow + [{:keys [id style offset-x offset-y blur spread hidden color] :as shadow}] + (when (some? shadow) + (obj/clear-empty + #js {:id (-> id format-id) + :style (-> style format-key) + :offsetX offset-x + :offsetY offset-y + :blur blur + :spread spread + :hidden hidden + :color (format-color color)}))) + +(defn format-shadows + [shadows] + (when (some? shadows) + (format-array format-shadow shadows))) + +;;export interface PenpotFill { +;; fillColor?: string; +;; fillOpacity?: number; +;; fillColorGradient?: PenpotGradient; +;; fillColorRefFile?: string; +;; fillColorRefId?: string; +;; fillImage?: PenpotImageData; +;;} +(defn format-fill + [{:keys [fill-color fill-opacity fill-color-gradient fill-color-ref-file fill-color-ref-id fill-image] :as fill}] + (when (some? fill) + (obj/clear-empty + #js {:fillColor fill-color + :fillOpacity fill-opacity + :fillColorGradient (format-gradient fill-color-gradient) + :fillColorRefFile (format-id fill-color-ref-file) + :fillColorRefId (format-id fill-color-ref-id) + :fillImage (format-image fill-image)}))) + +(defn format-fills + [fills] + (when (some? fills) + (format-array format-fill fills))) + +;; export interface PenpotStroke { +;; strokeColor?: string; +;; strokeColorRefFile?: string; +;; strokeColorRefId?: string; +;; strokeOpacity?: number; +;; strokeStyle?: 'solid' | 'dotted' | 'dashed' | 'mixed' | 'none' | 'svg'; +;; strokeWidth?: number; +;; strokeAlignment?: 'center' | 'inner' | 'outer'; +;; strokeCapStart?: PenpotStrokeCap; +;; strokeCapEnd?: PenpotStrokeCap; +;; strokeColorGradient?: PenpotGradient; +;; } +(defn format-stroke + [{:keys [stroke-color stroke-color-ref-file stroke-color-ref-id + stroke-opacity stroke-style stroke-width stroke-alignment + stroke-cap-start stroke-cap-end stroke-color-gradient] :as stroke}] + + (when (some? stroke) + (obj/clear-empty + #js {:strokeColor stroke-color + :strokeColorRefFile (format-id stroke-color-ref-file) + :strokeColorRefId (format-id stroke-color-ref-id) + :strokeOpacity stroke-opacity + :strokeStyle (format-key stroke-style) + :strokeWidth stroke-width + :strokeAlignment (format-key stroke-alignment) + :strokeCapStart (format-key stroke-cap-start) + :strokeCapEnd (format-key stroke-cap-end) + :strokeColorGradient (format-gradient stroke-color-gradient)}))) + +(defn format-strokes + [strokes] + (when (some? strokes) + (format-array format-stroke strokes))) + +;; export interface PenpotBlur { +;; id?: string; +;; type?: 'layer-blur'; +;; value?: number; +;; hidden?: boolean; +;; } +(defn format-blur + [{:keys [id type value hidden] :as blur}] + (when (some? blur) + (obj/clear-empty + #js {:id (format-id id) + :type (format-key type) + :value value + :hidden hidden}))) + +;; export interface PenpotExport { +;; type: 'png' | 'jpeg' | 'svg' | 'pdf'; +;; scale: number; +;; suffix: string; +;; } +(defn format-export + [{:keys [type scale suffix] :as export}] + (when (some? export) + (obj/clear-empty + #js {:type (format-key type) + :scale scale + :suffix suffix}))) + +(defn format-exports + [exports] + (when (some? exports) + (format-array format-export exports))) + +;; export interface PenpotFrameGuideColumnParams { +;; color: { color: string; opacity: number }; +;; type?: 'stretch' | 'left' | 'center' | 'right'; +;; size?: number; +;; margin?: number; +;; itemLength?: number; +;; gutter?: number; +;; } +(defn format-frame-guide-column-params + [{:keys [color type size margin item-length gutter] :as params}] + (when (some? params) + (obj/clear-empty + #js {:color (format-color color) + :type (format-key type) + :size size + :margin margin + :itemLength item-length + :gutter gutter}))) + +;; export interface PenpotFrameGuideColumn { +;; type: 'column'; +;; display: boolean; +;; params: PenpotFrameGuideColumnParams; +;; } +(defn format-frame-guide-column + [{:keys [type display params] :as guide}] + (when (some? guide) + (obj/clear-empty + #js {:type (format-key type) + :display display + :params (format-frame-guide-column-params params)}))) + +;; export interface PenpotFrameGuideRow { +;; type: 'row'; +;; display: boolean; +;; params: PenpotFrameGuideColumnParams; +;; } +(defn format-frame-guide-row + [{:keys [type display params] :as guide}] + (when (some? guide) + (obj/clear-empty + #js {:type (format-key type) + :display display + :params (format-frame-guide-column-params params)}))) + +;;export interface PenpotFrameGuideSquareParams { +;; color: { color: string; opacity: number }; +;; size?: number; +;;} +(defn format-frame-guide-square-params + [{:keys [color size] :as params}] + (when (some? params) + (obj/clear-empty + #js {:color (format-color color) + :size size}))) + +;; export interface PenpotFrameGuideSquare { +;; type: 'square'; +;; display: boolean; +;; params: PenpotFrameGuideSquareParams; +;; } + +(defn format-frame-guide-square + [{:keys [type display params] :as guide}] + (when (some? guide) + (obj/clear-empty + #js {:type (format-key type) + :display display + :params (format-frame-guide-column-params params)}))) + +(defn format-frame-guide + [{:keys [type] :as guide}] + (when (some? guide) + (case type + :column (format-frame-guide-column guide) + :row (format-frame-guide-row guide) + :square (format-frame-guide-square guide)))) + +(defn format-frame-guides + [guides] + (when (some? guides) + (format-array format-frame-guide guides))) + +;;interface PenpotPathCommand { +;; command: +;; | 'M' | 'move-to' +;; | 'Z' | 'close-path' +;; | 'L' | 'line-to' +;; | 'H' | 'line-to-horizontal' +;; | 'V' | 'line-to-vertical' +;; | 'C' | 'curve-to' +;; | 'S' | 'smooth-curve-to' +;; | 'Q' | 'quadratic-bezier-curve-to' +;; | 'T' | 'smooth-quadratic-bezier-curve-to' +;; | 'A' | 'elliptical-arc'; +;; +;; params?: { +;; x?: number; +;; y?: number; +;; c1x: number; +;; c1y: number; +;; c2x: number; +;; c2y: number; +;; rx?: number; +;; ry?: number; +;; xAxisRotation?: number; +;; largeArcFlag?: boolean; +;; sweepFlag?: boolean; +;; }; +;;} +(defn format-command-params + [{:keys [x y c1x c1y c2x c2y rx ry x-axis-rotation large-arc-flag sweep-flag] :as props}] + (when (some? props) + (obj/clear-empty + #js {:x x + :y y + :c1x c1x + :c1y c1y + :c2x c2x + :c2y c2y + :rx rx + :ry ry + :xAxisRotation x-axis-rotation + :largeArcFlag large-arc-flag + :sweepFlag sweep-flag}))) + +(defn format-command + [{:keys [command params] :as props}] + (when (some? props) + (obj/clear-empty + #js {:command (format-key command) + :params (format-command-params params)}))) + +(defn format-path-content + [content] + (when (some? content) + (format-array format-command content))) + +;; export type PenpotTrackType = 'flex' | 'fixed' | 'percent' | 'auto'; +;; +;; export interface PenpotTrack { +;; type: PenpotTrackType; +;; value: number | null; +;; } +(defn format-track + [{:keys [type value] :as track}] + (when (some? track) + (obj/clear-empty + #js {:type (-> type format-key) + :value value}))) + +(defn format-tracks + [tracks] + (when (some? tracks) + (format-array format-track tracks))) diff --git a/frontend/src/app/plugins/grid.cljs b/frontend/src/app/plugins/grid.cljs index a715e0c7a..eb255a008 100644 --- a/frontend/src/app/plugins/grid.cljs +++ b/frontend/src/app/plugins/grid.cljs @@ -13,6 +13,7 @@ [app.main.data.workspace.shape-layout :as dwsl] [app.main.data.workspace.transforms :as dwt] [app.main.store :as st] + [app.plugins.format :as format] [app.plugins.utils :as u] [app.util.object :as obj] [potok.v2.core :as ptk])) @@ -20,12 +21,6 @@ ;; Define in `app.plugins.shape` we do this way to prevent circular dependency (def shape-proxy? nil) -(defn- make-tracks - [tracks] - (.freeze - js/Object - (apply array (->> tracks (map u/to-js))))) - (deftype GridLayout [$plugin $file $page $id] Object @@ -190,10 +185,10 @@ (st/emit! (dwsl/update-layout #{id} {:layout-grid-dir value}))))))} {:name "rows" - :get #(-> % u/proxy->shape :layout-grid-rows make-tracks)} + :get #(-> % u/proxy->shape :layout-grid-rows format/format-tracks)} {:name "columns" - :get #(-> % u/proxy->shape :layout-grid-columns make-tracks)} + :get #(-> % u/proxy->shape :layout-grid-columns format/format-tracks)} {:name "alignItems" :get #(-> % u/proxy->shape :layout-align-items d/name) diff --git a/frontend/src/app/plugins/library.cljs b/frontend/src/app/plugins/library.cljs index 6c46530eb..7c90146f5 100644 --- a/frontend/src/app/plugins/library.cljs +++ b/frontend/src/app/plugins/library.cljs @@ -22,7 +22,10 @@ [app.main.data.workspace.texts :as dwt] [app.main.repo :as rp] [app.main.store :as st] + [app.plugins.format :as format] + [app.plugins.parser :as parser] [app.plugins.shape :as shape] + [app.plugins.text :as text] [app.plugins.utils :as u] [app.util.object :as obj] [beicon.v2.core :as rx] @@ -49,27 +52,25 @@ (asFill [_] (let [color (u/locate-library-color $file $id)] - (u/to-js - (d/without-nils - {:fill-color (:color color) - :fill-opacity (:opacity color) - :fill-color-gradient (:gradient color) - :fill-color-ref-file $file - :fill-color-ref-id $id - :fill-image (:image color)})))) + (format/format-fill + {:fill-color (:color color) + :fill-opacity (:opacity color) + :fill-color-gradient (:gradient color) + :fill-color-ref-file $file + :fill-color-ref-id $id + :fill-image (:image color)}))) (asStroke [_] (let [color (u/locate-library-color $file $id)] - (u/to-js - (d/without-nils - {:stroke-color (:color color) - :stroke-opacity (:opacity color) - :stroke-color-gradient (:gradient color) - :stroke-color-ref-file $file - :stroke-color-ref-id $id - :stroke-image (:image color) - :stroke-style :solid - :stroke-alignment :inner})))) + (format/format-stroke + {:stroke-color (:color color) + :stroke-opacity (:opacity color) + :stroke-color-gradient (:gradient color) + :stroke-color-ref-file $file + :stroke-color-ref-id $id + :stroke-image (:image color) + :stroke-style :solid + :stroke-alignment :inner}))) (getPluginData [self key] @@ -211,10 +212,10 @@ (st/emit! (dwl/update-color color file-id)))))} {:name "gradient" - :get #(-> % u/proxy->library-color :gradient u/to-js) + :get #(-> % u/proxy->library-color :gradient format/format-gradient) :set (fn [self value] - (let [value (u/from-js value)] + (let [value (parser/parse-gradient value)] (cond (not (sm/validate ::ctc/gradient value)) (u/display-not-valid :library-color-gradient value) @@ -225,10 +226,10 @@ (st/emit! (dwl/update-color color file-id))))))} {:name "image" - :get #(-> % u/proxy->library-color :image u/to-js) + :get #(-> % u/proxy->library-color :image format/format-image) :set (fn [self value] - (let [value (u/from-js value)] + (let [value (parser/parse-image-data value)] (cond (not (sm/validate ::ctc/image-color value)) (u/display-not-valid :library-color-image value) @@ -266,7 +267,7 @@ (applyToTextRange [self range] (cond - (not (shape/text-range? range)) + (not (text/text-range? range)) (u/display-not-valid :applyToText range) :else diff --git a/frontend/src/app/plugins/page.cljs b/frontend/src/app/plugins/page.cljs index 91da5bb0c..66ff1877f 100644 --- a/frontend/src/app/plugins/page.cljs +++ b/frontend/src/app/plugins/page.cljs @@ -13,9 +13,11 @@ [app.common.uuid :as uuid] [app.main.data.workspace :as dw] [app.main.store :as st] + [app.plugins.parser :as parser] [app.plugins.shape :as shape] [app.plugins.utils :as u] - [app.util.object :as obj])) + [app.util.object :as obj] + [cuerdas.core :as str])) (deftype PageProxy [$plugin $file $id] Object @@ -34,11 +36,28 @@ (shape/shape-proxy $plugin $file $id uuid/zero)) (findShapes - [_] + [_ criteria] ;; Returns a lazy (iterable) of all available shapes - (when (and (some? $file) (some? $id)) - (let [page (u/locate-page $file $id)] - (apply array (sequence (map (partial shape/shape-proxy $plugin)) (keys (:objects page))))))) + (let [criteria (parser/parse-criteria criteria) + match-criteria? + (if (some? criteria) + (fn [[_ shape]] + (and + (or (not (:name criteria)) + (= (str/lower (:name criteria)) (str/lower (:name shape)))) + + (or (not (:name-like criteria)) + (str/includes? (str/lower (:name shape)) (str/lower (:name-like criteria)))) + + (or (not (:type criteria)) + (= (:type criteria) (:type shape))))) + identity)] + (when (and (some? $file) (some? $id)) + (let [page (u/locate-page $file $id) + xf (comp + (filter match-criteria?) + (map #(shape/shape-proxy $plugin $file $id (first %))))] + (apply array (sequence xf (:objects page))))))) ;; Plugin data (getPluginData diff --git a/frontend/src/app/plugins/parser.cljs b/frontend/src/app/plugins/parser.cljs index 0b12c9643..3180a0e4b 100644 --- a/frontend/src/app/plugins/parser.cljs +++ b/frontend/src/app/plugins/parser.cljs @@ -21,7 +21,29 @@ (defn parse-hex [color] - (when color (-> color str/lower))) + (if (string? color) (-> color str/lower) color)) + +;; { +;; name?: string; +;; nameLike?: string; +;; type?: +;; | 'frame' +;; | 'group' +;; | 'bool' +;; | 'rect' +;; | 'path' +;; | 'text' +;; | 'circle' +;; | 'svg-raw' +;; | 'image'; +;; } +(defn parse-criteria + [^js criteria] + (when (some? criteria) + (d/without-nils + {:name (obj/get criteria "name") + :name-like (obj/get criteria "nameLike") + :type (-> (obj/get criteria "type") parse-keyword)}))) ;;export type PenpotImageData = { ;; name?: string; @@ -33,7 +55,7 @@ ;;} (defn parse-image-data [^js image-data] - (when image-data + (when (some? image-data) (d/without-nils {:id (-> (obj/get image-data "id") parse-id) :name (obj/get image-data "name") @@ -53,7 +75,7 @@ ;; } (defn parse-gradient-stop [^js stop] - (when stop + (when (some? stop) (d/without-nils {:color (-> (obj/get stop "color") parse-hex) :opacity (obj/get stop "opacity") @@ -61,7 +83,7 @@ (defn parse-gradient [^js gradient] - (when gradient + (when (some? gradient) (d/without-nils {:type (-> (obj/get gradient "type") parse-keyword) :start-x (obj/get gradient "startX") @@ -85,7 +107,7 @@ ;; } (defn parse-color [^js color] - (when color + (when (some? color) (d/without-nils {:id (-> (obj/get color "id") parse-id) :name (obj/get color "name") @@ -96,3 +118,279 @@ :ref-file (-> (obj/get color "refFile") parse-id) :gradient (-> (obj/get color "gradient") parse-gradient) :image (-> (obj/get color "image") parse-image-data)}))) + +;; export interface PenpotShadow { +;; id?: string; +;; style?: 'drop-shadow' | 'inner-shadow'; +;; offsetX?: number; +;; offsetY?: number; +;; blur?: number; +;; spread?: number; +;; hidden?: boolean; +;; color?: PenpotColor; +;; } +(defn parse-shadow + [^js shadow] + (when (some? shadow) + (d/without-nils + {:id (-> (obj/get shadow "id") parse-id) + :style (-> (obj/get shadow "style") parse-keyword) + :offset-x (obj/get shadow "offsetX") + :offset-y (obj/get shadow "offsetY") + :blur (obj/get shadow "blur") + :spread (obj/get shadow "spread") + :hidden (obj/get shadow "hidden") + :color (-> (obj/get shadow "color") parse-color)}))) + +(defn parse-shadows + [^js shadows] + (when (some? shadows) + (into [] (map parse-shadow) shadows))) + +;;export interface PenpotFill { +;; fillColor?: string; +;; fillOpacity?: number; +;; fillColorGradient?: PenpotGradient; +;; fillColorRefFile?: string; +;; fillColorRefId?: string; +;; fillImage?: PenpotImageData; +;;} +(defn parse-fill + [^js fill] + (when (some? fill) + (d/without-nils + {:fill-color (-> (obj/get fill "fillColor") parse-hex) + :fill-opacity (obj/get fill "fillOpacity") + :fill-color-gradient (-> (obj/get fill "fillColorGradient") parse-gradient) + :fill-color-ref-file (-> (obj/get fill "fillColorRefFile") parse-id) + :fill-color-ref-id (-> (obj/get fill "fillColorRefId") parse-id) + :fill-image (-> (obj/get fill "fillImage") parse-image-data)}))) + +(defn parse-fills + [^js fills] + (when (some? fills) + (into [] (map parse-fill) fills))) + +;; export interface PenpotStroke { +;; strokeColor?: string; +;; strokeColorRefFile?: string; +;; strokeColorRefId?: string; +;; strokeOpacity?: number; +;; strokeStyle?: 'solid' | 'dotted' | 'dashed' | 'mixed' | 'none' | 'svg'; +;; strokeWidth?: number; +;; strokeAlignment?: 'center' | 'inner' | 'outer'; +;; strokeCapStart?: PenpotStrokeCap; +;; strokeCapEnd?: PenpotStrokeCap; +;; strokeColorGradient?: PenpotGradient; +;; } +(defn parse-stroke + [^js stroke] + (when (some? stroke) + (d/without-nils + {:stroke-color (-> (obj/get stroke "strokeColor") parse-hex) + :stroke-color-ref-file (-> (obj/get stroke "strokeColorRefFile") parse-id) + :stroke-color-ref-id (-> (obj/get stroke "strokeColorRefId") parse-id) + :stroke-opacity (obj/get stroke "strokeOpacity") + :stroke-style (-> (obj/get stroke "strokeStyle") parse-keyword) + :stroke-width (obj/get stroke "strokeWidth") + :stroke-alignment (-> (obj/get stroke "strokeAlignment") parse-keyword) + :stroke-cap-start (-> (obj/get stroke "strokeCapStart") parse-keyword) + :stroke-cap-end (-> (obj/get stroke "strokeCapEnd") parse-keyword) + :stroke-color-gradient (-> (obj/get stroke "strokeColorGradient") parse-gradient)}))) + +(defn parse-strokes + [^js strokes] + (when (some? strokes) + (into [] (map parse-stroke) strokes))) + +;; export interface PenpotBlur { +;; id?: string; +;; type?: 'layer-blur'; +;; value?: number; +;; hidden?: boolean; +;; } +(defn parse-blur + [^js blur] + (when (some? blur) + (d/without-nils + {:id (-> (obj/get blur "id") parse-id) + :type (-> (obj/get blur "type") parse-keyword) + :value (obj/get blur "value") + :hidden (obj/get blur "hidden")}))) + + +;; export interface PenpotExport { +;; type: 'png' | 'jpeg' | 'svg' | 'pdf'; +;; scale: number; +;; suffix: string; +;; } +(defn parse-export + [^js export] + (when (some? export) + (d/without-nils + {:type (-> (obj/get export "type") parse-keyword) + :scale (obj/get export "scale") + :suffix (obj/get export "suffix")}))) + +(defn parse-exports + [^js exports] + (when (some? exports) + (into [] (map parse-export) exports))) + +;; export interface PenpotFrameGuideColumnParams { +;; color: { color: string; opacity: number }; +;; type?: 'stretch' | 'left' | 'center' | 'right'; +;; size?: number; +;; margin?: number; +;; itemLength?: number; +;; gutter?: number; +;; } +(defn parse-frame-guide-column-params + [^js params] + (when params + (d/without-nils + {:color (-> (obj/get params "color") parse-color) + :type (-> (obj/get params "type") parse-keyword) + :size (obj/get params "size") + :margin (obj/get params "margin") + :item-length (obj/get params "itemLength") + :gutter (obj/get params "gutter")}))) + +;; export interface PenpotFrameGuideColumn { +;; type: 'column'; +;; display: boolean; +;; params: PenpotFrameGuideColumnParams; +;; } +(defn parse-frame-guide-column + [^js guide] + (when guide + (d/without-nils + {:type (-> (obj/get guide "type") parse-keyword) + :display (obj/get guide "display") + :params (-> (obj/get guide "params") parse-frame-guide-column-params)}))) + +;; export interface PenpotFrameGuideRow { +;; type: 'row'; +;; display: boolean; +;; params: PenpotFrameGuideColumnParams; +;; } + +(defn parse-frame-guide-row + [^js guide] + (when guide + (d/without-nils + {:type (-> (obj/get guide "type") parse-keyword) + :display (obj/get guide "display") + :params (-> (obj/get guide "params") parse-frame-guide-column-params)}))) + +;;export interface PenpotFrameGuideSquareParams { +;; color: { color: string; opacity: number }; +;; size?: number; +;;} +(defn parse-frame-guide-square-params + [^js params] + (when (some? params) + (d/without-nils + {:color (-> (obj/get params "color") parse-color) + :size (obj/get params "size")}))) + +;; export interface PenpotFrameGuideSquare { +;; type: 'square'; +;; display: boolean; +;; params: PenpotFrameGuideSquareParams; +;; } +(defn parse-frame-guide-square + [^js guide] + (when guide + (d/without-nils + {:type (-> (obj/get guide "type") parse-keyword) + :display (obj/get guide "display") + :params (-> (obj/get guide "params") parse-frame-guide-column-params)}))) + +(defn parse-frame-guide + [^js guide] + (when (some? guide) + (case (obj/get guide "type") + "column" + parse-frame-guide-column + + "row" + parse-frame-guide-row + + "square" + (parse-frame-guide-square guide)))) + +(defn parse-frame-guides + [^js guides] + (when (some? guides) + (into [] (map parse-frame-guide) guides))) + +;;interface PenpotPathCommand { +;; command: +;; | 'M' | 'move-to' +;; | 'Z' | 'close-path' +;; | 'L' | 'line-to' +;; | 'H' | 'line-to-horizontal' +;; | 'V' | 'line-to-vertical' +;; | 'C' | 'curve-to' +;; | 'S' | 'smooth-curve-to' +;; | 'Q' | 'quadratic-bezier-curve-to' +;; | 'T' | 'smooth-quadratic-bezier-curve-to' +;; | 'A' | 'elliptical-arc'; +;; +;; params?: { +;; x?: number; +;; y?: number; +;; c1x: number; +;; c1y: number; +;; c2x: number; +;; c2y: number; +;; rx?: number; +;; ry?: number; +;; xAxisRotation?: number; +;; largeArcFlag?: boolean; +;; sweepFlag?: boolean; +;; }; +;;} +(defn parse-command-type + [^string command-type] + (case command-type + "M" :move-to + "Z" :close-path + "L" :line-to + "H" :line-to-horizontal + "V" :line-to-vertical + "C" :curve-to + "S" :smooth-curve-to + "Q" :quadratic-bezier-curve-to + "T" :smooth-quadratic-bezier-curve-to + "A" :elliptical-arc + (parse-keyword command-type))) + +(defn parse-command-params + [^js params] + (when (some? params) + (d/without-nils + {:x (obj/get params "x") + :y (obj/get params "y") + :c1x (obj/get params "c1x") + :c1y (obj/get params "c1y") + :c2x (obj/get params "c2x") + :c2y (obj/get params "c2y") + :rx (obj/get params "rx") + :ry (obj/get params "ry") + :x-axis-rotation (obj/get params "xAxisRotation") + :large-arc-flag (obj/get params "largeArcFlag") + :sweep-flag (obj/get params "sweepFlag")}))) + +(defn parse-command + [^js command] + (when (some? command) + (d/without-nils + {:command (-> (obj/get command "command") parse-command-type) + :params (-> (obj/get command "paras") parse-command-params)}))) + +(defn parse-path-content + [^js content] + (when (some? content) + (into [] (map parse-command) content))) diff --git a/frontend/src/app/plugins/public_utils.cljs b/frontend/src/app/plugins/public_utils.cljs index 4ea08fa57..d3ed6a46e 100644 --- a/frontend/src/app/plugins/public_utils.cljs +++ b/frontend/src/app/plugins/public_utils.cljs @@ -9,6 +9,7 @@ (:require [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] + [app.plugins.format :as format] [app.plugins.shape :as shape] [app.plugins.utils :as u])) @@ -22,4 +23,4 @@ (let [shapes (->> shapes (map u/proxy->shape))] (-> (gsh/shapes->rect shapes) (grc/rect->center) - (u/to-js))))) + (format/format-point))))) diff --git a/frontend/src/app/plugins/shape.cljs b/frontend/src/app/plugins/shape.cljs index 47312f016..fb5a88ff6 100644 --- a/frontend/src/app/plugins/shape.cljs +++ b/frontend/src/app/plugins/shape.cljs @@ -38,243 +38,18 @@ [app.main.data.workspace.texts :as dwt] [app.main.store :as st] [app.plugins.flex :as flex] + [app.plugins.format :as format] [app.plugins.grid :as grid] + [app.plugins.parser :as parser] + [app.plugins.text :as text] [app.plugins.utils :as u] [app.util.object :as obj] [app.util.path.format :as upf] - [app.util.text-editor :as ted] [cuerdas.core :as str])) (def lib-typography-proxy? nil) (def lib-component-proxy nil) -(deftype TextRange [$plugin $file $page $id start end] - Object - (applyTypography [_ typography] - (let [typography (u/proxy->library-typography typography) - attrs (-> typography - (assoc :typography-ref-file $file) - (assoc :typography-ref-id (:id typography)) - (dissoc :id :name))] - (st/emit! (dwt/update-text-range $id start end attrs))))) - -(defn mixed-value - [values] - (let [s (set values)] - (if (= (count s) 1) (first s) "mixed"))) - -(defn text-range? - [range] - (instance? TextRange range)) - -(defn text-range - [plugin-id file-id page-id id start end] - (-> (TextRange. plugin-id file-id page-id id start end) - (crc/add-properties! - {:name "$plugin" :enumerable false :get (constantly plugin-id)} - {:name "$id" :enumerable false :get (constantly id)} - {:name "$file" :enumerable false :get (constantly file-id)} - {:name "$page" :enumerable false :get (constantly page-id)} - - {:name "shape" - :get #(-> % u/proxy->shape)} - - {:name "characters" - :get #(let [range-data - (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] - (->> range-data (map :text) (str/join "")))} - - {:name "fontId" - :get #(let [range-data - (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] - (->> range-data (map :font-id) mixed-value)) - - :set - (fn [_ value] - (cond - (not (string? value)) - (u/display-not-valid :fontId value) - - :else - (st/emit! (dwt/update-text-range id start end {:font-id value}))))} - - {:name "fontFamily" - :get #(let [range-data - (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] - (->> range-data (map :font-family) mixed-value)) - - :set - (fn [_ value] - (cond - (not (string? value)) - (u/display-not-valid :fontFamily value) - - :else - (st/emit! (dwt/update-text-range id start end {:font-family value}))))} - - {:name "fontVariantId" - :get #(let [range-data - (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] - (->> range-data (map :font-variant-id) mixed-value)) - :set - (fn [_ value] - (cond - (not (string? value)) - (u/display-not-valid :fontVariantId value) - - :else - (st/emit! (dwt/update-text-range id start end {:font-variant-id value}))))} - - {:name "fontSize" - :get #(let [range-data - (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] - (->> range-data (map :font-size) mixed-value)) - :set - (fn [_ value] - (cond - (not (string? value)) - (u/display-not-valid :fontSize value) - - :else - (st/emit! (dwt/update-text-range id start end {:font-size value}))))} - - {:name "fontWeight" - :get #(let [range-data - (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] - (->> range-data (map :font-weight) mixed-value)) - :set - (fn [_ value] - (cond - (not (string? value)) - (u/display-not-valid :fontWeight value) - - :else - (st/emit! (dwt/update-text-range id start end {:font-weight value}))))} - - {:name "fontStyle" - :get #(let [range-data - (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] - (->> range-data (map :font-style) mixed-value)) - :set - (fn [_ value] - (cond - (not (string? value)) - (u/display-not-valid :fontStyle value) - - :else - (st/emit! (dwt/update-text-range id start end {:font-style value}))))} - - {:name "lineHeight" - :get #(let [range-data - (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] - (->> range-data (map :line-height) mixed-value)) - :set - (fn [_ value] - (cond - (not (string? value)) - (u/display-not-valid :lineHeight value) - - :else - (st/emit! (dwt/update-text-range id start end {:line-height value}))))} - - {:name "letterSpacing" - :get #(let [range-data - (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] - (->> range-data (map :letter-spacing) mixed-value)) - :set - (fn [_ value] - (cond - (not (string? value)) - (u/display-not-valid :letterSpacing value) - - :else - (st/emit! (dwt/update-text-range id start end {:letter-spacing value}))))} - - {:name "textTransform" - :get #(let [range-data - (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] - (->> range-data (map :text-transform) mixed-value)) - :set - (fn [_ value] - (cond - (not (string? value)) - (u/display-not-valid :textTransform value) - - :else - (st/emit! (dwt/update-text-range id start end {:text-transform value}))))} - - {:name "textDecoration" - :get #(let [range-data - (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] - (->> range-data (map :text-decoration) mixed-value)) - :set - (fn [_ value] - (cond - (not (string? value)) - (u/display-not-valid :textDecoration value) - - :else - (st/emit! (dwt/update-text-range id start end {:text-decoration value}))))} - - {:name "direction" - :get #(let [range-data - (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] - (->> range-data (map :direction) mixed-value)) - :set - (fn [_ value] - (cond - (not (string? value)) - (u/display-not-valid :direction value) - - :else - (st/emit! (dwt/update-text-range id start end {:direction value}))))} - - {:name "align" - :get #(let [range-data - (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] - (->> range-data (map :text-align) mixed-value)) - :set - (fn [_ value] - (cond - (not (string? value)) - (u/display-not-valid :text-align value) - - :else - (st/emit! (dwt/update-text-range id start end {:text-align value}))))} - - {:name "fills" - :get #(let [range-data - (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] - (->> range-data (map :fills) mixed-value u/array-to-js)) - :set - (fn [_ value] - (let [value (mapv #(u/from-js %) value)] - (cond - (not (sm/validate [:vector ::cts/fill] value)) - (u/display-not-valid :fills value) - - :else - (st/emit! (dwt/update-text-range id start end {:fills value})))))}))) - -(declare shape-proxy) - -(defn parse-command - [entry] - (update entry - :command - #(case % - "M" :move-to - "Z" :close-path - "L" :line-to - "H" :line-to-horizontal - "V" :line-to-vertical - "C" :curve-to - "S" :smooth-curve-to - "Q" :quadratic-bezier-curve-to - "T" :smooth-quadratic-bezier-curve-to - "A" :elliptical-arc - (keyword %)))) - (defn text-props [shape] (d/merge @@ -282,6 +57,48 @@ (dwt/current-paragraph-values {:shape shape :attrs txt/paragraph-attrs}) (dwt/current-text-values {:shape shape :attrs txt/text-node-attrs}))) +(declare shape-proxy) +(declare shape-proxy?) + +#_(defn parse-command + [entry] + (update entry + :command + #(case % + "M" :move-to + "Z" :close-path + "L" :line-to + "H" :line-to-horizontal + "V" :line-to-vertical + "C" :curve-to + "S" :smooth-curve-to + "Q" :quadratic-bezier-curve-to + "T" :smooth-quadratic-bezier-curve-to + "A" :elliptical-arc + (keyword %)))) + +(defn- shadow-defaults + [shadow] + (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} + shadow)) + +(defn- blur-defaults + [blur] + (d/patch-object + {:id (uuid/next) + :type :layer-blur + :value 4 + :hidden false} + blur)) + (deftype ShapeProxy [$plugin $file $page $id] Object (resize @@ -392,67 +209,107 @@ (getChildren [_] (let [shape (u/locate-shape $file $page $id)] - (if (or (cfh/frame-shape? shape) (cfh/group-shape? shape) (cfh/svg-raw-shape? shape) (cfh/bool-shape? shape)) - (apply array (->> (u/locate-shape $file $page $id) - :shapes - (map #(shape-proxy $plugin $file $page %)))) - (u/display-not-valid :getChildren (:type shape))))) + (cond + + (and (not (cfh/frame-shape? shape)) + (not (cfh/group-shape? shape)) + (not (cfh/svg-raw-shape? shape)) + (not (cfh/bool-shape? shape))) + (u/display-not-valid :getChildren (:type shape)) + + :else + (->> (u/locate-shape $file $page $id) + (:shapes) + (format/format-array #(shape-proxy $plugin $file $page %)))))) (appendChild [_ child] (let [shape (u/locate-shape $file $page $id)] - (if (or (cfh/frame-shape? shape) (cfh/group-shape? shape) (cfh/svg-raw-shape? shape) (cfh/bool-shape? shape)) + (cond + (and (not (cfh/frame-shape? shape)) + (not (cfh/group-shape? shape)) + (not (cfh/svg-raw-shape? shape)) + (not (cfh/bool-shape? shape))) + (u/display-not-valid :appendChild (:type shape)) + + (not (shape-proxy? child)) + (u/display-not-valid :appendChild-child child) + + :else (let [child-id (obj/get child "$id")] - (st/emit! (dw/relocate-shapes #{child-id} $id 0))) - (u/display-not-valid :appendChild (:type shape))))) + (st/emit! (dw/relocate-shapes #{child-id} $id 0)))))) (insertChild [_ index child] (let [shape (u/locate-shape $file $page $id)] - (if (or (cfh/frame-shape? shape) (cfh/group-shape? shape) (cfh/svg-raw-shape? shape) (cfh/bool-shape? shape)) + (cond + (and (not (cfh/frame-shape? shape)) + (not (cfh/group-shape? shape)) + (not (cfh/svg-raw-shape? shape)) + (not (cfh/bool-shape? shape))) + (u/display-not-valid :insertChild (:type shape)) + + (not (shape-proxy? child)) + (u/display-not-valid :insertChild-child child) + + :else (let [child-id (obj/get child "$id")] - (st/emit! (dw/relocate-shapes #{child-id} $id index))) - (u/display-not-valid :insertChild (:type shape))))) + (st/emit! (dw/relocate-shapes #{child-id} $id index)))))) ;; Only for frames (addFlexLayout [_] (let [shape (u/locate-shape $file $page $id)] - (if (cfh/frame-shape? shape) + (cond + (not (cfh/frame-shape? shape)) + (u/display-not-valid :addFlexLayout (:type shape)) + + :else (do (st/emit! (dwsl/create-layout-from-id $id :flex :from-frame? true :calculate-params? false)) - (grid/grid-layout-proxy $plugin $file $page $id)) - (u/display-not-valid :addFlexLayout (:type shape))))) + (grid/grid-layout-proxy $plugin $file $page $id))))) (addGridLayout [_] (let [shape (u/locate-shape $file $page $id)] - (if (cfh/frame-shape? shape) + (cond + (not (cfh/frame-shape? shape)) + (u/display-not-valid :addGridLayout (:type shape)) + + :else (do (st/emit! (dwsl/create-layout-from-id $id :grid :from-frame? true :calculate-params? false)) - (grid/grid-layout-proxy $plugin $file $page $id)) - (u/display-not-valid :addGridLayout (:type shape))))) + (grid/grid-layout-proxy $plugin $file $page $id))))) ;; Make masks for groups (makeMask [_] (let [shape (u/locate-shape $file $page $id)] - (if (cfh/group-shape? shape) - (st/emit! (dwg/mask-group #{$id})) - (u/display-not-valid :makeMask (:type shape))))) + (cond + (not (cfh/group-shape? shape)) + (u/display-not-valid :makeMask (:type shape)) + + :else + (st/emit! (dwg/mask-group #{$id}))))) (removeMask [_] (let [shape (u/locate-shape $file $page $id)] - (if (cfh/mask-shape? shape) - (st/emit! (dwg/unmask-group #{$id})) - (u/display-not-valid :removeMask (:type shape))))) + (cond + (not (cfh/mask-shape? shape)) + (u/display-not-valid :removeMask (:type shape)) + + :else + (st/emit! (dwg/unmask-group #{$id}))))) ;; Only for path and bool shapes (toD [_] (let [shape (u/locate-shape $file $page $id)] - (if (cfh/path-shape? shape) - (upf/format-path (:content shape)) - (u/display-not-valid :makeMask (:type shape))))) + (cond + (not (cfh/path-shape? shape)) + (u/display-not-valid :makeMask (:type shape)) + + :else + (upf/format-path (:content shape))))) ;; Text shapes (getRange @@ -469,7 +326,7 @@ (u/display-not-valid :getRange-end end) :else - (text-range $plugin $file $page $id start end)))) + (text/text-range $plugin $file $page $id start end)))) (applyTypography [_ typography] @@ -554,11 +411,6 @@ (let [[root component] (u/locate-component objects shape)] (lib-component-proxy $plugin (:component-file root) (:id component))))))) -(crc/define-properties! - ShapeProxy - {:name js/Symbol.toStringTag - :get (fn [] (str "ShapeProxy"))}) - (defn shape-proxy? [p] (instance? ShapeProxy p)) @@ -566,6 +418,11 @@ (do (set! flex/shape-proxy? shape-proxy?) (set! grid/shape-proxy? shape-proxy?)) +(crc/define-properties! + ShapeProxy + {:name js/Symbol.toStringTag + :get (fn [] (str "ShapeProxy"))}) + (defn shape-proxy ([plugin-id id] (shape-proxy plugin-id (:current-file-id @st/state) (:current-page-id @st/state) id)) @@ -782,23 +639,11 @@ (st/emit! (dwsh/update-shapes [id] #(assoc % :blend-mode value))))))} {:name "shadows" - :get #(-> % u/proxy->shape :shadow u/array-to-js) + :get #(-> % u/proxy->shape :shadow format/format-shadows) :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} - (u/from-js val #{:style :type}))) - value)] + value (mapv #(shadow-defaults (parser/parse-shadow %)) value)] (cond (not (sm/validate [:vector ::ctss/shadow] value)) (u/display-not-valid :shadows value) @@ -807,19 +652,13 @@ (st/emit! (dwsh/update-shapes [id] #(assoc % :shadow value))))))} {:name "blur" - :get #(-> % u/proxy->shape :blur u/to-js) + :get #(-> % u/proxy->shape :blur format/format-blur) :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} - (u/from-js value))] + value (blur-defaults (parser/parse-blur value))] (cond (not (sm/validate ::ctsb/blur value)) (u/display-not-valid :blur value) @@ -828,11 +667,11 @@ (st/emit! (dwsh/update-shapes [id] #(assoc % :blur value)))))))} {:name "exports" - :get #(-> % u/proxy->shape :exports u/array-to-js) + :get #(-> % u/proxy->shape :exports format/format-exports) :set (fn [self value] (let [id (obj/get self "$id") - value (mapv #(u/from-js %) value)] + value (parser/parse-exports value)] (cond (not (sm/validate [:vector ::ctse/export] value)) (u/display-not-valid :exports value) @@ -989,13 +828,13 @@ ;; Strokes and fills {:name "fills" :get #(if (cfh/text-shape? data) - (-> % u/proxy->shape text-props :fills u/array-to-js) - (-> % u/proxy->shape :fills u/array-to-js)) + (-> % u/proxy->shape text-props :fills format/format-fills) + (-> % u/proxy->shape :fills format/format-fills)) :set (fn [self value] (let [shape (u/proxy->shape self) id (:id shape) - value (mapv #(u/from-js %) value)] + value (parser/parse-fills value)] (cond (not (sm/validate [:vector ::cts/fill] value)) (u/display-not-valid :fills value) @@ -1007,11 +846,11 @@ (st/emit! (dwsh/update-shapes [id] #(assoc % :fills value))))))} {:name "strokes" - :get #(-> % u/proxy->shape :strokes u/array-to-js) + :get #(-> % u/proxy->shape :strokes format/format-strokes) :set (fn [self value] (let [id (obj/get self "$id") - value (mapv #(u/from-js % #{:stroke-style :stroke-alignment}) value)] + value (parser/parse-strokes value)] (cond (not (sm/validate [:vector ::cts/stroke] value)) (u/display-not-valid :strokes value) @@ -1068,10 +907,10 @@ (flex/flex-layout-proxy plugin-id file-id page-id id))))} {:name "guides" - :get #(-> % u/proxy->shape :grids u/array-to-js) + :get #(-> % u/proxy->shape :grids format/format-frame-guides) :set (fn [self value] (let [id (obj/get self "$id") - value (mapv #(u/from-js %) value)] + value (parser/parse-frame-guides value)] (cond (not (sm/validate [:vector ::ctg/grid] value)) (u/display-not-valid :guides value) @@ -1105,213 +944,21 @@ :else (st/emit! (dwsl/update-layout #{id} {:layout-item-v-sizing value})))))}))) - (cond-> (cfh/text-shape? data) - (crc/add-properties! - {:name "characters" - :get #(-> % u/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 - (cond - (or (not (string? value)) (empty? value)) - (u/display-not-valid :characters value) - - (contains? (:workspace-editor-state @st/state) id) - (let [shape (u/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))) - - :else - (st/emit! (dwsh/update-shapes [id] #(txt/change-text % value))))))} - - {:name "growType" - :get #(-> % u/proxy->shape :grow-type d/name) - :set - (fn [self value] - (let [id (obj/get self "$id") - value (keyword value)] - (cond - (not (contains? #{:auto-width :auto-height :fixed} value)) - (u/display-not-valid :growType value) - - :else - (st/emit! (dwsh/update-shapes [id] #(assoc % :grow-type value))))))} - - {:name "fontId" - :get #(-> % u/proxy->shape text-props :font-id) - :set - (fn [self value] - (let [id (obj/get self "$id")] - (cond - (not (string? value)) - (u/display-not-valid :fontId value) - - :else - (st/emit! (dwt/update-attrs id {:font-id value})))))} - - {:name "fontFamily" - :get #(-> % u/proxy->shape text-props :font-family) - :set - (fn [self value] - (let [id (obj/get self "$id")] - (cond - (not (string? value)) - (u/display-not-valid :fontFamily value) - - :else - (st/emit! (dwt/update-attrs id {:font-family value})))))} - - {:name "fontVariantId" - :get #(-> % u/proxy->shape text-props :font-variant-id) - :set - (fn [self value] - (let [id (obj/get self "$id")] - (cond - (not (string? value)) - (u/display-not-valid :fontVariantId value) - - :else - (st/emit! (dwt/update-attrs id {:font-variant-id value})))))} - - {:name "fontSize" - :get #(-> % u/proxy->shape text-props :font-size) - :set - (fn [self value] - (let [id (obj/get self "$id")] - (cond - (not (string? value)) - (u/display-not-valid :fontSize value) - - :else - (st/emit! (dwt/update-attrs id {:font-size value})))))} - - {:name "fontWeight" - :get #(-> % u/proxy->shape text-props :font-weight) - :set - (fn [self value] - (let [id (obj/get self "$id")] - (cond - (not (string? value)) - (u/display-not-valid :fontWeight value) - - :else - (st/emit! (dwt/update-attrs id {:font-weight value})))))} - - {:name "fontStyle" - :get #(-> % u/proxy->shape text-props :font-style) - :set - (fn [self value] - (let [id (obj/get self "$id")] - (cond - (not (string? value)) - (u/display-not-valid :fontStyle value) - - :else - (st/emit! (dwt/update-attrs id {:font-style value})))))} - - {:name "lineHeight" - :get #(-> % u/proxy->shape text-props :line-height) - :set - (fn [self value] - (let [id (obj/get self "$id")] - (cond - (not (string? value)) - (u/display-not-valid :lineHeight value) - - :else - (st/emit! (dwt/update-attrs id {:line-height value})))))} - - {:name "letterSpacing" - :get #(-> % u/proxy->shape text-props :letter-spacing) - :set - (fn [self value] - (let [id (obj/get self "$id")] - (cond - (not (string? value)) - (u/display-not-valid :letterSpacing value) - - :else - (st/emit! (dwt/update-attrs id {:letter-spacing value})))))} - - {:name "textTransform" - :get #(-> % u/proxy->shape text-props :text-transform) - :set - (fn [self value] - (let [id (obj/get self "$id")] - (cond - (not (string? value)) - (u/display-not-valid :textTransform value) - - :else - (st/emit! (dwt/update-attrs id {:text-transform value})))))} - - {:name "textDecoration" - :get #(-> % u/proxy->shape text-props :text-decoration) - :set - (fn [self value] - (let [id (obj/get self "$id")] - (cond - (not (string? value)) - (u/display-not-valid :textDecoration value) - - :else - (st/emit! (dwt/update-attrs id {:text-decoration value})))))} - - {:name "direction" - :get #(-> % u/proxy->shape text-props :text-direction) - :set - (fn [self value] - (let [id (obj/get self "$id")] - (cond - (not (string? value)) - (u/display-not-valid :textDecoration value) - - :else - (st/emit! (dwt/update-attrs id {:text-decoration value})))))} - - {:name "align" - :get #(-> % u/proxy->shape text-props :text-align) - :set - (fn [self value] - (let [id (obj/get self "$id")] - (cond - (not (string? value)) - (u/display-not-valid :align value) - - :else - (st/emit! (dwt/update-attrs id {:text-align value})))))} - - {:name "verticalAlign" - :get #(-> % u/proxy->shape text-props :vertical-align) - :set - (fn [self value] - (let [id (obj/get self "$id")] - (cond - (not (string? value)) - (u/display-not-valid :verticalAlign value) - - :else - (st/emit! (dwt/update-attrs id {:vertical-align value})))))})) + (cond-> (cfh/text-shape? data) (text/add-text-props)) (cond-> (or (cfh/path-shape? data) (cfh/bool-shape? data)) (crc/add-properties! {:name "content" - :get #(-> % u/proxy->shape :content u/array-to-js) + :get #(-> % u/proxy->shape :content format/format-path-content) :set (fn [_ value] (let [content - (->> value - (map u/from-js) - (mapv parse-command) + (->> (parser/parse-path-content value) (spp/simplify-commands))] (cond + (not (cfh/path-shape? data)) + (u/display-not-valid :content-type type) + (not (sm/validate ::ctsp/content content)) (u/display-not-valid :content value) diff --git a/frontend/src/app/plugins/text.cljs b/frontend/src/app/plugins/text.cljs new file mode 100644 index 000000000..5a05ebaaf --- /dev/null +++ b/frontend/src/app/plugins/text.cljs @@ -0,0 +1,434 @@ +;; 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.text + (:require + [app.common.data :as d] + [app.common.record :as crc] + [app.common.schema :as sm] + [app.common.text :as txt] + [app.common.types.shape :as cts] + [app.main.data.workspace.shapes :as dwsh] + [app.main.data.workspace.texts :as dwt] + [app.main.store :as st] + [app.plugins.format :as format] + [app.plugins.parser :as parser] + [app.plugins.utils :as u] + [app.util.object :as obj] + [app.util.text-editor :as ted] + [cuerdas.core :as str])) + +(defn mixed-value + [values] + (let [s (set values)] + (if (= (count s) 1) (first s) "mixed"))) + +(deftype TextRange [$plugin $file $page $id start end] + Object + (applyTypography [_ typography] + (let [typography (u/proxy->library-typography typography) + attrs (-> typography + (assoc :typography-ref-file $file) + (assoc :typography-ref-id (:id typography)) + (dissoc :id :name))] + (st/emit! (dwt/update-text-range $id start end attrs))))) + +(defn text-range? + [range] + (instance? TextRange range)) + +(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}))) + +(defn text-range + [plugin-id file-id page-id id start end] + (-> (TextRange. plugin-id file-id page-id id start end) + (crc/add-properties! + {:name "$plugin" :enumerable false :get (constantly plugin-id)} + {:name "$id" :enumerable false :get (constantly id)} + {:name "$file" :enumerable false :get (constantly file-id)} + {:name "$page" :enumerable false :get (constantly page-id)} + + {:name "shape" + :get #(-> % u/proxy->shape)} + + {:name "characters" + :get #(let [range-data + (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] + (->> range-data (map :text) (str/join "")))} + + {:name "fontId" + :get #(let [range-data + (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] + (->> range-data (map :font-id) mixed-value)) + + :set + (fn [_ value] + (cond + (not (string? value)) + (u/display-not-valid :fontId value) + + :else + (st/emit! (dwt/update-text-range id start end {:font-id value}))))} + + {:name "fontFamily" + :get #(let [range-data + (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] + (->> range-data (map :font-family) mixed-value)) + + :set + (fn [_ value] + (cond + (not (string? value)) + (u/display-not-valid :fontFamily value) + + :else + (st/emit! (dwt/update-text-range id start end {:font-family value}))))} + + {:name "fontVariantId" + :get #(let [range-data + (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] + (->> range-data (map :font-variant-id) mixed-value)) + :set + (fn [_ value] + (cond + (not (string? value)) + (u/display-not-valid :fontVariantId value) + + :else + (st/emit! (dwt/update-text-range id start end {:font-variant-id value}))))} + + {:name "fontSize" + :get #(let [range-data + (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] + (->> range-data (map :font-size) mixed-value)) + :set + (fn [_ value] + (cond + (not (string? value)) + (u/display-not-valid :fontSize value) + + :else + (st/emit! (dwt/update-text-range id start end {:font-size value}))))} + + {:name "fontWeight" + :get #(let [range-data + (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] + (->> range-data (map :font-weight) mixed-value)) + :set + (fn [_ value] + (cond + (not (string? value)) + (u/display-not-valid :fontWeight value) + + :else + (st/emit! (dwt/update-text-range id start end {:font-weight value}))))} + + {:name "fontStyle" + :get #(let [range-data + (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] + (->> range-data (map :font-style) mixed-value)) + :set + (fn [_ value] + (cond + (not (string? value)) + (u/display-not-valid :fontStyle value) + + :else + (st/emit! (dwt/update-text-range id start end {:font-style value}))))} + + {:name "lineHeight" + :get #(let [range-data + (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] + (->> range-data (map :line-height) mixed-value)) + :set + (fn [_ value] + (cond + (not (string? value)) + (u/display-not-valid :lineHeight value) + + :else + (st/emit! (dwt/update-text-range id start end {:line-height value}))))} + + {:name "letterSpacing" + :get #(let [range-data + (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] + (->> range-data (map :letter-spacing) mixed-value)) + :set + (fn [_ value] + (cond + (not (string? value)) + (u/display-not-valid :letterSpacing value) + + :else + (st/emit! (dwt/update-text-range id start end {:letter-spacing value}))))} + + {:name "textTransform" + :get #(let [range-data + (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] + (->> range-data (map :text-transform) mixed-value)) + :set + (fn [_ value] + (cond + (not (string? value)) + (u/display-not-valid :textTransform value) + + :else + (st/emit! (dwt/update-text-range id start end {:text-transform value}))))} + + {:name "textDecoration" + :get #(let [range-data + (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] + (->> range-data (map :text-decoration) mixed-value)) + :set + (fn [_ value] + (cond + (not (string? value)) + (u/display-not-valid :textDecoration value) + + :else + (st/emit! (dwt/update-text-range id start end {:text-decoration value}))))} + + {:name "direction" + :get #(let [range-data + (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] + (->> range-data (map :direction) mixed-value)) + :set + (fn [_ value] + (cond + (not (string? value)) + (u/display-not-valid :direction value) + + :else + (st/emit! (dwt/update-text-range id start end {:direction value}))))} + + {:name "align" + :get #(let [range-data + (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] + (->> range-data (map :text-align) mixed-value)) + :set + (fn [_ value] + (cond + (not (string? value)) + (u/display-not-valid :text-align value) + + :else + (st/emit! (dwt/update-text-range id start end {:text-align value}))))} + + {:name "fills" + :get #(let [range-data + (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] + (->> range-data (map :fills) mixed-value format/format-fills)) + :set + (fn [_ value] + (let [value (parser/parse-fills value)] + (cond + (not (sm/validate [:vector ::cts/fill] value)) + (u/display-not-valid :fills value) + + :else + (st/emit! (dwt/update-text-range id start end {:fills value})))))}))) + +(defn add-text-props + [shape-proxy] + (crc/add-properties! + shape-proxy + {:name "characters" + :get #(-> % u/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 + (cond + (or (not (string? value)) (empty? value)) + (u/display-not-valid :characters value) + + (contains? (:workspace-editor-state @st/state) id) + (let [shape (u/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))) + + :else + (st/emit! (dwsh/update-shapes [id] #(txt/change-text % value))))))} + + {:name "growType" + :get #(-> % u/proxy->shape :grow-type d/name) + :set + (fn [self value] + (let [id (obj/get self "$id") + value (keyword value)] + (cond + (not (contains? #{:auto-width :auto-height :fixed} value)) + (u/display-not-valid :growType value) + + :else + (st/emit! (dwsh/update-shapes [id] #(assoc % :grow-type value))))))} + + {:name "fontId" + :get #(-> % u/proxy->shape text-props :font-id) + :set + (fn [self value] + (let [id (obj/get self "$id")] + (cond + (not (string? value)) + (u/display-not-valid :fontId value) + + :else + (st/emit! (dwt/update-attrs id {:font-id value})))))} + + {:name "fontFamily" + :get #(-> % u/proxy->shape text-props :font-family) + :set + (fn [self value] + (let [id (obj/get self "$id")] + (cond + (not (string? value)) + (u/display-not-valid :fontFamily value) + + :else + (st/emit! (dwt/update-attrs id {:font-family value})))))} + + {:name "fontVariantId" + :get #(-> % u/proxy->shape text-props :font-variant-id) + :set + (fn [self value] + (let [id (obj/get self "$id")] + (cond + (not (string? value)) + (u/display-not-valid :fontVariantId value) + + :else + (st/emit! (dwt/update-attrs id {:font-variant-id value})))))} + + {:name "fontSize" + :get #(-> % u/proxy->shape text-props :font-size) + :set + (fn [self value] + (let [id (obj/get self "$id")] + (cond + (not (string? value)) + (u/display-not-valid :fontSize value) + + :else + (st/emit! (dwt/update-attrs id {:font-size value})))))} + + {:name "fontWeight" + :get #(-> % u/proxy->shape text-props :font-weight) + :set + (fn [self value] + (let [id (obj/get self "$id")] + (cond + (not (string? value)) + (u/display-not-valid :fontWeight value) + + :else + (st/emit! (dwt/update-attrs id {:font-weight value})))))} + + {:name "fontStyle" + :get #(-> % u/proxy->shape text-props :font-style) + :set + (fn [self value] + (let [id (obj/get self "$id")] + (cond + (not (string? value)) + (u/display-not-valid :fontStyle value) + + :else + (st/emit! (dwt/update-attrs id {:font-style value})))))} + + {:name "lineHeight" + :get #(-> % u/proxy->shape text-props :line-height) + :set + (fn [self value] + (let [id (obj/get self "$id")] + (cond + (not (string? value)) + (u/display-not-valid :lineHeight value) + + :else + (st/emit! (dwt/update-attrs id {:line-height value})))))} + + {:name "letterSpacing" + :get #(-> % u/proxy->shape text-props :letter-spacing) + :set + (fn [self value] + (let [id (obj/get self "$id")] + (cond + (not (string? value)) + (u/display-not-valid :letterSpacing value) + + :else + (st/emit! (dwt/update-attrs id {:letter-spacing value})))))} + + {:name "textTransform" + :get #(-> % u/proxy->shape text-props :text-transform) + :set + (fn [self value] + (let [id (obj/get self "$id")] + (cond + (not (string? value)) + (u/display-not-valid :textTransform value) + + :else + (st/emit! (dwt/update-attrs id {:text-transform value})))))} + + {:name "textDecoration" + :get #(-> % u/proxy->shape text-props :text-decoration) + :set + (fn [self value] + (let [id (obj/get self "$id")] + (cond + (not (string? value)) + (u/display-not-valid :textDecoration value) + + :else + (st/emit! (dwt/update-attrs id {:text-decoration value})))))} + + {:name "direction" + :get #(-> % u/proxy->shape text-props :text-direction) + :set + (fn [self value] + (let [id (obj/get self "$id")] + (cond + (not (string? value)) + (u/display-not-valid :textDecoration value) + + :else + (st/emit! (dwt/update-attrs id {:text-decoration value})))))} + + {:name "align" + :get #(-> % u/proxy->shape text-props :text-align) + :set + (fn [self value] + (let [id (obj/get self "$id")] + (cond + (not (string? value)) + (u/display-not-valid :align value) + + :else + (st/emit! (dwt/update-attrs id {:text-align value})))))} + + {:name "verticalAlign" + :get #(-> % u/proxy->shape text-props :vertical-align) + :set + (fn [self value] + (let [id (obj/get self "$id")] + (cond + (not (string? value)) + (u/display-not-valid :verticalAlign value) + + :else + (st/emit! (dwt/update-attrs id {:vertical-align value})))))})) diff --git a/frontend/src/app/plugins/user.cljs b/frontend/src/app/plugins/user.cljs index 42cd32f6e..220a5e08c 100644 --- a/frontend/src/app/plugins/user.cljs +++ b/frontend/src/app/plugins/user.cljs @@ -8,6 +8,7 @@ (:require [app.common.record :as crc] [app.config :as cfg] + [app.plugins.format :as format] [app.plugins.utils :as u] [app.util.object :as obj])) @@ -54,5 +55,5 @@ (-> (ActiveUserProxy. plugin-id session-id) (add-user-properties) (crc/add-properties! - {:name "position" :get (fn [_] (-> (u/locate-presence session-id) :point u/to-js))} + {:name "position" :get (fn [_] (-> (u/locate-presence session-id) :point format/format-point))} {:name "zoom" :get (fn [_] (-> (u/locate-presence session-id) :zoom))}))) diff --git a/frontend/src/app/plugins/utils.cljs b/frontend/src/app/plugins/utils.cljs index ab3015a0b..4df6207d8 100644 --- a/frontend/src/app/plugins/utils.cljs +++ b/frontend/src/app/plugins/utils.cljs @@ -9,13 +9,10 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.spec :as us] [app.common.types.container :as ctn] [app.common.types.file :as ctf] - [app.common.uuid :as uuid] [app.main.store :as st] [app.util.object :as obj] - [cuerdas.core :as str] [promesa.core :as p])) (defn locate-file @@ -143,58 +140,6 @@ (-> (get-state self attr) (mapfn)))) -(defn from-js - "Converts the object back to js" - ([obj] - (from-js obj #{:type})) - ([obj keyword-keys] - (when (some? obj) - (let [process-node - (fn process-node [node] - (reduce-kv - (fn [m k v] - (let [k (keyword (str/kebab k)) - v (cond (map? v) - (process-node v) - - (vector? v) - (mapv process-node v) - - (and (string? v) (re-matches us/uuid-rx v)) - (uuid/uuid v) - - (contains? keyword-keys k) - (keyword v) - - :else v)] - (assoc m k v))) - {} - node))] - (process-node (js->clj obj)))))) - -(defn to-js - "Converts to javascript an camelize the keys" - [obj] - (when (some? obj) - (let [result - (reduce-kv - (fn [m k v] - (let [v (cond (object? v) (to-js v) - (uuid? v) (dm/str v) - :else v)] - (assoc m (str/camel (name k)) v))) - {} - obj)] - (clj->js result)))) - -(defn array-to-js - [value] - (if (coll? value) - (.freeze - js/Object - (apply array (->> value (map to-js)))) - value)) - (defn result-p "Creates a pair of atom+promise. The promise will be resolved when the atom gets a value. We use this to return the promise to the library clients and resolve its value when a value is passed diff --git a/frontend/src/app/plugins/viewport.cljs b/frontend/src/app/plugins/viewport.cljs index c42696641..6973f33f7 100644 --- a/frontend/src/app/plugins/viewport.cljs +++ b/frontend/src/app/plugins/viewport.cljs @@ -14,6 +14,7 @@ [app.main.data.workspace.viewport :as dwv] [app.main.data.workspace.zoom :as dwz] [app.main.store :as st] + [app.plugins.format :as format] [app.plugins.utils :as u] [app.util.object :as obj])) @@ -88,6 +89,6 @@ :get (fn [_] (let [vport (dm/get-in @st/state [:workspace-local :vport])] - (.freeze js/Object (clj->js vport))))})) + (.freeze js/Object (format/format-bounds vport))))})) diff --git a/frontend/src/app/util/object.cljs b/frontend/src/app/util/object.cljs index 620303add..30962acc5 100644 --- a/frontend/src/app/util/object.cljs +++ b/frontend/src/app/util/object.cljs @@ -156,3 +156,8 @@ x) :else x))) + +(defn clear-empty + [^js obj] + (when (some? obj) + (js* "Object.entries(~{}).reduce((a, [k,v]) => (v == null ? a : (a[k]=v, a)), {}) " obj))) diff --git a/frontend/test/frontend_tests/plugins/context_shapes_test.cljs b/frontend/test/frontend_tests/plugins/context_shapes_test.cljs index ced9fff4b..e5e85a326 100644 --- a/frontend/test/frontend_tests/plugins/context_shapes_test.cljs +++ b/frontend/test/frontend_tests/plugins/context_shapes_test.cljs @@ -162,9 +162,8 @@ :offset-y 4 :blur 4 :spread 0 - :color {:color "#FABADA" :opacity 1} + :color {:color "#fabada" :opacity 1} :hidden false}])))) - (let [shadow #js {:style "fail"}] (set! (.-shadows shape) #js [shadow]) (t/is (= (-> (. shape -shadows) (aget 0) (aget "style")) "drop-shadow")))) @@ -208,20 +207,20 @@ (t/testing " - fills" (set! (.-fills shape) #js [#js {:fillColor 100}]) - (t/is (= (-> (. shape -fills) (aget 0) (aget "fillColor")) "#B1B2B5")) (t/is (= (get-in @store (get-shape-path :fills)) [{:fill-color "#B1B2B5" :fill-opacity 1}])) + (t/is (= (-> (. shape -fills) (aget 0) (aget "fillColor")) "#B1B2B5")) - (set! (.-fills shape) #js [#js {:fillColor "#FABADA" :fillOpacity 1}]) - (t/is (= (-> (. shape -fills) (aget 0) (aget "fillColor")) "#FABADA")) - (t/is (= (-> (. shape -fills) (aget 0) (aget "fillOpacity")) 1)) - (t/is (= (get-in @store (get-shape-path :fills)) [{:fill-color "#FABADA" :fill-opacity 1}]))) + (set! (.-fills shape) #js [#js {:fillColor "#fabada" :fillOpacity 1}]) + (t/is (= (get-in @store (get-shape-path :fills)) [{:fill-color "#fabada" :fill-opacity 1}])) + (t/is (= (-> (. shape -fills) (aget 0) (aget "fillColor")) "#fabada")) + (t/is (= (-> (. shape -fills) (aget 0) (aget "fillOpacity")) 1))) (t/testing " - strokes" - (set! (.-fills shape) #js [#js {:strokeColor "#FABADA" :strokeOpacity 1 :stroke-width 5}]) - (t/is (= (-> (. shape -fills) (aget 0) (aget "strokeColor")) "#FABADA")) - (t/is (= (-> (. shape -fills) (aget 0) (aget "strokeOpacity")) 1)) - (t/is (= (-> (. shape -fills) (aget 0) (aget "strokeWidth")) 5)) - (t/is (= (get-in @store (get-shape-path :fills)) [{:stroke-color "#FABADA" :stroke-opacity 1 :stroke-width 5}])))) + (set! (.-strokes shape) #js [#js {:strokeColor "#fabada" :strokeOpacity 1 :strokeWidth 5}]) + (t/is (= (get-in @store (get-shape-path :strokes)) [{:stroke-color "#fabada" :stroke-opacity 1 :stroke-width 5}])) + (t/is (= (-> (. shape -strokes) (aget 0) (aget "strokeColor")) "#fabada")) + (t/is (= (-> (. shape -strokes) (aget 0) (aget "strokeOpacity")) 1)) + (t/is (= (-> (. shape -strokes) (aget 0) (aget "strokeWidth")) 5)))) (t/testing "Relative properties" (let [frame (.createFrame context)]