mirror of
https://github.com/penpot/penpot.git
synced 2025-02-13 10:38:13 -05:00
✨ Adds new method to move objects
This commit is contained in:
parent
20c6ae867b
commit
e6200aae4c
4 changed files with 260 additions and 56 deletions
|
@ -15,19 +15,42 @@
|
|||
|
||||
(t/deftest process-change-add-obj-1
|
||||
(let [data cp/default-page-data
|
||||
id (uuid/next)
|
||||
chg {:type :add-obj
|
||||
:id id
|
||||
id-a (uuid/next)
|
||||
id-b (uuid/next)
|
||||
id-c (uuid/next)]
|
||||
(t/testing "Adds single object"
|
||||
(let [chg {:type :add-obj
|
||||
:id id-a
|
||||
:frame-id uuid/zero
|
||||
:obj {:id id
|
||||
:obj {:id id-a
|
||||
:frame-id uuid/zero
|
||||
:type :rect
|
||||
:name "rect"}}
|
||||
res (cp/process-changes data [chg])]
|
||||
|
||||
(t/is (= 2 (count (:objects res))))
|
||||
(t/is (= (:obj chg) (get-in res [:objects id])))
|
||||
(t/is (= [id] (get-in res [:objects uuid/zero :shapes])))))
|
||||
(t/is (= (:obj chg) (get-in res [:objects id-a])))
|
||||
(t/is (= [id-a] (get-in res [:objects uuid/zero :shapes])))))
|
||||
(t/testing "Adds several objects with different indexes"
|
||||
(let [data cp/default-page-data
|
||||
|
||||
chg (fn [id index] {:type :add-obj
|
||||
:id id
|
||||
:frame-id uuid/zero
|
||||
:index index
|
||||
:obj {:id id
|
||||
:frame-id uuid/zero
|
||||
:type :rect
|
||||
:name (str id)}})
|
||||
res (cp/process-changes data [(chg id-a 0)
|
||||
(chg id-b 0)
|
||||
(chg id-c 1)])]
|
||||
|
||||
(t/is (= 4 (count (:objects res))))
|
||||
(t/is (not (nil? (get-in res [:objects id-a]))))
|
||||
(t/is (not (nil? (get-in res [:objects id-b]))))
|
||||
(t/is (not (nil? (get-in res [:objects id-c]))))
|
||||
(t/is (= [id-b id-c id-a] (get-in res [:objects uuid/zero :shapes])))))))
|
||||
|
||||
(t/deftest process-change-mod-obj
|
||||
(let [data cp/default-page-data
|
||||
|
@ -177,3 +200,115 @@
|
|||
(t/is (= [id3 id1 id2] (get-in res [:objects uuid/zero :shapes])))))
|
||||
))
|
||||
|
||||
(t/deftest process-changes-move-objects
|
||||
(let [frame-a-id (uuid/next)
|
||||
frame-b-id (uuid/next)
|
||||
group-a-id (uuid/next)
|
||||
group-b-id (uuid/next)
|
||||
rect-a-id (uuid/next)
|
||||
rect-b-id (uuid/next)
|
||||
rect-c-id (uuid/next)
|
||||
rect-d-id (uuid/next)
|
||||
rect-e-id (uuid/next)
|
||||
data (-> cp/default-page-data
|
||||
(assoc-in [cp/root :shapes] [frame-a-id])
|
||||
(assoc-in [:objects frame-a-id] {:id frame-a-id :name "Frame a" :type :frame})
|
||||
(assoc-in [:objects frame-b-id] {:id frame-b-id :name "Frame b" :type :frame})
|
||||
|
||||
;; Groups
|
||||
(assoc-in [:objects group-a-id] {:id group-a-id :name "Group A" :type :group :frame-id frame-a-id})
|
||||
(assoc-in [:objects group-b-id] {:id group-b-id :name "Group B" :type :group :frame-id frame-a-id})
|
||||
|
||||
;; Shapes
|
||||
(assoc-in [:objects rect-a-id] {:id rect-a-id :name "Rect A" :type :rect :frame-id frame-a-id})
|
||||
(assoc-in [:objects rect-b-id] {:id rect-b-id :name "Rect B" :type :rect :frame-id frame-a-id})
|
||||
(assoc-in [:objects rect-c-id] {:id rect-c-id :name "Rect C" :type :rect :frame-id frame-a-id})
|
||||
(assoc-in [:objects rect-d-id] {:id rect-d-id :name "Rect D" :type :rect :frame-id frame-a-id})
|
||||
(assoc-in [:objects rect-e-id] {:id rect-e-id :name "Rect E" :type :rect :frame-id frame-a-id})
|
||||
|
||||
;; Relationships
|
||||
(assoc-in [:objects cp/root :shapes] [frame-a-id frame-b-id])
|
||||
(assoc-in [:objects frame-a-id :shapes] [group-a-id group-b-id rect-e-id])
|
||||
(assoc-in [:objects group-a-id :shapes] [rect-a-id rect-b-id rect-c-id])
|
||||
(assoc-in [:objects group-b-id :shapes] [rect-d-id]))]
|
||||
(t/testing "Create new group an add objects from the same group"
|
||||
(let [new-group-id (uuid/next)
|
||||
changes [{:type :add-obj
|
||||
:id new-group-id
|
||||
:frame-id frame-a-id
|
||||
:obj {:id new-group-id
|
||||
:type :group
|
||||
:frame-id frame-a-id
|
||||
:name "Group C"}}
|
||||
{:type :mov-objects
|
||||
:parent-id new-group-id
|
||||
:shapes [rect-b-id rect-c-id]}]
|
||||
res (cp/process-changes data changes)]
|
||||
(t/is (= [group-a-id group-b-id rect-e-id new-group-id] (get-in res [:objects frame-a-id :shapes])))
|
||||
(t/is (= [rect-b-id rect-c-id] (get-in res [:objects new-group-id :shapes])))
|
||||
(t/is (= [rect-a-id] (get-in res [:objects group-a-id :shapes])))))
|
||||
|
||||
(t/testing "Move elements to an existing group at index"
|
||||
(let [changes [{:type :mov-objects
|
||||
:parent-id group-b-id
|
||||
:index 0
|
||||
:shapes [rect-a-id rect-c-id]}]
|
||||
res (cp/process-changes data changes)]
|
||||
(t/is (= [group-a-id group-b-id rect-e-id] (get-in res [:objects frame-a-id :shapes])))
|
||||
(t/is (= [rect-b-id] (get-in res [:objects group-a-id :shapes])))
|
||||
(t/is (= [rect-a-id rect-c-id rect-d-id] (get-in res [:objects group-b-id :shapes])))))
|
||||
|
||||
(t/testing "Move elements from group and frame to an existing group at index"
|
||||
(let [changes [{:type :mov-objects
|
||||
:parent-id group-b-id
|
||||
:index 0
|
||||
:shapes [rect-a-id rect-e-id]}]
|
||||
res (cp/process-changes data changes)]
|
||||
(t/is (= [group-a-id group-b-id] (get-in res [:objects frame-a-id :shapes])))
|
||||
(t/is (= [rect-b-id rect-c-id] (get-in res [:objects group-a-id :shapes])))
|
||||
(t/is (= [rect-a-id rect-e-id rect-d-id] (get-in res [:objects group-b-id :shapes])))))
|
||||
|
||||
(t/testing "Move elements from several groups"
|
||||
(let [changes [{:type :mov-objects
|
||||
:parent-id group-b-id
|
||||
:index 0
|
||||
:shapes [rect-a-id rect-e-id]}]
|
||||
res (cp/process-changes data changes)]
|
||||
(t/is (= [group-a-id group-b-id] (get-in res [:objects frame-a-id :shapes])))
|
||||
(t/is (= [rect-b-id rect-c-id] (get-in res [:objects group-a-id :shapes])))
|
||||
(t/is (= [rect-a-id rect-e-id rect-d-id] (get-in res [:objects group-b-id :shapes])))))
|
||||
|
||||
(t/testing "Move elements and delete the empty group"
|
||||
(let [changes [{:type :mov-objects
|
||||
:parent-id group-a-id
|
||||
:shapes [rect-d-id]}]
|
||||
res (cp/process-changes data changes)]
|
||||
(t/is (= [group-a-id rect-e-id] (get-in res [:objects frame-a-id :shapes])))
|
||||
(t/is (nil? (get-in res [:objects group-b-id])))))
|
||||
|
||||
(t/testing "Move elements to a group with different frame"
|
||||
(let [changes [{:type :mov-objects
|
||||
:parent-id frame-b-id
|
||||
:shapes [group-a-id]}]
|
||||
res (cp/process-changes data changes)]
|
||||
(t/is (= [group-b-id rect-e-id] (get-in res [:objects frame-a-id :shapes])))
|
||||
(t/is (= [group-a-id] (get-in res [:objects frame-b-id :shapes])))
|
||||
(t/is (= frame-b-id (get-in res [:objects group-a-id :frame-id])))
|
||||
(t/is (= frame-b-id (get-in res [:objects rect-a-id :frame-id])))
|
||||
(t/is (= frame-b-id (get-in res [:objects rect-b-id :frame-id])))
|
||||
(t/is (= frame-b-id (get-in res [:objects rect-c-id :frame-id])))))
|
||||
|
||||
(t/testing "Move elements to frame zero"
|
||||
(let [changes [{:type :mov-objects
|
||||
:parent-id cp/root
|
||||
:shapes [group-a-id]
|
||||
:index 0}]
|
||||
res (cp/process-changes data changes)]
|
||||
(t/is (= [group-a-id frame-a-id frame-b-id] (get-in res [:objects cp/root :shapes])))))
|
||||
|
||||
(t/testing "Don't allow to move inside self"
|
||||
(let [changes [{:type :mov-objects
|
||||
:parent-id group-a-id
|
||||
:shapes [group-a-id]}]
|
||||
res (cp/process-changes data changes)]
|
||||
(t/is (= data res))))))
|
||||
|
|
|
@ -90,6 +90,11 @@
|
|||
(persistent!
|
||||
(reduce #(dissoc! %1 %2) (transient data) keys)))
|
||||
|
||||
(defn insert-at-index [vector index elements]
|
||||
(let [[before after] (split-at index vector)]
|
||||
(concat [] before elements after)))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Data Parsing / Conversion
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
(s/def ::shape-id uuid?)
|
||||
(s/def ::session-id uuid?)
|
||||
(s/def ::name string?)
|
||||
(s/def ::parent-id uuid?)
|
||||
|
||||
;; Page Options
|
||||
(s/def ::grid-x number?)
|
||||
|
@ -151,6 +152,10 @@
|
|||
(s/keys :req-un [::id ::frame-id]
|
||||
:opt-un [::session-id]))
|
||||
|
||||
(defmethod change-spec-impl :mov-objects [_]
|
||||
(s/keys :req-un [::parent-id ::shapes]
|
||||
:opt-un [::index]))
|
||||
|
||||
(s/def ::change (s/multi-spec change-spec-impl :type))
|
||||
(s/def ::changes (s/coll-of ::change))
|
||||
|
||||
|
@ -201,15 +206,9 @@
|
|||
(update-in [:objects frame-id :shapes]
|
||||
(fn [shapes]
|
||||
(cond
|
||||
(some #{id} shapes)
|
||||
shapes
|
||||
|
||||
(nil? index)
|
||||
(conj shapes id)
|
||||
|
||||
:else
|
||||
(let [[before after] (split-at index shapes)]
|
||||
(d/concat [] before [id] after))))))))
|
||||
(some #{id} shapes) shapes
|
||||
(nil? index) (conj shapes id)
|
||||
:else (d/insert-at-index shapes index [id])))))))
|
||||
|
||||
(defmethod process-change :mod-obj
|
||||
[data {:keys [id operations] :as change}]
|
||||
|
@ -238,6 +237,73 @@
|
|||
(seq shapes) ; Recursive delete all dependend objects
|
||||
(as-> $ (reduce #(or (process-change %1 {:type :del-obj :id %2}) %1) $ shapes))))))
|
||||
|
||||
(defn- calculate-child-parent-map
|
||||
[objects]
|
||||
(let [red-fn
|
||||
(fn [acc {:keys [id shapes]}]
|
||||
;; Insert every pair shape -> parent into accumulated value
|
||||
(into acc (map #(vector % id) (or shapes []))))]
|
||||
(reduce red-fn {} (vals objects))))
|
||||
|
||||
(defn- calculate-invalid-targets [shape-id objects]
|
||||
(let [result #{shape-id}
|
||||
children (get-in objects [shape-id :shape])
|
||||
reduce-fn (fn [result child-id]
|
||||
(into result (calculate-invalid-targets child-id objects)))]
|
||||
(reduce reduce-fn result children)))
|
||||
|
||||
(defmethod process-change :mov-objects
|
||||
[data {:keys [parent-id shapes index] :as change}]
|
||||
(let [child->parent (calculate-child-parent-map (:objects data))
|
||||
;; Check if the move from shape-id -> parent-id is valid
|
||||
is-valid-move
|
||||
(fn [shape-id]
|
||||
(let [invalid (calculate-invalid-targets shape-id (:objects data))]
|
||||
(not (invalid parent-id))))
|
||||
|
||||
valid? (every? is-valid-move shapes)
|
||||
|
||||
;; Add items into the :shapes property of the target parent-id
|
||||
add-items
|
||||
(fn [old-shapes]
|
||||
(let [old-shapes (or old-shapes [])]
|
||||
(if index
|
||||
(d/insert-at-index old-shapes index shapes)
|
||||
(into old-shapes shapes))))
|
||||
|
||||
;; Remove from the old :shapes the references that have been moved
|
||||
remove-in-parent
|
||||
(fn [data shape-id]
|
||||
(let [parent-id (child->parent shape-id)
|
||||
filter-shape (partial filterv #(not (= % shape-id)) )
|
||||
data-removed (update-in data [:objects parent-id :shapes] filter-shape)
|
||||
parent (get-in data-removed [:objects parent-id])]
|
||||
;; When the group is empty we should remove it
|
||||
(if (and (= :group (:type parent))
|
||||
(empty? (:shapes parent)))
|
||||
(-> data-removed
|
||||
(update :objects dissoc parent-id )
|
||||
(recur parent-id))
|
||||
data-removed)))
|
||||
|
||||
;; Frame-id of the target element
|
||||
frame-id (if (= :frame (get-in data [:objects parent-id :type]))
|
||||
parent-id
|
||||
(get-in data [:objects parent-id :frame-id]))
|
||||
|
||||
;; Updates the frame-id references that might be outdated
|
||||
update-frame-ids
|
||||
(fn update-frame-ids [data shape-id]
|
||||
(as-> data $
|
||||
(assoc-in $ [:objects shape-id :frame-id] frame-id)
|
||||
(reduce update-frame-ids $ (get-in $ [:objects shape-id :shapes]))))]
|
||||
(if valid?
|
||||
(as-> data $
|
||||
(update-in $ [:objects parent-id :shapes] add-items)
|
||||
(reduce remove-in-parent $ shapes)
|
||||
(reduce update-frame-ids $ (get-in $ [:objects parent-id :shapes])))
|
||||
data)))
|
||||
|
||||
(defmethod process-operation :set
|
||||
[shape op]
|
||||
(let [attr (:attr op)
|
||||
|
|
|
@ -2211,7 +2211,7 @@
|
|||
{:id id
|
||||
:type :group
|
||||
:name (name (gensym "Group-"))
|
||||
:shapes (vec selected)
|
||||
:shapes []
|
||||
:frame-id frame-id
|
||||
:x (:x selection-rect)
|
||||
:y (:y selection-rect)
|
||||
|
@ -2227,30 +2227,26 @@
|
|||
(if (not-empty selected)
|
||||
(let [page-id (get-in state [:workspace-page :id])
|
||||
objects (get-in state [:workspace-data page-id :objects])
|
||||
parent (get-parent (first selected) (vals objects))
|
||||
parent-id (:id parent)
|
||||
selected-objects (map (partial get objects) selected)
|
||||
selection-rect (geom/selection-rect selected-objects)
|
||||
frame-id (-> selected-objects first :frame-id)
|
||||
group-shape (group-shape id frame-id selected selection-rect)]
|
||||
group-shape (group-shape id frame-id selected selection-rect)
|
||||
frame-children (get-in objects [frame-id :shapes])
|
||||
index-frame (->> frame-children (map-indexed vector) (filter #(selected (second %))) first first)]
|
||||
|
||||
(let [updated-parent (helpers/replace-shapes parent selected id)
|
||||
rchanges [{:type :add-obj
|
||||
(let [rchanges [{:type :add-obj
|
||||
:id id
|
||||
:frame-id frame-id
|
||||
:obj group-shape}
|
||||
{:type :mod-obj
|
||||
:id parent-id
|
||||
:operations [{:type :set
|
||||
:attr :shapes
|
||||
:val (:shapes updated-parent)}]}]
|
||||
uchanges [{:type :del-obj
|
||||
:id id}
|
||||
{:type :mod-obj
|
||||
:id parent-id
|
||||
:operations [{:type :set
|
||||
:attr :shapes
|
||||
:val (:shapes parent)}]}]]
|
||||
:obj group-shape
|
||||
:index index-frame}
|
||||
{:type :mov-objects
|
||||
:parent-id id
|
||||
:shapes (into [] selected)}]
|
||||
uchanges [{:type :mov-objects
|
||||
:parent-id frame-id
|
||||
:shapes (into [] selected)}
|
||||
{:type :del-obj
|
||||
:id id}]]
|
||||
(rx/of (commit-changes rchanges uchanges {:commit-local? true})
|
||||
(fn [state] (assoc-in state [:workspace-local :selected] #{id})))))
|
||||
rx/empty))))))
|
||||
|
@ -2264,26 +2260,28 @@
|
|||
group (get-in state [:workspace-data (::page-id state) :objects group-id])]
|
||||
(if (and (= (count selected) 1) (= (:type group) :group))
|
||||
(let [objects (get-in state [:workspace-data (::page-id state) :objects])
|
||||
shapes (get-in objects [group-id :shapes])
|
||||
parent-id (helpers/get-parent group-id objects)
|
||||
parent (get objects parent-id)]
|
||||
(let [changed-parent (helpers/replace-shapes parent group-id (:shapes group))
|
||||
rchanges [{:type :mod-obj
|
||||
:id parent-id
|
||||
:operations [{:type :set :attr :shapes :val (:shapes changed-parent)}]}
|
||||
|
||||
;; Need to modify the object otherwise the children will be deleted
|
||||
{:type :mod-obj
|
||||
:id group-id
|
||||
:operations [{:type :set :attr :shapes :val []}]}
|
||||
{:type :del-obj
|
||||
:id group-id}]
|
||||
parent (get objects parent-id)
|
||||
index-in-parent (->> (:shapes parent)
|
||||
(map-indexed vector)
|
||||
(filter #(#{group-id} (second %)))
|
||||
first first)]
|
||||
(let [rchanges [{:type :mov-objects
|
||||
:parent-id parent-id
|
||||
:shapes shapes
|
||||
:index index-in-parent}]
|
||||
uchanges [{:type :add-obj
|
||||
:id group-id
|
||||
:frame-id (:frame-id group)
|
||||
:obj group}
|
||||
{:type :mod-obj
|
||||
:id parent-id
|
||||
:operations [{:type :set :attr :shapes :val (:shapes parent)}]}]]
|
||||
:obj (assoc group :shapes [])}
|
||||
{:type :mov-objects
|
||||
:parent-id group-id
|
||||
:shapes shapes}
|
||||
{:type :mov-objects
|
||||
:parent-id parent-id
|
||||
:shapes [group-id]
|
||||
:index index-in-parent}]]
|
||||
(rx/of (commit-changes rchanges uchanges {:commit-local? true}))))
|
||||
rx/empty)))))
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue