diff --git a/common/src/app/common/flags.cljc b/common/src/app/common/flags.cljc index ae2742277..804cac400 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/common/src/app/common/types/shape.cljc b/common/src/app/common/types/shape.cljc index 4f8f1f313..e46b009cf 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/playwright/data/design/get-file-fills-limit.json b/frontend/playwright/data/design/get-file-fills-limit.json new file mode 100644 index 000000000..4e5096972 --- /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/ui/specs/colorpicker.spec.js b/frontend/playwright/ui/specs/colorpicker.spec.js index 231c4f9d7..e555eed3f 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 ea73328de..97e1f8cbb 100644 --- a/frontend/playwright/ui/specs/design-tab.spec.js +++ b/frontend/playwright/ui/specs/design-tab.spec.js @@ -66,6 +66,31 @@ test.describe("Constraints", () => { }); }); +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.mockConfigFlags(["enable-frontend-binary-fills"]); + await workspace.setupEmptyFile(); + await workspace.mockRPC(/get\-file\?/, "design/get-file-fills-limit.json"); + + await workspace.goToWorkspace({ + fileId: "d2847136-a651-80ac-8006-4202d9214aa7", + pageId: "d2847136-a651-80ac-8006-4202d9214aa8", + }); + + await workspace.clickLeafLayer("Rectangle"); + + 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(); + }); +}); + 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/data/workspace/colors.cljs b/frontend/src/app/main/data/workspace/colors.cljs index 5b7a0ea0f..09cf5af7e 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) @@ -823,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) @@ -869,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)) @@ -890,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 f7717138c..dfa45562b 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 05cc9b371..d7bd7823e 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 25fe82178..d5e4538ca 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,7 +10,9 @@ [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.config :as cfg] [app.main.data.workspace.colors :as dc] [app.main.store :as st] [app.main.ui.components.title-bar :refer [title-bar]] @@ -44,7 +46,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") @@ -52,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? (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*) @@ -73,12 +80,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] @@ -146,11 +153,12 @@ :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 :data-testid "add-fill" + :disabled (not can-add-fills?) :icon "add"}])]] (when open? @@ -167,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 diff --git a/frontend/src/app/main/ui/workspace/viewport/gradients.cljs b/frontend/src/app/main/ui/workspace/viewport/gradients.cljs index 20e8fb5d3..3893216da 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)) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index e89ba466d..7e32e6a86 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 4d8257e62..404e29e22 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 diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 92b383b1a..1ff7c784e 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -5617,7 +5617,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 98fc145a8..6c96f3fb6 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -5646,7 +5646,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"