🐛 Keep library color and typography syncs from touching copies

A library color sync wrote a copy's fills through update-shapes, and a
library typography sync wrote a copy text's content with a plain :set,
so both marked the copy as overridden (:fill-group, or :content-group
with :text-content-attribute). A later edit of that fill or text style
in the main then never reached the copy. Both syncs are derived writes:
they now write with ignore-touched.

The typography sync also merged the whole typography into each text node
that uses it, so its :modified-at (and :path) ended in the content of
the main and of every copy. It now writes only the text styles of the
typography.

Benchmark (lower quantile, ms; baseline / now / ratio):
  deepest-edit-through-three-levels    35.2 /  34.7 / 0.98
  edit-through-a-chain-of-dependents   68.9 /  67.6 / 0.98
  library-update-into-copies           83.1 /  76.0 / 0.91
  main-edit-into-10-copies              1.8 /   1.5 / 0.86
  main-edit-into-100-copies            15.4 /  15.2 / 0.99
  main-edit-into-1000-copies          153.1 / 157.1 / 1.03
  variant-switch-into-copies           32.1 /  27.0 / 0.84

AI-assisted-by: claude-opus-5-5

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
alonso.torres 2026-10-13 22:19:10 +02:00
parent 38a58d2c11
commit 8778c876ec
3 changed files with 21 additions and 23 deletions

View File

@ -641,22 +641,25 @@
(shape-log :debug (:id shape) nil :msg "Sync colors of shape" :shape (:name shape))
;; Synchronize a shape that uses some colors of the library. The value of the
;; color in the library is copied to the shape.
;; color in the library is copied to the shape. It is a derived write, so it
;; touches no override of a copy.
(let [library-colors (get-in libraries [library-id :data :colors])]
(pcb/update-shapes changes
[(:id shape)]
#(ctl/sync-colors % library-id library-colors))))
#(ctl/sync-colors % library-id library-colors)
{:ignore-touched true})))
(defmethod generate-sync-shape :typographies
[_ changes library-id container shape libraries _]
(shape-log :debug (:id shape) nil :msg "Sync typographies of shape" :shape (:name shape))
;; Synchronize a shape that uses some typographies of the library. The attributes
;; of the typography are copied to the shape."
;; Synchronize a shape that uses some typographies of the library. The text
;; styles of the typography are copied to the shape. It is a derived write,
;; so it touches no override of a copy.
(let [typographies (get-in libraries [library-id :data :typographies])
update-node (fn [node]
(if-let [typography (get typographies (:typography-ref-id node))]
(merge node (dissoc typography :name :id))
(merge node (select-keys typography txt/typography-fields))
(dissoc node :typography-ref-id
:typography-ref-file)))]
(generate-sync-text-shape changes shape container update-node)))
@ -673,7 +676,8 @@
:id (:id shape)
:operations [{:type :set
:attr :content
:val new-content}
:val new-content
:ignore-touched true}
{:type :set
:attr :position-data
:val nil}]})
@ -685,7 +689,8 @@
:id (:id shape)
:operations [{:type :set
:attr :content
:val old-content}
:val old-content
:ignore-touched true}
{:type :set
:attr :position-data
:val nil}]})

View File

@ -15,7 +15,6 @@
[app.common.logic.libraries :as cll]
[app.common.logic.shapes :as cls]
[app.common.test-helpers.components :as thc]
[app.common.test-helpers.composable.core :as tm]
[app.common.test-helpers.compositions :as tho]
[app.common.test-helpers.files :as thf]
[app.common.test-helpers.ids-map :as thi]
@ -80,8 +79,8 @@
(let [file' (change-library-color (component-using-color) "#222222")
copy (ths/get-shape file' :copy-child)]
(t/is (= "#222222" (-> copy :fills first :fill-color)))
;; the sync writes the fill without ignore-touched
(tm/known-failure "F3" nil #{:fill-group} (:touched copy))))
;; a library sync is a derived write
(t/is (nil? (:touched copy)))))
(t/deftest main-fill-edit-reaches-the-copy-after-a-library-color-sync
(let [file' (-> (component-using-color)
@ -89,8 +88,7 @@
(sync-file :components (thi/id :component1))
(update-shape :main-child #(assoc % :fills (ths/sample-fills-color :fill-color "#333333")))
(sync-file :components (thi/id :component1)))]
;; the touched fill blocks it
(tm/known-failure "F3" "#333333" "#222222" (-> (ths/get-shape file' :copy-child) :fills first :fill-color))))
(t/is (= "#333333" (-> (ths/get-shape file' :copy-child) :fills first :fill-color)))))
;; ---- Typographies
@ -131,8 +129,8 @@
(let [file' (change-library-typography (component-using-typography) "24")
copy (ths/get-shape file' :copy-child)]
(t/is (= "24" (font-size-of copy)))
;; the sync writes the content with a raw :set, which touches it
(tm/known-failure "F3" nil #{:content-group :text-content-attribute} (:touched copy))))
;; a library sync is a derived write
(t/is (nil? (:touched copy)))))
(t/deftest main-font-edit-reaches-the-copy-after-a-library-typography-sync
(let [file' (-> (component-using-typography)
@ -143,12 +141,9 @@
(update shape :content
#(txt/transform-nodes txt/is-content-node? (fn [node] (assoc node :font-size "40")) %))))
(sync-file :components (thi/id :component1)))]
;; the touched text attributes block it
(tm/known-failure "F3" "40" "24" (font-size-of (ths/get-shape file' :copy-child)))))
(t/is (= "40" (font-size-of (ths/get-shape file' :copy-child))))))
(t/deftest library-typography-sync-writes-only-text-styles
;; the sync merges the whole typography into each node, its own
;; :modified-at included
(let [file' (change-library-typography (component-using-typography) "24")
node (get-in (ths/get-shape file' :main-child) [:content :children 0 :children 0 :children 0])]
(tm/known-failure "F80" false true (contains? node :modified-at))))
(t/is (not (contains? node :modified-at)))))

View File

@ -403,10 +403,8 @@
(sync :colors (thi/id :brand))
(thtx/set-text :main-child [[["New arrivals" {:weight 600 :size 18 :fills (brand "#7B3FD1")}]]])
(sync :components (thi/id :component1)))]
(tm/known-failure "F3"
[[["New arrivals" {:weight 600 :size 18 :fills (brand "#7B3FD1")}]]]
[[["New arrivals" {:weight 600 :fills (brand "#7B3FD1")}]]]
(thtx/text-of file' :copy-child))))
(t/is (= [[["New arrivals" {:weight 600 :size 18 :fills (brand "#7B3FD1")}]]]
(thtx/text-of file' :copy-child)))))
;; ---- The worked example: a copy that bolds "Hello" and adds "!!!"