diff --git a/common/src/app/common/render_wasm/api/select.cljs b/common/src/app/common/render_wasm/api/select.cljs new file mode 100644 index 0000000000..f461c06302 --- /dev/null +++ b/common/src/app/common/render_wasm/api/select.cljs @@ -0,0 +1,28 @@ +;; 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 app.common.render-wasm.api.select + "WASM cursor selection. + Note that setters in `app.common.render-wasm.api.props` assume the caller + selected first." + (:require + [app.common.render-wasm.helpers :as h] + [app.common.render-wasm.wasm :as wasm] + [app.common.uuid :as uuid])) + +(defn use-shape + "Makes the shape with `id` current in WASM so subsequent setters apply to it. + This is a no-op when there is no WASM context." + [id] + ;; Use `wasm/live?` (not `initialized?`) so context-restore reload can + ;; select shapes while `reloading?` still blocks external app callers. + (when (wasm/live?) + (let [buffer (uuid/get-u32 id)] + (h/call wasm/internal-module "_use_shape" + (aget buffer 0) + (aget buffer 1) + (aget buffer 2) + (aget buffer 3))))) diff --git a/common/src/app/common/render_wasm/serialize_shape.cljs b/common/src/app/common/render_wasm/serialize_shape.cljs index 07ce3697ab..1df4a84505 100644 --- a/common/src/app/common/render_wasm/serialize_shape.cljs +++ b/common/src/app/common/render_wasm/serialize_shape.cljs @@ -25,6 +25,7 @@ dispatching per changed key through the same underlying `props` setters." (:require [app.common.render-wasm.api.props :as props] + [app.common.render-wasm.api.select :as wselect] [app.common.render-wasm.api.upload :as upload] [app.common.render-wasm.svg-derived :as svg-derived])) @@ -62,15 +63,15 @@ "Structural batch upload plus per-shape svg-attrs/path tail. Derives via svg-derived, uploads one `_set_shapes_batch` with `opts`, - then selects each shape needing svg-attrs/path content and applies it. - Host text/grid/image sequencing stays in callers. Returns the prepared - vector for the downstream host-attrs loop." - [shapes opts select-fn] + then selects each shape needing svg-attrs/path content via `use-shape!` + and applies the tail. Host text/grid/image sequencing stays in callers. + Returns the prepared vector for the downstream host-attrs loop." + [shapes opts] (let [prepared (mapv svg-derived/apply-svg-derived shapes)] (when (seq prepared) (upload/flush-shapes-batch! prepared opts) (doseq [shape prepared] (when (needs-shape-tail? shape) - (select-fn (:id shape)) + (wselect/use-shape (:id shape)) (write-shape-tail! shape)))) prepared)) diff --git a/frontend/src/app/main/data/workspace/modifiers.cljs b/frontend/src/app/main/data/workspace/modifiers.cljs index 47d97b4249..2cee1752a2 100644 --- a/frontend/src/app/main/data/workspace/modifiers.cljs +++ b/frontend/src/app/main/data/workspace/modifiers.cljs @@ -17,6 +17,7 @@ [app.common.geom.shapes :as gsh] [app.common.logging :as log] [app.common.math :as mth] + [app.common.render-wasm.api.select :as wselect] [app.common.types.component :as ctk] [app.common.types.container :as ctn] [app.common.types.modifiers :as ctm] @@ -813,7 +814,7 @@ ;; propagating geometry, so propagate_modifiers sees the updated state. (doseq [[id {:keys [property value]}] (extract-property-changes modif-tree)] (when (= property :grow-type) - (wasm.api/use-shape id) + (wselect/use-shape id) (wasm.api/set-shape-grow-type value))) (let [objects (dsh/lookup-page-objects state) diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index 15990f2de3..5e5c033329 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -20,6 +20,7 @@ [app.common.geom.shapes.grid-layout :as gslg] [app.common.logic.shapes :as cls] [app.common.math :as mth] + [app.common.render-wasm.api.select :as wselect] [app.common.types.component :as ctk] [app.common.types.container :as ctn] [app.common.types.modifiers :as ctm] @@ -791,7 +792,7 @@ [target-frame objects position] (if (features/active-feature? @st/state "render-wasm/v1") (do - (wasm.api/use-shape target-frame) + (wselect/use-shape target-frame) (let [cell (wasm.api/get-grid-coords position)] (when (not= cell [-1 -1]) cell))) (gslg/get-drop-cell target-frame objects position))) diff --git a/frontend/src/app/main/data/workspace/wasm_text.cljs b/frontend/src/app/main/data/workspace/wasm_text.cljs index ddd4f81400..3162656735 100644 --- a/frontend/src/app/main/data/workspace/wasm_text.cljs +++ b/frontend/src/app/main/data/workspace/wasm_text.cljs @@ -14,6 +14,7 @@ [app.common.files.helpers :as cfh] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] + [app.common.render-wasm.api.select :as wselect] [app.common.types.modifiers :as ctm] [app.common.types.text :as ctt] [app.main.data.helpers :as dsh] @@ -40,7 +41,7 @@ ;; text shape is being edited): there is no design state to query, and ;; returning nil makes callers skip the WASM resize/modifier path. (when (and id (wasm.api/initialized?)) - (wasm.api/use-shape id) + (wselect/use-shape id) ;; While the WASM text editor is actively editing it already holds the live ;; content and layout. Re-pushing the content here calls `_clear_shape_text` ;; + `_update_shape_text_layout`, which resets the editor and drops every diff --git a/frontend/src/app/main/render_viewer_wasm.cljs b/frontend/src/app/main/render_viewer_wasm.cljs index 9a6bca0d99..18b34fe5a1 100644 --- a/frontend/src/app/main/render_viewer_wasm.cljs +++ b/frontend/src/app/main/render_viewer_wasm.cljs @@ -9,6 +9,7 @@ (:require [app.common.data.macros :as dm] [app.common.exceptions :as ex] + [app.common.render-wasm.api.select :as wselect] [app.common.render-wasm.wasm :as wasm] [app.render-wasm.api :as wasm.api] [app.util.dom :as dom] @@ -83,14 +84,14 @@ (when (seq include-ids) (wasm.api/set-render-include-filter! include-ids)) (doseq [id clear-fills-ids] - (wasm.api/use-shape id) + (wselect/use-shape id) (wasm.api/clear-shape-fills!))) (defn- viewer-restore-layer-mask! [page-objects clear-fills-ids] (wasm.api/clear-render-include-filter!) (doseq [id clear-fills-ids] - (wasm.api/use-shape id) + (wselect/use-shape id) (wasm.api/set-shape-fills id (get-in page-objects [id :fills] []) false))) (defn- viewer-do-render! diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs index c74fbdca56..eb8f59542b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs @@ -9,6 +9,7 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.render-wasm.api.select :as wselect] [app.main.data.workspace :as dw] [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.tokens.application :as dwta] @@ -116,7 +117,7 @@ (when wasm-renderer-enabled? (doseq [id ids] - (wasm.api/use-shape id) + (wselect/use-shape id) (wasm.api/set-shape-blend-mode value) (wasm.api/request-render "preview-blend-mode"))) @@ -130,7 +131,7 @@ (swap! state* assoc :preview-complete? true) (when wasm-renderer-enabled? (doseq [id ids] - (wasm.api/use-shape id) + (wselect/use-shape id) (wasm.api/set-shape-blend-mode current-blend-mode) (wasm.api/request-render "preview-blend-mode"))) (st/emit! (dw/unset-preview-blend-mode ids)))) diff --git a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs index 39bbef583b..3705839275 100644 --- a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs +++ b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs @@ -11,6 +11,7 @@ [app.common.data.macros :as dm] [app.common.files.helpers :as cfh] [app.common.geom.shapes :as gsh] + [app.common.render-wasm.api.select :as wselect] [app.common.types.color :as clr] [app.common.types.component :as ctk] [app.common.types.shape :as cts] @@ -502,7 +503,7 @@ (let [content (-> active-editor-state (ted/get-editor-current-content) (ted/export-content))] - (wasm.api/use-shape edition) + (wselect/use-shape edition) (wasm.api/set-shape-text-content edition content) (let [dimension (wasm.api/get-text-dimensions)] (st/emit! (dwt/resize-text-editor edition dimension)) @@ -651,12 +652,12 @@ (mf/with-effect [path-editing? edition @initialized?] (when (and path-editing? edition @initialized?) - (wasm.api/use-shape edition) + (wselect/use-shape edition) (wasm.api/set-shape-hidden true) (wasm.api/request-render "start-path-edition") (fn [] (when (wasm.api/initialized?) - (wasm.api/use-shape edition) + (wselect/use-shape edition) (wasm.api/set-shape-hidden false) (wasm.api/request-render "stop-path-edition"))))) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 0c5a4c3d0b..dd5d75f4da 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -17,6 +17,7 @@ [app.common.logging :as log] [app.common.math :as mth] [app.common.render-wasm.api.props :as props] + [app.common.render-wasm.api.select :as wselect] [app.common.render-wasm.helpers :as h] [app.common.render-wasm.mem :as mem] [app.common.render-wasm.mem.heap32 :as mem.h32] @@ -727,18 +728,6 @@ (declare get-text-dimensions) -(defn use-shape - [id] - ;; Use `wasm/live?` (not `initialized?`) so context-restore reload can - ;; select shapes while `reloading?` still blocks external app callers. - (when (wasm/live?) - (let [buffer (uuid/get-u32 id)] - (h/call wasm/internal-module "_use_shape" - (aget buffer 0) - (aget buffer 1) - (aget buffer 2) - (aget buffer 3))))) - (defn has-shape [id] (when (wasm/live?) @@ -793,7 +782,7 @@ Updates the cached content, pushes to WASM, and returns {:shape-id :content} for saving. `:with-fills?` also returns the selection's `:fills`." [styles & [opts]] - (let [result (text-editor/apply-styles-to-selection styles use-shape set-shape-text-content opts)] + (let [result (text-editor/apply-styles-to-selection styles wselect/use-shape set-shape-text-content opts)] (request-render "apply-styles-to-selection") result)) @@ -801,7 +790,7 @@ "Apply paragraph attrs to the paragraphs the editor selection touches. Returns {:shape-id :content} for saving." [attrs] - (let [result (text-editor/apply-paragraph-attrs-to-selection attrs use-shape set-shape-text-content)] + (let [result (text-editor/apply-paragraph-attrs-to-selection attrs wselect/use-shape set-shape-text-content)] (request-render "apply-paragraph-attrs-to-selection") result)) @@ -811,7 +800,7 @@ [shape-id range] (when-let [styles (text-editor/get-pending-caret-styles shape-id)] (let [result (text-editor/apply-styles-to-range - shape-id range styles use-shape set-shape-text-content)] + shape-id range styles wselect/use-shape set-shape-text-content)] (text-editor/clear-pending-caret-styles!) (request-render "apply-pending-caret-styles") result))) @@ -1130,7 +1119,7 @@ (h/call wasm/internal-module "_set_shape_hidden" hidden)) (defn clear-shape-fills! - "Clear the fills of the currently-selected shape (call `use-shape` first). + "Clear the fills of the currently-selected shape (call `use-shape!` first). Equivalent to `set-shape-fills` with an empty collection." [] (when (initialized?) @@ -1407,7 +1396,7 @@ (defn get-text-dimensions ([id] - (use-shape id) + (wselect/use-shape id) (get-text-dimensions)) ([] (if-not (initialized?) @@ -1588,7 +1577,7 @@ `skip-fills-strokes?` is true, fill/stroke records were already in the batch; only image fetches remain. - Always `use-shape` first: after a multi-shape batch the WASM current shape is + Always `use-shape!` first: after a multi-shape batch the WASM current shape is the last record in the chunk, not this shape." [shape skip-layout? & {:keys [skip-fills-strokes?] :or {skip-fills-strokes? false}}] (let [id (dm/get-prop shape :id) @@ -1608,7 +1597,7 @@ true))] (when needs-current? - (use-shape id)) + (wselect/use-shape id)) (when (and (some? content) (= type :svg-raw)) (set-shape-svg-raw-content (get-static-markup shape))) @@ -1814,8 +1803,7 @@ prepared (serialize-shape/serialize-shapes-batch! chunk {:include-layout? true - :include-fills-strokes? true} - use-shape)] + :include-fills-strokes? true})] (loop [xs prepared t-acc (transient thumbnails-acc) @@ -1964,8 +1952,7 @@ (let [prepared (serialize-shape/serialize-shapes-batch! shapes {:include-layout? true - :include-fills-strokes? true} - use-shape) + :include-fills-strokes? true}) total-shapes (count prepared) {:keys [thumbnails full text-font-state]} (loop [index 0 @@ -2772,7 +2759,7 @@ (defn shape-to-path [id] (when (initialized?) - (use-shape id) + (wselect/use-shape id) (try (let [offset (-> (h/call wasm/internal-module "_current_to_path") (mem/->offset-32)) @@ -2796,7 +2783,7 @@ the fill rule itself)." [id stroke-index] (when (initialized?) - (use-shape id) + (wselect/use-shape id) (try (let [offset (-> (h/call wasm/internal-module "_convert_stroke_to_path" stroke-index) (mem/->offset-32)) @@ -2886,7 +2873,7 @@ (defn calculate-position-data [shape] (when (initialized?) - (use-shape (:id shape)) + (wselect/use-shape (:id shape)) (let [heapf32 (mem/get-heap-f32) heapu32 (mem/get-heap-u32) offset (-> (h/call wasm/internal-module "_calculate_position_data") diff --git a/frontend/src/app/render_wasm/shape.cljs b/frontend/src/app/render_wasm/shape.cljs index 995ea816cd..3d895841c8 100644 --- a/frontend/src/app/render_wasm/shape.cljs +++ b/frontend/src/app/render_wasm/shape.cljs @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.files.helpers :as cfh] + [app.common.render-wasm.api.select :as wselect] [app.common.render-wasm.svg-derived :as svg-derived] [app.common.transit :as t] [app.common.types.shape :as shape] @@ -321,7 +322,7 @@ (let [shape-id (dm/get-prop shape :id)] (if (shape-in-current-page? shape-id) (do - (api/use-shape shape-id) + (wselect/use-shape shape-id) (->> properties (mapcat #(set-wasm-attr! shape %)) (d/index-by :key :callback) diff --git a/frontend/test/frontend_tests/helpers/wasm.cljs b/frontend/test/frontend_tests/helpers/wasm.cljs index 82d4f8ab99..1c955c4d82 100644 --- a/frontend/test/frontend_tests/helpers/wasm.cljs +++ b/frontend/test/frontend_tests/helpers/wasm.cljs @@ -20,6 +20,7 @@ verify the WASM code path was exercised." (:require [app.common.data :as d] + [app.common.render-wasm.api.select :as wselect] [app.render-wasm.api :as wasm.api] [app.render-wasm.api.fonts :as wasm.fonts])) @@ -198,7 +199,7 @@ ;; Save originals (reset! originals {:initialized? wasm.api/initialized? - :use-shape wasm.api/use-shape + :use-shape wselect/use-shape :calculate-position-data wasm.api/calculate-position-data :request-render wasm.api/request-render :clean-modifiers wasm.api/clean-modifiers @@ -217,7 +218,7 @@ :get-content-fonts wasm.fonts/get-content-fonts}) ;; Install mocks (set! wasm.api/initialized? mock-initialized?) - (set! wasm.api/use-shape mock-use-shape) + (set! wselect/use-shape mock-use-shape) (set! wasm.api/calculate-position-data mock-calculate-position-data) (set! wasm.api/request-render mock-request-render) (set! wasm.api/clean-modifiers mock-clean-modifiers) @@ -248,7 +249,7 @@ [] (when-let [orig (not-empty @originals)] (set! wasm.api/initialized? (:initialized? orig)) - (set! wasm.api/use-shape (:use-shape orig)) + (set! wselect/use-shape (:use-shape orig)) (set! wasm.api/calculate-position-data (:calculate-position-data orig)) (set! wasm.api/request-render (:request-render orig)) (set! wasm.api/clean-modifiers (:clean-modifiers orig)) diff --git a/frontend/test/frontend_tests/render_wasm/serialization_test.cljs b/frontend/test/frontend_tests/render_wasm/serialization_test.cljs index c067793ab9..cb5438d1ce 100644 --- a/frontend/test/frontend_tests/render_wasm/serialization_test.cljs +++ b/frontend/test/frontend_tests/render_wasm/serialization_test.cljs @@ -15,6 +15,7 @@ render output below the FFI line needs headed or exporter pixel runs." (:require [app.common.render-wasm.api.props :as props] + [app.common.render-wasm.api.select :as wselect] [app.common.render-wasm.api.upload :as upload] [app.common.render-wasm.serialize-shape :as serialize-shape] [app.common.render-wasm.svg-derived :as svg-derived] @@ -22,22 +23,26 @@ (defn- with-ffi-stubs* "Stubs the single-arity FFI boundary fns (`flush-shapes-batch!`, - `set-shape-svg-attrs`, `set-shape-path-content`). `set-shape-upload!` is - deliberately left real: it is a trivial one-liner over the stubbed flush, - so the single test exercises its actual delegation and default opts. A - plain fn `set!` onto a multi-arity var breaks under the `:esm` test build - (call sites dispatch via `cljs$core$IFn$_invoke$arity$N`; see `mock/stub`)." + `use-shape!`, `set-shape-svg-attrs`, `set-shape-path-content`). + `set-shape-upload!` is deliberately left real: it is a trivial one-liner + over the stubbed flush, so the single test exercises its actual delegation + and default opts. A plain fn `set!` onto a multi-arity var breaks under + the `:esm` test build (call sites dispatch via + `cljs$core$IFn$_invoke$arity$N`; see `mock/stub`)." [stubs thunk] (let [orig-flush upload/flush-shapes-batch! + orig-select wselect/use-shape orig-svg-attrs props/set-shape-svg-attrs orig-path props/set-shape-path-content] (set! upload/flush-shapes-batch! (:flush stubs)) + (set! wselect/use-shape (or (:select stubs) (fn [_] nil))) (set! props/set-shape-svg-attrs (:svg-attrs stubs)) (set! props/set-shape-path-content (:path stubs)) (try (thunk) (finally (set! upload/flush-shapes-batch! orig-flush) + (set! wselect/use-shape orig-select) (set! props/set-shape-svg-attrs orig-svg-attrs) (set! props/set-shape-path-content orig-path))))) @@ -109,14 +114,14 @@ flush-calls (atom []) events (atom []) current (atom nil) - select-fn (fn [id] - (reset! current id) - (swap! events conj [:select id])) stubs {:flush (fn [s o] (swap! flush-calls conj {:shapes s :opts o}) nil) + :select (fn [id] + (reset! current id) + (swap! events conj [:select id])) :svg-attrs (fn [attrs] (swap! events conj [:svg-attrs @current attrs]) nil) :path (fn [content] (swap! events conj [:path @current content]) nil)} result (with-ffi-stubs* stubs - #(serialize-shape/serialize-shapes-batch! shapes opts select-fn)) + #(serialize-shape/serialize-shapes-batch! shapes opts)) flushed (first @flush-calls)] (t/is (= 1 (count @flush-calls)) "exactly one flush") (t/is (= opts (:opts flushed)) "flush carries passed opts") @@ -152,13 +157,11 @@ flush-calls (atom []) selected (atom []) stubs {:flush (fn [s o] (swap! flush-calls conj {:shapes s :opts o}) nil) + :select (fn [id] (swap! selected conj id)) :svg-attrs (fn [_] (t/is false "no svg-attrs write on empty batch") nil) :path (fn [_] (t/is false "no path write on empty batch") nil)} result (with-ffi-stubs* stubs - #(serialize-shape/serialize-shapes-batch! - [] - opts - (fn [id] (swap! selected conj id))))] + #(serialize-shape/serialize-shapes-batch! [] opts))] (t/is (= [] result) "returns empty prepared") (t/is (empty? @flush-calls) "no flush on empty batch") (t/is (empty? @selected) "no select on empty batch")))