penpot/frontend/src/app/main/data/workspace.cljs
Dream 0c08dfb13d
Add the ability for save and restore selection state in undo/redo (#8652)
*  Capture selection state before changes are applied

Save current selection IDs in commit-changes so undo entries
can track what was selected before each action.

*  Save and restore selection state in undo/redo

Extend undo entry with selected-before and selected-after fields.
On undo, restore selection to what it was before the action.
On redo, restore selection to what it was after the action.
Handles single entries, stacked entries, accumulated transactions,
and undo groups.

Fixes #6007

* ♻️ Wire selected-before through workspace undo stream

Pass the captured selection state from commit data into
the undo entry so it is stored alongside changes.

* 🐛 Fix unmatched delimiter in changes.cljs

* 🐛 Pass selected-before through commit event to undo entry

selected-before was captured in commit-changes but dropped by the
commit function since it was missing from the destructuring and the
commit map. This caused restore-selection to receive nil on undo.

---------

Signed-off-by: eureka928 <meobius123@gmail.com>
Co-authored-by: Mihai <noreply@github.com>
2026-04-07 16:30:47 +02:00

1549 lines
55 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 app.main.data.workspace
(:require
[app.common.attrs :as attrs]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.changes-builder :as pcb]
[app.common.files.helpers :as cfh]
[app.common.files.variant :as cfv]
[app.common.geom.align :as gal]
[app.common.geom.point :as gpt]
[app.common.geom.proportions :as gpp]
[app.common.geom.shapes :as gsh]
[app.common.logging :as log]
[app.common.path-names :as cpn]
[app.common.transit :as t]
[app.common.types.component :as ctc]
[app.common.types.components-list :as ctkl]
[app.common.types.shape :as cts]
[app.common.types.variant :as ctv]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.broadcast :as mbc]
[app.main.data.changes :as dch]
[app.main.data.comments :as dcmt]
[app.main.data.common :as dcm]
[app.main.data.event :as ev]
[app.main.data.fonts :as df]
[app.main.data.helpers :as dsh]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
[app.main.data.persistence :as dps]
[app.main.data.plugins :as dp]
[app.main.data.profile :as du]
[app.main.data.project :as dpj]
[app.main.data.workspace.bool :as dwb]
[app.main.data.workspace.clipboard :as dwcp]
[app.main.data.workspace.colors :as dwcl]
[app.main.data.workspace.comments :as dwcm]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.drawing :as dwd]
[app.main.data.workspace.edition :as dwe]
[app.main.data.workspace.fix-deleted-fonts :as fdf]
[app.main.data.workspace.groups :as dwg]
[app.main.data.workspace.guides :as dwgu]
[app.main.data.workspace.highlight :as dwh]
[app.main.data.workspace.interactions :as dwi]
[app.main.data.workspace.layers :as dwly]
[app.main.data.workspace.layout :as layout]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.mcp :as mcp]
[app.main.data.workspace.notifications :as dwn]
[app.main.data.workspace.pages :as dwpg]
[app.main.data.workspace.path :as dwdp]
[app.main.data.workspace.path.shapes-to-path :as dwps]
[app.main.data.workspace.selection :as dws]
[app.main.data.workspace.shape-layout :as dwsl]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.thumbnails :as dwth]
[app.main.data.workspace.transforms :as dwt]
[app.main.data.workspace.undo :as dwu]
[app.main.data.workspace.variants :as dwva]
[app.main.data.workspace.viewport :as dwv]
[app.main.data.workspace.zoom :as dwz]
[app.main.errors]
[app.main.features :as features]
[app.main.features.pointer-map :as fpmap]
[app.main.refs :as refs]
[app.main.repo :as rp]
[app.main.router :as rt]
[app.render-wasm :as wasm]
[app.render-wasm.api :as wasm.api]
[app.util.dom :as dom]
[app.util.globals :as ug]
[app.util.http :as http]
[app.util.perf :as perf]
[app.util.storage :as storage]
[app.util.timers :as tm]
[app.util.webapi :as wapi]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[potok.v2.core :as ptk]))
(log/set-level! :info)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Workspace Initialization
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare ^:private workspace-initialized)
(declare ^:private fetch-libraries)
;; --- Initialize Workspace
(defn initialize-workspace-layout
[lname]
(ptk/reify ::initialize-layout
ptk/UpdateEvent
(update [_ state]
(-> state
(update :workspace-layout #(or % layout/default-layout))
(update :workspace-global #(or % layout/default-global))))
ptk/WatchEvent
(watch [_ _ _]
(if (and lname (contains? layout/presets lname))
(rx/of (layout/ensure-layout lname))
(rx/of (layout/ensure-layout :layers))))))
(defn- datauri->blob-uri
[uri]
(->> (http/send! {:uri uri
:response-type :blob
:method :get})
(rx/map :body)
(rx/map (fn [blob] (wapi/create-uri blob)))))
(defn- get-file-object-thumbnails
[file-id]
(->> (rp/cmd! :get-file-object-thumbnails {:file-id file-id})
(rx/mapcat (fn [thumbnails]
(->> (rx/from thumbnails)
(rx/mapcat (fn [[k v]]
;; we only need to fetch the thumbnail if
;; it is a data:uri, otherwise we can just
;; use the value as is.
(if (str/starts-with? v "data:")
(->> (datauri->blob-uri v)
(rx/map (fn [uri] [k uri])))
(rx/of [k v])))))))
(rx/reduce conj {})))
(defn- resolve-file
[file]
(log/inf :hint "resolve file"
:file-id (str (:id file))
:features (str/join " " (:features file)))
(->> (fpmap/resolve-file file)
(rx/map :data)
(rx/map
(fn [data]
(assoc file :data (d/removem (comp t/pointer? val) data))))))
(defn- check-libraries-synchronization
[file-id libraries]
(ptk/reify ::check-libraries-synchronization
ptk/WatchEvent
(watch [_ state _]
(let [file (dsh/lookup-file state file-id)
ignore-until (get file :ignore-sync-until)
needs-check?
(some #(and (> (:modified-at %) (:synced-at %))
(or (not ignore-until)
(> (:modified-at %) ignore-until)))
libraries)]
(when needs-check?
(->> (rx/of (dwl/notify-sync-file))
(rx/delay 1000)))))))
(defn- library-resolved
[library]
(ptk/reify ::library-resolved
ptk/UpdateEvent
(update [_ state]
(update state :files assoc (:id library) library))))
(defn- fetch-libraries
[file-id features]
(ptk/reify ::fetch-libries
ptk/WatchEvent
(watch [_ _ stream]
(let [stopper-s (rx/filter (ptk/type? ::finalize-workspace) stream)]
(->> (rx/concat
(->> (rp/cmd! :get-file-libraries {:file-id file-id})
(rx/mapcat
(fn [libraries]
(rx/concat
(rx/of (dwl/libraries-fetched file-id libraries))
(rx/merge
(->> (rx/from libraries)
(rx/merge-map
(fn [{:keys [id synced-at]}]
(->> (rp/cmd! :get-file {:id id :features features})
(rx/map #(assoc % :synced-at synced-at :library-of file-id)))))
(rx/mapcat resolve-file)
(rx/map library-resolved))
(->> (rx/from libraries)
(rx/map :id)
(rx/mapcat (fn [file-id]
(rp/cmd! :get-file-object-thumbnails {:file-id file-id :tag "component"})))
(rx/map dwl/library-thumbnails-fetched)))
(rx/of (check-libraries-synchronization file-id libraries))))))
;; This events marks that all the libraries have been resolved
(rx/of (ptk/data-event ::all-libraries-resolved {:file-id file-id})))
(rx/take-until stopper-s))))))
(defn- workspace-initialized
[file-id]
(ptk/reify ::workspace-initialized
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc :workspace-undo {})
(assoc :workspace-ready file-id)))
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dp/check-open-plugin)
(fdf/fix-deleted-fonts-for-local-library file-id)
(mcp/init-mcp-connection)))))
(defn- bundle-fetched
[{:keys [file file-id thumbnails] :as bundle}]
(ptk/reify ::bundle-fetched
IDeref
(-deref [_] bundle)
ptk/UpdateEvent
(update [_ state]
(let [pending-version-id (:workspace-pending-file-version-id state)
state (-> state
(assoc :thumbnails thumbnails)
(update :files assoc file-id file)
(dissoc :workspace-pending-file-version-id))]
(cond-> state
(some? pending-version-id)
(assoc :workspace-file-version-id pending-version-id)
(nil? pending-version-id)
(dissoc :workspace-file-version-id))))))
(defn zoom-to-frame
[]
(ptk/reify ::zoom-to-frame
ptk/WatchEvent
(watch [_ state _]
(let [params (rt/get-params state)
board-id (get params :board-id)
board-id (cond
(vector? board-id) board-id
(string? board-id) [board-id])
frames-id (->> board-id
(map uuid/uuid)
(into (d/ordered-set)))]
(rx/of (dws/select-shapes frames-id)
dwz/zoom-to-selected-shape)))))
(defn- fetch-bundle
"Multi-stage file bundle fetch coordinator"
[file-id features]
(ptk/reify ::fetch-bundle
ptk/WatchEvent
(watch [_ _ stream]
(log/debug :hint "fetch bundle" :file-id (dm/str file-id))
(let [stopper-s (rx/filter (ptk/type? ::finalize-workspace) stream)]
(->> (rx/zip (rp/cmd! :get-file {:id file-id :features features})
(get-file-object-thumbnails file-id))
(rx/take 1)
(rx/mapcat
(fn [[file thumbnails]]
(->> (resolve-file file)
(rx/map (fn [file]
(log/trace :hint "file resolved" :file-id file-id)
{:file file
:file-id file-id
:features features
:thumbnails thumbnails})))))
(rx/map bundle-fetched)
(rx/take-until stopper-s))))))
;; FIXME: this need docstring
(defn- process-wasm-object
[id]
(ptk/reify ::process-wasm-object
ptk/EffectEvent
(effect [_ state _]
(let [objects (dsh/lookup-page-objects state)
shape (get objects id)]
;; Only process objects that exist in the current page
;; This prevents errors when processing changes from other pages
(when shape
(wasm.api/process-object shape))))))
(defn initialize-workspace
([team-id file-id]
(initialize-workspace team-id file-id nil))
([team-id file-id version-id]
(assert (uuid? team-id) "expected valud uuid for `team-id`")
(assert (uuid? file-id) "expected valud uuid for `file-id`")
(ptk/reify ::initialize-workspace
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc :recent-colors (:recent-colors storage/user))
(assoc :recent-fonts (:recent-fonts storage/user))
(assoc :current-file-id file-id)
(assoc :workspace-presence {})
;; Store pending version-id; bundle-fetched will set workspace-file-version-id
;; when the new bundle is applied so the viewport re-inits with new data
(assoc :workspace-pending-file-version-id version-id)))
ptk/WatchEvent
(watch [_ state stream]
(let [stoper-s (rx/filter (ptk/type? ::finalize-workspace) stream)
rparams (rt/get-params state)
features (features/get-enabled-features state team-id)
render-wasm? (contains? features "render-wasm/v1")]
(log/debug :hint "initialize-workspace"
:team-id (dm/str team-id)
:file-id (dm/str file-id))
(->> (rx/merge
(rx/concat
;; Fetch all essential data that should be loaded before the file
(rx/merge
(if ^boolean render-wasm?
(->> (rx/from @wasm/module)
(rx/filter true?)
(rx/tap (fn [_]
(let [event (ug/event "penpot:wasm:loaded")]
(ug/dispatch! event))))
(rx/ignore))
(rx/empty))
(->> stream
(rx/filter (ptk/type? ::df/fonts-loaded))
(rx/take 1)
(rx/ignore))
(rx/of (ntf/hide)
(dcmt/retrieve-comment-threads file-id)
(dcmt/fetch-profiles)
(df/fetch-fonts team-id))
(when (contains? cf/flags :mcp)
(rx/of (du/fetch-access-tokens))))
;; Once the essential data is fetched, lets proceed to
;; fetch teh file bunldle
(rx/of (fetch-bundle file-id features)))
(->> stream
(rx/filter (ptk/type? ::bundle-fetched))
(rx/take 1)
(rx/map deref)
(rx/mapcat
(fn [{:keys [file]}]
(log/debug :hint "bundle fetched"
:team-id (dm/str team-id)
:file-id (dm/str file-id))
(rx/of (dpj/initialize-project (:project-id file))
(dwn/initialize team-id file-id)
(dwsl/initialize-shape-layout)
(fetch-libraries file-id features)
(-> (workspace-initialized file-id)
(with-meta {:team-id team-id
:file-id file-id}))))))
;; Install dev perf observers once the workspace is ready
(when (contains? cf/flags :perf-logs)
(->> stream
(rx/filter (ptk/type? ::workspace-initialized))
(rx/take 1)
(rx/tap (fn [_] (perf/setup)))))
(when (contains? cf/flags :mcp)
(->> mbc/stream
(rx/filter (mbc/type? :mcp-enabled-change-connection))
(rx/map deref)
(rx/mapcat (fn [value]
(rx/of (mcp/update-mcp-connection value)
(mcp/user-disconnect-mcp))))))
(when (contains? cf/flags :mcp)
(->> mbc/stream
(rx/filter (mbc/type? :mcp-enabled-change-status))
(rx/map deref)
(rx/map mcp/update-mcp-status)))
(->> stream
(rx/filter (ptk/type? ::dps/persistence-notification))
(rx/take 1)
(rx/map dwc/set-workspace-visited))
(when-let [component-id (some-> rparams :component-id uuid/parse)]
(->> stream
(rx/filter (ptk/type? ::workspace-initialized))
(rx/observe-on :async)
(rx/take 1)
(rx/map #(dwl/go-to-local-component :id component-id :update-layout? (:update-layout rparams)))))
(when (:board-id rparams)
(->> stream
(rx/filter (ptk/type? ::dwv/initialize-viewport))
(rx/take 1)
(rx/map zoom-to-frame)))
(when-let [comment-id (some-> rparams :comment-id uuid/parse)]
(->> stream
(rx/filter (ptk/type? ::workspace-initialized))
(rx/observe-on :async)
(rx/take 1)
(rx/map #(dwcm/navigate-to-comment-id comment-id))))
(when render-wasm?
(->> stream
(rx/filter dch/commit?)
(rx/map deref)
(rx/mapcat
(fn [{:keys [redo-changes]}]
(let [added (->> redo-changes
(filter #(= (:type %) :add-obj))
(map :id))]
(->> (rx/from added)
(rx/map process-wasm-object)))))))
(when render-wasm?
(let [local-commits-s
(->> stream
(rx/filter dch/commit?)
(rx/map deref)
(rx/filter #(and (= :local (:source %))
(not (contains? (:tags %) :position-data))))
(rx/filter (complement empty?)))
notifier-s
(rx/merge
(->> local-commits-s (rx/debounce 1000))
(->> stream (rx/filter dps/force-persist?)))
objects-s
(rx/from-atom refs/workspace-page-objects {:emit-current-value? true})
current-page-id-s
(rx/from-atom refs/current-page-id {:emit-current-value? true})]
(->> local-commits-s
(rx/buffer-until notifier-s)
(rx/with-latest-from objects-s)
(rx/map
(fn [[commits objects]]
(->> commits
(mapcat :redo-changes)
(filter #(contains? #{:mod-obj :add-obj} (:type %)))
(filter #(cfh/text-shape? objects (:id %)))
(map #(vector
(:id %)
(wasm.api/calculate-position-data (get objects (:id %))))))))
(rx/with-latest-from current-page-id-s)
(rx/map
(fn [[text-position-data page-id]]
(let [changes
(->> text-position-data
(mapv (fn [[id position-data]]
{:type :mod-obj
:id id
:page-id page-id
:operations
[{:type :set
:attr :position-data
:val position-data
:ignore-touched true
:ignore-geometry true}]})))]
(when (d/not-empty? changes)
(dch/commit-changes
{:redo-changes changes :undo-changes []
:save-undo? false
:tags #{:position-data}})))))
(rx/take-until stoper-s))))
(->> stream
(rx/filter dch/commit?)
(rx/map deref)
(rx/mapcat
(fn [{:keys [save-undo? undo-changes redo-changes undo-group tags stack-undo? selected-before]}]
(if (and save-undo? (seq undo-changes))
(let [entry {:undo-changes undo-changes
:redo-changes redo-changes
:undo-group undo-group
:tags tags
:selected-before selected-before}]
(rx/of (dwu/append-undo entry stack-undo?)))
(rx/empty))))))
(rx/take-until stoper-s))))
ptk/EffectEvent
(effect [_ _ _]
(let [name (dm/str "workspace-" file-id)]
(unchecked-set ug/global "name" name))))))
(defn finalize-workspace
[_team-id file-id]
(ptk/reify ::finalize-workspace
ptk/UpdateEvent
(update [_ state]
(-> state
;; FIXME: revisit
(dissoc
:current-file-id
:workspace-editor-state
:workspace-media-objects
:workspace-persistence
:workspace-presence
:workspace-tokens
:workspace-undo)
(update :workspace-global dissoc :read-only?)
(assoc-in [:workspace-global :options-mode] :design)
(update :files d/update-vals #(dissoc % :data))))
ptk/WatchEvent
(watch [_ state _]
(let [project-id (:current-project-id state)]
(rx/of (dwn/finalize file-id)
(dpj/finalize-project project-id)
(dwsl/finalize-shape-layout)
(dwcl/stop-picker)
(dwc/set-workspace-visited)
(modal/hide)
(ntf/hide))))))
(defn- reload-current-file
[]
(ptk/reify ::reload-current-file
ptk/WatchEvent
(watch [_ state _]
(let [file-id (:current-file-id state)
team-id (:current-team-id state)]
(rx/of (initialize-workspace team-id file-id))))))
;; Make this event callable through dynamic resolution
(defmethod ptk/resolve ::reload-current-file [_ _] (reload-current-file))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; WORKSPACE File Actions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; FIXME: move to common
(defn rename-file
[id name]
{:pre [(uuid? id) (string? name)]}
(let [name (dm/truncate name 200)]
(ptk/reify ::rename-file
IDeref
(-deref [_]
{::ev/origin "workspace" :id id :name name})
ptk/UpdateEvent
(update [_ state]
(let [file-id (:current-file-id state)]
(assoc-in state [:files file-id :name] name)))
ptk/WatchEvent
(watch [_ _ _]
(let [params {:id id :name name}]
(->> (rp/cmd! :rename-file params)
(rx/ignore)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Workspace State Manipulation
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; --- Layout Flags
(dm/export layout/toggle-layout-flag)
(dm/export layout/remove-layout-flag)
;; --- Profile
(defn update-nudge
[{:keys [big small] :as params}]
(ptk/reify ::update-nudge
IDeref
(-deref [_] (d/without-nils params))
ptk/UpdateEvent
(update [_ state]
(update-in state [:profile :props :nudge]
(fn [nudge]
(cond-> nudge
(number? big) (assoc :big big)
(number? small) (assoc :small small)))))
ptk/WatchEvent
(watch [_ state _]
(let [nudge (get-in state [:profile :props :nudge])]
(rx/of (du/update-profile-props {:nudge nudge}))))))
;; --- Set element options mode
(dm/export layout/set-options-mode)
;; --- Tooltip
(defn assign-cursor-tooltip
[content]
(ptk/reify ::assign-cursor-tooltip
ptk/UpdateEvent
(update [_ state]
(if (string? content)
(assoc-in state [:workspace-global :tooltip] content)
(assoc-in state [:workspace-global :tooltip] nil)))))
;; --- Update Shape Attrs
;; 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-generic-attrs attrs)]
(ptk/reify ::update-shape
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dwsh/update-shapes [id] #(merge % attrs)))))))
(defn start-rename-shape
"Start shape renaming process"
[id]
(dm/assert! (uuid? id))
(ptk/reify ::start-rename-shape
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-local :shape-for-rename] id))))
(defn end-rename-shape
"End the ongoing shape rename process"
([] (end-rename-shape nil nil))
([shape-id name]
(ptk/reify ::end-rename-shape
ptk/UpdateEvent
(update [_ state]
;; Remove rename state from workspace local state
(update state :workspace-local dissoc :shape-for-rename))
ptk/WatchEvent
(watch [_ state _]
(when-let [shape-id (d/nilv shape-id (dm/get-in state [:workspace-local :shape-for-rename]))]
(let [shape (dsh/lookup-shape state shape-id)
name (str/trim name)
clean-name (cpn/clean-path name)
valid? (and (not (str/ends-with? name "/"))
(string? clean-name)
(not (str/blank? clean-name)))
component-id (:component-id shape)
undo-id (js/Symbol)]
(when valid?
(if (ctc/is-variant-container? shape)
;; Rename the full variant when it is a variant container
(rx/of (dwva/rename-variant shape-id clean-name))
(rx/of
(dwu/start-undo-transaction undo-id)
;; Rename the shape if string is not empty/blank
(update-shape shape-id {:name clean-name})
;; Update the component in case shape is a main instance
(when (and (some? component-id) (ctc/main-instance? shape))
(dwl/rename-component component-id clean-name))
(dwu/commit-undo-transaction undo-id))))))))))
(defn rename-shape-or-variant
([id name]
(rename-shape-or-variant nil nil id name))
([file-id page-id id name]
(ptk/reify ::rename-shape-or-variant
ptk/WatchEvent
(watch [_ state _]
(let [file-id (d/nilv file-id (:current-file-id state))
page-id (d/nilv page-id (:current-page-id state))
file-data (dsh/lookup-file-data state file-id)
shape
(-> (dsh/lookup-page-objects state file-id page-id)
(get id))
is-variant? (ctc/is-variant? shape)
variant-id (when is-variant? (:variant-id shape))
variant-name (when is-variant? (:variant-name shape))
component-id (:component-id shape)
component (ctkl/get-component file-data (:component-id shape))
variant-properties (:variant-properties component)]
(cond
(and variant-name (ctv/valid-properties-formula? name))
(rx/of (dwva/update-properties-names-and-values
component-id variant-id variant-properties (ctv/properties-formula->map name))
(dwva/remove-empty-properties variant-id)
(dwva/update-error component-id))
variant-name
(rx/of (dwva/update-properties-names-and-values
component-id variant-id variant-properties {})
(dwva/remove-empty-properties variant-id)
(dwva/update-error component-id name))
:else
(rx/of (end-rename-shape id name))))))))
;; --- Update Selected Shapes attrs
(defn update-selected-shapes
[attrs]
(ptk/reify ::update-selected-shapes
ptk/WatchEvent
(watch [_ state _]
(let [selected (dsh/lookup-selected state)]
(rx/from (map #(update-shape % attrs) selected))))))
;; --- Delete Selected
(defn delete-selected
"Deselect all and remove all selected shapes."
[]
(ptk/reify ::delete-selected
ptk/WatchEvent
(watch [_ state _]
(let [selected (dsh/lookup-selected state)
hover-guides (get-in state [:workspace-guides :hover])]
(cond
(d/not-empty? selected)
(rx/of (dwsh/delete-shapes selected)
(dws/deselect-all))
(d/not-empty? hover-guides)
(rx/of (dwgu/remove-guides hover-guides)))))))
;; --- Start renaming selected shape
(defn start-rename-selected
"Rename selected shape."
[]
(ptk/reify ::start-rename-selected
ptk/WatchEvent
(watch [_ state _]
(let [selected (dsh/lookup-selected state)
id (first selected)]
(when (= (count selected) 1)
(rx/of (dcm/go-to-workspace :layout :layers)
(start-rename-shape id)))))))
;; --- Shape Vertical Ordering
(def valid-vertical-locations
#{:up :down :bottom :top})
(defn vertical-order-selected
[loc]
(dm/assert!
"expected valid location"
(contains? valid-vertical-locations loc))
(ptk/reify ::vertical-order-selected
ptk/WatchEvent
(watch [it state _]
(let [page-id (:current-page-id state)
objects (dsh/lookup-page-objects state page-id)
selected-ids (dsh/lookup-selected state)
selected-shapes (map (d/getf objects) selected-ids)
undo-id (js/Symbol)
move-shape
(fn [changes shape]
(let [parent (get objects (:parent-id shape))
sibling-ids (:shapes parent)
current-index (d/index-of sibling-ids (:id shape))
index-in-selection (d/index-of selected-ids (:id shape))
new-index (case loc
:top (count sibling-ids)
:down (max 0 (- current-index 1))
:up (min (count sibling-ids) (+ (inc current-index) 1))
:bottom index-in-selection)]
(pcb/change-parent changes
(:id parent)
[shape]
new-index)))
changes (reduce move-shape
(-> (pcb/empty-changes it page-id)
(pcb/with-objects objects))
selected-shapes)]
(rx/of (dwu/start-undo-transaction undo-id)
(dch/commit-changes changes)
(ptk/data-event :layout/update {:ids selected-ids})
(dwu/commit-undo-transaction undo-id))))))
(defn set-shape-index
[file-id page-id id new-index]
(ptk/reify ::set-shape-index
ptk/WatchEvent
(watch [it state _]
(let [file-id (or file-id (:current-file-id state))
page-id (or page-id (:current-page-id state))
objects (dsh/lookup-page-objects state file-id page-id)
undo-id (js/Symbol)
shape (get objects id)
parent (get objects (:parent-id shape))
current-index (d/index-of (:shapes parent) id)
new-index
(if (> new-index current-index)
(inc new-index)
new-index)
changes
(-> (pcb/empty-changes it page-id)
(pcb/with-objects objects)
(pcb/change-parent (:id parent) [shape] new-index))]
(rx/of (dwu/start-undo-transaction undo-id)
(dch/commit-changes changes)
(ptk/data-event :layout/update {:ids [id]})
(dwu/commit-undo-transaction undo-id))))))
(defn reorder-children
[file-id page-id parent-id children]
(ptk/reify ::reorder-children
ptk/WatchEvent
(watch [it state _]
(let [file-id (or file-id (:current-file-id state))
page-id (or page-id (:current-page-id state))
objects (dsh/lookup-page-objects state file-id page-id)
undo-id (js/Symbol)
changes
(-> (pcb/empty-changes it page-id)
(pcb/with-objects objects)
(pcb/reorder-children parent-id children))]
(rx/of (dwu/start-undo-transaction undo-id)
(dch/commit-changes changes)
(ptk/data-event :layout/update {:ids [parent-id]})
(dwu/commit-undo-transaction undo-id))))))
;; --- Change Shape Order (D&D Ordering)
(defn relocate-selected-shapes
[parent-id to-index]
(ptk/reify ::relocate-selected-shapes
ptk/WatchEvent
(watch [_ state _]
(let [selected (dsh/lookup-selected state)]
(rx/of (dwsh/relocate-shapes selected parent-id to-index))))))
(defn start-editing-selected
[]
(ptk/reify ::start-editing-selected
ptk/WatchEvent
(watch [_ state _]
(let [selected (dsh/lookup-selected state)
objects (dsh/lookup-page-objects state)]
(condp = (count selected)
0 (rx/empty)
1 (let [{:keys [id type] :as shape} (get objects (first selected))]
(case type
:text
(rx/of (dwe/start-edition-mode id))
(:group :bool :frame)
(let [shapes-ids (into (d/ordered-set) (get shape :shapes))]
(rx/of (dws/select-shapes shapes-ids)))
:svg-raw
nil
(rx/of (dwe/start-edition-mode id)
(dwdp/start-path-edit id))))
;; When we have multiple shapes selected, instead of enter
;; on the edition mode, we proceed to select all children of
;; the selected shapes. Because we can't enter on edition
;; mode on multiple shapes and this is a fallback operation.
(let [shapes-to-select
(->> selected
(reduce
(fn [result shape-id]
(let [children (dm/get-in objects [shape-id :shapes])]
(if (empty? children)
(conj result shape-id)
(into result children))))
(d/ordered-set)))]
(rx/of (dws/select-shapes shapes-to-select))))))))
(defn select-parent-layer
[]
(ptk/reify ::select-parent-layer
ptk/WatchEvent
(watch [_ state _]
(let [selected (dsh/lookup-selected state)
objects (dsh/lookup-page-objects state)
shapes-to-select
(->> selected
(reduce
(fn [result shape-id]
(let [parent-id (dm/get-in objects [shape-id :parent-id])]
(if (and (some? parent-id) (not= parent-id uuid/zero))
(conj result parent-id)
(conj result shape-id))))
(d/ordered-set)))]
(rx/of (dws/select-shapes shapes-to-select))))))
;; --- Change Page Order (D&D Ordering)
(defn relocate-page
[id index]
(ptk/reify ::relocate-page
ptk/WatchEvent
(watch [it state _]
(let [prev-index (-> (dsh/lookup-file-data state)
(get :pages)
(d/index-of id))
changes (-> (pcb/empty-changes it)
(pcb/move-page id index prev-index))]
(rx/of (dch/commit-changes changes))))))
;; --- Shape / Selection Alignment and Distribution
(defn can-align? [selected objects]
(cond
(empty? selected) false
(> (count selected) 1) true
:else
(not= uuid/zero (:parent-id (get objects (:id (first selected)))))))
(defn align-object-to-parent
[objects object-id axis]
(let [object (get objects object-id)
parent-id (:parent-id (get objects object-id))
parent (get objects parent-id)]
[(gal/align-to-parent object parent axis)]))
(defn align-objects-list
[objects selected axis]
(let [selected-objs (map #(get objects %) selected)
rect (gsh/shapes->rect selected-objs)]
(map #(gal/align-to-rect % rect axis) selected-objs)))
(defn align-objects
([axis]
(align-objects axis nil))
([axis selected]
(dm/assert!
"expected valid align axis value"
(contains? gal/valid-align-axis axis))
(ptk/reify ::align-objects
ptk/WatchEvent
(watch [_ state _]
(let [objects (dsh/lookup-page-objects state)
selected (or selected (dsh/lookup-selected state))
moved (if (= 1 (count selected))
(align-object-to-parent objects (first selected) axis)
(align-objects-list objects selected axis))
undo-id (js/Symbol)]
(when (can-align? selected objects)
(rx/of (dwu/start-undo-transaction undo-id)
(dwt/position-shapes moved)
(ptk/data-event :layout/update {:ids selected})
(dwu/commit-undo-transaction undo-id))))))))
(defn can-distribute? [selected]
(cond
(empty? selected) false
(< (count selected) 3) false
:else true))
(defn distribute-objects
([axis]
(distribute-objects axis nil))
([axis ids]
(dm/assert!
"expected valid distribute axis value"
(contains? gal/valid-dist-axis axis))
(ptk/reify ::distribute-objects
ptk/WatchEvent
(watch [_ state _]
(let [page-id (:current-page-id state)
objects (dsh/lookup-page-objects state page-id)
selected (or ids (dsh/lookup-selected state))
moved (-> (map #(get objects %) selected)
(gal/distribute-space axis))
undo-id (js/Symbol)]
(when (can-distribute? selected)
(rx/of (dwu/start-undo-transaction undo-id)
(dwt/position-shapes moved)
(ptk/data-event :layout/update {:ids selected})
(dwu/commit-undo-transaction undo-id))))))))
;; --- Shape Proportions
(defn set-shape-proportion-lock
[id lock]
(ptk/reify ::set-shape-proportion-lock
ptk/WatchEvent
(watch [_ _ _]
(letfn [(assign-proportions [shape]
(if-not lock
(assoc shape :proportion-lock false)
(-> (assoc shape :proportion-lock true)
(gpp/assign-proportions))))]
(rx/of (dwsh/update-shapes [id] assign-proportions))))))
(defn toggle-proportion-lock
[]
(ptk/reify ::toggle-proportion-lock
ptk/WatchEvent
(watch [_ state _]
(let [page-id (:current-page-id state)
objects (dsh/lookup-page-objects state page-id)
selected (dsh/lookup-selected state)
selected-obj (-> (map #(get objects %) selected))
multi (attrs/get-attrs-multi selected-obj [:proportion-lock])
multi? (= :multiple (:proportion-lock multi))]
(if multi?
(rx/of (dwsh/update-shapes selected #(assoc % :proportion-lock true)))
(rx/of (dwsh/update-shapes selected #(update % :proportion-lock not))))))))
(defn workspace-focus-lost
[]
(ptk/reify ::workspace-focus-lost
ptk/UpdateEvent
(update [_ state]
;; FIXME: remove the `?` from show-distances?
(assoc-in state [:workspace-global :show-distances?] false))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Navigation
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn set-assets-section-open
[file-id section open?]
(ptk/reify ::set-assets-section-open
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-assets :open-status file-id section] open?))))
(defn clear-assets-section-open
[]
(ptk/reify ::clear-assets-section-open
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-assets :open-status] {}))))
(defn set-assets-group-open
[file-id section path open?]
(ptk/reify ::set-assets-group-open
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-assets :open-status file-id :groups section path] open?))))
(defn- check-in-asset
[items element]
(let [items (or items #{})]
(if (contains? items element)
(disj items element)
(conj items element))))
(defn toggle-selected-assets
[file-id asset-id type]
(ptk/reify ::toggle-selected-assets
ptk/UpdateEvent
(update [_ state]
(update-in state [:workspace-assets :selected file-id type] check-in-asset asset-id))))
(defn select-single-asset
[file-id asset-id type]
(ptk/reify ::select-single-asset
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-assets :selected file-id type] #{asset-id}))))
(defn select-assets
[file-id assets-ids type]
(ptk/reify ::select-assets
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-assets :selected file-id type] (into #{} assets-ids)))))
(defn unselect-all-assets
([] (unselect-all-assets nil))
([file-id]
(ptk/reify ::unselect-all-assets
ptk/UpdateEvent
(update [_ state]
(if file-id
(update-in state [:workspace-assets :selected] dissoc file-id)
(update state :workspace-assets dissoc :selected))))))
(defn show-component-in-assets
[component-id]
(ptk/reify ::show-component-in-assets
ptk/WatchEvent
(watch [_ state _]
(let [file-id (:current-file-id state)
fdata (dsh/lookup-file-data state file-id)
component (cfv/get-primary-component fdata component-id)
cpath (:path component)
cpath (cpn/split-path cpath)
paths (map (fn [i] (cpn/join-path (take (inc i) cpath)))
(range (count cpath)))]
(rx/concat
(rx/from (map #(set-assets-group-open file-id :components % true) paths))
(rx/of (dcm/go-to-workspace :layout :assets)
(set-assets-section-open file-id :library true)
(set-assets-section-open file-id :components true)
(select-single-asset file-id (:id component) :components)))))
ptk/EffectEvent
(effect [_ state _]
(let [file-id (:current-file-id state)
fdata (dsh/lookup-file-data state file-id)
component (cfv/get-primary-component fdata component-id)
wrapper-id (str "component-shape-id-" (:id component))]
(tm/schedule-on-idle #(dom/scroll-into-view-if-needed! (dom/get-element wrapper-id)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Context Menu
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn show-context-menu
[{:keys [position] :as params}]
(dm/assert! (gpt/point? position))
(ptk/reify ::show-context-menu
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-local :context-menu] params))))
(defn show-shape-context-menu
[{:keys [shape] :as params}]
(ptk/reify ::show-shape-context-menu
ptk/WatchEvent
(watch [_ state _]
(let [selected (dsh/lookup-selected state)
objects (dsh/lookup-page-objects state)
all-selected (into [] (mapcat #(cfh/get-children-with-self objects %)) selected)
head (get objects (first selected))
not-group-like? (and (= (count selected) 1)
(not (contains? #{:group :bool} (:type head))))
no-bool-shapes? (->> all-selected (some (comp #{:frame :text} :type)))]
(if (and (some? shape) (not (contains? selected (:id shape))))
(rx/concat
(rx/of (dws/select-shape (:id shape)))
(rx/of (show-shape-context-menu params)))
(rx/of (show-context-menu
(-> params
(assoc
:kind :shape
:disable-booleans? (or no-bool-shapes? not-group-like?)
:disable-flatten? no-bool-shapes?
:selected (conj selected (:id shape)))))))))))
(defn show-page-item-context-menu
[{:keys [position page] :as params}]
(dm/assert! (gpt/point? position))
(ptk/reify ::show-page-item-context-menu
ptk/WatchEvent
(watch [_ _ _]
(rx/of (show-context-menu
(-> params (assoc :kind :page :selected (:id page))))))))
(defn show-track-context-menu
[{:keys [grid-id type index] :as params}]
(ptk/reify ::show-track-context-menu
ptk/WatchEvent
(watch [_ _ _]
(rx/of (show-context-menu
(-> params (assoc :kind :grid-track
:grid-id grid-id
:type type
:index index)))))))
(defn show-grid-cell-context-menu
[{:keys [grid-id] :as params}]
(ptk/reify ::show-grid-cell-context-menu
ptk/WatchEvent
(watch [_ state _]
(let [objects (dsh/lookup-page-objects state)
grid (get objects grid-id)
cells (->> (get-in state [:workspace-grid-edition grid-id :selected])
(map #(get-in grid [:layout-grid-cells %])))]
(rx/of (show-context-menu
(-> params (assoc :kind :grid-cells
:grid grid
:cells cells))))))))
(def hide-context-menu
(ptk/reify ::hide-context-menu
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-local :context-menu] nil))))
(defn toggle-distances-display [value]
(ptk/reify ::toggle-distances-display
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-global :show-distances?] value))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Interactions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(dm/export dwi/start-edit-interaction)
(dm/export dwi/move-edit-interaction)
(dm/export dwi/finish-edit-interaction)
(dm/export dwi/start-move-overlay-pos)
(dm/export dwi/move-overlay-pos)
(dm/export dwi/finish-move-overlay-pos)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CANVAS OPTIONS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn change-canvas-color
([color]
(change-canvas-color nil color))
([page-id color]
(ptk/reify ::change-canvas-color
ptk/WatchEvent
(watch [it state _]
(let [page-id (or page-id (:current-page-id state))
page (dsh/lookup-page state page-id)
changes (-> (pcb/empty-changes it)
(pcb/with-page page)
(pcb/mod-page {:background (:color color)}))]
(rx/of (dch/commit-changes changes)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Measurements
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn set-paddings-selected
[paddings-selected]
(ptk/reify ::set-paddings-selected
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-global :paddings-selected] paddings-selected))))
(defn set-gap-selected
[gap-selected]
(ptk/reify ::set-gap-selected
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-global :gap-selected] gap-selected))))
(defn set-margins-selected
[margins-selected]
(ptk/reify ::set-margins-selected
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-global :margins-selected] margins-selected))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Orphan Shapes
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- find-orphan-shapes
([state]
(find-orphan-shapes state (:current-page-id state)))
([state page-id]
(let [objects (dsh/lookup-page-objects state page-id)
objects (filter (fn [item]
(and
(not= (key item) uuid/zero)
(not (contains? objects (:parent-id (val item))))))
objects)]
objects)))
(defn fix-orphan-shapes
[]
(ptk/reify ::fix-orphan-shapes
ptk/WatchEvent
(watch [_ state _]
(let [orphans (set (into [] (keys (find-orphan-shapes state))))]
(rx/of (dwsh/relocate-shapes orphans uuid/zero 0 true))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Sitemap
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn start-rename-page-item
[id]
(ptk/reify ::start-rename-page-item
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-local :page-item] id))))
(defn stop-rename-page-item
[]
(ptk/reify ::stop-rename-page-item
ptk/UpdateEvent
(update [_ state]
(let [local (-> (:workspace-local state)
(dissoc :page-item))]
(assoc state :workspace-local local)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Components annotations
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn update-component-annotation
"Update the component with the given annotation"
[id annotation]
(dm/assert! (uuid? id))
(dm/assert! (or (nil? annotation) (string? annotation)))
(ptk/reify ::update-component-annotation
ptk/WatchEvent
(watch [it state _]
(let [data
(dsh/lookup-file-data state)
update-fn
(fn [component]
;; NOTE: we need to ensure the component exists,
;; because there are small possibilities of race
;; conditions with component deletion.
(when component
(if (nil? annotation)
(dissoc component :annotation)
(assoc component :annotation annotation))))
changes
(-> (pcb/empty-changes it)
(pcb/with-library-data data)
(pcb/update-component id update-fn))]
(rx/concat
(rx/of (dch/commit-changes changes))
(when (nil? annotation)
(rx/of (ptk/data-event ::ev/event {::ev/name "delete-component-annotation"}))))))))
(defn set-annotations-expanded
[expanded]
(ptk/reify ::set-annotations-expanded
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-annotations :expanded] expanded))))
(defn set-annotations-id-for-create
[id]
(ptk/reify ::set-annotations-id-for-create
ptk/UpdateEvent
(update [_ state]
(if id
(-> (assoc-in state [:workspace-annotations :id-for-create] id)
(assoc-in [:workspace-annotations :expanded] true))
(d/dissoc-in state [:workspace-annotations :id-for-create])))
ptk/WatchEvent
(watch [_ _ _]
(when (some? id)
(rx/of (ptk/data-event ::ev/event {::ev/name "create-component-annotation"}))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Preview blend modes
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn set-preview-blend-mode
[ids blend-mode]
(ptk/reify ::set-preview-blend-mode
ptk/UpdateEvent
(update [_ state]
(reduce #(assoc-in %1 [:workspace-preview-blend %2] blend-mode) state ids))))
(defn unset-preview-blend-mode
[ids]
(ptk/reify ::unset-preview-blend-mode
ptk/UpdateEvent
(update [_ state]
(reduce #(update %1 :workspace-preview-blend dissoc %2) state ids))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Components
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn find-components-norefs
[]
(ptk/reify ::find-components-norefs
ptk/WatchEvent
(watch [_ state _]
(let [objects (dsh/lookup-page-objects state)
copies (->> objects
vals
(filter #(and (ctc/instance-head? %) (not (ctc/main-instance? %)))))
copies-no-ref (filter #(not (:shape-ref %)) copies)
find-childs-no-ref (fn [acc-map item]
(let [id (:id item)
childs (->> (cfh/get-children objects id)
(filter #(not (:shape-ref %))))]
(if (seq childs)
(assoc acc-map id childs)
acc-map)))
childs-no-ref (reduce
find-childs-no-ref
{}
copies)]
(js/console.log "Copies no ref" (count copies-no-ref) (clj->js copies-no-ref))
(js/console.log "Childs no ref" (count childs-no-ref) (clj->js childs-no-ref))))))
(defn set-clipboard-style
[style]
(ptk/reify ::set-clipboard-style
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-global :clipboard-style] style))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Exports
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Transform
(dm/export dwt/trigger-bounding-box-cloaking)
(dm/export dwt/start-resize)
(dm/export dwt/update-dimensions)
(dm/export dwt/change-orientation)
(dm/export dwt/start-rotate)
(dm/export dwt/increase-rotation)
(dm/export dwt/start-move-selected)
(dm/export dwt/move-selected)
(dm/export dwt/update-position)
(dm/export dwt/update-positions)
(dm/export dwt/flip-horizontal-selected)
(dm/export dwt/flip-vertical-selected)
(dm/export dwly/set-opacity)
;; Common
(dm/export dwsh/add-shape)
(dm/export dwe/clear-edition-mode)
(dm/export dws/select-shapes)
(dm/export dwe/start-edition-mode)
;; Clipboard
(dm/export dwcp/copy-selected)
(dm/export dwcp/paste-from-clipboard)
(dm/export dwcp/paste-from-event)
(dm/export dwcp/copy-selected-css)
(dm/export dwcp/copy-selected-css-nested)
(dm/export dwcp/copy-selected-props)
(dm/export dwcp/copy-selected-svg)
(dm/export dwcp/copy-selected-text)
(dm/export dwcp/paste-selected-props)
(dm/export dwcp/paste-shapes)
(dm/export dwcp/paste-data-valid?)
(dm/export dwcp/copy-link-to-clipboard)
(dm/export dwcp/copy-as-image)
;; Drawing
(dm/export dwd/select-for-drawing)
;; Selection
(dm/export dws/toggle-focus-mode)
(dm/export dws/deselect-all)
(dm/export dws/deselect-shape)
(dm/export dws/duplicate-selected)
(dm/export dws/handle-area-selection)
(dm/export dws/select-all)
(dm/export dws/select-inside-group)
(dm/export dws/select-shape)
(dm/export dws/select-prev-shape)
(dm/export dws/select-next-shape)
(dm/export dws/shift-select-shapes)
;; Highlight
(dm/export dwh/highlight-shape)
(dm/export dwh/dehighlight-shape)
;; Shape flags
(dm/export dwsh/update-shape-flags)
(dm/export dwsh/toggle-visibility-selected)
(dm/export dwsh/toggle-lock-selected)
(dm/export dwsh/toggle-file-thumbnail-selected)
;; Groups
(dm/export dwg/mask-group)
(dm/export dwg/unmask-group)
(dm/export dwg/group-selected)
(dm/export dwg/ungroup-selected)
;; Boolean
(dm/export dwb/create-bool)
(dm/export dwb/group-to-bool)
(dm/export dwb/bool-to-group)
(dm/export dwb/change-bool-type)
;; Shapes to path
(dm/export dwps/convert-selected-to-path)
(dm/export dwps/convert-selected-strokes-to-path)
;; Guides
(dm/export dwgu/update-guides)
(dm/export dwgu/remove-guide)
(dm/export dwgu/set-hover-guide)
;; Zoom
(dm/export dwz/reset-zoom)
(dm/export dwz/zoom-to-selected-shape)
(dm/export dwz/start-zooming)
(dm/export dwz/finish-zooming)
(dm/export dwz/zoom-to-fit-all)
(dm/export dwz/decrease-zoom)
(dm/export dwz/increase-zoom)
(dm/export dwz/set-zoom)
;; Thumbnails
(dm/export dwth/update-thumbnail)
;; Viewport
(dm/export dwv/initialize-viewport)
(dm/export dwv/update-viewport-position)
(dm/export dwv/update-viewport-size)
(dm/export dwv/start-panning)
(dm/export dwv/finish-panning)
;; Undo
(dm/export dwu/reinitialize-undo)
;; Pages
(dm/export dwpg/initialize-page)
(dm/export dwpg/finalize-page)
(dm/export dwpg/create-page)
(dm/export dwpg/duplicate-page)
(dm/export dwpg/rename-page)
(dm/export dwpg/delete-page)