🎉 Allow duplicate/copy-paste/cut-paste variants

This commit is contained in:
Pablo Alba 2025-04-01 11:07:22 +02:00 committed by GitHub
parent 076d64df8f
commit f04229d8cb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 191 additions and 103 deletions

View file

@ -352,7 +352,8 @@
[:map {:title "RestoreComponentChange"} [:map {:title "RestoreComponentChange"}
[:type [:= :restore-component]] [:type [:= :restore-component]]
[:id ::sm/uuid] [:id ::sm/uuid]
[:page-id ::sm/uuid]]] [:page-id ::sm/uuid]
[:parent-id {:optional true} [:maybe ::sm/uuid]]]]
[:purge-component [:purge-component
[:map {:title "PurgeComponentChange"} [:map {:title "PurgeComponentChange"}
@ -963,8 +964,8 @@
(ctf/delete-component data id skip-undelete? main-instance)) (ctf/delete-component data id skip-undelete? main-instance))
(defmethod process-change :restore-component (defmethod process-change :restore-component
[data {:keys [id page-id]}] [data {:keys [id page-id parent-id]}]
(ctf/restore-component data id page-id)) (ctf/restore-component data id page-id parent-id))
(defmethod process-change :purge-component (defmethod process-change :purge-component
[data {:keys [id]}] [data {:keys [id]}]

View file

@ -1041,12 +1041,13 @@
:page-id page-id}))) :page-id page-id})))
(defn restore-component (defn restore-component
[changes id page-id main-instance] [changes id page-id main-instance parent-id]
(assert-library! changes) (assert-library! changes)
(-> changes (-> changes
(update :redo-changes conj {:type :restore-component (update :redo-changes conj {:type :restore-component
:id id :id id
:page-id page-id}) :page-id page-id
:parent-id parent-id})
(update :undo-changes conj {:type :del-component (update :undo-changes conj {:type :del-component
:id id :id id
:main-instance main-instance}))) :main-instance main-instance})))

View file

