0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-22 14:39:45 -05:00

♻️ Components refactor: move generators for duplicate

This commit is contained in:
Pablo Alba 2024-05-07 19:42:57 +02:00 committed by Andrés Moya
parent d92faaa6c6
commit bfe9caba15
4 changed files with 304 additions and 313 deletions

View file

@ -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)))))))

View file

@ -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)))

View file

@ -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) #{})

View file

@ -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