mirror of
https://github.com/penpot/penpot.git
synced 2026-06-09 17:02:05 +00:00
🎉 Basic viewer with wasm
This commit is contained in:
parent
f9f4d7e2cd
commit
9f5e89d5f8
@ -169,6 +169,7 @@
|
||||
|
||||
:mcp
|
||||
:background-blur
|
||||
:available-viewer-wasm
|
||||
:stroke-path})
|
||||
|
||||
(def all-flags
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
[app.common.types.shape-tree :as ctt]
|
||||
[app.common.types.shape.interactions :as ctsi]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.data.comments :as dcmt]
|
||||
[app.main.data.common :as dcm]
|
||||
[app.main.data.event :as ev]
|
||||
@ -219,7 +220,8 @@
|
||||
(ptk/reify ::update-page-position-data
|
||||
ptk/WatchEvent
|
||||
(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)
|
||||
|
||||
shapes
|
||||
|
||||
@ -30,6 +30,7 @@
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
[app.config :as cfg]
|
||||
[app.main.fonts :as fonts]
|
||||
[app.main.render-viewer-wasm :as rwv]
|
||||
[app.main.ui.context :as muc]
|
||||
[app.main.ui.shapes.bool :as bool]
|
||||
[app.main.ui.shapes.circle :as circle]
|
||||
@ -524,32 +525,6 @@
|
||||
(for [shape shapes]
|
||||
[:& 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/wrap [mf/memo]}
|
||||
[{:keys [objects object-id skip-children scale on-render] :as props}]
|
||||
@ -574,7 +549,7 @@
|
||||
(p/fmap
|
||||
(fn [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
|
||||
: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.svg-raw :as svg-raw]
|
||||
[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.object :as obj]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
@ -9,53 +9,26 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[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.types.page :as ctp]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.data.comments :as dcm]
|
||||
[app.main.data.viewer :as dv]
|
||||
[app.main.features :as features]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.hooks :as h]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[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.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[goog.events :as events]
|
||||
[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/wrap [mf/memo]
|
||||
::mf/wrap-props false}
|
||||
@ -74,7 +47,7 @@
|
||||
objects (:objects page)
|
||||
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
|
||||
(->> (remove (set fixed-ids) (keys objects))
|
||||
@ -86,7 +59,7 @@
|
||||
(map (d/getf objects))
|
||||
(concat [frame])
|
||||
(d/index-by :id)
|
||||
(prepare-objects frame size delta)))
|
||||
(vpc/prepare-objects frame size delta)))
|
||||
|
||||
objects-fixed
|
||||
(mf/with-memo [fixed-ids page frame size delta]
|
||||
@ -175,7 +148,10 @@
|
||||
page (unchecked-get props "page")
|
||||
frame (unchecked-get props "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]
|
||||
(let [on-click
|
||||
@ -210,13 +186,21 @@
|
||||
(events/unlistenByKey key2)
|
||||
(events/unlistenByKey key3))))
|
||||
|
||||
[:& viewport-svg {:page page
|
||||
:frame frame
|
||||
:base base
|
||||
:offset offset
|
||||
:size size
|
||||
:delta delta
|
||||
:fixed? fixed?}]))
|
||||
(if ^boolean render-wasm?
|
||||
[:& viewport.wasm/viewport-wasm {:page page
|
||||
:frame frame
|
||||
:base base
|
||||
:offset offset
|
||||
:size size
|
||||
: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/wrap [mf/memo]}
|
||||
|
||||
@ -294,6 +294,80 @@
|
||||
:pointer-events "none"
|
||||
: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
|
||||
|
||||
|
||||
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]
|
||||
(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
|
||||
[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")
|
||||
(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
|
||||
[entries]
|
||||
(when-not ^boolean (empty? entries)
|
||||
@ -1865,6 +1893,24 @@
|
||||
[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
|
||||
[]
|
||||
(cond-> 0
|
||||
|
||||
@ -151,6 +151,31 @@ pub extern "C" fn render_blurred_snapshot(blur_radius: f32) -> Result<()> {
|
||||
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]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn render_sync() -> Result<()> {
|
||||
|
||||
@ -372,6 +372,10 @@ pub(crate) struct RenderState {
|
||||
pub show_grid: Option<Uuid>,
|
||||
pub rulers: RulerState,
|
||||
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>,
|
||||
/// 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
|
||||
@ -568,6 +572,8 @@ impl RenderState {
|
||||
show_grid: None,
|
||||
rulers: RulerState::default(),
|
||||
focus_mode: FocusMode::new(),
|
||||
include_filter: None,
|
||||
viewer_render_root: None,
|
||||
touched_ids: HashSet::default(),
|
||||
ignore_nested_blurs: 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
|
||||
/// as-is across interactive-transform frames without stale overlay pixels.
|
||||
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() {
|
||||
debug::render(self);
|
||||
}
|
||||
@ -942,6 +955,20 @@ impl RenderState {
|
||||
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();
|
||||
// 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),
|
||||
@ -1043,6 +1070,55 @@ impl RenderState {
|
||||
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>> {
|
||||
let drop_shadows: Vec<&Shadow> = self
|
||||
.nested_shadows
|
||||
@ -2113,6 +2189,13 @@ impl RenderState {
|
||||
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
|
||||
| SurfaceId::Fills as u32
|
||||
| SurfaceId::InnerShadows as u32
|
||||
@ -3146,6 +3229,33 @@ impl RenderState {
|
||||
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
|
||||
// that extends beyond their selrect. Using selrect for early exit would incorrectly
|
||||
// skip frames/groups that have nested content in the current tile.
|
||||
@ -3428,6 +3538,7 @@ impl RenderState {
|
||||
allow_stop: bool,
|
||||
) -> Result<FrameType> {
|
||||
let mut should_stop = false;
|
||||
self.viewer_render_root = base_object.copied();
|
||||
let root_ids = {
|
||||
if let Some(shape_id) = base_object {
|
||||
vec![*shape_id]
|
||||
@ -3443,7 +3554,10 @@ impl RenderState {
|
||||
if let Some(current_tile) = self.current_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.
|
||||
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");
|
||||
let (is_empty, early_return) = self
|
||||
.render_shape_tree_partial_uncached(tree, timestamp, allow_stop, false)?;
|
||||
@ -3454,6 +3568,7 @@ impl RenderState {
|
||||
}
|
||||
|
||||
if early_return {
|
||||
self.viewer_render_root = None;
|
||||
return Ok(FrameType::Partial);
|
||||
}
|
||||
performance::end_measure!("render_shape_tree::uncached");
|
||||
@ -3504,12 +3619,15 @@ impl RenderState {
|
||||
// empty tile.
|
||||
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 {
|
||||
// If the tile is empty we do not need to render it.
|
||||
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
|
||||
// render it.
|
||||
continue;
|
||||
@ -3563,6 +3681,8 @@ impl RenderState {
|
||||
}
|
||||
}
|
||||
|
||||
self.viewer_render_root = None;
|
||||
|
||||
// Mark cache as valid for render_from_cache.
|
||||
// Only update for full-quality renders (non-fast mode).
|
||||
// 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).
|
||||
pub fn seed_backbuffer_from_target(&mut self) {
|
||||
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) {
|
||||
self.canvas(SurfaceId::Fills).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);
|
||||
}
|
||||
|
||||
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) {
|
||||
self.shapes.initialize(capacity);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user