penpot/library/src/lib/builder.cljs
2025-05-20 13:06:07 +02:00

289 lines
7.7 KiB
Clojure

;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns lib.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/->clj params)
params))
(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 (get @state ::fb/current-frame-id))}
:currentPageId
{:get #(get-current-page-id @state)}
:lastId
{: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 (-> (decode-params params)
(fb/decode-page))]
(-> (swap! state fb/add-page params)
(get-current-page-id)))
(catch :default cause
(handle-exception cause))))
:closePage
(fn []
(swap! state fb/close-page)
nil)
:addBoard
(fn [params]
(try
(let [params (-> (decode-params params)
(assoc :type :frame)
(fb/decode-shape))]
(-> (swap! state fb/add-board params)
(get-last-id)))
(catch :default cause
(handle-exception cause))))
:closeBoard
(fn []
(swap! state fb/close-board)
nil)
:addGroup
(fn [params]
(try
(let [params (-> (decode-params params)
(assoc :type :group)
(fb/decode-shape))]
(-> (swap! state fb/add-group params)
(get-last-id)))
(catch :default cause
(handle-exception cause))))
:closeGroup
(fn []
(swap! state fb/close-group)
nil)
:addBool
(fn [params]
(try
(let [params (-> (decode-params params)
(fb/decode-add-bool))]
(-> (swap! state fb/add-bool params)
(get-last-id)))
(catch :default cause
(handle-exception cause))))
:addRect
(fn [params]
(try
(let [params (-> (decode-params params)
(assoc :type :rect)
(fb/decode-shape))]
(-> (swap! state fb/add-shape params)
(get-last-id)))
(catch :default cause
(handle-exception cause))))
:addCircle
(fn [params]
(try
(let [params (-> (decode-params params)
(assoc :type :circle)
(fb/decode-shape))]
(-> (swap! state fb/add-shape params)
(get-last-id)))
(catch :default cause
(handle-exception cause))))
:addPath
(fn [params]
(try
(let [params (-> (decode-params params)
(assoc :type :path)
(fb/decode-shape))]
(-> (swap! state fb/add-shape params)
(get-last-id)))
(catch :default cause
(handle-exception cause))))
:addText
(fn [params]
(try
(let [params (-> (decode-params params)
(assoc :type :text)
(fb/decode-shape))]
(-> (swap! state fb/add-shape params)
(get-last-id)))
(catch :default cause
(handle-exception cause))))
:addLibraryColor
(fn [params]
(try
(let [params (-> (decode-params params)
(fb/decode-library-color)
(d/without-nils))]
(-> (swap! state fb/add-library-color params)
(get-last-id)))
(catch :default cause
(handle-exception cause))))
:addLibraryTypography
(fn [params]
(try
(let [params (-> (decode-params params)
(fb/decode-library-typography)
(d/without-nils))]
(-> (swap! state fb/add-library-typography params)
(get-last-id)))
(catch :default cause
(handle-exception cause))))
:addComponent
(fn [params]
(try
(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 (-> (decode-params params)
(fb/decode-add-component-instance))]
(-> (swap! state fb/add-component-instance params)
(get-last-id)))
(catch :default cause
(handle-exception cause))))
:addFileMedia
(fn [params blob]
(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 []
(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 [_] @state))))