mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 14:00:37 +00:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
commit
a5df69f60f
2
.gitignore
vendored
2
.gitignore
vendored
@ -83,6 +83,7 @@ opencode.json
|
||||
/web
|
||||
/library/target/
|
||||
/library/*.zip
|
||||
/library/.shadow-cljs
|
||||
/external
|
||||
/penpot-nitrate
|
||||
/test-results/
|
||||
@ -90,6 +91,7 @@ opencode.json
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
/render-wasm/target/
|
||||
/render-wasm/.render-wasm-build.lock
|
||||
/media-processor/dist/
|
||||
/media-processor/target/
|
||||
/**/node_modules
|
||||
|
||||
@ -23,18 +23,18 @@
|
||||
(if (:deleted component)
|
||||
(if (nil? (:objects component))
|
||||
(do
|
||||
(log/warn :msg "Adding empty :objects to deleted component"
|
||||
:component-id (:id component)
|
||||
(log/warn :hint "adding empty :objects to deleted component"
|
||||
:component-id (str (:id component))
|
||||
:component-name (:name component)
|
||||
:file-id (:id file-data))
|
||||
:file-id (str (:id file-data)))
|
||||
(assoc component :objects {}))
|
||||
component)
|
||||
(if (contains? component :objects)
|
||||
(do
|
||||
(log/warn :msg "Removing :objects from non-deleted component"
|
||||
:component-id (:id component)
|
||||
(log/warn :hint "removing :objects from non-deleted component"
|
||||
:component-id (str (:id component))
|
||||
:component-name (:name component)
|
||||
:file-id (:id file-data))
|
||||
:file-id (str (:id file-data)))
|
||||
(dissoc component :objects))
|
||||
component)))))
|
||||
|
||||
@ -50,15 +50,15 @@
|
||||
(fn [shape]
|
||||
(if (false? (:component-root shape))
|
||||
(do
|
||||
(log/warn :msg "Normalizing :component-root false on shape"
|
||||
:shape-id (:id shape)
|
||||
(log/warn :hint "normalizing :component-root false on shape"
|
||||
:shape-id (str (:id shape))
|
||||
:shape-name (:name shape)
|
||||
:file-id (:id file-data))
|
||||
:file-id (str (:id file-data)))
|
||||
{:result :update :updated-shape (dissoc shape :component-root)})
|
||||
{:result :keep})))
|
||||
(catch #?(:clj Throwable :cljs :default) e
|
||||
(log/warn :msg "Failed to normalize :component-root on shapes"
|
||||
:file-id (:id file-data)
|
||||
(log/warn :hint "failed to normalize :component-root on shapes"
|
||||
:file-id (str (:id file-data))
|
||||
:cause e)
|
||||
file-data)))
|
||||
|
||||
@ -82,19 +82,19 @@
|
||||
(not= (:shape-ref shape) (:id near-match))
|
||||
(nil? (ctk/get-swap-slot shape)))
|
||||
(let [updated-shape (ctk/set-swap-slot shape (:id near-match))]
|
||||
(log/warn :msg "Adding missing swap slot to shape"
|
||||
:shape-id (:id shape)
|
||||
(log/warn :hint "adding missing swap slot to shape"
|
||||
:shape-id (str (:id shape))
|
||||
:shape-name (:name shape)
|
||||
:swap-slot (:id near-match)
|
||||
:file-id (:id file)
|
||||
:container-id (:id container)
|
||||
:swap-slot (str (:id near-match))
|
||||
:file-id (str (:id file))
|
||||
:container-id (str (:id container))
|
||||
:container-type (:type container))
|
||||
{:result :update :updated-shape updated-shape})
|
||||
{:result :keep}))
|
||||
{:result :keep})))
|
||||
(catch #?(:clj Throwable :cljs :default) e
|
||||
(log/warn :msg "Failed to fix missing swap slots on shapes"
|
||||
:file-id (:id file-data)
|
||||
(log/warn :hint "failed to fix missing swap slots on shapes"
|
||||
:file-id (str (:id file-data))
|
||||
:cause e)
|
||||
file-data)))
|
||||
|
||||
@ -127,21 +127,21 @@
|
||||
|
||||
(nil? (:component-file ref-shape))
|
||||
(dissoc :component-file))]
|
||||
(log/warn :msg "Syncing component id and file with ref shape"
|
||||
:shape-id (:id shape)
|
||||
(log/warn :hint "syncing component id and file with ref shape"
|
||||
:shape-id (str (:id shape))
|
||||
:shape-name (:name shape)
|
||||
:component-id (:component-id shape')
|
||||
:component-file (:component-file shape')
|
||||
:ref-shape-id (:id ref-shape)
|
||||
:file-id (:id file)
|
||||
:container-id (:id container)
|
||||
:component-id (str (:component-id shape'))
|
||||
:component-file (str (:component-file shape'))
|
||||
:ref-shape-id (str (:id ref-shape))
|
||||
:file-id (str (:id file))
|
||||
:container-id (str (:id container))
|
||||
:container-type (:type container))
|
||||
{:result :update :updated-shape shape'})
|
||||
{:result :keep}))
|
||||
{:result :keep})))
|
||||
(catch #?(:clj Throwable :cljs :default) e
|
||||
(log/warn :msg "Failed to sync component id and file with ref shape"
|
||||
:file-id (:id file-data)
|
||||
(log/warn :hint "failed to sync component id and file with ref shape"
|
||||
:file-id (str (:id file-data))
|
||||
:cause e)
|
||||
file-data)))]
|
||||
;; If a copy inside a main is updated, we need to repeat the process for the change to be
|
||||
|
||||
@ -171,8 +171,8 @@ test("Selection size badge shows dimensions for path shapes", async ({ page }) =
|
||||
// Workaround: hover viewport first to avoid nil mouse position crash
|
||||
await workspacePage.viewport.hover();
|
||||
|
||||
// Draw a path with two segments; a single straight segment shows
|
||||
// endpoint controls instead of the size badge
|
||||
// Draw a path with two segments; a single straight segment places
|
||||
// the size badge along the line instead
|
||||
await workspacePage.pathButton.click();
|
||||
await workspacePage.clickAt(779, 163);
|
||||
await workspacePage.clickAt(951, 258);
|
||||
@ -189,7 +189,7 @@ test("Selection size badge shows dimensions for path shapes", async ({ page }) =
|
||||
await expect(badgeText).toHaveText(/\d+\.?\d* x \d+\.?\d*/);
|
||||
});
|
||||
|
||||
test("Selection size badge is hidden for straight line paths", async ({
|
||||
test("Selection size badge runs along straight line paths", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspacePage = new WasmWorkspacePage(page);
|
||||
@ -204,7 +204,7 @@ test("Selection size badge is hidden for straight line paths", async ({
|
||||
// Workaround: hover viewport first to avoid nil mouse position crash
|
||||
await workspacePage.viewport.hover();
|
||||
|
||||
// Draw a path with a single straight segment
|
||||
// Draw a path with a single straight segment, going down to the right
|
||||
await workspacePage.pathButton.click();
|
||||
await workspacePage.clickAt(779, 163);
|
||||
await workspacePage.clickAt(951, 258);
|
||||
@ -216,7 +216,16 @@ test("Selection size badge is hidden for straight line paths", async ({
|
||||
await page.keyboard.press("Escape");
|
||||
|
||||
await expect(page.locator(".line-controls")).toBeVisible();
|
||||
await expect(page.locator(".selection-size-badge")).toHaveCount(0);
|
||||
|
||||
// A straight line shows its length, with two decimals at most
|
||||
const badge = page.locator(".selection-size-badge");
|
||||
await expect(badge.locator("text")).toHaveText(/^\d+(\.\d{1,2})?$/);
|
||||
|
||||
// The badge follows the line angle, about 29 degrees
|
||||
const transform = await badge.getAttribute("transform");
|
||||
const rotation = Number(transform.match(/rotate\(([^)]+)\)/)[1]);
|
||||
expect(rotation).toBeGreaterThan(20);
|
||||
expect(rotation).toBeLessThan(40);
|
||||
});
|
||||
|
||||
test("User makes a group", async ({ page }) => {
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
[app.common.math :as mth]
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.types.path :as path]
|
||||
[app.common.types.path.helpers :as path.helpers]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.constants :as mconst]
|
||||
[app.main.ui.formats :as fmt]
|
||||
@ -239,28 +240,41 @@
|
||||
(<= (+ cy (/ badge-height 2))
|
||||
(+ (:y vbox) (:height vbox)))))
|
||||
|
||||
(mf/defc selection-size-badge*
|
||||
[{:keys [zoom shapes vbox]}]
|
||||
(defn- line-badge-position
|
||||
"Badge center and rotation for a badge running along the segment `p1`-`p2`.
|
||||
The direction is flipped when needed so the text stays upright."
|
||||
[p1 p2 offset]
|
||||
(let [dir (gpt/subtract p2 p1)
|
||||
angle (gpt/angle dir)
|
||||
upright? (and (> angle -90) (<= angle 90))
|
||||
dir (if upright? dir (gpt/negate dir))
|
||||
rot (cond
|
||||
upright? angle
|
||||
(pos? angle) (- angle 180)
|
||||
:else (+ angle 180))
|
||||
mid-point (gpt/lerp p1 p2 0.5)
|
||||
normal (gpt/normal-left dir)]
|
||||
{:cx (+ (:x mid-point) (* (:x normal) offset))
|
||||
:cy (+ (:y mid-point) (* (:y normal) offset))
|
||||
:rot rot}))
|
||||
|
||||
(defn size-badge-layout
|
||||
"Geometry of the size badge for the selected `shapes`, in viewport
|
||||
units. Returns nil when the badge is hidden, otherwise a map with the
|
||||
badge `:text`, `:width`, `:height`, its center `:cx`/`:cy` and its
|
||||
rotation `:rot` in degrees."
|
||||
[shapes zoom vbox]
|
||||
(let [badge-height (/ selection-badge-height zoom)
|
||||
badge-padding-x (/ selection-badge-padding-x zoom)
|
||||
badge-gap (/ selection-badge-vertical-gap zoom)
|
||||
badge-radius (/ selection-badge-border-radius zoom)
|
||||
badge-char-width (/ selection-badge-char-width zoom)
|
||||
|
||||
single-shape (and (= (count shapes) 1) (first shapes))
|
||||
|
||||
;; Straight paths use endpoint controls instead of a size badge.
|
||||
single-line? (and single-shape
|
||||
(cfh/path-shape? single-shape)
|
||||
(path/single-line? (dm/get-prop single-shape :content)))
|
||||
|
||||
component-color? (if single-shape
|
||||
(ctk/instance-head? single-shape)
|
||||
(every? ctk/instance-head? shapes))
|
||||
badge-bg-color (if component-color?
|
||||
selection-badge-bg-color-component
|
||||
selection-badge-bg-color)
|
||||
|
||||
rotation (when single-shape (dm/get-prop single-shape :rotation))
|
||||
has-rotation? (and rotation (not (mth/almost-zero? rotation)))
|
||||
|
||||
@ -269,86 +283,101 @@
|
||||
;; including component instances and shapes with transforms.
|
||||
selrect (gsh/shapes->rect shapes)
|
||||
|
||||
;; For single shapes we show the original dimensions,
|
||||
;; for multiple shapes show the bounding box
|
||||
shape-width (if single-shape
|
||||
(or (dm/get-prop single-shape :width)
|
||||
(:width selrect))
|
||||
(:width selrect))
|
||||
shape-height (if single-shape
|
||||
(or (dm/get-prop single-shape :height)
|
||||
(:height selrect))
|
||||
(:height selrect))
|
||||
;; For single shapes we show the same dimensions as the measures
|
||||
;; inputs (the shape :selrect, which ignores rotation and also
|
||||
;; exists for paths); for multiple shapes show the bounding box
|
||||
shape-rect (if single-shape
|
||||
(dm/get-prop single-shape :selrect)
|
||||
selrect)
|
||||
shape-width (:width shape-rect)
|
||||
shape-height (:height shape-rect)
|
||||
|
||||
text (dm/str (fmt/format-number shape-width) " x " (fmt/format-number shape-height))
|
||||
[line-p1 line-p2]
|
||||
(when single-line?
|
||||
(let [content (dm/get-prop single-shape :content)]
|
||||
[(path.helpers/segment->point (nth content 0))
|
||||
(path.helpers/segment->point (nth content 1))]))
|
||||
|
||||
line-length (when single-line?
|
||||
(gpt/distance line-p1 line-p2))
|
||||
|
||||
;; A straight line shows its length (two decimals at most);
|
||||
;; other shapes show their width and height.
|
||||
text (if single-line?
|
||||
(fmt/format-number line-length)
|
||||
(dm/str (fmt/format-number shape-width) " x " (fmt/format-number shape-height)))
|
||||
|
||||
text-width (* (count text) badge-char-width)
|
||||
badge-width (+ text-width (* 2 badge-padding-x))
|
||||
offset (+ badge-gap (/ badge-height 2))
|
||||
|
||||
hidden? (or single-line?
|
||||
(< shape-width badge-width)
|
||||
(< shape-height badge-height))]
|
||||
;; A straight line only has a length; any other shape, paths
|
||||
;; included, must fit the badge in both dimensions.
|
||||
hidden? (if single-line?
|
||||
(< line-length badge-width)
|
||||
(or (< shape-width badge-width)
|
||||
(< shape-height badge-height)))]
|
||||
|
||||
(when-not ^boolean hidden?
|
||||
(if has-rotation?
|
||||
(let [edge (get-edge-for-badge rotation)
|
||||
points (dm/get-prop single-shape :points)
|
||||
offset (+ badge-gap (/ badge-height 2))
|
||||
(-> (cond
|
||||
single-line?
|
||||
(let [position (line-badge-position line-p1 line-p2 offset)]
|
||||
(if (fits-above-vbox-bottom? (:cy position) badge-height vbox)
|
||||
position
|
||||
;; Same rotation, opposite side of the line.
|
||||
(let [flipped (line-badge-position line-p1 line-p2 (- offset))]
|
||||
(assoc flipped :rot (:rot position)))))
|
||||
|
||||
has-rotation?
|
||||
(let [edge (get-edge-for-badge rotation)
|
||||
points (dm/get-prop single-shape :points)
|
||||
position (edge-badge-position points edge offset)
|
||||
position (if (fits-above-vbox-bottom? (:cy position) badge-height vbox)
|
||||
position
|
||||
(edge-badge-position points (opposite-edge edge) offset))]
|
||||
;; Rotation follows the original edge even if the position
|
||||
;; flips to the opposite one for lack of space, so the text
|
||||
;; stays upright.
|
||||
badge-rot (+ rotation (edge-rot-offset edge))
|
||||
(assoc position :rot (+ rotation (edge-rot-offset edge))))
|
||||
|
||||
position (edge-badge-position points edge offset)
|
||||
position (if (fits-above-vbox-bottom? (:cy position) badge-height vbox)
|
||||
position
|
||||
(edge-badge-position points (opposite-edge edge) offset))
|
||||
:else
|
||||
(let [below-cy (+ (:y selrect) (:height selrect) offset)]
|
||||
{:cx (+ (:x selrect) (/ (:width selrect) 2))
|
||||
:cy (if (fits-above-vbox-bottom? below-cy badge-height vbox)
|
||||
below-cy
|
||||
(- (:y selrect) offset))
|
||||
:rot 0}))
|
||||
(assoc :text text
|
||||
:width badge-width
|
||||
:height badge-height)))))
|
||||
|
||||
badge-x (- (/ badge-width 2))
|
||||
badge-y (- (/ badge-height 2))
|
||||
badge-cx (:cx position)
|
||||
badge-cy (:cy position)]
|
||||
(mf/defc selection-size-badge*
|
||||
[{:keys [zoom shapes vbox]}]
|
||||
(let [single-shape (and (= (count shapes) 1) (first shapes))
|
||||
component-color? (if single-shape
|
||||
(ctk/instance-head? single-shape)
|
||||
(every? ctk/instance-head? shapes))
|
||||
badge-bg-color (if component-color?
|
||||
selection-badge-bg-color-component
|
||||
selection-badge-bg-color)
|
||||
badge-radius (/ selection-badge-border-radius zoom)]
|
||||
|
||||
[:g.selection-size-badge {:pointer-events "none"
|
||||
:transform (dm/str "translate(" badge-cx "," badge-cy ") rotate(" badge-rot ")")}
|
||||
[:rect {:x badge-x
|
||||
:y badge-y
|
||||
:width badge-width
|
||||
:height badge-height
|
||||
:rx badge-radius
|
||||
:ry badge-radius
|
||||
:style {:fill badge-bg-color}}]
|
||||
[:text {:class (stl/css :badge-text)
|
||||
:x 0
|
||||
:y 0
|
||||
:text-anchor "middle"
|
||||
:dominant-baseline "middle"}
|
||||
text]])
|
||||
|
||||
(let [badge-x (- (/ badge-width 2))
|
||||
badge-y (- (/ badge-height 2))
|
||||
badge-cx (+ (:x selrect) (/ (:width selrect) 2))
|
||||
below-cy (+ (:y selrect) (:height selrect) badge-gap (/ badge-height 2))
|
||||
badge-cy (if (fits-above-vbox-bottom? below-cy badge-height vbox)
|
||||
below-cy
|
||||
(- (:y selrect) badge-gap (/ badge-height 2)))]
|
||||
|
||||
[:g.selection-size-badge {:pointer-events "none"
|
||||
:transform (dm/str "translate(" badge-cx "," badge-cy ")")}
|
||||
[:rect {:x badge-x
|
||||
:y badge-y
|
||||
:width badge-width
|
||||
:height badge-height
|
||||
:rx badge-radius
|
||||
:ry badge-radius
|
||||
:style {:fill badge-bg-color}}]
|
||||
[:text {:class (stl/css :badge-text)
|
||||
:x 0
|
||||
:y 0
|
||||
:text-anchor "middle"
|
||||
:dominant-baseline "middle"}
|
||||
text]])))))
|
||||
(when-let [{:keys [text width height cx cy rot]} (size-badge-layout shapes zoom vbox)]
|
||||
[:g.selection-size-badge {:pointer-events "none"
|
||||
:transform (dm/str "translate(" cx "," cy ") rotate(" rot ")")}
|
||||
[:rect {:x (- (/ width 2))
|
||||
:y (- (/ height 2))
|
||||
:width width
|
||||
:height height
|
||||
:rx badge-radius
|
||||
:ry badge-radius
|
||||
:style {:fill badge-bg-color}}]
|
||||
[:text {:class (stl/css :badge-text)
|
||||
:x 0
|
||||
:y 0
|
||||
:text-anchor "middle"
|
||||
:dominant-baseline "middle"}
|
||||
text]])))
|
||||
|
||||
(mf/defc distance-display* [{:keys [from to zoom bounds]}]
|
||||
(let [fixed-x (if (gsh/fully-contained? from to)
|
||||
|
||||
@ -112,6 +112,7 @@
|
||||
[frontend-tests.ui.history-test]
|
||||
[frontend-tests.ui.inspect-stroke-tokens-test]
|
||||
[frontend-tests.ui.layout-container-multiple-test]
|
||||
[frontend-tests.ui.measurements-test]
|
||||
[frontend-tests.ui.measures-menu-props-test]
|
||||
[frontend-tests.ui.organization-team-switch-test]
|
||||
[frontend-tests.ui.routes-test]
|
||||
@ -252,6 +253,7 @@
|
||||
'frontend-tests.ui.history-test
|
||||
'frontend-tests.ui.inspect-stroke-tokens-test
|
||||
'frontend-tests.ui.layout-container-multiple-test
|
||||
'frontend-tests.ui.measurements-test
|
||||
'frontend-tests.ui.measures-menu-props-test
|
||||
'frontend-tests.ui.organization-team-switch-test
|
||||
'frontend-tests.ui.routes-test
|
||||
|
||||
143
frontend/test/frontend_tests/ui/measurements_test.cljs
Normal file
143
frontend/test/frontend_tests/ui/measurements_test.cljs
Normal file
@ -0,0 +1,143 @@
|
||||
;; 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 frontend-tests.ui.measurements-test
|
||||
(:require
|
||||
[app.common.geom.rect :as grc]
|
||||
[app.common.math :as mth]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.main.ui.measurements :as msr]
|
||||
[cljs.test :as t :include-macros true]))
|
||||
|
||||
;; At zoom 1 the badge is 16 high, has 6 of horizontal padding, 6.5 per
|
||||
;; character, and sits 8 away from the shape: its center is 16 away.
|
||||
;; A flat path selrect has a minimum side of 0.01, which the measures
|
||||
;; inputs show too.
|
||||
|
||||
(def ^:private big-vbox {:x -1000 :y -1000 :width 5000 :height 5000})
|
||||
|
||||
(defn- rect
|
||||
[x y width height]
|
||||
(cts/setup-shape {:type :rect :x x :y y :width width :height height}))
|
||||
|
||||
(defn- polyline
|
||||
"A path through `points`, each a [x y] pair."
|
||||
[& points]
|
||||
(let [[[x0 y0] & others] points]
|
||||
(cts/setup-shape
|
||||
{:type :path
|
||||
:content (into [{:command :move-to :params {:x x0 :y y0}}]
|
||||
(map (fn [[x y]] {:command :line-to :params {:x x :y y}}))
|
||||
others)})))
|
||||
|
||||
(defn- close-layout?
|
||||
[expected layout]
|
||||
(and (some? layout)
|
||||
(every? (fn [[k v]]
|
||||
(let [actual (get layout k)]
|
||||
(if (number? v)
|
||||
(mth/close? v actual 0.001)
|
||||
(= v actual))))
|
||||
expected)))
|
||||
|
||||
;; --- Plain shapes
|
||||
|
||||
(t/deftest rect-badge-sits-centered-below-the-shape
|
||||
(let [layout (msr/size-badge-layout [(rect 0 0 100 50)] 1 big-vbox)]
|
||||
;; "100 x 50" has 8 characters: 8 * 6.5 + 2 * 6
|
||||
(t/is (close-layout? {:text "100 x 50" :width 64 :height 16
|
||||
:cx 50 :cy 66 :rot 0}
|
||||
layout))))
|
||||
|
||||
(t/deftest rect-narrower-than-the-badge-hides-it
|
||||
(t/is (nil? (msr/size-badge-layout [(rect 0 0 40 40)] 1 big-vbox))))
|
||||
|
||||
(t/deftest rect-lower-than-the-badge-hides-it
|
||||
(t/is (nil? (msr/size-badge-layout [(rect 0 0 200 10)] 1 big-vbox))))
|
||||
|
||||
(t/deftest badge-size-scales-with-the-zoom
|
||||
;; At zoom 2 the badge is 28.75 x 8, so a 40 x 40 rect fits it.
|
||||
(let [layout (msr/size-badge-layout [(rect 0 0 40 40)] 2 big-vbox)]
|
||||
(t/is (close-layout? {:text "40 x 40" :width 28.75 :height 8 :cy 48}
|
||||
layout))))
|
||||
|
||||
(t/deftest rect-badge-moves-above-at-the-vbox-bottom
|
||||
(let [vbox {:x 0 :y 0 :width 1000 :height 60}
|
||||
layout (msr/size-badge-layout [(rect 0 0 100 50)] 1 vbox)]
|
||||
(t/is (close-layout? {:cx 50 :cy -16 :rot 0} layout))))
|
||||
|
||||
(t/deftest multiple-shapes-show-the-bounding-box
|
||||
(let [layout (msr/size-badge-layout [(rect 0 0 100 50) (rect 200 100 100 50)]
|
||||
1 big-vbox)]
|
||||
(t/is (close-layout? {:text "300 x 150" :cx 150 :cy 166} layout))))
|
||||
|
||||
;; --- Paths
|
||||
|
||||
(t/deftest path-badge-sits-centered-below-the-path
|
||||
;; Three points, so not a straight line: it uses the box badge.
|
||||
(let [layout (msr/size-badge-layout [(polyline [0 0] [100 100] [200 0])]
|
||||
1 big-vbox)]
|
||||
(t/is (close-layout? {:text "200 x 100" :cx 100 :cy 116 :rot 0}
|
||||
layout))))
|
||||
|
||||
(t/deftest path-lower-than-the-badge-hides-it
|
||||
(t/is (nil? (msr/size-badge-layout [(polyline [0 0] [100 10] [200 0])]
|
||||
1 big-vbox)))
|
||||
;; A flat path, too: its height is the 0.01 minimum.
|
||||
(t/is (nil? (msr/size-badge-layout [(polyline [0 100] [100 100] [200 100])]
|
||||
1 big-vbox))))
|
||||
|
||||
(t/deftest path-narrower-than-the-badge-hides-it
|
||||
(t/is (nil? (msr/size-badge-layout [(polyline [0 0] [10 100] [20 0])]
|
||||
1 big-vbox))))
|
||||
|
||||
(t/deftest path-badge-reads-the-selrect-like-the-inputs
|
||||
;; The points enclose a larger box than the selrect, as they do on a
|
||||
;; rotated path. The text must match the measures inputs.
|
||||
(let [path (-> (polyline [0 0] [100 50] [200 0])
|
||||
(assoc :points (grc/rect->points (grc/make-rect 0 0 300 300))))
|
||||
layout (msr/size-badge-layout [path] 1 big-vbox)]
|
||||
(t/is (= "200 x 50" (:text layout)))))
|
||||
|
||||
;; --- Straight two-point lines
|
||||
|
||||
(t/deftest horizontal-line-badge-runs-below-the-line
|
||||
(let [layout (msr/size-badge-layout [(polyline [0 50] [200 50])] 1 big-vbox)]
|
||||
(t/is (close-layout? {:text "200" :cx 100 :cy 66 :rot 0} layout))))
|
||||
|
||||
(t/deftest line-drawn-right-to-left-keeps-the-text-upright
|
||||
(let [layout (msr/size-badge-layout [(polyline [200 50] [0 50])] 1 big-vbox)]
|
||||
(t/is (close-layout? {:cx 100 :cy 66 :rot 0} layout))))
|
||||
|
||||
(t/deftest vertical-line-badge-runs-along-the-line
|
||||
(let [down (msr/size-badge-layout [(polyline [0 0] [0 200])] 1 big-vbox)
|
||||
up (msr/size-badge-layout [(polyline [0 200] [0 0])] 1 big-vbox)]
|
||||
(t/is (close-layout? {:text "200" :cx -16 :cy 100 :rot 90} down))
|
||||
(t/is (close-layout? {:cx -16 :cy 100 :rot 90} up))))
|
||||
|
||||
(t/deftest diagonal-line-badge-follows-the-line-angle
|
||||
(let [layout (msr/size-badge-layout [(polyline [0 0] [100 100])] 1 big-vbox)
|
||||
offset (/ 16 (mth/sqrt 2))]
|
||||
(t/is (close-layout? {:text "141.42" :cx (- 50 offset) :cy (+ 50 offset) :rot 45}
|
||||
layout))))
|
||||
|
||||
(t/deftest line-shorter-than-the-badge-hides-it
|
||||
;; "20" needs a 25 wide badge.
|
||||
(t/is (nil? (msr/size-badge-layout [(polyline [0 0] [20 0])] 1 big-vbox))))
|
||||
|
||||
(t/deftest line-badge-shows-the-length-with-two-decimals-at-most
|
||||
(t/is (= "70.71" (:text (msr/size-badge-layout [(polyline [0 0] [50 50])] 1 big-vbox))))
|
||||
(t/is (= "50" (:text (msr/size-badge-layout [(polyline [0 0] [30 40])] 1 big-vbox)))))
|
||||
|
||||
(t/deftest diagonal-line-uses-its-length-to-fit-the-badge
|
||||
;; Both sides (33) are narrower than the badge for "46.67" (44.5), but
|
||||
;; the line is about 46.67 long, so the badge fits along it.
|
||||
(t/is (some? (msr/size-badge-layout [(polyline [0 0] [33 33])] 1 big-vbox))))
|
||||
|
||||
(t/deftest line-badge-moves-to-the-other-side-at-the-vbox-bottom
|
||||
(let [vbox {:x 0 :y 0 :width 1000 :height 60}
|
||||
layout (msr/size-badge-layout [(polyline [0 50] [200 50])] 1 vbox)]
|
||||
(t/is (close-layout? {:cx 100 :cy 34 :rot 0} layout))))
|
||||
@ -1,5 +1,9 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 1.2.0
|
||||
|
||||
- Update deps and build it with penpot 2.18.3
|
||||
|
||||
|
||||
## 1.2.0-RC1
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "@penpot/library",
|
||||
"version": "1.2.0-RC1",
|
||||
"version": "1.2.0",
|
||||
"license": "MPL-2.0",
|
||||
"author": "Kaleidos INC Sucursal en España SL",
|
||||
"author": "KALEIDOS SUBSIDIARY SL",
|
||||
"packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user