diff --git a/common/src/app/common/logic/libraries.cljc b/common/src/app/common/logic/libraries.cljc index b8fca8ae69..0c8220d72c 100644 --- a/common/src/app/common/logic/libraries.cljc +++ b/common/src/app/common/logic/libraries.cljc @@ -206,7 +206,7 @@ :components (generate-sync-shape-direct changes (get libraries current-file-id) libraries - container + (assoc container :objects objects') (:id shape') false) :colors (clla/generate-sync-colors changes library-id shape' libraries) @@ -297,7 +297,12 @@ [changes file libraries container shape-id reset?] (shape-log :debug shape-id container :msg "Sync shape direct" :shape-inst (str shape-id) :reset? reset?) - (let [shape-inst (ctn/get-shape container shape-id) + (let [changes (if (pcb/get-objects changes) + changes + (-> changes + (pcb/with-container container) + (pcb/with-objects (:objects container)))) + shape-inst (ctn/get-shape container shape-id) library (dm/get-in libraries [(:component-file shape-inst) :data]) component (ctkl/get-component library (:component-id shape-inst) true)] (if (and (ctk/in-component-copy? shape-inst) @@ -399,6 +404,31 @@ (grc/rect->center (:selrect (get objects (:id shape-main)))))] (reduce #(update %1 %2 gsh/move delta) objects ids))) +(defn- sync-descendant-orientation + "A parent turned by sync carries its whole subtree, including geometry + overrides. Resize remains the job of constraints, not matrix scale." + [changes container before] + (let [after (get (pcb/get-objects changes) (:id before)) + changed? (and after + (or (not (mth/close? (or (:rotation before) 0) (or (:rotation after) 0) 1e-10)) + (not= (boolean (:flip-x before)) (boolean (:flip-x after))) + (not= (boolean (:flip-y before)) (boolean (:flip-y after))))) + ids (when changed? (cfh/get-children-ids (:objects container) (:id before)))] + (if (seq ids) + (let [orientation (fn [shape] + (let [{:keys [a b c d]} (or (:transform shape) (gmt/matrix)) + horizontal (gpt/unit (gpt/point a b)) + vertical (gpt/unit (gpt/point c d)) + matrix (gmt/matrix (:x horizontal) (:y horizontal) + (:x vertical) (:y vertical) 0 0)] + (assoc shape :transform matrix :transform-inverse (gmt/inverse matrix)))) + turn (orient-like (orientation before) (orientation after) (gsh/shape->center before)) + delta (gpt/subtract (gsh/shape->center after) (gsh/shape->center before))] + (pcb/update-shapes changes ids + #(cond-> % (some? turn) turn :always (gsh/move delta)) + {:ignore-touched true})) + changes))) + (defn- sync-copy-recursive "Sync the copy shape `shape-inst` from `shape-main`, the main shape it pairs with, then pair their children (`compare-children`) and walk each @@ -427,10 +457,15 @@ (update :objects placed-like shape-main placed)) ctx (assoc ctx :main-container main-container) shape-main (get-in main-container [:objects (:id shape-main)]) + changes (-> changes + (sync-copy-shape ctx shape-inst shape-main) + (sync-descendant-orientation container shape-inst)) + container (assoc container :objects (pcb/get-objects changes)) + ctx (assoc ctx :container container) + shape-inst (ctn/get-shape container (:id shape-inst)) children-inst (vec (ctn/get-direct-children container shape-inst)) children-main (vec (ctn/get-direct-children main-container shape-main))] (cond-> (-> changes - (sync-copy-shape ctx shape-inst shape-main) (compare-children children-inst children-main 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 60fc908de1..1a5569d08f 100644 --- a/common/test/common_tests/logic/comp_expected_geometry_test.cljc +++ b/common/test/common_tests/logic/comp_expected_geometry_test.cljc @@ -13,6 +13,7 @@ [app.common.files.changes-builder :as pcb] [app.common.geom.matrix :as gmt] [app.common.geom.modifiers :as gm] + [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.common.logic.libraries :as cll] [app.common.logic.placement :as clp] @@ -35,6 +36,58 @@ (t/use-fixtures :each thi/test-fixture) +(t/deftest authored-child-geometry-follows-a-parent-rotation-received-through-sync + (doseq [swapped? [false true] + angle [45 135] + own-angle [0 10]] + (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])) + initial (tm/make-situation file) + initial (cond-> initial swapped? (r/step (n/swap-shape :copy-a :other))) + edited (-> initial r/settle (r/step (n/change-height :copy-a 80)) r/settle + (r/step (n/rotate :copy-a own-angle)) r/settle) + pending (r/step edited (n/rotate :c-b angle)) + pending-file (tm/file pending) + bare-changes (cll/generate-sync-shape-direct + (pcb/empty-changes) pending-file {(:id pending-file) pending-file} + (ctn/make-container (thf/current-page pending-file) :page) + (thi/id :copy-c) false) + bare-file (thf/apply-changes pending-file bare-changes) + turned (r/settle pending) + parent (tm/resolve-shape turned :copy-b) + child (tm/resolve-shape turned :copy-a) + old-parent (tm/resolve-shape edited :copy-b) + old-child (tm/resolve-shape edited :copy-a) + expected (-> old-child + (gsh/transform-shape (ctm/rotation-modifiers old-child (gsh/shape->center old-parent) angle)) + (gsh/move (gpt/subtract (gsh/shape->center parent) (gsh/shape->center old-parent)))) + file (tm/file turned) + full (:file (r/sync-cascade file {} (keys (get-in file [:data :components]))))] + (t/is (mth/close? angle (:rotation parent) 0.001)) + (t/is (mth/close? (+ angle own-angle) (:rotation child) 0.001)) + (t/is (= (boolean (:flip-x parent)) (boolean (:flip-x child)))) + (t/is (= (boolean (:flip-y parent)) (boolean (:flip-y child)))) + (t/is (mth/close? 80 (:height child) 0.001)) + (t/is (gsh/close-attrs? :points (:points expected) (:points child) 0.001)) + (t/is (gsh/close-attrs? :points (:points expected) + (:points (ths/get-shape-by-id bare-file (:id child))) 0.001)) + (t/is (mth/close? (+ angle own-angle) (:rotation (ths/get-shape-by-id bare-file (:id child))) 0.001)) + (t/is (= (if swapped? #{:geometry-group :swap-group} #{:geometry-group}) (:touched child))) + (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 edited)) + (thn/normalize-file (tm/file (r/step turned (n/undo))))))))) + (t/deftest a-nested-copy-keeps-its-detached-parents-orientation-on-sync (let [detached (-> (setup/nested-component-with-copy) (r/step (n/rotate :copy-root 90)) r/settle @@ -225,6 +278,6 @@ 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])] + flips (fn [shape] (mapv #(boolean (get shape %)) [:flip-x :flip-y]))] (t/is (= (flips shape) (flips after))) (t/is (gsh/close-attrs? :points (:points shape) (:points after) 0.001)))))) diff --git a/common/test/common_tests/runner.cljc b/common/test/common_tests/runner.cljc index 6c69349f17..b3abbd48d6 100644 --- a/common/test/common_tests/runner.cljc +++ b/common/test/common_tests/runner.cljc @@ -63,6 +63,7 @@ [common-tests.logic.chained-propagation-test] [common-tests.logic.comp-creation-test] [common-tests.logic.comp-detach-with-nested-test] + [common-tests.logic.comp-expected-geometry-test] [common-tests.logic.comp-flex-interactions-test] [common-tests.logic.comp-main-edit-breaks-copy-slots-test] [common-tests.logic.comp-move-swapped-copies-test] @@ -194,6 +195,7 @@ 'common-tests.logic.chained-propagation-test 'common-tests.logic.comp-creation-test 'common-tests.logic.comp-detach-with-nested-test + 'common-tests.logic.comp-expected-geometry-test 'common-tests.logic.comp-flex-interactions-test 'common-tests.logic.comp-main-edit-breaks-copy-slots-test 'common-tests.logic.comp-move-swapped-copies-test