From e4a1c373bb4af228f0598541cc63e9750a3ee341 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Wed, 21 May 2025 14:17:37 +0200 Subject: [PATCH 1/7] :sparkles: Only take N amount of fills --- common/src/app/common/types/shape.cljc | 5 ++- frontend/src/app/render_wasm/api.cljs | 42 ++++++++----------- .../app/render_wasm/serializers/fills.cljs | 22 +++++++++- 3 files changed, 41 insertions(+), 28 deletions(-) diff --git a/common/src/app/common/types/shape.cljc b/common/src/app/common/types/shape.cljc index 4f8f1f313b..e46b009cf0 100644 --- a/common/src/app/common/types/shape.cljc +++ b/common/src/app/common/types/shape.cljc @@ -762,4 +762,7 @@ (cond-> (cfh/text-shape? shape) (patch-text-props props)) (cond-> (cfh/frame-shape? shape) (patch-layout-props props))))) -(def MAX-GRADIENT-STOPS 16) \ No newline at end of file +;; FIXME: Get these from the wasm module, and tweak the values +;; (we'd probably want 12 stops at most) +(def MAX-GRADIENT-STOPS 16) +(def MAX-FILLS 8) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 8a28f19130..6e1a1fa7ae 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -13,6 +13,7 @@ [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.types.path :as path] + [app.common.types.shape :as shp] [app.common.types.shape.layout :as ctl] [app.common.uuid :as uuid] [app.config :as cf] @@ -241,33 +242,24 @@ [fills] (h/call wasm/internal-module "_clear_shape_fills") (keep (fn [fill] - (let [opacity (or (:fill-opacity fill) 1.0) - color (:fill-color fill) - gradient (:fill-color-gradient fill) - image (:fill-image fill) - offset (mem/alloc-bytes sr-fills/FILL-BYTE-SIZE) + (let [offset (mem/alloc-bytes sr-fills/FILL-BYTE-SIZE) heap (mem/get-heap-u8) - dview (js/DataView. (.-buffer heap))] - (cond - (some? color) - (do - (sr-fills/write-solid-fill! offset dview (sr-clr/hex->u32argb color opacity)) - (h/call wasm/internal-module "_add_shape_fill")) - - (some? gradient) - (do - (sr-fills/write-gradient-fill! offset dview gradient opacity) - (h/call wasm/internal-module "_add_shape_fill")) - - (some? image) - (let [id (dm/get-prop image :id) - buffer (uuid/get-u32 id) - cached-image? (h/call wasm/internal-module "_is_image_cached" (aget buffer 0) (aget buffer 1) (aget buffer 2) (aget buffer 3))] - (sr-fills/write-image-fill! offset dview id opacity (dm/get-prop image :width) (dm/get-prop image :height)) - (h/call wasm/internal-module "_add_shape_fill") - (when (== cached-image? 0) + dview (js/DataView. (.-buffer heap)) + image (:fill-image fill)] + (sr-fills/write-fill! offset dview fill) + (h/call wasm/internal-module "_add_shape_fill") + ;; store image for image fills if not cached + (when (some? image) + (let [id (dm/get-prop image :id) + buffer (uuid/get-u32 id) + cached-image? (h/call wasm/internal-module "_is_image_cached" + (aget buffer 0) + (aget buffer 1) + (aget buffer 2) + (aget buffer 3))] + (when (zero? cached-image?) (store-image id)))))) - fills)) + (take shp/MAX-FILLS fills))) (defn set-shape-strokes [strokes] diff --git a/frontend/src/app/render_wasm/serializers/fills.cljs b/frontend/src/app/render_wasm/serializers/fills.cljs index 4d8257e626..404e29e22f 100644 --- a/frontend/src/app/render_wasm/serializers/fills.cljs +++ b/frontend/src/app/render_wasm/serializers/fills.cljs @@ -1,12 +1,12 @@ (ns app.render-wasm.serializers.fills (:require + [app.common.data.macros :as dm] [app.common.types.shape :as shp] [app.common.uuid :as uuid] [app.render-wasm.serializers.color :as clr])) (def ^:private GRADIENT-STOP-SIZE 8) - (def GRADIENT-BYTE-SIZE 156) (def SOLID-BYTE-SIZE 4) (def IMAGE-BYTE-SIZE 28) @@ -14,6 +14,7 @@ ;; FIXME: get it from the wasm module (def FILL-BYTE-SIZE (+ 4 (max GRADIENT-BYTE-SIZE IMAGE-BYTE-SIZE SOLID-BYTE-SIZE))) + (defn write-solid-fill! [offset dview argb] (.setUint8 dview offset 0x00 true) @@ -60,4 +61,21 @@ stop-offset (:offset stop)] (.setUint32 dview loop-offset argb true) (.setFloat32 dview (+ loop-offset 4) stop-offset true) - (recur (rest stops) (+ loop-offset GRADIENT-STOP-SIZE))))))) \ No newline at end of file + (recur (rest stops) (+ loop-offset GRADIENT-STOP-SIZE))))))) + +(defn write-fill! + [offset dview fill] + (let [opacity (or (:fill-opacity fill) 1.0) + color (:fill-color fill) + gradient (:fill-color-gradient fill) + image (:fill-image fill)] + (cond + (some? color) + (write-solid-fill! offset dview (clr/hex->u32argb color opacity)) + + (some? gradient) + (write-gradient-fill! offset dview gradient opacity) + + (some? image) + (let [id (dm/get-prop image :id)] + (write-image-fill! offset dview id opacity (dm/get-prop image :width) (dm/get-prop image :height)))))) \ No newline at end of file From 827d39a40665108d5150e50c69a9af729bbfa02b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Fri, 23 May 2025 14:52:36 +0200 Subject: [PATCH 2/7] :lipstick: Remove ununsed prop on fill-menu component --- .../src/app/main/ui/workspace/sidebar/options/menus/fill.cljs | 4 ++-- frontend/translations/en.po | 2 +- frontend/translations/es.po | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs index 25fe82178f..2026fa07d2 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs @@ -44,7 +44,7 @@ (mf/defc fill-menu {::mf/wrap [#(mf/memo' % (mf/check-props ["ids" "values"]))]} - [{:keys [ids type values disable-remove?] :as props}] + [{:keys [ids type values] :as props}] (let [label (case type :multiple (tr "workspace.options.selection-fill") :group (tr "workspace.options.group-fill") @@ -146,7 +146,7 @@ :title label :class (stl/css-case :title-spacing-fill (not has-fills?))} - (when (and (not disable-remove?) (not (= :multiple fills))) + (when (not (= :multiple fills)) [:> icon-button* {:variant "ghost" :aria-label (tr "workspace.options.fill.add-fill") :on-click on-add diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 7e4700bf32..39535f9a65 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -5608,7 +5608,7 @@ msgstr "Fill" #: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs:151 msgid "workspace.options.fill.add-fill" -msgstr "Add fill color" +msgstr "Add fill" #: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs:164 msgid "workspace.options.fill.remove-fill" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index b959980c77..b7c203e98e 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -5642,7 +5642,7 @@ msgstr "Relleno" #: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs:151 msgid "workspace.options.fill.add-fill" -msgstr "Añadir color de relleno" +msgstr "Añadir relleno" #: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs:164 msgid "workspace.options.fill.remove-fill" From 170d35dde2ada5ee7db0396e9d24e8ae5d500650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Fri, 23 May 2025 15:34:10 +0200 Subject: [PATCH 3/7] :sparkles: Disable new fills in UI when the cap is reached --- .../get-file-fragment-fills-limit.json | 215 ++++++++++++++++++ .../playwright/ui/specs/design-tab.spec.js | 24 +- .../src/app/main/data/workspace/colors.cljs | 1 - frontend/src/app/main/ui/shapes/attrs.cljs | 2 + .../workspace/sidebar/options/menus/fill.cljs | 16 +- 5 files changed, 247 insertions(+), 11 deletions(-) create mode 100644 frontend/playwright/data/workspace/get-file-fragment-fills-limit.json diff --git a/frontend/playwright/data/workspace/get-file-fragment-fills-limit.json b/frontend/playwright/data/workspace/get-file-fragment-fills-limit.json new file mode 100644 index 0000000000..dc8f26c378 --- /dev/null +++ b/frontend/playwright/data/workspace/get-file-fragment-fills-limit.json @@ -0,0 +1,215 @@ +{ + "~:id": "~ub3e5731a-c295-801d-8006-3fc3517249cf", + "~:file-id": "~ub3e5731a-c295-801d-8006-3fc33c3b1b13", + "~:created-at": "~m1748353475008", + "~:data": { + "~:objects": { + "~u00000000-0000-0000-0000-000000000000": { + "~#shape": { + "~:y": 0, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:name": "Root Frame", + "~:width": 0.01, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 0.0, + "~:y": 0.0 + } + }, + { + "~#point": { + "~:x": 0.01, + "~:y": 0.0 + } + }, + { + "~#point": { + "~:x": 0.01, + "~:y": 0.01 + } + }, + { + "~#point": { + "~:x": 0.0, + "~:y": 0.01 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:r3": 0, + "~:r1": 0, + "~:id": "~u00000000-0000-0000-0000-000000000000", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 0, + "~:proportion": 1.0, + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 0, + "~:y": 0, + "~:width": 0.01, + "~:height": 0.01, + "~:x1": 0, + "~:y1": 0, + "~:x2": 0.01, + "~:y2": 0.01 + } + }, + "~:fills": [ + { + "~:fill-color": "#FFFFFF", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 0.01, + "~:flip-y": null, + "~:shapes": [ + "~u893b7b23-26fe-80dd-8006-3fc33f3061f8" + ] + } + }, + "~u893b7b23-26fe-80dd-8006-3fc33f3061f8": { + "~#shape": { + "~:y": 307, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": false, + "~:name": "Rectangle", + "~:width": 141, + "~:type": "~:rect", + "~:points": [ + { + "~#point": { + "~:x": 673, + "~:y": 307 + } + }, + { + "~#point": { + "~:x": 814, + "~:y": 307 + } + }, + { + "~#point": { + "~:x": 814, + "~:y": 455 + } + }, + { + "~#point": { + "~:x": 673, + "~:y": 455 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:r3": 0, + "~:r1": 0, + "~:id": "~u893b7b23-26fe-80dd-8006-3fc33f3061f8", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 673, + "~:proportion": 1, + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 673, + "~:y": 307, + "~:width": 141, + "~:height": 148, + "~:x1": 673, + "~:y1": 307, + "~:x2": 814, + "~:y2": 455 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + }, + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + }, + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + }, + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + }, + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + }, + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + }, + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 148, + "~:flip-y": null + } + } + }, + "~:id": "~ub3e5731a-c295-801d-8006-3fc33c3b1b14", + "~:name": "Page 1" + } +} \ No newline at end of file diff --git a/frontend/playwright/ui/specs/design-tab.spec.js b/frontend/playwright/ui/specs/design-tab.spec.js index ea73328de7..30f2d3a3f0 100644 --- a/frontend/playwright/ui/specs/design-tab.spec.js +++ b/frontend/playwright/ui/specs/design-tab.spec.js @@ -1,4 +1,4 @@ -import { test, expect } from "@playwright/test"; +import { test, expect, describe } from "@playwright/test"; import { WorkspacePage } from "../pages/WorkspacePage"; test.beforeEach(async ({ page }) => { @@ -38,7 +38,7 @@ const setupFileWithMultipeAttributes = async (workspace) => { ); }; -test.describe("Constraints", () => { +describe("Constraints", () => { test("Constraint dropdown shows 'Mixed' when multiple layers are selected with different constraints", async ({ page, }) => { @@ -66,7 +66,25 @@ test.describe("Constraints", () => { }); }); -test.describe("Multiple shapes attributes", () => { +describe("Shape attributes", () => { + test("Cannot add a new fill when the limit has been reached", async ({ + page, + }) => { + const workspace = new WorkspacePage(page); + await workspace.setupEmptyFile(); + + await workspace.goToWorkspace({ + fileId: "b3e5731a-c295-801d-8006-3fc33c3b1b13", + pageId: "b3e5731a-c295-801d-8006-3fc33c3b1b14", + }); + + await workspace.clickLeafLayer("Rectangle"); + + expect(false); + }); +}); + +describe("Multiple shapes attributes", () => { test("User selects multiple shapes with sames fills, strokes, shadows and blur", async ({ page, }) => { diff --git a/frontend/src/app/main/data/workspace/colors.cljs b/frontend/src/app/main/data/workspace/colors.cljs index 5b7a0ea0f5..4b624188b0 100644 --- a/frontend/src/app/main/data/workspace/colors.cljs +++ b/frontend/src/app/main/data/workspace/colors.cljs @@ -222,7 +222,6 @@ (update :fills #(into [attrs] %)))) undo-id (js/Symbol)] - (rx/concat (rx/of (dwu/start-undo-transaction undo-id)) (transform-fill state ids color change-fn options) diff --git a/frontend/src/app/main/ui/shapes/attrs.cljs b/frontend/src/app/main/ui/shapes/attrs.cljs index dfb6094843..70a7709e84 100644 --- a/frontend/src/app/main/ui/shapes/attrs.cljs +++ b/frontend/src/app/main/ui/shapes/attrs.cljs @@ -66,6 +66,8 @@ ([attrs fill-data render-id index type] (add-fill! attrs fill-data render-id index type "none")) ([attrs fill-data render-id index type fill-default] + ;; TODO: check for MAX-FILLS + (js/console.log "add_fill!" (clj->js fill-data)) (let [index (if (some? index) (dm/str "-" index) "")] (cond (contains? fill-data :fill-image) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs index 2026fa07d2..01d0dbf4e4 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs @@ -10,6 +10,7 @@ [app.common.colors :as clr] [app.common.data :as d] [app.common.types.color :as ctc] + [app.common.types.shape :as shp] [app.common.types.shape.attrs :refer [default-color]] [app.main.data.workspace.colors :as dc] [app.main.store :as st] @@ -54,7 +55,7 @@ values (d/without-nils values) fills (:fills values) has-fills? (or (= :multiple fills) (some? (seq fills))) - + can-add-fills? (and (not (= :multiple fills)) (< (count fills) shp/MAX-FILLS)) state* (mf/use-state has-fills?) open? (deref state*) @@ -73,12 +74,12 @@ (mf/use-fn (mf/deps ids fills) (fn [_] - (st/emit! (dc/add-fill ids {:color default-color - :opacity 1})) - - (when (or (= :multiple fills) - (not (some? (seq fills)))) - (open-content)))) + (when can-add-fills? + (st/emit! (dc/add-fill ids {:color default-color + :opacity 1})) + (when (or (= :multiple fills) + (not (some? (seq fills)))) + (open-content))))) on-change (fn [index] @@ -151,6 +152,7 @@ :aria-label (tr "workspace.options.fill.add-fill") :on-click on-add :data-testid "add-fill" + :disabled (not can-add-fills?) :icon "add"}])]] (when open? From 7d5739b663cf05fbb377bc62a6c45205ec30ee7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Tue, 27 May 2025 17:49:24 +0200 Subject: [PATCH 4/7] :sparkles: Add playwright test for disabling adding fills --- .../data/design/get-file-fills-limit.json | 318 ++++++++++++++++++ .../get-file-fragment-fills-limit.json | 215 ------------ .../playwright/ui/specs/design-tab.spec.js | 20 +- frontend/src/app/main/ui/shapes/attrs.cljs | 2 - 4 files changed, 331 insertions(+), 224 deletions(-) create mode 100644 frontend/playwright/data/design/get-file-fills-limit.json delete mode 100644 frontend/playwright/data/workspace/get-file-fragment-fills-limit.json diff --git a/frontend/playwright/data/design/get-file-fills-limit.json b/frontend/playwright/data/design/get-file-fills-limit.json new file mode 100644 index 0000000000..4e50969722 --- /dev/null +++ b/frontend/playwright/data/design/get-file-fills-limit.json @@ -0,0 +1,318 @@ +{ + "~:features": { + "~#set": [ + "fdata/path-data", + "plugins/runtime", + "design-tokens/v1", + "layout/grid", + "styles/v2", + "fdata/pointer-map", + "fdata/objects-map", + "components/v2", + "fdata/shape-data-type" + ] + }, + "~:team-id": "~ua78f45d1-6166-80e4-8006-37f8ef82b13c", + "~:permissions": { + "~:type": "~:membership", + "~:is-owner": true, + "~:is-admin": true, + "~:can-edit": true, + "~:can-read": true, + "~:is-logged": true + }, + "~:has-media-trimmed": false, + "~:comment-thread-seqn": 0, + "~:name": "Cap fills", + "~:revn": 2, + "~:modified-at": "~m1748504366715", + "~:vern": 0, + "~:id": "~ud2847136-a651-80ac-8006-4202d9214aa7", + "~:is-shared": false, + "~:migrations": { + "~#ordered-set": [ + "legacy-2", + "legacy-3", + "legacy-5", + "legacy-6", + "legacy-7", + "legacy-8", + "legacy-9", + "legacy-10", + "legacy-11", + "legacy-12", + "legacy-13", + "legacy-14", + "legacy-16", + "legacy-17", + "legacy-18", + "legacy-19", + "legacy-25", + "legacy-26", + "legacy-27", + "legacy-28", + "legacy-29", + "legacy-31", + "legacy-32", + "legacy-33", + "legacy-34", + "legacy-36", + "legacy-37", + "legacy-38", + "legacy-39", + "legacy-40", + "legacy-41", + "legacy-42", + "legacy-43", + "legacy-44", + "legacy-45", + "legacy-46", + "legacy-47", + "legacy-48", + "legacy-49", + "legacy-50", + "legacy-51", + "legacy-52", + "legacy-53", + "legacy-54", + "legacy-55", + "legacy-56", + "legacy-57", + "legacy-59", + "legacy-62", + "legacy-65", + "legacy-66", + "legacy-67", + "0001-remove-tokens-from-groups", + "0002-normalize-bool-content", + "0002-clean-shape-interactions", + "0003-fix-root-shape", + "0003-convert-path-content" + ] + }, + "~:version": 67, + "~:project-id": "~ua78f45d1-6166-80e4-8006-37f8ef83114f", + "~:created-at": "~m1748504346802", + "~:data": { + "~:pages": [ + "~ud2847136-a651-80ac-8006-4202d9214aa8" + ], + "~:pages-index": { + "~ud2847136-a651-80ac-8006-4202d9214aa8": { + "~:objects": { + "~u00000000-0000-0000-0000-000000000000": { + "~#shape": { + "~:y": 0, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:name": "Root Frame", + "~:width": 0.01, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 0.0, + "~:y": 0.0 + } + }, + { + "~#point": { + "~:x": 0.01, + "~:y": 0.0 + } + }, + { + "~#point": { + "~:x": 0.01, + "~:y": 0.01 + } + }, + { + "~#point": { + "~:x": 0.0, + "~:y": 0.01 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:r3": 0, + "~:r1": 0, + "~:id": "~u00000000-0000-0000-0000-000000000000", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 0, + "~:proportion": 1.0, + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 0, + "~:y": 0, + "~:width": 0.01, + "~:height": 0.01, + "~:x1": 0, + "~:y1": 0, + "~:x2": 0.01, + "~:y2": 0.01 + } + }, + "~:fills": [ + { + "~:fill-color": "#FFFFFF", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 0.01, + "~:flip-y": null, + "~:shapes": [ + "~u0ade1723-2e87-80b6-8006-4202db189b25" + ] + } + }, + "~u0ade1723-2e87-80b6-8006-4202db189b25": { + "~#shape": { + "~:y": 406, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": false, + "~:name": "Rectangle", + "~:width": 170, + "~:type": "~:rect", + "~:points": [ + { + "~#point": { + "~:x": 716, + "~:y": 406 + } + }, + { + "~#point": { + "~:x": 886, + "~:y": 406 + } + }, + { + "~#point": { + "~:x": 886, + "~:y": 528 + } + }, + { + "~#point": { + "~:x": 716, + "~:y": 528 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:r3": 0, + "~:r1": 0, + "~:id": "~u0ade1723-2e87-80b6-8006-4202db189b25", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 716, + "~:proportion": 1, + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 716, + "~:y": 406, + "~:width": 170, + "~:height": 122, + "~:x1": 716, + "~:y1": 406, + "~:x2": 886, + "~:y2": 528 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + }, + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + }, + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + }, + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + }, + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + }, + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + }, + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 122, + "~:flip-y": null + } + } + }, + "~:id": "~ud2847136-a651-80ac-8006-4202d9214aa8", + "~:name": "Page 1" + } + }, + "~:id": "~ud2847136-a651-80ac-8006-4202d9214aa7", + "~:options": { + "~:components-v2": true, + "~:base-font-size": "16px" + } + } +} \ No newline at end of file diff --git a/frontend/playwright/data/workspace/get-file-fragment-fills-limit.json b/frontend/playwright/data/workspace/get-file-fragment-fills-limit.json deleted file mode 100644 index dc8f26c378..0000000000 --- a/frontend/playwright/data/workspace/get-file-fragment-fills-limit.json +++ /dev/null @@ -1,215 +0,0 @@ -{ - "~:id": "~ub3e5731a-c295-801d-8006-3fc3517249cf", - "~:file-id": "~ub3e5731a-c295-801d-8006-3fc33c3b1b13", - "~:created-at": "~m1748353475008", - "~:data": { - "~:objects": { - "~u00000000-0000-0000-0000-000000000000": { - "~#shape": { - "~:y": 0, - "~:hide-fill-on-export": false, - "~:transform": { - "~#matrix": { - "~:a": 1.0, - "~:b": 0.0, - "~:c": 0.0, - "~:d": 1.0, - "~:e": 0.0, - "~:f": 0.0 - } - }, - "~:rotation": 0, - "~:name": "Root Frame", - "~:width": 0.01, - "~:type": "~:frame", - "~:points": [ - { - "~#point": { - "~:x": 0.0, - "~:y": 0.0 - } - }, - { - "~#point": { - "~:x": 0.01, - "~:y": 0.0 - } - }, - { - "~#point": { - "~:x": 0.01, - "~:y": 0.01 - } - }, - { - "~#point": { - "~:x": 0.0, - "~:y": 0.01 - } - } - ], - "~:r2": 0, - "~:proportion-lock": false, - "~:transform-inverse": { - "~#matrix": { - "~:a": 1.0, - "~:b": 0.0, - "~:c": 0.0, - "~:d": 1.0, - "~:e": 0.0, - "~:f": 0.0 - } - }, - "~:r3": 0, - "~:r1": 0, - "~:id": "~u00000000-0000-0000-0000-000000000000", - "~:parent-id": "~u00000000-0000-0000-0000-000000000000", - "~:frame-id": "~u00000000-0000-0000-0000-000000000000", - "~:strokes": [], - "~:x": 0, - "~:proportion": 1.0, - "~:r4": 0, - "~:selrect": { - "~#rect": { - "~:x": 0, - "~:y": 0, - "~:width": 0.01, - "~:height": 0.01, - "~:x1": 0, - "~:y1": 0, - "~:x2": 0.01, - "~:y2": 0.01 - } - }, - "~:fills": [ - { - "~:fill-color": "#FFFFFF", - "~:fill-opacity": 1 - } - ], - "~:flip-x": null, - "~:height": 0.01, - "~:flip-y": null, - "~:shapes": [ - "~u893b7b23-26fe-80dd-8006-3fc33f3061f8" - ] - } - }, - "~u893b7b23-26fe-80dd-8006-3fc33f3061f8": { - "~#shape": { - "~:y": 307, - "~:transform": { - "~#matrix": { - "~:a": 1.0, - "~:b": 0.0, - "~:c": 0.0, - "~:d": 1.0, - "~:e": 0.0, - "~:f": 0.0 - } - }, - "~:rotation": 0, - "~:grow-type": "~:fixed", - "~:hide-in-viewer": false, - "~:name": "Rectangle", - "~:width": 141, - "~:type": "~:rect", - "~:points": [ - { - "~#point": { - "~:x": 673, - "~:y": 307 - } - }, - { - "~#point": { - "~:x": 814, - "~:y": 307 - } - }, - { - "~#point": { - "~:x": 814, - "~:y": 455 - } - }, - { - "~#point": { - "~:x": 673, - "~:y": 455 - } - } - ], - "~:r2": 0, - "~:proportion-lock": false, - "~:transform-inverse": { - "~#matrix": { - "~:a": 1.0, - "~:b": 0.0, - "~:c": 0.0, - "~:d": 1.0, - "~:e": 0.0, - "~:f": 0.0 - } - }, - "~:r3": 0, - "~:r1": 0, - "~:id": "~u893b7b23-26fe-80dd-8006-3fc33f3061f8", - "~:parent-id": "~u00000000-0000-0000-0000-000000000000", - "~:frame-id": "~u00000000-0000-0000-0000-000000000000", - "~:strokes": [], - "~:x": 673, - "~:proportion": 1, - "~:r4": 0, - "~:selrect": { - "~#rect": { - "~:x": 673, - "~:y": 307, - "~:width": 141, - "~:height": 148, - "~:x1": 673, - "~:y1": 307, - "~:x2": 814, - "~:y2": 455 - } - }, - "~:fills": [ - { - "~:fill-color": "#B1B2B5", - "~:fill-opacity": 1 - }, - { - "~:fill-color": "#B1B2B5", - "~:fill-opacity": 1 - }, - { - "~:fill-color": "#B1B2B5", - "~:fill-opacity": 1 - }, - { - "~:fill-color": "#B1B2B5", - "~:fill-opacity": 1 - }, - { - "~:fill-color": "#B1B2B5", - "~:fill-opacity": 1 - }, - { - "~:fill-color": "#B1B2B5", - "~:fill-opacity": 1 - }, - { - "~:fill-color": "#B1B2B5", - "~:fill-opacity": 1 - } - ], - "~:flip-x": null, - "~:height": 148, - "~:flip-y": null - } - } - }, - "~:id": "~ub3e5731a-c295-801d-8006-3fc33c3b1b14", - "~:name": "Page 1" - } -} \ No newline at end of file diff --git a/frontend/playwright/ui/specs/design-tab.spec.js b/frontend/playwright/ui/specs/design-tab.spec.js index 30f2d3a3f0..0df50e6ad1 100644 --- a/frontend/playwright/ui/specs/design-tab.spec.js +++ b/frontend/playwright/ui/specs/design-tab.spec.js @@ -1,4 +1,4 @@ -import { test, expect, describe } from "@playwright/test"; +import { test, expect } from "@playwright/test"; import { WorkspacePage } from "../pages/WorkspacePage"; test.beforeEach(async ({ page }) => { @@ -38,7 +38,7 @@ const setupFileWithMultipeAttributes = async (workspace) => { ); }; -describe("Constraints", () => { +test.describe("Constraints", () => { test("Constraint dropdown shows 'Mixed' when multiple layers are selected with different constraints", async ({ page, }) => { @@ -66,25 +66,31 @@ describe("Constraints", () => { }); }); -describe("Shape attributes", () => { +test.describe("Shape attributes", () => { test("Cannot add a new fill when the limit has been reached", async ({ page, }) => { const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); + await workspace.mockRPC(/get\-file\?/, "design/get-file-fills-limit.json"); await workspace.goToWorkspace({ - fileId: "b3e5731a-c295-801d-8006-3fc33c3b1b13", - pageId: "b3e5731a-c295-801d-8006-3fc33c3b1b14", + fileId: "d2847136-a651-80ac-8006-4202d9214aa7", + pageId: "d2847136-a651-80ac-8006-4202d9214aa8", }); await workspace.clickLeafLayer("Rectangle"); - expect(false); + await workspace.page.getByTestId("add-fill").click(); + await expect( + workspace.page.getByRole("button", { name: "#B1B2B5" }), + ).toHaveCount(8); + + await expect(workspace.page.getByTestId("add-fill")).toBeDisabled(); }); }); -describe("Multiple shapes attributes", () => { +test.describe("Multiple shapes attributes", () => { test("User selects multiple shapes with sames fills, strokes, shadows and blur", async ({ page, }) => { diff --git a/frontend/src/app/main/ui/shapes/attrs.cljs b/frontend/src/app/main/ui/shapes/attrs.cljs index 70a7709e84..dfb6094843 100644 --- a/frontend/src/app/main/ui/shapes/attrs.cljs +++ b/frontend/src/app/main/ui/shapes/attrs.cljs @@ -66,8 +66,6 @@ ([attrs fill-data render-id index type] (add-fill! attrs fill-data render-id index type "none")) ([attrs fill-data render-id index type fill-default] - ;; TODO: check for MAX-FILLS - (js/console.log "add_fill!" (clj->js fill-data)) (let [index (if (some? index) (dm/str "-" index) "")] (cond (contains? fill-data :fill-image) From c0a98288d07062349715321adbe0c70045c3d493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Wed, 28 May 2025 14:07:36 +0200 Subject: [PATCH 5/7] :wrench: Gate cap with config flag --- frontend/playwright/ui/specs/design-tab.spec.js | 1 + .../src/app/main/ui/workspace/sidebar/options/menus/fill.cljs | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/playwright/ui/specs/design-tab.spec.js b/frontend/playwright/ui/specs/design-tab.spec.js index 0df50e6ad1..2fc36fdef4 100644 --- a/frontend/playwright/ui/specs/design-tab.spec.js +++ b/frontend/playwright/ui/specs/design-tab.spec.js @@ -71,6 +71,7 @@ test.describe("Shape attributes", () => { page, }) => { const workspace = new WorkspacePage(page); + await workspace.mockConfigFlags(["enable-binary-fills"]); await workspace.setupEmptyFile(); await workspace.mockRPC(/get\-file\?/, "design/get-file-fills-limit.json"); diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs index 01d0dbf4e4..b51dcfbba7 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs @@ -12,6 +12,7 @@ [app.common.types.color :as ctc] [app.common.types.shape :as shp] [app.common.types.shape.attrs :refer [default-color]] + [app.config :as cfg] [app.main.data.workspace.colors :as dc] [app.main.store :as st] [app.main.ui.components.title-bar :refer [title-bar]] @@ -55,7 +56,7 @@ values (d/without-nils values) fills (:fills values) has-fills? (or (= :multiple fills) (some? (seq fills))) - can-add-fills? (and (not (= :multiple fills)) (< (count fills) shp/MAX-FILLS)) + can-add-fills? (and (contains? cfg/flags :binary-fills) (not (= :multiple fills)) (< (count fills) shp/MAX-FILLS)) state* (mf/use-state has-fills?) open? (deref state*) From f33c1fb53034df6db072e88ee895a7f14cf8b485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Thu, 29 May 2025 09:50:12 +0200 Subject: [PATCH 6/7] :sparkles: Update binary fills flag name and add it to supported flags --- common/src/app/common/flags.cljc | 3 ++- frontend/playwright/ui/specs/colorpicker.spec.js | 2 +- frontend/playwright/ui/specs/design-tab.spec.js | 2 +- frontend/src/app/main/data/workspace/colors.cljs | 6 +++--- frontend/src/app/main/ui/workspace/colorpicker.cljs | 2 +- .../src/app/main/ui/workspace/colorpicker/gradients.cljs | 2 +- .../app/main/ui/workspace/sidebar/options/menus/fill.cljs | 2 +- frontend/src/app/main/ui/workspace/viewport/gradients.cljs | 4 ++-- 8 files changed, 12 insertions(+), 11 deletions(-) diff --git a/common/src/app/common/flags.cljc b/common/src/app/common/flags.cljc index ae2742277e..804cac400b 100644 --- a/common/src/app/common/flags.cljc +++ b/common/src/app/common/flags.cljc @@ -127,7 +127,8 @@ :render-wasm-dpr :hide-release-modal :subscriptions - :subscriptions-old}) + :subscriptions-old + :frontend-binary-fills}) (def all-flags (set/union email login varia)) diff --git a/frontend/playwright/ui/specs/colorpicker.spec.js b/frontend/playwright/ui/specs/colorpicker.spec.js index 231c4f9d73..e555eed3fb 100644 --- a/frontend/playwright/ui/specs/colorpicker.spec.js +++ b/frontend/playwright/ui/specs/colorpicker.spec.js @@ -153,7 +153,7 @@ test("Create a RADIAL gradient", async ({ page }) => { test("Gradient stops limit", async ({ page }) => { const workspacePage = new WorkspacePage(page); - await workspacePage.mockConfigFlags(["enable-binary-fills"]); + await workspacePage.mockConfigFlags(["enable-frontend-binary-fills"]); await workspacePage.setupEmptyFile(page); await workspacePage.mockRPC( "get-file-fragment?file-id=*&fragment-id=*", diff --git a/frontend/playwright/ui/specs/design-tab.spec.js b/frontend/playwright/ui/specs/design-tab.spec.js index 2fc36fdef4..97e1f8cbb2 100644 --- a/frontend/playwright/ui/specs/design-tab.spec.js +++ b/frontend/playwright/ui/specs/design-tab.spec.js @@ -71,7 +71,7 @@ test.describe("Shape attributes", () => { page, }) => { const workspace = new WorkspacePage(page); - await workspace.mockConfigFlags(["enable-binary-fills"]); + await workspace.mockConfigFlags(["enable-frontend-binary-fills"]); await workspace.setupEmptyFile(); await workspace.mockRPC(/get\-file\?/, "design/get-file-fills-limit.json"); diff --git a/frontend/src/app/main/data/workspace/colors.cljs b/frontend/src/app/main/data/workspace/colors.cljs index 4b624188b0..09cf5af7e3 100644 --- a/frontend/src/app/main/data/workspace/colors.cljs +++ b/frontend/src/app/main/data/workspace/colors.cljs @@ -822,7 +822,7 @@ (update [_ state] (update state :colorpicker (fn [{:keys [stops editing-stop] :as state}] - (let [cap-stops? (or (features/active-feature? state "render-wasm/v1") (contains? cfg/flags :binary-fills)) + (let [cap-stops? (or (features/active-feature? state "render-wasm/v1") (contains? cfg/flags :frontend-binary-fills)) can-add-stop? (or (not cap-stops?) (< (count stops) shp/MAX-GRADIENT-STOPS))] (if can-add-stop? (if (cc/uniform-spread? stops) @@ -868,7 +868,7 @@ (update state :colorpicker (fn [state] (let [stops (:stops state) - cap-stops? (or (features/active-feature? state "render-wasm/v1") (contains? cfg/flags :binary-fills)) + cap-stops? (or (features/active-feature? state "render-wasm/v1") (contains? cfg/flags :frontend-binary-fills)) can-add-stop? (or (not cap-stops?) (< (count stops) shp/MAX-GRADIENT-STOPS))] (if can-add-stop? (let [new-stop (-> (cc/interpolate-gradient stops offset) (split-color-components)) @@ -889,7 +889,7 @@ (update state :colorpicker (fn [state] (let [stop (or (:editing-stop state) 0) - cap-stops? (or (features/active-feature? state "render-wasm/v1") (contains? cfg/flags :binary-fills)) + cap-stops? (or (features/active-feature? state "render-wasm/v1") (contains? cfg/flags :frontend-binary-fills)) stops (mapv split-color-components (if cap-stops? (take shp/MAX-GRADIENT-STOPS stops) stops))] (-> state (assoc :current-color (get stops stop)) diff --git a/frontend/src/app/main/ui/workspace/colorpicker.cljs b/frontend/src/app/main/ui/workspace/colorpicker.cljs index f7717138c4..dfa45562bb 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker.cljs @@ -338,7 +338,7 @@ (fn [value] (st/emit! (dc/update-colorpicker-gradient-opacity (/ value 100))))) - cap-stops? (or (features/use-feature "render-wasm/v1") (contains? cfg/flags :binary-fills)) + cap-stops? (or (features/use-feature "render-wasm/v1") (contains? cfg/flags :frontend-binary-fills)) tabs #js [#js {:aria-label (tr "workspace.libraries.colors.rgba") diff --git a/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs b/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs index 05cc9b3719..d7bd7823ee 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs @@ -287,7 +287,7 @@ (fn [] (when on-reverse-stops (on-reverse-stops)))) - cap-stops? (or (features/use-feature "render-wasm/v1") (contains? cfg/flags :binary-fills)) + cap-stops? (or (features/use-feature "render-wasm/v1") (contains? cfg/flags :frontend-binary-fills)) add-stop-disabled? (when cap-stops? (>= (count stops) shp/MAX-GRADIENT-STOPS))] [:div {:class (stl/css :gradient-panel)} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs index b51dcfbba7..337fedd476 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs @@ -56,7 +56,7 @@ values (d/without-nils values) fills (:fills values) has-fills? (or (= :multiple fills) (some? (seq fills))) - can-add-fills? (and (contains? cfg/flags :binary-fills) (not (= :multiple fills)) (< (count fills) shp/MAX-FILLS)) + can-add-fills? (and (contains? cfg/flags :frontend-binary-fills) (not (= :multiple fills)) (< (count fills) shp/MAX-FILLS)) state* (mf/use-state has-fills?) open? (deref state*) diff --git a/frontend/src/app/main/ui/workspace/viewport/gradients.cljs b/frontend/src/app/main/ui/workspace/viewport/gradients.cljs index 20e8fb5d32..3893216da1 100644 --- a/frontend/src/app/main/ui/workspace/viewport/gradients.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/gradients.cljs @@ -134,7 +134,7 @@ handler-state (mf/use-state {:display? false :offset 0 :hover nil}) - cap-stops? (or (features/use-feature "render-wasm/v1") (contains? cfg/flags :binary-fills)) + cap-stops? (or (features/use-feature "render-wasm/v1") (contains? cfg/flags :frontend-binary-fills)) can-add-stop? (if cap-stops? (< (count stops) shp/MAX-GRADIENT-STOPS) true) endpoint-on-pointer-down @@ -525,7 +525,7 @@ shape (mf/deref shape-ref) state (mf/deref refs/colorpicker) gradient (:gradient state) - cap-stops? (or (features/use-feature "render-wasm/v1") (contains? cfg/flags :binary-fills)) + cap-stops? (or (features/use-feature "render-wasm/v1") (contains? cfg/flags :frontend-binary-fills)) stops (if cap-stops? (vec (take shp/MAX-GRADIENT-STOPS (:stops state))) (:stops state)) From ce23fee2924ba7a213d9a5988f934e7989fbb4ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Thu, 29 May 2025 11:26:18 +0200 Subject: [PATCH 7/7] :sparkles: Limit the amount of fills shown in the UI --- .../main/ui/workspace/sidebar/options/menus/fill.cljs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs index 337fedd476..d5e4538ca1 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs @@ -54,9 +54,14 @@ ;; Excluding nil values values (d/without-nils values) - fills (:fills values) + fills (if (contains? cfg/flags :frontend-binary-fills) + (take shp/MAX-FILLS (d/nilv (:fills values) [])) + (:fills values)) has-fills? (or (= :multiple fills) (some? (seq fills))) - can-add-fills? (and (contains? cfg/flags :frontend-binary-fills) (not (= :multiple fills)) (< (count fills) shp/MAX-FILLS)) + can-add-fills? (if (contains? cfg/flags :frontend-binary-fills) + (and (not (= :multiple fills)) + (< (count fills) shp/MAX-FILLS)) + (not (= :multiple fills))) state* (mf/use-state has-fills?) open? (deref state*) @@ -170,7 +175,7 @@ (seq fills) [:& h/sortable-container {} - (for [[index value] (d/enumerate (:fills values []))] + (for [[index value] (d/enumerate fills)] [:> color-row* {:color (ctc/fill->shape-color value) :key index :index index