♻️ Refactor penpot library

This commit is contained in:
Andrey Antukh 2025-05-08 09:51:25 +02:00
parent 8bdec66927
commit 0b7b6e2c23
18 changed files with 817 additions and 1034 deletions

View file

@ -6,6 +6,36 @@
### :boom: Breaking changes & Deprecations ### :boom: Breaking changes & Deprecations
**Breaking changes on penpot library:**
- Change the signature of the `addPage` method: it now accepts an object (as a single argument) where you can pass `id`,
`name`, and `background` props (instead of the previous positional arguments)
- Rename the `file.createRect` method to `file.addRect`
- Rename the `file.createCircle` method to `file.addCircle`
- Rename the `file.createPath` method to `file.addPath`
- Rename the `file.createText` method to `file.addText`
- Rename `file.startComponent` to `file.addComponent` (to preserve the naming style)
- Rename `file.createComponentInstance` to `file.addComponentInstance` (to preserve the naming style)
- Rename `file.lookupShape` to `file.getShape`
- Rename `file.asMap` to `file.toMap`
- Remove `file.updateLibraryColor` (use `file.addLibraryColor` if you just need to replace a color)
- Remove `file.deleteLibraryColor` (this library is intended to build files)
- Remove `file.updateLibraryTypography` (use `file.addLibraryTypography` if you just need to replace a typography)
- Remove `file.deleteLibraryTypography` (this library is intended to build files)
- Remove `file.add/update/deleteLibraryMedia` (they are no longer supported by Penpot and have been replaced by components)
- Remove `file.deleteObject` (this library is intended to build files)
- Remove `file.updateObject` (this library is intended to build files)
- Remove `file.finishComponent` (it is no longer necessary; see below for more details on component creation changes)
- Change the `file.getCurrentPageId` function to a read-only `file.currentPageId` property
- Add `file.currentFrameId` read-only property
- Add `file.lastId` read-only property
There are also relevant semantic changes in how components should be created: this refactor removes
all notions of the old components (v1). Since v2, the shapes that are part of a component live on a
page. So, from now on, to create a component, you should first create a frame, then add shapes
and/or groups to that frame, and then create a component by declaring that frame as the component
root.
### :heart: Community contributions (Thank you!) ### :heart: Community contributions (Thank you!)
### :sparkles: New features ### :sparkles: New features

View file

@ -55,8 +55,8 @@
:features features :features features
:ignore-sync-until ignore-sync-until :ignore-sync-until ignore-sync-until
:modified-at modified-at :modified-at modified-at
:deleted-at deleted-at :deleted-at deleted-at}
:create-page create-page {:create-page create-page
:page-id page-id}) :page-id page-id})
file (-> (bfc/insert-file! cfg file) file (-> (bfc/insert-file! cfg file)
(bfc/decode-row))] (bfc/decode-row))]

File diff suppressed because it is too large Load diff

View file

@ -732,20 +732,22 @@
(update-group [group objects] (update-group [group objects]
(let [lookup (d/getf objects) (let [lookup (d/getf objects)
children (->> group :shapes (map lookup))] children (get group :shapes)]
(cond (cond
;; If the group is empty we don't make any changes. Will be removed by a later process ;; If the group is empty we don't make any changes. Will be removed by a later process
(empty? children) (empty? children)
group group
(= :bool (:type group)) (= :bool (:type group))
(gsh/update-bool group children objects) (gsh/update-bool group objects)
(:masked-group group) (:masked-group group)
(set-mask-selrect group children) (->> (map lookup children)
(set-mask-selrect group))
:else :else
(gsh/update-group-selrect group children))))] (->> (map lookup children)
(gsh/update-group-selrect group)))))]
(if page-id (if page-id
(d/update-in-when data [:pages-index page-id :objects] reg-objects) (d/update-in-when data [:pages-index page-id :objects] reg-objects)

View file

@ -660,9 +660,13 @@
nil ;; so it does not need resize nil ;; so it does not need resize
(= (:type parent) :bool) (= (:type parent) :bool)
(gsh/update-bool parent children objects) (gsh/update-bool parent objects)
(= (:type parent) :group) (= (:type parent) :group)
;; FIXME: this functions should be
;; normalized in the same way as
;; update-bool in order to make all
;; this code consistent
(if (:masked-group parent) (if (:masked-group parent)
(gsh/update-mask-selrect parent children) (gsh/update-mask-selrect parent children)
(gsh/update-group-selrect parent children)))] (gsh/update-group-selrect parent children)))]

View file

