diff --git a/common/src/app/common/logic/libraries.cljc b/common/src/app/common/logic/libraries.cljc index 458bcb2ec..330fa596e 100644 --- a/common/src/app/common/logic/libraries.cljc +++ b/common/src/app/common/logic/libraries.cljc @@ -22,8 +22,10 @@ [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-tree :as ctst] + [app.common.types.shape.interactions :as ctsi] [app.common.types.shape.layout :as ctl] [app.common.types.typography :as cty] [app.common.uuid :as uuid] @@ -1923,3 +1925,295 @@ (cond-> changes (some? swap-slot) (generate-sync-head file-full libraries container id components-v2 true)))) + +(defn generate-duplicate-flows + [changes shapes page ids-map] + (let [flows (-> page :options :flows) + unames (volatile! (into #{} (map :name flows))) + frames-with-flow (->> shapes + (filter #(= (:type %) :frame)) + (filter #(some? (ctp/get-frame-flow flows (:id %)))))] + (if-not (empty? frames-with-flow) + (let [update-flows (fn [flows] + (reduce + (fn [flows frame] + (let [name (cfh/generate-unique-name @unames "Flow 1") + _ (vswap! unames conj name) + new-flow {:id (uuid/next) + :name name + :starting-frame (get ids-map (:id frame))}] + (ctp/add-flow flows new-flow))) + flows + frames-with-flow))] + (pcb/update-page-option changes :flows update-flows)) + changes))) + +(defn generate-duplicate-guides + [changes shapes page ids-map delta] + (let [guides (get-in page [:options :guides]) + frames (->> shapes (filter cfh/frame-shape?)) + + new-guides + (reduce + (fn [g frame] + (let [new-id (ids-map (:id frame)) + new-frame (-> frame (gsh/move delta)) + + new-guides + (->> guides + (vals) + (filter #(= (:frame-id %) (:id frame))) + (map #(-> % + (assoc :id (uuid/next)) + (assoc :frame-id new-id) + (assoc :position (if (= (:axis %) :x) + (+ (:position %) (- (:x new-frame) (:x frame))) + (+ (:position %) (- (:y new-frame) (:y frame))))))))] + (cond-> g + (not-empty new-guides) + (conj (into {} (map (juxt :id identity) new-guides)))))) + guides + frames)] + (-> (pcb/with-page changes page) + (pcb/set-page-option :guides new-guides)))) + +(defn generate-duplicate-component-change + [changes objects page component-root parent-id frame-id delta libraries library-data] + (let [component-id (:component-id component-root) + file-id (:component-file component-root) + main-component (ctf/get-component libraries file-id component-id) + moved-component (gsh/move component-root delta) + pos (gpt/point (:x moved-component) (:y moved-component)) + origin-frame (get-in page [:objects frame-id]) + delta (cond-> delta + (some? origin-frame) + (gpt/subtract (-> origin-frame :selrect gpt/point))) + + instantiate-component + #(generate-instantiate-component changes + objects + file-id + (:component-id component-root) + pos + page + libraries + (:id component-root) + parent-id + frame-id + {}) + + restore-component + #(let [restore (prepare-restore-component changes library-data (:component-id component-root) page delta (:id component-root) parent-id frame-id)] + [(:shape restore) (:changes restore)]) + + [_shape changes] + (if (nil? main-component) + (restore-component) + (instantiate-component))] + changes)) + +(defn generate-duplicate-shape-change + ([changes objects page unames update-unames! ids-map obj delta level-delta libraries library-data file-id] + (generate-duplicate-shape-change changes objects page unames update-unames! ids-map obj delta level-delta libraries library-data file-id (:frame-id obj) (:parent-id obj) false false true)) + + ([changes objects page unames update-unames! ids-map obj delta level-delta libraries library-data file-id frame-id parent-id duplicating-component? child? remove-swap-slot?] + (cond + (nil? obj) + changes + + (ctf/is-main-of-known-component? obj libraries) + (generate-duplicate-component-change changes objects page obj parent-id frame-id delta libraries library-data) + + :else + (let [frame? (cfh/frame-shape? obj) + group? (cfh/group-shape? obj) + bool? (cfh/bool-shape? obj) + new-id (ids-map (:id obj)) + parent-id (or parent-id frame-id) + parent (get objects parent-id) + name (:name obj) + + is-component-root? (or (:saved-component-root obj) + ;; Backward compatibility + (:saved-component-root? obj) + (ctk/instance-root? obj)) + duplicating-component? (or duplicating-component? (ctk/instance-head? obj)) + is-component-main? (ctk/main-instance? obj) + subinstance-head? (ctk/subinstance-head? obj) + instance-root? (ctk/instance-root? obj) + + into-component? (and duplicating-component? + (ctn/in-any-component? objects parent)) + + level-delta (if (some? level-delta) + level-delta + (ctn/get-nesting-level-delta objects obj parent)) + new-shape-ref (ctf/advance-shape-ref nil page libraries obj level-delta {:include-deleted? true}) + + regenerate-component + (fn [changes shape] + (let [components-v2 (dm/get-in library-data [:options :components-v2]) + [_ changes] (generate-add-component-changes changes shape objects file-id (:id page) components-v2)] + changes)) + + new-obj + (-> obj + (assoc :id new-id + :name name + :parent-id parent-id + :frame-id frame-id) + + (cond-> (and (not instance-root?) + subinstance-head? + remove-swap-slot?) + (ctk/remove-swap-slot)) + + (dissoc :shapes + :use-for-thumbnail) + + (cond-> (not is-component-root?) + (dissoc :main-instance)) + + (cond-> into-component? + (dissoc :component-root)) + + (cond-> (and (ctk/instance-head? obj) + (not into-component?)) + (assoc :component-root true)) + + (cond-> (or frame? group? bool?) + (assoc :shapes [])) + + (cond-> (and (some? new-shape-ref) + (not= new-shape-ref (:shape-ref obj))) + (assoc :shape-ref new-shape-ref)) + + (gsh/move delta) + (d/update-when :interactions #(ctsi/remap-interactions % ids-map objects)) + + (cond-> (ctl/grid-layout? obj) + (ctl/remap-grid-cells ids-map))) + + new-obj (cond-> new-obj + (not duplicating-component?) + (ctk/detach-shape)) + + ;; We want the first added object to touch it's parent, but not subsequent children + changes (-> (pcb/add-object changes new-obj {:ignore-touched (and duplicating-component? child?)}) + (pcb/amend-last-change #(assoc % :old-id (:id obj))) + (cond-> (ctl/grid-layout? objects (:parent-id obj)) + (-> (pcb/update-shapes [(:parent-id obj)] ctl/assign-cells {:with-objects? true}) + (pcb/reorder-grid-children [(:parent-id obj)])))) + + changes (cond-> changes + (and is-component-root? is-component-main?) + (regenerate-component new-obj)) + + ;; This is needed for the recursive call to find the new object as parent + page' (ctst/add-shape (:id new-obj) + new-obj + {:objects objects} + (:frame-id new-obj) + (:parent-id new-obj) + nil + true)] + + (reduce (fn [changes child] + (generate-duplicate-shape-change changes + (:objects page') + page + unames + update-unames! + ids-map + child + delta + level-delta + libraries + library-data + file-id + (if frame? new-id frame-id) + new-id + duplicating-component? + true + (and remove-swap-slot? + ;; only remove swap slot of children when the current shape + ;; is not a subinstance head nor a instance root + (not subinstance-head?) + (not instance-root?)))) + changes + (map (d/getf objects) (:shapes obj))))))) + +(defn generate-duplicate-changes + "Prepare objects to duplicate: generate new id, give them unique names, + move to the desired position, and recalculate parents and frames as needed." + [changes all-objects page ids delta libraries library-data file-id] + (let [shapes (map (d/getf all-objects) ids) + unames (volatile! (cfh/get-used-names (:objects page))) + update-unames! (fn [new-name] (vswap! unames conj new-name)) + all-ids (reduce #(into %1 (cons %2 (cfh/get-children-ids all-objects %2))) (d/ordered-set) ids) + + ;; We need ids-map for remapping the grid layout. But when duplicating the guides + ;; we calculate a new one because the components will have created new shapes. + ids-map (into {} (map #(vector % (uuid/next))) all-ids) + + changes (-> changes + (pcb/with-page page) + (pcb/with-objects all-objects)) + changes + (->> shapes + (reduce #(generate-duplicate-shape-change %1 + all-objects + page + unames + update-unames! + ids-map + %2 + delta + nil + libraries + library-data + file-id) + changes)) + + ;; We need to check the changes to get the ids-map + ids-map + (into {} + (comp + (filter #(= :add-obj (:type %))) + (map #(vector (:old-id %) (-> % :obj :id)))) + (:redo-changes changes))] + + (-> changes + (generate-duplicate-flows shapes page ids-map) + (generate-duplicate-guides shapes page ids-map delta)))) + +(defn generate-duplicate-changes-update-indices + "Updates the changes to correctly set the indexes of the duplicated objects, + depending on the index of the original object respect their parent." + [changes objects ids] + (let [;; index-map is a map that goes from parent-id => vector([id index-in-parent]) + index-map (reduce (fn [index-map id] + (let [parent-id (get-in objects [id :parent-id]) + parent-index (cfh/get-position-on-parent objects id)] + (update index-map parent-id (fnil conj []) [id parent-index]))) + {} + ids) + + inc-indices + (fn [[offset result] [id index]] + [(inc offset) (conj result [id (+ index offset)])]) + + fix-indices + (fn [_ entry] + (->> entry + (sort-by second) + (reduce inc-indices [1 []]) + (second) + (into {}))) + + objects-indices (->> index-map (d/mapm fix-indices) (vals) (reduce merge))] + + (pcb/amend-changes + changes + (fn [change] + (assoc change :index (get objects-indices (:old-id change))))))) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index bb88913c4..ea52674f5 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -19,6 +19,7 @@ [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.common.geom.shapes.grid-layout :as gslg] + [app.common.logic.libraries :as cll] [app.common.logic.shapes :as cls] [app.common.schema :as sm] [app.common.text :as txt] @@ -1868,7 +1869,8 @@ drop-cell (when (ctl/grid-layout? all-objects parent-id) (gslg/get-drop-cell frame-id all-objects position)) - changes (-> (dws/prepare-duplicate-changes all-objects page selected delta it libraries ldata file-id) + changes (-> (pcb/empty-changes it) + (cll/generate-duplicate-changes all-objects page selected delta libraries ldata file-id) (pcb/amend-changes (partial process-rchange media-idx)) (pcb/amend-changes (partial change-add-obj-index objects selected index))) diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs index e0dee732f..f05cf5d5e 100644 --- a/frontend/src/app/main/data/workspace/selection.cljs +++ b/frontend/src/app/main/data/workspace/selection.cljs @@ -17,12 +17,6 @@ [app.common.logic.libraries :as cll] [app.common.record :as cr] [app.common.types.component :as ctk] - [app.common.types.container :as ctn] - [app.common.types.file :as ctf] - [app.common.types.page :as ctp] - [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.events :as ev] [app.main.data.modal :as md] @@ -368,308 +362,6 @@ (rx/of (select-shape (:id selected)))))))) ;; --- Duplicate Shapes -(declare prepare-duplicate-shape-change) -(declare prepare-duplicate-flows) -(declare prepare-duplicate-guides) - -(defn prepare-duplicate-changes - "Prepare objects to duplicate: generate new id, give them unique names, - move to the desired position, and recalculate parents and frames as needed." - ([all-objects page ids delta it libraries library-data file-id] - (let [init-changes - (-> (pcb/empty-changes it) - (pcb/with-page page) - (pcb/with-objects all-objects))] - (prepare-duplicate-changes all-objects page ids delta it libraries library-data file-id init-changes))) - - ([all-objects page ids delta it libraries library-data file-id init-changes] - (let [shapes (map (d/getf all-objects) ids) - unames (volatile! (cfh/get-used-names (:objects page))) - update-unames! (fn [new-name] (vswap! unames conj new-name)) - all-ids (reduce #(into %1 (cons %2 (cfh/get-children-ids all-objects %2))) (d/ordered-set) ids) - - ;; We need ids-map for remapping the grid layout. But when duplicating the guides - ;; we calculate a new one because the components will have created new shapes. - ids-map (into {} (map #(vector % (uuid/next))) all-ids) - - changes - (->> shapes - (reduce #(prepare-duplicate-shape-change %1 - all-objects - page - unames - update-unames! - ids-map - %2 - delta - nil - libraries - library-data - it - file-id) - init-changes)) - - ;; We need to check the changes to get the ids-map - ids-map - (into {} - (comp - (filter #(= :add-obj (:type %))) - (map #(vector (:old-id %) (-> % :obj :id)))) - (:redo-changes changes))] - - (-> changes - (prepare-duplicate-flows shapes page ids-map) - (prepare-duplicate-guides shapes page ids-map delta))))) - -(defn- prepare-duplicate-component-change - [changes objects page component-root parent-id frame-id delta libraries library-data] - (let [component-id (:component-id component-root) - file-id (:component-file component-root) - main-component (ctf/get-component libraries file-id component-id) - moved-component (gsh/move component-root delta) - pos (gpt/point (:x moved-component) (:y moved-component)) - origin-frame (get-in page [:objects frame-id]) - delta (cond-> delta - (some? origin-frame) - (gpt/subtract (-> origin-frame :selrect gpt/point))) - - instantiate-component - #(cll/generate-instantiate-component changes - objects - file-id - (:component-id component-root) - pos - page - libraries - (:id component-root) - parent-id - frame-id - {}) - - restore-component - #(let [restore (cll/prepare-restore-component changes library-data (:component-id component-root) page delta (:id component-root) parent-id frame-id)] - [(:shape restore) (:changes restore)]) - - [_shape changes] - (if (nil? main-component) - (restore-component) - (instantiate-component))] - changes)) - -;; TODO: move to common.files.shape-helpers -(defn- prepare-duplicate-shape-change - ([changes objects page unames update-unames! ids-map obj delta level-delta libraries library-data it file-id] - (prepare-duplicate-shape-change changes objects page unames update-unames! ids-map obj delta level-delta libraries library-data it file-id (:frame-id obj) (:parent-id obj) false false true)) - - ([changes objects page unames update-unames! ids-map obj delta level-delta libraries library-data it file-id frame-id parent-id duplicating-component? child? remove-swap-slot?] - (cond - (nil? obj) - changes - - (ctf/is-main-of-known-component? obj libraries) - (prepare-duplicate-component-change changes objects page obj parent-id frame-id delta libraries library-data) - - :else - (let [frame? (cfh/frame-shape? obj) - group? (cfh/group-shape? obj) - bool? (cfh/bool-shape? obj) - new-id (ids-map (:id obj)) - parent-id (or parent-id frame-id) - parent (get objects parent-id) - name (:name obj) - - is-component-root? (or (:saved-component-root obj) - ;; Backward compatibility - (:saved-component-root? obj) - (ctk/instance-root? obj)) - duplicating-component? (or duplicating-component? (ctk/instance-head? obj)) - is-component-main? (ctk/main-instance? obj) - subinstance-head? (ctk/subinstance-head? obj) - instance-root? (ctk/instance-root? obj) - - into-component? (and duplicating-component? - (ctn/in-any-component? objects parent)) - - level-delta (if (some? level-delta) - level-delta - (ctn/get-nesting-level-delta objects obj parent)) - new-shape-ref (ctf/advance-shape-ref nil page libraries obj level-delta {:include-deleted? true}) - - regenerate-component - (fn [changes shape] - (let [components-v2 (dm/get-in library-data [:options :components-v2]) - [_ changes] (cll/generate-add-component-changes changes shape objects file-id (:id page) components-v2)] - changes)) - - new-obj - (-> obj - (assoc :id new-id - :name name - :parent-id parent-id - :frame-id frame-id) - - (cond-> (and (not instance-root?) - subinstance-head? - remove-swap-slot?) - (ctk/remove-swap-slot)) - - (dissoc :shapes - :use-for-thumbnail) - - (cond-> (not is-component-root?) - (dissoc :main-instance)) - - (cond-> into-component? - (dissoc :component-root)) - - (cond-> (and (ctk/instance-head? obj) - (not into-component?)) - (assoc :component-root true)) - - (cond-> (or frame? group? bool?) - (assoc :shapes [])) - - (cond-> (and (some? new-shape-ref) - (not= new-shape-ref (:shape-ref obj))) - (assoc :shape-ref new-shape-ref)) - - (gsh/move delta) - (d/update-when :interactions #(ctsi/remap-interactions % ids-map objects)) - - (cond-> (ctl/grid-layout? obj) - (ctl/remap-grid-cells ids-map))) - - new-obj (cond-> new-obj - (not duplicating-component?) - (ctk/detach-shape)) - - ;; We want the first added object to touch it's parent, but not subsequent children - changes (-> (pcb/add-object changes new-obj {:ignore-touched (and duplicating-component? child?)}) - (pcb/amend-last-change #(assoc % :old-id (:id obj))) - (cond-> (ctl/grid-layout? objects (:parent-id obj)) - (-> (pcb/update-shapes [(:parent-id obj)] ctl/assign-cells {:with-objects? true}) - (pcb/reorder-grid-children [(:parent-id obj)])))) - - changes (cond-> changes - (and is-component-root? is-component-main?) - (regenerate-component new-obj)) - - ;; This is needed for the recursive call to find the new object as parent - page' (ctst/add-shape (:id new-obj) - new-obj - {:objects objects} - (:frame-id new-obj) - (:parent-id new-obj) - nil - true)] - - (reduce (fn [changes child] - (prepare-duplicate-shape-change changes - (:objects page') - page - unames - update-unames! - ids-map - child - delta - level-delta - libraries - library-data - it - file-id - (if frame? new-id frame-id) - new-id - duplicating-component? - true - (and remove-swap-slot? - ;; only remove swap slot of children when the current shape - ;; is not a subinstance head nor a instance root - (not subinstance-head?) - (not instance-root?)))) - changes - (map (d/getf objects) (:shapes obj))))))) - -(defn- prepare-duplicate-flows - [changes shapes page ids-map] - (let [flows (-> page :options :flows) - unames (volatile! (into #{} (map :name flows))) - frames-with-flow (->> shapes - (filter #(= (:type %) :frame)) - (filter #(some? (ctp/get-frame-flow flows (:id %)))))] - (if-not (empty? frames-with-flow) - (let [update-flows (fn [flows] - (reduce - (fn [flows frame] - (let [name (cfh/generate-unique-name @unames "Flow 1") - _ (vswap! unames conj name) - new-flow {:id (uuid/next) - :name name - :starting-frame (get ids-map (:id frame))}] - (ctp/add-flow flows new-flow))) - flows - frames-with-flow))] - (pcb/update-page-option changes :flows update-flows)) - changes))) - -(defn- prepare-duplicate-guides - [changes shapes page ids-map delta] - (let [guides (get-in page [:options :guides]) - frames (->> shapes (filter cfh/frame-shape?)) - - new-guides - (reduce - (fn [g frame] - (let [new-id (ids-map (:id frame)) - new-frame (-> frame (gsh/move delta)) - - new-guides - (->> guides - (vals) - (filter #(= (:frame-id %) (:id frame))) - (map #(-> % - (assoc :id (uuid/next)) - (assoc :frame-id new-id) - (assoc :position (if (= (:axis %) :x) - (+ (:position %) (- (:x new-frame) (:x frame))) - (+ (:position %) (- (:y new-frame) (:y frame))))))))] - (cond-> g - (not-empty new-guides) - (conj (into {} (map (juxt :id identity) new-guides)))))) - guides - frames)] - (-> (pcb/with-page changes page) - (pcb/set-page-option :guides new-guides)))) - -(defn duplicate-changes-update-indices - "Updates the changes to correctly set the indexes of the duplicated objects, - depending on the index of the original object respect their parent." - [objects ids changes] - (let [;; index-map is a map that goes from parent-id => vector([id index-in-parent]) - index-map (reduce (fn [index-map id] - (let [parent-id (get-in objects [id :parent-id]) - parent-index (cfh/get-position-on-parent objects id)] - (update index-map parent-id (fnil conj []) [id parent-index]))) - {} - ids) - - inc-indices - (fn [[offset result] [id index]] - [(inc offset) (conj result [id (+ index offset)])]) - - fix-indices - (fn [_ entry] - (->> entry - (sort-by second) - (reduce inc-indices [1 []]) - (second) - (into {}))) - - objects-indices (->> index-map (d/mapm fix-indices) (vals) (reduce merge))] - - (pcb/amend-changes - changes - (fn [change] - (assoc change :index (get objects-indices (:old-id change))))))) (defn clear-memorize-duplicated [] @@ -746,8 +438,9 @@ libraries (wsh/get-libraries state) library-data (wsh/get-file state file-id) - changes (->> (prepare-duplicate-changes objects page ids delta it libraries library-data file-id) - (duplicate-changes-update-indices objects ids)) + changes (-> (pcb/empty-changes it) + (cll/generate-duplicate-changes objects page ids delta libraries library-data file-id) + (cll/generate-duplicate-changes-update-indices objects ids)) tags (or (:tags changes) #{}) diff --git a/frontend/src/app/main/data/workspace/shape_layout.cljs b/frontend/src/app/main/data/workspace/shape_layout.cljs index b3c0d513f..765973787 100644 --- a/frontend/src/app/main/data/workspace/shape_layout.cljs +++ b/frontend/src/app/main/data/workspace/shape_layout.cljs @@ -15,6 +15,7 @@ [app.common.geom.point :as gpt] [app.common.geom.shapes.flex-layout :as flex] [app.common.geom.shapes.grid-layout :as grid] + [app.common.logic.libraries :as cll] [app.common.types.component :as ctc] [app.common.types.modifiers :as ctm] [app.common.types.shape.layout :as ctl] @@ -339,8 +340,9 @@ selected (set shapes-by-track) changes - (->> (dwse/prepare-duplicate-changes objects page selected (gpt/point 0 0) it libraries library-data file-id) - (dwse/duplicate-changes-update-indices objects selected)) + (-> (pcb/empty-changes it) + (cll/generate-duplicate-changes objects page selected (gpt/point 0 0) libraries library-data file-id) + (cll/generate-duplicate-changes-update-indices objects selected)) ;; Creates a map with shape-id => duplicated-shape-id ids-map