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))))))