From 1e9d19b21443bf1fa72435d76e6edf94c1af1a50 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Sat, 24 Oct 2026 09:53:30 +0200 Subject: [PATCH] :bug: Compose child flip flags through parent placement Keep the source and parent-frame flip composition when placing component content through a rotated parent. A fresh large matrix turn can otherwise set both child flip flags even though the user only rotated (F170). Preserve nil/false representation when the parent does not flip an axis. The portable regression fails two flag assertions before the fix. Eight geometry tests pass 61 assertions, the original library shrink passes, and the pure golden master matches all 1149 assertions without recording. Common and frontend lint and format pass. Benchmark ratios against Phase 1e: main 10/100/1000 copies 0.58/0.74/0.57, three levels 0.49, chain 0.49, library 0.52, many shapes 0.47, variant switch 0.61. No regression flags. AI-assisted-by: GPT-6 --- .serena/memories/common/geometry-invariants.md | 10 +++++++++- common/src/app/common/logic/libraries.cljc | 14 ++++++++++---- .../logic/comp_expected_geometry_test.cljc | 15 +++++++++++++++ 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/.serena/memories/common/geometry-invariants.md b/.serena/memories/common/geometry-invariants.md index f90475a285..b78034c242 100644 --- a/.serena/memories/common/geometry-invariants.md +++ b/.serena/memories/common/geometry-invariants.md @@ -20,6 +20,14 @@ After a geometric mutation, equivalent fields such as `:y`, `(:y :selrect)`, and - `gsh/transform-shape`: applies a full transform. - `cts/setup-shape`: initializes geometry for new shapes; variant test helpers such as `thv/add-variant-with-child` use it. +## Flip flags through parent placement + +When composing a parent's orientation onto content, compose flip flags from +the source shape and the origin/target parent frames. Matrix decomposition +can set both flags during a large rotation, even when several smaller +rotations authored neither flip (F170). Keep the original nil/false value +when the parent frames do not change that axis's flip. + ## Edits that break the invariant - `(assoc shape :x ...)` or `(assoc shape :y ...)`: updates only one field and leaves `:selrect` / `:points` stale. @@ -45,4 +53,4 @@ Using `(ths/update-shape file label :y 101)` leaves `:selrect.y` stale. Downstre When a copy shape changes geometry through the proper pipeline (`set-shape-attr` via `process-operation :set`), `:touched` gains `:geometry-group` unless ignored. Tests can either drive the production update with `cls/generate-update-shapes`, or inject `(assoc shape :touched #{:geometry-group})` when only touched state matters. -If a test needs both a new position and touched state, move the shape first with geometry-preserving helpers, then inject or assert touched state. \ No newline at end of file +If a test needs both a new position and touched state, move the shape first with geometry-preserving helpers, then inject or assert touched state. diff --git a/common/src/app/common/logic/libraries.cljc b/common/src/app/common/logic/libraries.cljc index 870d71730b..4816bf7b0d 100644 --- a/common/src/app/common/logic/libraries.cljc +++ b/common/src/app/common/logic/libraries.cljc @@ -276,10 +276,16 @@ main-container (let [transform (gmt/transform-in (gsh/shape->center shape-main) relative) angle (- (or (:rotation target) 0) (or (:rotation origin) 0)) - turn #(-> % - (assoc-in [:modifiers :rotation] angle) - (gsh/apply-transform transform) - (dissoc :modifiers))] + flip (fn [shape key] + (cond-> (get shape key) + (not= (boolean (get origin key)) (boolean (get target key))) not)) + turn (fn [shape] + (-> shape + (assoc-in [:modifiers :rotation] angle) + (gsh/apply-transform transform) + (dissoc :modifiers) + (assoc :flip-x (flip shape :flip-x) + :flip-y (flip shape :flip-y))))] (update main-container :objects (fn [objects] (reduce #(update %1 %2 turn) objects 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 559a35b550..60fc908de1 100644 --- a/common/test/common_tests/logic/comp_expected_geometry_test.cljc +++ b/common/test/common_tests/logic/comp_expected_geometry_test.cljc @@ -213,3 +213,18 @@ after (get-in (thf/current-page synced) [:objects (:id child)])] (t/is (= (select-keys child [:flip-x :flip-y]) (select-keys after [:flip-x :flip-y]))) (t/is (gsh/close-attrs? :points (:points child) (:points after) 0.001)))) + +(t/deftest update-main-keeps-child-flips-when-syncing-through-a-rotated-parent + (let [updated (-> (setup/nested-component-with-copy) + (r/step (n/rotate :copy-root 45)) r/settle + (r/step (n/rotate :copy-root 90)) r/settle + (r/step (n/update-main :copy-root)) r/settle) + file (tm/file updated) + main (tm/resolve-shape updated :main2-nested-head) + child (tm/shape-by-id updated (first (:shapes main))) + synced (:file (r/sync-cascade file {} (keys (get-in file [:data :components]))))] + (doseq [shape [main child]] + (let [after (ths/get-shape-by-id synced (:id shape)) + flips #(mapv (comp boolean %) [:flip-x :flip-y])] + (t/is (= (flips shape) (flips after))) + (t/is (gsh/close-attrs? :points (:points shape) (:points after) 0.001))))))