diff --git a/CHANGES.md b/CHANGES.md index ae4ad9f1bc..2bacc91b47 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,36 @@ ### :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!) ### :sparkles: New features diff --git a/backend/src/app/rpc/commands/files_create.clj b/backend/src/app/rpc/commands/files_create.clj index eeafddd0ce..fc9dcb7fef 100644 --- a/backend/src/app/rpc/commands/files_create.clj +++ b/backend/src/app/rpc/commands/files_create.clj @@ -55,8 +55,8 @@ :features features :ignore-sync-until ignore-sync-until :modified-at modified-at - :deleted-at deleted-at - :create-page create-page + :deleted-at deleted-at} + {:create-page create-page :page-id page-id}) file (-> (bfc/insert-file! cfg file) (bfc/decode-row))] diff --git a/common/src/app/common/files/builder.cljc b/common/src/app/common/files/builder.cljc index b233267107..c62b098dda 100644 --- a/common/src/app/common/files/builder.cljc +++ b/common/src/app/common/files/builder.cljc @@ -5,86 +5,86 @@ ;; Copyright (c) KALEIDOS INC (ns app.common.files.builder + "Internal implementation of file builder. Mainly used as base impl + for penpot library" (:require [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.exceptions :as ex] + [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.pprint :as pp] [app.common.schema :as sm] [app.common.svg :as csvg] - [app.common.text :as txt] - [app.common.types.components-list :as ctkl] - [app.common.types.container :as ctn] - [app.common.types.file :as ctf] - [app.common.types.page :as ctp] - [app.common.types.pages-list :as ctpl] - [app.common.types.shape :as cts] + [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] [cuerdas.core :as str])) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; IMPL & HELPERS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + (def ^:private root-id uuid/zero) (def ^:private conjv (fnil conj [])) (def ^:private conjs (fnil conj #{})) +(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))) + (defn- commit-change - ([file change] - (commit-change file change nil)) + [file change & {:keys [add-container] + :or {add-container false}}] - ([file change {:keys [add-container? - fail-on-spec?] - :or {add-container? false - fail-on-spec? false}}] - (let [change (cond-> change - add-container? - (assoc :page-id (:current-page-id file) - :frame-id (:current-frame-id file))) - - valid? (or (and (nil? (:component-id change)) - (nil? (:page-id change))) - (ch/valid-change? change))] - - (when-not valid? - (let [explain (sm/explain ::ch/change change)] - (pp/pprint (sm/humanize-explain explain)) - (when fail-on-spec? - (ex/raise :type :assertion - :code :data-validation - :hint "invalid change" - ::sm/explain explain)))) - - (cond-> file - (and valid? (or (not add-container?) (some? (:component-id change)) (some? (:page-id change)))) - (-> (update :changes conjv change) - (update :data ch/process-changes [change] false)) - - (not valid?) - (update :errors conjv change))))) + (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] - (if (some? (:current-component-id file)) - (dm/get-in file [:data :components (:current-component-id file) :objects]) - (dm/get-in file [:data :pages-index (:current-page-id file) :objects])))) + [file] + (dm/get-in file [:data :pages-index (::current-page-id file) :objects])) -(defn lookup-shape [file shape-id] - (-> (lookup-objects file) - (get shape-id))) +(defn- commit-shape + [file shape] + (let [parent-id + (-> file ::parent-stack peek) -(defn- commit-shape [file obj] - (let [parent-id (-> file :parent-stack peek) - change {:type :add-obj - :id (:id obj) - :ignore-touched true - :obj obj - :parent-id parent-id} + frame-id + (::current-frame-id file) - fail-on-spec? (or (= :group (:type obj)) - (= :frame (:type obj)))] + page-id + (::current-page-id file) - (commit-change file change {:add-container? true :fail-on-spec? fail-on-spec?}))) + change + {:type :add-obj + :id (:id shape) + :ignore-touched true + :obj shape + :parent-id parent-id + :frame-id frame-id + :page-id page-id}] + + (-> file + (commit-change change) + (track-used-name (:name shape))))) (defn- generate-name [type data] @@ -96,24 +96,16 @@ :else (str tag)))) (str/capital (d/name type)))) -(defn- add-name - [file name] - (let [container-id (or (:current-component-id file) - (:current-page-id file))] - (-> file - (update-in [:unames container-id] conjs name)))) - (defn- unique-name [name file] - (let [container-id (or (:current-component-id file) - (:current-page-id file)) + (let [container-id (::current-page-id file) unames (dm/get-in file [:unames container-id])] (d/unique-name name (or unames #{})))) -(defn clear-names [file] - (dissoc file :unames)) +(defn- clear-names [file] + (dissoc file ::unames)) -(defn- check-name +(defn- assign-name "Given a tag returns its layer name" [data file type] @@ -124,592 +116,356 @@ :always (update :name unique-name file))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; SCHEMAS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(def decode-file + (sm/decode-fn types.file/schema:file sm/json-transformer)) + +(def decode-page + (sm/decode-fn types.page/schema:page sm/json-transformer)) + +(def decode-shape + (sm/decode-fn types.shape/schema:shape-attrs sm/json-transformer)) + +(def decode-library-color + (sm/decode-fn types.color/schema:color sm/json-transformer)) + +(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-instance + [:map + [:component-id ::sm/uuid] + [:x ::sm/safe-number] + [:y ::sm/safe-number]]) + +(def check-add-component-instance + (sm/check-fn schema:add-component-instance)) + +(def decode-add-component-instance + (sm/decode-fn schema:add-component-instance sm/json-transformer)) + +(def schema:add-bool + [:map + [:group-id ::sm/uuid] + [:type [::sm/one-of types.shape/bool-types]]]) + +(def decode-add-bool + (sm/decode-fn schema:add-bool sm/json-transformer)) + +(def check-add-bool + (sm/check-fn schema:add-bool)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; PUBLIC API +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn lookup-shape [file shape-id] + (-> (lookup-objects file) + (get shape-id))) + +(defn get-current-page + [file] + (let [page-id (::current-page-id file)] + (dm/get-in file [:data :pages-index page-id]))) (defn create-file - ([name] - (create-file (uuid/next) name)) - - ([id name] - (-> (ctf/make-file {:id id :name name :create-page false}) - (assoc :changes [])))) ;; We keep the changes so we can send them to the backend + [params] + (let [params (-> params + (assoc :features cfeat/default-features) + (assoc :migrations fmig/available-migrations))] + (types.file/make-file params :create-page false))) (defn add-page - [file data] - (dm/assert! (nil? (:current-component-id file))) - (let [page-id (or (:id data) (uuid/next)) - page (-> (ctp/make-empty-page {:id page-id :name "Page 1"}) - (d/deep-merge data))] + [file params] + (let [page (-> (types.page/make-empty-page params) + (types.page/check-page)) + change {:type :add-page + :page page}] + (-> file - (commit-change - {:type :add-page - :page page}) + (commit-change change) ;; Current page being edited - (assoc :current-page-id page-id) + (assoc ::current-page-id (:id page)) ;; Current frame-id - (assoc :current-frame-id root-id) + (assoc ::current-frame-id root-id) ;; Current parent stack we'll be nesting - (assoc :parent-stack [root-id]) + (assoc ::parent-stack [root-id]) ;; Last object id added - (assoc :last-id nil)))) + (assoc ::last-id nil)))) (defn close-page [file] - (dm/assert! (nil? (:current-component-id file))) (-> file - (dissoc :current-page-id) - (dissoc :parent-stack) - (dissoc :last-id) + (dissoc ::current-page-id) + (dissoc ::parent-stack) + (dissoc ::last-id) (clear-names))) -(defn add-artboard [file data] - (let [obj (-> (cts/setup-shape (assoc data :type :frame)) - (check-name file :frame))] - (-> file - (commit-shape obj) - (assoc :current-frame-id (:id obj)) - (assoc :last-id (:id obj)) - (add-name (:name obj)) - (update :parent-stack conjv (:id obj))))) +(defn add-artboard + [file data] + (let [{:keys [id] :as shape} + (-> data + (update :id default-uuid) + (assoc :type :frame) + (assign-name file :frame) + (types.shape/setup-shape) + (types.shape/check-shape))] -(defn close-artboard [file] - (let [parent-id (-> file :parent-stack peek) - parent (lookup-shape file parent-id) - current-frame-id (or (:frame-id parent) - root-id)] (-> file - (assoc :current-frame-id current-frame-id) - (update :parent-stack pop)))) + (commit-shape shape) + (update ::parent-stack conjv id) + (assoc ::current-frame-id id) + (assoc ::last-id id)))) -(defn add-group [file data] - (let [frame-id (:current-frame-id file) - obj (-> (cts/setup-shape (assoc data :type :group :frame-id frame-id)) - (check-name file :group))] +(defn close-artboard + [file] + (let [parent-id (-> file ::parent-stack peek) + parent (lookup-shape file parent-id)] (-> file - (commit-shape obj) - (assoc :last-id (:id obj)) - (add-name (:name obj)) - (update :parent-stack conjv (:id obj))))) + (assoc ::current-frame-id (or (:frame-id parent) root-id)) + (update ::parent-stack pop)))) -(defn close-group [file] +(defn add-group + [file params] + (let [{:keys [id] :as shape} + (-> params + (update :id default-uuid) + (assoc :type :group) + (assign-name file :group) + (types.shape/setup-shape) + (types.shape/check-shape))] + (-> file + (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) - children (->> group :shapes (mapv #(lookup-shape file %))) + children (->> (get group :shapes) + (into [] (keep (partial lookup-shape file))) + (not-empty))] - file - (cond - (empty? children) - (commit-change - file - {:type :del-obj - :ignore-touched true - :id group-id} - {:add-container? true}) + (assert (some? children) "group expect to have at least 1 children") - (:masked-group group) - (let [mask (first children)] - (commit-change - file - {:type :mod-obj - :id group-id - :operations - [{:type :set :attr :x :val (-> mask :selrect :x) :ignore-touched true} - {:type :set :attr :y :val (-> mask :selrect :y) :ignore-touched true} - {:type :set :attr :width :val (-> mask :selrect :width) :ignore-touched true} - {:type :set :attr :height :val (-> mask :selrect :height) :ignore-touched true} - {:type :set :attr :flip-x :val (-> mask :flip-x) :ignore-touched true} - {: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}]} - {:add-container? true})) + (let [file (if (:masked-group group) + (let [mask (first children) + change {:type :mod-obj + :id group-id + :operations + [{:type :set :attr :x :val (-> mask :selrect :x) :ignore-touched true} + {:type :set :attr :y :val (-> mask :selrect :y) :ignore-touched true} + {:type :set :attr :width :val (-> mask :selrect :width) :ignore-touched true} + {:type :set :attr :height :val (-> mask :selrect :height) :ignore-touched true} + {:type :set :attr :flip-x :val (-> mask :flip-x) :ignore-touched true} + {: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)) + (let [group (gsh/update-group-selrect group children) + change {:type :mod-obj + :id group-id + :operations + [{:type :set :attr :selrect :val (:selrect group) :ignore-touched true} + {:type :set :attr :points :val (:points group) :ignore-touched true} + {:type :set :attr :x :val (-> group :selrect :x) :ignore-touched true} + {:type :set :attr :y :val (-> group :selrect :y) :ignore-touched true} + {:type :set :attr :width :val (-> group :selrect :width) :ignore-touched true} + {:type :set :attr :height :val (-> group :selrect :height) :ignore-touched true}]}] - :else - (let [group' (gsh/update-group-selrect group children)] - (commit-change - file - {:type :mod-obj - :id group-id - :operations - [{:type :set :attr :selrect :val (:selrect group') :ignore-touched true} - {:type :set :attr :points :val (:points group') :ignore-touched true} - {:type :set :attr :x :val (-> group' :selrect :x) :ignore-touched true} - {:type :set :attr :y :val (-> group' :selrect :y) :ignore-touched true} - {: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)))) - {:add-container? true})))] +(defn add-bool + [file params] + (let [{:keys [group-id type]} + (check-add-bool params) - (-> file - (update :parent-stack pop)))) + group + (lookup-shape file group-id) -(defn add-bool [file data] - (let [frame-id (:current-frame-id file) - obj (-> (cts/setup-shape (assoc data :type :bool :frame-id frame-id)) - (check-name file :bool))] + children + (->> (get group :shapes) + (not-empty))] + + (assert (some? children) "expect group to have at least 1 element") + + (let [objects (lookup-objects file) + bool (-> group + (assoc :type :bool) + (gsh/update-bool objects)) + change {:type :mod-obj + :id (:id bool) + :operations + [{:type :set :attr :content :val (:content bool) :ignore-touched true} + {:type :set :attr :type :val :bool :ignore-touched true} + {:type :set :attr :bool-type :val type :ignore-touched true} + {:type :set :attr :selrect :val (:selrect bool) :ignore-touched true} + {:type :set :attr :points :val (:points bool) :ignore-touched true} + {:type :set :attr :x :val (-> bool :selrect :x) :ignore-touched true} + {:type :set :attr :y :val (-> bool :selrect :y) :ignore-touched true} + {:type :set :attr :width :val (-> bool :selrect :width) :ignore-touched true} + {:type :set :attr :height :val (-> bool :selrect :height) :ignore-touched true}]}] + + (-> file + (commit-change change :add-container true) + (assoc ::last-id group-id))))) + +(defn add-shape + [file params] + (let [obj (-> params + (d/update-when :svg-attrs csvg/attrs->props) + (types.shape/setup-shape) + (assign-name file :type))] (-> file (commit-shape obj) - (assoc :last-id (:id obj)) - (add-name (:name obj)) - (update :parent-stack conjv (:id obj))))) - -(defn close-bool [file] - (let [bool-id (-> file :parent-stack peek) - bool (lookup-shape file bool-id) - children (->> bool :shapes (mapv #(lookup-shape file %))) - - file - (cond - (empty? children) - (commit-change - file - {:type :del-obj - :ignore-touched true - :id bool-id} - {:add-container? true}) - - :else - (let [objects (lookup-objects file) - bool' (gsh/update-bool bool children objects)] - (commit-change - file - {:type :mod-obj - :id bool-id - :operations - [{:type :set :attr :content :val (:content bool') :ignore-touched true} - {:type :set :attr :selrect :val (:selrect bool') :ignore-touched true} - {:type :set :attr :points :val (:points bool') :ignore-touched true} - {:type :set :attr :x :val (-> bool' :selrect :x) :ignore-touched true} - {:type :set :attr :y :val (-> bool' :selrect :y) :ignore-touched true} - {:type :set :attr :width :val (-> bool' :selrect :width) :ignore-touched true} - {:type :set :attr :height :val (-> bool' :selrect :height) :ignore-touched true}]} - - {:add-container? true})))] - - (-> file - (update :parent-stack pop)))) - -(defn create-shape - [file type data] - (let [obj (-> (assoc data :type type) - (update :svg-attrs csvg/attrs->props) - (cts/setup-shape) - (check-name file :type))] - - (-> file - (commit-shape obj) - (assoc :last-id (:id obj)) - (add-name (:name obj))))) - -(defn create-rect [file data] - (create-shape file :rect data)) - -(defn create-circle [file data] - (create-shape file :circle data)) - -(defn create-path [file data] - (create-shape file :path data)) - -(defn- clean-text-content - "Clean the content data so it doesn't break the validation" - [content] - (letfn [(update-fill [fill] - (d/update-in-when fill [:fill-color-gradient :type] keyword))] - (txt/transform-nodes - (fn [node] - (d/update-when node :fills #(mapv update-fill %))) - content))) - -(defn create-text [file data] - (let [data (d/update-when data :content clean-text-content)] - (create-shape file :text data))) - -(defn create-image [file data] - (create-shape file :image data)) - -(declare close-svg-raw) - -(defn create-svg-raw [file data] - (let [file (as-> file $ - (create-shape $ :svg-raw data) - (update $ :parent-stack conjv (:last-id $))) - - create-child - (fn [file child] - (-> file - (create-svg-raw (assoc data - :id (uuid/next) - :content child)) - (close-svg-raw)))] - - ;; First :content is the the shape attribute, the other content is the - ;; XML children - (reduce create-child file (dm/get-in data [:content :content])))) - -(defn close-svg-raw [file] - (-> file - (update :parent-stack pop))) - -(defn- read-classifier - [interaction-src] - (select-keys interaction-src [:event-type :action-type])) - -(defmulti read-event-opts :event-type) - -(defmethod read-event-opts :after-delay - [interaction-src] - (select-keys interaction-src [:delay])) - -(defmethod read-event-opts :default - [_] - {}) - -(defmulti read-action-opts :action-type) - -(defmethod read-action-opts :navigate - [interaction-src] - (select-keys interaction-src [:destination - :preserve-scroll])) - -(defmethod read-action-opts :open-overlay - [interaction-src] - (select-keys interaction-src [:destination - :overlay-position - :overlay-pos-type - :close-click-outside - :background-overlay])) - -(defmethod read-action-opts :toggle-overlay - [interaction-src] - (select-keys interaction-src [:destination - :overlay-position - :overlay-pos-type - :close-click-outside - :background-overlay])) - -(defmethod read-action-opts :close-overlay - [interaction-src] - (select-keys interaction-src [:destination])) - -(defmethod read-action-opts :prev-screen - [_] - {}) - -(defmethod read-action-opts :open-url - [interaction-src] - (select-keys interaction-src [:url])) - -(defn add-interaction - [file from-id interaction-src] - - (let [shape (lookup-shape file from-id)] - (if (nil? shape) - file - (let [{:keys [event-type action-type]} (read-classifier interaction-src) - {:keys [delay]} (read-event-opts interaction-src) - {:keys [destination overlay-pos-type overlay-position url - close-click-outside background-overlay preserve-scroll]} - (read-action-opts interaction-src) - - interactions (-> shape - :interactions - (conjv - (d/without-nils {:event-type event-type - :action-type action-type - :delay delay - :destination destination - :overlay-pos-type overlay-pos-type - :overlay-position overlay-position - :url url - :close-click-outside close-click-outside - :background-overlay background-overlay - :preserve-scroll preserve-scroll})))] - (commit-change - file - {:type :mod-obj - :page-id (:current-page-id file) - :id from-id - - :operations - [{:type :set :attr :interactions :val interactions :ignore-touched true}]}))))) - -(defn generate-changes - [file] - (:changes file)) + (assoc ::last-id (:id obj))))) (defn add-library-color [file color] - (let [id (or (:id color) (uuid/next))] + (let [color (-> color + (update :id default-uuid) + (types.color/check-library-color color)) + change {:type :add-color + :color color}] (-> file - (commit-change - {:type :add-color - :color (assoc color :id id)}) - (assoc :last-id id)))) - -(defn update-library-color - [file color] - (let [id (uuid/uuid (:id color))] - (-> file - (commit-change - {:type :mod-color - :color (assoc color :id id)}) - (assoc :last-id (:id color))))) - -(defn delete-library-color - [file color-id] - (let [id (uuid/uuid color-id)] - (-> file - (commit-change - {:type :del-color - :id id})))) + (commit-change change) + (assoc ::last-id (:id color))))) (defn add-library-typography [file typography] - (let [id (or (:id typography) (uuid/next))] + (let [typography (-> typography + (update :id default-uuid) + (d/without-nils)) + change {:type :add-typography + :id (:id typography) + :typography typography}] (-> file - (commit-change - {:type :add-typography - :id id - :typography (assoc typography :id id)}) - (assoc :last-id id)))) + (commit-change change) + (assoc ::last-id (:id typography))))) -(defn delete-library-typography - [file typography-id] - (let [id (uuid/uuid typography-id)] +(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)} + + 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 - {:type :del-typography - :id id})))) + (commit-change change1) + (commit-change change2) + (assoc ::last-id comp-id) + (assoc ::current-frame-id comp-id)))) -(defn add-library-media - [file media] - (let [id (or (:id media) (uuid/next))] - (-> file - (commit-change - {:type :add-media - :object (assoc media :id id)}) - (assoc :last-id id)))) +(defn add-component-instance + [{:keys [id data] :as file} params] -(defn delete-library-media - [file media-id] - (let [id (uuid/uuid media-id)] - (-> file - (commit-change - {:type :del-media - :id id})))) + (let [{:keys [component-id x y]} + (check-add-component-instance params) -(defn start-component - ([file data] - (start-component file data :frame)) + component + (types.components-list/get-component data component-id) - ([file data root-type] - (let [name (:name data) - path (:path data) - main-instance-id (:main-instance-id data) - main-instance-page (:main-instance-page data) + page-id + (get file ::current-page-id)] - obj-id (or (:id data) (uuid/next))] + (assert (uuid? page-id) "page-id is expected to be set") + (assert (uuid? component) "component is expected to exist") - (-> file - (commit-change - {:type :add-component - :id obj-id - :name name - :path path - :main-instance-id main-instance-id - :main-instance-page main-instance-page}) + ;; 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) - (assoc :last-id obj-id) - (assoc :parent-stack [obj-id]) - (assoc :current-component-id obj-id) - (assoc :current-frame-id (if (= root-type :frame) obj-id uuid/zero)))))) + [shape shapes] + (types.container/make-component-instance page component id pos) -(defn start-deleted-component - [file data] - (let [attrs (-> data - (assoc :id (:main-instance-id data)) - (assoc :component-file (:id file)) - (assoc :component-id (:id data)) - (assoc :x (:main-instance-x data)) - (assoc :y (:main-instance-y data)) - (dissoc :path) - (dissoc :main-instance-id) - (dissoc :main-instance-page) - (dissoc :main-instance-x) - (dissoc :main-instance-y) - (dissoc :main-instance-parent) - (dissoc :main-instance-frame))] - ;; To create a deleted component, first we add all shapes of the main instance - ;; in the main instance page, and in the finish event we delete it. - (-> file - (update :parent-stack conjv (:main-instance-parent data)) - (assoc :current-page-id (:main-instance-page data)) - (assoc :current-frame-id (:main-instance-frame data)) - (add-artboard attrs)))) + 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)] -(defn finish-component - [file] - (let [component-id (:current-component-id file) - component-data (ctkl/get-component (:data file) component-id) + (assoc file ::last-id (:id shape))))) - component (lookup-shape file component-id) - children (->> component :shapes (mapv #(lookup-shape file %))) - - file - (cond - component-data - (update file :data - (fn [data] - (ctkl/update-component data component-id dissoc :objects))) - - (empty? children) - (commit-change - file - {:type :del-component - :id component-id - :skip-undelete? true}) - - (:masked-group component) - (let [mask (first children)] - (commit-change - file - {:type :mod-obj - :id component-id - :operations - [{:type :set :attr :x :val (-> mask :selrect :x) :ignore-touched true} - {:type :set :attr :y :val (-> mask :selrect :y) :ignore-touched true} - {:type :set :attr :width :val (-> mask :selrect :width) :ignore-touched true} - {:type :set :attr :height :val (-> mask :selrect :height) :ignore-touched true} - {:type :set :attr :flip-x :val (-> mask :flip-x) :ignore-touched true} - {: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}]} - - {:add-container? true})) - - (= (:type component) :group) - (let [component' (gsh/update-group-selrect component children)] - (commit-change - file - {:type :mod-obj - :id component-id - :operations - [{:type :set :attr :selrect :val (:selrect component') :ignore-touched true} - {:type :set :attr :points :val (:points component') :ignore-touched true} - {:type :set :attr :x :val (-> component' :selrect :x) :ignore-touched true} - {:type :set :attr :y :val (-> component' :selrect :y) :ignore-touched true} - {:type :set :attr :width :val (-> component' :selrect :width) :ignore-touched true} - {:type :set :attr :height :val (-> component' :selrect :height) :ignore-touched true}]} - {:add-container? true})) - - :else file)] - - (-> file - (dissoc :current-component-id) - (dissoc :current-frame-id) - (update :parent-stack pop)))) - -(defn finish-deleted-component - [component-id file] - (let [file (assoc file :current-component-id component-id) - component (ctkl/get-component (:data file) component-id)] - (-> file - (close-artboard) - (commit-change {:type :del-component - :id component-id}) - (commit-change {:type :del-obj - :page-id (:main-instance-page component) - :id (:main-instance-id component) - :ignore-touched true}) - (dissoc :current-page-id)))) - -(defn create-component-instance - [file data] - (let [component-id (uuid/uuid (:component-id data)) - x (:x data) - y (:y data) - file (assoc file :current-component-id component-id) - page-id (:current-page-id file) - page (ctpl/get-page (:data file) page-id) - component (ctkl/get-component (:data file) component-id) - - [shape shapes] - (ctn/make-component-instance page - component - (:id file) - (gpt/point x - y))] - - (as-> 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}) - $ - shapes) - - (assoc $ :last-id (:id shape)) - (dissoc $ :current-component-id)))) - -(defn delete-object +(defn delete-shape [file id] - (let [page-id (:current-page-id file)] - (commit-change - file - {:type :del-obj - :page-id page-id - :ignore-touched true - :id id}))) + (commit-change + file + {:type :del-obj + :page-id (::current-page-id file) + :ignore-touched true + :id id})) + +(defn update-shape + [file shape-id f] + (let [page-id (::current-page-id file) + objects (lookup-objects file) + old-shape (get objects shape-id) + new-shape (f old-shape) + attrs (d/concat-set + (keys old-shape) + (keys new-shape)) -(defn update-object - [file old-obj new-obj] - (let [page-id (:current-page-id file) - new-obj (cts/setup-shape new-obj) - attrs (d/concat-set (keys old-obj) (keys new-obj)) generate-operation (fn [changes attr] - (let [old-val (get old-obj attr) - new-val (get new-obj attr)] + (let [old-val (get old-shape attr) + new-val (get new-shape attr)] (if (= old-val new-val) changes (conj changes {:type :set :attr attr :val new-val :ignore-touched true}))))] + (-> file (commit-change {:type :mod-obj :operations (reduce generate-operation [] attrs) :page-id page-id - :id (:id old-obj)}) - (assoc :last-id (:id old-obj))))) - -(defn get-current-page - [file] - (let [page-id (:current-page-id file)] - (dm/get-in file [:data :pages-index page-id]))) + :id (:id old-shape)}) + (assoc ::last-id shape-id)))) (defn add-guide [file guide] (let [guide (cond-> guide (nil? (:id guide)) (assoc :id (uuid/next))) - page-id (:current-page-id file)] + page-id (::current-page-id file)] (-> file (commit-change {:type :set-guide :page-id page-id :id (:id guide) :params guide}) - (assoc :last-id (:id guide))))) + (assoc ::last-id (:id guide))))) (defn delete-guide [file id] - (let [page-id (:current-page-id file)] + (let [page-id (::current-page-id file)] (commit-change file {:type :set-guide :page-id page-id @@ -718,7 +474,7 @@ (defn update-guide [file guide] - (let [page-id (:current-page-id file)] + (let [page-id (::current-page-id file)] (commit-change file {:type :set-guide :page-id page-id diff --git a/common/src/app/common/files/changes.cljc b/common/src/app/common/files/changes.cljc index 61b08e6b00..23dda4ac17 100644 --- a/common/src/app/common/files/changes.cljc +++ b/common/src/app/common/files/changes.cljc @@ -732,20 +732,22 @@ (update-group [group objects] (let [lookup (d/getf objects) - children (->> group :shapes (map lookup))] + children (get group :shapes)] (cond ;; If the group is empty we don't make any changes. Will be removed by a later process (empty? children) group (= :bool (:type group)) - (gsh/update-bool group children objects) + (gsh/update-bool group objects) (:masked-group group) - (set-mask-selrect group children) + (->> (map lookup children) + (set-mask-selrect group)) :else - (gsh/update-group-selrect group children))))] + (->> (map lookup children) + (gsh/update-group-selrect group)))))] (if page-id (d/update-in-when data [:pages-index page-id :objects] reg-objects) diff --git a/common/src/app/common/files/changes_builder.cljc b/common/src/app/common/files/changes_builder.cljc index 9028c04d97..af9a1379ae 100644 --- a/common/src/app/common/files/changes_builder.cljc +++ b/common/src/app/common/files/changes_builder.cljc @@ -660,9 +660,13 @@ nil ;; so it does not need resize (= (:type parent) :bool) - (gsh/update-bool parent children objects) + (gsh/update-bool parent objects) (= (: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) (gsh/update-mask-selrect parent children) (gsh/update-group-selrect parent children)))] diff --git a/common/src/app/common/geom/shapes/transforms.cljc b/common/src/app/common/geom/shapes/transforms.cljc index 46031a54a5..01dd329dda 100644 --- a/common/src/app/common/geom/shapes/transforms.cljc +++ b/common/src/app/common/geom/shapes/transforms.cljc @@ -455,12 +455,12 @@ (defn update-bool "Calculates the selrect+points for the boolean shape" - [shape _children objects] - + [shape objects] (let [content (path/calc-bool-content shape objects) shape (assoc shape :content content)] (path/update-geometry shape))) +;; FIXME: revisit (defn update-shapes-geometry [objects ids] (->> ids @@ -474,7 +474,7 @@ (update-mask-selrect shape children) (cfh/bool-shape? shape) - (update-bool shape children objects) + (update-bool shape objects) (cfh/group-shape? shape) (update-group-selrect shape children) diff --git a/common/src/app/common/test_helpers/files.cljc b/common/src/app/common/test_helpers/files.cljc index d7c18dcd93..5420a0c377 100644 --- a/common/src/app/common/test_helpers/files.cljc +++ b/common/src/app/common/test_helpers/files.cljc @@ -23,28 +23,32 @@ (defn sample-file [label & {:keys [page-label name view-only?] :as params}] - (binding [ffeat/*current* #{"components/v2"}] - (let [params (cond-> params - label - (assoc :id (thi/new-id! label)) + (let [params + (cond-> params + label + (assoc :id (thi/new-id! label)) - page-label - (assoc :page-id (thi/new-id! page-label)) + (nil? name) + (assoc :name "Test file") - (nil? name) - (assoc :name "Test file")) + :always + (assoc :features ffeat/default-features)) - file (-> (ctf/make-file (dissoc params :page-label)) - (assoc :features #{"components/v2"}) - (assoc :permissions {:can-edit (not (true? view-only?))})) + opts + (cond-> {} + page-label + (assoc :page-id (thi/new-id! page-label))) - page (-> file - :data - (ctpl/pages-seq) - (first))] + file (-> (ctf/make-file params opts) + (assoc :permissions {:can-edit (not (true? view-only?))})) - (with-meta file - {:current-page-id (:id page)})))) + page (-> file + :data + (ctpl/pages-seq) + (first))] + + (with-meta file + {:current-page-id (:id page)}))) (defn validate-file! ([file] (validate-file! file {})) diff --git a/common/src/app/common/types/file.cljc b/common/src/app/common/types/file.cljc index 93e41924d6..e4ee9ceb9e 100644 --- a/common/src/app/common/types/file.cljc +++ b/common/src/app/common/types/file.cljc @@ -137,33 +137,36 @@ (update :options assoc :components-v2 true))))) (defn make-file - [{:keys [id project-id name revn is-shared features - ignore-sync-until modified-at deleted-at - create-page page-id] - :or {is-shared false revn 0 create-page true}}] + [{:keys [id project-id name revn is-shared features migrations + ignore-sync-until modified-at deleted-at] + :or {is-shared false revn 0}} + + & {:keys [create-page page-id] + :or {create-page true}}] (let [id (or id (uuid/next)) - data (if create-page (if page-id (make-file-data id page-id) (make-file-data id)) (make-file-data id nil)) - file {:id id - :project-id project-id - :name name - :revn revn - :vern 0 - :is-shared is-shared - :version version - :data data - :features features - :ignore-sync-until ignore-sync-until - :modified-at modified-at - :deleted-at deleted-at}] + file (d/without-nils + {:id id + :project-id project-id + :name name + :revn revn + :vern 0 + :is-shared is-shared + :version version + :data data + :features features + :migrations migrations + :ignore-sync-until ignore-sync-until + :modified-at modified-at + :deleted-at deleted-at})] - (d/without-nils file))) + (check-file file))) ;; Helpers diff --git a/frontend/shadow-cljs.edn b/frontend/shadow-cljs.edn index 54a0c98e7d..2cd8a48b86 100644 --- a/frontend/shadow-cljs.edn +++ b/frontend/shadow-cljs.edn @@ -149,13 +149,16 @@ {:test {:init-fn frontend-tests.runner/init :prepend-js ";if (typeof globalThis.navigator?.userAgent === 'undefined') { globalThis.navigator = {userAgent: ''}; };"}}} - :lib-penpot + :library {:target :esm - :output-dir "resources/public/libs" + :runtime :custom + :output-dir "target/library" + :devtools {:autoload false} :modules - {:penpot {:exports {:renderPage app.libs.render/render-page-export - :createFile app.libs.file-builder/create-file-export}}} + {:penpot + {:exports {BuilderError lib.file-builder/BuilderError + createFile lib.file-builder/create-file}}} :compiler-options {:output-feature-set :es2020 @@ -165,6 +168,8 @@ :release {:compiler-options {:fn-invoke-direct true + :optimizations #shadow/env ["PENPOT_BUILD_OPTIMIZATIONS" :as :keyword :default :advanced] + :pretty-print false :source-map true :elide-asserts true :anon-fn-naming-policy :off diff --git a/frontend/src/app/libs/file_builder.cljs b/frontend/src/app/libs/file_builder.cljs deleted file mode 100644 index 36e22caef7..0000000000 --- a/frontend/src/app/libs/file_builder.cljs +++ /dev/null @@ -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}) diff --git a/frontend/src/app/libs/render.cljs b/frontend/src/app/libs/render.cljs deleted file mode 100644 index 7ece057c01..0000000000 --- a/frontend/src/app/libs/render.cljs +++ /dev/null @@ -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}) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 9221356ece..48b67fe398 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -771,12 +771,12 @@ ;; --- 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 [id attrs] (assert (uuid? id) "expected valid uuid for `id`") - - (let [attrs (cts/check-shape-attrs attrs)] + (let [attrs (cts/check-shape-generic-attrs attrs)] (ptk/reify ::update-shape ptk/WatchEvent (watch [_ _ _] diff --git a/frontend/src/app/main/data/workspace/bool.cljs b/frontend/src/app/main/data/workspace/bool.cljs index 858239b75d..e91e66ca6a 100644 --- a/frontend/src/app/main/data/workspace/bool.cljs +++ b/frontend/src/app/main/data/workspace/bool.cljs @@ -12,7 +12,6 @@ [app.common.geom.shapes :as gsh] [app.common.types.component :as ctc] [app.common.types.container :as ctn] - [app.common.types.path :as path] [app.common.types.path.bool :as bool] [app.common.types.shape :as cts] [app.common.types.shape.layout :as ctl] @@ -30,9 +29,6 @@ (let [shape-id (or id (uuid/next)) - shapes - (mapv #(path/convert-to-path % objects) shapes) - head (if (= type :difference) (first shapes) (last shapes)) @@ -48,13 +44,13 @@ :frame-id (:frame-id head) :parent-id (:parent-id head) :name name - :shapes (mapv :id shapes)} + :shapes (vec shapes)} shape (-> shape (merge (select-keys head bool/style-properties)) (cts/setup-shape) - (gsh/update-bool shapes objects))] + (gsh/update-bool objects))] [shape (cph/get-position-on-parent objects (:id head))])) @@ -108,19 +104,16 @@ (defn group->bool [type group objects] (let [shapes (->> (:shapes group) - (map #(get objects %)) - (mapv #(path/convert-to-path % objects))) + (map (d/getf objects))) head (if (= type :difference) (first shapes) (last shapes)) head (cond-> head (and (contains? head :svg-attrs) (empty? (:fills head))) - (assoc :fills bool/default-fills)) - head-data (select-keys head bool/style-properties)] - + (assoc :fills bool/default-fills))] (-> group (assoc :type :bool) (assoc :bool-type type) - (merge head-data) - (gsh/update-bool shapes objects)))) + (merge (select-keys head bool/style-properties)) + (gsh/update-bool objects)))) (defn group-to-bool [shape-id type] diff --git a/frontend/src/app/util/object.cljc b/frontend/src/app/util/object.cljc index 072d2a5f91..d7404b8702 100644 --- a/frontend/src/app/util/object.cljc +++ b/frontend/src/app/util/object.cljc @@ -6,7 +6,7 @@ (ns app.util.object "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])) (:require [clojure.core :as c])) diff --git a/frontend/src/lib/file_builder.cljs b/frontend/src/lib/file_builder.cljs new file mode 100644 index 0000000000..cf09d7ee23 --- /dev/null +++ b/frontend/src/lib/file_builder.cljs @@ -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)))) diff --git a/frontend/src/lib/playground/sample1.js b/frontend/src/lib/playground/sample1.js new file mode 100644 index 0000000000..8dd7c87992 --- /dev/null +++ b/frontend/src/lib/playground/sample1.js @@ -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); diff --git a/frontend/test/frontend_tests/plugins/context_shapes_test.cljs b/frontend/test/frontend_tests/plugins/context_shapes_test.cljs index 6afbf63bdc..590162203f 100644 --- a/frontend/test/frontend_tests/plugins/context_shapes_test.cljs +++ b/frontend/test/frontend_tests/plugins/context_shapes_test.cljs @@ -16,8 +16,7 @@ (t/deftest test-common-shape-properties (let [;; ==== Setup - store (ths/setup-store - (cthf/sample-file :file1 :page-label :page1)) + store (ths/setup-store (cthf/sample-file :file1 :page-label :page1)) ^js context (api/create-context "TEST") diff --git a/frontend/test/frontend_tests/util_snap_data_test.cljs b/frontend/test/frontend_tests/util_snap_data_test.cljs index d988c4e9a0..f46fba7d6a 100644 --- a/frontend/test/frontend_tests/util_snap_data_test.cljs +++ b/frontend/test/frontend_tests/util_snap_data_test.cljs @@ -13,13 +13,21 @@ [cljs.pprint :refer [pprint]] [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/testing "Create empty data" (let [data (sd/make-snap-data)] (t/is (some? data)))) (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/get-current-page)) @@ -28,10 +36,11 @@ (t/is (some? data)))) (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/create-rect - {:x 0 + (fb/add-shape + {:type :rect + :x 0 :y 0 :width 100 :height 100})) @@ -57,7 +66,7 @@ (t/is (= (first (nth result-x 2)) 100)))) (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-artboard {:x 0 @@ -66,10 +75,10 @@ :height 100}) (fb/close-artboard)) - frame-id (:last-id file) + frame-id (::fb/last-id file) page (fb/get-current-page file) - ;; frame-id (:last-id file) + ;; frame-id (::fb/last-id file) data (-> (sd/make-snap-data) (sd/add-page page)) @@ -81,47 +90,49 @@ (t/is (= (count result-frame-x) 3)))) (t/testing "Add page with some shapes inside frames" - (let [file (-> (fb/create-file "Test") - (fb/add-page {:name "Page 1"}) - (fb/add-artboard - {:x 0 - :y 0 - :width 100 - :height 100})) - frame-id (:last-id file) + (with-redefs [uuid/next (get-mocked-uuid)] + (let [file (-> (fb/create-file {:name "Test"}) + (fb/add-page {:name "Page 1"}) + (fb/add-artboard + {:x 0 + :y 0 + :width 100 + :height 100})) - file (-> file - (fb/create-rect - {:x 25 - :y 25 - :width 50 - :height 50}) - (fb/close-artboard)) + frame-id (::fb/last-id file) - 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) - data (-> (sd/make-snap-data) - (sd/add-page page)) + page (fb/get-current-page file) - result-zero-x (sd/query data (:id page) uuid/zero :x [0 100]) - result-frame-x (sd/query data (:id page) frame-id :x [0 100])] + data (-> (sd/make-snap-data) + (sd/add-page page)) - (t/is (some? data)) - (t/is (= (count result-zero-x) 3)) - (t/is (= (count result-frame-x) 5)))) + result-zero-x (sd/query data (:id page) uuid/zero :x [0 100]) + result-frame-x (sd/query data (:id page) frame-id :x [0 100])] + + (t/is (some? data)) + (t/is (= (count result-zero-x) 3)) + (t/is (= (count result-frame-x) 5))))) (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-guide {:position 50 :axis :x}) (fb/add-artboard {:x 200 :y 200 :width 100 :height 100}) (fb/close-artboard)) - frame-id (:last-id file) + frame-id (::fb/last-id file) page (fb/get-current-page file) - ;; frame-id (:last-id file) + ;; frame-id (::fb/last-id file) data (-> (sd/make-snap-data) (sd/add-page page)) @@ -140,26 +151,26 @@ (t/is (= (count result-frame-y) 0)))) (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-artboard {:x 200 :y 200 :width 100 :height 100}) (fb/close-artboard)) - frame-id (:last-id file) + frame-id (::fb/last-id file) file (-> file (fb/add-guide {:position 50 :axis :x :frame-id frame-id})) page (fb/get-current-page file) - ;; frame-id (:last-id file) - data (-> (sd/make-snap-data) - (sd/add-page page)) + data (-> (sd/make-snap-data) + (sd/add-page page)) 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-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])] + (t/is (some? data)) ;; We can snap in the root (t/is (= (count result-zero-x) 0)) @@ -171,7 +182,7 @@ (t/deftest test-update-index (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-artboard {:x 0 @@ -180,15 +191,15 @@ :height 100}) (fb/close-artboard)) - shape-id (:last-id file) + shape-id (::fb/last-id file) page (fb/get-current-page file) - ;; frame-id (:last-id file) + ;; frame-id (::fb/last-id file) data (-> (sd/make-snap-data) (sd/add-page page)) file (-> file - (fb/delete-object shape-id)) + (fb/delete-shape shape-id)) new-page (fb/get-current-page file) data (sd/update-page data page new-page) @@ -201,22 +212,23 @@ (t/is (= (count result-y) 0)))) (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/create-rect - {:x 0 + (fb/add-shape + {:type :rect + :x 0 :y 0 :width 100 :height 100})) - shape-id (:last-id file) + shape-id (::fb/last-id file) page (fb/get-current-page file) - ;; frame-id (:last-id file) + ;; frame-id (::fb/last-id file) data (-> (sd/make-snap-data) (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) data (sd/update-page data page new-page) @@ -229,17 +241,17 @@ (t/is (= (count result-y) 0)))) (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-artboard {:x 0 :y 0 :width 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}) - shape-id (:last-id file) + file (fb/add-shape file {:type :rect :x 25 :y 25 :width 50 :height 50}) + shape-id (::fb/last-id file) file (fb/close-artboard file) @@ -247,7 +259,7 @@ data (-> (sd/make-snap-data) (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) data (sd/update-page data page new-page) @@ -260,16 +272,16 @@ (t/is (= (count result-frame-x) 3)))) (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-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}) (fb/close-artboard)) - frame-id (:last-id file) + frame-id (::fb/last-id file) page (fb/get-current-page file) data (-> (sd/make-snap-data) (sd/add-page page)) @@ -293,14 +305,14 @@ (t/is (= (count result-frame-y) 0)))) (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-artboard {:x 200 :y 200 :width 100 :height 100}) (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}) - guide-id (:last-id file) + guide-id (::fb/last-id file) page (fb/get-current-page file) data (-> (sd/make-snap-data) (sd/add-page page)) @@ -324,7 +336,7 @@ (t/is (= (count result-frame-y) 0)))) (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-artboard {:x 0 @@ -333,17 +345,18 @@ :height 100}) (fb/close-artboard)) - frame-id (:last-id file) + frame-id (::fb/last-id file) page (fb/get-current-page file) data (-> (sd/make-snap-data) (sd/add-page page)) - frame (fb/lookup-shape file frame-id) - new-frame (-> frame - (dissoc :selrect :points) - (assoc :x 200 :y 200) - (cts/setup-shape)) + file (fb/update-shape file frame-id + (fn [shape] + (-> shape + (dissoc :selrect :points) + (assoc :x 200 :y 200) + (cts/setup-shape)))) + - file (fb/update-object file frame new-frame) new-page (fb/get-current-page file) data (sd/update-page data page new-page) @@ -360,27 +373,30 @@ (t/is (= (count result-frame-x-2) 3)))) (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/create-rect - {:x 0 + (fb/add-shape + {:type :rect + :x 0 :y 0 :width 100 :height 100})) - shape-id (:last-id file) + shape-id (::fb/last-id 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) - new-shape (-> shape - (dissoc :selrect :points) - (assoc :x 200 :y 200)) + file (fb/update-shape file shape-id + (fn [shape] + (-> shape + (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) - - data (sd/update-page data page new-page) + ;; FIXME: update + 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-2 (sd/query data (:id page) uuid/zero :x [200 300])] @@ -391,17 +407,17 @@ (t/testing "Update global guide" (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-guide guide)) - guide-id (:last-id file) + guide-id (::fb/last-id file) guide (assoc guide :id guide-id) file (-> (fb/add-artboard file {:x 500 :y 500 :width 100 :height 100}) (fb/close-artboard)) - frame-id (:last-id file) + frame-id (::fb/last-id file) page (fb/get-current-page file) data (-> (sd/make-snap-data) (sd/add-page page))