mirror of
https://github.com/penpot/penpot.git
synced 2026-08-14 16:58:35 +00:00
* ✨ Add headless wasm render backend to the exporter * ♻️ Move render-wasm bridge to common and split wasm builds * 🔧 Upload builtin font variants in the wasm exporter * ♻️ Move shared font and resources utils out of render_wasm * ⚡ Fetch only the exported roots in the wasm exporter * ⚡ Bound save_layer rects in the vector export path
287 lines
12 KiB
Clojure
287 lines
12 KiB
Clojure
;; 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.common.render-wasm.wasm :as wasm]
|
|
[app.render-wasm.api :as wasm.api]
|
|
[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)))
|