mirror of
https://github.com/penpot/penpot.git
synced 2025-05-28 15:46:11 +02:00
🎉 Add .penpot (binfile-v3) support for library
This commit is contained in:
parent
1fea1e8f5b
commit
29d23577d2
20 changed files with 926 additions and 751 deletions
80
CHANGES.md
80
CHANGES.md
|
@ -8,33 +8,65 @@
|
|||
|
||||
**Breaking changes on penpot library:**
|
||||
|
||||
The library entrypoint API object has been changed. From now you start creating a new
|
||||
build context, from where you can add multiple files and attach media. This change add the
|
||||
ability to build more than one file at same time and export them in an unique .penpot
|
||||
file.
|
||||
|
||||
```js
|
||||
const context = penpot.createBuildContext()
|
||||
|
||||
context.addFile({name:"aa"})
|
||||
context.addPage({name:"aa"})
|
||||
context.closePage()
|
||||
context.closeFile()
|
||||
|
||||
;; barray is instance of Uint8Array
|
||||
const barray = penpot.exportAsBytes(context);
|
||||
```
|
||||
|
||||
The previous `file.export()` method has been removed and several alternatives are
|
||||
added as first level functions on penpot library API entrypoint:
|
||||
|
||||
- `exportAsBytes(BuildContext context) -> Promise<Uint8Array>`
|
||||
- `exportAsBlob(BuildContext context) -> Promise<Blob>`
|
||||
- `exportStream(BuildContext context, WritableStream stream) -> Promise<Void>`
|
||||
|
||||
The stream variant allows writting data as it is generated to the stream, without the need
|
||||
to store the generated output entirelly in the memory.
|
||||
|
||||
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.
|
||||
|
||||
A non exhaustive list of changes:
|
||||
|
||||
- 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
|
||||
- Rename the `createRect` method to `addRect`
|
||||
- Rename the `createCircle` method to `addCircle`
|
||||
- Rename the `createPath` method to `addPath`
|
||||
- Rename the `createText` method to `addText`
|
||||
- Rename the `addArtboard` method to `addBoard`
|
||||
- Rename `startComponent` to `addComponent` (to preserve the naming style)
|
||||
- Rename `createComponentInstance` to `addComponentInstance` (to preserve the naming style)
|
||||
- Remove `lookupShape`
|
||||
- Remove `asMap`
|
||||
- Remove `updateLibraryColor` (use `addLibraryColor` if you just need to replace a color)
|
||||
- Remove `deleteLibraryColor` (this library is intended to build files)
|
||||
- Remove `updateLibraryTypography` (use `addLibraryTypography` if you just need to replace a typography)
|
||||
- Remove `deleteLibraryTypography` (this library is intended to build files)
|
||||
- Remove `add/update/deleteLibraryMedia` (they are no longer supported by Penpot and have been replaced by components)
|
||||
- Remove `deleteObject` (this library is intended to build files)
|
||||
- Remove `updateObject` (this library is intended to build files)
|
||||
- Remove `finishComponent` (it is no longer necessary; see below for more details on component creation changes)
|
||||
|
||||
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.
|
||||
- Change the `getCurrentPageId` function to a read-only `currentPageId` property
|
||||
- Add `currentFileId` read-only property
|
||||
- Add `currentFrameId` read-only property
|
||||
- Add `lastId` read-only property
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
[app.common.files.migrations :as-alias fmg]
|
||||
[app.common.json :as json]
|
||||
[app.common.logging :as l]
|
||||
[app.common.media :as cmedia]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.thumbnails :as cth]
|
||||
[app.common.types.color :as ctcl]
|
||||
|
@ -73,7 +74,7 @@
|
|||
[:size ::sm/int]
|
||||
[:content-type :string]
|
||||
[:bucket [::sm/one-of {:format :string} sto/valid-buckets]]
|
||||
[:hash :string]])
|
||||
[:hash {:optional true} :string]])
|
||||
|
||||
(def ^:private schema:file-thumbnail
|
||||
[:map {:title "FileThumbnail"}
|
||||
|
@ -88,13 +89,19 @@
|
|||
ctf/schema:file
|
||||
[:map [:options {:optional true} ctf/schema:options]]])
|
||||
|
||||
;; --- HELPERS
|
||||
|
||||
(defn- default-now
|
||||
[o]
|
||||
(or o (dt/now)))
|
||||
|
||||
;; --- ENCODERS
|
||||
|
||||
(def encode-file
|
||||
(sm/encoder schema:file sm/json-transformer))
|
||||
|
||||
(def encode-page
|
||||
(sm/encoder ::ctp/page sm/json-transformer))
|
||||
(sm/encoder ctp/schema:page sm/json-transformer))
|
||||
|
||||
(def encode-shape
|
||||
(sm/encoder ::cts/shape sm/json-transformer))
|
||||
|
@ -129,7 +136,7 @@
|
|||
(sm/decoder schema:manifest sm/json-transformer))
|
||||
|
||||
(def decode-media
|
||||
(sm/decoder ::ctf/media sm/json-transformer))
|
||||
(sm/decoder ctf/schema:media sm/json-transformer))
|
||||
|
||||
(def decode-component
|
||||
(sm/decoder ::ctc/component sm/json-transformer))
|
||||
|
@ -229,27 +236,13 @@
|
|||
:always
|
||||
(bfc/clean-file-features))))))
|
||||
|
||||
(defn- resolve-extension
|
||||
[mtype]
|
||||
(case mtype
|
||||
"image/png" ".png"
|
||||
"image/jpeg" ".jpg"
|
||||
"image/gif" ".gif"
|
||||
"image/svg+xml" ".svg"
|
||||
"image/webp" ".webp"
|
||||
"font/woff" ".woff"
|
||||
"font/woff2" ".woff2"
|
||||
"font/ttf" ".ttf"
|
||||
"font/otf" ".otf"
|
||||
"application/octet-stream" ".bin"))
|
||||
|
||||
(defn- export-storage-objects
|
||||
[{:keys [::output] :as cfg}]
|
||||
(let [storage (sto/resolve cfg)]
|
||||
(doseq [id (-> bfc/*state* deref :storage-objects not-empty)]
|
||||
(let [sobject (sto/get-object storage id)
|
||||
smeta (meta sobject)
|
||||
ext (resolve-extension (:content-type smeta))
|
||||
ext (cmedia/mtype->extension (:content-type smeta))
|
||||
path (str "objects/" id ".json")
|
||||
params (-> (meta sobject)
|
||||
(assoc :id (:id sobject))
|
||||
|
@ -574,7 +567,14 @@
|
|||
(let [object (->> (read-entry input entry)
|
||||
(decode-media)
|
||||
(validate-media))
|
||||
object (assoc object :file-id file-id)]
|
||||
object (-> object
|
||||
(assoc :file-id file-id)
|
||||
(update :created-at default-now)
|
||||
(update :modified-at default-now)
|
||||
;; FIXME: this is set default to true for
|
||||
;; setting a value, this prop is no longer
|
||||
;; relevant;
|
||||
(assoc :is-local true))]
|
||||
(if (= id (:id object))
|
||||
(conj result object)
|
||||
result)))
|
||||
|
@ -800,7 +800,7 @@
|
|||
:expected-id (str id)
|
||||
:found-id (str (:id object))))
|
||||
|
||||
(let [ext (resolve-extension (:content-type object))
|
||||
(let [ext (cmedia/mtype->extension (:content-type object))
|
||||
path (str "objects/" id ext)
|
||||
content (->> path
|
||||
(get-zip-entry input)
|
||||
|
@ -814,13 +814,14 @@
|
|||
:expected-size (:size object)
|
||||
:found-size (sto/get-size content)))
|
||||
|
||||
(when (not= (:hash object) (sto/get-hash content))
|
||||
(when-let [hash (get object :hash)]
|
||||
(when (not= hash (sto/get-hash content))
|
||||
(ex/raise :type :validation
|
||||
:code :inconsistent-penpot-file
|
||||
:hint "found corrupted storage object: hash does not match"
|
||||
:path path
|
||||
:expected-hash (:hash object)
|
||||
:found-hash (sto/get-hash content)))
|
||||
:found-hash (sto/get-hash content))))
|
||||
|
||||
(let [params (-> object
|
||||
(dissoc :id :size)
|
||||
|
|
|
@ -1712,6 +1712,7 @@
|
|||
[{:fill-image
|
||||
{:id (:id fmedia)
|
||||
:name "test"
|
||||
:mtype "image/jpeg"
|
||||
:width 200
|
||||
:height 200}}]]
|
||||
|
||||
|
|
|
@ -13,17 +13,12 @@
|
|||
[app.common.features :as cfeat]
|
||||
[app.common.files.changes :as ch]
|
||||
[app.common.files.migrations :as fmig]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.svg :as csvg]
|
||||
[app.common.types.color :as types.color]
|
||||
[app.common.types.component :as types.component]
|
||||
[app.common.types.components-list :as types.components-list]
|
||||
[app.common.types.container :as types.container]
|
||||
[app.common.types.file :as types.file]
|
||||
[app.common.types.page :as types.page]
|
||||
[app.common.types.pages-list :as types.pages-list]
|
||||
[app.common.types.shape :as types.shape]
|
||||
[app.common.types.typography :as types.typography]
|
||||
[app.common.uuid :as uuid]
|
||||
|
@ -37,41 +32,36 @@
|
|||
(def ^:private conjv (fnil conj []))
|
||||
(def ^:private conjs (fnil conj #{}))
|
||||
|
||||
(defn default-uuid
|
||||
(defn- default-uuid
|
||||
[v]
|
||||
(or v (uuid/next)))
|
||||
|
||||
(defn- track-used-name
|
||||
[file name]
|
||||
(let [container-id (::current-page-id file)]
|
||||
(update-in file [::unames container-id] conjs name)))
|
||||
[state name]
|
||||
(let [container-id (::current-page-id state)]
|
||||
(update-in state [::unames container-id] conjs name)))
|
||||
|
||||
(defn- commit-change
|
||||
[file change & {:keys [add-container]
|
||||
:or {add-container false}}]
|
||||
[state change & {:keys [add-container]}]
|
||||
(let [file-id (get state ::current-file-id)]
|
||||
(assert (uuid? file-id) "no current file id")
|
||||
|
||||
(let [change (cond-> change
|
||||
add-container
|
||||
(assoc :page-id (::current-page-id file)
|
||||
:frame-id (::current-frame-id file)))]
|
||||
(-> file
|
||||
(update ::changes conjv change)
|
||||
(update :data ch/process-changes [change] false))))
|
||||
|
||||
(defn- lookup-objects
|
||||
[file]
|
||||
(dm/get-in file [:data :pages-index (::current-page-id file) :objects]))
|
||||
(assoc :page-id (::current-page-id state)
|
||||
:frame-id (::current-frame-id state)))]
|
||||
(update-in state [::files file-id :data] ch/process-changes [change] false))))
|
||||
|
||||
(defn- commit-shape
|
||||
[file shape]
|
||||
[state shape]
|
||||
(let [parent-id
|
||||
(-> file ::parent-stack peek)
|
||||
(-> state ::parent-stack peek)
|
||||
|
||||
frame-id
|
||||
(::current-frame-id file)
|
||||
(get state ::current-frame-id)
|
||||
|
||||
page-id
|
||||
(::current-page-id file)
|
||||
(get state ::current-page-id)
|
||||
|
||||
change
|
||||
{:type :add-obj
|
||||
|
@ -82,39 +72,31 @@
|
|||
:frame-id frame-id
|
||||
:page-id page-id}]
|
||||
|
||||
(-> file
|
||||
(-> state
|
||||
(commit-change change)
|
||||
(track-used-name (:name shape)))))
|
||||
|
||||
(defn- generate-name
|
||||
[type data]
|
||||
(if (= type :svg-raw)
|
||||
(let [tag (dm/get-in data [:content :tag])]
|
||||
(str "svg-" (cond (string? tag) tag
|
||||
(keyword? tag) (d/name tag)
|
||||
(nil? tag) "node"
|
||||
:else (str tag))))
|
||||
(str/capital (d/name type))))
|
||||
|
||||
(defn- unique-name
|
||||
[name file]
|
||||
(let [container-id (::current-page-id file)
|
||||
unames (dm/get-in file [:unames container-id])]
|
||||
[name state]
|
||||
(let [container-id (::current-page-id state)
|
||||
unames (dm/get-in state [:unames container-id])]
|
||||
(d/unique-name name (or unames #{}))))
|
||||
|
||||
(defn- clear-names [file]
|
||||
(dissoc file ::unames))
|
||||
|
||||
(defn- assign-name
|
||||
(defn- assign-shape-name
|
||||
"Given a tag returns its layer name"
|
||||
[data file type]
|
||||
|
||||
(cond-> data
|
||||
(nil? (:name data))
|
||||
(assoc :name (generate-name type data))
|
||||
[shape state]
|
||||
(cond-> shape
|
||||
(nil? (:name shape))
|
||||
(assoc :name (let [type (get shape :type)]
|
||||
(case type
|
||||
:frame "Board"
|
||||
(str/capital (d/name type)))))
|
||||
|
||||
:always
|
||||
(update :name unique-name file)))
|
||||
(update :name unique-name state)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; SCHEMAS
|
||||
|
@ -135,16 +117,25 @@
|
|||
(def decode-library-typography
|
||||
(sm/decode-fn types.typography/schema:typography sm/json-transformer))
|
||||
|
||||
(def decode-component
|
||||
(sm/decode-fn types.component/schema:component sm/json-transformer))
|
||||
(def schema:add-component
|
||||
[:map
|
||||
[:component-id ::sm/uuid]
|
||||
[:file-id {:optional true} ::sm/uuid]
|
||||
[:name {:optional true} ::sm/text]
|
||||
[:path {:optional true} ::sm/text]])
|
||||
|
||||
(def ^:private check-add-component
|
||||
(sm/check-fn schema:add-component))
|
||||
|
||||
(def decode-add-component
|
||||
(sm/decode-fn schema:add-component sm/json-transformer))
|
||||
|
||||
(def schema:add-component-instance
|
||||
[:map
|
||||
[:component-id ::sm/uuid]
|
||||
[:x ::sm/safe-number]
|
||||
[:y ::sm/safe-number]])
|
||||
[:file-id {:optional true} ::sm/uuid]])
|
||||
|
||||
(def check-add-component-instance
|
||||
(def ^:private check-add-component-instance
|
||||
(sm/check-fn schema:add-component-instance))
|
||||
|
||||
(def decode-add-component-instance
|
||||
|
@ -158,37 +149,76 @@
|
|||
(def decode-add-bool
|
||||
(sm/decode-fn schema:add-bool sm/json-transformer))
|
||||
|
||||
(def check-add-bool
|
||||
(def ^:private check-add-bool
|
||||
(sm/check-fn schema:add-bool))
|
||||
|
||||
(def schema:add-file-media
|
||||
[:map
|
||||
[:id {:optional true} ::sm/uuid]
|
||||
[:name ::sm/text]
|
||||
[:width ::sm/int]
|
||||
[:height ::sm/int]])
|
||||
|
||||
(def decode-add-file-media
|
||||
(sm/decode-fn schema:add-file-media sm/json-transformer))
|
||||
|
||||
(def check-add-file-media
|
||||
(sm/check-fn schema:add-file-media))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; PUBLIC API
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn lookup-shape [file shape-id]
|
||||
(-> (lookup-objects file)
|
||||
(get shape-id)))
|
||||
(defn create-state
|
||||
[]
|
||||
{})
|
||||
|
||||
(defn get-current-page
|
||||
[file]
|
||||
(let [page-id (::current-page-id file)]
|
||||
(dm/get-in file [:data :pages-index page-id])))
|
||||
[state]
|
||||
(let [file-id (get state ::current-file-id)
|
||||
page-id (get state ::current-page-id)]
|
||||
|
||||
(defn create-file
|
||||
[params]
|
||||
(assert (uuid? file-id) "expected current-file-id to be assigned")
|
||||
(assert (uuid? page-id) "expected current-page-id to be assigned")
|
||||
(dm/get-in state [::files file-id :data :pages-index page-id])))
|
||||
|
||||
(defn get-current-objects
|
||||
[state]
|
||||
(-> (get-current-page state)
|
||||
(get :objects)))
|
||||
|
||||
(defn get-shape
|
||||
[state shape-id]
|
||||
(-> (get-current-objects state)
|
||||
(get shape-id)))
|
||||
|
||||
(defn add-file
|
||||
[state params]
|
||||
(let [params (-> params
|
||||
(assoc :features cfeat/default-features)
|
||||
(assoc :migrations fmig/available-migrations))]
|
||||
(types.file/make-file params :create-page false)))
|
||||
(assoc :migrations fmig/available-migrations))
|
||||
file (types.file/make-file params :create-page false)]
|
||||
(-> state
|
||||
(update ::files assoc (:id file) file)
|
||||
(assoc ::current-file-id (:id file)))))
|
||||
|
||||
(declare close-page)
|
||||
|
||||
(defn close-file
|
||||
[state]
|
||||
(let [state (-> state
|
||||
(close-page)
|
||||
(dissoc ::current-file-id))]
|
||||
state))
|
||||
|
||||
(defn add-page
|
||||
[file params]
|
||||
[state params]
|
||||
(let [page (-> (types.page/make-empty-page params)
|
||||
(types.page/check-page))
|
||||
change {:type :add-page
|
||||
:page page}]
|
||||
|
||||
(-> file
|
||||
(-> state
|
||||
(commit-change change)
|
||||
|
||||
;; Current page being edited
|
||||
|
@ -203,62 +233,62 @@
|
|||
;; Last object id added
|
||||
(assoc ::last-id nil))))
|
||||
|
||||
(defn close-page [file]
|
||||
(-> file
|
||||
(defn close-page [state]
|
||||
(-> state
|
||||
(dissoc ::current-page-id)
|
||||
(dissoc ::parent-stack)
|
||||
(dissoc ::last-id)
|
||||
(clear-names)))
|
||||
|
||||
(defn add-artboard
|
||||
[file data]
|
||||
(defn add-board
|
||||
[state params]
|
||||
(let [{:keys [id] :as shape}
|
||||
(-> data
|
||||
(-> params
|
||||
(update :id default-uuid)
|
||||
(assoc :type :frame)
|
||||
(assign-name file :frame)
|
||||
(assign-shape-name state)
|
||||
(types.shape/setup-shape)
|
||||
(types.shape/check-shape))]
|
||||
|
||||
(-> file
|
||||
(-> state
|
||||
(commit-shape shape)
|
||||
(update ::parent-stack conjv id)
|
||||
(assoc ::current-frame-id id)
|
||||
(assoc ::last-id id))))
|
||||
|
||||
(defn close-artboard
|
||||
[file]
|
||||
(let [parent-id (-> file ::parent-stack peek)
|
||||
parent (lookup-shape file parent-id)]
|
||||
(-> file
|
||||
(defn close-board
|
||||
[state]
|
||||
(let [parent-id (-> state ::parent-stack peek)
|
||||
parent (get-shape state parent-id)]
|
||||
(-> state
|
||||
(assoc ::current-frame-id (or (:frame-id parent) root-id))
|
||||
(update ::parent-stack pop))))
|
||||
|
||||
(defn add-group
|
||||
[file params]
|
||||
[state params]
|
||||
(let [{:keys [id] :as shape}
|
||||
(-> params
|
||||
(update :id default-uuid)
|
||||
(assoc :type :group)
|
||||
(assign-name file :group)
|
||||
(assign-shape-name state)
|
||||
(types.shape/setup-shape)
|
||||
(types.shape/check-shape))]
|
||||
(-> file
|
||||
(-> state
|
||||
(commit-shape shape)
|
||||
(assoc ::last-id id)
|
||||
(update ::parent-stack conjv id))))
|
||||
|
||||
(defn close-group
|
||||
[file]
|
||||
(let [group-id (-> file :parent-stack peek)
|
||||
group (lookup-shape file group-id)
|
||||
[state]
|
||||
(let [group-id (-> state :parent-stack peek)
|
||||
group (get-shape state group-id)
|
||||
children (->> (get group :shapes)
|
||||
(into [] (keep (partial lookup-shape file)))
|
||||
(into [] (keep (partial get-shape state)))
|
||||
(not-empty))]
|
||||
|
||||
(assert (some? children) "group expect to have at least 1 children")
|
||||
|
||||
(let [file (if (:masked-group group)
|
||||
(let [state (if (:masked-group group)
|
||||
(let [mask (first children)
|
||||
change {:type :mod-obj
|
||||
:id group-id
|
||||
|
@ -271,7 +301,7 @@
|
|||
{:type :set :attr :flip-y :val (-> mask :flip-y) :ignore-touched true}
|
||||
{:type :set :attr :selrect :val (-> mask :selrect) :ignore-touched true}
|
||||
{:type :set :attr :points :val (-> mask :points) :ignore-touched true}]}]
|
||||
(commit-change file change :add-container true))
|
||||
(commit-change state change :add-container true))
|
||||
(let [group (gsh/update-group-selrect group children)
|
||||
change {:type :mod-obj
|
||||
:id group-id
|
||||
|
@ -283,16 +313,16 @@
|
|||
{:type :set :attr :width :val (-> group :selrect :width) :ignore-touched true}
|
||||
{:type :set :attr :height :val (-> group :selrect :height) :ignore-touched true}]}]
|
||||
|
||||
(commit-change file change :add-container true)))]
|
||||
(update file ::parent-stack pop))))
|
||||
(commit-change state change :add-container true)))]
|
||||
(update state ::parent-stack pop))))
|
||||
|
||||
(defn add-bool
|
||||
[file params]
|
||||
[state params]
|
||||
(let [{:keys [group-id type]}
|
||||
(check-add-bool params)
|
||||
|
||||
group
|
||||
(lookup-shape file group-id)
|
||||
(get-shape state group-id)
|
||||
|
||||
children
|
||||
(->> (get group :shapes)
|
||||
|
@ -300,7 +330,7 @@
|
|||
|
||||
(assert (some? children) "expect group to have at least 1 element")
|
||||
|
||||
(let [objects (lookup-objects file)
|
||||
(let [objects (get-current-objects state)
|
||||
bool (-> group
|
||||
(assoc :type :bool)
|
||||
(gsh/update-bool objects))
|
||||
|
@ -317,101 +347,102 @@
|
|||
{:type :set :attr :width :val (-> bool :selrect :width) :ignore-touched true}
|
||||
{:type :set :attr :height :val (-> bool :selrect :height) :ignore-touched true}]}]
|
||||
|
||||
(-> file
|
||||
(-> state
|
||||
(commit-change change :add-container true)
|
||||
(assoc ::last-id group-id)))))
|
||||
|
||||
(defn add-shape
|
||||
[file params]
|
||||
[state params]
|
||||
(let [obj (-> params
|
||||
(d/update-when :svg-attrs csvg/attrs->props)
|
||||
(types.shape/setup-shape)
|
||||
(assign-name file :type))]
|
||||
(-> file
|
||||
(assign-shape-name state))]
|
||||
(-> state
|
||||
(commit-shape obj)
|
||||
(assoc ::last-id (:id obj)))))
|
||||
|
||||
(defn add-library-color
|
||||
[file color]
|
||||
[state color]
|
||||
(let [color (-> color
|
||||
(update :opacity d/nilv 1)
|
||||
(update :id default-uuid)
|
||||
(types.color/check-library-color color))
|
||||
|
||||
change {:type :add-color
|
||||
:color color}]
|
||||
(-> file
|
||||
|
||||
(-> state
|
||||
(commit-change change)
|
||||
(assoc ::last-id (:id color)))))
|
||||
|
||||
(defn add-library-typography
|
||||
[file typography]
|
||||
[state typography]
|
||||
(let [typography (-> typography
|
||||
(update :id default-uuid)
|
||||
(d/without-nils))
|
||||
change {:type :add-typography
|
||||
:id (:id typography)
|
||||
:typography typography}]
|
||||
(-> file
|
||||
(-> state
|
||||
(commit-change change)
|
||||
(assoc ::last-id (:id typography)))))
|
||||
|
||||
(defn add-component
|
||||
[file params]
|
||||
(let [change1 {:type :add-component
|
||||
:id (or (:id params) (uuid/next))
|
||||
:name (:name params)
|
||||
:path (:path params)
|
||||
:main-instance-id (:main-instance-id params)
|
||||
:main-instance-page (:main-instance-page params)}
|
||||
[state params]
|
||||
(let [{:keys [component-id file-id name path]}
|
||||
(check-add-component params)
|
||||
|
||||
comp-id (get change1 :id)
|
||||
|
||||
change2 {:type :mod-obj
|
||||
:id (:main-instance-id params)
|
||||
:operations
|
||||
[{:type :set :attr :component-root :val true}
|
||||
{:type :set :attr :component-id :val comp-id}
|
||||
{:type :set :attr :component-file :val (:id file)}]}]
|
||||
(-> file
|
||||
(commit-change change1)
|
||||
(commit-change change2)
|
||||
(assoc ::last-id comp-id)
|
||||
(assoc ::current-frame-id comp-id))))
|
||||
|
||||
(defn add-component-instance
|
||||
[{:keys [id data] :as file} params]
|
||||
|
||||
(let [{:keys [component-id x y]}
|
||||
(check-add-component-instance params)
|
||||
|
||||
component
|
||||
(types.components-list/get-component data component-id)
|
||||
frame-id
|
||||
(get state ::current-frame-id)
|
||||
|
||||
page-id
|
||||
(get file ::current-page-id)]
|
||||
(get state ::current-page-id)
|
||||
|
||||
(assert (uuid? page-id) "page-id is expected to be set")
|
||||
(assert (uuid? component) "component is expected to exist")
|
||||
component-id
|
||||
(or component-id (uuid/next))
|
||||
|
||||
;; FIXME: this should be on files and not in pages-list
|
||||
(let [page (types.pages-list/get-page (:data file) page-id)
|
||||
pos (gpt/point x y)
|
||||
change1
|
||||
(d/without-nils
|
||||
{:type :add-component
|
||||
:id component-id
|
||||
:name (or name "anonmous")
|
||||
:path path
|
||||
:main-instance-id frame-id
|
||||
:main-instance-page page-id})
|
||||
|
||||
[shape shapes]
|
||||
(types.container/make-component-instance page component id pos)
|
||||
change2
|
||||
{:type :mod-obj
|
||||
:id frame-id
|
||||
:operations
|
||||
[{:type :set :attr :component-root :val true}
|
||||
{:type :set :attr :component-id :val component-id}
|
||||
{:type :set :attr :component-file :val file-id}]}]
|
||||
|
||||
file
|
||||
(reduce #(commit-change %1
|
||||
{:type :add-obj
|
||||
:id (:id %2)
|
||||
:page-id (:id page)
|
||||
:parent-id (:parent-id %2)
|
||||
:frame-id (:frame-id %2)
|
||||
:ignore-touched true
|
||||
:obj %2})
|
||||
file
|
||||
shapes)]
|
||||
(-> state
|
||||
(commit-change change1)
|
||||
(commit-change change2))))
|
||||
|
||||
(assoc file ::last-id (:id shape)))))
|
||||
(defn add-component-instance
|
||||
[state params]
|
||||
|
||||
(let [{:keys [component-id file-id]}
|
||||
(check-add-component-instance params)
|
||||
|
||||
file-id
|
||||
(or file-id (get state ::current-file-id))
|
||||
|
||||
frame-id
|
||||
(get state ::current-frame-id)
|
||||
|
||||
change
|
||||
{:type :mod-obj
|
||||
:id frame-id
|
||||
:operations
|
||||
[{:type :set :attr :component-root :val false}
|
||||
{:type :set :attr :component-id :val component-id}
|
||||
{:type :set :attr :component-file :val file-id}]}]
|
||||
|
||||
(commit-change state change)))
|
||||
|
||||
(defn delete-shape
|
||||
[file id]
|
||||
|
@ -423,10 +454,12 @@
|
|||
:id id}))
|
||||
|
||||
(defn update-shape
|
||||
[file shape-id f]
|
||||
(let [page-id (::current-page-id file)
|
||||
objects (lookup-objects file)
|
||||
[state shape-id f]
|
||||
(let [page-id (get state ::current-page-id)
|
||||
|
||||
objects (get-current-objects state)
|
||||
old-shape (get objects shape-id)
|
||||
|
||||
new-shape (f old-shape)
|
||||
attrs (d/concat-set
|
||||
(keys old-shape)
|
||||
|
@ -440,7 +473,7 @@
|
|||
changes
|
||||
(conj changes {:type :set :attr attr :val new-val :ignore-touched true}))))]
|
||||
|
||||
(-> file
|
||||
(-> state
|
||||
(commit-change
|
||||
{:type :mod-obj
|
||||
:operations (reduce generate-operation [] attrs)
|
||||
|
@ -449,12 +482,12 @@
|
|||
(assoc ::last-id shape-id))))
|
||||
|
||||
(defn add-guide
|
||||
[file guide]
|
||||
[state guide]
|
||||
(let [guide (cond-> guide
|
||||
(nil? (:id guide))
|
||||
(assoc :id (uuid/next)))
|
||||
page-id (::current-page-id file)]
|
||||
(-> file
|
||||
page-id (::current-page-id state)]
|
||||
(-> state
|
||||
(commit-change
|
||||
{:type :set-guide
|
||||
:page-id page-id
|
||||
|
@ -463,24 +496,54 @@
|
|||
(assoc ::last-id (:id guide)))))
|
||||
|
||||
(defn delete-guide
|
||||
[file id]
|
||||
|
||||
(let [page-id (::current-page-id file)]
|
||||
(commit-change file
|
||||
[state id]
|
||||
(let [page-id (::current-page-id state)]
|
||||
(commit-change state
|
||||
{:type :set-guide
|
||||
:page-id page-id
|
||||
:id id
|
||||
:params nil})))
|
||||
|
||||
(defn update-guide
|
||||
[file guide]
|
||||
(let [page-id (::current-page-id file)]
|
||||
(commit-change file
|
||||
[state guide]
|
||||
(let [page-id (::current-page-id state)]
|
||||
(commit-change state
|
||||
{:type :set-guide
|
||||
:page-id page-id
|
||||
:id (:id guide)
|
||||
:params guide})))
|
||||
|
||||
(defn strip-image-extension [filename]
|
||||
(let [image-extensions-re #"(\.png)|(\.jpg)|(\.jpeg)|(\.webp)|(\.gif)|(\.svg)$"]
|
||||
(str/replace filename image-extensions-re "")))
|
||||
(defrecord BlobWrapper [mtype size blob])
|
||||
|
||||
(defn add-file-media
|
||||
[state params blob]
|
||||
(assert (instance? BlobWrapper blob) "expect blob to be wrapped")
|
||||
|
||||
(let [media-id
|
||||
(uuid/next)
|
||||
|
||||
file-id
|
||||
(get state ::current-file-id)
|
||||
|
||||
{:keys [id width height name]}
|
||||
(-> params
|
||||
(update :id default-uuid)
|
||||
(check-add-file-media params))]
|
||||
|
||||
(-> state
|
||||
(update ::blobs assoc media-id blob)
|
||||
(update ::media assoc media-id
|
||||
{:id media-id
|
||||
:bucket "file-media-object"
|
||||
:content-type (get blob :mtype)
|
||||
:size (get blob :size)})
|
||||
(update ::file-media assoc id
|
||||
{:id id
|
||||
:name name
|
||||
:width width
|
||||
:height height
|
||||
:file-id file-id
|
||||
:media-id media-id
|
||||
:mtype (get blob :mtype)})
|
||||
|
||||
(assoc ::last-id id))))
|
||||
|
|
|
@ -310,12 +310,12 @@
|
|||
[:add-media
|
||||
[:map {:title "AddMediaChange"}
|
||||
[:type [:= :add-media]]
|
||||
[:object ::ctf/media-object]]]
|
||||
[:object ctf/schema:media]]]
|
||||
|
||||
[:mod-media
|
||||
[:map {:title "ModMediaChange"}
|
||||
[:type [:= :mod-media]]
|
||||
[:object ::ctf/media-object]]]
|
||||
[:object ctf/schema:media]]]
|
||||
|
||||
[:del-media
|
||||
[:map {:title "DelMediaChange"}
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.common.media
|
||||
"Media assets helpers (images, fonts, etc)"
|
||||
(:require
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
;; We have added ".ttf" as string to solve a problem with chrome input selector
|
||||
|
@ -59,27 +59,17 @@
|
|||
"application/penpot" ".penpot"
|
||||
"application/pdf" ".pdf"
|
||||
"text/plain" ".txt"
|
||||
"font/woff" ".woff"
|
||||
"font/woff2" ".woff2"
|
||||
"font/ttf" ".ttf"
|
||||
"font/otf" ".otf"
|
||||
"application/octet-stream" ".bin"
|
||||
nil))
|
||||
|
||||
(s/def ::id uuid?)
|
||||
(s/def ::name string?)
|
||||
(s/def ::width number?)
|
||||
(s/def ::height number?)
|
||||
(s/def ::created-at inst?)
|
||||
(s/def ::modified-at inst?)
|
||||
(s/def ::mtype string?)
|
||||
(s/def ::uri string?)
|
||||
|
||||
(s/def ::media-object
|
||||
(s/keys :req-un [::id
|
||||
::name
|
||||
::width
|
||||
::height
|
||||
::mtype
|
||||
::created-at
|
||||
::modified-at
|
||||
::uri]))
|
||||
|
||||
(defn strip-image-extension
|
||||
[filename]
|
||||
(let [image-extensions-re #"(\.png)|(\.jpg)|(\.jpeg)|(\.webp)|(\.gif)|(\.svg)$"]
|
||||
(str/replace filename image-extensions-re "")))
|
||||
|
||||
(defn parse-font-weight
|
||||
[variant]
|
||||
|
|
|
@ -54,13 +54,13 @@
|
|||
::oapi/type "integer"
|
||||
::oapi/format "int64"}}))
|
||||
|
||||
(def schema:image-color
|
||||
(def schema:image
|
||||
[:map {:title "ImageColor"}
|
||||
[:name {:optional true} :string]
|
||||
[:width ::sm/int]
|
||||
[:height ::sm/int]
|
||||
[:mtype {:optional true} [:maybe :string]]
|
||||
[:mtype ::sm/text]
|
||||
[:id ::sm/uuid]
|
||||
[:name {:optional true} ::sm/text]
|
||||
[:keep-aspect-ratio {:optional true} :boolean]])
|
||||
|
||||
(def gradient-types
|
||||
|
@ -93,7 +93,7 @@
|
|||
[:ref-id {:optional true} ::sm/uuid]
|
||||
[:ref-file {:optional true} ::sm/uuid]
|
||||
[:gradient {:optional true} [:maybe schema:gradient]]
|
||||
[:image {:optional true} [:maybe schema:image-color]]
|
||||
[:image {:optional true} [:maybe schema:image]]
|
||||
[:plugin-data {:optional true} ::ctpg/plugin-data]])
|
||||
|
||||
(def schema:color
|
||||
|
@ -106,7 +106,7 @@
|
|||
[:opacity {:optional true} [:maybe ::sm/safe-number]]
|
||||
[:color {:optional true} [:maybe schema:rgb-color]]
|
||||
[:gradient {:optional true} [:maybe schema:gradient]]
|
||||
[:image {:optional true} [:maybe schema:image-color]]]
|
||||
[:image {:optional true} [:maybe schema:image]]]
|
||||
[::sm/contains-any {:strict true} [:color :gradient :image]]])
|
||||
|
||||
;; Same as color but with :id prop required
|
||||
|
@ -115,9 +115,10 @@
|
|||
(sm/required-keys schema:color-attrs [:id])
|
||||
[::sm/contains-any {:strict true} [:color :gradient :image]]])
|
||||
|
||||
;; FIXME: revisit if we really need this all registers
|
||||
(sm/register! ::color schema:color)
|
||||
(sm/register! ::gradient schema:gradient)
|
||||
(sm/register! ::image-color schema:image-color)
|
||||
(sm/register! ::image-color schema:image)
|
||||
(sm/register! ::recent-color schema:recent-color)
|
||||
(sm/register! ::color-attrs schema:color-attrs)
|
||||
|
||||
|
|
|
@ -38,18 +38,18 @@
|
|||
|
||||
(def schema:media
|
||||
"A schema that represents the file media object"
|
||||
[:map {:title "FileMediaObject"}
|
||||
[:map {:title "FileMedia"}
|
||||
[:id ::sm/uuid]
|
||||
[:created-at ::sm/inst]
|
||||
[:created-at {:optional true} ::sm/inst]
|
||||
[:deleted-at {:optional true} ::sm/inst]
|
||||
[:name :string]
|
||||
[:width ::sm/safe-int]
|
||||
[:height ::sm/safe-int]
|
||||
[:mtype :string]
|
||||
[:file-id {:optional true} ::sm/uuid]
|
||||
[:media-id ::sm/uuid]
|
||||
[:file-id {:optional true} ::sm/uuid]
|
||||
[:thumbnail-id {:optional true} ::sm/uuid]
|
||||
[:is-local :boolean]])
|
||||
[:is-local {:optional true} :boolean]])
|
||||
|
||||
(def schema:colors
|
||||
[:map-of {:gen/max 5} ::sm/uuid ::ctc/color])
|
||||
|
@ -102,7 +102,6 @@
|
|||
(sm/register! ::media schema:media)
|
||||
(sm/register! ::colors schema:colors)
|
||||
(sm/register! ::typographies schema:typographies)
|
||||
(sm/register! ::media-object schema:media)
|
||||
|
||||
(def check-file
|
||||
(sm/check-fn schema:file :hint "check error on validating file"))
|
||||
|
@ -110,7 +109,7 @@
|
|||
(def check-file-data
|
||||
(sm/check-fn schema:data))
|
||||
|
||||
(def check-media-object
|
||||
(def check-file-media
|
||||
(sm/check-fn schema:media))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
|
|
@ -34,23 +34,18 @@
|
|||
|
||||
(defn export-files
|
||||
[files format]
|
||||
(dm/assert!
|
||||
"expected valid files param"
|
||||
(check-export-files files))
|
||||
(assert (contains? valid-formats format)
|
||||
"expected valid export format")
|
||||
|
||||
(dm/assert!
|
||||
"expected valid format"
|
||||
(contains? valid-formats format))
|
||||
(let [files (check-export-files files)]
|
||||
|
||||
(ptk/reify ::export-files
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [features (get state :features)
|
||||
team-id (:current-team-id state)
|
||||
(let [team-id (get state :current-team-id)
|
||||
evname (if (= format :legacy-zip)
|
||||
"export-standard-files"
|
||||
"export-binary-files")]
|
||||
|
||||
(rx/merge
|
||||
(rx/of (ptk/event ::ev/event {::ev/name evname
|
||||
::ev/origin "dashboard"
|
||||
|
@ -65,10 +60,9 @@
|
|||
(rx/map (fn [files]
|
||||
(modal/show
|
||||
{:type ::export-files
|
||||
:features features
|
||||
:team-id team-id
|
||||
:files files
|
||||
:format format})))))))))
|
||||
:format format}))))))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Team Request
|
||||
|
|
|
@ -254,7 +254,7 @@
|
|||
|
||||
(defn add-media
|
||||
[media]
|
||||
(let [media (ctf/check-media-object media)]
|
||||
(let [media (ctf/check-file-media media)]
|
||||
(ptk/reify ::add-media
|
||||
ev/Event
|
||||
(-data [_] media)
|
||||
|
|
|
@ -10,11 +10,11 @@
|
|||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.files.builder :as fb]
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.files.shapes-builder :as sb]
|
||||
[app.common.logging :as log]
|
||||
[app.common.math :as mth]
|
||||
[app.common.media :as media]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.container :as ctn]
|
||||
[app.common.types.shape :as cts]
|
||||
|
@ -137,7 +137,7 @@
|
|||
(= (.-type blob) "image/svg+xml")))
|
||||
|
||||
(prepare-blob [blob]
|
||||
(let [name (or name (if (dmm/file? blob) (fb/strip-image-extension (.-name blob)) "blob"))]
|
||||
(let [name (or name (if (dmm/file? blob) (media/strip-image-extension (.-name blob)) "blob"))]
|
||||
{:file-id file-id
|
||||
:name name
|
||||
:is-local local?
|
||||
|
|
|
@ -27,7 +27,8 @@
|
|||
(t/is (some? data))))
|
||||
|
||||
(t/testing "Add empty page (only root-frame)"
|
||||
(let [page (-> (fb/create-file {:name "Test"})
|
||||
(let [page (-> (fb/create-state)
|
||||
(fb/add-file {:name "Test"})
|
||||
(fb/add-page {:name "Page 1"})
|
||||
(fb/get-current-page))
|
||||
|
||||
|
@ -36,7 +37,8 @@
|
|||
(t/is (some? data))))
|
||||
|
||||
(t/testing "Create simple shape on root"
|
||||
(let [file (-> (fb/create-file {:name "Test"})
|
||||
(let [state (-> (fb/create-state)
|
||||
(fb/add-file {:name "Test"})
|
||||
(fb/add-page {:name "Page 1"})
|
||||
(fb/add-shape
|
||||
{:type :rect
|
||||
|
@ -44,7 +46,7 @@
|
|||
:y 0
|
||||
:width 100
|
||||
:height 100}))
|
||||
page (fb/get-current-page file)
|
||||
page (fb/get-current-page state)
|
||||
|
||||
data (-> (sd/make-snap-data)
|
||||
(sd/add-page page))
|
||||
|
@ -66,17 +68,18 @@
|
|||
(t/is (= (first (nth result-x 2)) 100))))
|
||||
|
||||
(t/testing "Add page with single empty frame"
|
||||
(let [file (-> (fb/create-file {:name "Test"})
|
||||
(let [state (-> (fb/create-state)
|
||||
(fb/add-file {:name "Test"})
|
||||
(fb/add-page {:name "Page 1"})
|
||||
(fb/add-artboard
|
||||
(fb/add-board
|
||||
{:x 0
|
||||
:y 0
|
||||
:width 100
|
||||
:height 100})
|
||||
(fb/close-artboard))
|
||||
(fb/close-board))
|
||||
|
||||
frame-id (::fb/last-id file)
|
||||
page (fb/get-current-page file)
|
||||
frame-id (::fb/last-id state)
|
||||
page (fb/get-current-page state)
|
||||
|
||||
;; frame-id (::fb/last-id file)
|
||||
data (-> (sd/make-snap-data)
|
||||
|
@ -91,26 +94,27 @@
|
|||
|
||||
(t/testing "Add page with some shapes inside frames"
|
||||
(with-redefs [uuid/next (get-mocked-uuid)]
|
||||
(let [file (-> (fb/create-file {:name "Test"})
|
||||
(let [state (-> (fb/create-state)
|
||||
(fb/add-file {:name "Test"})
|
||||
(fb/add-page {:name "Page 1"})
|
||||
(fb/add-artboard
|
||||
(fb/add-board
|
||||
{:x 0
|
||||
:y 0
|
||||
:width 100
|
||||
:height 100}))
|
||||
|
||||
frame-id (::fb/last-id file)
|
||||
frame-id (::fb/last-id state)
|
||||
|
||||
file (-> file
|
||||
state (-> state
|
||||
(fb/add-shape
|
||||
{:type :rect
|
||||
:x 25
|
||||
:y 25
|
||||
:width 50
|
||||
:height 50})
|
||||
(fb/close-artboard))
|
||||
(fb/close-board))
|
||||
|
||||
page (fb/get-current-page file)
|
||||
page (fb/get-current-page state)
|
||||
|
||||
data (-> (sd/make-snap-data)
|
||||
(sd/add-page page))
|
||||
|
@ -123,16 +127,16 @@
|
|||
(t/is (= (count result-frame-x) 5)))))
|
||||
|
||||
(t/testing "Add a global guide"
|
||||
(let [file (-> (fb/create-file {:name "Test"})
|
||||
(let [state (-> (fb/create-state)
|
||||
(fb/add-file {:name "Test"})
|
||||
(fb/add-page {:name "Page 1"})
|
||||
(fb/add-guide {:position 50 :axis :x})
|
||||
(fb/add-artboard {:x 200 :y 200 :width 100 :height 100})
|
||||
(fb/close-artboard))
|
||||
(fb/add-board {:x 200 :y 200 :width 100 :height 100})
|
||||
(fb/close-board))
|
||||
|
||||
frame-id (::fb/last-id file)
|
||||
page (fb/get-current-page file)
|
||||
frame-id (::fb/last-id state)
|
||||
page (fb/get-current-page state)
|
||||
|
||||
;; frame-id (::fb/last-id file)
|
||||
data (-> (sd/make-snap-data)
|
||||
(sd/add-page page))
|
||||
|
||||
|
@ -151,17 +155,18 @@
|
|||
(t/is (= (count result-frame-y) 0))))
|
||||
|
||||
(t/testing "Add a frame guide"
|
||||
(let [file (-> (fb/create-file {:name "Test"})
|
||||
(let [state (-> (fb/create-state)
|
||||
(fb/add-file {:name "Test"})
|
||||
(fb/add-page {:name "Page 1"})
|
||||
(fb/add-artboard {:x 200 :y 200 :width 100 :height 100})
|
||||
(fb/close-artboard))
|
||||
(fb/add-board {:x 200 :y 200 :width 100 :height 100})
|
||||
(fb/close-board))
|
||||
|
||||
frame-id (::fb/last-id file)
|
||||
frame-id (::fb/last-id state)
|
||||
|
||||
file (-> file
|
||||
state (-> state
|
||||
(fb/add-guide {:position 50 :axis :x :frame-id frame-id}))
|
||||
|
||||
page (fb/get-current-page file)
|
||||
page (fb/get-current-page state)
|
||||
|
||||
data (-> (sd/make-snap-data)
|
||||
(sd/add-page page))
|
||||
|
@ -182,26 +187,26 @@
|
|||
|
||||
(t/deftest test-update-index
|
||||
(t/testing "Create frame on root and then remove it."
|
||||
(let [file (-> (fb/create-file {:name "Test"})
|
||||
(let [state (-> (fb/create-state)
|
||||
(fb/add-file {:name "Test"})
|
||||
(fb/add-page {:name "Page 1"})
|
||||
(fb/add-artboard
|
||||
(fb/add-board
|
||||
{:x 0
|
||||
:y 0
|
||||
:width 100
|
||||
:height 100})
|
||||
(fb/close-artboard))
|
||||
(fb/close-board))
|
||||
|
||||
shape-id (::fb/last-id file)
|
||||
page (fb/get-current-page file)
|
||||
shape-id (::fb/last-id state)
|
||||
page (fb/get-current-page state)
|
||||
|
||||
;; frame-id (::fb/last-id file)
|
||||
data (-> (sd/make-snap-data)
|
||||
(sd/add-page page))
|
||||
|
||||
file (-> file
|
||||
state (-> state
|
||||
(fb/delete-shape shape-id))
|
||||
|
||||
new-page (fb/get-current-page file)
|
||||
new-page (fb/get-current-page state)
|
||||
data (sd/update-page data page new-page)
|
||||
|
||||
result-x (sd/query data (:id page) uuid/zero :x [0 100])
|
||||
|
@ -212,7 +217,8 @@
|
|||
(t/is (= (count result-y) 0))))
|
||||
|
||||
(t/testing "Create simple shape on root. Then remove it"
|
||||
(let [file (-> (fb/create-file {:name "Test"})
|
||||
(let [state (-> (fb/create-state)
|
||||
(fb/add-file {:name "Test"})
|
||||
(fb/add-page {:name "Page 1"})
|
||||
(fb/add-shape
|
||||
{:type :rect
|
||||
|
@ -221,16 +227,16 @@
|
|||
:width 100
|
||||
:height 100}))
|
||||
|
||||
shape-id (::fb/last-id file)
|
||||
page (fb/get-current-page file)
|
||||
shape-id (::fb/last-id state)
|
||||
page (fb/get-current-page state)
|
||||
|
||||
;; frame-id (::fb/last-id file)
|
||||
;; frame-id (::fb/last-id state)
|
||||
data (-> (sd/make-snap-data)
|
||||
(sd/add-page page))
|
||||
|
||||
file (fb/delete-shape file shape-id)
|
||||
state (fb/delete-shape state shape-id)
|
||||
|
||||
new-page (fb/get-current-page file)
|
||||
new-page (fb/get-current-page state)
|
||||
data (sd/update-page data page new-page)
|
||||
|
||||
result-x (sd/query data (:id page) uuid/zero :x [0 100])
|
||||
|
@ -241,26 +247,27 @@
|
|||
(t/is (= (count result-y) 0))))
|
||||
|
||||
(t/testing "Create shape inside frame, then remove it"
|
||||
(let [file (-> (fb/create-file {:name "Test"})
|
||||
(let [state (-> (fb/create-state)
|
||||
(fb/add-file {:name "Test"})
|
||||
(fb/add-page {:name "Page 1"})
|
||||
(fb/add-artboard
|
||||
(fb/add-board
|
||||
{:x 0
|
||||
:y 0
|
||||
:width 100
|
||||
:height 100}))
|
||||
frame-id (::fb/last-id file)
|
||||
frame-id (::fb/last-id state)
|
||||
|
||||
file (fb/add-shape file {:type :rect :x 25 :y 25 :width 50 :height 50})
|
||||
shape-id (::fb/last-id file)
|
||||
state (fb/add-shape state {:type :rect :x 25 :y 25 :width 50 :height 50})
|
||||
shape-id (::fb/last-id state)
|
||||
|
||||
file (fb/close-artboard file)
|
||||
state (fb/close-board state)
|
||||
|
||||
page (fb/get-current-page file)
|
||||
page (fb/get-current-page state)
|
||||
data (-> (sd/make-snap-data)
|
||||
(sd/add-page page))
|
||||
|
||||
file (fb/delete-shape file shape-id)
|
||||
new-page (fb/get-current-page file)
|
||||
state (fb/delete-shape state shape-id)
|
||||
new-page (fb/get-current-page state)
|
||||
|
||||
data (sd/update-page data page new-page)
|
||||
|
||||
|
@ -272,20 +279,22 @@
|
|||
(t/is (= (count result-frame-x) 3))))
|
||||
|
||||
(t/testing "Create global guide then remove it"
|
||||
(let [file (-> (fb/create-file {:name "Test"})
|
||||
(let [state (-> (fb/create-state)
|
||||
(fb/add-file {:name "Test"})
|
||||
(fb/add-page {:name "Page 1"})
|
||||
(fb/add-guide {:position 50 :axis :x}))
|
||||
|
||||
guide-id (::fb/last-id file)
|
||||
guide-id (::fb/last-id state)
|
||||
|
||||
file (-> (fb/add-artboard file {:x 200 :y 200 :width 100 :height 100})
|
||||
(fb/close-artboard))
|
||||
state (-> (fb/add-board state {:x 200 :y 200 :width 100 :height 100})
|
||||
(fb/close-board))
|
||||
|
||||
frame-id (::fb/last-id file)
|
||||
page (fb/get-current-page file)
|
||||
data (-> (sd/make-snap-data) (sd/add-page page))
|
||||
frame-id (::fb/last-id state)
|
||||
page (fb/get-current-page state)
|
||||
data (-> (sd/make-snap-data)
|
||||
(sd/add-page page))
|
||||
|
||||
new-page (-> (fb/delete-guide file guide-id)
|
||||
new-page (-> (fb/delete-guide state guide-id)
|
||||
(fb/get-current-page))
|
||||
|
||||
data (sd/update-page data page new-page)
|
||||
|
@ -305,10 +314,11 @@
|
|||
(t/is (= (count result-frame-y) 0))))
|
||||
|
||||
(t/testing "Create frame guide then remove it"
|
||||
(let [file (-> (fb/create-file {:name "Test"})
|
||||
(let [file (-> (fb/create-state)
|
||||
(fb/add-file {:name "Test"})
|
||||
(fb/add-page {:name "Page 1"})
|
||||
(fb/add-artboard {:x 200 :y 200 :width 100 :height 100})
|
||||
(fb/close-artboard))
|
||||
(fb/add-board {:x 200 :y 200 :width 100 :height 100})
|
||||
(fb/close-board))
|
||||
|
||||
frame-id (::fb/last-id file)
|
||||
file (fb/add-guide file {:position 50 :axis :x :frame-id frame-id})
|
||||
|
@ -336,20 +346,22 @@
|
|||
(t/is (= (count result-frame-y) 0))))
|
||||
|
||||
(t/testing "Update frame coordinates"
|
||||
(let [file (-> (fb/create-file {:name "Test"})
|
||||
(let [state (-> (fb/create-state)
|
||||
(fb/add-file {:name "Test"})
|
||||
(fb/add-page {:name "Page 1"})
|
||||
(fb/add-artboard
|
||||
(fb/add-board
|
||||
{:x 0
|
||||
:y 0
|
||||
:width 100
|
||||
:height 100})
|
||||
(fb/close-artboard))
|
||||
(fb/close-board))
|
||||
|
||||
frame-id (::fb/last-id file)
|
||||
page (fb/get-current-page file)
|
||||
data (-> (sd/make-snap-data) (sd/add-page page))
|
||||
frame-id (::fb/last-id state)
|
||||
page (fb/get-current-page state)
|
||||
data (-> (sd/make-snap-data)
|
||||
(sd/add-page page))
|
||||
|
||||
file (fb/update-shape file frame-id
|
||||
state (fb/update-shape state frame-id
|
||||
(fn [shape]
|
||||
(-> shape
|
||||
(dissoc :selrect :points)
|
||||
|
@ -357,8 +369,7 @@
|
|||
(cts/setup-shape))))
|
||||
|
||||
|
||||
new-page (fb/get-current-page file)
|
||||
|
||||
new-page (fb/get-current-page state)
|
||||
data (sd/update-page data page new-page)
|
||||
|
||||
result-zero-x-1 (sd/query data (:id page) uuid/zero :x [0 100])
|
||||
|
@ -373,7 +384,8 @@
|
|||
(t/is (= (count result-frame-x-2) 3))))
|
||||
|
||||
(t/testing "Update shape coordinates"
|
||||
(let [file (-> (fb/create-file {:name "Test"})
|
||||
(let [state (-> (fb/create-state)
|
||||
(fb/add-file {:name "Test"})
|
||||
(fb/add-page {:name "Page 1"})
|
||||
(fb/add-shape
|
||||
{:type :rect
|
||||
|
@ -382,19 +394,19 @@
|
|||
:width 100
|
||||
:height 100}))
|
||||
|
||||
shape-id (::fb/last-id file)
|
||||
page (fb/get-current-page file)
|
||||
shape-id (::fb/last-id state)
|
||||
page (fb/get-current-page state)
|
||||
data (-> (sd/make-snap-data)
|
||||
(sd/add-page page))
|
||||
|
||||
file (fb/update-shape file shape-id
|
||||
state (fb/update-shape state shape-id
|
||||
(fn [shape]
|
||||
(-> shape
|
||||
(dissoc :selrect :points)
|
||||
(assoc :x 200 :y 200)
|
||||
(cts/setup-shape))))
|
||||
|
||||
new-page (fb/get-current-page file)
|
||||
new-page (fb/get-current-page state)
|
||||
;; FIXME: update
|
||||
data (sd/update-page data page new-page)
|
||||
|
||||
|
@ -407,21 +419,22 @@
|
|||
|
||||
(t/testing "Update global guide"
|
||||
(let [guide {:position 50 :axis :x}
|
||||
file (-> (fb/create-file {:name "Test"})
|
||||
state (-> (fb/create-state)
|
||||
(fb/add-file {:name "Test"})
|
||||
(fb/add-page {:name "Page 1"})
|
||||
(fb/add-guide guide))
|
||||
|
||||
guide-id (::fb/last-id file)
|
||||
guide-id (::fb/last-id state)
|
||||
guide (assoc guide :id guide-id)
|
||||
|
||||
file (-> (fb/add-artboard file {:x 500 :y 500 :width 100 :height 100})
|
||||
(fb/close-artboard))
|
||||
state (-> (fb/add-board state {:x 500 :y 500 :width 100 :height 100})
|
||||
(fb/close-board))
|
||||
|
||||
frame-id (::fb/last-id file)
|
||||
page (fb/get-current-page file)
|
||||
frame-id (::fb/last-id state)
|
||||
page (fb/get-current-page state)
|
||||
data (-> (sd/make-snap-data) (sd/add-page page))
|
||||
|
||||
new-page (-> (fb/update-guide file (assoc guide :position 150))
|
||||
new-page (-> (fb/update-guide state (assoc guide :position 150))
|
||||
(fb/get-current-page))
|
||||
|
||||
data (sd/update-page data page new-page)
|
||||
|
|
|
@ -33,7 +33,6 @@
|
|||
"dependencies": {
|
||||
"@zip.js/zip.js": "patch:@zip.js/zip.js@npm%3A2.7.60#~/.yarn/patches/@zip.js-zip.js-npm-2.7.60-b6b814410b.patch",
|
||||
"luxon": "^3.6.1",
|
||||
"sax": "^1.4.1",
|
||||
"source-map-support": "^0.5.21"
|
||||
}
|
||||
}
|
||||
|
|
BIN
library/playground/sample.jpg
Normal file
BIN
library/playground/sample.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
|
@ -1,47 +1,87 @@
|
|||
import * as penpot from "../target/library/penpot.js";
|
||||
import { writeFile } from 'fs/promises';
|
||||
import { writeFile, readFile } from 'fs/promises';
|
||||
import { createWriteStream } from 'fs';
|
||||
import { Writable } from "stream";
|
||||
|
||||
console.log(penpot);
|
||||
// console.log(penpot);
|
||||
|
||||
(async function() {
|
||||
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});
|
||||
const context = penpot.createBuildContext();
|
||||
|
||||
{
|
||||
let result = await penpot.exportAsBytes(file)
|
||||
context.addFile({name: "Test File 1"});
|
||||
context.addPage({name: "Foo Page"})
|
||||
|
||||
// Add image media
|
||||
const buffer = await readFile("./playground/sample.jpg");
|
||||
const blob = new Blob([buffer], { type: 'image/jpeg' });
|
||||
|
||||
const mediaId = context.addFileMedia({
|
||||
name: "avatar.jpg",
|
||||
width: 512,
|
||||
height: 512
|
||||
}, blob);
|
||||
|
||||
// Add image color asset
|
||||
const assetColorId = context.addLibraryColor({
|
||||
name: "Avatar",
|
||||
opacity: 1,
|
||||
image: {
|
||||
...context.getMediaAsImage(mediaId),
|
||||
keepAspectRatio: true
|
||||
}
|
||||
});
|
||||
|
||||
const boardId = context.addBoard({
|
||||
name: "Foo Board",
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 500,
|
||||
height: 300,
|
||||
})
|
||||
|
||||
const fill = {
|
||||
fillColorRefId: assetColorId,
|
||||
fillColorRefFile: context.currentFileId,
|
||||
fillImage: {
|
||||
...context.getMediaAsImage(mediaId),
|
||||
keepAspectRatio: true
|
||||
}
|
||||
};
|
||||
|
||||
context.addRect({
|
||||
name: "Rect 1",
|
||||
x: 20,
|
||||
y: 20,
|
||||
width:100,
|
||||
height:200,
|
||||
fills: [fill]
|
||||
});
|
||||
|
||||
context.closeBoard();
|
||||
context.closeFile();
|
||||
}
|
||||
|
||||
{
|
||||
let result = await penpot.exportAsBytes(context)
|
||||
await writeFile("sample-sync.zip", result);
|
||||
}
|
||||
|
||||
{
|
||||
// Create a file stream to write the zip to
|
||||
const output = createWriteStream('sample-stream.zip');
|
||||
|
||||
// Wrap Node's stream in a WHATWG WritableStream
|
||||
const writable = Writable.toWeb(output);
|
||||
|
||||
await penpot.exportStream(file, writable);
|
||||
}
|
||||
// {
|
||||
// // Create a file stream to write the zip to
|
||||
// const output = createWriteStream('sample-stream.zip');
|
||||
// // Wrap Node's stream in a WHATWG WritableStream
|
||||
// const writable = Writable.toWeb(output);
|
||||
// await penpot.exportStream(context, writable);
|
||||
// }
|
||||
|
||||
})().catch((cause) => {
|
||||
console.log(cause);
|
||||
console.error(cause);
|
||||
|
||||
const innerCause = cause.cause;
|
||||
if (innerCause) {
|
||||
console.error("Inner cause:", innerCause);
|
||||
}
|
||||
process.exit(-1);
|
||||
}).finally(() => {
|
||||
process.exit(0);
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
:modules
|
||||
{:penpot
|
||||
{:exports {BuilderError lib.builder/BuilderError
|
||||
createFile lib.builder/create-file
|
||||
createBuildContext lib.builder/create-build-context
|
||||
exportAsBytes lib.export/export-bytes
|
||||
exportAsBlob lib.export/export-blob
|
||||
exportStream lib.export/export-stream
|
||||
|
@ -34,7 +34,7 @@
|
|||
:js-options
|
||||
{:entry-keys ["module" "browser" "main"]
|
||||
:export-conditions ["module" "import", "browser" "require" "default"]
|
||||
:js-provider :import
|
||||
;; :js-provider :import
|
||||
;; :external-index "target/library/dependencies.js"
|
||||
;; :external-index-format :esm
|
||||
}
|
||||
|
|
|
@ -55,201 +55,235 @@
|
|||
(defn- decode-params
|
||||
[params]
|
||||
(if (obj/plain-object? params)
|
||||
(json/->js params)
|
||||
(json/->clj params)
|
||||
params))
|
||||
|
||||
(defn- create-file-api
|
||||
[file]
|
||||
(let [state* (volatile! file)
|
||||
api (obj/reify {:name "File"}
|
||||
:id
|
||||
{:get #(dm/str (:id @state*))}
|
||||
(defn- get-current-page-id
|
||||
[state]
|
||||
(dm/str (get state ::fb/current-page-id)))
|
||||
|
||||
(defn- get-last-id
|
||||
[state]
|
||||
(dm/str (get state ::fb/last-id)))
|
||||
|
||||
(defn- create-builder-api
|
||||
[state]
|
||||
(obj/reify {:name "File"}
|
||||
:currentFileId
|
||||
{:get #(dm/str (get @state ::fb/current-file-id))}
|
||||
|
||||
:currentFrameId
|
||||
{:get #(dm/str (::fb/current-frame-id @state*))}
|
||||
{:get #(dm/str (get @state ::fb/current-frame-id))}
|
||||
|
||||
:currentPageId
|
||||
{:get #(dm/str (::fb/current-page-id @state*))}
|
||||
{:get #(get-current-page-id @state)}
|
||||
|
||||
:lastId
|
||||
{:get #(dm/str (::fb/last-id @state*))}
|
||||
{:get #(get-last-id @state)}
|
||||
|
||||
:addFile
|
||||
(fn [params]
|
||||
(try
|
||||
(let [params (-> params decode-params fb/decode-file)]
|
||||
(-> (swap! state fb/add-file params)
|
||||
(get ::fb/current-file-id)))
|
||||
(catch :default cause
|
||||
(handle-exception cause))))
|
||||
|
||||
:closeFile
|
||||
(fn []
|
||||
(swap! state fb/close-file)
|
||||
nil)
|
||||
|
||||
:addPage
|
||||
(fn [params]
|
||||
(try
|
||||
(let [params (-> params
|
||||
(decode-params)
|
||||
(let [params (-> (decode-params params)
|
||||
(fb/decode-page))]
|
||||
(vswap! state* fb/add-page params)
|
||||
(dm/str (::fb/current-page-id @state*)))
|
||||
|
||||
(-> (swap! state fb/add-page params)
|
||||
(get-current-page-id)))
|
||||
|
||||
(catch :default cause
|
||||
(handle-exception cause))))
|
||||
|
||||
:closePage
|
||||
(fn []
|
||||
(vswap! state* fb/close-page))
|
||||
(swap! state fb/close-page)
|
||||
nil)
|
||||
|
||||
:addArtboard
|
||||
:addBoard
|
||||
(fn [params]
|
||||
(try
|
||||
(let [params (-> params
|
||||
(json/->clj)
|
||||
(let [params (-> (decode-params params)
|
||||
(assoc :type :frame)
|
||||
(fb/decode-shape))]
|
||||
(vswap! state* fb/add-artboard params)
|
||||
(dm/str (::fb/last-id @state*)))
|
||||
(-> (swap! state fb/add-board params)
|
||||
(get-last-id)))
|
||||
(catch :default cause
|
||||
(handle-exception cause))))
|
||||
|
||||
:closeArtboard
|
||||
:closeBoard
|
||||
(fn []
|
||||
(vswap! state* fb/close-artboard))
|
||||
(swap! state fb/close-board)
|
||||
nil)
|
||||
|
||||
:addGroup
|
||||
(fn [params]
|
||||
(try
|
||||
(let [params (-> params
|
||||
(json/->clj)
|
||||
(let [params (-> (decode-params params)
|
||||
(assoc :type :group)
|
||||
(fb/decode-shape))]
|
||||
(vswap! state* fb/add-group params)
|
||||
(dm/str (::fb/last-id @state*)))
|
||||
(-> (swap! state fb/add-group params)
|
||||
(get-last-id)))
|
||||
(catch :default cause
|
||||
(handle-exception cause))))
|
||||
|
||||
:closeGroup
|
||||
(fn []
|
||||
(vswap! state* fb/close-group))
|
||||
(swap! state fb/close-group)
|
||||
nil)
|
||||
|
||||
:addBool
|
||||
(fn [params]
|
||||
(try
|
||||
(let [params (-> params
|
||||
(json/->clj)
|
||||
(let [params (-> (decode-params params)
|
||||
(fb/decode-add-bool))]
|
||||
(vswap! state* fb/add-bool params)
|
||||
(dm/str (::fb/last-id @state*)))
|
||||
(-> (swap! state fb/add-bool params)
|
||||
(get-last-id)))
|
||||
(catch :default cause
|
||||
(handle-exception cause))))
|
||||
|
||||
:addRect
|
||||
(fn [params]
|
||||
(try
|
||||
(let [params (-> params
|
||||
(json/->clj)
|
||||
(let [params (-> (decode-params params)
|
||||
(assoc :type :rect)
|
||||
(fb/decode-shape))]
|
||||
(vswap! state* fb/add-shape params)
|
||||
(dm/str (::fb/last-id @state*)))
|
||||
(-> (swap! state fb/add-shape params)
|
||||
(get-last-id)))
|
||||
(catch :default cause
|
||||
(handle-exception cause))))
|
||||
|
||||
:addCircle
|
||||
(fn [params]
|
||||
(try
|
||||
(let [params (-> params
|
||||
(json/->clj)
|
||||
(let [params (-> (decode-params params)
|
||||
(assoc :type :circle)
|
||||
(fb/decode-shape))]
|
||||
(vswap! state* fb/add-shape params)
|
||||
(dm/str (::fb/last-id @state*)))
|
||||
(-> (swap! state fb/add-shape params)
|
||||
(get-last-id)))
|
||||
(catch :default cause
|
||||
(handle-exception cause))))
|
||||
|
||||
:addPath
|
||||
(fn [params]
|
||||
(try
|
||||
(let [params (-> params
|
||||
(json/->clj)
|
||||
(let [params (-> (decode-params params)
|
||||
(assoc :type :path)
|
||||
(fb/decode-shape))]
|
||||
(vswap! state* fb/add-shape params)
|
||||
(dm/str (::fb/last-id @state*)))
|
||||
(-> (swap! state fb/add-shape params)
|
||||
(get-last-id)))
|
||||
(catch :default cause
|
||||
(handle-exception cause))))
|
||||
|
||||
:addText
|
||||
(fn [params]
|
||||
(try
|
||||
(let [params (-> params
|
||||
(json/->clj)
|
||||
(let [params (-> (decode-params params)
|
||||
(assoc :type :text)
|
||||
(fb/decode-shape))]
|
||||
(vswap! state* fb/add-shape params)
|
||||
(dm/str (::fb/last-id @state*)))
|
||||
(-> (swap! state fb/add-shape params)
|
||||
(get-last-id)))
|
||||
(catch :default cause
|
||||
(handle-exception cause))))
|
||||
|
||||
:addLibraryColor
|
||||
(fn [params]
|
||||
(try
|
||||
(let [params (-> params
|
||||
(json/->clj)
|
||||
(let [params (-> (decode-params params)
|
||||
(fb/decode-library-color)
|
||||
(d/without-nils))]
|
||||
(vswap! state* fb/add-library-color params)
|
||||
(dm/str (::fb/last-id @state*)))
|
||||
(-> (swap! state fb/add-library-color params)
|
||||
(get-last-id)))
|
||||
(catch :default cause
|
||||
(handle-exception cause))))
|
||||
|
||||
:addLibraryTypography
|
||||
(fn [params]
|
||||
(try
|
||||
(let [params (-> params
|
||||
(json/->clj)
|
||||
(let [params (-> (decode-params params)
|
||||
(fb/decode-library-typography)
|
||||
(d/without-nils))]
|
||||
(vswap! state* fb/add-library-typography params)
|
||||
(dm/str (::fb/last-id @state*)))
|
||||
(-> (swap! state fb/add-library-typography params)
|
||||
(get-last-id)))
|
||||
(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*)))
|
||||
(let [params (-> (decode-params params)
|
||||
(fb/decode-add-component))]
|
||||
(-> (swap! state fb/add-component params)
|
||||
(get-last-id)))
|
||||
(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*)))
|
||||
(let [params (-> (decode-params params)
|
||||
(fb/decode-add-component-instance))]
|
||||
(-> (swap! state fb/add-component-instance params)
|
||||
(get-last-id)))
|
||||
(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))))
|
||||
:addFileMedia
|
||||
(fn [params blob]
|
||||
|
||||
:toMap
|
||||
(when-not (instance? js/Blob blob)
|
||||
(throw (BuilderError. "validation"
|
||||
"invalid-media"
|
||||
"only Blob instance are soported")))
|
||||
(try
|
||||
(let [blob (fb/map->BlobWrapper
|
||||
{:size (.-size ^js blob)
|
||||
:mtype (.-type ^js blob)
|
||||
:blob blob})
|
||||
params
|
||||
(-> (decode-params params)
|
||||
(fb/decode-add-file-media))]
|
||||
|
||||
(-> (swap! state fb/add-file-media params blob)
|
||||
(get-last-id)))
|
||||
|
||||
(catch :default cause
|
||||
(handle-exception cause))))
|
||||
|
||||
:getMediaAsImage
|
||||
(fn [id]
|
||||
(let [id (uuid/parse id)]
|
||||
(when-let [fmedia (get-in @state [::fb/file-media id])]
|
||||
(let [image {:id (get fmedia :id)
|
||||
:width (get fmedia :width)
|
||||
:height (get fmedia :height)
|
||||
:name (get fmedia :name)
|
||||
:mtype (get fmedia :mtype)}]
|
||||
(json/->js (d/without-nils image))))))
|
||||
|
||||
:genId
|
||||
(fn []
|
||||
(-> @state*
|
||||
(d/without-qualified)
|
||||
(json/->js))))]
|
||||
(dm/str (uuid/next)))))
|
||||
|
||||
|
||||
(defn create-build-context
|
||||
"Create an empty builder state context."
|
||||
[]
|
||||
(let [state (atom {})
|
||||
api (create-builder-api state)]
|
||||
|
||||
(specify! api
|
||||
cljs.core/IDeref
|
||||
(-deref [_]
|
||||
(d/without-qualified @state*)))))
|
||||
|
||||
(defn create-file
|
||||
[params]
|
||||
(try
|
||||
(let [params (-> params json/->clj fb/decode-file)
|
||||
file (fb/create-file params)]
|
||||
(create-file-api file))
|
||||
(catch :default cause
|
||||
(handle-exception cause))))
|
||||
(-deref [_] @state))))
|
||||
|
|
|
@ -8,12 +8,10 @@
|
|||
"A .penpot export implementation"
|
||||
(: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.media :as media]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.util.object :as obj]
|
||||
[app.common.types.color :as types.color]
|
||||
[app.common.types.component :as types.component]
|
||||
[app.common.types.file :as types.file]
|
||||
|
@ -22,8 +20,8 @@
|
|||
[app.common.types.shape :as types.shape]
|
||||
[app.common.types.tokens-lib :as types.tokens-lib]
|
||||
[app.common.types.typography :as types.typography]
|
||||
[cuerdas.core :as str]
|
||||
[app.util.zip :as zip]
|
||||
[cuerdas.core :as str]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(def ^:private schema:file
|
||||
|
@ -43,9 +41,6 @@
|
|||
(def ^:private encode-component
|
||||
(sm/encoder types.component/schema:component sm/json-transformer))
|
||||
|
||||
;; (def encode-media
|
||||
;; (sm/encoder ::ctf/media sm/json-transformer))
|
||||
|
||||
(def encode-color
|
||||
(sm/encoder types.color/schema:color sm/json-transformer))
|
||||
|
||||
|
@ -68,7 +63,6 @@
|
|||
"file-data-fragment"
|
||||
"file-change"})
|
||||
|
||||
;; FIXME: move to types
|
||||
(def ^:private schema:storage-object
|
||||
[:map {:title "StorageObject"}
|
||||
[:id ::sm/uuid]
|
||||
|
@ -80,12 +74,6 @@
|
|||
(def encode-storage-object
|
||||
(sm/encoder schema:storage-object sm/json-transformer))
|
||||
|
||||
;; (def encode-file-thumbnail
|
||||
;; (sm/encoder schema:file-thumbnail sm/json-transformer))
|
||||
|
||||
|
||||
;; FIXME: naming
|
||||
|
||||
(def ^:private file-attrs
|
||||
#{:id
|
||||
:name
|
||||
|
@ -96,8 +84,6 @@
|
|||
|
||||
(defn- generate-file-export-procs
|
||||
[{:keys [id data] :as file}]
|
||||
;; (prn "generate-file-export-procs")
|
||||
;; (app.common.pprint/pprint file)
|
||||
(cons
|
||||
(let [file (cond-> (select-keys file file-attrs)
|
||||
(:options data)
|
||||
|
@ -108,11 +94,11 @@
|
|||
(concat
|
||||
(let [pages (get data :pages)
|
||||
pages-index (get data :pages-index)]
|
||||
|
||||
(->> (d/enumerate pages)
|
||||
(mapcat
|
||||
(fn [[index page-id]]
|
||||
(let [path (str "files/" id "/pages/" page-id ".json")
|
||||
page (get pages-index page-id)
|
||||
(let [page (get pages-index page-id)
|
||||
objects (:objects page)
|
||||
page (-> page
|
||||
(dissoc :objects)
|
||||
|
@ -155,45 +141,74 @@
|
|||
(when-let [tokens-lib (get data :tokens-lib)]
|
||||
(list [(str "files/" id "/tokens.json")
|
||||
(delay (-> tokens-lib
|
||||
(encode-tokens-lib tokens-lib)
|
||||
encode-tokens-lib
|
||||
json/encode))])))))
|
||||
|
||||
(defn generate-manifest-procs
|
||||
[file]
|
||||
(let [mdata {:id (:id file)
|
||||
(defn- generate-files-export-procs
|
||||
[state]
|
||||
(->> (vals (get state ::fb/files))
|
||||
(mapcat generate-file-export-procs)))
|
||||
|
||||
(defn- generate-media-export-procs
|
||||
[state]
|
||||
(->> (get state ::fb/file-media)
|
||||
(mapcat (fn [[file-media-id file-media]]
|
||||
(let [media-id (get file-media :media-id)
|
||||
media (get-in state [::fb/media media-id])
|
||||
blob (get-in state [::fb/blobs media-id])]
|
||||
(list
|
||||
[(str "objects/" media-id (media/mtype->extension (:content-type media)))
|
||||
(delay (get blob :blob))]
|
||||
|
||||
[(str "objects/" media-id ".json")
|
||||
(delay (-> media
|
||||
;; FIXME: proper encode?
|
||||
(json/encode)))]
|
||||
[(str "files/" (:file-id file-media) "/media/" file-media-id ".json")
|
||||
(delay (-> file-media
|
||||
(dissoc :file-id)
|
||||
(json/encode)))]))))))
|
||||
|
||||
(defn- generate-manifest-procs
|
||||
[state]
|
||||
(let [files (->> (get state ::fb/files)
|
||||
(mapv (fn [[file-id file]]
|
||||
{:id file-id
|
||||
:name (:name file)
|
||||
:features (:features file)}
|
||||
:features (:features file)})))
|
||||
params {:type "penpot/export-files"
|
||||
:version 1
|
||||
;; FIXME: set proper placeholder for replacement on build
|
||||
:generated-by "penpot-lib/develop"
|
||||
:files [mdata]
|
||||
:files files
|
||||
:relations []}]
|
||||
(list
|
||||
["manifest.json" (delay (json/encode params))])))
|
||||
["manifest.json" (delay (json/encode params))]))
|
||||
|
||||
(defn- export
|
||||
[file writer]
|
||||
(->> (p/reduce (fn [writer [path proc]]
|
||||
(let [data (deref proc)]
|
||||
[state writer]
|
||||
(->> (p/reduce (fn [writer [path data]]
|
||||
(let [data (if (delay? data) (deref data) data)]
|
||||
(js/console.log "export" path)
|
||||
(->> (zip/add writer path data)
|
||||
(p/fmap (constantly writer)))))
|
||||
|
||||
writer
|
||||
(cons (generate-manifest-procs @state)
|
||||
(concat
|
||||
(generate-manifest-procs @file)
|
||||
(generate-file-export-procs @file)))
|
||||
(generate-files-export-procs @state)
|
||||
(generate-media-export-procs @state))))
|
||||
|
||||
(p/mcat (fn [writer]
|
||||
(zip/close writer)))))
|
||||
|
||||
(defn export-bytes
|
||||
[file]
|
||||
(export file (zip/writer (zip/bytes-writer))))
|
||||
[state]
|
||||
(export state (zip/writer (zip/bytes-writer))))
|
||||
|
||||
(defn export-blob
|
||||
[file]
|
||||
(export file (zip/writer (zip/blob-writer))))
|
||||
[state]
|
||||
(export state (zip/writer (zip/blob-writer))))
|
||||
|
||||
(defn export-stream
|
||||
[file stream]
|
||||
(export file (zip/writer stream)))
|
||||
[state stream]
|
||||
(export state (zip/writer stream)))
|
||||
|
|
|
@ -59,7 +59,6 @@ __metadata:
|
|||
concurrently: "npm:^9.1.2"
|
||||
luxon: "npm:^3.6.1"
|
||||
nodemon: "npm:^3.1.9"
|
||||
sax: "npm:^1.4.1"
|
||||
shadow-cljs: "npm:3.0.5"
|
||||
source-map-support: "npm:^0.5.21"
|
||||
languageName: unknown
|
||||
|
@ -73,11 +72,11 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"@types/node@npm:^22.12.0":
|
||||
version: 22.15.17
|
||||
resolution: "@types/node@npm:22.15.17"
|
||||
version: 22.15.18
|
||||
resolution: "@types/node@npm:22.15.18"
|
||||
dependencies:
|
||||
undici-types: "npm:~6.21.0"
|
||||
checksum: 10c0/fb92aa10b628683c5b965749f955bc2322485ecb0ea6c2f4cae5f2c7537a16834607e67083a9e9281faaae8d7dee9ada8d6a5c0de9a52c17d82912ef00c0fdd4
|
||||
checksum: 10c0/e23178c568e2dc6b93b6aa3b8dfb45f9556e527918c947fe7406a4c92d2184c7396558912400c3b1b8d0fa952ec63819aca2b8e4d3545455fc6f1e9623e09ca6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -318,14 +317,14 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"debug@npm:4, debug@npm:^4, debug@npm:^4.3.4":
|
||||
version: 4.4.0
|
||||
resolution: "debug@npm:4.4.0"
|
||||
version: 4.4.1
|
||||
resolution: "debug@npm:4.4.1"
|
||||
dependencies:
|
||||
ms: "npm:^2.1.3"
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
checksum: 10c0/db94f1a182bf886f57b4755f85b3a74c39b5114b9377b7ab375dc2cfa3454f09490cc6c30f829df3fc8042bc8b8995f6567ce5cd96f3bc3688bd24027197d9de
|
||||
checksum: 10c0/d2b44bc1afd912b49bb7ebb0d50a860dc93a4dd7d946e8de94abc957bb63726b7dd5aa48c18c2386c379ec024c46692e15ed3ed97d481729f929201e671fcd55
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -962,13 +961,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"sax@npm:^1.4.1":
|
||||
version: 1.4.1
|
||||
resolution: "sax@npm:1.4.1"
|
||||
checksum: 10c0/6bf86318a254c5d898ede6bd3ded15daf68ae08a5495a2739564eb265cd13bcc64a07ab466fb204f67ce472bb534eb8612dac587435515169593f4fffa11de7c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"semver@npm:^7.3.5, semver@npm:^7.5.3":
|
||||
version: 7.7.2
|
||||
resolution: "semver@npm:7.7.2"
|
||||
|
|
|
@ -11,13 +11,14 @@
|
|||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"fmt:clj:check": "cljfmt check --parallel=true common/src/ common/test/ frontend/src/ frontend/test/ backend/src/ backend/test/ exporter/src/",
|
||||
"fmt:clj": "cljfmt fix --parallel=true common/src/ common/test/ frontend/src/ frontend/test/ backend/src/ backend/test/ exporter/src/",
|
||||
"fmt:clj:check": "cljfmt check --parallel=true common/src/ common/test/ frontend/src/ frontend/test/ backend/src/ backend/test/ exporter/src/ library/src",
|
||||
"fmt:clj": "cljfmt fix --parallel=true common/src/ common/test/ frontend/src/ frontend/test/ backend/src/ backend/test/ exporter/src/ library/src",
|
||||
"lint:clj:common": "clj-kondo --parallel=true --lint common/src",
|
||||
"lint:clj:frontend": "clj-kondo --parallel=true --lint frontend/src",
|
||||
"lint:clj:backend": "clj-kondo --parallel=true --lint backend/src",
|
||||
"lint:clj:exporter": "clj-kondo --parallel=true --lint exporter/src",
|
||||
"lint:clj": "yarn run lint:clj:common && yarn run lint:clj:frontend && yarn run lint:clj:backend && yarn run lint:clj:exporter"
|
||||
"lint:clj:library": "clj-kondo --parallel=true --lint library/src",
|
||||
"lint:clj": "yarn run lint:clj:common && yarn run lint:clj:frontend && yarn run lint:clj:backend && yarn run lint:clj:exporter && yarn run lint:clj:library"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.43.1",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue