mirror of
https://github.com/penpot/penpot.git
synced 2026-05-24 01:13:43 +00:00
This commit should not not be backported to, because the affected code is already refactored and the issue is already fixed on develop branch
547 lines
22 KiB
Clojure
547 lines
22 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.shapes
|
|
(:require
|
|
[app.common.data :as d]
|
|
[app.common.data.macros :as dm]
|
|
[app.common.geom.proportions :as gpp]
|
|
[app.common.geom.shapes :as gsh]
|
|
[app.common.pages.changes-builder :as pcb]
|
|
[app.common.pages.helpers :as cph]
|
|
[app.common.schema :as sm]
|
|
[app.common.types.component :as ctk]
|
|
[app.common.types.container :as ctn]
|
|
[app.common.types.page :as ctp]
|
|
[app.common.types.shape :as cts]
|
|
[app.common.types.shape-tree :as ctst]
|
|
[app.common.types.shape.interactions :as ctsi]
|
|
[app.common.types.shape.layout :as ctl]
|
|
[app.common.uuid :as uuid]
|
|
[app.main.data.comments :as dc]
|
|
[app.main.data.workspace.changes :as dch]
|
|
[app.main.data.workspace.edition :as dwe]
|
|
[app.main.data.workspace.selection :as dws]
|
|
[app.main.data.workspace.state-helpers :as wsh]
|
|
[app.main.data.workspace.undo :as dwu]
|
|
[app.main.features :as features]
|
|
[app.main.streams :as ms]
|
|
[beicon.core :as rx]
|
|
[potok.core :as ptk]))
|
|
|
|
(defn get-shape-layer-position
|
|
[objects selected attrs]
|
|
|
|
;; Calculate the frame over which we're drawing
|
|
(let [position @ms/mouse-position
|
|
frame-id (:frame-id attrs (ctst/top-nested-frame objects position))
|
|
shape (when-not (empty? selected)
|
|
(cph/get-base-shape objects selected))]
|
|
|
|
;; When no shapes has been selected or we're over a different frame
|
|
;; we add it as the latest shape of that frame
|
|
(if (or (not shape) (not= (:frame-id shape) frame-id))
|
|
[frame-id frame-id nil]
|
|
|
|
;; Otherwise, we add it to next to the selected shape
|
|
(let [index (cph/get-position-on-parent objects (:id shape))
|
|
{:keys [frame-id parent-id]} shape]
|
|
[frame-id parent-id (inc index)]))))
|
|
|
|
(defn make-new-shape
|
|
[attrs objects selected]
|
|
(let [default-attrs (if (= :frame (:type attrs))
|
|
cts/default-frame-attrs
|
|
cts/default-shape-attrs)
|
|
|
|
default-attrs (if (or (= :group (:type attrs))
|
|
(= :bool (:type attrs)))
|
|
(assoc default-attrs :shapes [])
|
|
default-attrs)
|
|
|
|
selected-non-frames
|
|
(into #{} (comp (map (d/getf objects))
|
|
(remove cph/frame-shape?))
|
|
selected)
|
|
|
|
[frame-id parent-id index]
|
|
(get-shape-layer-position objects selected-non-frames attrs)]
|
|
|
|
(-> (merge default-attrs attrs)
|
|
(gpp/setup-proportions)
|
|
(assoc :frame-id frame-id
|
|
:parent-id parent-id
|
|
:index index))))
|
|
|
|
(defn prepare-add-shape
|
|
[changes attrs objects selected]
|
|
(let [id (or (:id attrs) (uuid/next))
|
|
name (:name attrs)
|
|
|
|
shape (make-new-shape
|
|
(assoc attrs :id id :name name)
|
|
objects
|
|
selected)
|
|
|
|
index (:index (meta attrs))
|
|
|
|
changes (-> changes
|
|
(pcb/with-objects objects)
|
|
(cond-> (some? index)
|
|
(pcb/add-object shape {:index index}))
|
|
(cond-> (nil? index)
|
|
(pcb/add-object shape))
|
|
(cond-> (some? (:parent-id attrs))
|
|
(pcb/change-parent (:parent-id attrs) [shape] index))
|
|
(cond-> (ctl/grid-layout? objects (:parent-id shape))
|
|
(pcb/update-shapes [(:parent-id shape)] ctl/assign-cells)))]
|
|
|
|
[shape changes]))
|
|
|
|
(defn add-shape
|
|
([attrs]
|
|
(add-shape attrs {}))
|
|
([attrs {:keys [no-select? no-update-layout?]}]
|
|
(dm/assert! (cts/shape-attrs? attrs))
|
|
(ptk/reify ::add-shape
|
|
ptk/WatchEvent
|
|
(watch [it state _]
|
|
(let [page-id (:current-page-id state)
|
|
objects (wsh/lookup-page-objects state page-id)
|
|
selected (wsh/lookup-selected state)
|
|
|
|
changes (-> (pcb/empty-changes it page-id)
|
|
(pcb/with-objects objects))
|
|
|
|
[shape changes]
|
|
(prepare-add-shape changes attrs objects selected)
|
|
|
|
changes (cond-> changes
|
|
(cph/text-shape? shape)
|
|
(pcb/set-undo-group (:id shape)))
|
|
|
|
undo-id (js/Symbol)]
|
|
|
|
(rx/concat
|
|
(rx/of (dwu/start-undo-transaction undo-id)
|
|
(dch/commit-changes changes)
|
|
(when-not no-update-layout?
|
|
(ptk/data-event :layout/update [(:parent-id shape)]))
|
|
(when-not no-select?
|
|
(dws/select-shapes (d/ordered-set (:id shape))))
|
|
(dwu/commit-undo-transaction undo-id))
|
|
(when (= :text (:type attrs))
|
|
(->> (rx/of (dwe/start-edition-mode (:id shape)))
|
|
(rx/observe-on :async)))))))))
|
|
|
|
(defn prepare-move-shapes-into-frame
|
|
[changes frame-id shapes objects]
|
|
(let [ordered-indexes (cph/order-by-indexed-shapes objects shapes)
|
|
parent-id (get-in objects [frame-id :parent-id])
|
|
ordered-indexes (->> ordered-indexes (remove #(= % parent-id)))
|
|
to-move-shapes (map (d/getf objects) ordered-indexes)]
|
|
(when (d/not-empty? to-move-shapes)
|
|
(-> changes
|
|
(cond-> (not (ctl/any-layout? objects frame-id))
|
|
(pcb/update-shapes ordered-indexes ctl/remove-layout-item-data))
|
|
(pcb/update-shapes ordered-indexes #(cond-> % (cph/frame-shape? %) (assoc :hide-in-viewer true)))
|
|
(pcb/change-parent frame-id to-move-shapes 0)
|
|
(cond-> (ctl/grid-layout? objects frame-id)
|
|
(pcb/update-shapes [frame-id] ctl/assign-cells))))))
|
|
|
|
(defn move-shapes-into-frame
|
|
[frame-id shapes]
|
|
(ptk/reify ::move-shapes-into-frame
|
|
ptk/WatchEvent
|
|
(watch [it state _]
|
|
(let [page-id (:current-page-id state)
|
|
objects (wsh/lookup-page-objects state page-id)
|
|
shapes (->> shapes (remove #(dm/get-in objects [% :blocked])))
|
|
changes (-> (pcb/empty-changes it page-id)
|
|
(pcb/with-objects objects))
|
|
changes (prepare-move-shapes-into-frame changes
|
|
frame-id
|
|
shapes
|
|
objects)]
|
|
(if (some? changes)
|
|
(rx/of (dch/commit-changes changes))
|
|
(rx/empty))))))
|
|
|
|
(declare real-delete-shapes)
|
|
(declare update-shape-flags)
|
|
|
|
(defn delete-shapes
|
|
([ids] (delete-shapes nil ids))
|
|
([page-id ids]
|
|
(dm/assert! (sm/set-of-uuid? ids))
|
|
(ptk/reify ::delete-shapes
|
|
ptk/WatchEvent
|
|
(watch [it state _]
|
|
(let [file-id (:current-file-id state)
|
|
page-id (or page-id (:current-page-id state))
|
|
file (wsh/get-file state file-id)
|
|
page (wsh/lookup-page state page-id)
|
|
objects (wsh/lookup-page-objects state page-id)
|
|
|
|
components-v2 (features/active-feature? state :components-v2)
|
|
|
|
ids (cph/clean-loops objects ids)
|
|
|
|
in-component-copy?
|
|
(fn [shape-id]
|
|
;; Look for shapes that are inside a component copy, but are
|
|
;; not the root. In this case, they must not be deleted,
|
|
;; but hidden (to be able to recover them more easily).
|
|
(let [shape (get objects shape-id)
|
|
component-shape (ctn/get-component-shape objects shape)]
|
|
(and (ctk/in-component-copy? shape)
|
|
(not= shape component-shape)
|
|
(not (ctk/main-instance? component-shape)))))
|
|
|
|
[ids-to-delete ids-to-hide]
|
|
(if components-v2
|
|
(loop [ids-seq (seq ids)
|
|
ids-to-delete []
|
|
ids-to-hide []]
|
|
(let [id (first ids-seq)]
|
|
(if (nil? id)
|
|
[ids-to-delete ids-to-hide]
|
|
(if (in-component-copy? id)
|
|
(recur (rest ids-seq)
|
|
ids-to-delete
|
|
(conj ids-to-hide id))
|
|
(recur (rest ids-seq)
|
|
(conj ids-to-delete id)
|
|
ids-to-hide)))))
|
|
[ids []])
|
|
|
|
undo-id (js/Symbol)]
|
|
|
|
(rx/concat
|
|
(rx/of (dwu/start-undo-transaction undo-id)
|
|
(update-shape-flags ids-to-hide {:hidden true}))
|
|
(real-delete-shapes file page objects ids-to-delete it components-v2)
|
|
(rx/of (dwu/commit-undo-transaction undo-id))))))))
|
|
|
|
(defn- real-delete-shapes-changes
|
|
([file page objects ids it components-v2]
|
|
(let [changes (-> (pcb/empty-changes it (:id page))
|
|
(pcb/with-page page)
|
|
(pcb/with-objects objects)
|
|
(pcb/with-library-data file))]
|
|
(real-delete-shapes-changes changes file page objects ids it components-v2)))
|
|
([changes file page objects ids _it components-v2]
|
|
(let [lookup (d/getf objects)
|
|
groups-to-unmask
|
|
(reduce (fn [group-ids id]
|
|
;; When the shape to delete is the mask of a masked group,
|
|
;; the mask condition must be removed, and it must be
|
|
;; converted to a normal group.
|
|
(let [obj (lookup id)
|
|
parent (lookup (:parent-id obj))]
|
|
(if (and (:masked-group? parent)
|
|
(= id (first (:shapes parent))))
|
|
(conj group-ids (:id parent))
|
|
group-ids)))
|
|
#{}
|
|
ids)
|
|
|
|
interacting-shapes
|
|
(filter (fn [shape]
|
|
;; If any of the deleted shapes is the destination of
|
|
;; some interaction, this must be deleted, too.
|
|
(let [interactions (:interactions shape)]
|
|
(some #(and (ctsi/has-destination %)
|
|
(contains? ids (:destination %)))
|
|
interactions)))
|
|
(vals objects))
|
|
|
|
;; If any of the deleted shapes is a frame with guides
|
|
guides (into {}
|
|
(comp (map second)
|
|
(remove #(contains? ids (:frame-id %)))
|
|
(map (juxt :id identity)))
|
|
(dm/get-in page [:options :guides]))
|
|
|
|
starting-flows
|
|
(filter (fn [flow]
|
|
;; If any of the deleted is a frame that starts a flow,
|
|
;; this must be deleted, too.
|
|
(contains? ids (:starting-frame flow)))
|
|
(-> page :options :flows))
|
|
|
|
all-parents
|
|
(reduce (fn [res id]
|
|
;; All parents of any deleted shape must be resized.
|
|
(into res (cph/get-parent-ids objects id)))
|
|
(d/ordered-set)
|
|
ids)
|
|
|
|
all-children
|
|
(->> ids ;; Children of deleted shapes must be also deleted.
|
|
(reduce (fn [res id]
|
|
(into res (cph/get-children-ids objects id)))
|
|
[])
|
|
(reverse)
|
|
(into (d/ordered-set)))
|
|
|
|
find-all-empty-parents
|
|
(fn recursive-find-empty-parents [empty-parents]
|
|
(let [all-ids (into empty-parents ids)
|
|
contains? (partial contains? all-ids)
|
|
xform (comp (map lookup)
|
|
(filter #(or (cph/group-shape? %) (cph/bool-shape? %)))
|
|
(remove #(->> (:shapes %) (remove contains?) seq))
|
|
(map :id))
|
|
parents (into #{} xform all-parents)]
|
|
(if (= empty-parents parents)
|
|
empty-parents
|
|
(recursive-find-empty-parents parents))))
|
|
|
|
empty-parents
|
|
;; Any parent whose children are all deleted, must be deleted too.
|
|
(into (d/ordered-set) (find-all-empty-parents #{}))
|
|
|
|
components-to-delete
|
|
(if components-v2
|
|
(reduce (fn [components id]
|
|
(let [shape (get objects id)]
|
|
(if (and (= (:component-file shape) (:id file)) ;; Main instances should exist only in local file
|
|
(:main-instance? shape)) ;; but check anyway
|
|
(conj components (:component-id shape))
|
|
components)))
|
|
[]
|
|
(into ids all-children))
|
|
[])
|
|
|
|
changes (-> changes
|
|
(pcb/set-page-option :guides guides))
|
|
|
|
changes (reduce (fn [changes component-id]
|
|
;; It's important to delete the component before the main instance, because we
|
|
;; need to store the instance position if we want to restore it later.
|
|
(pcb/delete-component changes component-id))
|
|
changes
|
|
components-to-delete)
|
|
|
|
changes (-> changes
|
|
(pcb/remove-objects all-children {:ignore-touched true})
|
|
(pcb/remove-objects ids)
|
|
(pcb/remove-objects empty-parents)
|
|
(pcb/resize-parents all-parents)
|
|
(pcb/update-shapes groups-to-unmask
|
|
(fn [shape]
|
|
(assoc shape :masked-group? false)))
|
|
(pcb/update-shapes (map :id interacting-shapes)
|
|
(fn [shape]
|
|
(d/update-when shape :interactions
|
|
(fn [interactions]
|
|
(into []
|
|
(remove #(and (ctsi/has-destination %)
|
|
(contains? ids (:destination %))))
|
|
interactions)))))
|
|
(cond-> (seq starting-flows)
|
|
(pcb/update-page-option :flows (fn [flows]
|
|
(->> (map :id starting-flows)
|
|
(reduce ctp/remove-flow flows))))))]
|
|
[changes all-parents])))
|
|
|
|
|
|
(defn delete-shapes-changes
|
|
[changes file page objects ids it components-v2]
|
|
(let [[changes _all-parents] (real-delete-shapes-changes changes file page objects ids it components-v2)]
|
|
changes))
|
|
|
|
|
|
(defn- real-delete-shapes
|
|
[file page objects ids it components-v2]
|
|
(let [[changes all-parents] (real-delete-shapes-changes file page objects ids it components-v2)
|
|
undo-id (js/Symbol)]
|
|
(rx/of (dwu/start-undo-transaction undo-id)
|
|
(dc/detach-comment-thread ids)
|
|
(dch/commit-changes changes)
|
|
(ptk/data-event :layout/update all-parents)
|
|
(dwu/commit-undo-transaction undo-id))))
|
|
|
|
(defn create-and-add-shape
|
|
[type frame-x frame-y data]
|
|
(ptk/reify ::create-and-add-shape
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [{:keys [width height]} data
|
|
|
|
vbc (wsh/viewport-center state)
|
|
x (:x data (- (:x vbc) (/ width 2)))
|
|
y (:y data (- (:y vbc) (/ height 2)))
|
|
page-id (:current-page-id state)
|
|
objects (wsh/lookup-page-objects state page-id)
|
|
frame-id (-> (wsh/lookup-page-objects state page-id)
|
|
(ctst/top-nested-frame {:x frame-x :y frame-y}))
|
|
selected (wsh/lookup-selected state)
|
|
page-objects (wsh/lookup-page-objects state)
|
|
base (cph/get-base-shape page-objects selected)
|
|
selected-frame? (and (= 1 (count selected))
|
|
(= :frame (get-in objects [(first selected) :type])))
|
|
parent-id (if
|
|
(or selected-frame? (empty? selected)) frame-id
|
|
(:parent-id base))
|
|
|
|
shape (-> (cts/make-minimal-shape type)
|
|
(merge data)
|
|
(merge {:x x :y y})
|
|
(assoc :frame-id frame-id :parent-id parent-id)
|
|
(cts/setup-rect-selrect))]
|
|
(rx/of (add-shape shape))))))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Artboard
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defn prepare-create-artboard-from-selection
|
|
[changes id parent-id objects selected index frame-name without-fill?]
|
|
(let [selected-objs (map #(get objects %) selected)
|
|
new-index (or index
|
|
(cph/get-index-replacement selected objects))]
|
|
(when (d/not-empty? selected)
|
|
(let [srect (gsh/selection-rect selected-objs)
|
|
frame-id (get-in objects [(first selected) :frame-id])
|
|
parent-id (or parent-id (get-in objects [(first selected) :parent-id]))
|
|
shape (-> (cts/make-minimal-shape :frame)
|
|
(merge {:x (:x srect) :y (:y srect) :width (:width srect) :height (:height srect)})
|
|
(cond-> id
|
|
(assoc :id id))
|
|
(cond-> frame-name
|
|
(assoc :name frame-name))
|
|
(assoc :frame-id frame-id :parent-id parent-id)
|
|
(with-meta {:index new-index})
|
|
(cond-> (or (not= frame-id uuid/zero) without-fill?)
|
|
(assoc :fills [] :hide-in-viewer true))
|
|
(cts/setup-rect-selrect))
|
|
|
|
[shape changes]
|
|
(prepare-add-shape changes shape objects selected)
|
|
|
|
changes
|
|
(prepare-move-shapes-into-frame changes (:id shape) selected objects)]
|
|
|
|
[shape changes]))))
|
|
|
|
(defn create-artboard-from-selection
|
|
([]
|
|
(create-artboard-from-selection nil))
|
|
([id]
|
|
(create-artboard-from-selection id nil))
|
|
([id parent-id]
|
|
(create-artboard-from-selection id parent-id nil))
|
|
([id parent-id index]
|
|
(ptk/reify ::create-artboard-from-selection
|
|
ptk/WatchEvent
|
|
(watch [it state _]
|
|
(let [page-id (:current-page-id state)
|
|
objects (wsh/lookup-page-objects state page-id)
|
|
selected (wsh/lookup-selected state)
|
|
selected (cph/clean-loops objects selected)
|
|
|
|
changes (-> (pcb/empty-changes it page-id)
|
|
(pcb/with-objects objects))
|
|
|
|
[frame-shape changes]
|
|
(prepare-create-artboard-from-selection changes
|
|
id
|
|
parent-id
|
|
objects
|
|
selected
|
|
index
|
|
nil
|
|
false)
|
|
|
|
undo-id (js/Symbol)]
|
|
|
|
(when changes
|
|
(rx/of
|
|
(dwu/start-undo-transaction undo-id)
|
|
(dch/commit-changes changes)
|
|
(dws/select-shapes (d/ordered-set (:id frame-shape)))
|
|
(ptk/data-event :layout/update [(:id frame-shape)])
|
|
(dwu/commit-undo-transaction undo-id))))))))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Shape Flags
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defn update-shape-flags
|
|
[ids {:keys [blocked hidden] :as flags}]
|
|
(dm/assert!
|
|
"expected valid coll of uuids"
|
|
(every? uuid? ids))
|
|
|
|
(dm/assert!
|
|
"expected valid shape-attrs value for `flags`"
|
|
(cts/shape-attrs? flags))
|
|
|
|
(ptk/reify ::update-shape-flags
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [update-fn
|
|
(fn [obj]
|
|
(cond-> obj
|
|
(boolean? blocked) (assoc :blocked blocked)
|
|
(boolean? hidden) (assoc :hidden hidden)))
|
|
objects (wsh/lookup-page-objects state)
|
|
;; We have change only the hidden behaviour, to hide only the
|
|
;; selected shape, block behaviour remains the same.
|
|
ids (if (boolean? blocked)
|
|
(into ids (->> ids (mapcat #(cph/get-children-ids objects %))))
|
|
ids)]
|
|
(rx/of (dch/update-shapes ids update-fn))))))
|
|
|
|
(defn toggle-visibility-selected
|
|
[]
|
|
(ptk/reify ::toggle-visibility-selected
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [selected (wsh/lookup-selected state)]
|
|
(rx/of (dch/update-shapes selected #(update % :hidden not)))))))
|
|
|
|
(defn toggle-lock-selected
|
|
[]
|
|
(ptk/reify ::toggle-lock-selected
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [selected (wsh/lookup-selected state)]
|
|
(rx/of (dch/update-shapes selected #(update % :blocked not)))))))
|
|
|
|
|
|
;; FIXME: this need to be refactored
|
|
|
|
(defn toggle-file-thumbnail-selected
|
|
[]
|
|
(ptk/reify ::toggle-file-thumbnail-selected
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [selected (wsh/lookup-selected state)
|
|
pages (-> state :workspace-data :pages-index vals)]
|
|
|
|
(rx/concat
|
|
;; First: clear the `:use-for-thumbnail?` flag from all not
|
|
;; selected frames.
|
|
(rx/from
|
|
(->> pages
|
|
(mapcat
|
|
(fn [{:keys [objects id] :as page}]
|
|
(->> (ctst/get-frames objects)
|
|
(sequence
|
|
(comp (filter :use-for-thumbnail?)
|
|
(map :id)
|
|
(remove selected)
|
|
(map (partial vector id)))))))
|
|
(d/group-by first second)
|
|
(map (fn [[page-id frame-ids]]
|
|
(dch/update-shapes frame-ids #(dissoc % :use-for-thumbnail?) {:page-id page-id})))))
|
|
|
|
;; And finally: toggle the flag value on all the selected shapes
|
|
(rx/of (dch/update-shapes selected #(update % :use-for-thumbnail? not))))))))
|