🐛 Keep grid cell overrides within newly inherited tracks

Create default cells in newly inherited grid tracks before assigning
added copy children. A cell override previously made sync append an extra
track; a later full sync shrank the tracks beneath the retained cells.
Keep existing authored cells and skip positions covered by existing spans.

Add common and real frontend row/column growth histories, including an
opposite copy direction, retained overrides, exact undo/redo and stable
full sync. The original common test failed 12 of 100 assertions and the
frontend test failed 4 of 100. Keep those assertions and add checks that
each child has exactly one cell.

Sync benchmark lower quantiles (ms), baseline -> now (ratio):
  deepest three levels:      35.215 -> 18.770 (0.53)
  chain of dependents:       68.931 -> 37.764 (0.55)
  library update:            83.083 -> 49.418 (0.59)
  main among many shapes:     4.837 ->  3.008 (0.62)
  main into 10 copies:        1.790 ->  0.968 (0.54)
  main into 100 copies:      15.415 ->  9.474 (0.61)
  main into 1000 copies:    153.086 -> 97.063 (0.63)
  variant switch:            32.052 -> 22.157 (0.69)
All worlds pass the recorded 10% regression limit. The baseline used
another machine/JVM, so ratios are rough comparisons.

Validation: common JVM 2,016 tests / 33,356 assertions; common JS
1,952 / 32,015; frontend 1,752 / 14,088; backend 1,793 / 9,007;
exporter 16 / 40. Six properties pass 40 programs at seed 712 in
both common runtimes. Lint and format checks pass for all four modules.
Golden-master data is unchanged.

AI-assisted-by: gpt-6
This commit is contained in:
alonso.torres 2026-10-29 14:43:37 +01:00
parent fcac23c52f
commit 40fc00bcf7
3 changed files with 114 additions and 8 deletions

View File

