mirror of
https://github.com/penpot/penpot.git
synced 2026-09-13 23:48:42 +00:00
Merge pull request #8980 from penpot/superalex-atlas-2
🎉 Tiles atlas support
This commit is contained in:
commit
b74d920d03
@ -30,6 +30,7 @@
|
|||||||
[app.util.timers :as timers]
|
[app.util.timers :as timers]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[okulary.core :as l]
|
[okulary.core :as l]
|
||||||
|
[promesa.core :as p]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
;; FIXME: can we unify this two refs in one?
|
;; FIXME: can we unify this two refs in one?
|
||||||
@ -72,18 +73,21 @@
|
|||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps id current-page-id)
|
(mf/deps id current-page-id)
|
||||||
(fn []
|
(fn []
|
||||||
;; For the wasm renderer, apply a blur effect to the viewport canvas
|
;; WASM page transitions:
|
||||||
;; when we navigate to a different page.
|
;; - Capture the current page (A) once
|
||||||
|
;; - Show a blurred snapshot while the target page (B/C/...) renders
|
||||||
|
;; - If the user clicks again during the transition, keep showing the original (A) snapshot
|
||||||
(if (and (features/active-feature? @st/state "render-wasm/v1")
|
(if (and (features/active-feature? @st/state "render-wasm/v1")
|
||||||
(not= id current-page-id))
|
(not= id current-page-id))
|
||||||
(do
|
(do
|
||||||
(wasm.api/capture-canvas-pixels)
|
(-> (wasm.api/apply-canvas-blur)
|
||||||
(wasm.api/apply-canvas-blur)
|
(p/finally
|
||||||
;; NOTE: it seems we need two RAF so the blur is actually applied and visible
|
(fn []
|
||||||
;; in the canvas :(
|
;; NOTE: it seems we need two RAF so the blur is actually applied and visible
|
||||||
(timers/raf
|
;; in the canvas :(
|
||||||
(fn []
|
(timers/raf
|
||||||
(timers/raf navigate-fn))))
|
(fn []
|
||||||
|
(timers/raf navigate-fn)))))))
|
||||||
(navigate-fn))))
|
(navigate-fn))))
|
||||||
|
|
||||||
on-delete
|
on-delete
|
||||||
|
|||||||
@ -132,21 +132,21 @@
|
|||||||
(apply-modifiers-to-objects base-objects wasm-modifiers))
|
(apply-modifiers-to-objects base-objects wasm-modifiers))
|
||||||
|
|
||||||
;; STATE
|
;; STATE
|
||||||
alt? (mf/use-state false)
|
alt? (mf/use-state false)
|
||||||
shift? (mf/use-state false)
|
shift? (mf/use-state false)
|
||||||
mod? (mf/use-state false)
|
mod? (mf/use-state false)
|
||||||
space? (mf/use-state false)
|
space? (mf/use-state false)
|
||||||
z? (mf/use-state false)
|
z? (mf/use-state false)
|
||||||
cursor (mf/use-state (utils/get-cursor :pointer-inner))
|
cursor (mf/use-state (utils/get-cursor :pointer-inner))
|
||||||
hover-ids (mf/use-state nil)
|
hover-ids (mf/use-state nil)
|
||||||
hover (mf/use-state nil)
|
hover (mf/use-state nil)
|
||||||
measure-hover (mf/use-state nil)
|
measure-hover (mf/use-state nil)
|
||||||
hover-disabled? (mf/use-state false)
|
hover-disabled? (mf/use-state false)
|
||||||
hover-top-frame-id (mf/use-state nil)
|
hover-top-frame-id (mf/use-state nil)
|
||||||
frame-hover (mf/use-state nil)
|
frame-hover (mf/use-state nil)
|
||||||
active-frames (mf/use-state #{})
|
active-frames (mf/use-state #{})
|
||||||
canvas-init? (mf/use-state false)
|
canvas-init? (mf/use-state false)
|
||||||
initialized? (mf/use-state false)
|
initialized? (mf/use-state false)
|
||||||
|
|
||||||
;; REFS
|
;; REFS
|
||||||
[viewport-ref
|
[viewport-ref
|
||||||
@ -205,6 +205,9 @@
|
|||||||
|
|
||||||
mode-inspect? (= options-mode :inspect)
|
mode-inspect? (= options-mode :inspect)
|
||||||
|
|
||||||
|
;; True when we are opening a new file or switching to a new page
|
||||||
|
page-transition? (mf/deref wasm.api/page-transition?)
|
||||||
|
|
||||||
on-click (actions/on-click hover selected edition path-drawing? drawing-tool space? selrect z?)
|
on-click (actions/on-click hover selected edition path-drawing? drawing-tool space? selrect z?)
|
||||||
on-context-menu (actions/on-context-menu hover hover-ids read-only?)
|
on-context-menu (actions/on-context-menu hover hover-ids read-only?)
|
||||||
on-double-click (actions/on-double-click hover hover-ids hover-top-frame-id path-drawing? base-objects edition drawing-tool z? read-only?)
|
on-double-click (actions/on-double-click hover hover-ids hover-top-frame-id path-drawing? base-objects edition drawing-tool z? read-only?)
|
||||||
@ -234,41 +237,46 @@
|
|||||||
show-cursor-tooltip? tooltip
|
show-cursor-tooltip? tooltip
|
||||||
show-draw-area? drawing-obj
|
show-draw-area? drawing-obj
|
||||||
show-gradient-handlers? (= (count selected) 1)
|
show-gradient-handlers? (= (count selected) 1)
|
||||||
show-grids? (contains? layout :display-guides)
|
show-grids? (and (contains? layout :display-guides) (not page-transition?))
|
||||||
|
|
||||||
show-frame-outline? (and (= transform :move) (not panning))
|
show-frame-outline? (and (= transform :move) (not panning) (not page-transition?))
|
||||||
show-outlines? (and (nil? transform)
|
show-outlines? (and (nil? transform)
|
||||||
(not panning)
|
(not panning)
|
||||||
(not edition)
|
(not edition)
|
||||||
(not drawing-obj)
|
(not drawing-obj)
|
||||||
(not (#{:comments :path :curve} drawing-tool)))
|
(not (#{:comments :path :curve} drawing-tool))
|
||||||
|
(not page-transition?))
|
||||||
|
|
||||||
show-pixel-grid? (and (contains? layout :show-pixel-grid)
|
show-pixel-grid? (and (contains? layout :show-pixel-grid)
|
||||||
(>= zoom 8))
|
(>= zoom 8)
|
||||||
show-text-editor? (and editing-shape (= :text (:type editing-shape)))
|
(not page-transition?))
|
||||||
|
show-text-editor? (and editing-shape (= :text (:type editing-shape)) (not page-transition?))
|
||||||
|
|
||||||
hover-grid? (and (some? @hover-top-frame-id)
|
hover-grid? (and (some? @hover-top-frame-id)
|
||||||
(ctl/grid-layout? objects @hover-top-frame-id))
|
(ctl/grid-layout? objects @hover-top-frame-id)
|
||||||
|
(not page-transition?))
|
||||||
|
|
||||||
show-grid-editor? (and editing-shape (ctl/grid-layout? editing-shape))
|
show-grid-editor? (and editing-shape (ctl/grid-layout? editing-shape) (not page-transition?))
|
||||||
show-presence? page-id
|
show-presence? (and page-id (not page-transition?))
|
||||||
show-prototypes? (= options-mode :prototype)
|
show-prototypes? (and (= options-mode :prototype) (not page-transition?))
|
||||||
show-selection-handlers? (and (seq selected) (not show-text-editor?))
|
show-selection-handlers? (and (seq selected) (not show-text-editor?) (not page-transition?))
|
||||||
show-snap-distance? (and (contains? layout :dynamic-alignment)
|
show-snap-distance? (and (contains? layout :dynamic-alignment)
|
||||||
(= transform :move)
|
(= transform :move)
|
||||||
(seq selected))
|
(seq selected)
|
||||||
|
(not page-transition?))
|
||||||
show-snap-points? (and (or (contains? layout :dynamic-alignment)
|
show-snap-points? (and (or (contains? layout :dynamic-alignment)
|
||||||
(contains? layout :snap-guides))
|
(contains? layout :snap-guides))
|
||||||
(or drawing-obj transform))
|
(or drawing-obj transform)
|
||||||
show-selrect? (and selrect (empty? drawing) (not text-editing?))
|
(not page-transition?))
|
||||||
|
show-selrect? (and selrect (empty? drawing) (not text-editing?) (not page-transition?))
|
||||||
show-measures? (and (not transform)
|
show-measures? (and (not transform)
|
||||||
(not path-editing?)
|
(not path-editing?)
|
||||||
(or show-distances? mode-inspect?))
|
(or show-distances? mode-inspect?)
|
||||||
show-artboard-names? (contains? layout :display-artboard-names)
|
(not page-transition?))
|
||||||
|
show-artboard-names? (and (contains? layout :display-artboard-names) (not page-transition?))
|
||||||
hide-ui? (contains? layout :hide-ui)
|
hide-ui? (contains? layout :hide-ui)
|
||||||
show-rulers? (and (contains? layout :rulers) (not hide-ui?))
|
show-rulers? (and (contains? layout :rulers) (not hide-ui?))
|
||||||
|
|
||||||
|
|
||||||
disabled-guides? (or drawing-tool transform path-drawing? path-editing?)
|
disabled-guides? (or drawing-tool transform path-drawing? path-editing?)
|
||||||
|
|
||||||
single-select? (= (count selected-shapes) 1)
|
single-select? (= (count selected-shapes) 1)
|
||||||
@ -279,6 +287,8 @@
|
|||||||
(or (ctk/is-variant-container? first-shape)
|
(or (ctk/is-variant-container? first-shape)
|
||||||
(ctk/is-variant? first-shape)))
|
(ctk/is-variant? first-shape)))
|
||||||
|
|
||||||
|
show-scrollbar? (not page-transition?)
|
||||||
|
|
||||||
add-variant
|
add-variant
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps first-shape)
|
(mf/deps first-shape)
|
||||||
@ -311,7 +321,8 @@
|
|||||||
rule-area-size (/ rulers/ruler-area-size zoom)
|
rule-area-size (/ rulers/ruler-area-size zoom)
|
||||||
preview-blend (-> refs/workspace-preview-blend
|
preview-blend (-> refs/workspace-preview-blend
|
||||||
(mf/deref))
|
(mf/deref))
|
||||||
shapes-loading? (mf/deref wasm.api/shapes-loading?)]
|
shapes-loading? (mf/deref wasm.api/shapes-loading?)
|
||||||
|
transition-image-url (mf/deref wasm.api/transition-image-url*)]
|
||||||
|
|
||||||
;; NOTE: We need this page-id dependency to react to it and reset the
|
;; NOTE: We need this page-id dependency to react to it and reset the
|
||||||
;; canvas, even though we are not using `page-id` inside the hook.
|
;; canvas, even though we are not using `page-id` inside the hook.
|
||||||
@ -341,15 +352,7 @@
|
|||||||
(cond
|
(cond
|
||||||
init?
|
init?
|
||||||
(do
|
(do
|
||||||
(reset! canvas-init? true)
|
(reset! canvas-init? true))
|
||||||
(wasm.api/apply-canvas-blur)
|
|
||||||
(if (wasm.api/has-captured-pixels?)
|
|
||||||
;; Page switch: restore previously captured pixels (blurred)
|
|
||||||
(wasm.api/restore-previous-canvas-pixels)
|
|
||||||
;; First load: try to draw a blurred page thumbnail
|
|
||||||
(when-let [frame-id (get page :thumbnail-frame-id)]
|
|
||||||
(when-let [uri (dm/get-in @st/state [:thumbnails frame-id])]
|
|
||||||
(wasm.api/draw-thumbnail-to-canvas uri)))))
|
|
||||||
|
|
||||||
(pos? retries)
|
(pos? retries)
|
||||||
(vreset! timeout-id-ref
|
(vreset! timeout-id-ref
|
||||||
@ -392,14 +395,15 @@
|
|||||||
(when @canvas-init?
|
(when @canvas-init?
|
||||||
(if (not @initialized?)
|
(if (not @initialized?)
|
||||||
(do
|
(do
|
||||||
|
;; Initial file open uses the same transition workflow as page switches,
|
||||||
|
;; but with a solid background-color blurred placeholder.
|
||||||
|
(wasm.api/start-initial-load-transition! background)
|
||||||
;; Keep the blurred previous-page preview (page switch) or
|
;; Keep the blurred previous-page preview (page switch) or
|
||||||
;; blank canvas (first load) visible while shapes load.
|
;; blank canvas (first load) visible while shapes load.
|
||||||
;; The loading overlay is suppressed because on-shapes-ready
|
;; The loading overlay is suppressed because on-shapes-ready
|
||||||
;; is set.
|
;; is set.
|
||||||
(wasm.api/initialize-viewport
|
(wasm.api/initialize-viewport
|
||||||
base-objects zoom vbox background 1 nil
|
base-objects zoom vbox background)
|
||||||
(fn []
|
|
||||||
(wasm.api/clear-canvas-pixels)))
|
|
||||||
(reset! initialized? true)
|
(reset! initialized? true)
|
||||||
(mf/set-ref-val! last-file-version-id-ref file-version-id))
|
(mf/set-ref-val! last-file-version-id-ref file-version-id))
|
||||||
(when (and (some? file-version-id)
|
(when (and (some? file-version-id)
|
||||||
@ -476,6 +480,21 @@
|
|||||||
:style {:background-color background
|
:style {:background-color background
|
||||||
:pointer-events "none"}}]
|
:pointer-events "none"}}]
|
||||||
|
|
||||||
|
;; Show the transition image when we are opening a new file or switching to a new page
|
||||||
|
(when (and page-transition? (some? transition-image-url))
|
||||||
|
(let [src transition-image-url]
|
||||||
|
[:img {:data-testid "canvas-wasm-transition"
|
||||||
|
:src src
|
||||||
|
:draggable false
|
||||||
|
:style {:position "absolute"
|
||||||
|
:inset 0
|
||||||
|
:width "100%"
|
||||||
|
:height "100%"
|
||||||
|
:object-fit "cover"
|
||||||
|
:pointer-events "none"
|
||||||
|
:filter "blur(4px)"}}]))
|
||||||
|
|
||||||
|
|
||||||
[:svg.viewport-controls
|
[:svg.viewport-controls
|
||||||
{:xmlns "http://www.w3.org/2000/svg"
|
{:xmlns "http://www.w3.org/2000/svg"
|
||||||
:xmlnsXlink "http://www.w3.org/1999/xlink"
|
:xmlnsXlink "http://www.w3.org/1999/xlink"
|
||||||
@ -776,9 +795,10 @@
|
|||||||
(get objects-modified @hover-top-frame-id))
|
(get objects-modified @hover-top-frame-id))
|
||||||
:view-only (not show-grid-editor?)}])]
|
:view-only (not show-grid-editor?)}])]
|
||||||
|
|
||||||
[:g.scrollbar-wrapper {:clipPath "url(#clip-handlers)"}
|
(when show-scrollbar?
|
||||||
[:> scroll-bars/viewport-scrollbars*
|
[:g.scrollbar-wrapper {:clipPath "url(#clip-handlers)"}
|
||||||
{:objects base-objects
|
[:> scroll-bars/viewport-scrollbars*
|
||||||
:zoom zoom
|
{:objects base-objects
|
||||||
:vbox vbox
|
:zoom zoom
|
||||||
:bottom-padding (when palete-size (+ palete-size 8))}]]]]]))
|
:vbox vbox
|
||||||
|
:bottom-padding (when palete-size (+ palete-size 8))}]])]]]))
|
||||||
|
|||||||
@ -55,6 +55,109 @@
|
|||||||
|
|
||||||
(def use-dpr? (contains? cf/flags :render-wasm-dpr))
|
(def use-dpr? (contains? cf/flags :render-wasm-dpr))
|
||||||
|
|
||||||
|
;; --- Page transition state (WASM viewport)
|
||||||
|
;;
|
||||||
|
;; Goal: avoid showing tile-by-tile rendering during page switches (and initial load),
|
||||||
|
;; by keeping a blurred snapshot overlay visible until WASM dispatches
|
||||||
|
;; `penpot:wasm:tiles-complete`.
|
||||||
|
;;
|
||||||
|
;; - `page-transition?`: true while the overlay should be considered active.
|
||||||
|
;; - `transition-image-url*`: URL used by the UI overlay (usually `blob:` from the
|
||||||
|
;; current WebGL canvas snapshot; on initial load it may be a tiny SVG data-url
|
||||||
|
;; derived from the page background color).
|
||||||
|
;; - `transition-epoch*`: monotonic counter used to ignore stale async work/events
|
||||||
|
;; when the user clicks pages rapidly (A -> B -> C).
|
||||||
|
;; - `transition-tiles-handler*`: the currently installed DOM event handler for
|
||||||
|
;; `penpot:wasm:tiles-complete`, so we can remove/replace it safely.
|
||||||
|
(defonce page-transition? (atom false))
|
||||||
|
(defonce transition-image-url* (atom nil))
|
||||||
|
(defonce transition-epoch* (atom 0))
|
||||||
|
(defonce transition-tiles-handler* (atom nil))
|
||||||
|
|
||||||
|
(def ^:private transition-blur-css "blur(4px)")
|
||||||
|
|
||||||
|
(defn- set-transition-blur!
|
||||||
|
[]
|
||||||
|
(when-let [canvas ^js wasm/canvas]
|
||||||
|
(dom/set-style! canvas "filter" transition-blur-css))
|
||||||
|
(when-let [nodes (.querySelectorAll ^js ug/document ".blurrable")]
|
||||||
|
(doseq [^js node (array-seq nodes)]
|
||||||
|
(dom/set-style! node "filter" transition-blur-css))))
|
||||||
|
|
||||||
|
(defn- clear-transition-blur!
|
||||||
|
[]
|
||||||
|
(when-let [canvas ^js wasm/canvas]
|
||||||
|
(dom/set-style! canvas "filter" ""))
|
||||||
|
(when-let [nodes (.querySelectorAll ^js ug/document ".blurrable")]
|
||||||
|
(doseq [^js node (array-seq nodes)]
|
||||||
|
(dom/set-style! node "filter" ""))))
|
||||||
|
|
||||||
|
(defn set-transition-image-from-background!
|
||||||
|
"Sets `transition-image-url*` to a data URL representing a solid background color."
|
||||||
|
[background]
|
||||||
|
(when (string? background)
|
||||||
|
(let [svg (str "<svg xmlns='http://www.w3.org/2000/svg' width='1' height='1'>"
|
||||||
|
"<rect width='1' height='1' fill='" background "'/>"
|
||||||
|
"</svg>")]
|
||||||
|
(reset! transition-image-url*
|
||||||
|
(str "data:image/svg+xml;charset=utf-8," (js/encodeURIComponent svg))))))
|
||||||
|
|
||||||
|
(defn begin-page-transition!
|
||||||
|
[]
|
||||||
|
(reset! page-transition? true)
|
||||||
|
(swap! transition-epoch* inc))
|
||||||
|
|
||||||
|
(defn end-page-transition!
|
||||||
|
[]
|
||||||
|
(reset! page-transition? false)
|
||||||
|
(when-let [prev @transition-tiles-handler*]
|
||||||
|
(.removeEventListener ^js ug/document "penpot:wasm:tiles-complete" prev))
|
||||||
|
(reset! transition-tiles-handler* nil)
|
||||||
|
(reset! transition-image-url* nil)
|
||||||
|
(clear-transition-blur!)
|
||||||
|
;; Clear captured pixels so future transitions must explicitly capture again.
|
||||||
|
(set! wasm/canvas-snapshot-url nil))
|
||||||
|
|
||||||
|
(defn- set-transition-tiles-complete-handler!
|
||||||
|
"Installs a tiles-complete handler bound to the current transition epoch.
|
||||||
|
Replaces any previous handler so rapid page switching doesn't end the wrong transition."
|
||||||
|
[epoch f]
|
||||||
|
(when-let [prev @transition-tiles-handler*]
|
||||||
|
(.removeEventListener ^js ug/document "penpot:wasm:tiles-complete" prev))
|
||||||
|
(letfn [(handler [_]
|
||||||
|
(when (= epoch @transition-epoch*)
|
||||||
|
(.removeEventListener ^js ug/document "penpot:wasm:tiles-complete" handler)
|
||||||
|
(reset! transition-tiles-handler* nil)
|
||||||
|
(f)))]
|
||||||
|
(reset! transition-tiles-handler* handler)
|
||||||
|
(.addEventListener ^js ug/document "penpot:wasm:tiles-complete" handler)))
|
||||||
|
|
||||||
|
(defn start-initial-load-transition!
|
||||||
|
"Starts a page-transition workflow for initial file open.
|
||||||
|
|
||||||
|
- Sets `page-transition?` to true
|
||||||
|
- Installs a tiles-complete handler to end the transition
|
||||||
|
- Uses a solid background-color placeholder as the transition image"
|
||||||
|
[background]
|
||||||
|
;; If something already toggled `page-transition?` (e.g. legacy init code paths),
|
||||||
|
;; ensure we still have a deterministic placeholder on initial load.
|
||||||
|
(when (or (not @page-transition?) (nil? @transition-image-url*))
|
||||||
|
(set-transition-image-from-background! background))
|
||||||
|
(when-not @page-transition?
|
||||||
|
;; Start transition + bind the tiles-complete handler to this epoch.
|
||||||
|
(let [epoch (begin-page-transition!)]
|
||||||
|
(set-transition-tiles-complete-handler! epoch end-page-transition!))))
|
||||||
|
|
||||||
|
(defn listen-tiles-render-complete-once!
|
||||||
|
"Registers a one-shot listener for `penpot:wasm:tiles-complete`, dispatched from WASM
|
||||||
|
when a full tile pass finishes."
|
||||||
|
[f]
|
||||||
|
(.addEventListener ^js ug/document
|
||||||
|
"penpot:wasm:tiles-complete"
|
||||||
|
(fn [_]
|
||||||
|
(f))
|
||||||
|
#js {:once true}))
|
||||||
|
|
||||||
(defn text-editor-wasm?
|
(defn text-editor-wasm?
|
||||||
[]
|
[]
|
||||||
(or (contains? cf/flags :feature-text-editor-wasm)
|
(or (contains? cf/flags :feature-text-editor-wasm)
|
||||||
@ -94,16 +197,9 @@
|
|||||||
(def ^:const TEXT_EDITOR_EVENT_NEEDS_LAYOUT 4)
|
(def ^:const TEXT_EDITOR_EVENT_NEEDS_LAYOUT 4)
|
||||||
|
|
||||||
;; Re-export public WebGL functions
|
;; Re-export public WebGL functions
|
||||||
(def capture-canvas-pixels webgl/capture-canvas-pixels)
|
(def capture-canvas-snapshot-url webgl/capture-canvas-snapshot-url)
|
||||||
(def restore-previous-canvas-pixels webgl/restore-previous-canvas-pixels)
|
|
||||||
(def clear-canvas-pixels webgl/clear-canvas-pixels)
|
|
||||||
(def draw-thumbnail-to-canvas webgl/draw-thumbnail-to-canvas)
|
(def draw-thumbnail-to-canvas webgl/draw-thumbnail-to-canvas)
|
||||||
|
|
||||||
(defn has-captured-pixels?
|
|
||||||
"Returns true if there are saved canvas pixels from a previous page."
|
|
||||||
[]
|
|
||||||
(some? wasm/canvas-pixels))
|
|
||||||
|
|
||||||
;; Re-export public text editor functions
|
;; Re-export public text editor functions
|
||||||
(def text-editor-focus text-editor/text-editor-focus)
|
(def text-editor-focus text-editor/text-editor-focus)
|
||||||
(def text-editor-blur text-editor/text-editor-blur)
|
(def text-editor-blur text-editor/text-editor-blur)
|
||||||
@ -1539,6 +1635,8 @@
|
|||||||
(h/call wasm/internal-module "_set_render_options" flags dpr)
|
(h/call wasm/internal-module "_set_render_options" flags dpr)
|
||||||
(when-let [t (wasm-aa-threshold-from-route-params)]
|
(when-let [t (wasm-aa-threshold-from-route-params)]
|
||||||
(h/call wasm/internal-module "_set_antialias_threshold" t))
|
(h/call wasm/internal-module "_set_antialias_threshold" t))
|
||||||
|
(when-let [max-tex (webgl/max-texture-size context)]
|
||||||
|
(h/call wasm/internal-module "_set_max_atlas_texture_size" max-tex))
|
||||||
|
|
||||||
;; Set browser and canvas size only after initialization
|
;; Set browser and canvas size only after initialization
|
||||||
(h/call wasm/internal-module "_set_browser" browser)
|
(h/call wasm/internal-module "_set_browser" browser)
|
||||||
@ -1776,9 +1874,36 @@
|
|||||||
|
|
||||||
(defn apply-canvas-blur
|
(defn apply-canvas-blur
|
||||||
[]
|
[]
|
||||||
(when wasm/canvas (dom/set-style! wasm/canvas "filter" "blur(4px)"))
|
(let [already? @page-transition?
|
||||||
(let [controls-to-blur (dom/query-all (dom/get-element "viewport-controls") ".blurrable")]
|
epoch (begin-page-transition!)]
|
||||||
(run! #(dom/set-style! % "filter" "blur(4px)") controls-to-blur)))
|
(set-transition-tiles-complete-handler! epoch end-page-transition!)
|
||||||
|
;; Two-phase transition:
|
||||||
|
;; - Apply CSS blur to the live canvas immediately (no async wait), so the user
|
||||||
|
;; sees the transition right away.
|
||||||
|
;; - In parallel, capture a `blob:` snapshot URL; once ready, switch the overlay
|
||||||
|
;; to that fixed image (and guard with `epoch` to avoid stale async updates).
|
||||||
|
(set-transition-blur!)
|
||||||
|
;; Lock the snapshot for the whole transition: if the user clicks to another page
|
||||||
|
;; while the transition is active, keep showing the original page snapshot until
|
||||||
|
;; the final target page finishes rendering.
|
||||||
|
(if already?
|
||||||
|
(p/resolved nil)
|
||||||
|
(do
|
||||||
|
;; If we already have a snapshot URL, use it immediately.
|
||||||
|
(when-let [url wasm/canvas-snapshot-url]
|
||||||
|
(when (string? url)
|
||||||
|
(reset! transition-image-url* url)))
|
||||||
|
|
||||||
|
;; Capture a fresh snapshot asynchronously and update the overlay as soon
|
||||||
|
;; as it is ready (guarded by `epoch` to avoid stale async updates).
|
||||||
|
(-> (capture-canvas-snapshot-url)
|
||||||
|
(p/then (fn [url]
|
||||||
|
(when (and (string? url)
|
||||||
|
@page-transition?
|
||||||
|
(= epoch @transition-epoch*))
|
||||||
|
(reset! transition-image-url* url))
|
||||||
|
url))
|
||||||
|
(p/catch (fn [_] nil)))))))
|
||||||
|
|
||||||
(defn render-shape-pixels
|
(defn render-shape-pixels
|
||||||
[shape-id scale]
|
[shape-id scale]
|
||||||
|
|||||||
@ -9,9 +9,17 @@
|
|||||||
(:require
|
(:require
|
||||||
[app.common.logging :as log]
|
[app.common.logging :as log]
|
||||||
[app.render-wasm.wasm :as wasm]
|
[app.render-wasm.wasm :as wasm]
|
||||||
[app.util.dom :as dom]
|
|
||||||
[promesa.core :as p]))
|
[promesa.core :as p]))
|
||||||
|
|
||||||
|
(defn max-texture-size
|
||||||
|
"Returns `gl.MAX_TEXTURE_SIZE` (max dimension of a 2D texture), or nil if
|
||||||
|
unavailable."
|
||||||
|
[gl]
|
||||||
|
(when gl
|
||||||
|
(let [n (.getParameter ^js gl (.-MAX_TEXTURE_SIZE ^js gl))]
|
||||||
|
(when (and (number? n) (pos? n) (js/isFinite n))
|
||||||
|
(js/Math.floor n)))))
|
||||||
|
|
||||||
(defn get-webgl-context
|
(defn get-webgl-context
|
||||||
"Gets the WebGL context from the WASM module"
|
"Gets the WebGL context from the WASM module"
|
||||||
[]
|
[]
|
||||||
@ -135,38 +143,29 @@ void main() {
|
|||||||
(.bindTexture ^js gl (.-TEXTURE_2D ^js gl) nil)
|
(.bindTexture ^js gl (.-TEXTURE_2D ^js gl) nil)
|
||||||
(.deleteTexture ^js gl texture))))
|
(.deleteTexture ^js gl texture))))
|
||||||
|
|
||||||
(defn restore-previous-canvas-pixels
|
(defn capture-canvas-snapshot-url
|
||||||
"Restores previous canvas pixels into the new canvas"
|
"Captures the current viewport canvas as a PNG `blob:` URL and stores it in
|
||||||
[]
|
`wasm/canvas-snapshot-url`.
|
||||||
(when-let [previous-canvas-pixels wasm/canvas-pixels]
|
|
||||||
(when-let [gl wasm/gl-context]
|
|
||||||
(draw-imagedata-to-webgl gl previous-canvas-pixels)
|
|
||||||
(set! wasm/canvas-pixels nil))))
|
|
||||||
|
|
||||||
(defn clear-canvas-pixels
|
Returns a promise resolving to the URL string (or nil)."
|
||||||
[]
|
[]
|
||||||
(when wasm/canvas
|
(if-let [^js canvas wasm/canvas]
|
||||||
(let [context wasm/gl-context]
|
(p/create
|
||||||
(.clearColor ^js context 0 0 0 0.0)
|
(fn [resolve _reject]
|
||||||
(.clear ^js context (.-COLOR_BUFFER_BIT ^js context))
|
;; Revoke previous snapshot to avoid leaking blob URLs.
|
||||||
(.clear ^js context (.-DEPTH_BUFFER_BIT ^js context))
|
(when-let [prev wasm/canvas-snapshot-url]
|
||||||
(.clear ^js context (.-STENCIL_BUFFER_BIT ^js context)))
|
(when (and (string? prev) (.startsWith ^js prev "blob:"))
|
||||||
(dom/set-style! wasm/canvas "filter" "none")
|
(js/URL.revokeObjectURL prev)))
|
||||||
(let [controls-to-unblur (dom/query-all (dom/get-element "viewport-controls") ".blurrable")]
|
(set! wasm/canvas-snapshot-url nil)
|
||||||
(run! #(dom/set-style! % "filter" "none") controls-to-unblur))
|
(.toBlob canvas
|
||||||
(set! wasm/canvas-pixels nil)))
|
(fn [^js blob]
|
||||||
|
(if blob
|
||||||
(defn capture-canvas-pixels
|
(let [url (js/URL.createObjectURL blob)]
|
||||||
"Captures the pixels of the viewport canvas"
|
(set! wasm/canvas-snapshot-url url)
|
||||||
[]
|
(resolve url))
|
||||||
(when wasm/canvas
|
(resolve nil)))
|
||||||
(let [context wasm/gl-context
|
"image/png")))
|
||||||
width (.-width wasm/canvas)
|
(p/resolved nil)))
|
||||||
height (.-height wasm/canvas)
|
|
||||||
buffer (js/Uint8ClampedArray. (* width height 4))
|
|
||||||
_ (.readPixels ^js context 0 0 width height (.-RGBA ^js context) (.-UNSIGNED_BYTE ^js context) buffer)
|
|
||||||
image-data (js/ImageData. buffer width height)]
|
|
||||||
(set! wasm/canvas-pixels image-data))))
|
|
||||||
|
|
||||||
(defn draw-thumbnail-to-canvas
|
(defn draw-thumbnail-to-canvas
|
||||||
"Loads an image from `uri` and draws it stretched to fill the WebGL canvas.
|
"Loads an image from `uri` and draws it stretched to fill the WebGL canvas.
|
||||||
|
|||||||
@ -12,8 +12,9 @@
|
|||||||
|
|
||||||
;; Reference to the HTML canvas element.
|
;; Reference to the HTML canvas element.
|
||||||
(defonce canvas nil)
|
(defonce canvas nil)
|
||||||
;; Reference to the captured pixels of the canvas (for page switching effect)
|
;; Snapshot of the current canvas suitable for `<img src=...>` overlays.
|
||||||
(defonce canvas-pixels nil)
|
;; This is typically a `blob:` URL created via `canvas.toBlob`.
|
||||||
|
(defonce canvas-snapshot-url nil)
|
||||||
|
|
||||||
;; Reference to the Emscripten GL context wrapper.
|
;; Reference to the Emscripten GL context wrapper.
|
||||||
(defonce gl-context-handle nil)
|
(defonce gl-context-handle nil)
|
||||||
|
|||||||
@ -135,6 +135,28 @@
|
|||||||
(wasm.mem/free)
|
(wasm.mem/free)
|
||||||
text)))
|
text)))
|
||||||
|
|
||||||
|
(defn ^:export wasmAtlasConsole
|
||||||
|
"Logs the current render-wasm atlas as an image in the JS console (if present)."
|
||||||
|
[]
|
||||||
|
(let [module wasm/internal-module
|
||||||
|
f (when module (unchecked-get module "_debug_atlas_console"))]
|
||||||
|
(if (fn? f)
|
||||||
|
(wasm.h/call module "_debug_atlas_console")
|
||||||
|
(js/console.warn "[debug] render-wasm module not ready or missing _debug_atlas_console"))))
|
||||||
|
|
||||||
|
(defn ^:export wasmAtlasBase64
|
||||||
|
"Returns the atlas PNG base64 (empty string if missing/empty)."
|
||||||
|
[]
|
||||||
|
(let [module wasm/internal-module
|
||||||
|
f (when module (unchecked-get module "_debug_atlas_base64"))]
|
||||||
|
(if (fn? f)
|
||||||
|
(let [ptr (wasm.h/call module "_debug_atlas_base64")
|
||||||
|
s (or (wasm-read-len-prefixed-utf8 ptr) "")]
|
||||||
|
s)
|
||||||
|
(do
|
||||||
|
(js/console.warn "[debug] render-wasm module not ready or missing _debug_atlas_base64")
|
||||||
|
""))))
|
||||||
|
|
||||||
(defn ^:export wasmCacheConsole
|
(defn ^:export wasmCacheConsole
|
||||||
"Logs the current render-wasm cache surface as an image in the JS console."
|
"Logs the current render-wasm cache surface as an image in the JS console."
|
||||||
[]
|
[]
|
||||||
|
|||||||
@ -154,6 +154,18 @@ pub extern "C" fn set_antialias_threshold(threshold: f32) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
#[wasm_error]
|
||||||
|
pub extern "C" fn set_max_atlas_texture_size(max_px: i32) -> Result<()> {
|
||||||
|
with_state_mut!(state, {
|
||||||
|
state
|
||||||
|
.render_state_mut()
|
||||||
|
.surfaces
|
||||||
|
.set_max_atlas_texture_size(max_px);
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
#[wasm_error]
|
#[wasm_error]
|
||||||
pub extern "C" fn set_canvas_background(raw_color: u32) -> Result<()> {
|
pub extern "C" fn set_canvas_background(raw_color: u32) -> Result<()> {
|
||||||
|
|||||||
@ -43,6 +43,13 @@ const VIEWPORT_INTEREST_AREA_THRESHOLD: i32 = 3;
|
|||||||
|
|
||||||
const MAX_BLOCKING_TIME_MS: i32 = 32;
|
const MAX_BLOCKING_TIME_MS: i32 = 32;
|
||||||
const NODE_BATCH_THRESHOLD: i32 = 3;
|
const NODE_BATCH_THRESHOLD: i32 = 3;
|
||||||
|
|
||||||
|
/// Dispatches `penpot:wasm:tiles-complete` on `document` so the UI can react when a full
|
||||||
|
/// tile pass has finished (e.g. remove page-transition blur).
|
||||||
|
fn notify_tiles_render_complete() {
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
crate::run_script!("document.dispatchEvent(new CustomEvent('penpot:wasm:tiles-complete'))");
|
||||||
|
}
|
||||||
const BLUR_DOWNSCALE_THRESHOLD: f32 = 8.0;
|
const BLUR_DOWNSCALE_THRESHOLD: f32 = 8.0;
|
||||||
|
|
||||||
type ClipStack = Vec<(Rect, Option<Corners>, Matrix)>;
|
type ClipStack = Vec<(Rect, Option<Corners>, Matrix)>;
|
||||||
@ -715,12 +722,14 @@ impl RenderState {
|
|||||||
// In fast mode the viewport is moving (pan/zoom) so Cache surface
|
// In fast mode the viewport is moving (pan/zoom) so Cache surface
|
||||||
// positions would be wrong — only save to the tile HashMap.
|
// positions would be wrong — only save to the tile HashMap.
|
||||||
self.surfaces.cache_current_tile_texture(
|
self.surfaces.cache_current_tile_texture(
|
||||||
|
&mut self.gpu_state,
|
||||||
&self.tile_viewbox,
|
&self.tile_viewbox,
|
||||||
&self
|
&self
|
||||||
.current_tile
|
.current_tile
|
||||||
.ok_or(Error::CriticalError("Current tile not found".to_string()))?,
|
.ok_or(Error::CriticalError("Current tile not found".to_string()))?,
|
||||||
&tile_rect,
|
&tile_rect,
|
||||||
fast_mode,
|
fast_mode,
|
||||||
|
self.render_area,
|
||||||
);
|
);
|
||||||
|
|
||||||
self.surfaces.draw_cached_tile_surface(
|
self.surfaces.draw_cached_tile_surface(
|
||||||
@ -1464,6 +1473,28 @@ impl RenderState {
|
|||||||
performance::begin_measure!("render_from_cache");
|
performance::begin_measure!("render_from_cache");
|
||||||
let cached_scale = self.get_cached_scale();
|
let cached_scale = self.get_cached_scale();
|
||||||
|
|
||||||
|
let bg_color = self.background_color;
|
||||||
|
|
||||||
|
// During fast mode (pan/zoom), if a previous full-quality render still has pending tiles,
|
||||||
|
// always prefer the persistent atlas. The atlas is incrementally updated as tiles finish,
|
||||||
|
// and drawing from it avoids mixing a partially-updated Cache surface with missing tiles.
|
||||||
|
if self.options.is_fast_mode() && self.render_in_progress && self.surfaces.has_atlas() {
|
||||||
|
self.surfaces
|
||||||
|
.draw_atlas_to_target(self.viewbox, self.options.dpr(), bg_color);
|
||||||
|
|
||||||
|
if self.options.is_debug_visible() {
|
||||||
|
debug::render(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui::render(self, shapes);
|
||||||
|
debug::render_wasm_label(self);
|
||||||
|
|
||||||
|
self.flush_and_submit();
|
||||||
|
performance::end_measure!("render_from_cache");
|
||||||
|
performance::end_timed_log!("render_from_cache", _start);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if we have a valid cached viewbox (non-zero dimensions indicate valid cache)
|
// Check if we have a valid cached viewbox (non-zero dimensions indicate valid cache)
|
||||||
if self.cached_viewbox.area.width() > 0.0 {
|
if self.cached_viewbox.area.width() > 0.0 {
|
||||||
// Scale and translate the target according to the cached data
|
// Scale and translate the target according to the cached data
|
||||||
@ -1480,7 +1511,62 @@ impl RenderState {
|
|||||||
let offset_y = self.viewbox.area.top * self.cached_viewbox.zoom * self.options.dpr();
|
let offset_y = self.viewbox.area.top * self.cached_viewbox.zoom * self.options.dpr();
|
||||||
let translate_x = (start_tile_x as f32 * tiles::TILE_SIZE) - offset_x;
|
let translate_x = (start_tile_x as f32 * tiles::TILE_SIZE) - offset_x;
|
||||||
let translate_y = (start_tile_y as f32 * tiles::TILE_SIZE) - offset_y;
|
let translate_y = (start_tile_y as f32 * tiles::TILE_SIZE) - offset_y;
|
||||||
let bg_color = self.background_color;
|
|
||||||
|
// For zoom-out, prefer cache only if it fully covers the viewport.
|
||||||
|
// Otherwise, atlas will provide a more correct full-viewport preview.
|
||||||
|
let zooming_out = self.viewbox.zoom < self.cached_viewbox.zoom;
|
||||||
|
if zooming_out {
|
||||||
|
let cache_dim = self.surfaces.cache_dimensions();
|
||||||
|
let cache_w = cache_dim.width as f32;
|
||||||
|
let cache_h = cache_dim.height as f32;
|
||||||
|
|
||||||
|
// Viewport in target pixels.
|
||||||
|
let vw = (self.viewbox.width * self.options.dpr()).max(1.0);
|
||||||
|
let vh = (self.viewbox.height * self.options.dpr()).max(1.0);
|
||||||
|
|
||||||
|
// Inverse-map viewport corners into cache coordinates.
|
||||||
|
// target = (cache * navigate_zoom) translated by (translate_x, translate_y) (in cache coords).
|
||||||
|
// => cache = (target / navigate_zoom) - translate
|
||||||
|
let inv = if navigate_zoom.abs() > f32::EPSILON {
|
||||||
|
1.0 / navigate_zoom
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
let cx0 = (0.0 * inv) - translate_x;
|
||||||
|
let cy0 = (0.0 * inv) - translate_y;
|
||||||
|
let cx1 = (vw * inv) - translate_x;
|
||||||
|
let cy1 = (vh * inv) - translate_y;
|
||||||
|
|
||||||
|
let min_x = cx0.min(cx1);
|
||||||
|
let min_y = cy0.min(cy1);
|
||||||
|
let max_x = cx0.max(cx1);
|
||||||
|
let max_y = cy0.max(cy1);
|
||||||
|
|
||||||
|
let cache_covers =
|
||||||
|
min_x >= 0.0 && min_y >= 0.0 && max_x <= cache_w && max_y <= cache_h;
|
||||||
|
if !cache_covers {
|
||||||
|
// Early return only if atlas exists; otherwise keep cache path.
|
||||||
|
if self.surfaces.has_atlas() {
|
||||||
|
self.surfaces.draw_atlas_to_target(
|
||||||
|
self.viewbox,
|
||||||
|
self.options.dpr(),
|
||||||
|
bg_color,
|
||||||
|
);
|
||||||
|
|
||||||
|
if self.options.is_debug_visible() {
|
||||||
|
debug::render(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui::render(self, shapes);
|
||||||
|
debug::render_wasm_label(self);
|
||||||
|
self.flush_and_submit();
|
||||||
|
performance::end_measure!("render_from_cache");
|
||||||
|
performance::end_timed_log!("render_from_cache", _start);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Setup canvas transform
|
// Setup canvas transform
|
||||||
{
|
{
|
||||||
@ -1536,6 +1622,7 @@ impl RenderState {
|
|||||||
|
|
||||||
self.flush_and_submit();
|
self.flush_and_submit();
|
||||||
}
|
}
|
||||||
|
|
||||||
performance::end_measure!("render_from_cache");
|
performance::end_measure!("render_from_cache");
|
||||||
performance::end_timed_log!("render_from_cache", _start);
|
performance::end_timed_log!("render_from_cache", _start);
|
||||||
}
|
}
|
||||||
@ -1675,6 +1762,7 @@ impl RenderState {
|
|||||||
self.cancel_animation_frame();
|
self.cancel_animation_frame();
|
||||||
self.render_request_id = Some(wapi::request_animation_frame!());
|
self.render_request_id = Some(wapi::request_animation_frame!());
|
||||||
} else {
|
} else {
|
||||||
|
notify_tiles_render_complete();
|
||||||
performance::end_measure!("render");
|
performance::end_measure!("render");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1692,6 +1780,7 @@ impl RenderState {
|
|||||||
self.render_shape_tree_partial(base_object, tree, timestamp, false)?;
|
self.render_shape_tree_partial(base_object, tree, timestamp, false)?;
|
||||||
}
|
}
|
||||||
self.flush_and_submit();
|
self.flush_and_submit();
|
||||||
|
notify_tiles_render_complete();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -2705,13 +2794,8 @@ impl RenderState {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
performance::begin_measure!("render_shape_tree::uncached");
|
performance::begin_measure!("render_shape_tree::uncached");
|
||||||
// Only allow stopping (yielding) if the current tile is NOT visible.
|
let (is_empty, early_return) = self
|
||||||
// This ensures all visible tiles render synchronously before showing,
|
.render_shape_tree_partial_uncached(tree, timestamp, allow_stop, false)?;
|
||||||
// eliminating empty squares during zoom. Interest-area tiles can still yield.
|
|
||||||
let tile_is_visible = self.tile_viewbox.is_visible(¤t_tile);
|
|
||||||
let can_stop = allow_stop && !tile_is_visible;
|
|
||||||
let (is_empty, early_return) =
|
|
||||||
self.render_shape_tree_partial_uncached(tree, timestamp, can_stop, false)?;
|
|
||||||
|
|
||||||
if early_return {
|
if early_return {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|||||||
@ -194,6 +194,15 @@ pub fn console_debug_surface(render_state: &mut RenderState, id: SurfaceId) {
|
|||||||
run_script!(format!("console.log('%c ', 'font-size: 1px; background: url(data:image/png;base64,{base64_image}) no-repeat; padding: 100px; background-size: contain;')"));
|
run_script!(format!("console.log('%c ', 'font-size: 1px; background: url(data:image/png;base64,{base64_image}) no-repeat; padding: 100px; background-size: contain;')"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn console_debug_surface_base64(render_state: &mut RenderState, id: SurfaceId) {
|
||||||
|
let base64_image = render_state
|
||||||
|
.surfaces
|
||||||
|
.base64_snapshot(id)
|
||||||
|
.expect("Failed to get base64 image");
|
||||||
|
|
||||||
|
println!("{}", base64_image);
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
pub fn console_debug_surface_rect(render_state: &mut RenderState, id: SurfaceId, rect: skia::Rect) {
|
pub fn console_debug_surface_rect(render_state: &mut RenderState, id: SurfaceId, rect: skia::Rect) {
|
||||||
@ -223,3 +232,33 @@ pub extern "C" fn debug_cache_console() -> Result<()> {
|
|||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
#[wasm_error]
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
pub extern "C" fn debug_cache_base64() -> Result<()> {
|
||||||
|
with_state_mut!(state, {
|
||||||
|
console_debug_surface_base64(state.render_state_mut(), SurfaceId::Cache);
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
#[wasm_error]
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
pub extern "C" fn debug_atlas_console() -> Result<()> {
|
||||||
|
with_state_mut!(state, {
|
||||||
|
console_debug_surface(state.render_state_mut(), SurfaceId::Atlas);
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
#[wasm_error]
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
pub extern "C" fn debug_atlas_base64() -> Result<()> {
|
||||||
|
with_state_mut!(state, {
|
||||||
|
console_debug_surface_base64(state.render_state_mut(), SurfaceId::Atlas);
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
use crate::performance;
|
use crate::performance;
|
||||||
use crate::shapes::Shape;
|
use crate::shapes::Shape;
|
||||||
|
use crate::view::Viewbox;
|
||||||
|
|
||||||
use skia_safe::{self as skia, IRect, Paint, RRect};
|
use skia_safe::{self as skia, IRect, Paint, RRect};
|
||||||
|
|
||||||
@ -15,6 +16,16 @@ const TEXTURES_BATCH_DELETE: usize = 256;
|
|||||||
// If it's too big it could affect performance.
|
// If it's too big it could affect performance.
|
||||||
const TILE_SIZE_MULTIPLIER: i32 = 2;
|
const TILE_SIZE_MULTIPLIER: i32 = 2;
|
||||||
|
|
||||||
|
/// Atlas texture size limits (px per side).
|
||||||
|
///
|
||||||
|
/// - `DEFAULT_MAX_ATLAS_TEXTURE_SIZE` is the startup fallback used until the
|
||||||
|
/// frontend reads the real `gl.MAX_TEXTURE_SIZE` and sends it via
|
||||||
|
/// [`Surfaces::set_max_atlas_texture_size`].
|
||||||
|
/// - `MAX_ATLAS_TEXTURE_SIZE` is a hard upper bound to clamp the runtime value
|
||||||
|
/// (defensive cap to avoid accidentally creating oversized GPU textures).
|
||||||
|
const MAX_ATLAS_TEXTURE_SIZE: i32 = 4096;
|
||||||
|
const DEFAULT_MAX_ATLAS_TEXTURE_SIZE: i32 = 1024;
|
||||||
|
|
||||||
#[repr(u32)]
|
#[repr(u32)]
|
||||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||||
pub enum SurfaceId {
|
pub enum SurfaceId {
|
||||||
@ -30,6 +41,7 @@ pub enum SurfaceId {
|
|||||||
Export = 0b010_0000_0000,
|
Export = 0b010_0000_0000,
|
||||||
UI = 0b100_0000_0000,
|
UI = 0b100_0000_0000,
|
||||||
Debug = 0b100_0000_0001,
|
Debug = 0b100_0000_0001,
|
||||||
|
Atlas = 0b100_0000_0010,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Surfaces {
|
pub struct Surfaces {
|
||||||
@ -57,6 +69,18 @@ pub struct Surfaces {
|
|||||||
export: skia::Surface,
|
export: skia::Surface,
|
||||||
|
|
||||||
tiles: TileTextureCache,
|
tiles: TileTextureCache,
|
||||||
|
// Persistent 1:1 document-space atlas that gets incrementally updated as tiles render.
|
||||||
|
// It grows dynamically to include any rendered document rect.
|
||||||
|
atlas: skia::Surface,
|
||||||
|
atlas_origin: skia::Point,
|
||||||
|
atlas_size: skia::ISize,
|
||||||
|
/// Atlas pixel density relative to document pixels (1.0 == 1:1 doc px).
|
||||||
|
/// When the atlas would exceed `max_atlas_texture_size`, this value is
|
||||||
|
/// reduced so the atlas stays within the fixed texture cap.
|
||||||
|
atlas_scale: f32,
|
||||||
|
/// Max width/height in pixels for the atlas surface (typically browser
|
||||||
|
/// `MAX_TEXTURE_SIZE`). Set from ClojureScript after WebGL context creation.
|
||||||
|
max_atlas_texture_size: i32,
|
||||||
sampling_options: skia::SamplingOptions,
|
sampling_options: skia::SamplingOptions,
|
||||||
pub margins: skia::ISize,
|
pub margins: skia::ISize,
|
||||||
// Tracks which surfaces have content (dirty flag bitmask)
|
// Tracks which surfaces have content (dirty flag bitmask)
|
||||||
@ -99,6 +123,10 @@ impl Surfaces {
|
|||||||
|
|
||||||
let ui = gpu_state.create_surface_with_dimensions("ui".to_string(), width, height)?;
|
let ui = gpu_state.create_surface_with_dimensions("ui".to_string(), width, height)?;
|
||||||
let debug = gpu_state.create_surface_with_dimensions("debug".to_string(), width, height)?;
|
let debug = gpu_state.create_surface_with_dimensions("debug".to_string(), width, height)?;
|
||||||
|
// Keep atlas as a regular surface like the rest. Start with a tiny
|
||||||
|
// transparent surface and grow it on demand.
|
||||||
|
let mut atlas = gpu_state.create_surface_with_dimensions("atlas".to_string(), 1, 1)?;
|
||||||
|
atlas.canvas().clear(skia::Color::TRANSPARENT);
|
||||||
|
|
||||||
let tiles = TileTextureCache::new();
|
let tiles = TileTextureCache::new();
|
||||||
Ok(Surfaces {
|
Ok(Surfaces {
|
||||||
@ -115,6 +143,11 @@ impl Surfaces {
|
|||||||
debug,
|
debug,
|
||||||
export,
|
export,
|
||||||
tiles,
|
tiles,
|
||||||
|
atlas,
|
||||||
|
atlas_origin: skia::Point::new(0.0, 0.0),
|
||||||
|
atlas_size: skia::ISize::new(0, 0),
|
||||||
|
atlas_scale: 1.0,
|
||||||
|
max_atlas_texture_size: DEFAULT_MAX_ATLAS_TEXTURE_SIZE,
|
||||||
sampling_options,
|
sampling_options,
|
||||||
margins,
|
margins,
|
||||||
dirty_surfaces: 0,
|
dirty_surfaces: 0,
|
||||||
@ -122,10 +155,185 @@ impl Surfaces {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the maximum atlas texture dimension (one side). Should match the
|
||||||
|
/// WebGL `MAX_TEXTURE_SIZE` reported by the browser. Values are clamped to
|
||||||
|
/// a small minimum so the atlas logic stays well-defined.
|
||||||
|
pub fn set_max_atlas_texture_size(&mut self, max_px: i32) {
|
||||||
|
self.max_atlas_texture_size = max_px.clamp(TILE_SIZE as i32, MAX_ATLAS_TEXTURE_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_atlas_contains(
|
||||||
|
&mut self,
|
||||||
|
gpu_state: &mut GpuState,
|
||||||
|
doc_rect: skia::Rect,
|
||||||
|
) -> Result<()> {
|
||||||
|
if doc_rect.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Current atlas bounds in document space (1 unit == 1 px).
|
||||||
|
let current_left = self.atlas_origin.x;
|
||||||
|
let current_top = self.atlas_origin.y;
|
||||||
|
let atlas_scale = self.atlas_scale.max(0.01);
|
||||||
|
let current_right = current_left + (self.atlas_size.width as f32) / atlas_scale;
|
||||||
|
let current_bottom = current_top + (self.atlas_size.height as f32) / atlas_scale;
|
||||||
|
|
||||||
|
let mut new_left = current_left;
|
||||||
|
let mut new_top = current_top;
|
||||||
|
let mut new_right = current_right;
|
||||||
|
let mut new_bottom = current_bottom;
|
||||||
|
|
||||||
|
// If atlas is empty/uninitialized, seed to rect (expanded to tile boundaries for fewer reallocs).
|
||||||
|
let needs_init = self.atlas_size.width <= 0 || self.atlas_size.height <= 0;
|
||||||
|
if needs_init {
|
||||||
|
new_left = doc_rect.left.floor();
|
||||||
|
new_top = doc_rect.top.floor();
|
||||||
|
new_right = doc_rect.right.ceil();
|
||||||
|
new_bottom = doc_rect.bottom.ceil();
|
||||||
|
} else {
|
||||||
|
new_left = new_left.min(doc_rect.left.floor());
|
||||||
|
new_top = new_top.min(doc_rect.top.floor());
|
||||||
|
new_right = new_right.max(doc_rect.right.ceil());
|
||||||
|
new_bottom = new_bottom.max(doc_rect.bottom.ceil());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add padding to reduce realloc frequency.
|
||||||
|
let pad = TILE_SIZE;
|
||||||
|
new_left -= pad;
|
||||||
|
new_top -= pad;
|
||||||
|
new_right += pad;
|
||||||
|
new_bottom += pad;
|
||||||
|
|
||||||
|
let doc_w = (new_right - new_left).max(1.0);
|
||||||
|
let doc_h = (new_bottom - new_top).max(1.0);
|
||||||
|
|
||||||
|
// Compute atlas scale needed to fit within the fixed texture cap.
|
||||||
|
// Keep the highest possible scale (closest to 1.0) that still fits.
|
||||||
|
let cap = self.max_atlas_texture_size.max(TILE_SIZE as i32) as f32;
|
||||||
|
let required_scale = (cap / doc_w).min(cap / doc_h).clamp(0.01, 1.0);
|
||||||
|
|
||||||
|
// Never upscale the atlas (it would add blur and churn).
|
||||||
|
let new_scale = self.atlas_scale.min(required_scale).max(0.01);
|
||||||
|
|
||||||
|
let new_w = (doc_w * new_scale).ceil().clamp(1.0, cap) as i32;
|
||||||
|
let new_h = (doc_h * new_scale).ceil().clamp(1.0, cap) as i32;
|
||||||
|
|
||||||
|
// Fast path: existing atlas already contains the rect.
|
||||||
|
if !needs_init
|
||||||
|
&& doc_rect.left >= current_left
|
||||||
|
&& doc_rect.top >= current_top
|
||||||
|
&& doc_rect.right <= current_right
|
||||||
|
&& doc_rect.bottom <= current_bottom
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut new_atlas =
|
||||||
|
gpu_state.create_surface_with_dimensions("atlas".to_string(), new_w, new_h)?;
|
||||||
|
new_atlas.canvas().clear(skia::Color::TRANSPARENT);
|
||||||
|
|
||||||
|
// Copy old atlas into the new one with offset.
|
||||||
|
if !needs_init {
|
||||||
|
let old_scale = self.atlas_scale.max(0.01);
|
||||||
|
let scale_ratio = new_scale / old_scale;
|
||||||
|
let dx = (current_left - new_left) * new_scale;
|
||||||
|
let dy = (current_top - new_top) * new_scale;
|
||||||
|
|
||||||
|
let image = self.atlas.image_snapshot();
|
||||||
|
let src = skia::Rect::from_xywh(
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
self.atlas_size.width as f32,
|
||||||
|
self.atlas_size.height as f32,
|
||||||
|
);
|
||||||
|
let dst = skia::Rect::from_xywh(
|
||||||
|
dx,
|
||||||
|
dy,
|
||||||
|
(self.atlas_size.width as f32) * scale_ratio,
|
||||||
|
(self.atlas_size.height as f32) * scale_ratio,
|
||||||
|
);
|
||||||
|
new_atlas.canvas().draw_image_rect(
|
||||||
|
&image,
|
||||||
|
Some((&src, skia::canvas::SrcRectConstraint::Fast)),
|
||||||
|
dst,
|
||||||
|
&skia::Paint::default(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.atlas_origin = skia::Point::new(new_left, new_top);
|
||||||
|
self.atlas_size = skia::ISize::new(new_w, new_h);
|
||||||
|
self.atlas_scale = new_scale;
|
||||||
|
self.atlas = new_atlas;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn blit_tile_image_into_atlas(
|
||||||
|
&mut self,
|
||||||
|
gpu_state: &mut GpuState,
|
||||||
|
tile_image: &skia::Image,
|
||||||
|
doc_rect: skia::Rect,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.ensure_atlas_contains(gpu_state, doc_rect)?;
|
||||||
|
|
||||||
|
// Destination is document-space rect mapped into atlas pixel coords.
|
||||||
|
let dst = skia::Rect::from_xywh(
|
||||||
|
(doc_rect.left - self.atlas_origin.x) * self.atlas_scale,
|
||||||
|
(doc_rect.top - self.atlas_origin.y) * self.atlas_scale,
|
||||||
|
doc_rect.width() * self.atlas_scale,
|
||||||
|
doc_rect.height() * self.atlas_scale,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.atlas
|
||||||
|
.canvas()
|
||||||
|
.draw_image_rect(tile_image, None, dst, &skia::Paint::default());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn clear_tiles(&mut self) {
|
pub fn clear_tiles(&mut self) {
|
||||||
self.tiles.clear();
|
self.tiles.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn has_atlas(&self) -> bool {
|
||||||
|
self.atlas_size.width > 0 && self.atlas_size.height > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw the persistent atlas onto the target using the current viewbox transform.
|
||||||
|
/// Intended for fast pan/zoom-out previews (avoids per-tile composition).
|
||||||
|
pub fn draw_atlas_to_target(&mut self, viewbox: Viewbox, dpr: f32, background: skia::Color) {
|
||||||
|
if !self.has_atlas() {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let canvas = self.target.canvas();
|
||||||
|
canvas.save();
|
||||||
|
canvas.reset_matrix();
|
||||||
|
let size = canvas.base_layer_size();
|
||||||
|
canvas.clip_rect(
|
||||||
|
skia::Rect::from_xywh(0.0, 0.0, size.width as f32, size.height as f32),
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
let s = viewbox.zoom * dpr;
|
||||||
|
let atlas_scale = self.atlas_scale.max(0.01);
|
||||||
|
|
||||||
|
canvas.clear(background);
|
||||||
|
canvas.translate((
|
||||||
|
(self.atlas_origin.x + viewbox.pan_x) * s,
|
||||||
|
(self.atlas_origin.y + viewbox.pan_y) * s,
|
||||||
|
));
|
||||||
|
canvas.scale((s / atlas_scale, s / atlas_scale));
|
||||||
|
|
||||||
|
self.atlas.draw(
|
||||||
|
canvas,
|
||||||
|
(0.0, 0.0),
|
||||||
|
self.sampling_options,
|
||||||
|
Some(&skia::Paint::default()),
|
||||||
|
);
|
||||||
|
|
||||||
|
canvas.restore();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn margins(&self) -> skia::ISize {
|
pub fn margins(&self) -> skia::ISize {
|
||||||
self.margins
|
self.margins
|
||||||
}
|
}
|
||||||
@ -255,6 +463,10 @@ impl Surfaces {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn cache_dimensions(&self) -> skia::ISize {
|
||||||
|
skia::ISize::new(self.cache.width(), self.cache.height())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn apply_mut(&mut self, ids: u32, mut f: impl FnMut(&mut skia::Surface)) {
|
pub fn apply_mut(&mut self, ids: u32, mut f: impl FnMut(&mut skia::Surface)) {
|
||||||
performance::begin_measure!("apply_mut::flags");
|
performance::begin_measure!("apply_mut::flags");
|
||||||
if ids & SurfaceId::Target as u32 != 0 {
|
if ids & SurfaceId::Target as u32 != 0 {
|
||||||
@ -352,6 +564,7 @@ impl Surfaces {
|
|||||||
SurfaceId::Debug => &mut self.debug,
|
SurfaceId::Debug => &mut self.debug,
|
||||||
SurfaceId::UI => &mut self.ui,
|
SurfaceId::UI => &mut self.ui,
|
||||||
SurfaceId::Export => &mut self.export,
|
SurfaceId::Export => &mut self.export,
|
||||||
|
SurfaceId::Atlas => &mut self.atlas,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -369,6 +582,7 @@ impl Surfaces {
|
|||||||
SurfaceId::Debug => &self.debug,
|
SurfaceId::Debug => &self.debug,
|
||||||
SurfaceId::UI => &self.ui,
|
SurfaceId::UI => &self.ui,
|
||||||
SurfaceId::Export => &self.export,
|
SurfaceId::Export => &self.export,
|
||||||
|
SurfaceId::Atlas => &self.atlas,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -546,10 +760,12 @@ impl Surfaces {
|
|||||||
|
|
||||||
pub fn cache_current_tile_texture(
|
pub fn cache_current_tile_texture(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
gpu_state: &mut GpuState,
|
||||||
tile_viewbox: &TileViewbox,
|
tile_viewbox: &TileViewbox,
|
||||||
tile: &Tile,
|
tile: &Tile,
|
||||||
tile_rect: &skia::Rect,
|
tile_rect: &skia::Rect,
|
||||||
skip_cache_surface: bool,
|
skip_cache_surface: bool,
|
||||||
|
tile_doc_rect: skia::Rect,
|
||||||
) {
|
) {
|
||||||
let rect = IRect::from_xywh(
|
let rect = IRect::from_xywh(
|
||||||
self.margins.width,
|
self.margins.width,
|
||||||
@ -571,6 +787,9 @@ impl Surfaces {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Incrementally update persistent 1:1 atlas in document space.
|
||||||
|
// `tile_doc_rect` is in world/document coordinates (1 unit == 1 px at 100%).
|
||||||
|
let _ = self.blit_tile_image_into_atlas(gpu_state, &tile_image, tile_doc_rect);
|
||||||
self.tiles.add(tile_viewbox, tile, tile_image);
|
self.tiles.add(tile_viewbox, tile, tile_image);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user