@ -455,12 +455,12 @@
(defn update-bool (defn update-bool
"Calculates the selrect+points for the boolean shape" "Calculates the selrect+points for the boolean shape"
[shape _children objects] [shape objects]
(let [content (path/calc-bool-content shape objects) (let [content (path/calc-bool-content shape objects)
shape (assoc shape :content content)] shape (assoc shape :content content)]
(path/update-geometry shape))) (path/update-geometry shape)))
;; FIXME: revisit
(defn update-shapes-geometry (defn update-shapes-geometry
[objects ids] [objects ids]
(->> ids (->> ids
@ -474,7 +474,7 @@
(update-mask-selrect shape children) (update-mask-selrect shape children)
(cfh/bool-shape? shape) (cfh/bool-shape? shape)
(update-bool shape children objects) (update-bool shape objects)
(cfh/group-shape? shape) (cfh/group-shape? shape)
(update-group-selrect shape children) (update-group-selrect shape children)

View file

@ -23,28 +23,32 @@
(defn sample-file (defn sample-file
[label & {:keys [page-label name view-only?] :as params}] [label & {:keys [page-label name view-only?] :as params}]
(binding [ffeat/*current* #{"components/v2"}] (let [params
(let [params (cond-> params (cond-> params
label label
(assoc :id (thi/new-id! label)) (assoc :id (thi/new-id! label))
page-label (nil? name)
(assoc :page-id (thi/new-id! page-label)) (assoc :name "Test file")
(nil? name) :always
(assoc :name "Test file")) (assoc :features ffeat/default-features))
file (-> (ctf/make-file (dissoc params :page-label)) opts
(assoc :features #{"components/v2"}) (cond-> {}
(assoc :permissions {:can-edit (not (true? view-only?))})) page-label
(assoc :page-id (thi/new-id! page-label)))
page (-> file file (-> (ctf/make-file params opts)
:data (assoc :permissions {:can-edit (not (true? view-only?))}))
(ctpl/pages-seq)
(first))]
(with-meta file page (-> file
{:current-page-id (:id page)})))) :data
(ctpl/pages-seq)
(first))]
(with-meta file
{:current-page-id (:id page)})))
(defn validate-file! (defn validate-file!
([file] (validate-file! file {})) ([file] (validate-file! file {}))

View file

@ -137,33 +137,36 @@
(update :options assoc :components-v2 true))))) (update :options assoc :components-v2 true)))))
(defn make-file (defn make-file
[{:keys [id project-id name revn is-shared features [{:keys [id project-id name revn is-shared features migrations
ignore-sync-until modified-at deleted-at ignore-sync-until modified-at deleted-at]
create-page page-id] :or {is-shared false revn 0}}
:or {is-shared false revn 0 create-page true}}]
& {:keys [create-page page-id]
:or {create-page true}}]
(let [id (or id (uuid/next)) (let [id (or id (uuid/next))
data (if create-page data (if create-page
(if page-id (if page-id
(make-file-data id page-id) (make-file-data id page-id)
(make-file-data id)) (make-file-data id))
(make-file-data id nil)) (make-file-data id nil))
file {:id id file (d/without-nils
:project-id project-id {:id id
:name name :project-id project-id
:revn revn :name name
:vern 0 :revn revn
:is-shared is-shared :vern 0
:version version :is-shared is-shared
:data data :version version
:features features :data data
:ignore-sync-until ignore-sync-until :features features
:modified-at modified-at :migrations migrations
:deleted-at deleted-at}] :ignore-sync-until ignore-sync-until
:modified-at modified-at
:deleted-at deleted-at})]
(d/without-nils file))) (check-file file)))
;; Helpers ;; Helpers

View file

@ -149,13 +149,16 @@
{:test {:init-fn frontend-tests.runner/init {:test {:init-fn frontend-tests.runner/init
:prepend-js ";if (typeof globalThis.navigator?.userAgent === 'undefined') { globalThis.navigator = {userAgent: ''}; };"}}} :prepend-js ";if (typeof globalThis.navigator?.userAgent === 'undefined') { globalThis.navigator = {userAgent: ''}; };"}}}
:lib-penpot :library
{:target :esm {:target :esm
:output-dir "resources/public/libs" :runtime :custom
:output-dir "target/library"
:devtools {:autoload false}
:modules :modules
{:penpot {:exports {:renderPage app.libs.render/render-page-export {:penpot
:createFile app.libs.file-builder/create-file-export}}} {:exports {BuilderError lib.file-builder/BuilderError
createFile lib.file-builder/create-file}}}
:compiler-options :compiler-options
{:output-feature-set :es2020 {:output-feature-set :es2020
@ -165,6 +168,8 @@
:release :release
{:compiler-options {:compiler-options
{:fn-invoke-direct true {:fn-invoke-direct true
:optimizations #shadow/env ["PENPOT_BUILD_OPTIMIZATIONS" :as :keyword :default :advanced]
:pretty-print false
:source-map true :source-map true
:elide-asserts true :elide-asserts true
:anon-fn-naming-policy :off :anon-fn-naming-policy :off

View file

@ -1,281 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.libs.file-builder
(:require
[app.common.data :as d]
[app.common.features :as cfeat]
[app.common.files.builder :as fb]
[app.common.media :as cm]
[app.common.types.components-list :as ctkl]
[app.common.uuid :as uuid]
[app.util.json :as json]
[app.util.webapi :as wapi]
[app.util.zip :as uz]
[app.worker.export :as e]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[promesa.core :as p]))
(defn parse-data [data]
(as-> data $
(js->clj $ :keywordize-keys true)
;; Transforms camelCase to kebab-case
(d/deep-mapm
(fn [[key value]]
(let [value (if (= (type value) js/Symbol)
(keyword (js/Symbol.keyFor value))
value)
key (-> key d/name str/kebab keyword)]
[key value])) $)))
(defn data-uri->blob
[data-uri]
(let [[mtype b64-data] (str/split data-uri ";base64,")
mtype (subs mtype (inc (str/index-of mtype ":")))
decoded (.atob js/window b64-data)
size (.-length ^js decoded)
content (js/Uint8Array. size)]
(doseq [i (range 0 size)]
(aset content i (.charCodeAt decoded i)))
(wapi/create-blob content mtype)))
(defn parse-library-media
[[file-id media]]
(rx/merge
(let [markup
(->> (vals media)
(reduce e/collect-media {})
(json/encode))]
(rx/of (vector (str file-id "/media.json") markup)))
(->> (rx/from (vals media))
(rx/map #(assoc % :file-id file-id))
(rx/merge-map
(fn [media]
(let [file-path (str/concat file-id "/media/" (:id media) (cm/mtype->extension (:mtype media)))
blob (data-uri->blob (:uri media))]
(rx/of (vector file-path blob))))))))
(defn export-file
[file]
(let [file (assoc file
:name (:name file)
:file-name (:name file)
:is-shared false)
files-stream (->> (rx/of {(:id file) file})
(rx/share))
manifest-stream
(->> files-stream
(rx/map #(e/create-manifest (uuid/next) (:id file) :all % cfeat/default-features))
(rx/map (fn [a]
(vector "manifest.json" a))))
render-stream
(->> files-stream
(rx/merge-map vals)
(rx/merge-map e/process-pages)
(rx/observe-on :async)
(rx/merge-map e/get-page-data)
(rx/share))
colors-stream
(->> files-stream
(rx/merge-map vals)
(rx/map #(vector (:id %) (get-in % [:data :colors])))
(rx/filter #(d/not-empty? (second %)))
(rx/map e/parse-library-color))
typographies-stream
(->> files-stream
(rx/merge-map vals)
(rx/map #(vector (:id %) (get-in % [:data :typographies])))
(rx/filter #(d/not-empty? (second %)))
(rx/map e/parse-library-typographies))
media-stream
(->> files-stream
(rx/merge-map vals)
(rx/map #(vector (:id %) (get-in % [:data :media])))
(rx/filter #(d/not-empty? (second %)))
(rx/merge-map parse-library-media))
components-stream
(->> files-stream
(rx/merge-map vals)
(rx/filter #(d/not-empty? (ctkl/components-seq (:data %))))
(rx/merge-map e/parse-library-components))
pages-stream
(->> render-stream
(rx/map e/collect-page))]
(rx/merge
(->> render-stream
(rx/map #(hash-map
:type :progress
:file (:id file)
:data (str "Render " (:file-name %) " - " (:name %)))))
(->> (rx/merge
manifest-stream
pages-stream
components-stream
media-stream
colors-stream
typographies-stream)
(rx/reduce conj [])
(rx/with-latest-from files-stream)
(rx/merge-map (fn [[data _]]
(->> (uz/compress-files data)
(rx/map #(vector file %)))))))))
(deftype File [^:mutable file]
Object
(addPage [_ name]
(set! file (fb/add-page file {:name name}))
(str (:current-page-id file)))
(addPage [_ name options]
(set! file (fb/add-page file {:name name :options (parse-data options)}))
(str (:current-page-id file)))
(closePage [_]
(set! file (fb/close-page file)))
(addArtboard [_ data]
(set! file (fb/add-artboard file (parse-data data)))
(str (:last-id file)))
(closeArtboard [_]
(set! file (fb/close-artboard file)))
(addGroup [_ data]
(set! file (fb/add-group file (parse-data data)))
(str (:last-id file)))
(closeGroup [_]
(set! file (fb/close-group file)))
(addBool [_ data]
(set! file (fb/add-bool file (parse-data data)))
(str (:last-id file)))
(closeBool [_]
(set! file (fb/close-bool file)))
(createRect [_ data]
(set! file (fb/create-rect file (parse-data data)))
(str (:last-id file)))
(createCircle [_ data]
(set! file (fb/create-circle file (parse-data data)))
(str (:last-id file)))
(createPath [_ data]
(set! file (fb/create-path file (parse-data data)))
(str (:last-id file)))
(createText [_ data]
(set! file (fb/create-text file (parse-data data)))
(str (:last-id file)))
(createImage [_ data]
(set! file (fb/create-image file (parse-data data)))
(str (:last-id file)))
(createSVG [_ data]
(set! file (fb/create-svg-raw file (parse-data data)))
(str (:last-id file)))
(closeSVG [_]
(set! file (fb/close-svg-raw file)))
(addLibraryColor [_ data]
(set! file (fb/add-library-color file (parse-data data)))
(str (:last-id file)))
(updateLibraryColor [_ data]
(set! file (fb/update-library-color file (parse-data data)))
(str (:last-id file)))
(deleteLibraryColor [_ data]
(set! file (fb/delete-library-color file (parse-data data)))
(str (:last-id file)))
(addLibraryMedia [_ data]
(set! file (fb/add-library-media file (parse-data data)))
(str (:last-id file)))
(deleteLibraryMedia [_ data]
(set! file (fb/delete-library-media file (parse-data data)))
(str (:last-id file)))
(addLibraryTypography [_ data]
(set! file (fb/add-library-typography file (parse-data data)))
(str (:last-id file)))
(deleteLibraryTypography [_ data]
(set! file (fb/delete-library-typography file (parse-data data)))
(str (:last-id file)))
(startComponent [_ data]
(set! file (fb/start-component file (parse-data data)))
(str (:current-component-id file)))
(finishComponent [_]
(set! file (fb/finish-component file)))
(createComponentInstance [_ data]
(set! file (fb/create-component-instance file (parse-data data)))
(str (:last-id file)))
(lookupShape [_ shape-id]
(clj->js (fb/lookup-shape file (uuid/parse shape-id))))
(updateObject [_ id new-obj]
(let [old-obj (fb/lookup-shape file (uuid/parse id))
new-obj (d/deep-merge old-obj (parse-data new-obj))]
(set! file (fb/update-object file old-obj new-obj))))
(deleteObject [_ id]
(set! file (fb/delete-object file (uuid/parse id))))
(getId [_]
(:id file))
(getCurrentPageId [_]
(:current-page-id file))
(asMap [_]
(clj->js file))
(newId [_]
(uuid/next))
(export [_]
(p/create
(fn [resolve reject]
(->> (export-file file)
(rx/filter #(not= (:type %) :progress))
(rx/take 1)
(rx/subs!
(fn [value]
(let [[_ export-blob] value]
(resolve export-blob)))
reject))))))
(defn create-file-export [^string name]
(binding [cfeat/*current* cfeat/default-features]
(File. (fb/create-file name))))
(defn exports []
#js {:createFile create-file-export})

View file

@ -1,28 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.libs.render
(:require
[app.common.uuid :as uuid]
[app.main.render :as r]
[beicon.v2.core :as rx]
[promesa.core :as p]))
(defn render-page-export
[file ^string page-id]
;; Better to expose the api as a promise to be consumed from JS
(let [page-id (uuid/parse page-id)
file-data (.-file file)
data (get-in file-data [:data :pages-index page-id])]
(p/create
(fn [resolve reject]
(->> (r/render-page data)
(rx/take 1)
(rx/subs! resolve reject))))))
(defn exports []
#js {:renderPage render-page-export})

View file

@ -771,12 +771,12 @@
;; --- Update Shape Attrs ;; --- Update Shape Attrs
;; FIXME: revisit this before merge ;; FIXME: rename to update-shape-generic-attrs because on the end we
;; only allow here to update generic attrs
(defn update-shape (defn update-shape
[id attrs] [id attrs]
(assert (uuid? id) "expected valid uuid for `id`") (assert (uuid? id) "expected valid uuid for `id`")
(let [attrs (cts/check-shape-generic-attrs attrs)]
(let [attrs (cts/check-shape-attrs attrs)]
(ptk/reify ::update-shape (ptk/reify ::update-shape
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]

View file

@ -12,7 +12,6 @@
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
[app.common.types.component :as ctc] [app.common.types.component :as ctc]
[app.common.types.container :as ctn] [app.common.types.container :as ctn]
[app.common.types.path :as path]
[app.common.types.path.bool :as bool] [app.common.types.path.bool :as bool]
[app.common.types.shape :as cts] [app.common.types.shape :as cts]
[app.common.types.shape.layout :as ctl] [app.common.types.shape.layout :as ctl]
@ -30,9 +29,6 @@
(let [shape-id (let [shape-id
(or id (uuid/next)) (or id (uuid/next))
shapes
(mapv #(path/convert-to-path % objects) shapes)
head head
(if (= type :difference) (first shapes) (last shapes)) (if (= type :difference) (first shapes) (last shapes))
@ -48,13 +44,13 @@
:frame-id (:frame-id head) :frame-id (:frame-id head)
:parent-id (:parent-id head) :parent-id (:parent-id head)
:name name :name name
:shapes (mapv :id shapes)} :shapes (vec shapes)}
shape shape
(-> shape (-> shape
(merge (select-keys head bool/style-properties)) (merge (select-keys head bool/style-properties))
(cts/setup-shape) (cts/setup-shape)
(gsh/update-bool shapes objects))] (gsh/update-bool objects))]
[shape (cph/get-position-on-parent objects (:id head))])) [shape (cph/get-position-on-parent objects (:id head))]))
@ -108,19 +104,16 @@
(defn group->bool (defn group->bool
[type group objects] [type group objects]
(let [shapes (->> (:shapes group) (let [shapes (->> (:shapes group)
(map #(get objects %)) (map (d/getf objects)))
(mapv #(path/convert-to-path % objects)))
head (if (= type :difference) (first shapes) (last shapes)) head (if (= type :difference) (first shapes) (last shapes))
head (cond-> head head (cond-> head
(and (contains? head :svg-attrs) (empty? (:fills head))) (and (contains? head :svg-attrs) (empty? (:fills head)))
(assoc :fills bool/default-fills)) (assoc :fills bool/default-fills))]
head-data (select-keys head bool/style-properties)]
(-> group (-> group
(assoc :type :bool) (assoc :type :bool)
(assoc :bool-type type) (assoc :bool-type type)
(merge head-data) (merge (select-keys head bool/style-properties))
(gsh/update-bool shapes objects)))) (gsh/update-bool objects))))
(defn group-to-bool (defn group-to-bool
[shape-id type] [shape-id type]

View file

@ -6,7 +6,7 @@
(ns app.util.object (ns app.util.object
"A collection of helpers for work with javascript objects." "A collection of helpers for work with javascript objects."
(:refer-clojure :exclude [set! new get merge clone contains? array? into-array reify]) (:refer-clojure :exclude [set! new get merge clone contains? array? into-array reify class])
#?(:cljs (:require-macros [app.util.object])) #?(:cljs (:require-macros [app.util.object]))
(:require (:require
[clojure.core :as c])) [clojure.core :as c]))

View file

@ -0,0 +1,250 @@
;; 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 lib.file-builder
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.builder :as fb]
[app.common.json :as json]
[app.common.schema :as sm]
[app.common.uuid :as uuid]
[app.util.object :as obj]))
(def BuilderError
(obj/class
:name "BuilderError"
:extends js/Error
:constructor
(fn [this type code hint cause]
(.call js/Error this hint)
(set! (.-name this) (str "Exception: " hint))
(set! (.-type this) type)
(set! (.-code this) code)
(set! (.-hint this) hint)
(when (exists? js/Error.captureStackTrace)
(.captureStackTrace js/Error this))
(obj/add-properties!
this
{:name "cause"
:enumerable true
:this false
:get (fn [] cause)}
{:name "data"
:enumerable true
:this false
:get (fn []
(let [data (ex-data cause)]
(when-let [explain (::sm/explain data)]
(json/->js (sm/simplify explain)))))}))))
(defn- handle-exception
[cause]
(let [data (ex-data cause)]
(throw (new BuilderError
(d/name (get data :type :unknown))
(d/name (get data :code :unknown))
(or (get data :hint) (ex-message cause))
cause))))
(defn- decode-params
[params]
(if (obj/plain-object? params)
(json/->js params)
params))
(defn- create-file*
[file]
(let [state* (volatile! file)]
(obj/reify {:name "File"}
:id
{:get #(dm/str (:id @state*))}
:currentFrameId
{:get #(dm/str (::fb/current-frame-id @state*))}
:currentPageId
{:get #(dm/str (::fb/current-page-id @state*))}
:lastId
{:get #(dm/str (::fb/last-id @state*))}
:addPage
(fn [params]
(try
(let [params (-> params
(decode-params)
(fb/decode-page))]
(vswap! state* fb/add-page params)
(dm/str (::fb/current-page-id @state*)))
(catch :default cause
(handle-exception cause))))
:closePage
(fn []
(vswap! state* fb/close-page))
:addArtboard
(fn [params]
(try
(let [params (-> params
(json/->clj)
(assoc :type :frame)
(fb/decode-shape))]
(vswap! state* fb/add-artboard params)
(dm/str (::fb/last-id @state*)))
(catch :default cause
(handle-exception cause))))
:closeArtboard
(fn []
(vswap! state* fb/close-artboard))
:addGroup
(fn [params]
(try
(let [params (-> params
(json/->clj)
(assoc :type :group)
(fb/decode-shape))]
(vswap! state* fb/add-group params)
(dm/str (::fb/last-id @state*)))
(catch :default cause
(handle-exception cause))))
:closeGroup
(fn []
(vswap! state* fb/close-group))
:addBool
(fn [params]
(try
(let [params (-> params
(json/->clj)
(fb/decode-add-bool))]
(vswap! state* fb/add-bool params)
(dm/str (::fb/last-id @state*)))
(catch :default cause
(handle-exception cause))))
:addRect
(fn [params]
(try
(let [params (-> params
(json/->clj)
(assoc :type :rect)
(fb/decode-shape))]
(vswap! state* fb/add-shape params)
(dm/str (::fb/last-id @state*)))
(catch :default cause
(handle-exception cause))))
:addCircle
(fn [params]
(try
(let [params (-> params
(json/->clj)
(assoc :type :circle)
(fb/decode-shape))]
(vswap! state* fb/add-shape params)
(dm/str (::fb/last-id @state*)))
(catch :default cause
(handle-exception cause))))
:addPath
(fn [params]
(try
(let [params (-> params
(json/->clj)
(assoc :type :path)
(fb/decode-shape))]
(vswap! state* fb/add-shape params)
(dm/str (::fb/last-id @state*)))
(catch :default cause
(handle-exception cause))))
:addText
(fn [params]
(try
(let [params (-> params
(json/->clj)
(assoc :type :text)
(fb/decode-shape))]
(vswap! state* fb/add-shape params)
(dm/str (::fb/last-id @state*)))
(catch :default cause
(handle-exception cause))))
:addLibraryColor
(fn [params]
(try
(let [params (-> params
(json/->clj)
(fb/decode-library-color)
(d/without-nils))]
(vswap! state* fb/add-library-color params)
(dm/str (::fb/last-id @state*)))
(catch :default cause
(handle-exception cause))))
:addLibraryTypography
(fn [params]
(try
(let [params (-> params
(json/->clj)
(fb/decode-library-typography)
(d/without-nils))]
(vswap! state* fb/add-library-typography params)
(dm/str (::fb/last-id @state*)))
(catch :default cause
(handle-exception cause))))
:addComponent
(fn [params]
(try
(let [params (-> params
(json/->clj)
(fb/decode-component)
(d/without-nils))]
(vswap! state* fb/add-component params)
(dm/str (::fb/last-id @state*)))
(catch :default cause
(handle-exception cause))))
:addComponentInstance
(fn [params]
(try
(let [params (-> params
(json/->clj)
(fb/decode-add-component-instance)
(d/without-nils))]
(vswap! state* fb/add-component-instance params)
(dm/str (::fb/last-id @state*)))
(catch :default cause
(handle-exception cause))))
:getShape
(fn [shape-id]
(let [shape-id (uuid/parse shape-id)]
(some-> (fb/lookup-shape @state* shape-id)
(json/->js))))
:toMap
(fn []
(-> @state*
(d/without-qualified)
(json/->js))))))
(defn create-file
[params]
(try
(let [params (-> params json/->clj fb/decode-file)
file (fb/create-file params)]
(create-file* file))
(catch :default cause
(handle-exception cause))))

View file

@ -0,0 +1,30 @@
import * as penpot from "../../../target/library/penpot.js";
console.log(penpot);
try {
const file = penpot.createFile({name: "Test"});
file.addPage({name: "Foo Page"})
const boardId = file.addArtboard({name: "Foo Board"})
const rectId = file.addRect({name: "Foo Rect", width:100, height: 200})
file.addLibraryColor({color: "#fabada", opacity: 0.5})
console.log("created board", boardId);
console.log("created rect", rectId);
const board = file.getShape(boardId);
console.log("=========== BOARD =============")
console.dir(board, {depth: 10});
const rect = file.getShape(rectId);
console.log("=========== RECT =============")
console.dir(rect, {depth: 10});
// console.dir(file.toMap(), {depth:10});
} catch (e) {
console.log(e);
// console.log(e.data);
}
process.exit(0);

View file

@ -16,8 +16,7 @@
(t/deftest test-common-shape-properties (t/deftest test-common-shape-properties
(let [;; ==== Setup (let [;; ==== Setup
store (ths/setup-store store (ths/setup-store (cthf/sample-file :file1 :page-label :page1))
(cthf/sample-file :file1 :page-label :page1))
^js context (api/create-context "TEST") ^js context (api/create-context "TEST")

View file

@ -13,13 +13,21 @@
[cljs.pprint :refer [pprint]] [cljs.pprint :refer [pprint]]
[cljs.test :as t :include-macros true])) [cljs.test :as t :include-macros true]))
(def uuid-counter 1)
(defn get-mocked-uuid
[]
(let [counter (atom 0)]
(fn []
(uuid/custom 123456789 (swap! counter inc)))))
(t/deftest test-create-index (t/deftest test-create-index
(t/testing "Create empty data" (t/testing "Create empty data"
(let [data (sd/make-snap-data)] (let [data (sd/make-snap-data)]
(t/is (some? data)))) (t/is (some? data))))
(t/testing "Add empty page (only root-frame)" (t/testing "Add empty page (only root-frame)"
(let [page (-> (fb/create-file "Test") (let [page (-> (fb/create-file {:name "Test"})
(fb/add-page {:name "Page 1"}) (fb/add-page {:name "Page 1"})
(fb/get-current-page)) (fb/get-current-page))
@ -28,10 +36,11 @@
(t/is (some? data)))) (t/is (some? data))))
(t/testing "Create simple shape on root" (t/testing "Create simple shape on root"
(let [file (-> (fb/create-file "Test") (let [file (-> (fb/create-file {:name "Test"})
(fb/add-page {:name "Page 1"}) (fb/add-page {:name "Page 1"})
(fb/create-rect (fb/add-shape
{:x 0 {:type :rect
:x 0
:y 0 :y 0
:width 100 :width 100
:height 100})) :height 100}))
@ -57,7 +66,7 @@
(t/is (= (first (nth result-x 2)) 100)))) (t/is (= (first (nth result-x 2)) 100))))
(t/testing "Add page with single empty frame" (t/testing "Add page with single empty frame"
(let [file (-> (fb/create-file "Test") (let [file (-> (fb/create-file {:name "Test"})
(fb/add-page {:name "Page 1"}) (fb/add-page {:name "Page 1"})
(fb/add-artboard (fb/add-artboard
{:x 0 {:x 0
@ -66,10 +75,10 @@
:height 100}) :height 100})
(fb/close-artboard)) (fb/close-artboard))
frame-id (:last-id file) frame-id (::fb/last-id file)
page (fb/get-current-page file) page (fb/get-current-page file)
;; frame-id (:last-id file) ;; frame-id (::fb/last-id file)
data (-> (sd/make-snap-data) data (-> (sd/make-snap-data)
(sd/add-page page)) (sd/add-page page))
@ -81,47 +90,49 @@
(t/is (= (count result-frame-x) 3)))) (t/is (= (count result-frame-x) 3))))
(t/testing "Add page with some shapes inside frames" (t/testing "Add page with some shapes inside frames"
(let [file (-> (fb/create-file "Test") (with-redefs [uuid/next (get-mocked-uuid)]
(fb/add-page {:name "Page 1"}) (let [file (-> (fb/create-file {:name "Test"})
(fb/add-artboard (fb/add-page {:name "Page 1"})
{:x 0 (fb/add-artboard
:y 0 {:x 0
:width 100 :y 0
:height 100})) :width 100
frame-id (:last-id file) :height 100}))
file (-> file frame-id (::fb/last-id file)
(fb/create-rect
{:x 25
:y 25
:width 50
:height 50})
(fb/close-artboard))
page (fb/get-current-page file) file (-> file
(fb/add-shape
{:type :rect
:x 25
:y 25
:width 50
:height 50})
(fb/close-artboard))
;; frame-id (:last-id file) page (fb/get-current-page file)
data (-> (sd/make-snap-data)
(sd/add-page page))
result-zero-x (sd/query data (:id page) uuid/zero :x [0 100]) data (-> (sd/make-snap-data)
result-frame-x (sd/query data (:id page) frame-id :x [0 100])] (sd/add-page page))
(t/is (some? data)) result-zero-x (sd/query data (:id page) uuid/zero :x [0 100])
(t/is (= (count result-zero-x) 3)) result-frame-x (sd/query data (:id page) frame-id :x [0 100])]
(t/is (= (count result-frame-x) 5))))
(t/is (some? data))
(t/is (= (count result-zero-x) 3))
(t/is (= (count result-frame-x) 5)))))
(t/testing "Add a global guide" (t/testing "Add a global guide"
(let [file (-> (fb/create-file "Test") (let [file (-> (fb/create-file {:name "Test"})
(fb/add-page {:name "Page 1"}) (fb/add-page {:name "Page 1"})
(fb/add-guide {:position 50 :axis :x}) (fb/add-guide {:position 50 :axis :x})
(fb/add-artboard {:x 200 :y 200 :width 100 :height 100}) (fb/add-artboard {:x 200 :y 200 :width 100 :height 100})
(fb/close-artboard)) (fb/close-artboard))
frame-id (:last-id file) frame-id (::fb/last-id file)
page (fb/get-current-page file) page (fb/get-current-page file)
;; frame-id (:last-id file) ;; frame-id (::fb/last-id file)
data (-> (sd/make-snap-data) data (-> (sd/make-snap-data)
(sd/add-page page)) (sd/add-page page))
@ -140,26 +151,26 @@
(t/is (= (count result-frame-y) 0)))) (t/is (= (count result-frame-y) 0))))
(t/testing "Add a frame guide" (t/testing "Add a frame guide"
(let [file (-> (fb/create-file "Test") (let [file (-> (fb/create-file {:name "Test"})
(fb/add-page {:name "Page 1"}) (fb/add-page {:name "Page 1"})
(fb/add-artboard {:x 200 :y 200 :width 100 :height 100}) (fb/add-artboard {:x 200 :y 200 :width 100 :height 100})
(fb/close-artboard)) (fb/close-artboard))
frame-id (:last-id file) frame-id (::fb/last-id file)
file (-> file file (-> file
(fb/add-guide {:position 50 :axis :x :frame-id frame-id})) (fb/add-guide {:position 50 :axis :x :frame-id frame-id}))
page (fb/get-current-page file) page (fb/get-current-page file)
;; frame-id (:last-id file) data (-> (sd/make-snap-data)
data (-> (sd/make-snap-data) (sd/add-page page))
(sd/add-page page))
result-zero-x (sd/query data (:id page) uuid/zero :x [0 100]) result-zero-x (sd/query data (:id page) uuid/zero :x [0 100])
result-zero-y (sd/query data (:id page) uuid/zero :y [0 100]) result-zero-y (sd/query data (:id page) uuid/zero :y [0 100])
result-frame-x (sd/query data (:id page) frame-id :x [0 100]) result-frame-x (sd/query data (:id page) frame-id :x [0 100])
result-frame-y (sd/query data (:id page) frame-id :y [0 100])] result-frame-y (sd/query data (:id page) frame-id :y [0 100])]
(t/is (some? data)) (t/is (some? data))
;; We can snap in the root ;; We can snap in the root
(t/is (= (count result-zero-x) 0)) (t/is (= (count result-zero-x) 0))
@ -171,7 +182,7 @@
(t/deftest test-update-index (t/deftest test-update-index
(t/testing "Create frame on root and then remove it." (t/testing "Create frame on root and then remove it."
(let [file (-> (fb/create-file "Test") (let [file (-> (fb/create-file {:name "Test"})
(fb/add-page {:name "Page 1"}) (fb/add-page {:name "Page 1"})
(fb/add-artboard (fb/add-artboard
{:x 0 {:x 0
@ -180,15 +191,15 @@
:height 100}) :height 100})
(fb/close-artboard)) (fb/close-artboard))
shape-id (:last-id file) shape-id (::fb/last-id file)
page (fb/get-current-page file) page (fb/get-current-page file)
;; frame-id (:last-id file) ;; frame-id (::fb/last-id file)
data (-> (sd/make-snap-data) data (-> (sd/make-snap-data)
(sd/add-page page)) (sd/add-page page))
file (-> file file (-> file
(fb/delete-object shape-id)) (fb/delete-shape shape-id))
new-page (fb/get-current-page file) new-page (fb/get-current-page file)
data (sd/update-page data page new-page) data (sd/update-page data page new-page)
@ -201,22 +212,23 @@
(t/is (= (count result-y) 0)))) (t/is (= (count result-y) 0))))
(t/testing "Create simple shape on root. Then remove it" (t/testing "Create simple shape on root. Then remove it"
(let [file (-> (fb/create-file "Test") (let [file (-> (fb/create-file {:name "Test"})
(fb/add-page {:name "Page 1"}) (fb/add-page {:name "Page 1"})
(fb/create-rect (fb/add-shape
{:x 0 {:type :rect
:x 0
:y 0 :y 0
:width 100 :width 100
:height 100})) :height 100}))
shape-id (:last-id file) shape-id (::fb/last-id file)
page (fb/get-current-page file) page (fb/get-current-page file)
;; frame-id (:last-id file) ;; frame-id (::fb/last-id file)
data (-> (sd/make-snap-data) data (-> (sd/make-snap-data)
(sd/add-page page)) (sd/add-page page))
file (fb/delete-object file shape-id) file (fb/delete-shape file shape-id)
new-page (fb/get-current-page file) new-page (fb/get-current-page file)
data (sd/update-page data page new-page) data (sd/update-page data page new-page)
@ -229,17 +241,17 @@
(t/is (= (count result-y) 0)))) (t/is (= (count result-y) 0))))
(t/testing "Create shape inside frame, then remove it" (t/testing "Create shape inside frame, then remove it"
(let [file (-> (fb/create-file "Test") (let [file (-> (fb/create-file {:name "Test"})
(fb/add-page {:name "Page 1"}) (fb/add-page {:name "Page 1"})
(fb/add-artboard (fb/add-artboard
{:x 0 {:x 0
:y 0 :y 0
:width 100 :width 100
:height 100})) :height 100}))
frame-id (:last-id file) frame-id (::fb/last-id file)
file (fb/create-rect file {:x 25 :y 25 :width 50 :height 50}) file (fb/add-shape file {:type :rect :x 25 :y 25 :width 50 :height 50})
shape-id (:last-id file) shape-id (::fb/last-id file)
file (fb/close-artboard file) file (fb/close-artboard file)
@ -247,7 +259,7 @@
data (-> (sd/make-snap-data) data (-> (sd/make-snap-data)
(sd/add-page page)) (sd/add-page page))
file (fb/delete-object file shape-id) file (fb/delete-shape file shape-id)
new-page (fb/get-current-page file) new-page (fb/get-current-page file)
data (sd/update-page data page new-page) data (sd/update-page data page new-page)
@ -260,16 +272,16 @@
(t/is (= (count result-frame-x) 3)))) (t/is (= (count result-frame-x) 3))))
(t/testing "Create global guide then remove it" (t/testing "Create global guide then remove it"
(let [file (-> (fb/create-file "Test") (let [file (-> (fb/create-file {:name "Test"})
(fb/add-page {:name "Page 1"}) (fb/add-page {:name "Page 1"})
(fb/add-guide {:position 50 :axis :x})) (fb/add-guide {:position 50 :axis :x}))
guide-id (:last-id file) guide-id (::fb/last-id file)
file (-> (fb/add-artboard file {:x 200 :y 200 :width 100 :height 100}) file (-> (fb/add-artboard file {:x 200 :y 200 :width 100 :height 100})
(fb/close-artboard)) (fb/close-artboard))
frame-id (:last-id file) frame-id (::fb/last-id file)
page (fb/get-current-page file) page (fb/get-current-page file)
data (-> (sd/make-snap-data) (sd/add-page page)) data (-> (sd/make-snap-data) (sd/add-page page))
@ -293,14 +305,14 @@
(t/is (= (count result-frame-y) 0)))) (t/is (= (count result-frame-y) 0))))
(t/testing "Create frame guide then remove it" (t/testing "Create frame guide then remove it"
(let [file (-> (fb/create-file "Test") (let [file (-> (fb/create-file {:name "Test"})
(fb/add-page {:name "Page 1"}) (fb/add-page {:name "Page 1"})
(fb/add-artboard {:x 200 :y 200 :width 100 :height 100}) (fb/add-artboard {:x 200 :y 200 :width 100 :height 100})
(fb/close-artboard)) (fb/close-artboard))
frame-id (:last-id file) frame-id (::fb/last-id file)
file (fb/add-guide file {:position 50 :axis :x :frame-id frame-id}) file (fb/add-guide file {:position 50 :axis :x :frame-id frame-id})
guide-id (:last-id file) guide-id (::fb/last-id file)
page (fb/get-current-page file) page (fb/get-current-page file)
data (-> (sd/make-snap-data) (sd/add-page page)) data (-> (sd/make-snap-data) (sd/add-page page))
@ -324,7 +336,7 @@
(t/is (= (count result-frame-y) 0)))) (t/is (= (count result-frame-y) 0))))
(t/testing "Update frame coordinates" (t/testing "Update frame coordinates"
(let [file (-> (fb/create-file "Test") (let [file (-> (fb/create-file {:name "Test"})
(fb/add-page {:name "Page 1"}) (fb/add-page {:name "Page 1"})
(fb/add-artboard (fb/add-artboard
{:x 0 {:x 0
@ -333,17 +345,18 @@
:height 100}) :height 100})
(fb/close-artboard)) (fb/close-artboard))
frame-id (:last-id file) frame-id (::fb/last-id file)
page (fb/get-current-page file) page (fb/get-current-page file)
data (-> (sd/make-snap-data) (sd/add-page page)) data (-> (sd/make-snap-data) (sd/add-page page))
frame (fb/lookup-shape file frame-id) file (fb/update-shape file frame-id
new-frame (-> frame (fn [shape]
(dissoc :selrect :points) (-> shape
(assoc :x 200 :y 200) (dissoc :selrect :points)
(cts/setup-shape)) (assoc :x 200 :y 200)
(cts/setup-shape))))
file (fb/update-object file frame new-frame)
new-page (fb/get-current-page file) new-page (fb/get-current-page file)
data (sd/update-page data page new-page) data (sd/update-page data page new-page)
@ -360,27 +373,30 @@
(t/is (= (count result-frame-x-2) 3)))) (t/is (= (count result-frame-x-2) 3))))
(t/testing "Update shape coordinates" (t/testing "Update shape coordinates"
(let [file (-> (fb/create-file "Test") (let [file (-> (fb/create-file {:name "Test"})
(fb/add-page {:name "Page 1"}) (fb/add-page {:name "Page 1"})
(fb/create-rect (fb/add-shape
{:x 0 {:type :rect
:x 0
:y 0 :y 0
:width 100 :width 100
:height 100})) :height 100}))
shape-id (:last-id file) shape-id (::fb/last-id file)
page (fb/get-current-page file) page (fb/get-current-page file)
data (-> (sd/make-snap-data) (sd/add-page page)) data (-> (sd/make-snap-data)
(sd/add-page page))
shape (fb/lookup-shape file shape-id) file (fb/update-shape file shape-id
new-shape (-> shape (fn [shape]
(dissoc :selrect :points) (-> shape
(assoc :x 200 :y 200)) (dissoc :selrect :points)
(assoc :x 200 :y 200)
(cts/setup-shape))))
file (fb/update-object file shape new-shape)
new-page (fb/get-current-page file) new-page (fb/get-current-page file)
;; FIXME: update
data (sd/update-page data page new-page) data (sd/update-page data page new-page)
result-zero-x-1 (sd/query data (:id page) uuid/zero :x [0 100]) result-zero-x-1 (sd/query data (:id page) uuid/zero :x [0 100])
result-zero-x-2 (sd/query data (:id page) uuid/zero :x [200 300])] result-zero-x-2 (sd/query data (:id page) uuid/zero :x [200 300])]
@ -391,17 +407,17 @@
(t/testing "Update global guide" (t/testing "Update global guide"
(let [guide {:position 50 :axis :x} (let [guide {:position 50 :axis :x}
file (-> (fb/create-file "Test") file (-> (fb/create-file {:name "Test"})
(fb/add-page {:name "Page 1"}) (fb/add-page {:name "Page 1"})
(fb/add-guide guide)) (fb/add-guide guide))
guide-id (:last-id file) guide-id (::fb/last-id file)
guide (assoc guide :id guide-id) guide (assoc guide :id guide-id)
file (-> (fb/add-artboard file {:x 500 :y 500 :width 100 :height 100}) file (-> (fb/add-artboard file {:x 500 :y 500 :width 100 :height 100})
(fb/close-artboard)) (fb/close-artboard))
frame-id (:last-id file) frame-id (::fb/last-id file)
page (fb/get-current-page file) page (fb/get-current-page file)
data (-> (sd/make-snap-data) (sd/add-page page)) data (-> (sd/make-snap-data) (sd/add-page page))