mirror of
https://github.com/penpot/penpot.git
synced 2026-09-10 22:19:19 +00:00
Merge pull request #10038 from penpot/superalex-viewer-wasm
🎉 Basic viewer with wasm
This commit is contained in:
commit
facea16444
@ -169,6 +169,7 @@
|
|||||||
|
|
||||||
:mcp
|
:mcp
|
||||||
:background-blur
|
:background-blur
|
||||||
|
:available-viewer-wasm
|
||||||
:stroke-path})
|
:stroke-path})
|
||||||
|
|
||||||
(def all-flags
|
(def all-flags
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
[app.common.types.shape-tree :as ctt]
|
[app.common.types.shape-tree :as ctt]
|
||||||
[app.common.types.shape.interactions :as ctsi]
|
[app.common.types.shape.interactions :as ctsi]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
|
[app.config :as cf]
|
||||||
[app.main.data.comments :as dcmt]
|
[app.main.data.comments :as dcmt]
|
||||||
[app.main.data.common :as dcm]
|
[app.main.data.common :as dcm]
|
||||||
[app.main.data.event :as ev]
|
[app.main.data.event :as ev]
|
||||||
@ -219,7 +220,8 @@
|
|||||||
(ptk/reify ::update-page-position-data
|
(ptk/reify ::update-page-position-data
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ state _]
|
(watch [_ state _]
|
||||||
(if (features/active-feature? state "render-wasm/v1")
|
(if (and (features/active-feature? state "render-wasm/v1")
|
||||||
|
(contains? cf/flags :available-viewer-wasm))
|
||||||
(let [objects (dsh/lookup-page-objects state file-id page-id)
|
(let [objects (dsh/lookup-page-objects state file-id page-id)
|
||||||
|
|
||||||
shapes
|
shapes
|
||||||
|
|||||||
@ -30,6 +30,7 @@
|
|||||||
[app.common.types.shape.layout :as ctl]
|
[app.common.types.shape.layout :as ctl]
|
||||||
[app.config :as cfg]
|
[app.config :as cfg]
|
||||||
[app.main.fonts :as fonts]
|
[app.main.fonts :as fonts]
|
||||||
|
[app.main.render-viewer-wasm :as rwv]
|
||||||
[app.main.ui.context :as muc]
|
[app.main.ui.context :as muc]
|
||||||
[app.main.ui.shapes.bool :as bool]
|
[app.main.ui.shapes.bool :as bool]
|
||||||
[app.main.ui.shapes.circle :as circle]
|
[app.main.ui.shapes.circle :as circle]
|
||||||
@ -524,32 +525,6 @@
|
|||||||
(for [shape shapes]
|
(for [shape shapes]
|
||||||
[:& shape-wrapper {:key (dm/str (:id shape)) :shape shape}])]]]))
|
[:& shape-wrapper {:key (dm/str (:id shape)) :shape shape}])]]]))
|
||||||
|
|
||||||
(defn render-to-canvas
|
|
||||||
[objects canvas bounds scale object-id on-render]
|
|
||||||
(let [width (.-width canvas)
|
|
||||||
height (.-height canvas)
|
|
||||||
os-canvas (js/OffscreenCanvas. width height)]
|
|
||||||
(try
|
|
||||||
(when (wasm.api/init-canvas-context os-canvas)
|
|
||||||
(wasm.api/initialize-viewport
|
|
||||||
objects scale bounds
|
|
||||||
:background-opacity 0
|
|
||||||
:on-render
|
|
||||||
(fn []
|
|
||||||
(wasm.api/render-sync-shape object-id)
|
|
||||||
(ts/raf
|
|
||||||
(fn []
|
|
||||||
(let [bitmap (.transferToImageBitmap os-canvas)
|
|
||||||
ctx2d (.getContext canvas "2d")]
|
|
||||||
(.clearRect ctx2d 0 0 width height)
|
|
||||||
(.drawImage ctx2d bitmap 0 0)
|
|
||||||
(dom/set-attribute! canvas "id" (dm/str "screenshot-" object-id))
|
|
||||||
(wasm.api/clear-canvas)
|
|
||||||
(on-render)))))))
|
|
||||||
(catch :default e
|
|
||||||
(js/console.error "Error initializing canvas context:" e)
|
|
||||||
false))))
|
|
||||||
|
|
||||||
(mf/defc object-wasm
|
(mf/defc object-wasm
|
||||||
{::mf/wrap [mf/memo]}
|
{::mf/wrap [mf/memo]}
|
||||||
[{:keys [objects object-id skip-children scale on-render] :as props}]
|
[{:keys [objects object-id skip-children scale on-render] :as props}]
|
||||||
@ -574,7 +549,7 @@
|
|||||||
(p/fmap
|
(p/fmap
|
||||||
(fn [ready?]
|
(fn [ready?]
|
||||||
(when ready?
|
(when ready?
|
||||||
(render-to-canvas objects canvas bounds scale object-id on-render))))))))
|
(rwv/render-to-canvas objects canvas bounds scale object-id on-render))))))))
|
||||||
|
|
||||||
[:canvas {:ref canvas-ref
|
[:canvas {:ref canvas-ref
|
||||||
:width (* scale width)
|
:width (* scale width)
|
||||||
|
|||||||
241
frontend/src/app/main/render_viewer_wasm.cljs
Normal file
241
frontend/src/app/main/render_viewer_wasm.cljs
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
;; 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.render-wasm.api :as wasm.api]
|
||||||
|
[app.render-wasm.wasm :as wasm]
|
||||||
|
[app.util.dom :as dom]
|
||||||
|
[app.util.timers :as ts]
|
||||||
|
[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}))
|
||||||
|
|
||||||
|
(defn- reset-viewer-snapshot! []
|
||||||
|
(reset! viewer-snapshot
|
||||||
|
{:os-canvas nil
|
||||||
|
:page-key nil
|
||||||
|
:canvas-w 0
|
||||||
|
:canvas-h 0}))
|
||||||
|
|
||||||
|
(defn- draw-bitmap!
|
||||||
|
[canvas os-canvas object-id vis-w vis-h finish]
|
||||||
|
(ts/raf
|
||||||
|
(fn []
|
||||||
|
(let [ctx2d (.getContext canvas "2d")]
|
||||||
|
(.clearRect ctx2d 0 0 vis-w vis-h)
|
||||||
|
;; Draw directly from OffscreenCanvas so it can be reused across passes.
|
||||||
|
(.drawImage ctx2d os-canvas 0 0 vis-w vis-h)
|
||||||
|
(dom/set-attribute! canvas "id" (str "screenshot-" object-id))
|
||||||
|
(finish)))))
|
||||||
|
|
||||||
|
(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)
|
||||||
|
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)))
|
||||||
|
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))
|
||||||
|
(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})
|
||||||
|
(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-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]
|
||||||
|
(mf/use-layout-effect
|
||||||
|
(mf/deps page-id page-objects size scale frame-id
|
||||||
|
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 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]
|
||||||
|
(use-fixed-scroll-sync! (some? fixed-scroll-layer-ref) fixed-scroll-layer-ref)
|
||||||
|
(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))
|
||||||
@ -23,7 +23,7 @@
|
|||||||
[app.main.ui.shapes.shape :refer [shape-container]]
|
[app.main.ui.shapes.shape :refer [shape-container]]
|
||||||
[app.main.ui.shapes.svg-raw :as svg-raw]
|
[app.main.ui.shapes.svg-raw :as svg-raw]
|
||||||
[app.main.ui.shapes.text :as text]
|
[app.main.ui.shapes.text :as text]
|
||||||
[app.main.ui.viewer.interactions :refer [prepare-objects]]
|
[app.main.ui.viewer.viewport-common :refer [prepare-objects]]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.object :as obj]
|
[app.util.object :as obj]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
|||||||
@ -9,53 +9,26 @@
|
|||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
[app.common.files.helpers :as cfh]
|
|
||||||
[app.common.geom.point :as gpt]
|
[app.common.geom.point :as gpt]
|
||||||
[app.common.geom.shapes :as gsh]
|
|
||||||
[app.common.types.modifiers :as ctm]
|
|
||||||
[app.common.types.page :as ctp]
|
[app.common.types.page :as ctp]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
|
[app.config :as cf]
|
||||||
[app.main.data.comments :as dcm]
|
[app.main.data.comments :as dcm]
|
||||||
[app.main.data.viewer :as dv]
|
[app.main.data.viewer :as dv]
|
||||||
|
[app.main.features :as features]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||||
[app.main.ui.hooks :as h]
|
[app.main.ui.hooks :as h]
|
||||||
[app.main.ui.icons :as deprecated-icon]
|
[app.main.ui.icons :as deprecated-icon]
|
||||||
[app.main.ui.viewer.shapes :as shapes]
|
[app.main.ui.viewer.shapes :as shapes]
|
||||||
|
[app.main.ui.viewer.viewport-common :as vpc]
|
||||||
|
[app.main.ui.viewer.viewport-wasm :as viewport.wasm]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.i18n :as i18n :refer [tr]]
|
[app.util.i18n :as i18n :refer [tr]]
|
||||||
[app.util.keyboard :as kbd]
|
[app.util.keyboard :as kbd]
|
||||||
[goog.events :as events]
|
[goog.events :as events]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
(defn prepare-objects
|
|
||||||
[frame size delta objects]
|
|
||||||
(let [frame-id (:id frame)
|
|
||||||
vector (-> (gpt/point (:x size) (:y size))
|
|
||||||
(gpt/add delta)
|
|
||||||
(gpt/negate))
|
|
||||||
update-fn #(d/update-when %1 %2 gsh/transform-shape (ctm/move-modifiers vector))]
|
|
||||||
(->> (cfh/get-children-ids objects frame-id)
|
|
||||||
(into [frame-id])
|
|
||||||
(reduce update-fn objects))))
|
|
||||||
|
|
||||||
(defn get-fixed-ids
|
|
||||||
[objects]
|
|
||||||
(let [fixed-ids (filter cfh/fixed-scroll? (vals objects))
|
|
||||||
|
|
||||||
;; we have to consider the children if the fixed element is a group
|
|
||||||
fixed-children-ids
|
|
||||||
(into #{} (mapcat #(cfh/get-children-ids objects (:id %)) fixed-ids))
|
|
||||||
|
|
||||||
parent-children-ids
|
|
||||||
(->> fixed-ids
|
|
||||||
(mapcat #(cons (:id %) (cfh/get-parent-ids objects (:id %))))
|
|
||||||
(remove #(= % uuid/zero)))
|
|
||||||
|
|
||||||
fixed-ids
|
|
||||||
(concat fixed-children-ids parent-children-ids)]
|
|
||||||
fixed-ids))
|
|
||||||
|
|
||||||
(mf/defc viewport-svg
|
(mf/defc viewport-svg
|
||||||
{::mf/wrap [mf/memo]
|
{::mf/wrap [mf/memo]
|
||||||
::mf/wrap-props false}
|
::mf/wrap-props false}
|
||||||
@ -74,7 +47,7 @@
|
|||||||
objects (:objects page)
|
objects (:objects page)
|
||||||
objects (cond-> objects fixed? (assoc-in [(:id frame) :fixed-scroll] true))
|
objects (cond-> objects fixed? (assoc-in [(:id frame) :fixed-scroll] true))
|
||||||
|
|
||||||
fixed-ids (get-fixed-ids objects)
|
fixed-ids (vpc/get-fixed-ids objects)
|
||||||
|
|
||||||
not-fixed-ids
|
not-fixed-ids
|
||||||
(->> (remove (set fixed-ids) (keys objects))
|
(->> (remove (set fixed-ids) (keys objects))
|
||||||
@ -86,7 +59,7 @@
|
|||||||
(map (d/getf objects))
|
(map (d/getf objects))
|
||||||
(concat [frame])
|
(concat [frame])
|
||||||
(d/index-by :id)
|
(d/index-by :id)
|
||||||
(prepare-objects frame size delta)))
|
(vpc/prepare-objects frame size delta)))
|
||||||
|
|
||||||
objects-fixed
|
objects-fixed
|
||||||
(mf/with-memo [fixed-ids page frame size delta]
|
(mf/with-memo [fixed-ids page frame size delta]
|
||||||
@ -175,7 +148,10 @@
|
|||||||
page (unchecked-get props "page")
|
page (unchecked-get props "page")
|
||||||
frame (unchecked-get props "frame")
|
frame (unchecked-get props "frame")
|
||||||
base (unchecked-get props "base-frame")
|
base (unchecked-get props "base-frame")
|
||||||
fixed? (unchecked-get props "fixed?")]
|
fixed? (unchecked-get props "fixed?")
|
||||||
|
|
||||||
|
render-wasm? (and (features/use-feature "render-wasm/v1")
|
||||||
|
(contains? cf/flags :available-viewer-wasm))]
|
||||||
|
|
||||||
(mf/with-effect [mode]
|
(mf/with-effect [mode]
|
||||||
(let [on-click
|
(let [on-click
|
||||||
@ -210,13 +186,21 @@
|
|||||||
(events/unlistenByKey key2)
|
(events/unlistenByKey key2)
|
||||||
(events/unlistenByKey key3))))
|
(events/unlistenByKey key3))))
|
||||||
|
|
||||||
[:& viewport-svg {:page page
|
(if ^boolean render-wasm?
|
||||||
:frame frame
|
[:& viewport.wasm/viewport-wasm {:page page
|
||||||
:base base
|
:frame frame
|
||||||
:offset offset
|
:base base
|
||||||
:size size
|
:offset offset
|
||||||
:delta delta
|
:size size
|
||||||
:fixed? fixed?}]))
|
:delta delta
|
||||||
|
:fixed? fixed?}]
|
||||||
|
[:& viewport-svg {:page page
|
||||||
|
:frame frame
|
||||||
|
:base base
|
||||||
|
:offset offset
|
||||||
|
:size size
|
||||||
|
:delta delta
|
||||||
|
:fixed? fixed?}])))
|
||||||
|
|
||||||
(mf/defc flows-menu*
|
(mf/defc flows-menu*
|
||||||
{::mf/wrap [mf/memo]}
|
{::mf/wrap [mf/memo]}
|
||||||
|
|||||||
@ -294,6 +294,80 @@
|
|||||||
:pointer-events "none"
|
:pointer-events "none"
|
||||||
:transform (gsh/transform-str shape)}])))
|
:transform (gsh/transform-str shape)}])))
|
||||||
|
|
||||||
|
;; --- WASM viewer hotspots ---
|
||||||
|
;; In WASM viewer mode the frame pixels come from a WASM snapshot, so we don't
|
||||||
|
;; render the SVG visuals at all. We only render the actionable areas (hotspots)
|
||||||
|
;; on top of the image: a transparent hit/highlight rect per interactive shape,
|
||||||
|
;; wired to the same interaction handlers as the regular SVG tree.
|
||||||
|
|
||||||
|
(mf/defc hotspot*
|
||||||
|
{::mf/wrap-props false}
|
||||||
|
[props]
|
||||||
|
(let [shape (unchecked-get props "shape")
|
||||||
|
all-objects (unchecked-get props "all-objects")
|
||||||
|
base-frame (mf/use-ctx base-frame-ctx)
|
||||||
|
frame-offset (mf/use-ctx frame-offset-ctx)
|
||||||
|
show-interactions (mf/deref ref:viewer-show-interactions)
|
||||||
|
overlays (mf/deref refs/viewer-overlays)
|
||||||
|
interactions (:interactions shape)
|
||||||
|
{:keys [x y width height]} (:selrect shape)
|
||||||
|
|
||||||
|
on-pd (mf/use-fn (mf/deps shape base-frame frame-offset all-objects overlays)
|
||||||
|
#(on-pointer-down % shape base-frame frame-offset all-objects overlays))
|
||||||
|
on-pu (mf/use-fn (mf/deps shape base-frame frame-offset all-objects overlays)
|
||||||
|
#(on-pointer-up % shape base-frame frame-offset all-objects overlays))
|
||||||
|
on-pe (mf/use-fn (mf/deps shape base-frame frame-offset all-objects overlays)
|
||||||
|
#(on-pointer-enter % shape base-frame frame-offset all-objects overlays))
|
||||||
|
on-pl (mf/use-fn (mf/deps shape base-frame frame-offset all-objects overlays)
|
||||||
|
#(on-pointer-leave % shape base-frame frame-offset all-objects overlays))]
|
||||||
|
|
||||||
|
(mf/with-effect []
|
||||||
|
(let [sems (on-load shape base-frame frame-offset all-objects overlays)]
|
||||||
|
(partial run! tm/dispose! sems)))
|
||||||
|
|
||||||
|
[:g {:style {:cursor (when (ctsi/actionable? interactions) "pointer")}
|
||||||
|
:on-pointer-down on-pd
|
||||||
|
:on-pointer-up on-pu
|
||||||
|
:on-pointer-enter on-pe
|
||||||
|
:on-pointer-leave on-pl}
|
||||||
|
[:rect {:x (- x 1)
|
||||||
|
:y (- y 1)
|
||||||
|
:width (+ width 2)
|
||||||
|
:height (+ height 2)
|
||||||
|
:fill "var(--color-accent-tertiary)"
|
||||||
|
:stroke "var(--color-accent-tertiary)"
|
||||||
|
:stroke-width (if show-interactions 1 0)
|
||||||
|
:fill-opacity (if show-interactions 0.2 0)
|
||||||
|
;; This rect is the only hit target, so it must always capture
|
||||||
|
;; pointer events even when fully transparent.
|
||||||
|
:pointer-events "all"
|
||||||
|
:transform (gsh/transform-str shape)}]]))
|
||||||
|
|
||||||
|
(mf/defc frame-hotspots*
|
||||||
|
"Renders interaction hotspots for a frame subtree (WASM viewer mode).
|
||||||
|
`objects` must be the prepared (vbox-space) objects and `frame` the prepared
|
||||||
|
frame; only shapes with interactions produce a hotspot.
|
||||||
|
|
||||||
|
Optional `shape-filter` is a predicate that receives the shape id and returns
|
||||||
|
true when it should be included (used to split fixed-scroll vs normal layers)."
|
||||||
|
{::mf/wrap-props false}
|
||||||
|
[props]
|
||||||
|
(let [objects (unchecked-get props "objects")
|
||||||
|
all-objects (or (unchecked-get props "all-objects") objects)
|
||||||
|
shape-filter (unchecked-get props "shape-filter")
|
||||||
|
frame (unchecked-get props "frame")
|
||||||
|
frame-id (:id frame)
|
||||||
|
ids (cond->> (cons frame-id (cfh/get-children-ids objects frame-id))
|
||||||
|
shape-filter (filter shape-filter))
|
||||||
|
hotspots (->> ids
|
||||||
|
(keep #(get objects %))
|
||||||
|
(filter (fn [s] (and (not (:hidden s))
|
||||||
|
(seq (:interactions s))))))]
|
||||||
|
[:* (for [shape hotspots]
|
||||||
|
[:& hotspot* {:key (str (:id shape))
|
||||||
|
:shape shape
|
||||||
|
:all-objects all-objects}])]))
|
||||||
|
|
||||||
|
|
||||||
;; TODO: use-memo use-fn
|
;; TODO: use-memo use-fn
|
||||||
|
|
||||||
|
|||||||
67
frontend/src/app/main/ui/viewer/viewport_common.cljs
Normal file
67
frontend/src/app/main/ui/viewer/viewport_common.cljs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
;; 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.ui.viewer.viewport-common
|
||||||
|
"Shared object preparation for viewer viewports (SVG and WASM)."
|
||||||
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
|
[app.common.files.helpers :as cfh]
|
||||||
|
[app.common.geom.point :as gpt]
|
||||||
|
[app.common.geom.shapes :as gsh]
|
||||||
|
[app.common.types.modifiers :as ctm]
|
||||||
|
[app.common.uuid :as uuid]))
|
||||||
|
|
||||||
|
(defn prepare-objects
|
||||||
|
[frame size delta objects]
|
||||||
|
(let [frame-id (:id frame)
|
||||||
|
vector (-> (gpt/point (:x size) (:y size))
|
||||||
|
(gpt/add delta)
|
||||||
|
(gpt/negate))
|
||||||
|
update-fn #(d/update-when %1 %2 gsh/transform-shape (ctm/move-modifiers vector))]
|
||||||
|
(->> (cfh/get-children-ids objects frame-id)
|
||||||
|
(into [frame-id])
|
||||||
|
(reduce update-fn objects))))
|
||||||
|
|
||||||
|
(defn get-fixed-ids
|
||||||
|
[objects]
|
||||||
|
(let [fixed-ids (filter cfh/fixed-scroll? (vals objects))
|
||||||
|
|
||||||
|
fixed-children-ids
|
||||||
|
(into #{} (mapcat #(cfh/get-children-ids objects (:id %)) fixed-ids))
|
||||||
|
|
||||||
|
parent-children-ids
|
||||||
|
(->> fixed-ids
|
||||||
|
(mapcat #(cons (:id %) (cfh/get-parent-ids objects (:id %))))
|
||||||
|
(remove #(= % uuid/zero)))
|
||||||
|
|
||||||
|
fixed-ids
|
||||||
|
(concat fixed-children-ids parent-children-ids)]
|
||||||
|
fixed-ids))
|
||||||
|
|
||||||
|
(defn frame-fixed-mask-ids
|
||||||
|
"Fixed-layer shape ids inside `frame-id` (same rules as `get-fixed-ids`)."
|
||||||
|
[objects frame-id]
|
||||||
|
(when frame-id
|
||||||
|
(let [subtree (into #{} (cfh/get-children-ids-with-self objects frame-id))]
|
||||||
|
(into #{}
|
||||||
|
(filter #(contains? subtree %)
|
||||||
|
(get-fixed-ids objects))))))
|
||||||
|
|
||||||
|
(defn prepare-page-objects
|
||||||
|
"Transform all page objects into vbox-space (for overlay positioning)."
|
||||||
|
[objects size delta]
|
||||||
|
(let [vector (-> (gpt/point (:x size) (:y size))
|
||||||
|
(gpt/add delta)
|
||||||
|
(gpt/negate))
|
||||||
|
update-fn #(d/update-when %1 %2 gsh/transform-shape (ctm/move-modifiers vector))
|
||||||
|
ids (->> (keys objects) (remove #(= % uuid/zero)))]
|
||||||
|
(reduce update-fn objects ids)))
|
||||||
|
|
||||||
|
(defn viewer-scale
|
||||||
|
[size]
|
||||||
|
(if (and (:base-width size) (pos? (:base-width size)))
|
||||||
|
(/ (:width size) (:base-width size))
|
||||||
|
1))
|
||||||
160
frontend/src/app/main/ui/viewer/viewport_wasm.cljs
Normal file
160
frontend/src/app/main/ui/viewer/viewport_wasm.cljs
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
;; 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.ui.viewer.viewport-wasm
|
||||||
|
(:require-macros [app.main.style :as stl])
|
||||||
|
(:require
|
||||||
|
[app.common.files.helpers :as cfh]
|
||||||
|
[app.common.geom.point :as gpt]
|
||||||
|
[app.main.render-viewer-wasm :as rwv]
|
||||||
|
[app.main.ui.viewer.shapes :as shapes]
|
||||||
|
[app.main.ui.viewer.viewport-common :as vpc]
|
||||||
|
[app.util.object :as obj]
|
||||||
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
|
(defn- canvas-dimensions
|
||||||
|
[scale size]
|
||||||
|
{:width (js/Math.round (* scale (:base-width size)))
|
||||||
|
:height (js/Math.round (* scale (:base-height size)))})
|
||||||
|
|
||||||
|
(defn- frame-hotspots-props
|
||||||
|
"frame-hotspots* uses ::mf/wrap-props false and expects string keys."
|
||||||
|
[prepared prepared-all prepared-frame shape-filter]
|
||||||
|
(let [props #js {"objects" prepared
|
||||||
|
"all-objects" prepared-all
|
||||||
|
"frame" prepared-frame}]
|
||||||
|
(when shape-filter
|
||||||
|
(obj/set! props "shape-filter" shape-filter))
|
||||||
|
props))
|
||||||
|
|
||||||
|
(mf/defc wasm-hotspots-svg
|
||||||
|
[{:keys [vbox size class prepared prepared-all prepared-frame shape-filter]}]
|
||||||
|
[:svg {:view-box vbox
|
||||||
|
:width (:width size)
|
||||||
|
:height (:height size)
|
||||||
|
:version "1.1"
|
||||||
|
:xmlnsXlink "http://www.w3.org/1999/xlink"
|
||||||
|
:xmlns "http://www.w3.org/2000/svg"
|
||||||
|
:fill "none"
|
||||||
|
:style {:position "absolute"
|
||||||
|
:top 0
|
||||||
|
:left 0}
|
||||||
|
:class class}
|
||||||
|
[:& shapes/frame-hotspots*
|
||||||
|
(frame-hotspots-props prepared prepared-all prepared-frame shape-filter)]])
|
||||||
|
|
||||||
|
(mf/defc wasm-layer
|
||||||
|
[{:keys [canvas-ref scale size vbox svg-props]}]
|
||||||
|
(let [{:keys [width height]} (canvas-dimensions scale size)]
|
||||||
|
[:div {:style {:position "absolute"
|
||||||
|
:top 0
|
||||||
|
:left 0}}
|
||||||
|
[:canvas {:ref canvas-ref :width width :height height :style {:width "100%"
|
||||||
|
:height "100%"
|
||||||
|
:background "transparent"
|
||||||
|
:pointer-events "none"}}]
|
||||||
|
[:& wasm-hotspots-svg (assoc svg-props :vbox vbox :size size)]]))
|
||||||
|
|
||||||
|
(defn- fixed-scroll-layer-ids
|
||||||
|
[objects frame-id has-fixed?]
|
||||||
|
(let [frame-subtree-ids (into #{} (cfh/get-children-ids-with-self objects frame-id))
|
||||||
|
fixed-mask-ids (when has-fixed? (vpc/frame-fixed-mask-ids objects frame-id))
|
||||||
|
fixed-mask-set (or fixed-mask-ids #{})
|
||||||
|
not-fixed-include-ids
|
||||||
|
(when has-fixed?
|
||||||
|
(into []
|
||||||
|
(distinct
|
||||||
|
(conj (->> frame-subtree-ids
|
||||||
|
(remove #(contains? fixed-mask-set %)))
|
||||||
|
frame-id))))
|
||||||
|
fixed-include-ids
|
||||||
|
(when has-fixed?
|
||||||
|
(vec (conj (or fixed-mask-ids #{}) frame-id)))
|
||||||
|
fixed-clear-fills-ids (when has-fixed? #{frame-id})]
|
||||||
|
{:fixed-mask-set fixed-mask-set
|
||||||
|
:not-fixed-include-ids not-fixed-include-ids
|
||||||
|
:fixed-include-ids fixed-include-ids
|
||||||
|
:fixed-clear-fills-ids fixed-clear-fills-ids}))
|
||||||
|
|
||||||
|
(mf/defc viewport-wasm
|
||||||
|
{::mf/wrap [mf/memo]
|
||||||
|
::mf/wrap-props false}
|
||||||
|
[props]
|
||||||
|
(let [page (unchecked-get props "page")
|
||||||
|
frame (unchecked-get props "frame")
|
||||||
|
base (unchecked-get props "base")
|
||||||
|
offset (unchecked-get props "offset")
|
||||||
|
size (unchecked-get props "size")
|
||||||
|
delta (or (unchecked-get props "delta") (gpt/point 0 0))
|
||||||
|
vbox (:vbox size)
|
||||||
|
fixed? (true? (unchecked-get props "fixed?"))
|
||||||
|
|
||||||
|
fixed-layer-ref (mf/use-ref nil)
|
||||||
|
not-fixed-wasm-ref (mf/use-ref nil)
|
||||||
|
fixed-wasm-ref (mf/use-ref nil)
|
||||||
|
|
||||||
|
objects (:objects page)
|
||||||
|
frame-id (:id frame)
|
||||||
|
scale (vpc/viewer-scale size)
|
||||||
|
page-id (:id page)
|
||||||
|
|
||||||
|
frame (cond-> frame fixed? (assoc :fixed-scroll true))
|
||||||
|
objects (cond-> objects fixed? (assoc-in [frame-id :fixed-scroll] true))
|
||||||
|
|
||||||
|
has-fixed?
|
||||||
|
(and (not fixed?)
|
||||||
|
(some #(cfh/fixed-scroll? (get objects %))
|
||||||
|
(cfh/get-children-ids objects frame-id)))
|
||||||
|
|
||||||
|
prepared
|
||||||
|
(mf/with-memo [objects frame size delta]
|
||||||
|
(vpc/prepare-objects frame size delta objects))
|
||||||
|
|
||||||
|
prepared-all
|
||||||
|
(mf/with-memo [objects size delta]
|
||||||
|
(vpc/prepare-page-objects objects size delta))
|
||||||
|
|
||||||
|
{:keys [fixed-mask-set not-fixed-include-ids fixed-include-ids fixed-clear-fills-ids]}
|
||||||
|
(mf/with-memo [objects frame-id has-fixed?]
|
||||||
|
(fixed-scroll-layer-ids objects frame-id has-fixed?))
|
||||||
|
|
||||||
|
prepared-frame (get prepared frame-id)
|
||||||
|
|
||||||
|
svg-base {:prepared prepared
|
||||||
|
:prepared-all prepared-all
|
||||||
|
:prepared-frame prepared-frame}]
|
||||||
|
|
||||||
|
(rwv/use-viewer-wasm-viewport!
|
||||||
|
page-id objects size scale frame-id
|
||||||
|
not-fixed-wasm-ref fixed-wasm-ref
|
||||||
|
(when has-fixed? fixed-layer-ref)
|
||||||
|
not-fixed-include-ids fixed-include-ids fixed-clear-fills-ids)
|
||||||
|
|
||||||
|
[:& (mf/provider shapes/base-frame-ctx) {:value (get prepared-all (:id base))}
|
||||||
|
[:& (mf/provider shapes/frame-offset-ctx) {:value offset}
|
||||||
|
[:*
|
||||||
|
[:& wasm-layer
|
||||||
|
{:canvas-ref not-fixed-wasm-ref
|
||||||
|
:scale scale
|
||||||
|
:size size
|
||||||
|
:vbox vbox
|
||||||
|
:svg-props (assoc svg-base
|
||||||
|
:class (if has-fixed?
|
||||||
|
(stl/css :not-fixed)
|
||||||
|
(when fixed? (stl/css :fixed)))
|
||||||
|
:shape-filter (when has-fixed?
|
||||||
|
#(not (contains? fixed-mask-set %))))}]
|
||||||
|
|
||||||
|
(when has-fixed?
|
||||||
|
[:div {:ref fixed-layer-ref}
|
||||||
|
[:& wasm-layer
|
||||||
|
{:canvas-ref fixed-wasm-ref
|
||||||
|
:scale scale
|
||||||
|
:size size
|
||||||
|
:vbox vbox
|
||||||
|
:svg-props (assoc svg-base
|
||||||
|
:class (stl/css :not-fixed)
|
||||||
|
:shape-filter #(contains? fixed-mask-set %))}]])]]]))
|
||||||
@ -931,6 +931,13 @@
|
|||||||
[hidden]
|
[hidden]
|
||||||
(h/call wasm/internal-module "_set_shape_hidden" hidden))
|
(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).
|
||||||
|
Equivalent to `set-shape-fills` with an empty collection."
|
||||||
|
[]
|
||||||
|
(when (initialized?)
|
||||||
|
(h/call wasm/internal-module "_clear_shape_fills")))
|
||||||
|
|
||||||
(defn set-shape-bool-type
|
(defn set-shape-bool-type
|
||||||
[bool-type]
|
[bool-type]
|
||||||
(h/call wasm/internal-module "_set_shape_bool_type" (sr/translate-bool-type bool-type)))
|
(h/call wasm/internal-module "_set_shape_bool_type" (sr/translate-bool-type bool-type)))
|
||||||
@ -1666,6 +1673,27 @@
|
|||||||
(h/call wasm/internal-module "_set_focus_mode")
|
(h/call wasm/internal-module "_set_focus_mode")
|
||||||
(request-render "set-focus-mode"))))
|
(request-render "set-focus-mode"))))
|
||||||
|
|
||||||
|
(defn clear-render-include-filter!
|
||||||
|
"Clear the viewer include filter (render all shapes in the subtree again)."
|
||||||
|
[]
|
||||||
|
(when (initialized?)
|
||||||
|
(h/call wasm/internal-module "_clear_render_include_filter")))
|
||||||
|
|
||||||
|
(defn set-render-include-filter!
|
||||||
|
"Restrict the next render to `shape-ids` and descendants of whitelisted nodes.
|
||||||
|
Used for viewer fixed-scroll layers; does not change shape hidden flags."
|
||||||
|
[shape-ids]
|
||||||
|
(when (and (initialized?) (seq shape-ids))
|
||||||
|
(let [ids (vec shape-ids)
|
||||||
|
size (mem/get-alloc-size ids UUID-U8-SIZE)
|
||||||
|
heap (mem/get-heap-u32)
|
||||||
|
offset (mem/alloc->offset-32 size)]
|
||||||
|
(reduce (fn [offset id]
|
||||||
|
(mem.h32/write-uuid offset heap id))
|
||||||
|
offset
|
||||||
|
ids)
|
||||||
|
(h/call wasm/internal-module "_set_render_include_filter"))))
|
||||||
|
|
||||||
(defn set-structure-modifiers
|
(defn set-structure-modifiers
|
||||||
[entries]
|
[entries]
|
||||||
(when-not ^boolean (empty? entries)
|
(when-not ^boolean (empty? entries)
|
||||||
@ -1865,6 +1893,24 @@
|
|||||||
[width height]
|
[width height]
|
||||||
(h/call wasm/internal-module "_resize_viewbox" width height))
|
(h/call wasm/internal-module "_resize_viewbox" width height))
|
||||||
|
|
||||||
|
(defn set-viewer-viewport!
|
||||||
|
"Update viewer zoom/pan and rebuild the tile index (frame hops in the viewer).
|
||||||
|
`vbox` must have at least `:x` and `:y` keys (design-space top-left corner)."
|
||||||
|
[zoom vbox]
|
||||||
|
(h/call wasm/internal-module "_set_view" zoom (- (:x vbox)) (- (:y vbox)))
|
||||||
|
(when (initialized?)
|
||||||
|
(h/call wasm/internal-module "_set_view_end")
|
||||||
|
(reset! view-interaction-active? false)))
|
||||||
|
|
||||||
|
(defn resize-offscreen-canvas!
|
||||||
|
"Resize a persistent OffscreenCanvas to new physical-pixel dimensions and
|
||||||
|
update the WASM render surfaces accordingly (via `_resize_viewbox`). The
|
||||||
|
design state (shape pool) is preserved so `set-objects` is not needed again."
|
||||||
|
[canvas new-physical-w new-physical-h]
|
||||||
|
(set! (.-width canvas) new-physical-w)
|
||||||
|
(set! (.-height canvas) new-physical-h)
|
||||||
|
(resize-viewbox (/ new-physical-w dpr) (/ new-physical-h dpr)))
|
||||||
|
|
||||||
(defn- debug-flags
|
(defn- debug-flags
|
||||||
[]
|
[]
|
||||||
(cond-> 0
|
(cond-> 0
|
||||||
|
|||||||
@ -151,6 +151,31 @@ pub extern "C" fn render_blurred_snapshot(blur_radius: f32) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
#[wasm_error]
|
||||||
|
pub extern "C" fn clear_render_include_filter() -> Result<()> {
|
||||||
|
with_state!(state, {
|
||||||
|
state.clear_include_filter();
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
#[wasm_error]
|
||||||
|
pub extern "C" fn set_render_include_filter() -> Result<()> {
|
||||||
|
let bytes = mem::bytes();
|
||||||
|
|
||||||
|
let entries: Vec<Uuid> = bytes
|
||||||
|
.chunks(size_of::<<Uuid as SerializableResult>::BytesType>())
|
||||||
|
.map(|data| Uuid::try_from(data).map_err(|e| Error::RecoverableError(e.to_string())))
|
||||||
|
.collect::<Result<Vec<Uuid>>>()?;
|
||||||
|
|
||||||
|
with_state!(state, {
|
||||||
|
state.set_include_filter(entries);
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
#[wasm_error]
|
#[wasm_error]
|
||||||
pub extern "C" fn render_sync() -> Result<()> {
|
pub extern "C" fn render_sync() -> Result<()> {
|
||||||
|
|||||||
@ -372,6 +372,10 @@ pub(crate) struct RenderState {
|
|||||||
pub show_grid: Option<Uuid>,
|
pub show_grid: Option<Uuid>,
|
||||||
pub rulers: RulerState,
|
pub rulers: RulerState,
|
||||||
pub focus_mode: FocusMode,
|
pub focus_mode: FocusMode,
|
||||||
|
/// Viewer-only whitelist for fixed-scroll layer passes.
|
||||||
|
pub include_filter: Option<HashSet<Uuid>>,
|
||||||
|
/// Frame id passed as `base_object` for viewer renders; always traversed.
|
||||||
|
pub viewer_render_root: Option<Uuid>,
|
||||||
pub touched_ids: HashSet<Uuid>,
|
pub touched_ids: HashSet<Uuid>,
|
||||||
/// Temporary flag used for off-screen passes (drop-shadow masks, filter surfaces, etc.)
|
/// Temporary flag used for off-screen passes (drop-shadow masks, filter surfaces, etc.)
|
||||||
/// where we must render shapes without inheriting ancestor layer blurs. Toggle it through
|
/// where we must render shapes without inheriting ancestor layer blurs. Toggle it through
|
||||||
@ -568,6 +572,8 @@ impl RenderState {
|
|||||||
show_grid: None,
|
show_grid: None,
|
||||||
rulers: RulerState::default(),
|
rulers: RulerState::default(),
|
||||||
focus_mode: FocusMode::new(),
|
focus_mode: FocusMode::new(),
|
||||||
|
include_filter: None,
|
||||||
|
viewer_render_root: None,
|
||||||
touched_ids: HashSet::default(),
|
touched_ids: HashSet::default(),
|
||||||
ignore_nested_blurs: false,
|
ignore_nested_blurs: false,
|
||||||
preview_mode: false,
|
preview_mode: false,
|
||||||
@ -850,7 +856,14 @@ impl RenderState {
|
|||||||
/// on top of Target, then present. Backbuffer is left clean so it can be reused
|
/// on top of Target, then present. Backbuffer is left clean so it can be reused
|
||||||
/// as-is across interactive-transform frames without stale overlay pixels.
|
/// as-is across interactive-transform frames without stale overlay pixels.
|
||||||
pub fn present_frame(&mut self, tree: ShapesPoolRef) {
|
pub fn present_frame(&mut self, tree: ShapesPoolRef) {
|
||||||
self.surfaces.copy_backbuffer_to_target();
|
// Viewer masked passes render a partial scene onto a transparent backbuffer.
|
||||||
|
// SrcOver would keep pass-1 pixels wherever the backbuffer stays transparent.
|
||||||
|
if self.viewer_masked_pass() {
|
||||||
|
self.surfaces.clear_target(skia::Color::TRANSPARENT);
|
||||||
|
self.surfaces.copy_backbuffer_to_target_replace();
|
||||||
|
} else {
|
||||||
|
self.surfaces.copy_backbuffer_to_target();
|
||||||
|
}
|
||||||
if self.options.is_debug_visible() {
|
if self.options.is_debug_visible() {
|
||||||
debug::render(self);
|
debug::render(self);
|
||||||
}
|
}
|
||||||
@ -942,6 +955,20 @@ impl RenderState {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Viewer masked passes render a partial scene. Reusing the tile texture cache would
|
||||||
|
// SrcOver-blend onto textures from the previous pass and leak pixels into the blob.
|
||||||
|
if self.viewer_masked_pass() {
|
||||||
|
// Use viewbox-aligned bounds (not grid-snapped) to match interactive-transform
|
||||||
|
// compositing and avoid a visible offset vs the DOM canvas.
|
||||||
|
let tile_rect = self.get_current_tile_bounds()?;
|
||||||
|
self.surfaces.draw_current_tile_into_backbuffer(
|
||||||
|
&tile_rect,
|
||||||
|
self.background_color,
|
||||||
|
surfaces::DrawOnCache::No,
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
let fast_mode = self.options.is_fast_mode();
|
let fast_mode = self.options.is_fast_mode();
|
||||||
// Decide *now* (at the first real cache blit) whether we need to clear Cache.
|
// Decide *now* (at the first real cache blit) whether we need to clear Cache.
|
||||||
// This avoids clearing Cache on renders that don't actually paint tiles (e.g. hover/UI),
|
// This avoids clearing Cache on renders that don't actually paint tiles (e.g. hover/UI),
|
||||||
@ -1043,6 +1070,55 @@ impl RenderState {
|
|||||||
self.focus_mode.set_shapes(shapes);
|
self.focus_mode.set_shapes(shapes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn clear_include_filter(&mut self) {
|
||||||
|
self.include_filter = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_include_filter(&mut self, shapes: Vec<Uuid>) {
|
||||||
|
self.include_filter = Some(shapes.into_iter().collect());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn viewer_masked_pass(&self) -> bool {
|
||||||
|
self.include_filter.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset_viewer_masked_surfaces(&mut self) {
|
||||||
|
self.surfaces.clear_backbuffer(self.background_color);
|
||||||
|
self.surfaces.clear_tile_atlas();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True when the shape or any descendant is whitelisted.
|
||||||
|
pub fn shape_visible_in_include_filter(&self, shape_id: &Uuid, tree: ShapesPoolRef) -> bool {
|
||||||
|
let Some(ref include) = self.include_filter else {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
if include.contains(shape_id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let Some(shape) = tree.get(shape_id) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
shape
|
||||||
|
.children_ids_iter(false)
|
||||||
|
.any(|child_id| self.shape_visible_in_include_filter(child_id, tree))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When an include whitelist is active, only those ids are painted.
|
||||||
|
fn shape_should_paint_for_viewer_layer(&self, shape_id: &Uuid) -> bool {
|
||||||
|
match &self.include_filter {
|
||||||
|
Some(include) => include.contains(shape_id),
|
||||||
|
None => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Viewer layer mask: traverse whitelisted subtrees; paint only listed ids.
|
||||||
|
pub fn shape_visible_for_viewer_layer(&self, shape_id: &Uuid, tree: ShapesPoolRef) -> bool {
|
||||||
|
if self.viewer_render_root.as_ref() == Some(shape_id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
self.shape_visible_in_include_filter(shape_id, tree)
|
||||||
|
}
|
||||||
|
|
||||||
fn get_inherited_drop_shadows(&self) -> Option<Vec<skia_safe::Paint>> {
|
fn get_inherited_drop_shadows(&self) -> Option<Vec<skia_safe::Paint>> {
|
||||||
let drop_shadows: Vec<&Shadow> = self
|
let drop_shadows: Vec<&Shadow> = self
|
||||||
.nested_shadows
|
.nested_shadows
|
||||||
@ -2113,6 +2189,13 @@ impl RenderState {
|
|||||||
self.interactive_target_seeded = false;
|
self.interactive_target_seeded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Viewer fixed-scroll passes reuse the same WASM context; `reset` does not
|
||||||
|
// clear Backbuffer, so pass 2 would otherwise keep pass-1 pixels in regions
|
||||||
|
// that render no shapes for the current mask. Target is cleared in present_frame.
|
||||||
|
if self.viewer_masked_pass() {
|
||||||
|
self.reset_viewer_masked_surfaces();
|
||||||
|
}
|
||||||
|
|
||||||
let surface_ids = SurfaceId::Strokes as u32
|
let surface_ids = SurfaceId::Strokes as u32
|
||||||
| SurfaceId::Fills as u32
|
| SurfaceId::Fills as u32
|
||||||
| SurfaceId::InnerShadows as u32
|
| SurfaceId::InnerShadows as u32
|
||||||
@ -3146,6 +3229,33 @@ impl RenderState {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !self.shape_visible_for_viewer_layer(&node_id, tree) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ancestors needed to reach whitelisted descendants: traverse only.
|
||||||
|
if self.include_filter.is_some()
|
||||||
|
&& self.shape_visible_for_viewer_layer(&node_id, tree)
|
||||||
|
&& !self.shape_should_paint_for_viewer_layer(&node_id)
|
||||||
|
{
|
||||||
|
if element.is_recursive() {
|
||||||
|
let children_ids: Vec<_> =
|
||||||
|
element.children_ids_iter(false).copied().collect();
|
||||||
|
let children_ids = sort_z_index(tree, element, children_ids);
|
||||||
|
for child_id in children_ids.iter() {
|
||||||
|
self.pending_nodes.push(NodeRenderState {
|
||||||
|
id: *child_id,
|
||||||
|
visited_children: false,
|
||||||
|
clip_bounds: clip_bounds.clone(),
|
||||||
|
visited_mask: false,
|
||||||
|
mask: false,
|
||||||
|
flattened: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// For frames and groups, we must use extrect because they can have nested content
|
// For frames and groups, we must use extrect because they can have nested content
|
||||||
// that extends beyond their selrect. Using selrect for early exit would incorrectly
|
// that extends beyond their selrect. Using selrect for early exit would incorrectly
|
||||||
// skip frames/groups that have nested content in the current tile.
|
// skip frames/groups that have nested content in the current tile.
|
||||||
@ -3428,6 +3538,7 @@ impl RenderState {
|
|||||||
allow_stop: bool,
|
allow_stop: bool,
|
||||||
) -> Result<FrameType> {
|
) -> Result<FrameType> {
|
||||||
let mut should_stop = false;
|
let mut should_stop = false;
|
||||||
|
self.viewer_render_root = base_object.copied();
|
||||||
let root_ids = {
|
let root_ids = {
|
||||||
if let Some(shape_id) = base_object {
|
if let Some(shape_id) = base_object {
|
||||||
vec![*shape_id]
|
vec![*shape_id]
|
||||||
@ -3443,7 +3554,10 @@ impl RenderState {
|
|||||||
if let Some(current_tile) = self.current_tile {
|
if let Some(current_tile) = self.current_tile {
|
||||||
// NOTE: For now we don't need to cover the case where the tile
|
// NOTE: For now we don't need to cover the case where the tile
|
||||||
// is not cached because everything will be handled from draw_atlas.
|
// is not cached because everything will be handled from draw_atlas.
|
||||||
if !self.surfaces.has_cached_tile_surface(current_tile) {
|
// Viewer masked passes (include_filter) must not reuse cached tiles from
|
||||||
|
// a previous pass; otherwise pass-1 pixels can leak into pass 2.
|
||||||
|
if self.viewer_masked_pass() || !self.surfaces.has_cached_tile_surface(current_tile)
|
||||||
|
{
|
||||||
performance::begin_measure!("render_shape_tree::uncached");
|
performance::begin_measure!("render_shape_tree::uncached");
|
||||||
let (is_empty, early_return) = self
|
let (is_empty, early_return) = self
|
||||||
.render_shape_tree_partial_uncached(tree, timestamp, allow_stop, false)?;
|
.render_shape_tree_partial_uncached(tree, timestamp, allow_stop, false)?;
|
||||||
@ -3454,6 +3568,7 @@ impl RenderState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if early_return {
|
if early_return {
|
||||||
|
self.viewer_render_root = None;
|
||||||
return Ok(FrameType::Partial);
|
return Ok(FrameType::Partial);
|
||||||
}
|
}
|
||||||
performance::end_measure!("render_shape_tree::uncached");
|
performance::end_measure!("render_shape_tree::uncached");
|
||||||
@ -3504,12 +3619,15 @@ impl RenderState {
|
|||||||
// empty tile.
|
// empty tile.
|
||||||
self.current_tile_had_shapes = false;
|
self.current_tile_had_shapes = false;
|
||||||
|
|
||||||
|
let viewer_masked_pass = self.viewer_masked_pass();
|
||||||
|
|
||||||
let Some(ids) = self.tiles.get_shapes_at(next_tile) else {
|
let Some(ids) = self.tiles.get_shapes_at(next_tile) else {
|
||||||
// If the tile is empty we do not need to render it.
|
// If the tile is empty we do not need to render it.
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
if self.surfaces.has_cached_tile_surface(next_tile) {
|
// Never skip based on cached surfaces during viewer masked passes.
|
||||||
|
if !viewer_masked_pass && self.surfaces.has_cached_tile_surface(next_tile) {
|
||||||
// If the tile is cached, then we do not need to
|
// If the tile is cached, then we do not need to
|
||||||
// render it.
|
// render it.
|
||||||
continue;
|
continue;
|
||||||
@ -3563,6 +3681,8 @@ impl RenderState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.viewer_render_root = None;
|
||||||
|
|
||||||
// Mark cache as valid for render_from_cache.
|
// Mark cache as valid for render_from_cache.
|
||||||
// Only update for full-quality renders (non-fast mode).
|
// Only update for full-quality renders (non-fast mode).
|
||||||
// An async render can complete while fast mode is active
|
// An async render can complete while fast mode is active
|
||||||
|
|||||||
@ -864,6 +864,30 @@ impl Surfaces {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Replace `Target` pixels with `Backbuffer` (Src blend).
|
||||||
|
///
|
||||||
|
/// Used for viewer masked passes: transparent backbuffer regions must not
|
||||||
|
/// preserve prior `Target` content from an earlier pass.
|
||||||
|
pub fn copy_backbuffer_to_target_replace(&mut self) {
|
||||||
|
let sampling_options = self.sampling_options;
|
||||||
|
let mut paint = skia::Paint::default();
|
||||||
|
paint.set_blend_mode(skia::BlendMode::Src);
|
||||||
|
self.backbuffer.draw(
|
||||||
|
self.target.canvas(),
|
||||||
|
(0.0, 0.0),
|
||||||
|
sampling_options,
|
||||||
|
Some(&paint),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_target(&mut self, color: skia::Color) {
|
||||||
|
self.target.canvas().clear(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_tile_atlas(&mut self) {
|
||||||
|
self.tile_atlas.canvas().clear(skia::Color::TRANSPARENT);
|
||||||
|
}
|
||||||
|
|
||||||
/// Seed `Backbuffer` from `Target` (last presented frame).
|
/// Seed `Backbuffer` from `Target` (last presented frame).
|
||||||
pub fn seed_backbuffer_from_target(&mut self) {
|
pub fn seed_backbuffer_from_target(&mut self) {
|
||||||
let sampling_options = self.sampling_options;
|
let sampling_options = self.sampling_options;
|
||||||
@ -1026,6 +1050,17 @@ impl Surfaces {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Full backbuffer clear (viewer layer passes must not reuse prior pass pixels).
|
||||||
|
pub fn clear_backbuffer(&mut self, color: skia::Color) {
|
||||||
|
self.backbuffer.canvas().clear(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_backbuffer_rect(&mut self, rect: skia::Rect, color: skia::Color) {
|
||||||
|
let mut paint = Paint::default();
|
||||||
|
paint.set_color(color);
|
||||||
|
self.backbuffer.canvas().draw_rect(rect, &paint);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn reset(&mut self, color: skia::Color) {
|
pub fn reset(&mut self, color: skia::Color) {
|
||||||
self.canvas(SurfaceId::Fills).restore_to_count(1);
|
self.canvas(SurfaceId::Fills).restore_to_count(1);
|
||||||
self.canvas(SurfaceId::InnerShadows).restore_to_count(1);
|
self.canvas(SurfaceId::InnerShadows).restore_to_count(1);
|
||||||
|
|||||||
@ -118,6 +118,14 @@ impl State {
|
|||||||
get_render_state().set_focus_mode(shapes);
|
get_render_state().set_focus_mode(shapes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn clear_include_filter(&mut self) {
|
||||||
|
get_render_state().clear_include_filter();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_include_filter(&mut self, shapes: Vec<Uuid>) {
|
||||||
|
get_render_state().set_include_filter(shapes);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn init_shapes_pool(&mut self, capacity: usize) {
|
pub fn init_shapes_pool(&mut self, capacity: usize) {
|
||||||
self.shapes.initialize(capacity);
|
self.shapes.initialize(capacity);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user