🎉 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,12 +103,10 @@
(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])]
(if components-v2
(let [main-instance-page (ctf/get-component-page library-data component) (let [main-instance-page (ctf/get-component-page library-data component)
main-instance-shape (ctf/get-component-root library-data component) main-instance-shape (ctf/get-component-root library-data component)
delta (gpt/point (+ (:width main-instance-shape) 50) 0) delta (or delta (gpt/point (+ (:width main-instance-shape) 50) 0))
ids-map (volatile! {}) ids-map (volatile! {})
inverted-ids-map (volatile! {}) inverted-ids-map (volatile! {})
@ -131,6 +129,9 @@
(= (:component-id new-shape) (:id component)) (= (:component-id new-shape) (:id component))
(assoc :component-id new-component-id) (assoc :component-id new-component-id)
(some? variant-id)
(assoc :variant-id variant-id)
:always :always
(gsh/move delta))) (gsh/move delta)))
@ -167,20 +168,11 @@
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- duplicate-variant
[changes library component base-pos parent-id]
(let [component-page (ctpl/get-page (:data library) (:main-instance-page component))
component-shape (dm/get-in component-page [:objects (:main-instance-id component)])
orig-pos (gpt/point (:x component-shape) (:y component-shape))
delta (gpt/subtract base-pos orig-pos)
new-component-id (uuid/next)
[shape changes] (generate-duplicate-component changes
library
(:component-id component-shape)
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 (defn generate-duplicate-component-change
[changes objects page component-root parent-id frame-id delta libraries library-data] [changes objects page main parent-id frame-id delta libraries library-data ids-map]
(let [component-id (:component-id component-root) (let [main-id (:id main)
file-id (:component-file component-root) component-id (:component-id main)
main-component (ctf/get-component libraries file-id component-id) file-id (:component-file main)
moved-component (gsh/move component-root delta) component (ctf/get-component libraries file-id component-id)
pos (gpt/point (:x moved-component) (:y moved-component)) pos (as-> (gsh/move main delta) $
origin-frame (get-in page [:objects frame-id]) (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))
restore-component
#(let [origin-frame (get-in page [:objects frame-id])
delta (cond-> delta delta (cond-> delta
(some? origin-frame) (some? origin-frame)
(gpt/subtract (-> origin-frame :selrect gpt/point))) (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])
instantiate-component [_shape changes]
#(generate-instantiate-component changes (if (nil? component)
(restore-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 objects
file-id file-id
(:component-id component-root) component-id
pos pos
page page
libraries libraries
(:id component-root) main-id
parent-id parent-id
frame-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)) 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))))