diff --git a/.serena/memories/common/component-sync-contract.md b/.serena/memories/common/component-sync-contract.md index 0b805c2232..10dafa2b85 100644 --- a/.serena/memories/common/component-sync-contract.md +++ b/.serena/memories/common/component-sync-contract.md @@ -66,3 +66,9 @@ Intended semantics of copies, `:touched` and component sync. This is the spec: w - `{:type :set-touched}` replaces `:touched` wholesale and only on copies (an empty set is dropped). The builders write only it (`pcb/update-shapes` turns a `:touched` change into it); `{:type :set :attr :touched}`, a raw assoc with no copy guard, is still applied for stored changes. - Builders that write to copies must emit the inverse `:set-touched` in undo (as `pcb/update-shapes` does). + +A parent turn settles swapped descendant placement in the transform's +change builder (F182). Its content and position reference can have different +sizes, so rotating the content rigidly can leave its untransformed anchor +different from full sync. Keep the swapped head/children untouched and put +the derived correction in the same undo group. Authored geometry stays. diff --git a/.serena/memories/frontend/workspace-transform-subtleties.md b/.serena/memories/frontend/workspace-transform-subtleties.md index c7a7aa81dc..9b82bd8b8b 100644 --- a/.serena/memories/frontend/workspace-transform-subtleties.md +++ b/.serena/memories/frontend/workspace-transform-subtleties.md @@ -17,4 +17,14 @@ - WASM modifier updates set plugin/local props with parsed geometry/structure modifiers rather than directly mutating file data. - The position-data recomputation watcher ignores commits tagged `:position-data`; keep that tag when adding derived position-data commits. -- Rotation has separate WASM and non-WASM event paths. Check both when changing rotation modifier semantics. \ No newline at end of file +- Rotation has separate WASM and non-WASM event paths. Check both when changing rotation modifier semantics. + +## Settling swapped placement after a parent turn + +Both modifier commit paths set settle-component-placement? (F182). The +update-shapes builder settles untouched swapped heads after parent bounds +update, before the single commit. A swapped head can keep its content size +while its position reference has another size; a rigid turn alone then +leaves placement different from full sync. Preserve authored geometry and +join derived placement to the transform's undo group. The common helper +compares matrix axes as well as rotation/flip metadata for the WASM path. diff --git a/common/src/app/common/logic/libraries.cljc b/common/src/app/common/logic/libraries.cljc index 0c8220d72c..264d7644cd 100644 --- a/common/src/app/common/logic/libraries.cljc +++ b/common/src/app/common/logic/libraries.cljc @@ -332,6 +332,34 @@ changes) changes))) +(defn generate-sync-transformed-placement + "Settle untouched swapped heads carried through a parent turn in the + same change group. Their position frame can have a different size from + their content, so a rigid turn alone does not settle their placement." + [changes file libraries page before ids] + (let [objects (pcb/get-objects changes) + orientation (fn [shape] + [(select-keys (or (:transform shape) (gmt/matrix)) [:a :b :c :d]) + (:rotation shape) (boolean (:flip-x shape)) (boolean (:flip-y shape))]) + roots (into (d/ordered-set) + (keep (fn [id] + (let [shape (get objects id)] + (when (and (ctk/swapped? shape) + (not (ctk/touched-group? shape :geometry-group)) + (not= (orientation (get before id)) (orientation shape))) + (:id (ctn/get-component-shape objects shape)))))) + ids)] + (reduce (fn [changes id] + (let [objects (pcb/get-objects changes) + page (assoc page :objects objects) + file (assoc-in file [:data :pages-index (:id page) :objects] objects) + container (ctn/make-container page :page)] + (-> changes + (pcb/with-container container) + (generate-sync-shape-direct file (assoc libraries (:id file) file) + container id false)))) + changes roots))) + (defn- sync-copy-shape "Sync the copy shape `shape-inst` from `shape-main`, the main shape it pairs with. A reset also clears its overrides and its `:remote-synced`, diff --git a/common/src/app/common/test_helpers/composable/comp/nodes.cljc b/common/src/app/common/test_helpers/composable/comp/nodes.cljc index b9e04c08ac..2f6aa2a5eb 100644 --- a/common/src/app/common/test_helpers/composable/comp/nodes.cljc +++ b/common/src/app/common/test_helpers/composable/comp/nodes.cljc @@ -286,14 +286,18 @@ (defn transform-changes "Apply a transform through constraints and the shared placement step." - [page shape modifiers] - (let [objects (:objects page) - tree (gm/set-objects-modifiers {(:id shape) {:modifiers modifiers}} objects) - transform (clp/modifiers-transform tree)] - (cls/generate-update-shapes - (pcb/empty-changes nil (:id page)) (keys tree) transform objects - {:ignore-tree (clp/calculate-ignore-tree objects (keys tree) transform #{(:id shape)}) - :attrs clp/transform-attrs}))) + ([page shape modifiers] + (transform-changes page shape modifiers nil)) + ([page shape modifiers {:keys [file libraries]}] + (let [objects (:objects page) + tree (gm/set-objects-modifiers {(:id shape) {:modifiers modifiers}} objects) + transform (clp/modifiers-transform tree) + changes (cls/generate-update-shapes + (pcb/empty-changes nil (:id page)) (keys tree) transform objects + {:ignore-tree (clp/calculate-ignore-tree objects (keys tree) transform #{(:id shape)}) + :attrs clp/transform-attrs})] + (cond-> changes + file (cll/generate-sync-transformed-placement file libraries page objects (keys tree)))))) (defrecord Rotate [target angle] IComponentOperation @@ -307,7 +311,8 @@ objects (:objects page) shape (get objects shape-id) center (gsh/shape->center shape) - changes (transform-changes page shape (ctm/rotation-modifiers shape center angle)) + changes (transform-changes page shape (ctm/rotation-modifiers shape center angle) + {:file the-file :libraries (cf/all-files situation)}) file' (thf/apply-changes the-file changes)] (-> situation (tm/with-file file') @@ -332,7 +337,8 @@ page (thf/current-page the-file) objects (:objects page) shape (get objects shape-id) - changes (transform-changes page shape (ctm/change-size shape nil value)) + changes (transform-changes page shape (ctm/change-size shape nil value) + {:file the-file :libraries (cf/all-files situation)}) file' (thf/apply-changes the-file changes)] (-> situation (tm/with-file file') diff --git a/common/test/common_tests/logic/comp_expected_geometry_test.cljc b/common/test/common_tests/logic/comp_expected_geometry_test.cljc index 1a5569d08f..2b5dddc511 100644 --- a/common/test/common_tests/logic/comp_expected_geometry_test.cljc +++ b/common/test/common_tests/logic/comp_expected_geometry_test.cljc @@ -36,6 +36,38 @@ (t/use-fixtures :each thi/test-fixture) +(t/deftest turning-a-parent-settles-a-swapped-head-with-a-different-position-size + (doseq [angle [45 90]] + (let [file (-> (thf/sample-file :file) + (tho/add-simple-component :a :main-a :rect-a + :root-params {:width 100 :height 100}) + (tho/add-simple-component :other :main-other :rect-other + :root-params {:width 100 :height 100}) + (tho/add-frame :main-b {:width 200 :height 200}) + (thc/instantiate-component :a :b-a :parent-label :main-b) + (thc/make-component :b :main-b) + (tho/add-frame :main-c {:width 300 :height 300}) + (thc/instantiate-component :b :c-b :parent-label :main-c) + (thc/make-component :c :main-c) + (thc/instantiate-component :c :copy-c :children-labels [:copy-b :copy-a])) + before (-> (tm/make-situation file) + (r/step (n/swap-shape :copy-a :other)) r/settle + (r/step (n/change-height :b-a 80)) r/settle) + turned (-> before (r/step (n/rotate :copy-c angle)) r/settle) + head (tm/resolve-shape turned :copy-a) + child (tm/shape-by-id turned (first (:shapes head))) + file (tm/file turned) + full (:file (r/sync-cascade file {} (keys (get-in file [:data :components]))))] + (t/is (mth/close? 100 (:height head) 0.001)) + (t/is (= #{:swap-group} (:touched head))) + (t/is (empty? (:touched child))) + (t/is (gsh/close-attrs? :points (:points head) + (:points (ths/get-shape-by-id full (:id head))) 0.001)) + (t/is (gsh/close-attrs? :points (:points child) + (:points (ths/get-shape-by-id full (:id child))) 0.001)) + (t/is (= (thn/normalize-file (tm/file before)) + (thn/normalize-file (tm/file (r/step turned (n/undo))))))))) + (t/deftest authored-child-geometry-follows-a-parent-rotation-received-through-sync (doseq [swapped? [false true] angle [45 135] diff --git a/frontend/src/app/main/data/workspace/modifiers.cljs b/frontend/src/app/main/data/workspace/modifiers.cljs index 3ddd323e7c..373392926e 100644 --- a/frontend/src/app/main/data/workspace/modifiers.cljs +++ b/frontend/src/app/main/data/workspace/modifiers.cljs @@ -701,6 +701,7 @@ (assoc :undo-group undo-group) (assoc :reg-objects? true) (assoc :ignore-tree ignore-tree) + (assoc :settle-component-placement? true) (assoc :translation? translation?) ;; Attributes that can change in the transform. This ;; way we don't have to check all the attributes @@ -864,6 +865,7 @@ (assoc :undo-group undo-group) (assoc :reg-objects? true) (assoc :ignore-tree ignore-tree) + (assoc :settle-component-placement? true) ;; Attributes that can change in the transform. This ;; way we don't have to check all the attributes (assoc :attrs clp/transform-attrs)) diff --git a/frontend/src/app/main/data/workspace/shapes.cljs b/frontend/src/app/main/data/workspace/shapes.cljs index 18702666df..b5e738d33c 100644 --- a/frontend/src/app/main/data/workspace/shapes.cljs +++ b/frontend/src/app/main/data/workspace/shapes.cljs @@ -11,6 +11,7 @@ [app.common.files.changes-builder :as pcb] [app.common.files.helpers :as cfh] [app.common.files.shapes-helpers :as cfsh] + [app.common.logic.libraries :as cll] [app.common.logic.shapes :as cls] [app.common.schema :as sm] [app.common.types.component :as ctc] @@ -212,7 +213,7 @@ {:as props :keys [reg-objects? save-undo? stack-undo? attrs ignore-tree page-id ignore-touched undo-group with-objects? changed-sub-attr changed-item-index translation? - skip-grid-reassignment? skip-component-sync? resize-ids] + skip-grid-reassignment? skip-component-sync? resize-ids settle-component-placement?] :or {reg-objects? false save-undo? true stack-undo? false @@ -280,7 +281,13 @@ (rx/empty)) (if (seq (:redo-changes changes)) - (let [changes (cond-> changes reg-objects? (pcb/resize-parents (or resize-ids ids)))] + (let [changes (cond-> changes + reg-objects? (pcb/resize-parents (or resize-ids ids)) + (and settle-component-placement? (not skip-component-sync?)) + (cll/generate-sync-transformed-placement + (dsh/lookup-file state (:current-file-id state)) + (dsh/lookup-libraries state) + (dsh/lookup-page state page-id) objects ids))] (rx/of (dch/commit-changes changes))) (rx/empty))