@ -490,7 +490,7 @@
`:root-main`, and `:reset?`. The main shapes of the children of a copy
shape that keeps its own geometry are placed like it first, so they get
their expected geometry."
[changes {:keys [container main-container library component reset?] :as ctx} shape-inst shape-main]
[changes {:keys [container main-container component reset?] :as ctx} shape-inst shape-main]
(shape-log :debug (:id shape-inst) container
:msg "Sync shape direct recursive"
:shape-inst (str (:name shape-inst) " " (pretty-uuid (:id shape-inst)))
@ -500,6 +500,7 @@
changes
(let [;; a copy shape that keeps its own geometry passes it on to its
;; children, which follow it as they follow their main's
shape-before shape-inst
placed (when (and (not reset?) (ctk/touched-group? shape-inst :geometry-group))
(in-main-frame shape-inst (:root-inst ctx) (:root-main ctx)))
main-container (cond-> main-container
@ -533,7 +534,7 @@
(reorder-copy-children shape-inst children-main container))
(ctsl/grid-layout? shape-inst)
(update-grid-copy-attrs (:id shape-inst) shape-main library component (not reset?))))))
(update-grid-copy-attrs shape-before shape-main (not reset?))))))
(defn generate-rename-component
"Generate the changes for rename the component with the given id, in the current file library."
@ -1542,13 +1543,30 @@
%)
{:ignore-touched true}))))
(defn- add-inherited-grid-cells
"Add default cells in newly inherited tracks without replacing authored cells."
[shape before]
(let [old-rows (count (:layout-grid-rows before))
old-cols (count (:layout-grid-columns before))
rows (count (:layout-grid-rows shape))
cols (count (:layout-grid-columns shape))
added (concat (for [row (range (inc old-rows) (inc rows))
column (range 1 (inc cols))] [row column])
(for [row (range 1 (inc (min old-rows rows)))
column (range (inc old-cols) (inc cols))] [row column]))]
(reduce (fn [shape [row column]]
(if (ctsl/cell-by-row-column shape row column)
shape
(ctsl/create-cells shape [column row 1 1])))
shape added)))
(defn- update-grid-copy-attrs
"Synchronizes the `layout-grid-cells` property from the main shape to the copies"
[changes shape-copy-id shape-main _library _component omit-touched?]
[changes shape-before shape-main omit-touched?]
(-> changes
(pcb/apply-changes-local)
(pcb/update-shapes
[shape-copy-id]
[(:id shape-before)]
(fn [shape-copy objects]
(let [ids-map
(into {}
@ -1570,14 +1588,15 @@
(fn [cell]
(update cell :shapes #(filterv child? %)))))))
;; Take cells from main and remap the shapes to assign it to the copy
copy-cells (-> shape-copy :layout-grid-cells (remove-orphan-cells shape-copy))
main-cells (-> shape-main (ctsl/remap-grid-cells ids-map) :layout-grid-cells)]
copy-cells (-> shape-copy :layout-grid-cells (remove-orphan-cells shape-copy))
main-cells (-> shape-main (ctsl/remap-grid-cells ids-map) :layout-grid-cells)
keep-cells? (and omit-touched? (contains? (:touched shape-copy) :layout-grid-cells))]
(-> shape-copy
(assoc :layout-grid-cells
(if (and omit-touched?
(contains? (:touched shape-copy) :layout-grid-cells))
(if keep-cells?
copy-cells
main-cells))
(cond-> keep-cells? (add-inherited-grid-cells shape-before))
(ctsl/assign-cells objects))))
{:ignore-touched true :with-objects? true})))

View File

@ -18,7 +18,9 @@
[app.common.test-helpers.composable.comp.nodes :as n]
[app.common.test-helpers.composable.comp.runner :as r]
[app.common.test-helpers.composable.comp.setups :as setup]
[app.common.test-helpers.composable.comp.undo-check :as uc]
[app.common.test-helpers.composable.core :as tm]
[app.common.test-helpers.composable.gen :as cg]
[app.common.test-helpers.compositions :as tho]
[app.common.test-helpers.files :as thf]
[app.common.test-helpers.ids-map :as thi]
@ -33,6 +35,50 @@
(t/use-fixtures :each thi/test-fixture)
(t/deftest grid-growth-fills-inherited-tracks-before-assigning-copy-children
(doseq [direction [:row :column]
turn-copy? [false true]]
(let [initial (cg/world [1 0 {:layout :grid :layout-children true :grid-square true}])
main (n/main-head-of "random-0")
copy (n/nth-copy-head-of "random-0" 0)
other (n/nth-copy-head-of "random-0" 1)
snapshot #(uc/snapshot (tm/file %))
ops (concat (when (= direction :column) [(n/edit-grid main :direction :column)])
[(n/edit-grid copy :cell {:row 1 :column 1 :props {:align-self :end}})]
(when turn-copy? [(n/edit-grid copy :direction (if (= direction :row) :column :row))])
[(n/add-child main :extra-a :width 20 :height 20)
(n/add-child main :extra-b :width 20 :height 20)])
grown (reduce (fn [before op]
(let [after (-> before (r/step op) r/settle)
undone (r/step after (n/undo))
redone (r/step undone (n/redo))]
(t/is (< (r/undo-depth before) (r/undo-depth after)))
(t/is (uc/same-state? (snapshot before) (snapshot undone)))
(t/is (uc/same-state? (snapshot after) (snapshot redone)))
after)) initial ops)
shapes (mapv #(tm/shape-by-id grown (% grown)) [main copy other])
dims (if (= direction :row) [2 3] [3 2])
coords (set (for [row (range 1 (inc (first dims)))
column (range 1 (inc (second dims)))] [row column]))
objects (:objects (thf/current-page (tm/file grown)))
main-ids (set (:shapes (first shapes)))
component (:component-id (first shapes))
synced (r/settle grown #{component})]
(doseq [shape shapes]
(t/is (= dims (mapv #(count (get shape %)) [:layout-grid-rows :layout-grid-columns])))
(t/is (= coords (set (map (juxt :row :column) (vals (:layout-grid-cells shape))))))
(t/is (= main-ids (set (map #(or (:shape-ref (get objects %)) %)
(mapcat :shapes (vals (:layout-grid-cells shape)))))))
(t/is (= (frequencies main-ids)
(frequencies (map #(or (:shape-ref (get objects %)) %)
(mapcat :shapes (vals (:layout-grid-cells shape))))))))
(t/is (= :end (:align-self (first (filter #(= [1 1] [(:row %) (:column %)])
(vals (:layout-grid-cells (second shapes))))))))
(t/is (= (cond-> #{:layout-grid-cells} turn-copy? (conj :layout-grid-dir))
(:touched (second shapes))))
(t/is (empty? (:touched (nth shapes 2))))
(t/is (uc/same-state? (snapshot grown) (snapshot synced))))))
(t/deftest sync-keeps-a-real-subpixel-position-change
(let [initial (setup/simple-component-with-labeled-copy)
original (tm/resolve-shape initial :copy-child)

View File

@ -8,6 +8,7 @@
(:require
[app.common.logic.shapes :as cls]
[app.common.test-helpers.components :as thc]
[app.common.test-helpers.composable.comp.nodes :as n]
[app.common.test-helpers.composable.comp.undo-check :as uc]
[app.common.test-helpers.composable.core :as tm]
[app.common.test-helpers.compositions :as tho]
@ -95,6 +96,46 @@
[(ptk/data-event :layout/update {:ids [(thi/id :main) (thi/id :copy) (thi/id :other)]})]))))
(t/is (= (get-cells :main) (get-cells :copy) (get-cells :other))))
(t/deftest ^:async grid-growth-keeps-overrides-within-inherited-tracks
(await
(mock/with-mocks* {rp/cmd! mock/rpc-cmd-mock rx/timer mock/timer-mock}
(doseq [direction [:row :column]
turn-copy? [false true]]
(await (install-grid))
(let [initial (tm/make-situation (ftm/current-file))
_ (ftm/uninstall!)
ops (concat (when (= direction :column) [(n/edit-grid :main :direction :column)])
[(n/edit-grid :copy :cell {:row 1 :column 1 :props {:align-self :end}})]
(when turn-copy? [(n/edit-grid :copy :direction (if (= direction :row) :column :row))])
[(n/add-child :main :extra-a :width 20 :height 20)
(n/add-child :main :extra-b :width 20 :height 20)])
grown (loop [live (ftm/start-generated initial) ops ops]
(if-let [op (first ops)]
(let [next (await (ftm/run-generated-step live op))]
(t/is (some? next))
(t/is (empty? (uc/failures next)))
(recur next (rest ops)))
live))
dims (if (= direction :row) [2 3] [3 2])
coords (set (for [row (range 1 (inc (first dims)))
column (range 1 (inc (second dims)))] [row column]))
members (set (:shapes (get-shape :main)))]
(doseq [label [:main :copy :other]]
(t/is (= dims (mapv #(count (get (get-shape label) %)) [:layout-grid-rows :layout-grid-columns])))
(t/is (= coords (set (map (juxt :row :column) (get-cells label)))))
(t/is (= members (set (mapcat :shapes (get-cells label)))))
(t/is (= (frequencies members) (frequencies (mapcat :shapes (get-cells label))))))
(t/is (= :end (:align-self (first (get-cells :copy)))))
(t/is (= (cond-> #{:layout-grid-cells} turn-copy? (conj :layout-grid-dir))
(:touched (get-shape :copy))))
(t/is (empty? (:touched (get-shape :other))))
(let [before (ftm/snapshot)
file-id (:id (ftm/current-file))]
(t/is (= :settled (await (ftm/await-step [(dwl/sync-file file-id file-id :components (thi/id :component))]))))
(t/is (uc/same-state? before (ftm/snapshot))))
(t/is (empty? (uc/failures (await (ftm/finish-generated (ftm/refresh-generated grown))))))
(ftm/uninstall!))))))
(t/deftest ^:async grid-direction-overrides-survive-main-edits-update-main-and-reset
(await
(mock/with-mocks* {rp/cmd! mock/rpc-cmd-mock rx/timer mock/timer-mock}