@ -68,7 +68,8 @@
:variant-bad-name :variant-bad-name
:variant-bad-variant-name :variant-bad-variant-name
:variant-component-bad-name :variant-component-bad-name
:variant-no-properties}) :variant-no-properties
:variant-component-bad-id})
(def ^:private schema:error (def ^:private schema:error
[:map {:title "ValidationError"} [:map {:title "ValidationError"}
@ -469,6 +470,10 @@
(when-not (= (:name parent) (cfh/merge-path-item (:path component) (:name component))) (when-not (= (:name parent) (cfh/merge-path-item (:path component) (:name component)))
(report-error :variant-component-bad-name (report-error :variant-component-bad-name
(str/ffmt "Component % has an invalid name" (:id shape)) (str/ffmt "Component % has an invalid name" (:id shape))
shape file page))
(when-not (= (:variant-id component) (:variant-id shape))
(report-error :variant-component-bad-id
(str/ffmt "Variant % has adifferent variant-id than its component" (:id shape))
shape file page)))) shape file page))))
(defn- check-shape (defn- check-shape

View file

@ -103,84 +103,76 @@
(defn- duplicate-component (defn- duplicate-component
"Clone the root shape of the component and all children. Generate new "Clone the root shape of the component and all children. Generate new
ids from all of them." ids from all of them."
[component new-component-id library-data force-id] [component new-component-id library-data force-id delta variant-id]
(let [components-v2 (dm/get-in library-data [:options :components-v2])] (let [main-instance-page (ctf/get-component-page library-data component)
(if components-v2 main-instance-shape (ctf/get-component-root library-data component)
(let [main-instance-page (ctf/get-component-page library-data component) delta (or delta (gpt/point (+ (:width main-instance-shape) 50) 0))
main-instance-shape (ctf/get-component-root library-data component)
delta (gpt/point (+ (:width main-instance-shape) 50) 0)
ids-map (volatile! {}) ids-map (volatile! {})
inverted-ids-map (volatile! {}) inverted-ids-map (volatile! {})
nested-main-heads (volatile! #{}) nested-main-heads (volatile! #{})
update-original-shape update-original-shape
(fn [original-shape new-shape] (fn [original-shape new-shape]
; Save some ids for later ; Save some ids for later
(vswap! ids-map assoc (:id original-shape) (:id new-shape)) (vswap! ids-map assoc (:id original-shape) (:id new-shape))
(vswap! inverted-ids-map assoc (:id new-shape) (:id original-shape)) (vswap! inverted-ids-map assoc (:id new-shape) (:id original-shape))
(when (and (ctk/main-instance? original-shape) (when (and (ctk/main-instance? original-shape)
(not= (:component-id original-shape) (:id component))) (not= (:component-id original-shape) (:id component)))
(vswap! nested-main-heads conj (:id original-shape))) (vswap! nested-main-heads conj (:id original-shape)))
original-shape) original-shape)
update-new-shape update-new-shape
(fn [new-shape _] (fn [new-shape _]
(cond-> new-shape (cond-> new-shape
; Link the new main to the new component ; Link the new main to the new component
(= (:component-id new-shape) (:id component)) (= (:component-id new-shape) (:id component))
(assoc :component-id new-component-id) (assoc :component-id new-component-id)
:always (some? variant-id)
(gsh/move delta))) (assoc :variant-id variant-id)
[new-instance-shape new-instance-shapes _] :always
(ctst/clone-shape main-instance-shape (gsh/move delta)))
(:parent-id main-instance-shape)
(:objects main-instance-page)
:update-new-shape update-new-shape
:update-original-shape update-original-shape
:force-id force-id)
remap-frame [new-instance-shape new-instance-shapes _]
(fn [shape] (ctst/clone-shape main-instance-shape
(:parent-id main-instance-shape)
(:objects main-instance-page)
:update-new-shape update-new-shape
:update-original-shape update-original-shape
:force-id force-id)
remap-frame
(fn [shape]
; Remap all frame-ids internal to the component to the new shapes ; Remap all frame-ids internal to the component to the new shapes
(update shape :frame-id (update shape :frame-id
#(get @ids-map % (:frame-id shape)))) #(get @ids-map % (:frame-id shape))))
convert-nested-main convert-nested-main
(fn [shape] (fn [shape]
; If there is some nested main instance, convert it into a copy of ; If there is some nested main instance, convert it into a copy of
; main nested in the original component. ; main nested in the original component.
(let [origin-shape-id (get @inverted-ids-map (:id shape)) (let [origin-shape-id (get @inverted-ids-map (:id shape))
objects (:objects main-instance-page) objects (:objects main-instance-page)
parent-ids (cfh/get-parent-ids-seq-with-self objects origin-shape-id)] parent-ids (cfh/get-parent-ids-seq-with-self objects origin-shape-id)]
(cond-> shape (cond-> shape
(@nested-main-heads origin-shape-id) (@nested-main-heads origin-shape-id)
(dissoc :main-instance) (dissoc :main-instance)
(some @nested-main-heads parent-ids) (some @nested-main-heads parent-ids)
(assoc :shape-ref origin-shape-id)))) (assoc :shape-ref origin-shape-id))))
xf-shape (comp (map remap-frame) xf-shape (comp (map remap-frame)
(map convert-nested-main)) (map convert-nested-main))
new-instance-shapes (into [] xf-shape new-instance-shapes)] new-instance-shapes (into [] xf-shape new-instance-shapes)]
[nil nil new-instance-shape new-instance-shapes]) [nil nil new-instance-shape new-instance-shapes]))
(let [component-root (d/seek #(nil? (:parent-id %)) (vals (:objects component)))
[new-component-shape new-component-shapes _]
(ctst/clone-shape component-root
nil
(get component :objects))]
[new-component-shape new-component-shapes nil nil]))))
(defn generate-duplicate-component (defn generate-duplicate-component
"Create a new component copied from the one with the given id." "Create a new component copied from the one with the given id."
[changes library component-id new-component-id components-v2 & {:keys [new-shape-id apply-changes-local-library?]}] [changes library component-id new-component-id components-v2 & {:keys [new-shape-id apply-changes-local-library? delta new-variant-id]}]
(let [component (ctkl/get-component (:data library) component-id) (let [component (ctkl/get-component (:data library) component-id)
new-name (:name component) new-name (:name component)
@ -192,7 +184,7 @@
[new-component-shape new-component-shapes ; <- null in components-v2 [new-component-shape new-component-shapes ; <- null in components-v2
new-main-instance-shape new-main-instance-shapes] new-main-instance-shape new-main-instance-shapes]
(duplicate-component component new-component-id (:data library) new-shape-id)] (duplicate-component component new-component-id (:data library) new-shape-id delta new-variant-id)]
[new-main-instance-shape [new-main-instance-shape
(-> changes (-> changes
@ -209,7 +201,7 @@
(:id new-main-instance-shape) (:id new-main-instance-shape)
(:id main-instance-page) (:id main-instance-page)
(:annotation component) (:annotation component)
(:variant-id component) (or new-variant-id (:variant-id component))
(:variant-properties component) (:variant-properties component)
{:apply-changes-local-library? apply-changes-local-library?}) {:apply-changes-local-library? apply-changes-local-library?})
;; Update grid layout if the new main instance is inside ;; Update grid layout if the new main instance is inside
@ -376,6 +368,7 @@
inside-component? (some? (ctn/get-instance-root (:objects page) parent)) inside-component? (some? (ctn/get-instance-root (:objects page) parent))
shapes (cfh/get-children-with-self (:objects component) (:main-instance-id component)) shapes (cfh/get-children-with-self (:objects component) (:main-instance-id component))
shapes (map #(gsh/move % delta) shapes) shapes (map #(gsh/move % delta) shapes)
is-variant? (ctk/is-variant? component)
first-shape (cond-> (first shapes) first-shape (cond-> (first shapes)
(not (nil? parent-id)) (not (nil? parent-id))
@ -389,7 +382,9 @@
inside-component? inside-component?
(dissoc :component-root) (dissoc :component-root)
(not inside-component?) (not inside-component?)
(assoc :component-root true)) (assoc :component-root true)
(and is-variant? (some? parent-id))
(assoc :variant-id parent-id))
changes (-> changes changes (-> changes
(pcb/with-page page) (pcb/with-page page)
@ -400,7 +395,7 @@
changes (reduce #(pcb/add-object %1 %2 {:ignore-touched true}) changes (reduce #(pcb/add-object %1 %2 {:ignore-touched true})
changes changes
(rest shapes))] (rest shapes))]
{:changes (pcb/restore-component changes component-id (:id page) main-inst) {:changes (pcb/restore-component changes component-id (:id page) main-inst parent-id)
:shape (first shapes)}))) :shape (first shapes)})))
;; ---- General library synchronization functions ---- ;; ---- General library synchronization functions ----
@ -2160,52 +2155,89 @@
(pcb/with-page changes page) (pcb/with-page changes page)
frames))) frames)))
(defn generate-duplicate-component-change (defn- duplicate-variant
[changes objects page component-root parent-id frame-id delta libraries library-data] [changes library component base-pos parent-id]
(let [component-id (:component-id component-root) (let [component-page (ctpl/get-page (:data library) (:main-instance-page component))
file-id (:component-file component-root) component-shape (dm/get-in component-page [:objects (:main-instance-id component)])
main-component (ctf/get-component libraries file-id component-id) orig-pos (gpt/point (:x component-shape) (:y component-shape))
moved-component (gsh/move component-root delta) delta (gpt/subtract base-pos orig-pos)
pos (gpt/point (:x moved-component) (:y moved-component)) new-component-id (uuid/next)
origin-frame (get-in page [:objects frame-id]) [shape changes] (generate-duplicate-component changes
delta (cond-> delta library
(some? origin-frame) (:component-id component-shape)
(gpt/subtract (-> origin-frame :selrect gpt/point))) new-component-id
true
{:apply-changes-local-library? true
:delta delta
:new-variant-id parent-id})]
[shape
(-> changes
(pcb/change-parent parent-id [shape]))]))
(defn generate-duplicate-component-change
[changes objects page main parent-id frame-id delta libraries library-data ids-map]
(let [main-id (:id main)
component-id (:component-id main)
file-id (:component-file main)
component (ctf/get-component libraries file-id component-id)
pos (as-> (gsh/move main delta) $
(gpt/point (:x $) (:y $)))
;; When we duplicate a variant alone, we will instanciate it
;; When we duplicate a variant along with its variant-container, we will duplicate it
in-variant-container? (contains? ids-map (:variant-id main))
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 restore-component
#(let [restore (prepare-restore-component changes library-data (:component-id component-root) page delta (:id component-root) parent-id frame-id)] #(let [origin-frame (get-in page [:objects frame-id])
[(:shape restore) (:changes restore)]) delta (cond-> delta
(some? origin-frame)
(gpt/subtract (-> origin-frame :selrect gpt/point)))
{:keys [shape changes]} (prepare-restore-component changes
library-data
component-id
page
delta
main-id
parent-id
frame-id)]
[shape changes])
[_shape changes] [_shape changes]
(if (nil? main-component) (if (nil? component)
(restore-component) (restore-component)
(instantiate-component))] (if (and (ctk/is-variant? main) in-variant-container?)
(duplicate-variant changes
(get libraries file-id)
component
pos
parent-id)
(generate-instantiate-component changes
objects
file-id
component-id
pos
page
libraries
main-id
parent-id
frame-id
{})))]
changes)) changes))
(defn generate-duplicate-shape-change (defn generate-duplicate-shape-change
([changes objects page unames update-unames! ids-map obj delta level-delta libraries library-data file-id] ([changes objects page unames update-unames! ids 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)) (generate-duplicate-shape-change changes objects page unames update-unames! ids 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?] ([changes objects page unames update-unames! ids ids-map obj delta level-delta libraries library-data file-id frame-id parent-id duplicating-component? child? remove-swap-slot?]
(cond (cond
(nil? obj) (nil? obj)
changes changes
(ctf/is-main-of-known-component? obj libraries) (ctf/is-main-of-known-component? obj libraries)
(generate-duplicate-component-change changes objects page obj parent-id frame-id delta libraries library-data) (generate-duplicate-component-change changes objects page obj parent-id frame-id delta libraries library-data ids-map)
:else :else
(let [frame? (cfh/frame-shape? obj) (let [frame? (cfh/frame-shape? obj)
@ -2307,6 +2339,7 @@
page page
unames unames
update-unames! update-unames!
ids
ids-map ids-map
child child
delta delta
@ -2349,6 +2382,7 @@
page page
unames unames
update-unames! update-unames!
ids
ids-map ids-map
%2 %2
delta delta

View file

@ -433,14 +433,19 @@
(defn restore-component (defn restore-component
"Recover a deleted component and all its shapes and put all this again in place." "Recover a deleted component and all its shapes and put all this again in place."
[file-data component-id page-id] [file-data component-id page-id parent-id]
(let [components-v2 (dm/get-in file-data [:options :components-v2]) (let [components-v2 (dm/get-in file-data [:options :components-v2])
update-page? (and components-v2 (not (nil? page-id)))] update-page? (and components-v2 (not (nil? page-id)))
component (ctkl/get-component file-data component-id true)
update-variant? (and (some? parent-id)
(ctk/is-variant? component))]
(-> file-data (-> file-data
(ctkl/update-component component-id #(dissoc % :objects)) (ctkl/update-component component-id #(dissoc % :objects))
(ctkl/mark-component-undeleted component-id) (ctkl/mark-component-undeleted component-id)
(cond-> update-page? (cond-> update-page?
(ctkl/update-component component-id #(assoc % :main-instance-page page-id)))))) (ctkl/update-component component-id #(assoc % :main-instance-page page-id)))
(cond-> update-variant?
(ctkl/update-component component-id #(assoc % :variant-id parent-id))))))
(defn purge-component (defn purge-component
"Remove permanently a component." "Remove permanently a component."

View file

@ -7,6 +7,8 @@
(ns common-tests.logic.variants-test (ns common-tests.logic.variants-test
(:require (:require
[app.common.files.changes-builder :as pcb] [app.common.files.changes-builder :as pcb]
[app.common.geom.point :as gpt]
[app.common.logic.libraries :as cll]
[app.common.logic.variant-properties :as clvp] [app.common.logic.variant-properties :as clvp]
[app.common.test-helpers.components :as thc] [app.common.test-helpers.components :as thc]
[app.common.test-helpers.files :as thf] [app.common.test-helpers.files :as thf]
@ -192,3 +194,43 @@
;; ==== Check ;; ==== Check
(t/is (= (-> comp01' :variant-properties first :value) "NewValue1")) (t/is (= (-> comp01' :variant-properties first :value) "NewValue1"))
(t/is (= (-> comp02' :variant-properties first :value) "NewValue2")))) (t/is (= (-> comp02' :variant-properties first :value) "NewValue2"))))
(t/deftest test-duplicate-variant-container
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(thv/add-variant :v01 :c01 :m01 :c02 :m02))
data (:data file)
page (thf/current-page file)
objects (:objects page)
variant-container (ths/get-shape file :v01)
;; ==== Action
changes (-> (pcb/empty-changes nil)
(pcb/with-page-id (:id page))
(pcb/with-library-data (:data file))
(pcb/with-objects (:objects page))
(cll/generate-duplicate-changes objects ;; objects
page ;; page
#{(:id variant-container)} ;; ids
(gpt/point 0 0) ;; delta
{(:id file) file} ;; libraries
(:data file) ;; library-data
(:id file))) ;; file-id
;; ==== Get
file' (thf/apply-changes file changes)
data' (:data file')
page' (thf/current-page file')
objects' (:objects page')]
;; ==== Check
(thf/validate-file! file')
(t/is (= (count (:components data)) 2))
(t/is (= (count (:components data')) 4))
(t/is (= (count objects) 4))
(t/is (= (count objects') 7))))