;; 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 INC Sucursal en España SL (ns app.main.render-viewer-wasm "WASM offscreen rendering for the shared viewer (snapshot + fixed-scroll)." (:require [app.common.data.macros :as dm] [app.common.exceptions :as ex] [app.render-wasm.api :as wasm.api] [app.render-wasm.wasm :as wasm] [app.util.dom :as dom] [app.util.timers :as ts] [app.util.webapi :as webapi] [goog.events :as events] [promesa.core :as p] [rumext.v2 :as mf])) ;; The WASM module is a single global instance; serialize offscreen work. (defonce ^:private wasm-render-queue (atom (p/resolved nil))) (defn- enqueue-wasm-render! [task] (let [next-p (-> @wasm-render-queue (p/handle (fn [_ _] (task))))] (reset! wasm-render-queue (p/handle next-p (fn [_ _] nil))) next-p)) (defonce ^:private viewer-snapshot (atom {:os-canvas nil :page-key nil :canvas-w 0 :canvas-h 0 :dpr 1})) (defn- reset-viewer-snapshot! [] (reset! viewer-snapshot {:os-canvas nil :page-key nil :canvas-w 0 :canvas-h 0 :dpr 1})) (defn- draw-bitmap! "Blit the rendered OffscreenCanvas onto the visible 2D `canvas`. Firefox+NVIDIA can't `drawImage` a managed OffscreenCanvas directly (yields transparent pixels), but `drawImage` of an `ImageBitmap` works — so go through `createImageBitmap` (GPU-side, correct orientation, no CPU readback)." [canvas os-canvas object-id vis-w vis-h finish] (-> (js/createImageBitmap os-canvas) (p/then (fn [bitmap] (ts/raf (fn [] (let [ctx2d (.getContext canvas "2d")] (.clearRect ctx2d 0 0 vis-w vis-h) (.drawImage ctx2d bitmap 0 0 vis-w vis-h) (.close bitmap) (dom/set-attribute! canvas "id" (str "screenshot-" object-id)) (finish)))))) (p/catch (fn [e] (finish) (ts/schedule (fn [] (ex/raise :type :wasm-error :code :viewer-canvas-failed :hint "Viewer canvas failed" :cause e))))))) (defn- viewer-disable-wasm-ui-overlay! "Workspace WASM UI (rulers + rounded viewport frame) is composited in `present_frame`; the viewer must not show that chrome." [] (wasm.api/set-rulers-frame-visible! false) (wasm.api/set-rulers-visible! false)) (defn- viewer-apply-layer-mask! [include-ids clear-fills-ids] (wasm.api/clear-render-include-filter!) (when (seq include-ids) (wasm.api/set-render-include-filter! include-ids)) (doseq [id clear-fills-ids] (wasm.api/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) (wasm.api/set-shape-fills id (get-in page-objects [id :fills] []) false))) (defn- viewer-do-render! [page-objects canvas os-canvas object-id vis-w vis-h scale size include-ids clear-fills-ids finish] (viewer-disable-wasm-ui-overlay!) (viewer-apply-layer-mask! include-ids clear-fills-ids) (wasm.api/set-viewer-viewport! scale size) (wasm.api/render-sync-shape object-id) (viewer-restore-layer-mask! page-objects clear-fills-ids) (draw-bitmap! canvas os-canvas object-id vis-w vis-h finish)) (defn- render-to-canvas* [objects canvas bounds scale object-id on-render] (p/create (fn [resolve _reject] (let [width (.-width canvas) height (.-height canvas) prev-disable @wasm/disable-request-render? finish (fn [] (reset! wasm/disable-request-render? prev-disable) (when (fn? on-render) (on-render)) (resolve nil))] (try (reset! wasm/disable-request-render? true) (let [os-canvas (js/OffscreenCanvas. width height)] (if (wasm.api/init-canvas-context os-canvas) (wasm.api/initialize-viewport objects scale bounds :background-opacity 0 :force-sync true :on-render (fn [] (viewer-disable-wasm-ui-overlay!) (wasm.api/render-sync-shape object-id) (draw-bitmap! canvas os-canvas object-id width height (fn [] (wasm.api/clear-canvas {:lose-browser-context? false}) (reset-viewer-snapshot!) (finish))))) (finish))) (catch :default e (js/console.error "Error initializing canvas context:" e) (finish))))))) (defn render-to-canvas "One-shot WASM render into `canvas` (exports, thumbnails). Serialized globally." [objects canvas bounds scale object-id on-render] (enqueue-wasm-render! (fn [] (render-to-canvas* objects canvas bounds scale object-id on-render)))) (defn- render-viewer-frame* [page-key page-objects canvas size scale object-id on-render {:keys [include-ids clear-fills-ids] :or {clear-fills-ids #{}}}] (p/create (fn [resolve _reject] (let [prev-disable @wasm/disable-request-render? finish (fn [] (reset! wasm/disable-request-render? prev-disable) (when (fn? on-render) (on-render)) (resolve nil)) vis-w (.-width canvas) vis-h (.-height canvas) dpr (wasm.api/get-dpr) snap @viewer-snapshot same-page? (and (some? page-key) (identical? page-key (:page-key snap))) same-size? (and (= vis-w (:canvas-w snap)) (= vis-h (:canvas-h snap)) (= dpr (:dpr snap))) os (:os-canvas snap) do-render! (fn [os-canvas] (viewer-do-render! page-objects canvas os-canvas object-id vis-w vis-h scale size include-ids clear-fills-ids finish))] (reset! wasm/disable-request-render? true) (try (if (and same-page? (wasm.api/initialized?) os) (do (when-not same-size? (wasm.api/resize-offscreen-canvas! os vis-w vis-h) (swap! viewer-snapshot assoc :canvas-w vis-w :canvas-h vis-h :dpr dpr)) (do-render! os)) (let [os-canvas (js/OffscreenCanvas. vis-w vis-h)] (when (wasm.api/initialized?) (wasm.api/clear-canvas {:lose-browser-context? false})) (if (wasm.api/init-canvas-context os-canvas) (do (reset! viewer-snapshot {:os-canvas os-canvas :page-key page-key :canvas-w vis-w :canvas-h vis-h :dpr dpr}) (wasm.api/initialize-viewport page-objects scale size :background-opacity 0 :force-sync true :on-render #(do-render! os-canvas))) (finish)))) (catch :default e (js/console.error "viewer-snapshot: render error" e) (finish))))))) (defn- use-fixed-scroll-sync! [enabled? layer-ref] (mf/use-layout-effect (mf/deps enabled?) (fn [] (when enabled? (let [section (dom/get-element "viewer-section") sync! (fn [] (when-let [layer (mf/ref-val layer-ref)] (dom/set-style! layer "transform" (dm/str "translate(" (or (dom/get-h-scroll-pos section) 0) "px, " (or (dom/get-scroll-pos section) 0) "px)"))))] (when section (sync!) (let [key (events/listen section "scroll" (fn [_] (sync!)))] #(events/unlistenByKey key)))))))) (defn- use-viewer-dpr-key "Bump a counter when browser zoom changes devicePixelRatio so WASM canvases are resized like the workspace viewport." [] (let [dpr-key (mf/use-state 0)] (mf/use-effect (mf/deps []) (fn [] (webapi/on-dpr-change (fn [_] (swap! dpr-key inc))))) @dpr-key)) (defn- use-viewer-wasm-layers! [page-id page-objects size scale frame-id not-fixed-ref fixed-ref not-fixed-include-ids fixed-include-ids fixed-clear-fills-ids delta dpr-key] ;; The hot-areas SVG shifts every object by `-(size + delta)` so the frame ;; `selrect` lands flush against the overlay snap side, ignoring the extra ;; padding reserved for shadows/blur/strokes. Bake the same `delta` into the ;; WASM view origin so the rendered canvas aligns with that SVG (otherwise it ;; appears offset by the shadow margin). (let [render-size (-> size (update :x + (:x delta 0)) (update :y + (:y delta 0)))] (mf/use-layout-effect (mf/deps page-id page-objects render-size scale frame-id dpr-key not-fixed-include-ids fixed-include-ids fixed-clear-fills-ids) (fn [] (when (get page-objects frame-id) (->> @wasm.api/module (p/fmap (fn [ready?] (when ready? (let [not-fixed-canvas (mf/ref-val not-fixed-ref) fixed-canvas (mf/ref-val fixed-ref) passes (cond-> [] not-fixed-canvas (conj {:canvas not-fixed-canvas :opts (cond-> {} (seq not-fixed-include-ids) (assoc :include-ids not-fixed-include-ids))}) (and fixed-canvas (seq fixed-include-ids)) (conj {:canvas fixed-canvas :opts (cond-> {:include-ids fixed-include-ids} (seq fixed-clear-fills-ids) (assoc :clear-fills-ids fixed-clear-fills-ids))}))] (when (seq passes) (enqueue-wasm-render! (fn [] (reduce (fn [chain {:keys [canvas opts]}] (p/then chain #(render-viewer-frame* page-id page-objects canvas render-size scale frame-id nil opts))) (p/resolved nil) passes)))))))))))))) (defn use-viewer-wasm-viewport! "WASM render passes and fixed-scroll DOM sync for the viewer viewport." [page-id page-objects size scale frame-id not-fixed-ref fixed-ref fixed-scroll-layer-ref not-fixed-include-ids fixed-include-ids fixed-clear-fills-ids delta] (use-fixed-scroll-sync! (some? fixed-scroll-layer-ref) fixed-scroll-layer-ref) (let [dpr-key (use-viewer-dpr-key)] (use-viewer-wasm-layers! page-id page-objects size scale frame-id not-fixed-ref fixed-ref not-fixed-include-ids fixed-include-ids fixed-clear-fills-ids delta dpr-key)))