diff --git a/common/test/common_tests/geom_flex_layout_test.cljc b/common/test/common_tests/geom_flex_layout_test.cljc new file mode 100644 index 0000000000..bc63b03c8c --- /dev/null +++ b/common/test/common_tests/geom_flex_layout_test.cljc @@ -0,0 +1,106 @@ +;; 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 INC + +(ns common-tests.geom-flex-layout-test + (:require + [app.common.geom.rect :as grc] + [app.common.geom.shapes.flex-layout.positions :as flp] + [app.common.math :as mth] + [app.common.types.shape :as cts] + [app.common.types.shape.layout :as ctl] + [clojure.test :as t])) + +;; ---- helpers ---- + +(defn- make-col-frame + "Minimal col? flex frame with wrap enabled. + wrap is required for the content-around? predicate to activate." + [& {:as opts}] + (cts/setup-shape (merge {:type :frame + :layout :flex + :layout-flex-dir :column + :layout-wrap-type :wrap + :x 0 :y 0 :width 200 :height 200} + opts))) + +(defn- rect->bounds + "Convert a rect to the 4-point layout-bounds vector expected by gpo/*." + [rect] + (grc/rect->points rect)) + +;; ---- get-base-line (around? branch) ---- +;; +;; Bug: in positions.cljc the col? + around? branch had a mis-parenthesised +;; expression `(/ free-width num-lines) 2`, which was parsed as three +;; arguments to `max`: +;; (max lines-gap-col (/ free-width num-lines) 2) +;; instead of the intended two-argument max with a nested division: +;; (max lines-gap-col (/ free-width num-lines 2)) +;; +;; For a col? layout the cross-axis is horizontal (hv), so the around? offset +;; is applied as hv(delta) — i.e. the delta ends up in (:x base-p). + +(t/deftest get-base-line-around-uses-half-per-line-free-width + (t/testing "col? + content-around? offset is free-width / num-lines / 2" + ;; Layout: col? wrap, width=200, 3 lines each 20px wide → free-width=140 + ;; lines-gap-col = 0 (no gap defined) + ;; Expected horizontal offset = max(0, 140/3/2) ≈ 23.33 + ;; Before the bug fix the formula was (max ... (/ 140 3) 2) ≈ 46.67. + (let [frame (make-col-frame :layout-align-content :space-around) + bounds (rect->bounds (grc/make-rect 0 0 200 200)) + ;; 3 lines of 20px each (widths); no row gap + num-lines 3 + total-width 60 + total-height 0 + base-p (flp/get-base-line frame bounds total-width total-height num-lines) + free-width (- 200 total-width) + ;; lines-gap-col = (dec 3) * 0 = 0; max(0, free-width/num-lines/2) + expected-x (/ free-width num-lines 2)] + + ;; The base point x-coordinate (hv offset) should equal half per-line free space. + (t/is (mth/close? expected-x (:x base-p) 0.01)))) + + (t/testing "col? + content-around? offset respects lines-gap-col minimum" + ;; When the accumulated column gap exceeds the computed half-per-line value + ;; max(lines-gap-col, free-width/num-lines/2) returns the gap. + (let [frame (make-col-frame :layout-align-content :space-around + :layout-gap {:column-gap 50 :row-gap 0}) + bounds (rect->bounds (grc/make-rect 0 0 200 200)) + ;; 4 lines × 20px = 80px used; free-width=120; half-per-line = 120/4/2 = 15 + ;; lines-gap-col = (dec 4)*50 = 150 → max(150, 15) = 150 + num-lines 4 + total-width 80 + total-height 0 + base-p (flp/get-base-line frame bounds total-width total-height num-lines) + lines-gap-col (* (dec num-lines) 50)] + + (t/is (mth/close? lines-gap-col (:x base-p) 0.01))))) + +;; ---- v-end? guard (drop-line-area) ---- +;; +;; Bug: `v-end?` inside `drop-line-area` was guarded by `row?` instead of +;; `col?`, so vertical-end alignment in a column layout was never triggered. +;; We verify the predicate behaviour directly via ctl/v-end?. + +(t/deftest v-end-guard-uses-col-not-row + (t/testing "v-end? is true for col? frame with justify-content :end" + ;; col? + justify-content=:end → ctl/v-end? must be true + (let [frame (cts/setup-shape {:type :frame + :layout :flex + :layout-flex-dir :column + :layout-justify-content :end + :x 0 :y 0 :width 100 :height 100})] + (t/is (true? (ctl/v-end? frame))))) + + (t/testing "v-end? is false for row? frame with only justify-content :end" + ;; row? + justify-content=:end alone does NOT set v-end?; for row layouts + ;; v-end? checks align-items, not justify-content. + (let [frame (cts/setup-shape {:type :frame + :layout :flex + :layout-flex-dir :row + :layout-justify-content :end + :x 0 :y 0 :width 100 :height 100})] + (t/is (not (ctl/v-end? frame)))))) diff --git a/common/test/common_tests/geom_point_test.cljc b/common/test/common_tests/geom_point_test.cljc index 6ba7239f07..2d0e2e6468 100644 --- a/common/test/common_tests/geom_point_test.cljc +++ b/common/test/common_tests/geom_point_test.cljc @@ -289,3 +289,33 @@ (t/is (mth/close? 1.2091818119288809 (:x rs))) (t/is (mth/close? 1.8275638211757912 (:y rs))))) +;; ---- gpt/abs ---- + +(t/deftest abs-point-returns-point-instance + (t/testing "abs of a point with negative coordinates returns a Point record" + (let [p (gpt/point -3 -4) + rs (gpt/abs p)] + (t/is (gpt/point? rs)) + (t/is (mth/close? 3 (:x rs))) + (t/is (mth/close? 4 (:y rs))))) + + (t/testing "abs of a point with mixed-sign coordinates" + (let [p (gpt/point -5 7) + rs (gpt/abs p)] + (t/is (gpt/point? rs)) + (t/is (mth/close? 5 (:x rs))) + (t/is (mth/close? 7 (:y rs))))) + + (t/testing "abs of a point already positive is unchanged" + (let [p (gpt/point 2 9) + rs (gpt/abs p)] + (t/is (gpt/point? rs)) + (t/is (mth/close? 2 (:x rs))) + (t/is (mth/close? 9 (:y rs))))) + + (t/testing "abs of a zero point stays zero" + (let [rs (gpt/abs (gpt/point 0 0))] + (t/is (gpt/point? rs)) + (t/is (mth/close? 0 (:x rs))) + (t/is (mth/close? 0 (:y rs)))))) + diff --git a/common/test/common_tests/geom_rect_test.cljc b/common/test/common_tests/geom_rect_test.cljc new file mode 100644 index 0000000000..8abfb76854 --- /dev/null +++ b/common/test/common_tests/geom_rect_test.cljc @@ -0,0 +1,94 @@ +;; 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 INC + +(ns common-tests.geom-rect-test + (:require + [app.common.geom.rect :as grc] + [app.common.math :as mth] + [clojure.test :as t])) + +;; ---- update-rect :size ---- + +(t/deftest update-rect-size-sets-all-corners + (t/testing ":size updates x1/y1 as well as x2/y2 from x/y/width/height" + (let [r (grc/make-rect 10 20 30 40) + r' (grc/update-rect r :size)] + ;; x1/y1 must mirror x/y + (t/is (mth/close? (:x r) (:x1 r'))) + (t/is (mth/close? (:y r) (:y1 r'))) + ;; x2/y2 must be x+width / y+height + (t/is (mth/close? (+ (:x r) (:width r)) (:x2 r'))) + (t/is (mth/close? (+ (:y r) (:height r)) (:y2 r'))))) + + (t/testing ":size is consistent with :corners round-trip" + ;; Applying :size then :corners should recover the original x/y/w/h + (let [r (grc/make-rect 5 15 100 50) + r' (-> r (grc/update-rect :size) (grc/update-rect :corners))] + (t/is (mth/close? (:x r) (:x r'))) + (t/is (mth/close? (:y r) (:y r'))) + (t/is (mth/close? (:width r) (:width r'))) + (t/is (mth/close? (:height r) (:height r'))))) + + (t/testing ":size works for a rect at the origin" + (let [r (grc/make-rect 0 0 200 100) + r' (grc/update-rect r :size)] + (t/is (mth/close? 0 (:x1 r'))) + (t/is (mth/close? 0 (:y1 r'))) + (t/is (mth/close? 200 (:x2 r'))) + (t/is (mth/close? 100 (:y2 r')))))) + +;; ---- corners->rect ---- + +(t/deftest corners->rect-normal-order + (t/testing "p1 top-left, p2 bottom-right yields a valid rect" + (let [r (grc/corners->rect 0 0 10 20)] + (t/is (grc/rect? r)) + (t/is (mth/close? 0 (:x r))) + (t/is (mth/close? 0 (:y r))) + (t/is (mth/close? 10 (:width r))) + (t/is (mth/close? 20 (:height r)))))) + +(t/deftest corners->rect-reversed-corners + (t/testing "reversed x-coordinates still produce a positive-width rect" + (let [r (grc/corners->rect 10 0 0 20)] + (t/is (grc/rect? r)) + (t/is (mth/close? 0 (:x r))) + (t/is (mth/close? 10 (:width r))))) + + (t/testing "reversed y-coordinates still produce a positive-height rect" + (let [r (grc/corners->rect 0 20 10 0)] + (t/is (grc/rect? r)) + (t/is (mth/close? 0 (:y r))) + (t/is (mth/close? 20 (:height r))))) + + (t/testing "both axes reversed yield the same rect as normal order" + (let [r-normal (grc/corners->rect 0 0 10 20) + r-reversed (grc/corners->rect 10 20 0 0)] + (t/is (mth/close? (:x r-normal) (:x r-reversed))) + (t/is (mth/close? (:y r-normal) (:y r-reversed))) + (t/is (mth/close? (:width r-normal) (:width r-reversed))) + (t/is (mth/close? (:height r-normal) (:height r-reversed)))))) + +(t/deftest corners->rect-from-points + (t/testing "two-arity overload taking point maps works identically" + (let [p1 {:x 5 :y 10} + p2 {:x 15 :y 30} + r (grc/corners->rect p1 p2)] + (t/is (grc/rect? r)) + (t/is (mth/close? 5 (:x r))) + (t/is (mth/close? 10 (:y r))) + (t/is (mth/close? 10 (:width r))) + (t/is (mth/close? 20 (:height r))))) + + (t/testing "two-arity overload with reversed points" + (let [p1 {:x 15 :y 30} + p2 {:x 5 :y 10} + r (grc/corners->rect p1 p2)] + (t/is (grc/rect? r)) + (t/is (mth/close? 5 (:x r))) + (t/is (mth/close? 10 (:y r))) + (t/is (mth/close? 10 (:width r))) + (t/is (mth/close? 20 (:height r)))))) diff --git a/common/test/common_tests/geom_shapes_constraints_test.cljc b/common/test/common_tests/geom_shapes_constraints_test.cljc new file mode 100644 index 0000000000..175cc6f77b --- /dev/null +++ b/common/test/common_tests/geom_shapes_constraints_test.cljc @@ -0,0 +1,27 @@ +;; 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 INC + +(ns common-tests.geom-shapes-constraints-test + (:require + [app.common.geom.shapes.constraints :as gsc] + [clojure.test :as t])) + +;; ---- constraint-modifier :default ---- + +(t/deftest constraint-modifier-default-returns-empty-vector + (t/testing ":default method accepts 6 args and returns an empty vector" + ;; Before the fix the :default method only accepted 5 positional args + ;; (plus the dispatch value), so calling it with 6 args would throw an + ;; arity error. After the fix it takes [_ _ _ _ _ _] and returns []. + (let [result (gsc/constraint-modifier :unknown-constraint-type + :x nil nil nil nil)] + (t/is (vector? result)) + (t/is (empty? result)))) + + (t/testing ":default method returns [] for :scale-like unknown type on :y axis" + (let [result (gsc/constraint-modifier :some-other-unknown + :y nil nil nil nil)] + (t/is (= [] result))))) diff --git a/common/test/common_tests/geom_shapes_test.cljc b/common/test/common_tests/geom_shapes_test.cljc index 29401412ec..7e5c83658d 100644 --- a/common/test/common_tests/geom_shapes_test.cljc +++ b/common/test/common_tests/geom_shapes_test.cljc @@ -230,3 +230,44 @@ (t/is (true? (gsin/slow-has-point? shape point1))) (t/is (false? (gsin/fast-has-point? shape point2))) (t/is (false? (gsin/fast-has-point? shape point2))))) + +;; ---- adjust-shape-flips (via apply-transform / transform-shape) ---- + +(t/deftest flip-x-only-toggles-flip-x-and-negates-rotation + (t/testing "Flipping only X axis toggles flip-x and negates rotation" + ;; Build a rect with a known rotation, then apply a scale(-1, 1) + ;; from the left edge to simulate an X-axis flip. + (let [shape (create-test-shape :rect {:rotation 30}) + ;; Flip horizontally about x=0 (left edge of shape) + origin (gpt/point (get-in shape [:selrect :x]) (get-in shape [:selrect :y])) + mods (ctm/resize-modifiers (gpt/point -1 1) origin) + result (gsh/transform-shape shape mods)] + ;; flip-x should have been toggled (from nil/false to true) + (t/is (true? (:flip-x result))) + ;; flip-y should NOT be set + (t/is (not (true? (:flip-y result)))) + ;; rotation is negated then normalised into [0,360): -30 mod 360 = 330 + (t/is (mth/close? 330 (:rotation result)))))) + +(t/deftest flip-y-only-toggles-flip-y-and-negates-rotation + (t/testing "Flipping only Y axis toggles flip-y and negates rotation" + (let [shape (create-test-shape :rect {:rotation 45}) + origin (gpt/point (get-in shape [:selrect :x]) (get-in shape [:selrect :y])) + mods (ctm/resize-modifiers (gpt/point 1 -1) origin) + result (gsh/transform-shape shape mods)] + (t/is (not (true? (:flip-x result)))) + (t/is (true? (:flip-y result))) + ;; -45 mod 360 = 315 + (t/is (mth/close? 315 (:rotation result)))))) + +(t/deftest flip-both-axes-toggles-both-flags-but-preserves-rotation + (t/testing "Flipping both axes toggles flip-x and flip-y, but does NOT negate rotation" + ;; Two simultaneous axis flips = 180° rotation, so stored rotation is unchanged. + (let [shape (create-test-shape :rect {:rotation 30}) + origin (gpt/point (get-in shape [:selrect :x]) (get-in shape [:selrect :y])) + mods (ctm/resize-modifiers (gpt/point -1 -1) origin) + result (gsh/transform-shape shape mods)] + (t/is (true? (:flip-x result))) + (t/is (true? (:flip-y result))) + ;; rotation must not be negated when both axes are flipped + (t/is (mth/close? 30 (:rotation result)))))) diff --git a/common/test/common_tests/geom_test.cljc b/common/test/common_tests/geom_test.cljc index b0d336475a..28560d3544 100644 --- a/common/test/common_tests/geom_test.cljc +++ b/common/test/common_tests/geom_test.cljc @@ -9,6 +9,7 @@ [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.math :as mth] + [app.common.schema :as sm] [clojure.test :as t])) (t/deftest point-constructors-test @@ -100,3 +101,28 @@ (let [m (-> (gmt/matrix) (gmt/rotate 10))] (t/is (= m (gmt/matrix 0.984807753012208 0.17364817766693033 -0.17364817766693033 0.984807753012208 0 0))))) + +;; ---- matrix->str (no trailing comma) ---- + +(t/deftest matrix-str-roundtrip-test + (t/testing "Identity matrix encodes and decodes back to equal matrix" + (let [m (gmt/matrix) + enc (sm/encode gmt/schema:matrix m (sm/string-transformer)) + dec (sm/decode gmt/schema:matrix enc (sm/string-transformer))] + (t/is (string? enc)) + ;; Must not end with a comma + (t/is (not= \, (last enc))) + (t/is (gmt/close? m dec)))) + + (t/testing "Arbitrary matrix encodes without trailing comma and round-trips" + (let [m (gmt/matrix 2 0.5 -0.5 3 10 20) + enc (sm/encode gmt/schema:matrix m (sm/string-transformer)) + dec (sm/decode gmt/schema:matrix enc (sm/string-transformer))] + (t/is (string? enc)) + (t/is (not= \, (last enc))) + (t/is (gmt/close? m dec)))) + + (t/testing "Encoded string contains exactly 5 commas (6 fields)" + (let [m (gmt/matrix 1 0 0 1 0 0) + enc (sm/encode gmt/schema:matrix m (sm/string-transformer))] + (t/is (= 5 (count (filter #(= \, %) enc))))))) diff --git a/common/test/common_tests/runner.cljc b/common/test/common_tests/runner.cljc index 10ab8a48f6..368bc4ce31 100644 --- a/common/test/common_tests/runner.cljc +++ b/common/test/common_tests/runner.cljc @@ -15,6 +15,7 @@ [common-tests.files-migrations-test] [common-tests.geom-align-test] [common-tests.geom-bounds-map-test] + [common-tests.geom-flex-layout-test] [common-tests.geom-grid-layout-test] [common-tests.geom-grid-test] [common-tests.geom-line-test] @@ -22,7 +23,9 @@ [common-tests.geom-modifiers-test] [common-tests.geom-point-test] [common-tests.geom-proportions-test] + [common-tests.geom-rect-test] [common-tests.geom-shapes-common-test] + [common-tests.geom-shapes-constraints-test] [common-tests.geom-shapes-corners-test] [common-tests.geom-shapes-effects-test] [common-tests.geom-shapes-intersect-test] @@ -88,13 +91,17 @@ 'common-tests.files-migrations-test 'common-tests.geom-align-test 'common-tests.geom-bounds-map-test + 'common-tests.geom-flex-layout-test + 'common-tests.geom-grid-layout-test 'common-tests.geom-grid-test 'common-tests.geom-line-test 'common-tests.geom-modif-tree-test 'common-tests.geom-modifiers-test 'common-tests.geom-point-test 'common-tests.geom-proportions-test + 'common-tests.geom-rect-test 'common-tests.geom-shapes-common-test + 'common-tests.geom-shapes-constraints-test 'common-tests.geom-shapes-corners-test 'common-tests.geom-shapes-effects-test 'common-tests.geom-shapes-intersect-test