mirror of
https://github.com/penpot/penpot.git
synced 2026-09-13 23:48:42 +00:00
Merge pull request #8611 from penpot/alotor-export-wasm
✨ Add support for export with wasm engine
This commit is contained in:
commit
dd10be1fb4
@ -26,6 +26,7 @@
|
|||||||
(s/def ::file-id ::us/uuid)
|
(s/def ::file-id ::us/uuid)
|
||||||
(s/def ::page-id ::us/uuid)
|
(s/def ::page-id ::us/uuid)
|
||||||
(s/def ::object-id ::us/uuid)
|
(s/def ::object-id ::us/uuid)
|
||||||
|
(s/def ::is-wasm ::us/boolean)
|
||||||
|
|
||||||
(s/def ::export
|
(s/def ::export
|
||||||
(s/keys :req-un [::file-id ::page-id ::object-id ::name]))
|
(s/keys :req-un [::file-id ::page-id ::object-id ::name]))
|
||||||
@ -35,7 +36,7 @@
|
|||||||
|
|
||||||
(s/def ::params
|
(s/def ::params
|
||||||
(s/keys :req-un [::exports]
|
(s/keys :req-un [::exports]
|
||||||
:opt-un [::name]))
|
:opt-un [::name ::is-wasm]))
|
||||||
|
|
||||||
(defn handler
|
(defn handler
|
||||||
[{:keys [:request/auth-token] :as exchange} {:keys [exports] :as params}]
|
[{:keys [:request/auth-token] :as exchange} {:keys [exports] :as params}]
|
||||||
@ -47,7 +48,7 @@
|
|||||||
(handle-export exchange (assoc params :exports exports))))
|
(handle-export exchange (assoc params :exports exports))))
|
||||||
|
|
||||||
(defn handle-export
|
(defn handle-export
|
||||||
[{:keys [:request/auth-token] :as exchange} {:keys [exports name profile-id] :as params}]
|
[{:keys [:request/auth-token] :as exchange} {:keys [exports name profile-id is-wasm] :as params}]
|
||||||
(let [topic (str profile-id)
|
(let [topic (str profile-id)
|
||||||
file-id (-> exports first :file-id)
|
file-id (-> exports first :file-id)
|
||||||
|
|
||||||
@ -94,7 +95,7 @@
|
|||||||
|
|
||||||
procs
|
procs
|
||||||
(->> (seq exports)
|
(->> (seq exports)
|
||||||
(map #(rd/render % on-object)))]
|
(map #(rd/render (assoc % :is-wasm is-wasm) on-object)))]
|
||||||
|
|
||||||
(->> (p/all procs)
|
(->> (p/all procs)
|
||||||
(p/fmap (fn [] @result-cache))
|
(p/fmap (fn [] @result-cache))
|
||||||
|
|||||||
@ -37,6 +37,7 @@
|
|||||||
(s/def ::suffix ::us/string)
|
(s/def ::suffix ::us/string)
|
||||||
(s/def ::type ::us/keyword)
|
(s/def ::type ::us/keyword)
|
||||||
(s/def ::wait ::us/boolean)
|
(s/def ::wait ::us/boolean)
|
||||||
|
(s/def ::is-wasm ::us/boolean)
|
||||||
|
|
||||||
(s/def ::export
|
(s/def ::export
|
||||||
(s/keys :req-un [::page-id ::file-id ::object-id ::type ::suffix ::scale ::name]
|
(s/keys :req-un [::page-id ::file-id ::object-id ::type ::suffix ::scale ::name]
|
||||||
@ -47,7 +48,7 @@
|
|||||||
|
|
||||||
(s/def ::params
|
(s/def ::params
|
||||||
(s/keys :req-un [::exports ::profile-id]
|
(s/keys :req-un [::exports ::profile-id]
|
||||||
:opt-un [::wait ::name ::skip-children ::force-multiple]))
|
:opt-un [::wait ::name ::skip-children ::force-multiple ::is-wasm]))
|
||||||
|
|
||||||
(defn handler
|
(defn handler
|
||||||
[{:keys [:request/auth-token] :as exchange} {:keys [exports force-multiple] :as params}]
|
[{:keys [:request/auth-token] :as exchange} {:keys [exports force-multiple] :as params}]
|
||||||
@ -61,9 +62,9 @@
|
|||||||
(handle-multiple-export exchange (assoc params :exports exports)))))
|
(handle-multiple-export exchange (assoc params :exports exports)))))
|
||||||
|
|
||||||
(defn- handle-single-export
|
(defn- handle-single-export
|
||||||
[{:keys [:request/auth-token] :as exchange} {:keys [export name skip-children] :as params}]
|
[{:keys [:request/auth-token] :as exchange} {:keys [export name skip-children is-wasm] :as params}]
|
||||||
(let [resource (rsc/create (:type export) (or name (:name export)))
|
(let [resource (rsc/create (:type export) (or name (:name export)))
|
||||||
export (assoc export :skip-children skip-children)]
|
export (assoc export :skip-children skip-children :is-wasm is-wasm)]
|
||||||
|
|
||||||
(->> (rd/render export
|
(->> (rd/render export
|
||||||
(fn [{:keys [path] :as object}]
|
(fn [{:keys [path] :as object}]
|
||||||
@ -80,7 +81,7 @@
|
|||||||
(p/rejected cause))))))
|
(p/rejected cause))))))
|
||||||
|
|
||||||
(defn- handle-multiple-export
|
(defn- handle-multiple-export
|
||||||
[{:keys [:request/auth-token] :as exchange} {:keys [exports wait profile-id name] :as params}]
|
[{:keys [:request/auth-token] :as exchange} {:keys [exports wait profile-id name is-wasm] :as params}]
|
||||||
(let [resource (rsc/create :zip (or name (-> exports first :name)))
|
(let [resource (rsc/create :zip (or name (-> exports first :name)))
|
||||||
total (count exports)
|
total (count exports)
|
||||||
topic (str profile-id)
|
topic (str profile-id)
|
||||||
@ -111,7 +112,7 @@
|
|||||||
(rsc/add-to-zip zip path (str/replace filename sanitize-file-regex "_")))
|
(rsc/add-to-zip zip path (str/replace filename sanitize-file-regex "_")))
|
||||||
|
|
||||||
proc (->> exports
|
proc (->> exports
|
||||||
(map (fn [export] (rd/render export append)))
|
(map (fn [export] (rd/render (assoc export :is-wasm is-wasm) append)))
|
||||||
(p/all)
|
(p/all)
|
||||||
(p/mcat (fn [_] (rsc/close-zip zip)))
|
(p/mcat (fn [_] (rsc/close-zip zip)))
|
||||||
(p/fmap (constantly resource))
|
(p/fmap (constantly resource))
|
||||||
|
|||||||
@ -22,6 +22,7 @@
|
|||||||
(s/def ::scale ::us/number)
|
(s/def ::scale ::us/number)
|
||||||
(s/def ::token ::us/string)
|
(s/def ::token ::us/string)
|
||||||
(s/def ::filename ::us/string)
|
(s/def ::filename ::us/string)
|
||||||
|
(s/def ::is-wasm ::us/boolean)
|
||||||
|
|
||||||
(s/def ::object
|
(s/def ::object
|
||||||
(s/keys :req-un [::id ::name ::suffix ::filename]
|
(s/keys :req-un [::id ::name ::suffix ::filename]
|
||||||
@ -31,7 +32,8 @@
|
|||||||
(s/coll-of ::object :min-count 1))
|
(s/coll-of ::object :min-count 1))
|
||||||
|
|
||||||
(s/def ::render-params
|
(s/def ::render-params
|
||||||
(s/keys :req-un [::file-id ::page-id ::scale ::token ::type ::objects]))
|
(s/keys :req-un [::file-id ::page-id ::scale ::token ::type ::objects]
|
||||||
|
:opt-un [::is-wasm]))
|
||||||
|
|
||||||
(defn render
|
(defn render
|
||||||
[{:keys [type] :as params} on-object]
|
[{:keys [type] :as params} on-object]
|
||||||
|
|||||||
@ -17,7 +17,7 @@
|
|||||||
[promesa.core :as p]))
|
[promesa.core :as p]))
|
||||||
|
|
||||||
(defn render
|
(defn render
|
||||||
[{:keys [file-id page-id share-id token scale type objects skip-children] :as params} on-object]
|
[{:keys [file-id page-id share-id token scale type objects skip-children is-wasm] :as params} on-object]
|
||||||
(letfn [(prepare-options [uri]
|
(letfn [(prepare-options [uri]
|
||||||
#js {:screen #js {:width bw/default-viewport-width
|
#js {:screen #js {:width bw/default-viewport-width
|
||||||
:height bw/default-viewport-height}
|
:height bw/default-viewport-height}
|
||||||
@ -25,7 +25,7 @@
|
|||||||
:height bw/default-viewport-height}
|
:height bw/default-viewport-height}
|
||||||
:locale "en-US"
|
:locale "en-US"
|
||||||
:storageState #js {:cookies (bw/create-cookies uri {:token token})}
|
:storageState #js {:cookies (bw/create-cookies uri {:token token})}
|
||||||
:deviceScaleFactor scale
|
:deviceScaleFactor (if is-wasm 1 scale) ;; wasm won't use deviceScaleFactor
|
||||||
:userAgent bw/default-user-agent})
|
:userAgent bw/default-user-agent})
|
||||||
|
|
||||||
(render-object [page {:keys [id] :as object}]
|
(render-object [page {:keys [id] :as object}]
|
||||||
@ -58,7 +58,9 @@
|
|||||||
:share-id share-id
|
:share-id share-id
|
||||||
:object-id (mapv :id objects)
|
:object-id (mapv :id objects)
|
||||||
:route "objects"
|
:route "objects"
|
||||||
:skip-children skip-children}
|
:skip-children skip-children
|
||||||
|
:wasm (when is-wasm "true")
|
||||||
|
:scale scale}
|
||||||
uri (-> (cf/get :public-uri)
|
uri (-> (cf/get :public-uri)
|
||||||
(assoc :path "/render.html")
|
(assoc :path "/render.html")
|
||||||
(assoc :query (u/map->query-string params)))]
|
(assoc :query (u/map->query-string params)))]
|
||||||
|
|||||||
@ -8,10 +8,13 @@
|
|||||||
(:require
|
(:require
|
||||||
[app.common.time :as ct]
|
[app.common.time :as ct]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
|
[app.config :as cf]
|
||||||
[app.main.data.event :as ev]
|
[app.main.data.event :as ev]
|
||||||
|
[app.main.data.exports.wasm :as wasm.exports]
|
||||||
[app.main.data.helpers :as dsh]
|
[app.main.data.helpers :as dsh]
|
||||||
[app.main.data.modal :as modal]
|
[app.main.data.modal :as modal]
|
||||||
[app.main.data.persistence :as dwp]
|
[app.main.data.persistence :as dwp]
|
||||||
|
[app.main.features :as features]
|
||||||
[app.main.refs :as refs]
|
[app.main.refs :as refs]
|
||||||
[app.main.repo :as rp]
|
[app.main.repo :as rp]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
@ -152,35 +155,46 @@
|
|||||||
|
|
||||||
(defn request-simple-export
|
(defn request-simple-export
|
||||||
[{:keys [export]}]
|
[{:keys [export]}]
|
||||||
(ptk/reify ::request-simple-export
|
(if (and (contains? cf/flags :wasm-export)
|
||||||
ptk/UpdateEvent
|
(contains? #{:jpeg :webp :png} (:type export)))
|
||||||
(update [_ state]
|
(ptk/reify ::request-simple-export-wasm
|
||||||
(update state :export assoc :in-progress true :id uuid/zero))
|
ptk/EffectEvent
|
||||||
|
(effect [_ _ _]
|
||||||
|
(wasm.exports/export-image export)))
|
||||||
|
|
||||||
ptk/WatchEvent
|
(ptk/reify ::request-simple-export
|
||||||
(watch [_ state _]
|
ptk/UpdateEvent
|
||||||
(let [profile-id (:profile-id state)
|
(update [_ state]
|
||||||
params {:exports [export]
|
(update state :export assoc :in-progress true :id uuid/zero))
|
||||||
:profile-id profile-id
|
|
||||||
:cmd :export-shapes
|
|
||||||
:wait true}]
|
|
||||||
(rx/concat
|
|
||||||
(rx/of ::dwp/force-persist)
|
|
||||||
|
|
||||||
;; Wait the persist to be succesfull
|
ptk/WatchEvent
|
||||||
(->> (rx/from-atom refs/persistence-state {:emit-current-value? true})
|
(watch [_ state _]
|
||||||
(rx/filter #(or (nil? %) (= :saved %)))
|
(let [profile-id (:profile-id state)
|
||||||
(rx/first)
|
params {:exports [export]
|
||||||
(rx/timeout 400 (rx/empty)))
|
:profile-id profile-id
|
||||||
|
:cmd :export-shapes
|
||||||
|
:wait true
|
||||||
|
:is-wasm
|
||||||
|
(and
|
||||||
|
(features/active-feature? state "render-wasm/v1")
|
||||||
|
(contains? cf/flags :wasm-export))}]
|
||||||
|
(rx/concat
|
||||||
|
(rx/of ::dwp/force-persist)
|
||||||
|
|
||||||
(->> (rp/cmd! :export params)
|
;; Wait the persist to be succesfull
|
||||||
(rx/map (fn [{:keys [filename mtype uri]}]
|
(->> (rx/from-atom refs/persistence-state {:emit-current-value? true})
|
||||||
(dom/trigger-download-uri filename mtype uri)
|
(rx/filter #(or (nil? %) (= :saved %)))
|
||||||
(clear-export-state uuid/zero)))
|
(rx/first)
|
||||||
(rx/catch (fn [cause]
|
(rx/timeout 400 (rx/empty)))
|
||||||
(rx/concat
|
|
||||||
(rx/of (clear-export-state uuid/zero))
|
(->> (rp/cmd! :export params)
|
||||||
(rx/throw cause))))))))))
|
(rx/map (fn [{:keys [filename mtype uri]}]
|
||||||
|
(dom/trigger-download-uri filename mtype uri)
|
||||||
|
(clear-export-state uuid/zero)))
|
||||||
|
(rx/catch (fn [cause]
|
||||||
|
(rx/concat
|
||||||
|
(rx/of (clear-export-state uuid/zero))
|
||||||
|
(rx/throw cause)))))))))))
|
||||||
|
|
||||||
(defn request-multiple-export
|
(defn request-multiple-export
|
||||||
[{:keys [exports cmd]
|
[{:keys [exports cmd]
|
||||||
@ -195,7 +209,11 @@
|
|||||||
params {:exports exports
|
params {:exports exports
|
||||||
:cmd cmd
|
:cmd cmd
|
||||||
:profile-id profile-id
|
:profile-id profile-id
|
||||||
:force-multiple true}
|
:force-multiple true
|
||||||
|
:is-wasm
|
||||||
|
(and
|
||||||
|
(features/active-feature? state "render-wasm/v1")
|
||||||
|
(contains? cf/flags :wasm-export))}
|
||||||
|
|
||||||
progress-stream
|
progress-stream
|
||||||
(->> (ws/get-rcv-stream ws-conn)
|
(->> (ws/get-rcv-stream ws-conn)
|
||||||
|
|||||||
28
frontend/src/app/main/data/exports/wasm.cljs
Normal file
28
frontend/src/app/main/data/exports/wasm.cljs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
;; 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
|
||||||
|
|
||||||
|
(ns app.main.data.exports.wasm
|
||||||
|
(:require
|
||||||
|
[app.common.media :refer [format->mtype]]
|
||||||
|
[app.render-wasm.api :as wasm.api]
|
||||||
|
[app.util.dom :as dom]
|
||||||
|
[app.util.webapi :as wapi]))
|
||||||
|
|
||||||
|
(defn export-image-uri
|
||||||
|
[{:keys [type scale object-id]}]
|
||||||
|
(let [bytes (wasm.api/render-shape-pixels object-id scale)
|
||||||
|
mtype (format->mtype type)
|
||||||
|
blob (wapi/create-blob bytes mtype)]
|
||||||
|
(wapi/create-uri blob)))
|
||||||
|
|
||||||
|
(defn export-image
|
||||||
|
[{:keys [type suffix name] :as params}]
|
||||||
|
(let [url (export-image-uri params)
|
||||||
|
mtype (format->mtype type)
|
||||||
|
filename (str name (or suffix ""))]
|
||||||
|
(dom/trigger-download-uri filename mtype url)
|
||||||
|
(wapi/revoke-uri url)
|
||||||
|
nil))
|
||||||
@ -45,6 +45,7 @@
|
|||||||
[app.main.ui.shapes.svg-raw :as svg-raw]
|
[app.main.ui.shapes.svg-raw :as svg-raw]
|
||||||
[app.main.ui.shapes.text :as text]
|
[app.main.ui.shapes.text :as text]
|
||||||
[app.main.ui.shapes.text.fontfaces :as ff]
|
[app.main.ui.shapes.text.fontfaces :as ff]
|
||||||
|
[app.render-wasm.api :as wasm.api]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.http :as http]
|
[app.util.http :as http]
|
||||||
[app.util.strings :as ust]
|
[app.util.strings :as ust]
|
||||||
@ -53,6 +54,7 @@
|
|||||||
[beicon.v2.core :as rx]
|
[beicon.v2.core :as rx]
|
||||||
[clojure.set :as set]
|
[clojure.set :as set]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
|
[promesa.core :as p]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
(def ^:const viewbox-decimal-precision 3)
|
(def ^:const viewbox-decimal-precision 3)
|
||||||
@ -171,6 +173,8 @@
|
|||||||
;; Don't wrap svg elements inside a <g> otherwise some can break
|
;; Don't wrap svg elements inside a <g> otherwise some can break
|
||||||
[:> svg-raw-wrapper {:shape shape :frame frame}]))))))
|
[:> svg-raw-wrapper {:shape shape :frame frame}]))))))
|
||||||
|
|
||||||
|
(set! wasm.api/shape-wrapper-factory shape-wrapper-factory)
|
||||||
|
|
||||||
(defn format-viewbox
|
(defn format-viewbox
|
||||||
"Format a viewbox given a rectangle"
|
"Format a viewbox given a rectangle"
|
||||||
[{:keys [x y width height] :or {x 0 y 0 width 100 height 100}}]
|
[{:keys [x y width height] :or {x 0 y 0 width 100 height 100}}]
|
||||||
@ -196,7 +200,7 @@
|
|||||||
;; Replace the previous object with the new one
|
;; Replace the previous object with the new one
|
||||||
objects (assoc objects object-id object)
|
objects (assoc objects object-id object)
|
||||||
|
|
||||||
vector (-> (gpt/point (:x object) (:y object))
|
vector (-> (gpt/point (-> object :selrect :x) (-> object :selrect :y))
|
||||||
(gpt/negate))
|
(gpt/negate))
|
||||||
|
|
||||||
mod-ids (cons object-id (cfh/get-children-ids objects object-id))
|
mod-ids (cons object-id (cfh/get-children-ids objects object-id))
|
||||||
@ -480,6 +484,50 @@
|
|||||||
[:& ff/fontfaces-style {:fonts fonts}]
|
[:& ff/fontfaces-style {:fonts fonts}]
|
||||||
[:& shape-wrapper {:shape object}]]]]))
|
[:& shape-wrapper {:shape object}]]]]))
|
||||||
|
|
||||||
|
(defn render-to-canvas
|
||||||
|
[objects canvas bounds scale object-id]
|
||||||
|
(try
|
||||||
|
(when (wasm.api/init-canvas-context canvas)
|
||||||
|
(wasm.api/initialize-viewport
|
||||||
|
objects scale bounds "#000000" 0
|
||||||
|
(fn []
|
||||||
|
(wasm.api/render-sync-shape object-id)
|
||||||
|
(dom/set-attribute! canvas "id" (dm/str "screenshot-" object-id)))))
|
||||||
|
(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] :as props}]
|
||||||
|
(let [object (get objects object-id)
|
||||||
|
object (cond-> object
|
||||||
|
(:hide-fill-on-export object)
|
||||||
|
(assoc :fills [])
|
||||||
|
|
||||||
|
skip-children
|
||||||
|
(assoc :shapes []))
|
||||||
|
|
||||||
|
{:keys [width height] :as bounds}
|
||||||
|
(gsb/get-object-bounds objects object {:ignore-margin? false})
|
||||||
|
|
||||||
|
scale (or scale 1)
|
||||||
|
canvas-ref (mf/use-ref nil)]
|
||||||
|
|
||||||
|
(mf/use-effect
|
||||||
|
(fn []
|
||||||
|
(let [canvas (mf/ref-val canvas-ref)]
|
||||||
|
(->> @wasm.api/module
|
||||||
|
(p/fmap
|
||||||
|
(fn [ready?]
|
||||||
|
(when ready?
|
||||||
|
(render-to-canvas objects canvas bounds scale object-id))))))))
|
||||||
|
|
||||||
|
[:canvas {:ref canvas-ref
|
||||||
|
:width (* scale width)
|
||||||
|
:height (* scale height)
|
||||||
|
:style {:background "transparent"}}]))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; SPRITES (DEBUG)
|
;; SPRITES (DEBUG)
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|||||||
@ -161,7 +161,7 @@
|
|||||||
[:& blur-menu {:ids ids
|
[:& blur-menu {:ids ids
|
||||||
:values (select-keys shape [:blur])}]
|
:values (select-keys shape [:blur])}]
|
||||||
[:& frame-grid {:shape shape}]
|
[:& frame-grid {:shape shape}]
|
||||||
[:> exports-menu* {:type type
|
[:> exports-menu* {:type shape-type
|
||||||
:ids ids
|
:ids ids
|
||||||
:shapes shapes
|
:shapes shapes
|
||||||
:values (select-keys shape exports-attrs)
|
:values (select-keys shape exports-attrs)
|
||||||
|
|||||||
@ -32,6 +32,8 @@
|
|||||||
[app.common.types.shape.shadow :as ctss]
|
[app.common.types.shape.shadow :as ctss]
|
||||||
[app.common.types.text :as txt]
|
[app.common.types.text :as txt]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
|
[app.config :as cf]
|
||||||
|
[app.main.data.exports.wasm :as wasm.exports]
|
||||||
[app.main.data.plugins :as dp]
|
[app.main.data.plugins :as dp]
|
||||||
[app.main.data.workspace :as dw]
|
[app.main.data.workspace :as dw]
|
||||||
[app.main.data.workspace.groups :as dwg]
|
[app.main.data.workspace.groups :as dwg]
|
||||||
@ -1200,30 +1202,53 @@
|
|||||||
(u/not-valid plugin-id :export value)
|
(u/not-valid plugin-id :export value)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [shape (u/locate-shape file-id page-id id)
|
(if (and (contains? cf/flags :wasm-export)
|
||||||
payload
|
(contains? #{:jpeg :webp :png} (:type value :png)))
|
||||||
{:cmd :export-shapes
|
;; New export with wasm
|
||||||
:profile-id (:profile-id @st/state)
|
(let [uri (wasm.exports/export-image-uri
|
||||||
:wait true
|
{:file-id file-id
|
||||||
:exports [{:file-id file-id
|
:page-id page-id
|
||||||
:page-id page-id
|
:object-id id
|
||||||
:object-id id
|
:type (:type value :png)
|
||||||
:name (:name shape)
|
:scale (:scale value 1)})]
|
||||||
:type (:type value :png)
|
(js/Promise.
|
||||||
:suffix (:suffix value "")
|
(fn [resolve reject]
|
||||||
:scale (:scale value 1)}]}]
|
(->> (http/send!
|
||||||
(js/Promise.
|
{:method :get
|
||||||
(fn [resolve reject]
|
:uri uri
|
||||||
(->> (rp/cmd! :export payload)
|
:response-type :blob
|
||||||
(rx/mapcat (fn [{:keys [uri]}]
|
:omit-default-headers true})
|
||||||
(->> (http/send! {:method :get
|
(rx/map :body)
|
||||||
:uri uri
|
(rx/mapcat #(.arrayBuffer %))
|
||||||
:response-type :blob
|
(rx/map #(js/Uint8Array. %))
|
||||||
:omit-default-headers true})
|
(rx/subs! resolve reject)))))
|
||||||
(rx/map :body))))
|
|
||||||
(rx/mapcat #(.arrayBuffer %))
|
|
||||||
(rx/map #(js/Uint8Array. %))
|
;; Old export through exporter
|
||||||
(rx/subs! resolve reject))))))))
|
(let [shape (u/locate-shape file-id page-id id)
|
||||||
|
payload
|
||||||
|
{:cmd :export-shapes
|
||||||
|
:profile-id (:profile-id @st/state)
|
||||||
|
:wait true
|
||||||
|
:exports [{:file-id file-id
|
||||||
|
:page-id page-id
|
||||||
|
:object-id id
|
||||||
|
:name (:name shape)
|
||||||
|
:type (:type value :png)
|
||||||
|
:suffix (:suffix value "")
|
||||||
|
:scale (:scale value 1)}]}]
|
||||||
|
(js/Promise.
|
||||||
|
(fn [resolve reject]
|
||||||
|
(->> (rp/cmd! :export payload)
|
||||||
|
(rx/mapcat (fn [{:keys [uri]}]
|
||||||
|
(->> (http/send! {:method :get
|
||||||
|
:uri uri
|
||||||
|
:response-type :blob
|
||||||
|
:omit-default-headers true})
|
||||||
|
(rx/map :body))))
|
||||||
|
(rx/mapcat #(.arrayBuffer %))
|
||||||
|
(rx/map #(js/Uint8Array. %))
|
||||||
|
(rx/subs! resolve reject)))))))))
|
||||||
|
|
||||||
;; Interactions
|
;; Interactions
|
||||||
:addInteraction
|
:addInteraction
|
||||||
|
|||||||
@ -63,7 +63,7 @@
|
|||||||
|
|
||||||
(mf/defc object-svg
|
(mf/defc object-svg
|
||||||
{::mf/wrap-props false}
|
{::mf/wrap-props false}
|
||||||
[{:keys [object-id embed skip-children]}]
|
[{:keys [object-id embed skip-children wasm scale]}]
|
||||||
(let [objects (mf/deref ref:objects)]
|
(let [objects (mf/deref ref:objects)]
|
||||||
|
|
||||||
;; Set the globa CSS to assign the page size, needed for PDF
|
;; Set the globa CSS to assign the page size, needed for PDF
|
||||||
@ -77,27 +77,44 @@
|
|||||||
(mth/ceil height) "px")}))))
|
(mth/ceil height) "px")}))))
|
||||||
|
|
||||||
(when objects
|
(when objects
|
||||||
[:& (mf/provider ctx/is-render?) {:value true}
|
(if wasm
|
||||||
[:& render/object-svg
|
[:& render/object-wasm
|
||||||
{:objects objects
|
{:objects objects
|
||||||
:object-id object-id
|
:object-id object-id
|
||||||
:embed embed
|
:embed embed
|
||||||
:skip-children skip-children}]])))
|
:scale scale
|
||||||
|
:skip-children skip-children}]
|
||||||
|
|
||||||
(mf/defc objects-svg
|
|
||||||
{::mf/wrap-props false}
|
|
||||||
[{:keys [object-ids embed skip-children]}]
|
|
||||||
(when-let [objects (mf/deref ref:objects)]
|
|
||||||
(for [object-id object-ids]
|
|
||||||
(let [objects (render/adapt-objects-for-shape objects object-id)]
|
|
||||||
[:& (mf/provider ctx/is-render?) {:value true}
|
[:& (mf/provider ctx/is-render?) {:value true}
|
||||||
[:& render/object-svg
|
[:& render/object-svg
|
||||||
{:objects objects
|
{:objects objects
|
||||||
:key (str object-id)
|
|
||||||
:object-id object-id
|
:object-id object-id
|
||||||
:embed embed
|
:embed embed
|
||||||
:skip-children skip-children}]]))))
|
:skip-children skip-children}]]))))
|
||||||
|
|
||||||
|
(mf/defc objects-svg
|
||||||
|
{::mf/wrap-props false}
|
||||||
|
[{:keys [object-ids embed skip-children wasm scale]}]
|
||||||
|
(when-let [objects (mf/deref ref:objects)]
|
||||||
|
(for [object-id object-ids]
|
||||||
|
(let [objects (render/adapt-objects-for-shape objects object-id)]
|
||||||
|
(if wasm
|
||||||
|
[:& render/object-wasm
|
||||||
|
{:objects objects
|
||||||
|
:key (str object-id)
|
||||||
|
:object-id object-id
|
||||||
|
:embed embed
|
||||||
|
:scale scale
|
||||||
|
:skip-children skip-children}]
|
||||||
|
|
||||||
|
[:& (mf/provider ctx/is-render?) {:value true}
|
||||||
|
[:& render/object-svg
|
||||||
|
{:objects objects
|
||||||
|
:key (str object-id)
|
||||||
|
:object-id object-id
|
||||||
|
:embed embed
|
||||||
|
:skip-children skip-children}]])))))
|
||||||
|
|
||||||
(defn- fetch-objects-bundle
|
(defn- fetch-objects-bundle
|
||||||
[& {:keys [file-id page-id share-id object-id] :as options}]
|
[& {:keys [file-id page-id share-id object-id] :as options}]
|
||||||
(ptk/reify ::fetch-objects-bundle
|
(ptk/reify ::fetch-objects-bundle
|
||||||
@ -136,7 +153,7 @@
|
|||||||
(defn- render-objects
|
(defn- render-objects
|
||||||
[params]
|
[params]
|
||||||
(try
|
(try
|
||||||
(let [{:keys [file-id page-id embed share-id object-id skip-children] :as params}
|
(let [{:keys [file-id page-id embed share-id object-id skip-children wasm scale] :as params}
|
||||||
(coerce-render-objects-params params)]
|
(coerce-render-objects-params params)]
|
||||||
(st/emit! (fetch-objects-bundle :file-id file-id :page-id page-id :share-id share-id :object-id object-id))
|
(st/emit! (fetch-objects-bundle :file-id file-id :page-id page-id :share-id share-id :object-id object-id))
|
||||||
(if (uuid? object-id)
|
(if (uuid? object-id)
|
||||||
@ -147,7 +164,9 @@
|
|||||||
:share-id share-id
|
:share-id share-id
|
||||||
:object-id object-id
|
:object-id object-id
|
||||||
:embed embed
|
:embed embed
|
||||||
:skip-children skip-children}])
|
:skip-children skip-children
|
||||||
|
:wasm wasm
|
||||||
|
:scale scale}])
|
||||||
|
|
||||||
(mf/html
|
(mf/html
|
||||||
[:& objects-svg
|
[:& objects-svg
|
||||||
@ -156,7 +175,9 @@
|
|||||||
:share-id share-id
|
:share-id share-id
|
||||||
:object-ids (into #{} object-id)
|
:object-ids (into #{} object-id)
|
||||||
:embed embed
|
:embed embed
|
||||||
:skip-children skip-children}])))
|
:skip-children skip-children
|
||||||
|
:wasm wasm
|
||||||
|
:scale scale}])))
|
||||||
(catch :default cause
|
(catch :default cause
|
||||||
(when-let [explain (-> cause ex-data ::sm/explain)]
|
(when-let [explain (-> cause ex-data ::sm/explain)]
|
||||||
(js/console.log "Unexpected error")
|
(js/console.log "Unexpected error")
|
||||||
|
|||||||
@ -23,7 +23,6 @@
|
|||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.main.refs :as refs]
|
[app.main.refs :as refs]
|
||||||
[app.main.render :as render]
|
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.main.ui.shapes.text]
|
[app.main.ui.shapes.text]
|
||||||
[app.main.worker :as mw]
|
[app.main.worker :as mw]
|
||||||
@ -110,6 +109,9 @@
|
|||||||
(def noop-fn
|
(def noop-fn
|
||||||
(constantly nil))
|
(constantly nil))
|
||||||
|
|
||||||
|
;;
|
||||||
|
(def shape-wrapper-factory nil)
|
||||||
|
|
||||||
(defn- yield-to-browser
|
(defn- yield-to-browser
|
||||||
"Returns a promise that resolves after yielding to the browser's event loop.
|
"Returns a promise that resolves after yielding to the browser's event loop.
|
||||||
Uses requestAnimationFrame for smooth visual updates during loading."
|
Uses requestAnimationFrame for smooth visual updates during loading."
|
||||||
@ -125,7 +127,7 @@
|
|||||||
(let [objects (mf/deref refs/workspace-page-objects)
|
(let [objects (mf/deref refs/workspace-page-objects)
|
||||||
shape-wrapper
|
shape-wrapper
|
||||||
(mf/with-memo [shape]
|
(mf/with-memo [shape]
|
||||||
(render/shape-wrapper-factory objects))]
|
(shape-wrapper-factory objects))]
|
||||||
|
|
||||||
[:svg {:version "1.1"
|
[:svg {:version "1.1"
|
||||||
:xmlns "http://www.w3.org/2000/svg"
|
:xmlns "http://www.w3.org/2000/svg"
|
||||||
@ -1010,62 +1012,62 @@
|
|||||||
(defn set-object
|
(defn set-object
|
||||||
[shape]
|
[shape]
|
||||||
(perf/begin-measure "set-object")
|
(perf/begin-measure "set-object")
|
||||||
(let [shape (svg-filters/apply-svg-derived shape)
|
(when shape
|
||||||
id (dm/get-prop shape :id)
|
(let [shape (svg-filters/apply-svg-derived shape)
|
||||||
type (dm/get-prop shape :type)
|
id (dm/get-prop shape :id)
|
||||||
|
type (dm/get-prop shape :type)
|
||||||
|
|
||||||
masked (get shape :masked-group)
|
masked (get shape :masked-group)
|
||||||
|
|
||||||
fills (get shape :fills)
|
fills (get shape :fills)
|
||||||
strokes (if (= type :group)
|
strokes (if (= type :group)
|
||||||
[] (get shape :strokes))
|
[] (get shape :strokes))
|
||||||
children (get shape :shapes)
|
children (get shape :shapes)
|
||||||
content (let [content (get shape :content)]
|
content (let [content (get shape :content)]
|
||||||
(if (= type :text)
|
(if (= type :text)
|
||||||
(ensure-text-content content)
|
(ensure-text-content content)
|
||||||
content))
|
content))
|
||||||
bool-type (get shape :bool-type)
|
bool-type (get shape :bool-type)
|
||||||
grow-type (get shape :grow-type)
|
grow-type (get shape :grow-type)
|
||||||
blur (get shape :blur)
|
blur (get shape :blur)
|
||||||
svg-attrs (get shape :svg-attrs)
|
svg-attrs (get shape :svg-attrs)
|
||||||
shadows (get shape :shadow)]
|
shadows (get shape :shadow)]
|
||||||
|
|
||||||
(shapes/set-shape-base-props shape)
|
(shapes/set-shape-base-props shape)
|
||||||
|
|
||||||
;; Remaining properties that need separate calls (variable-length or conditional)
|
;; Remaining properties that need separate calls (variable-length or conditional)
|
||||||
(set-shape-children children)
|
(set-shape-children children)
|
||||||
(set-shape-blur blur)
|
(set-shape-blur blur)
|
||||||
(when (= type :group)
|
(when (= type :group)
|
||||||
(set-masked (boolean masked)))
|
(set-masked (boolean masked)))
|
||||||
(when (= type :bool)
|
(when (= type :bool)
|
||||||
(set-shape-bool-type bool-type))
|
(set-shape-bool-type bool-type))
|
||||||
(when (and (some? content)
|
(when (and (some? content)
|
||||||
(or (= type :path)
|
(or (= type :path)
|
||||||
(= type :bool)))
|
(= type :bool)))
|
||||||
(set-shape-path-content content))
|
(set-shape-path-content content))
|
||||||
(when (some? svg-attrs)
|
(when (some? svg-attrs)
|
||||||
(set-shape-svg-attrs svg-attrs))
|
(set-shape-svg-attrs svg-attrs))
|
||||||
(when (and (some? content) (= type :svg-raw))
|
(when (and (some? content) (= type :svg-raw))
|
||||||
(set-shape-svg-raw-content (get-static-markup shape)))
|
(set-shape-svg-raw-content (get-static-markup shape)))
|
||||||
(set-shape-shadows shadows)
|
(set-shape-shadows shadows)
|
||||||
(when (= type :text)
|
(when (= type :text)
|
||||||
(set-shape-grow-type grow-type))
|
(set-shape-grow-type grow-type))
|
||||||
|
|
||||||
(set-shape-layout shape)
|
(set-shape-layout shape)
|
||||||
(set-layout-data shape)
|
(set-layout-data shape)
|
||||||
|
(let [pending_thumbnails (into [] (concat
|
||||||
(let [pending_thumbnails (into [] (concat
|
(set-shape-text-content id content)
|
||||||
(set-shape-text-content id content)
|
(set-shape-text-images id content true)
|
||||||
(set-shape-text-images id content true)
|
(set-shape-fills id fills true)
|
||||||
(set-shape-fills id fills true)
|
(set-shape-strokes id strokes true)))
|
||||||
(set-shape-strokes id strokes true)))
|
pending_full (into [] (concat
|
||||||
pending_full (into [] (concat
|
(set-shape-text-images id content false)
|
||||||
(set-shape-text-images id content false)
|
(set-shape-fills id fills false)
|
||||||
(set-shape-fills id fills false)
|
(set-shape-strokes id strokes false)))]
|
||||||
(set-shape-strokes id strokes false)))]
|
(perf/end-measure "set-object")
|
||||||
(perf/end-measure "set-object")
|
{:thumbnails pending_thumbnails
|
||||||
{:thumbnails pending_thumbnails
|
:full pending_full}))))
|
||||||
:full pending_full})))
|
|
||||||
|
|
||||||
(defn update-text-layouts
|
(defn update-text-layouts
|
||||||
[shapes]
|
[shapes]
|
||||||
@ -1375,9 +1377,11 @@
|
|||||||
|
|
||||||
(defn initialize-viewport
|
(defn initialize-viewport
|
||||||
([base-objects zoom vbox background]
|
([base-objects zoom vbox background]
|
||||||
(initialize-viewport base-objects zoom vbox background nil))
|
(initialize-viewport base-objects zoom vbox background 1 nil))
|
||||||
([base-objects zoom vbox background callback]
|
([base-objects zoom vbox background callback]
|
||||||
(let [rgba (sr-clr/hex->u32argb background 1)
|
(initialize-viewport base-objects zoom vbox background 1 callback))
|
||||||
|
([base-objects zoom vbox background background-opacity callback]
|
||||||
|
(let [rgba (sr-clr/hex->u32argb background background-opacity)
|
||||||
shapes (into [] (vals base-objects))
|
shapes (into [] (vals base-objects))
|
||||||
total-shapes (count shapes)]
|
total-shapes (count shapes)]
|
||||||
(h/call wasm/internal-module "_set_canvas_background" rgba)
|
(h/call wasm/internal-module "_set_canvas_background" rgba)
|
||||||
@ -1654,6 +1658,24 @@
|
|||||||
(let [controls-to-blur (dom/query-all (dom/get-element "viewport-controls") ".blurrable")]
|
(let [controls-to-blur (dom/query-all (dom/get-element "viewport-controls") ".blurrable")]
|
||||||
(run! #(dom/set-style! % "filter" "blur(4px)") controls-to-blur)))
|
(run! #(dom/set-style! % "filter" "blur(4px)") controls-to-blur)))
|
||||||
|
|
||||||
|
(defn render-shape-pixels
|
||||||
|
[shape-id scale]
|
||||||
|
(let [buffer (uuid/get-u32 shape-id)
|
||||||
|
|
||||||
|
offset
|
||||||
|
(h/call wasm/internal-module "_render_shape_pixels"
|
||||||
|
(aget buffer 0)
|
||||||
|
(aget buffer 1)
|
||||||
|
(aget buffer 2)
|
||||||
|
(aget buffer 3)
|
||||||
|
scale)
|
||||||
|
|
||||||
|
heap (mem/get-heap-u8)
|
||||||
|
heapu32 (mem/get-heap-u32)
|
||||||
|
length (aget heapu32 (mem/->offset-32 offset))
|
||||||
|
result (dr/read-image-bytes heap (+ offset 12) length)]
|
||||||
|
(mem/free)
|
||||||
|
result))
|
||||||
|
|
||||||
(defn init-wasm-module
|
(defn init-wasm-module
|
||||||
[module]
|
[module]
|
||||||
|
|||||||
@ -45,6 +45,10 @@
|
|||||||
:center (gpt/point cx cy)
|
:center (gpt/point cx cy)
|
||||||
:transform (gmt/matrix a b c d e f)}))
|
:transform (gmt/matrix a b c d e f)}))
|
||||||
|
|
||||||
|
(defn read-image-bytes
|
||||||
|
[heap offset length]
|
||||||
|
(.slice ^js heap offset (+ offset length)))
|
||||||
|
|
||||||
(defn read-position-data-entry
|
(defn read-position-data-entry
|
||||||
[heapu32 heapf32 offset]
|
[heapu32 heapf32 offset]
|
||||||
(let [paragraph (aget heapu32 (+ offset 0))
|
(let [paragraph (aget heapu32 (+ offset 0))
|
||||||
|
|||||||
@ -848,6 +848,31 @@ pub extern "C" fn end_temp_objects() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
#[wasm_error]
|
||||||
|
pub extern "C" fn render_shape_pixels(
|
||||||
|
a: u32,
|
||||||
|
b: u32,
|
||||||
|
c: u32,
|
||||||
|
d: u32,
|
||||||
|
scale: f32,
|
||||||
|
) -> Result<*mut u8> {
|
||||||
|
let id = uuid_from_u32_quartet(a, b, c, d);
|
||||||
|
|
||||||
|
with_state_mut!(state, {
|
||||||
|
let (data, width, height) =
|
||||||
|
state.render_shape_pixels(&id, scale, performance::get_time())?;
|
||||||
|
|
||||||
|
let len = data.len() as u32;
|
||||||
|
let mut buf = Vec::with_capacity(4 + data.len());
|
||||||
|
buf.extend_from_slice(&len.to_le_bytes());
|
||||||
|
buf.extend_from_slice(&width.to_le_bytes());
|
||||||
|
buf.extend_from_slice(&height.to_le_bytes());
|
||||||
|
buf.extend_from_slice(&data);
|
||||||
|
Ok(mem::write_bytes(buf))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
init_gl!();
|
init_gl!();
|
||||||
|
|||||||
@ -18,6 +18,7 @@ use std::borrow::Cow;
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use gpu_state::GpuState;
|
use gpu_state::GpuState;
|
||||||
|
|
||||||
use options::RenderOptions;
|
use options::RenderOptions;
|
||||||
pub use surfaces::{SurfaceId, Surfaces};
|
pub use surfaces::{SurfaceId, Surfaces};
|
||||||
|
|
||||||
@ -45,6 +46,7 @@ const BLUR_DOWNSCALE_THRESHOLD: f32 = 8.0;
|
|||||||
|
|
||||||
type ClipStack = Vec<(Rect, Option<Corners>, Matrix)>;
|
type ClipStack = Vec<(Rect, Option<Corners>, Matrix)>;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct NodeRenderState {
|
pub struct NodeRenderState {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
// We use this bool to keep that we've traversed all the children inside this node.
|
// We use this bool to keep that we've traversed all the children inside this node.
|
||||||
@ -305,6 +307,7 @@ pub(crate) struct RenderState {
|
|||||||
pub ignore_nested_blurs: bool,
|
pub ignore_nested_blurs: bool,
|
||||||
/// Preview render mode - when true, uses simplified rendering for progressive loading
|
/// Preview render mode - when true, uses simplified rendering for progressive loading
|
||||||
pub preview_mode: bool,
|
pub preview_mode: bool,
|
||||||
|
pub export_context: Option<(Rect, f32)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_cache_size(viewbox: Viewbox, scale: f32) -> skia::ISize {
|
pub fn get_cache_size(viewbox: Viewbox, scale: f32) -> skia::ISize {
|
||||||
@ -378,6 +381,7 @@ impl RenderState {
|
|||||||
touched_ids: HashSet::default(),
|
touched_ids: HashSet::default(),
|
||||||
ignore_nested_blurs: false,
|
ignore_nested_blurs: false,
|
||||||
preview_mode: false,
|
preview_mode: false,
|
||||||
|
export_context: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -645,7 +649,7 @@ impl RenderState {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn apply_drawing_to_render_canvas(&mut self, shape: Option<&Shape>) {
|
pub fn apply_drawing_to_render_canvas(&mut self, shape: Option<&Shape>, target: SurfaceId) {
|
||||||
performance::begin_measure!("apply_drawing_to_render_canvas");
|
performance::begin_measure!("apply_drawing_to_render_canvas");
|
||||||
|
|
||||||
let paint = skia::Paint::default();
|
let paint = skia::Paint::default();
|
||||||
@ -653,12 +657,12 @@ impl RenderState {
|
|||||||
// Only draw surfaces that have content (dirty flag optimization)
|
// Only draw surfaces that have content (dirty flag optimization)
|
||||||
if self.surfaces.is_dirty(SurfaceId::TextDropShadows) {
|
if self.surfaces.is_dirty(SurfaceId::TextDropShadows) {
|
||||||
self.surfaces
|
self.surfaces
|
||||||
.draw_into(SurfaceId::TextDropShadows, SurfaceId::Current, Some(&paint));
|
.draw_into(SurfaceId::TextDropShadows, target, Some(&paint));
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.surfaces.is_dirty(SurfaceId::Fills) {
|
if self.surfaces.is_dirty(SurfaceId::Fills) {
|
||||||
self.surfaces
|
self.surfaces
|
||||||
.draw_into(SurfaceId::Fills, SurfaceId::Current, Some(&paint));
|
.draw_into(SurfaceId::Fills, target, Some(&paint));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut render_overlay_below_strokes = false;
|
let mut render_overlay_below_strokes = false;
|
||||||
@ -668,17 +672,17 @@ impl RenderState {
|
|||||||
|
|
||||||
if render_overlay_below_strokes && self.surfaces.is_dirty(SurfaceId::InnerShadows) {
|
if render_overlay_below_strokes && self.surfaces.is_dirty(SurfaceId::InnerShadows) {
|
||||||
self.surfaces
|
self.surfaces
|
||||||
.draw_into(SurfaceId::InnerShadows, SurfaceId::Current, Some(&paint));
|
.draw_into(SurfaceId::InnerShadows, target, Some(&paint));
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.surfaces.is_dirty(SurfaceId::Strokes) {
|
if self.surfaces.is_dirty(SurfaceId::Strokes) {
|
||||||
self.surfaces
|
self.surfaces
|
||||||
.draw_into(SurfaceId::Strokes, SurfaceId::Current, Some(&paint));
|
.draw_into(SurfaceId::Strokes, target, Some(&paint));
|
||||||
}
|
}
|
||||||
|
|
||||||
if !render_overlay_below_strokes && self.surfaces.is_dirty(SurfaceId::InnerShadows) {
|
if !render_overlay_below_strokes && self.surfaces.is_dirty(SurfaceId::InnerShadows) {
|
||||||
self.surfaces
|
self.surfaces
|
||||||
.draw_into(SurfaceId::InnerShadows, SurfaceId::Current, Some(&paint));
|
.draw_into(SurfaceId::InnerShadows, target, Some(&paint));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build mask of dirty surfaces that need clearing
|
// Build mask of dirty surfaces that need clearing
|
||||||
@ -751,6 +755,7 @@ impl RenderState {
|
|||||||
offset: Option<(f32, f32)>,
|
offset: Option<(f32, f32)>,
|
||||||
parent_shadows: Option<Vec<skia_safe::Paint>>,
|
parent_shadows: Option<Vec<skia_safe::Paint>>,
|
||||||
outset: Option<f32>,
|
outset: Option<f32>,
|
||||||
|
target_surface: SurfaceId,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let surface_ids = fills_surface_id as u32
|
let surface_ids = fills_surface_id as u32
|
||||||
| strokes_surface_id as u32
|
| strokes_surface_id as u32
|
||||||
@ -795,28 +800,23 @@ impl RenderState {
|
|||||||
&& !shape
|
&& !shape
|
||||||
.svg_attrs
|
.svg_attrs
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|attrs| attrs.fill_none);
|
.is_some_and(|attrs| attrs.fill_none)
|
||||||
|
&& target_surface != SurfaceId::Export;
|
||||||
|
|
||||||
if can_render_directly {
|
if can_render_directly {
|
||||||
let scale = self.get_scale();
|
let scale = self.get_scale();
|
||||||
let translation = self
|
let translation = self
|
||||||
.surfaces
|
.surfaces
|
||||||
.get_render_context_translation(self.render_area, scale);
|
.get_render_context_translation(self.render_area, scale);
|
||||||
self.surfaces.apply_mut(SurfaceId::Current as u32, |s| {
|
|
||||||
|
self.surfaces.apply_mut(target_surface as u32, |s| {
|
||||||
let canvas = s.canvas();
|
let canvas = s.canvas();
|
||||||
canvas.save();
|
canvas.save();
|
||||||
canvas.scale((scale, scale));
|
canvas.scale((scale, scale));
|
||||||
canvas.translate(translation);
|
canvas.translate(translation);
|
||||||
});
|
});
|
||||||
|
|
||||||
fills::render(
|
fills::render(self, shape, &shape.fills, antialias, target_surface, None)?;
|
||||||
self,
|
|
||||||
shape,
|
|
||||||
&shape.fills,
|
|
||||||
antialias,
|
|
||||||
SurfaceId::Current,
|
|
||||||
None,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Pass strokes in natural order; stroke merging handles top-most ordering internally.
|
// Pass strokes in natural order; stroke merging handles top-most ordering internally.
|
||||||
let visible_strokes: Vec<&Stroke> = shape.visible_strokes().collect();
|
let visible_strokes: Vec<&Stroke> = shape.visible_strokes().collect();
|
||||||
@ -824,12 +824,12 @@ impl RenderState {
|
|||||||
self,
|
self,
|
||||||
shape,
|
shape,
|
||||||
&visible_strokes,
|
&visible_strokes,
|
||||||
Some(SurfaceId::Current),
|
Some(target_surface),
|
||||||
antialias,
|
antialias,
|
||||||
outset,
|
outset,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
self.surfaces.apply_mut(SurfaceId::Current as u32, |s| {
|
self.surfaces.apply_mut(target_surface as u32, |s| {
|
||||||
s.canvas().restore();
|
s.canvas().restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1289,7 +1289,7 @@ impl RenderState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if apply_to_current_surface {
|
if apply_to_current_surface {
|
||||||
self.apply_drawing_to_render_canvas(Some(&shape));
|
self.apply_drawing_to_render_canvas(Some(&shape), target_surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only restore if we saved (optimization for simple shapes)
|
// Only restore if we saved (optimization for simple shapes)
|
||||||
@ -1461,7 +1461,7 @@ impl RenderState {
|
|||||||
self.current_tile = None;
|
self.current_tile = None;
|
||||||
self.render_in_progress = true;
|
self.render_in_progress = true;
|
||||||
|
|
||||||
self.apply_drawing_to_render_canvas(None);
|
self.apply_drawing_to_render_canvas(None, SurfaceId::Current);
|
||||||
|
|
||||||
if sync_render {
|
if sync_render {
|
||||||
self.render_shape_tree_sync(base_object, tree, timestamp)?;
|
self.render_shape_tree_sync(base_object, tree, timestamp)?;
|
||||||
@ -1512,6 +1512,56 @@ impl RenderState {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn render_shape_pixels(
|
||||||
|
&mut self,
|
||||||
|
id: &Uuid,
|
||||||
|
tree: ShapesPoolRef,
|
||||||
|
scale: f32,
|
||||||
|
timestamp: i32,
|
||||||
|
) -> Result<(Vec<u8>, i32, i32)> {
|
||||||
|
let target_surface = SurfaceId::Export;
|
||||||
|
|
||||||
|
self.surfaces
|
||||||
|
.canvas(target_surface)
|
||||||
|
.clear(skia::Color::TRANSPARENT);
|
||||||
|
|
||||||
|
if tree.len() != 0 {
|
||||||
|
let shape = tree.get(id).unwrap();
|
||||||
|
let mut extrect = shape.extrect(tree, scale);
|
||||||
|
self.export_context = Some((extrect, scale));
|
||||||
|
let margins = self.surfaces.margins;
|
||||||
|
extrect.offset((margins.width as f32 / scale, margins.height as f32 / scale));
|
||||||
|
|
||||||
|
self.surfaces.resize_export_surface(scale, extrect);
|
||||||
|
self.surfaces.update_render_context(extrect, scale);
|
||||||
|
|
||||||
|
self.pending_nodes.push(NodeRenderState {
|
||||||
|
id: *id,
|
||||||
|
visited_children: false,
|
||||||
|
clip_bounds: None,
|
||||||
|
visited_mask: false,
|
||||||
|
mask: false,
|
||||||
|
flattened: false,
|
||||||
|
});
|
||||||
|
self.render_shape_tree_partial_uncached(tree, timestamp, false, true)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.surfaces
|
||||||
|
.flush_and_submit(&mut self.gpu_state, target_surface);
|
||||||
|
|
||||||
|
let image = self.surfaces.snapshot(target_surface);
|
||||||
|
let data = image
|
||||||
|
.encode(
|
||||||
|
&mut self.gpu_state.context,
|
||||||
|
skia::EncodedImageFormat::PNG,
|
||||||
|
100,
|
||||||
|
)
|
||||||
|
.expect("PNG encode failed");
|
||||||
|
let skia::ISize { width, height } = image.dimensions();
|
||||||
|
|
||||||
|
Ok((data.as_bytes().to_vec(), width, height))
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn should_stop_rendering(&self, iteration: i32, timestamp: i32) -> bool {
|
pub fn should_stop_rendering(&self, iteration: i32, timestamp: i32) -> bool {
|
||||||
iteration % NODE_BATCH_THRESHOLD == 0
|
iteration % NODE_BATCH_THRESHOLD == 0
|
||||||
@ -1519,7 +1569,7 @@ impl RenderState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn render_shape_enter(&mut self, element: &Shape, mask: bool) {
|
pub fn render_shape_enter(&mut self, element: &Shape, mask: bool, target_surface: SurfaceId) {
|
||||||
// Masked groups needs two rendering passes, the first one rendering
|
// Masked groups needs two rendering passes, the first one rendering
|
||||||
// the content and the second one rendering the mask so we need to do
|
// the content and the second one rendering the mask so we need to do
|
||||||
// an extra save_layer to keep all the masked group separate from
|
// an extra save_layer to keep all the masked group separate from
|
||||||
@ -1533,9 +1583,7 @@ impl RenderState {
|
|||||||
if group.masked {
|
if group.masked {
|
||||||
let paint = skia::Paint::default();
|
let paint = skia::Paint::default();
|
||||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
|
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
|
||||||
self.surfaces
|
self.surfaces.canvas(target_surface).save_layer(&layer_rec);
|
||||||
.canvas(SurfaceId::Current)
|
|
||||||
.save_layer(&layer_rec);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1550,9 +1598,7 @@ impl RenderState {
|
|||||||
let mut mask_paint = skia::Paint::default();
|
let mut mask_paint = skia::Paint::default();
|
||||||
mask_paint.set_blend_mode(skia::BlendMode::DstIn);
|
mask_paint.set_blend_mode(skia::BlendMode::DstIn);
|
||||||
let mask_rec = skia::canvas::SaveLayerRec::default().paint(&mask_paint);
|
let mask_rec = skia::canvas::SaveLayerRec::default().paint(&mask_paint);
|
||||||
self.surfaces
|
self.surfaces.canvas(target_surface).save_layer(&mask_rec);
|
||||||
.canvas(SurfaceId::Current)
|
|
||||||
.save_layer(&mask_rec);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only create save_layer if actually needed
|
// Only create save_layer if actually needed
|
||||||
@ -1579,9 +1625,7 @@ impl RenderState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
|
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
|
||||||
self.surfaces
|
self.surfaces.canvas(target_surface).save_layer(&layer_rec);
|
||||||
.canvas(SurfaceId::Current)
|
|
||||||
.save_layer(&layer_rec);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.focus_mode.enter(&element.id);
|
self.focus_mode.enter(&element.id);
|
||||||
@ -1593,6 +1637,7 @@ impl RenderState {
|
|||||||
element: &Shape,
|
element: &Shape,
|
||||||
visited_mask: bool,
|
visited_mask: bool,
|
||||||
clip_bounds: Option<ClipStack>,
|
clip_bounds: Option<ClipStack>,
|
||||||
|
target_surface: SurfaceId,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if visited_mask {
|
if visited_mask {
|
||||||
// Because masked groups needs two rendering passes (first drawing
|
// Because masked groups needs two rendering passes (first drawing
|
||||||
@ -1600,7 +1645,7 @@ impl RenderState {
|
|||||||
// extra restore.
|
// extra restore.
|
||||||
if let Type::Group(group) = element.shape_type {
|
if let Type::Group(group) = element.shape_type {
|
||||||
if group.masked {
|
if group.masked {
|
||||||
self.surfaces.canvas(SurfaceId::Current).restore();
|
self.surfaces.canvas(target_surface).restore();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -1664,6 +1709,7 @@ impl RenderState {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
target_surface,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1672,7 +1718,7 @@ impl RenderState {
|
|||||||
let needs_layer = element.needs_layer();
|
let needs_layer = element.needs_layer();
|
||||||
|
|
||||||
if needs_layer {
|
if needs_layer {
|
||||||
self.surfaces.canvas(SurfaceId::Current).restore();
|
self.surfaces.canvas(target_surface).restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
self.focus_mode.exit(&element.id);
|
self.focus_mode.exit(&element.id);
|
||||||
@ -1758,8 +1804,8 @@ impl RenderState {
|
|||||||
shadow: &Shadow,
|
shadow: &Shadow,
|
||||||
clip_bounds: Option<ClipStack>,
|
clip_bounds: Option<ClipStack>,
|
||||||
scale: f32,
|
scale: f32,
|
||||||
translation: (f32, f32),
|
|
||||||
extra_layer_blur: Option<Blur>,
|
extra_layer_blur: Option<Blur>,
|
||||||
|
target_surface: SurfaceId,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut transformed_shadow: Cow<Shadow> = Cow::Borrowed(shadow);
|
let mut transformed_shadow: Cow<Shadow> = Cow::Borrowed(shadow);
|
||||||
transformed_shadow.to_mut().offset = (0.0, 0.0);
|
transformed_shadow.to_mut().offset = (0.0, 0.0);
|
||||||
@ -1822,7 +1868,8 @@ impl RenderState {
|
|||||||
// Account for the shadow offset so the temporary surface fully contains the shifted blur.
|
// Account for the shadow offset so the temporary surface fully contains the shifted blur.
|
||||||
bounds.offset(world_offset);
|
bounds.offset(world_offset);
|
||||||
// Early cull if the shadow bounds are outside the render area.
|
// Early cull if the shadow bounds are outside the render area.
|
||||||
if !bounds.intersects(self.render_area_with_margins) {
|
if !bounds.intersects(self.render_area_with_margins) && target_surface != SurfaceId::Export
|
||||||
|
{
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1830,8 +1877,8 @@ impl RenderState {
|
|||||||
if scale > 1.0 && shadow.blur <= 0.0 {
|
if scale > 1.0 && shadow.blur <= 0.0 {
|
||||||
let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows);
|
let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows);
|
||||||
drop_canvas.save();
|
drop_canvas.save();
|
||||||
drop_canvas.scale((scale, scale));
|
//drop_canvas.scale((scale, scale));
|
||||||
drop_canvas.translate(translation);
|
//drop_canvas.translate(translation);
|
||||||
|
|
||||||
self.with_nested_blurs_suppressed(|state| {
|
self.with_nested_blurs_suppressed(|state| {
|
||||||
state.render_shape(
|
state.render_shape(
|
||||||
@ -1845,6 +1892,7 @@ impl RenderState {
|
|||||||
Some(shadow.offset),
|
Some(shadow.offset),
|
||||||
None,
|
None,
|
||||||
Some(shadow.spread),
|
Some(shadow.spread),
|
||||||
|
target_surface,
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@ -1872,8 +1920,8 @@ impl RenderState {
|
|||||||
if use_low_zoom_path {
|
if use_low_zoom_path {
|
||||||
let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows);
|
let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows);
|
||||||
drop_canvas.save_layer(&layer_rec);
|
drop_canvas.save_layer(&layer_rec);
|
||||||
drop_canvas.scale((scale, scale));
|
//drop_canvas.scale((scale, scale));
|
||||||
drop_canvas.translate(translation);
|
//drop_canvas.translate(translation);
|
||||||
|
|
||||||
self.with_nested_blurs_suppressed(|state| {
|
self.with_nested_blurs_suppressed(|state| {
|
||||||
state.render_shape(
|
state.render_shape(
|
||||||
@ -1887,6 +1935,7 @@ impl RenderState {
|
|||||||
Some(shadow.offset), // Offset is geometric
|
Some(shadow.offset), // Offset is geometric
|
||||||
None,
|
None,
|
||||||
Some(shadow.spread),
|
Some(shadow.spread),
|
||||||
|
target_surface,
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@ -1928,6 +1977,7 @@ impl RenderState {
|
|||||||
Some(shadow.offset), // Offset is geometric
|
Some(shadow.offset), // Offset is geometric
|
||||||
None,
|
None,
|
||||||
Some(shadow.spread),
|
Some(shadow.spread),
|
||||||
|
target_surface,
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@ -1939,8 +1989,8 @@ impl RenderState {
|
|||||||
if let Some((mut surface, filter_scale)) = filter_result {
|
if let Some((mut surface, filter_scale)) = filter_result {
|
||||||
let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows);
|
let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows);
|
||||||
drop_canvas.save();
|
drop_canvas.save();
|
||||||
drop_canvas.scale((scale, scale));
|
//drop_canvas.scale((scale, scale));
|
||||||
drop_canvas.translate(translation);
|
//drop_canvas.translate(translation);
|
||||||
let mut drop_paint = skia::Paint::default();
|
let mut drop_paint = skia::Paint::default();
|
||||||
drop_paint.set_image_filter(blur_filter.clone());
|
drop_paint.set_image_filter(blur_filter.clone());
|
||||||
|
|
||||||
@ -1969,6 +2019,7 @@ impl RenderState {
|
|||||||
}
|
}
|
||||||
drop_canvas.restore();
|
drop_canvas.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1984,6 +2035,7 @@ impl RenderState {
|
|||||||
scale: f32,
|
scale: f32,
|
||||||
translation: (f32, f32),
|
translation: (f32, f32),
|
||||||
node_render_state: &NodeRenderState,
|
node_render_state: &NodeRenderState,
|
||||||
|
target_surface: SurfaceId,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let element_extrect = extrect.get_or_insert_with(|| element.extrect(tree, scale));
|
let element_extrect = extrect.get_or_insert_with(|| element.extrect(tree, scale));
|
||||||
let inherited_layer_blur = match element.shape_type {
|
let inherited_layer_blur = match element.shape_type {
|
||||||
@ -2004,8 +2056,8 @@ impl RenderState {
|
|||||||
shadow,
|
shadow,
|
||||||
clip_bounds.clone(),
|
clip_bounds.clone(),
|
||||||
scale,
|
scale,
|
||||||
translation,
|
|
||||||
None,
|
None,
|
||||||
|
target_surface,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
if !matches!(element.shape_type, Type::Bool(_)) {
|
if !matches!(element.shape_type, Type::Bool(_)) {
|
||||||
@ -2033,8 +2085,8 @@ impl RenderState {
|
|||||||
shadow,
|
shadow,
|
||||||
nested_clip_bounds,
|
nested_clip_bounds,
|
||||||
scale,
|
scale,
|
||||||
translation,
|
|
||||||
inherited_layer_blur,
|
inherited_layer_blur,
|
||||||
|
target_surface,
|
||||||
)?;
|
)?;
|
||||||
} else {
|
} else {
|
||||||
let paint = skia::Paint::default();
|
let paint = skia::Paint::default();
|
||||||
@ -2071,6 +2123,7 @@ impl RenderState {
|
|||||||
None,
|
None,
|
||||||
Some(vec![new_shadow_paint.clone()]),
|
Some(vec![new_shadow_paint.clone()]),
|
||||||
None,
|
None,
|
||||||
|
target_surface,
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
self.surfaces.canvas(SurfaceId::DropShadows).restore();
|
self.surfaces.canvas(SurfaceId::DropShadows).restore();
|
||||||
@ -2084,48 +2137,75 @@ impl RenderState {
|
|||||||
self.surfaces
|
self.surfaces
|
||||||
.canvas(SurfaceId::DropShadows)
|
.canvas(SurfaceId::DropShadows)
|
||||||
.draw_paint(&paint);
|
.draw_paint(&paint);
|
||||||
|
|
||||||
self.surfaces.canvas(SurfaceId::DropShadows).restore();
|
self.surfaces.canvas(SurfaceId::DropShadows).restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(clips) = clip_bounds.as_ref() {
|
if let Some(clips) = clip_bounds.as_ref() {
|
||||||
let antialias = element.should_use_antialias(scale);
|
let antialias = element.should_use_antialias(scale);
|
||||||
self.surfaces.canvas(SurfaceId::Current).save();
|
self.surfaces.canvas(target_surface).save();
|
||||||
for (bounds, corners, transform) in clips.iter() {
|
for (bounds, corners, transform) in clips.iter() {
|
||||||
let mut total_matrix = Matrix::new_identity();
|
if target_surface == SurfaceId::Export {
|
||||||
total_matrix.pre_scale((scale, scale), None);
|
let Some((export_rect, export_scale)) = self.export_context else {
|
||||||
total_matrix.pre_translate((translation.0, translation.1));
|
continue;
|
||||||
total_matrix.pre_concat(transform);
|
};
|
||||||
|
|
||||||
self.surfaces
|
let mut total_matrix = Matrix::new_identity();
|
||||||
.canvas(SurfaceId::Current)
|
|
||||||
.concat(&total_matrix);
|
|
||||||
|
|
||||||
if let Some(corners) = corners {
|
total_matrix.pre_scale((export_scale, export_scale), None);
|
||||||
let rrect = RRect::new_rect_radii(*bounds, corners);
|
total_matrix.pre_translate((-export_rect.x(), -export_rect.y()));
|
||||||
self.surfaces.canvas(SurfaceId::Current).clip_rrect(
|
|
||||||
rrect,
|
total_matrix.pre_concat(transform);
|
||||||
skia::ClipOp::Intersect,
|
|
||||||
antialias,
|
let canvas = self.surfaces.canvas(target_surface);
|
||||||
);
|
canvas.concat(&total_matrix);
|
||||||
|
|
||||||
|
let bounds = *bounds;
|
||||||
|
if let Some(corners) = corners {
|
||||||
|
let rrect = RRect::new_rect_radii(bounds, corners);
|
||||||
|
canvas.clip_rrect(rrect, skia::ClipOp::Intersect, antialias);
|
||||||
|
} else {
|
||||||
|
canvas.clip_rect(bounds, skia::ClipOp::Intersect, antialias);
|
||||||
|
}
|
||||||
|
self.surfaces
|
||||||
|
.canvas(target_surface)
|
||||||
|
.concat(&total_matrix.invert().unwrap_or_default());
|
||||||
} else {
|
} else {
|
||||||
self.surfaces.canvas(SurfaceId::Current).clip_rect(
|
let mut total_matrix = Matrix::new_identity();
|
||||||
*bounds,
|
total_matrix.pre_scale((scale, scale), None);
|
||||||
skia::ClipOp::Intersect,
|
total_matrix.pre_translate((translation.0, translation.1));
|
||||||
antialias,
|
total_matrix.pre_concat(transform);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.surfaces
|
self.surfaces.canvas(target_surface).concat(&total_matrix);
|
||||||
.canvas(SurfaceId::Current)
|
|
||||||
.concat(&total_matrix.invert().unwrap_or_default());
|
if let Some(corners) = corners {
|
||||||
|
let rrect = RRect::new_rect_radii(*bounds, corners);
|
||||||
|
self.surfaces.canvas(target_surface).clip_rrect(
|
||||||
|
rrect,
|
||||||
|
skia::ClipOp::Intersect,
|
||||||
|
antialias,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
self.surfaces.canvas(target_surface).clip_rect(
|
||||||
|
*bounds,
|
||||||
|
skia::ClipOp::Intersect,
|
||||||
|
antialias,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.surfaces
|
||||||
|
.canvas(target_surface)
|
||||||
|
.concat(&total_matrix.invert().unwrap_or_default());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.surfaces
|
self.surfaces
|
||||||
.draw_into(SurfaceId::DropShadows, SurfaceId::Current, None);
|
.draw_into(SurfaceId::DropShadows, target_surface, None);
|
||||||
self.surfaces.canvas(SurfaceId::Current).restore();
|
self.surfaces.canvas(target_surface).restore();
|
||||||
} else {
|
} else {
|
||||||
self.surfaces
|
self.surfaces
|
||||||
.draw_into(SurfaceId::DropShadows, SurfaceId::Current, None);
|
.draw_into(SurfaceId::DropShadows, target_surface, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.surfaces
|
self.surfaces
|
||||||
.canvas(SurfaceId::DropShadows)
|
.canvas(SurfaceId::DropShadows)
|
||||||
.clear(skia::Color::TRANSPARENT);
|
.clear(skia::Color::TRANSPARENT);
|
||||||
@ -2137,10 +2217,16 @@ impl RenderState {
|
|||||||
tree: ShapesPoolRef,
|
tree: ShapesPoolRef,
|
||||||
timestamp: i32,
|
timestamp: i32,
|
||||||
allow_stop: bool,
|
allow_stop: bool,
|
||||||
|
export: bool,
|
||||||
) -> Result<(bool, bool)> {
|
) -> Result<(bool, bool)> {
|
||||||
let mut iteration = 0;
|
let mut iteration = 0;
|
||||||
let mut is_empty = true;
|
let mut is_empty = true;
|
||||||
|
|
||||||
|
let mut target_surface = SurfaceId::Current;
|
||||||
|
if export {
|
||||||
|
target_surface = SurfaceId::Export;
|
||||||
|
}
|
||||||
|
|
||||||
while let Some(node_render_state) = self.pending_nodes.pop() {
|
while let Some(node_render_state) = self.pending_nodes.pop() {
|
||||||
let node_id = node_render_state.id;
|
let node_id = node_render_state.id;
|
||||||
let visited_children = node_render_state.visited_children;
|
let visited_children = node_render_state.visited_children;
|
||||||
@ -2165,7 +2251,7 @@ impl RenderState {
|
|||||||
|
|
||||||
if visited_children {
|
if visited_children {
|
||||||
if !node_render_state.flattened {
|
if !node_render_state.flattened {
|
||||||
self.render_shape_exit(element, visited_mask, clip_bounds)?;
|
self.render_shape_exit(element, visited_mask, clip_bounds, target_surface)?;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -2188,16 +2274,17 @@ impl RenderState {
|
|||||||
|
|
||||||
let has_effects = transformed_element.has_effects_that_extend_bounds();
|
let has_effects = transformed_element.has_effects_that_extend_bounds();
|
||||||
|
|
||||||
let is_visible = if is_container || has_effects {
|
let is_visible = export
|
||||||
let element_extrect =
|
|| if is_container || has_effects {
|
||||||
extrect.get_or_insert_with(|| transformed_element.extrect(tree, scale));
|
let element_extrect =
|
||||||
element_extrect.intersects(self.render_area_with_margins)
|
extrect.get_or_insert_with(|| transformed_element.extrect(tree, scale));
|
||||||
&& !transformed_element.visually_insignificant(scale, tree)
|
element_extrect.intersects(self.render_area_with_margins)
|
||||||
} else {
|
&& !transformed_element.visually_insignificant(scale, tree)
|
||||||
let selrect = transformed_element.selrect();
|
} else {
|
||||||
selrect.intersects(self.render_area_with_margins)
|
let selrect = transformed_element.selrect();
|
||||||
&& !transformed_element.visually_insignificant(scale, tree)
|
selrect.intersects(self.render_area_with_margins)
|
||||||
};
|
&& !transformed_element.visually_insignificant(scale, tree)
|
||||||
|
};
|
||||||
|
|
||||||
if self.options.is_debug_visible() {
|
if self.options.is_debug_visible() {
|
||||||
let shape_extrect_bounds = self.get_shape_extrect_bounds(element, tree);
|
let shape_extrect_bounds = self.get_shape_extrect_bounds(element, tree);
|
||||||
@ -2231,6 +2318,7 @@ impl RenderState {
|
|||||||
let translation = self
|
let translation = self
|
||||||
.surfaces
|
.surfaces
|
||||||
.get_render_context_translation(self.render_area, scale);
|
.get_render_context_translation(self.render_area, scale);
|
||||||
|
|
||||||
self.render_element_drop_shadows_and_composite(
|
self.render_element_drop_shadows_and_composite(
|
||||||
element,
|
element,
|
||||||
tree,
|
tree,
|
||||||
@ -2239,6 +2327,7 @@ impl RenderState {
|
|||||||
scale,
|
scale,
|
||||||
translation,
|
translation,
|
||||||
&node_render_state,
|
&node_render_state,
|
||||||
|
target_surface,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2254,7 +2343,7 @@ impl RenderState {
|
|||||||
self.render_background_blur(element);
|
self.render_background_blur(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.render_shape_enter(element, mask);
|
self.render_shape_enter(element, mask, target_surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !node_render_state.is_root() && self.focus_mode.is_active() {
|
if !node_render_state.is_root() && self.focus_mode.is_active() {
|
||||||
@ -2281,6 +2370,7 @@ impl RenderState {
|
|||||||
scale,
|
scale,
|
||||||
translation,
|
translation,
|
||||||
&node_render_state,
|
&node_render_state,
|
||||||
|
target_surface,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2295,13 +2385,14 @@ impl RenderState {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
target_surface,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
self.surfaces
|
self.surfaces
|
||||||
.canvas(SurfaceId::DropShadows)
|
.canvas(SurfaceId::DropShadows)
|
||||||
.clear(skia::Color::TRANSPARENT);
|
.clear(skia::Color::TRANSPARENT);
|
||||||
} else if visited_children {
|
} else if visited_children {
|
||||||
self.apply_drawing_to_render_canvas(Some(element));
|
self.apply_drawing_to_render_canvas(Some(element), target_surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip nested state updates for flattened containers
|
// Skip nested state updates for flattened containers
|
||||||
@ -2433,7 +2524,7 @@ impl RenderState {
|
|||||||
let tile_is_visible = self.tile_viewbox.is_visible(¤t_tile);
|
let tile_is_visible = self.tile_viewbox.is_visible(¤t_tile);
|
||||||
let can_stop = allow_stop && !tile_is_visible;
|
let can_stop = allow_stop && !tile_is_visible;
|
||||||
let (is_empty, early_return) =
|
let (is_empty, early_return) =
|
||||||
self.render_shape_tree_partial_uncached(tree, timestamp, can_stop)?;
|
self.render_shape_tree_partial_uncached(tree, timestamp, can_stop, false)?;
|
||||||
|
|
||||||
if early_return {
|
if early_return {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|||||||
@ -123,4 +123,39 @@ impl GpuState {
|
|||||||
|
|
||||||
Ok(surface)
|
Ok(surface)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn create_surface_from_texture(
|
||||||
|
&mut self,
|
||||||
|
width: i32,
|
||||||
|
height: i32,
|
||||||
|
texture_id: u32,
|
||||||
|
) -> skia::Surface {
|
||||||
|
let texture_info = TextureInfo {
|
||||||
|
target: gl::TEXTURE_2D,
|
||||||
|
id: texture_id,
|
||||||
|
format: gl::RGBA8,
|
||||||
|
protected: skia::gpu::Protected::No,
|
||||||
|
};
|
||||||
|
|
||||||
|
let backend_texture = unsafe {
|
||||||
|
gpu::backend_textures::make_gl(
|
||||||
|
(width, height),
|
||||||
|
gpu::Mipmapped::No,
|
||||||
|
texture_info,
|
||||||
|
String::from("export_texture"),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
gpu::surfaces::wrap_backend_texture(
|
||||||
|
&mut self.context,
|
||||||
|
&backend_texture,
|
||||||
|
gpu::SurfaceOrigin::BottomLeft,
|
||||||
|
None,
|
||||||
|
skia::ColorType::RGBA8888,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,17 +18,18 @@ const TILE_SIZE_MULTIPLIER: i32 = 2;
|
|||||||
#[repr(u32)]
|
#[repr(u32)]
|
||||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||||
pub enum SurfaceId {
|
pub enum SurfaceId {
|
||||||
Target = 0b00_0000_0001,
|
Target = 0b000_0000_0001,
|
||||||
Filter = 0b00_0000_0010,
|
Filter = 0b000_0000_0010,
|
||||||
Cache = 0b00_0000_0100,
|
Cache = 0b000_0000_0100,
|
||||||
Current = 0b00_0000_1000,
|
Current = 0b000_0000_1000,
|
||||||
Fills = 0b00_0001_0000,
|
Fills = 0b000_0001_0000,
|
||||||
Strokes = 0b00_0010_0000,
|
Strokes = 0b000_0010_0000,
|
||||||
DropShadows = 0b00_0100_0000,
|
DropShadows = 0b000_0100_0000,
|
||||||
InnerShadows = 0b00_1000_0000,
|
InnerShadows = 0b000_1000_0000,
|
||||||
TextDropShadows = 0b01_0000_0000,
|
TextDropShadows = 0b001_0000_0000,
|
||||||
UI = 0b10_0000_0000,
|
Export = 0b010_0000_0000,
|
||||||
Debug = 0b10_0000_0001,
|
UI = 0b100_0000_0000,
|
||||||
|
Debug = 0b100_0000_0001,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Surfaces {
|
pub struct Surfaces {
|
||||||
@ -53,11 +54,15 @@ pub struct Surfaces {
|
|||||||
// for drawing debug info.
|
// for drawing debug info.
|
||||||
debug: skia::Surface,
|
debug: skia::Surface,
|
||||||
// for drawing tiles.
|
// for drawing tiles.
|
||||||
|
export: skia::Surface,
|
||||||
|
|
||||||
tiles: TileTextureCache,
|
tiles: TileTextureCache,
|
||||||
sampling_options: skia::SamplingOptions,
|
sampling_options: skia::SamplingOptions,
|
||||||
margins: skia::ISize,
|
pub margins: skia::ISize,
|
||||||
// Tracks which surfaces have content (dirty flag bitmask)
|
// Tracks which surfaces have content (dirty flag bitmask)
|
||||||
dirty_surfaces: u32,
|
dirty_surfaces: u32,
|
||||||
|
|
||||||
|
extra_tile_dims: skia::ISize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@ -79,6 +84,7 @@ impl Surfaces {
|
|||||||
let cache = gpu_state.create_surface_with_dimensions("cache".to_string(), width, height)?;
|
let cache = gpu_state.create_surface_with_dimensions("cache".to_string(), width, height)?;
|
||||||
let current =
|
let current =
|
||||||
gpu_state.create_surface_with_isize("current".to_string(), extra_tile_dims)?;
|
gpu_state.create_surface_with_isize("current".to_string(), extra_tile_dims)?;
|
||||||
|
|
||||||
let drop_shadows =
|
let drop_shadows =
|
||||||
gpu_state.create_surface_with_isize("drop_shadows".to_string(), extra_tile_dims)?;
|
gpu_state.create_surface_with_isize("drop_shadows".to_string(), extra_tile_dims)?;
|
||||||
let inner_shadows =
|
let inner_shadows =
|
||||||
@ -89,6 +95,7 @@ impl Surfaces {
|
|||||||
gpu_state.create_surface_with_isize("shape_fills".to_string(), extra_tile_dims)?;
|
gpu_state.create_surface_with_isize("shape_fills".to_string(), extra_tile_dims)?;
|
||||||
let shape_strokes =
|
let shape_strokes =
|
||||||
gpu_state.create_surface_with_isize("shape_strokes".to_string(), extra_tile_dims)?;
|
gpu_state.create_surface_with_isize("shape_strokes".to_string(), extra_tile_dims)?;
|
||||||
|
let export = gpu_state.create_surface_with_isize("export".to_string(), extra_tile_dims)?;
|
||||||
|
|
||||||
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)?;
|
||||||
@ -106,10 +113,12 @@ impl Surfaces {
|
|||||||
shape_strokes,
|
shape_strokes,
|
||||||
ui,
|
ui,
|
||||||
debug,
|
debug,
|
||||||
|
export,
|
||||||
tiles,
|
tiles,
|
||||||
sampling_options,
|
sampling_options,
|
||||||
margins,
|
margins,
|
||||||
dirty_surfaces: 0,
|
dirty_surfaces: 0,
|
||||||
|
extra_tile_dims,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -278,6 +287,9 @@ impl Surfaces {
|
|||||||
if ids & SurfaceId::Debug as u32 != 0 {
|
if ids & SurfaceId::Debug as u32 != 0 {
|
||||||
f(self.get_mut(SurfaceId::Debug));
|
f(self.get_mut(SurfaceId::Debug));
|
||||||
}
|
}
|
||||||
|
if ids & SurfaceId::Export as u32 != 0 {
|
||||||
|
f(self.get_mut(SurfaceId::Export));
|
||||||
|
}
|
||||||
performance::begin_measure!("apply_mut::flags");
|
performance::begin_measure!("apply_mut::flags");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -301,7 +313,8 @@ impl Surfaces {
|
|||||||
let surface_ids = SurfaceId::Fills as u32
|
let surface_ids = SurfaceId::Fills as u32
|
||||||
| SurfaceId::Strokes as u32
|
| SurfaceId::Strokes as u32
|
||||||
| SurfaceId::InnerShadows as u32
|
| SurfaceId::InnerShadows as u32
|
||||||
| SurfaceId::TextDropShadows as u32;
|
| SurfaceId::TextDropShadows as u32
|
||||||
|
| SurfaceId::DropShadows as u32;
|
||||||
|
|
||||||
// Clear surfaces before updating transformations to remove residual content
|
// Clear surfaces before updating transformations to remove residual content
|
||||||
self.apply_mut(surface_ids, |s| {
|
self.apply_mut(surface_ids, |s| {
|
||||||
@ -313,6 +326,7 @@ impl Surfaces {
|
|||||||
self.mark_dirty(SurfaceId::Strokes);
|
self.mark_dirty(SurfaceId::Strokes);
|
||||||
self.mark_dirty(SurfaceId::InnerShadows);
|
self.mark_dirty(SurfaceId::InnerShadows);
|
||||||
self.mark_dirty(SurfaceId::TextDropShadows);
|
self.mark_dirty(SurfaceId::TextDropShadows);
|
||||||
|
self.mark_dirty(SurfaceId::DropShadows);
|
||||||
|
|
||||||
// Update transformations
|
// Update transformations
|
||||||
self.apply_mut(surface_ids, |s| {
|
self.apply_mut(surface_ids, |s| {
|
||||||
@ -324,7 +338,7 @@ impl Surfaces {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn get_mut(&mut self, id: SurfaceId) -> &mut skia::Surface {
|
pub fn get_mut(&mut self, id: SurfaceId) -> &mut skia::Surface {
|
||||||
match id {
|
match id {
|
||||||
SurfaceId::Target => &mut self.target,
|
SurfaceId::Target => &mut self.target,
|
||||||
SurfaceId::Filter => &mut self.filter,
|
SurfaceId::Filter => &mut self.filter,
|
||||||
@ -337,6 +351,7 @@ impl Surfaces {
|
|||||||
SurfaceId::Strokes => &mut self.shape_strokes,
|
SurfaceId::Strokes => &mut self.shape_strokes,
|
||||||
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -353,6 +368,7 @@ impl Surfaces {
|
|||||||
SurfaceId::Strokes => &self.shape_strokes,
|
SurfaceId::Strokes => &self.shape_strokes,
|
||||||
SurfaceId::Debug => &self.debug,
|
SurfaceId::Debug => &self.debug,
|
||||||
SurfaceId::UI => &self.ui,
|
SurfaceId::UI => &self.ui,
|
||||||
|
SurfaceId::Export => &self.export,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -492,12 +508,14 @@ impl Surfaces {
|
|||||||
self.canvas(SurfaceId::TextDropShadows).restore_to_count(1);
|
self.canvas(SurfaceId::TextDropShadows).restore_to_count(1);
|
||||||
self.canvas(SurfaceId::Strokes).restore_to_count(1);
|
self.canvas(SurfaceId::Strokes).restore_to_count(1);
|
||||||
self.canvas(SurfaceId::Current).restore_to_count(1);
|
self.canvas(SurfaceId::Current).restore_to_count(1);
|
||||||
|
self.canvas(SurfaceId::Export).restore_to_count(1);
|
||||||
self.apply_mut(
|
self.apply_mut(
|
||||||
SurfaceId::Fills as u32
|
SurfaceId::Fills as u32
|
||||||
| SurfaceId::Strokes as u32
|
| SurfaceId::Strokes as u32
|
||||||
| SurfaceId::Current as u32
|
| SurfaceId::Current as u32
|
||||||
| SurfaceId::InnerShadows as u32
|
| SurfaceId::InnerShadows as u32
|
||||||
| SurfaceId::TextDropShadows as u32,
|
| SurfaceId::TextDropShadows as u32
|
||||||
|
| SurfaceId::Export as u32,
|
||||||
|s| {
|
|s| {
|
||||||
s.canvas().clear(color).reset_matrix();
|
s.canvas().clear(color).reset_matrix();
|
||||||
},
|
},
|
||||||
@ -627,6 +645,47 @@ impl Surfaces {
|
|||||||
pub fn gc(&mut self) {
|
pub fn gc(&mut self) {
|
||||||
self.tiles.gc();
|
self.tiles.gc();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn resize_export_surface(&mut self, scale: f32, rect: skia::Rect) {
|
||||||
|
let target_w = (scale * rect.width()).ceil() as i32;
|
||||||
|
let target_h = (scale * rect.height()).ceil() as i32;
|
||||||
|
|
||||||
|
let max_w = i32::max(self.extra_tile_dims.width, target_w);
|
||||||
|
let max_h = i32::max(self.extra_tile_dims.height, target_h);
|
||||||
|
|
||||||
|
if max_w > self.extra_tile_dims.width || max_h > self.extra_tile_dims.height {
|
||||||
|
self.extra_tile_dims = skia::ISize::new(max_w, max_h);
|
||||||
|
self.drop_shadows = self
|
||||||
|
.drop_shadows
|
||||||
|
.new_surface_with_dimensions((max_w, max_h))
|
||||||
|
.unwrap();
|
||||||
|
self.inner_shadows = self
|
||||||
|
.inner_shadows
|
||||||
|
.new_surface_with_dimensions((max_w, max_h))
|
||||||
|
.unwrap();
|
||||||
|
self.text_drop_shadows = self
|
||||||
|
.text_drop_shadows
|
||||||
|
.new_surface_with_dimensions((max_w, max_h))
|
||||||
|
.unwrap();
|
||||||
|
self.text_drop_shadows = self
|
||||||
|
.text_drop_shadows
|
||||||
|
.new_surface_with_dimensions((max_w, max_h))
|
||||||
|
.unwrap();
|
||||||
|
self.shape_strokes = self
|
||||||
|
.shape_strokes
|
||||||
|
.new_surface_with_dimensions((max_w, max_h))
|
||||||
|
.unwrap();
|
||||||
|
self.shape_fills = self
|
||||||
|
.shape_strokes
|
||||||
|
.new_surface_with_dimensions((max_w, max_h))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.export = self
|
||||||
|
.export
|
||||||
|
.new_surface_with_dimensions((target_w, target_h))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TileTextureCache {
|
pub struct TileTextureCache {
|
||||||
|
|||||||
@ -101,6 +101,16 @@ impl State {
|
|||||||
.start_render_loop(Some(id), &self.shapes, timestamp, true)
|
.start_render_loop(Some(id), &self.shapes, timestamp, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn render_shape_pixels(
|
||||||
|
&mut self,
|
||||||
|
id: &Uuid,
|
||||||
|
scale: f32,
|
||||||
|
timestamp: i32,
|
||||||
|
) -> Result<(Vec<u8>, i32, i32)> {
|
||||||
|
self.render_state
|
||||||
|
.render_shape_pixels(id, &self.shapes, scale, timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn start_render_loop(&mut self, timestamp: i32) -> Result<()> {
|
pub fn start_render_loop(&mut self, timestamp: i32) -> Result<()> {
|
||||||
// If zoom changed (e.g. interrupted zoom render followed by pan), the
|
// If zoom changed (e.g. interrupted zoom render followed by pan), the
|
||||||
// tile index may be stale for the new viewport position. Rebuild the
|
// tile index may be stale for the new viewport position. Rebuild the
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user