diff --git a/common/src/app/common/files/migrations.cljc b/common/src/app/common/files/migrations.cljc index de82f16f9e..6a82e463ef 100644 --- a/common/src/app/common/files/migrations.cljc +++ b/common/src/app/common/files/migrations.cljc @@ -2296,6 +2296,45 @@ (update :pages-index d/update-vals update-container) (d/update-when :components d/update-vals update-container)))) +(defmethod migrate-data "0034-fix-path-content-offset" + [data _] + ;; A path stores its geometry twice: the box (:selrect and :points) and the + ;; drawing (:content), and the box is always derived from the drawing. Some + ;; files have component copies whose drawing and box were moved apart, so + ;; the drawing renders away from where the sidebar, the layouts and the + ;; syncs place the shape. We move the content back onto the box. A path whose drawing + ;; has another size than its box is not a displaced drawing, and is left as + ;; it is. + (let [tolerance 0.1] + (letfn [(fix-path [shape] + (when (and (cfh/path-shape? shape) + (some? (:selrect shape)) + (some? (path.segment/content->selrect (:content shape)))) + (let [selrect (:selrect shape) + derived (:selrect (path/update-geometry shape)) + offset (gpt/subtract (grc/rect->center selrect) + (grc/rect->center derived))] + (when (and (mth/close? (:width selrect) (:width derived) tolerance) + (mth/close? (:height selrect) (:height derived) tolerance) + (or (> (abs (:x offset)) tolerance) + (> (abs (:y offset)) tolerance))) + (update shape :content path/move-content offset))))) + + (fix-objects [objects] + (reduce-kv (fn [objects id shape] + (if-let [shape (fix-path shape)] + (assoc objects id shape) + objects)) + objects + objects)) + + (update-container [container] + (d/update-when container :objects fix-objects))] + + (-> data + (update :pages-index d/update-vals update-container) + (d/update-when :components d/update-vals update-container))))) + (def available-migrations (into (d/ordered-set) ["legacy-2" @@ -2386,4 +2425,5 @@ "0030-remove-stroke-per-side-attr" "0031-migrate-stroke-width-token-attr" "0032-fix-children-not-in-parent" - "0033-normalize-main-instance-names"])) + "0033-normalize-main-instance-names" + "0034-fix-path-content-offset"])) diff --git a/common/test/common_tests/files_migrations_0034_test.cljc b/common/test/common_tests/files_migrations_0034_test.cljc new file mode 100644 index 0000000000..82d335e4ff --- /dev/null +++ b/common/test/common_tests/files_migrations_0034_test.cljc @@ -0,0 +1,116 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS SUBSIDIARY SL + +(ns common-tests.files-migrations-0034-test + (:require + [app.common.files.migrations :as cfm] + [app.common.geom.matrix :as gmt] + [app.common.geom.point :as gpt] + [app.common.geom.shapes :as gsh] + [app.common.math :as mth] + [app.common.types.modifiers :as ctm] + [app.common.types.path :as path] + [app.common.types.path.segment :as segment] + [app.common.types.shape :as cts] + [app.common.uuid :as uuid] + [clojure.test :as t])) + +;; 0034-fix-path-content-offset +;; A path whose drawing (:content) was moved apart from its box +;; (:selrect and :points) gets its content moved back onto the box. + +(def ^:private migration "0034-fix-path-content-offset") + +(defn- triangle + [x0 y0] + [{:command :move-to :params {:x x0 :y y0}} + {:command :line-to :params {:x (+ x0 14) :y y0}} + {:command :line-to :params {:x (+ x0 7) :y (+ y0 14)}} + {:command :close-path}]) + +(defn- make-path + [shape-id x0 y0] + (cts/setup-shape {:id shape-id :type :path :content (triangle x0 y0)})) + +(defn- rotate + [shape angle] + (gsh/transform-shape shape (ctm/rotation-modifiers shape (gsh/shape->center shape) angle))) + +(defn- displace-content + [shape delta] + (update shape :content path/move-content delta)) + +(defn- migrate-shape + [shape] + (let [page-id (uuid/next) + data {:pages-index {page-id {:objects {(:id shape) shape}}}} + data' (cfm/migrate-data data migration)] + (get-in data' [:pages-index page-id :objects (:id shape)]))) + +(defn- content-matches-box? + [shape] + (let [derived (path/update-geometry shape)] + (and (every? true? (map gpt/close? (:points shape) (:points derived))) + (every? #(mth/close? (get (:selrect shape) %) (get (:selrect derived) %)) + [:x :y :width :height])))) + +(t/deftest migration-0034-moves-displaced-content-onto-its-box + (let [shape (make-path (uuid/next) 1 1) + broken (displace-content shape (gpt/point 0 -182)) + shape' (migrate-shape broken)] + + (t/is (not (content-matches-box? broken)) "the fixture is broken") + (t/is (content-matches-box? shape') "content back onto the box") + (t/is (= (:selrect broken) (:selrect shape')) "box unchanged") + (t/is (= (:points broken) (:points shape')) "points unchanged") + (t/is (= (segment/content->selrect (:content shape)) + (segment/content->selrect (:content shape'))) + "content where it was before being displaced"))) + +(t/deftest migration-0034-moves-displaced-content-of-a-rotated-path + (let [shape (rotate (make-path (uuid/next) 100 100) 30) + broken (displace-content shape (gpt/point 25 -40)) + shape' (migrate-shape broken)] + + (t/is (not (gmt/close? (gmt/matrix) (:transform shape))) "the fixture is rotated") + (t/is (not (content-matches-box? broken)) "the fixture is broken") + (t/is (content-matches-box? shape') "content back onto the box") + (t/is (= (:selrect broken) (:selrect shape')) "box unchanged") + (t/is (= (:transform broken) (:transform shape')) "rotation unchanged"))) + +(t/deftest migration-0034-keeps-paths-whose-content-matches-the-box + (let [shape (make-path (uuid/next) 10 20) + small (displace-content shape (gpt/point 0.05 0))] + + (t/is (identical? shape (migrate-shape shape)) "consistent path untouched") + (t/is (identical? small (migrate-shape small)) "float noise untouched"))) + +(t/deftest migration-0034-keeps-paths-whose-content-has-another-size + (let [shape (make-path (uuid/next) 0 0) + bigger (assoc shape :content (path/content [{:command :move-to :params {:x 40 :y 0}} + {:command :line-to :params {:x 70 :y 0}} + {:command :line-to :params {:x 55 :y 30}} + {:command :close-path}]))] + + (t/is (identical? bigger (migrate-shape bigger)) "size mismatch untouched"))) + +(t/deftest migration-0034-ignores-shapes-other-than-paths + (let [shape-id (uuid/next) + rect (cts/setup-shape {:id shape-id :type :rect :x 0 :y 0 :width 10 :height 10})] + (t/is (identical? rect (migrate-shape rect))))) + +(t/deftest migration-0034-fixes-paths-inside-components + (let [shape (make-path (uuid/next) 1 1) + broken (displace-content shape (gpt/point 0 109)) + component-id (uuid/next) + data {:pages-index {} + :components {component-id {:id component-id + :deleted true + :objects {(:id broken) broken}}}} + data' (cfm/migrate-data data migration) + shape' (get-in data' [:components component-id :objects (:id broken)])] + + (t/is (content-matches-box? shape') "deleted component content fixed"))) diff --git a/common/test/common_tests/runner.cljc b/common/test/common_tests/runner.cljc index d28a8adb41..9a774d271d 100644 --- a/common/test/common_tests/runner.cljc +++ b/common/test/common_tests/runner.cljc @@ -22,6 +22,7 @@ [common-tests.files-changes-test] [common-tests.files-migrations-0025-test] [common-tests.files-migrations-0026-test] + [common-tests.files-migrations-0034-test] [common-tests.files-migrations-test] [common-tests.files.comp-processors-test] [common-tests.files.helpers-test] @@ -130,6 +131,7 @@ 'common-tests.files-changes-test 'common-tests.files-migrations-0025-test 'common-tests.files-migrations-0026-test + 'common-tests.files-migrations-0034-test 'common-tests.files-migrations-test 'common-tests.files.comp-processors-test 'common-tests.files.helpers-test