mirror of
https://github.com/penpot/penpot.git
synced 2026-11-11 04:20:42 +00:00
Cross-axis fill sizing reaches distribution before the current line size is initialized. Count eligible items from the minimum fallback, as the update loop already does. Use zero increments when every item is at its maximum, avoiding division by zero. Add portable controls for minimum initialization, redistribution at a cap and fully saturated items. All three raise arithmetic errors before the fix and pass afterward. Generated vertical-fill histories cover the full layout and component pipeline too. Validate the final continuation on common JVM (2004 tests / 32919 assertions), common JS (1940 / 31522), frontend (1745 / 13569), backend (1793 / 9007) and exporter (16 / 40). All four modules pass lint and format. All six generated properties pass 40 programs at seed 712 in both runtimes. Golden data is unchanged. The final continuation benchmark passes with no regression flags. Lower quantile ratios against Phase 1e: main 10/100/1000 0.51/0.58/0.57, three levels 0.57, chain 0.53, library 0.57, many shapes 0.45, variant switch 0.66. Machine/JVM differences make these rough comparisons; baseline unchanged. AI-assisted-by: gpt-6
194 lines
9.4 KiB
Clojure
194 lines
9.4 KiB
Clojure
;; 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.geom-flex-layout-test
|
||
(:require
|
||
[app.common.geom.modifiers]
|
||
[app.common.geom.rect :as grc]
|
||
[app.common.geom.shapes.flex-layout.layout-data :as fld]
|
||
[app.common.geom.shapes.flex-layout.params :as flparams]
|
||
[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))
|
||
|
||
(t/deftest empty-wrapped-layout-has-no-lines
|
||
(doseq [direction [:row :column]
|
||
alignment [:stretch :space-around :space-evenly :space-between :center :end]]
|
||
(let [frame (make-col-frame :layout-flex-dir direction
|
||
:layout-align-content alignment
|
||
:layout-padding {:p1 0 :p2 0 :p3 0 :p4 0}
|
||
:layout-padding-type :simple)
|
||
bounds (rect->bounds (grc/make-rect 0 0 200 200))]
|
||
(t/is (= [] (:layout-lines (fld/calc-layout-data frame bounds [] {} {})))))))
|
||
|
||
(t/deftest space-between-distributes-gap-only-between-several-children
|
||
(doseq [direction [:row :row-reverse :column :column-reverse]
|
||
children [1 2]]
|
||
(let [row? (#{:row :row-reverse} direction)
|
||
frame (make-col-frame :layout-flex-dir direction
|
||
:layout-justify-content :space-between
|
||
:layout-gap {:row-gap 11 :column-gap 7})
|
||
bounds (rect->bounds (grc/make-rect 0 0 200 200))
|
||
line (fld/add-line-spacing frame bounds false
|
||
{:num-children children
|
||
:line-width (if row? (* 50 children) 50)
|
||
:line-height (if row? 30 (* 30 children))})
|
||
gap (get line (if row? :layout-gap-col :layout-gap-row))]
|
||
(t/is (== (if (= children 1) (if row? 7 11) (if row? 100 140)) gap))
|
||
(t/is (every? mth/finite? (map line [:layout-gap-col :layout-gap-row :margin-x :margin-y]))))))
|
||
|
||
;; ---- 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))))))
|
||
|
||
;; ---- calculate-params paddings ----
|
||
;;
|
||
;; Adding a flex layout derives the paddings from the distance between the
|
||
;; board and its children. A child that overflows the board, or floating
|
||
;; point noise after "Resize board to fit content", gave negative paddings
|
||
;; that the shape schema rejects, which broke autosave (#12107).
|
||
|
||
(defn- params-for-child
|
||
[child-attrs]
|
||
(let [parent (cts/setup-shape {:type :frame :x 0 :y 0 :width 100 :height 100})
|
||
child (cts/setup-shape (merge {:type :rect :parent-id (:id parent)} child-attrs))
|
||
parent (assoc parent :shapes [(:id child)])
|
||
objects {(:id parent) parent (:id child) child}]
|
||
(flparams/calculate-params objects [child] parent)))
|
||
|
||
(t/deftest calculate-params-keeps-positive-padding
|
||
(t/is (= {:p1 20 :p2 10 :p3 20 :p4 10}
|
||
(:layout-padding (params-for-child {:x 10 :y 20 :width 50 :height 50})))))
|
||
|
||
(t/deftest calculate-params-clamps-overflowing-child-padding
|
||
(t/is (= {:p1 0 :p2 0 :p3 0 :p4 0}
|
||
(:layout-padding (params-for-child {:x -10 :y -5 :width 150 :height 150})))))
|
||
|
||
(t/deftest calculate-params-clamps-float-noise-padding
|
||
(let [padding (:layout-padding
|
||
(params-for-child {:x -1.1728368917829357E-5
|
||
:y -2.1457672119140625E-6
|
||
:width 100 :height 100}))]
|
||
(t/is (every? #(not (neg? %)) (vals padding)))))
|
||
|
||
(t/deftest first-child-starts-a-flex-line-on-both-runtimes
|
||
(doseq [direction [:row :column]
|
||
justify [:start :space-around :space-evenly]]
|
||
(let [frame (make-col-frame :layout-flex-dir direction :layout-justify-content justify)
|
||
child (cts/setup-shape {:type :rect :x 0 :y 0 :width 40 :height 30})
|
||
bounds (rect->bounds (grc/make-rect 0 0 200 200))
|
||
child-bounds (rect->bounds (grc/make-rect 0 0 40 30))
|
||
objects {(:id frame) frame (:id child) child}
|
||
lines (fld/init-layout-lines frame [[child-bounds child]] bounds
|
||
{(:id frame) bounds (:id child) child-bounds} objects false)]
|
||
(t/is (= 1 (count lines)))
|
||
(t/is (= 1 (:num-children (first lines)))))))
|
||
|
||
(t/deftest flex-space-distribution-starts-at-the-minimum-and-stops-at-the-maximum
|
||
(t/is (= [{:min-size 20 :max-size 30 :size 30}
|
||
{:min-size 10 :max-size 100 :size 40}]
|
||
(fld/distribute-space :size :min-size :max-size 30 70
|
||
[{:min-size 20 :max-size 30}
|
||
{:min-size 10 :max-size 100}])))
|
||
(t/is (= [{:min-size 20 :max-size 30 :size 30}
|
||
{:min-size 10 :max-size 40 :size 40}]
|
||
(fld/distribute-space :size :min-size :max-size 30 100
|
||
[{:min-size 20 :max-size 30}
|
||
{:min-size 10 :max-size 40}])))
|
||
(t/is (= [[{:min-size 10 :max-size 10 :size 10}] 5]
|
||
(fld/add-space-to-items :size :min-size :max-size 5
|
||
[{:min-size 10 :max-size 10 :size 10}]))))
|