mirror of
https://github.com/penpot/penpot.git
synced 2026-05-27 19:03:39 +00:00
Merge pull request #7691 from penpot/alotor-improved-render-tiling
✨ Improve tile rendering updating
This commit is contained in:
commit
445d40b71c
@ -58,7 +58,9 @@
|
|||||||
|
|
||||||
([{:keys [id points selrect] :as shape} content]
|
([{:keys [id points selrect] :as shape} content]
|
||||||
(wasm.api/use-shape id)
|
(wasm.api/use-shape id)
|
||||||
(wasm.api/set-shape-text id content false)
|
(wasm.api/set-shape-text-content id content)
|
||||||
|
(wasm.api/set-shape-text-images id content)
|
||||||
|
|
||||||
(let [dimension (wasm.api/get-text-dimensions)
|
(let [dimension (wasm.api/get-text-dimensions)
|
||||||
resize-v (gpt/point
|
resize-v (gpt/point
|
||||||
(/ (:width dimension) (-> selrect :width))
|
(/ (:width dimension) (-> selrect :width))
|
||||||
|
|||||||
@ -98,7 +98,6 @@
|
|||||||
(doseq [id ids]
|
(doseq [id ids]
|
||||||
(wasm.api/use-shape id)
|
(wasm.api/use-shape id)
|
||||||
(wasm.api/set-shape-blend-mode value)
|
(wasm.api/set-shape-blend-mode value)
|
||||||
(wasm.api/update-shape-tiles)
|
|
||||||
(wasm.api/request-render "preview-blend-mode")))
|
(wasm.api/request-render "preview-blend-mode")))
|
||||||
|
|
||||||
(st/emit! (dw/trigger-bounding-box-cloaking ids))
|
(st/emit! (dw/trigger-bounding-box-cloaking ids))
|
||||||
|
|||||||
@ -311,7 +311,6 @@
|
|||||||
(wasm.api/set-shape-text-content edition content)
|
(wasm.api/set-shape-text-content edition content)
|
||||||
(let [dimension (wasm.api/get-text-dimensions)]
|
(let [dimension (wasm.api/get-text-dimensions)]
|
||||||
(st/emit! (dwt/resize-text-editor edition dimension))
|
(st/emit! (dwt/resize-text-editor edition dimension))
|
||||||
(wasm.api/clear-drawing-cache)
|
|
||||||
(wasm.api/request-render "content"))))))
|
(wasm.api/request-render "content"))))))
|
||||||
|
|
||||||
(mf/with-effect [vport]
|
(mf/with-effect [vport]
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
["react-dom/server" :as rds]
|
["react-dom/server" :as rds]
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
|
[app.common.files.helpers :as cfh]
|
||||||
[app.common.math :as mth]
|
[app.common.math :as mth]
|
||||||
[app.common.types.fills :as types.fills]
|
[app.common.types.fills :as types.fills]
|
||||||
[app.common.types.fills.impl :as types.fills.impl]
|
[app.common.types.fills.impl :as types.fills.impl]
|
||||||
@ -33,8 +34,6 @@
|
|||||||
[app.render-wasm.wasm :as wasm]
|
[app.render-wasm.wasm :as wasm]
|
||||||
[app.util.debug :as dbg]
|
[app.util.debug :as dbg]
|
||||||
[app.util.functions :as fns]
|
[app.util.functions :as fns]
|
||||||
[app.util.http :as http]
|
|
||||||
[app.util.webapi :as wapi]
|
|
||||||
[beicon.v2.core :as rx]
|
[beicon.v2.core :as rx]
|
||||||
[promesa.core :as p]
|
[promesa.core :as p]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
@ -81,13 +80,19 @@
|
|||||||
;; This should never be called from the outside.
|
;; This should never be called from the outside.
|
||||||
(defn- render
|
(defn- render
|
||||||
[timestamp]
|
[timestamp]
|
||||||
(h/call wasm/internal-module "_render" timestamp)
|
(when wasm/context-initialized?
|
||||||
(set! wasm/internal-frame-id nil)
|
(h/call wasm/internal-module "_render" timestamp)
|
||||||
;; emit custom event
|
(set! wasm/internal-frame-id nil)
|
||||||
(let [event (js/CustomEvent. "wasm:render")]
|
;; emit custom event
|
||||||
(js/document.dispatchEvent ^js event)))
|
(let [event (js/CustomEvent. "wasm:render")]
|
||||||
|
(js/document.dispatchEvent ^js event))))
|
||||||
|
|
||||||
(def debounce-render (fns/debounce render 100))
|
(def set-view-render
|
||||||
|
(fns/debounce
|
||||||
|
(fn [ts]
|
||||||
|
(h/call wasm/internal-module "_set_view_end")
|
||||||
|
(render ts))
|
||||||
|
200))
|
||||||
|
|
||||||
(defonce pending-render (atom false))
|
(defonce pending-render (atom false))
|
||||||
|
|
||||||
@ -227,49 +232,90 @@
|
|||||||
[string]
|
[string]
|
||||||
(+ (count string) 1))
|
(+ (count string) 1))
|
||||||
|
|
||||||
|
(defn- create-webgl-texture-from-image
|
||||||
|
"Creates a WebGL texture from an HTMLImageElement or ImageBitmap and returns the texture object"
|
||||||
|
[gl image-element]
|
||||||
|
(let [texture (.createTexture ^js gl)]
|
||||||
|
(.bindTexture ^js gl (.-TEXTURE_2D ^js gl) texture)
|
||||||
|
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_WRAP_S ^js gl) (.-CLAMP_TO_EDGE ^js gl))
|
||||||
|
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_WRAP_T ^js gl) (.-CLAMP_TO_EDGE ^js gl))
|
||||||
|
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_MIN_FILTER ^js gl) (.-LINEAR ^js gl))
|
||||||
|
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_MAG_FILTER ^js gl) (.-LINEAR ^js gl))
|
||||||
|
(.texImage2D ^js gl (.-TEXTURE_2D ^js gl) 0 (.-RGBA ^js gl) (.-RGBA ^js gl) (.-UNSIGNED_BYTE ^js gl) image-element)
|
||||||
|
(.bindTexture ^js gl (.-TEXTURE_2D ^js gl) nil)
|
||||||
|
texture))
|
||||||
|
|
||||||
|
(defn- get-webgl-context
|
||||||
|
"Gets the WebGL context from the WASM module"
|
||||||
|
[]
|
||||||
|
(when wasm/context-initialized?
|
||||||
|
(let [gl-obj (unchecked-get wasm/internal-module "GL")]
|
||||||
|
(when gl-obj
|
||||||
|
;; Get the current WebGL context from Emscripten
|
||||||
|
;; The GL object has a currentContext property that contains the context handle
|
||||||
|
(let [current-ctx (.-currentContext ^js gl-obj)]
|
||||||
|
(when current-ctx
|
||||||
|
(.-GLctx ^js current-ctx)))))))
|
||||||
|
|
||||||
|
(defn- get-texture-id-for-gl-object
|
||||||
|
"Registers a WebGL texture with Emscripten's GL object system and returns its ID"
|
||||||
|
[texture]
|
||||||
|
(let [gl-obj (unchecked-get wasm/internal-module "GL")
|
||||||
|
textures (.-textures ^js gl-obj)
|
||||||
|
new-id (.getNewId ^js gl-obj textures)]
|
||||||
|
(aset textures new-id texture)
|
||||||
|
new-id))
|
||||||
|
|
||||||
(defn- fetch-image
|
(defn- fetch-image
|
||||||
|
"Loads an image and creates a WebGL texture from it, passing the texture ID to WASM.
|
||||||
|
This avoids decoding the image twice (once in browser, once in WASM)."
|
||||||
[shape-id image-id thumbnail?]
|
[shape-id image-id thumbnail?]
|
||||||
(let [url (cf/resolve-file-media {:id image-id} thumbnail?)]
|
(let [url (cf/resolve-file-media {:id image-id} thumbnail?)]
|
||||||
{:key url
|
{:key url
|
||||||
:thumbnail? thumbnail?
|
:thumbnail? thumbnail?
|
||||||
:callback #(->> (http/send! {:method :get
|
:callback #(->> (p/create
|
||||||
:uri url
|
(fn [resolve reject]
|
||||||
:response-type :blob})
|
(let [img (js/Image.)
|
||||||
(rx/map :body)
|
on-load (fn []
|
||||||
(rx/mapcat wapi/read-file-as-array-buffer)
|
(resolve img))
|
||||||
(rx/map (fn [image]
|
on-error (fn [err]
|
||||||
(let [size (.-byteLength image)
|
(reject err))]
|
||||||
padded-size (if (zero? (mod size 4)) size (+ size (- 4 (mod size 4))))
|
(set! (.-crossOrigin img) "anonymous")
|
||||||
;; 36 bytes header (32 for UUIDs + 4 for thumbnail flag) + padded image
|
(.addEventListener img "load" on-load)
|
||||||
total-bytes (+ 36 padded-size)
|
(.addEventListener img "error" on-error)
|
||||||
offset (mem/alloc->offset-32 total-bytes)
|
(set! (.-src img) url))))
|
||||||
heap32 (mem/get-heap-u32)
|
(rx/from)
|
||||||
data (js/Uint8Array. image)
|
(rx/map (fn [img]
|
||||||
padded (js/Uint8Array. padded-size)]
|
(when-let [gl (get-webgl-context)]
|
||||||
|
(let [texture (create-webgl-texture-from-image gl img)
|
||||||
|
texture-id (get-texture-id-for-gl-object texture)
|
||||||
|
width (.-width ^js img)
|
||||||
|
height (.-height ^js img)
|
||||||
|
;; Header: 32 bytes (2 UUIDs) + 4 bytes (thumbnail) + 4 bytes (texture ID) + 8 bytes (dimensions)
|
||||||
|
total-bytes 48
|
||||||
|
offset (mem/alloc->offset-32 total-bytes)
|
||||||
|
heap32 (mem/get-heap-u32)]
|
||||||
|
|
||||||
;; 1. Set shape id (offset + 0 to offset + 3)
|
;; 1. Set shape id (offset + 0 to offset + 3)
|
||||||
(mem.h32/write-uuid offset heap32 shape-id)
|
(mem.h32/write-uuid offset heap32 shape-id)
|
||||||
|
|
||||||
;; 2. Set image id (offset + 4 to offset + 7)
|
;; 2. Set image id (offset + 4 to offset + 7)
|
||||||
(mem.h32/write-uuid (+ offset 4) heap32 image-id)
|
(mem.h32/write-uuid (+ offset 4) heap32 image-id)
|
||||||
|
|
||||||
;; 3. Set thumbnail flag as u32 (offset + 8)
|
;; 3. Set thumbnail flag as u32 (offset + 8)
|
||||||
(aset heap32 (+ offset 8) thumbnail?)
|
(aset heap32 (+ offset 8) (if thumbnail? 1 0))
|
||||||
|
|
||||||
;; 4. Adjust padding on image data
|
;; 4. Set texture ID (offset + 9)
|
||||||
(.set padded data)
|
(aset heap32 (+ offset 9) texture-id)
|
||||||
(when (< size padded-size)
|
|
||||||
(dotimes [i (- padded-size size)]
|
|
||||||
(aset padded (+ size i) 0)))
|
|
||||||
|
|
||||||
;; 5. Set image data (starting at offset + 9)
|
;; 5. Set width (offset + 10)
|
||||||
(let [u32view (js/Uint32Array. (.-buffer padded))
|
(aset heap32 (+ offset 10) width)
|
||||||
image-u32-offset (+ offset 9)]
|
|
||||||
(.set heap32 u32view image-u32-offset))
|
|
||||||
|
|
||||||
(h/call wasm/internal-module "_store_image")
|
;; 6. Set height (offset + 11)
|
||||||
true))))}))
|
(aset heap32 (+ offset 11) height)
|
||||||
|
|
||||||
|
(h/call wasm/internal-module "_store_image_from_texture")
|
||||||
|
true)))))}))
|
||||||
|
|
||||||
(defn- get-fill-images
|
(defn- get-fill-images
|
||||||
[leaf]
|
[leaf]
|
||||||
@ -290,13 +336,15 @@
|
|||||||
(fetch-image shape-id id thumbnail?)))))
|
(fetch-image shape-id id thumbnail?)))))
|
||||||
|
|
||||||
(defn set-shape-text-images
|
(defn set-shape-text-images
|
||||||
[shape-id content thumbnail?]
|
([shape-id content]
|
||||||
(let [paragraph-set (first (get content :children))
|
(set-shape-text-images shape-id content false))
|
||||||
paragraphs (get paragraph-set :children)]
|
([shape-id content thumbnail?]
|
||||||
(->> paragraphs
|
(let [paragraph-set (first (get content :children))
|
||||||
(mapcat :children)
|
paragraphs (get paragraph-set :children)]
|
||||||
(mapcat get-fill-images)
|
(->> paragraphs
|
||||||
(map #(process-fill-image shape-id % thumbnail?)))))
|
(mapcat :children)
|
||||||
|
(mapcat get-fill-images)
|
||||||
|
(map #(process-fill-image shape-id % thumbnail?))))))
|
||||||
|
|
||||||
(defn set-shape-fills
|
(defn set-shape-fills
|
||||||
[shape-id fills thumbnail?]
|
[shape-id fills thumbnail?]
|
||||||
@ -734,12 +782,6 @@
|
|||||||
|
|
||||||
result)))))
|
result)))))
|
||||||
|
|
||||||
(defn set-shape-text
|
|
||||||
[shape-id content thumbnail?]
|
|
||||||
(concat
|
|
||||||
(set-shape-text-images shape-id content thumbnail?)
|
|
||||||
(set-shape-text-content shape-id content)))
|
|
||||||
|
|
||||||
(defn set-shape-grow-type
|
(defn set-shape-grow-type
|
||||||
[grow-type]
|
[grow-type]
|
||||||
(h/call wasm/internal-module "_set_shape_grow_type" (sr/translate-grow-type grow-type)))
|
(h/call wasm/internal-module "_set_shape_grow_type" (sr/translate-grow-type grow-type)))
|
||||||
@ -761,14 +803,7 @@
|
|||||||
(defn set-view-box
|
(defn set-view-box
|
||||||
[zoom vbox]
|
[zoom vbox]
|
||||||
(h/call wasm/internal-module "_set_view" zoom (- (:x vbox)) (- (:y vbox)))
|
(h/call wasm/internal-module "_set_view" zoom (- (:x vbox)) (- (:y vbox)))
|
||||||
(h/call wasm/internal-module "_render_from_cache")
|
(set-view-render))
|
||||||
(debounce-render))
|
|
||||||
|
|
||||||
(defn clear-drawing-cache []
|
|
||||||
(h/call wasm/internal-module "_clear_drawing_cache"))
|
|
||||||
|
|
||||||
(defn update-shape-tiles []
|
|
||||||
(h/call wasm/internal-module "_update_shape_tiles"))
|
|
||||||
|
|
||||||
(defn set-object
|
(defn set-object
|
||||||
[objects shape]
|
[objects shape]
|
||||||
@ -850,42 +885,47 @@
|
|||||||
(set-shape-selrect selrect)
|
(set-shape-selrect selrect)
|
||||||
|
|
||||||
(let [pending_thumbnails (into [] (concat
|
(let [pending_thumbnails (into [] (concat
|
||||||
(set-shape-text id content true)
|
(set-shape-text-content id content)
|
||||||
|
(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 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
|
||||||
|
[shapes]
|
||||||
|
(->> shapes
|
||||||
|
(filter cfh/text-shape?)
|
||||||
|
(map :id)
|
||||||
|
(run! f/update-text-layout)))
|
||||||
|
|
||||||
(defn process-pending
|
(defn process-pending
|
||||||
[thumbnails full]
|
[shapes thumbnails full]
|
||||||
(let [event (js/CustomEvent. "wasm:set-objects-finished")
|
(let [event (js/CustomEvent. "wasm:set-objects-finished")
|
||||||
pending-thumbnails (-> (d/index-by :key :callback thumbnails) vals)
|
pending-thumbnails (-> (d/index-by :key :callback thumbnails) vals)
|
||||||
pending-full (-> (d/index-by :key :callback full) vals)]
|
pending-full (-> (d/index-by :key :callback full) vals)]
|
||||||
(->> (rx/from pending-thumbnails)
|
(->> (rx/concat
|
||||||
(rx/merge-map (fn [callback] (callback)))
|
(->> (rx/from pending-thumbnails)
|
||||||
(rx/reduce conj [])
|
(rx/merge-map (fn [callback] (callback)))
|
||||||
(rx/merge-map (fn [_]
|
(rx/reduce conj [])
|
||||||
(clear-drawing-cache)
|
(rx/tap #(.dispatchEvent ^js js/document event)))
|
||||||
(request-render "pending-thumbnails-finished")
|
(->> (rx/from pending-full)
|
||||||
(h/call wasm/internal-module "_update_shape_text_layout_for_all")
|
(rx/mapcat (fn [callback] (callback)))
|
||||||
(.dispatchEvent ^js js/document event)
|
(rx/reduce conj [])))
|
||||||
;; After thumbnails are done, process full images
|
(rx/subs!
|
||||||
(rx/from pending-full)))
|
(fn [_]
|
||||||
(rx/merge-map (fn [callback] (callback)))
|
(update-text-layouts shapes)
|
||||||
(rx/reduce conj [])
|
(request-render "pending-finished"))))))
|
||||||
(rx/subs! (fn [_]
|
|
||||||
(clear-drawing-cache)
|
|
||||||
(request-render "pending-full-finished"))))))
|
|
||||||
|
|
||||||
(defn process-object
|
(defn process-object
|
||||||
[shape]
|
[shape]
|
||||||
(let [{:keys [thumbnails full]} (set-object [] shape)]
|
(let [{:keys [thumbnails full]} (set-object [] shape)]
|
||||||
(process-pending thumbnails full)))
|
(process-pending [shape] thumbnails full)))
|
||||||
|
|
||||||
(defn set-objects
|
(defn set-objects
|
||||||
[objects]
|
[objects]
|
||||||
@ -903,12 +943,11 @@
|
|||||||
(into full-acc full)))
|
(into full-acc full)))
|
||||||
{:thumbnails thumbnails-acc :full full-acc}))]
|
{:thumbnails thumbnails-acc :full full-acc}))]
|
||||||
(perf/end-measure "set-objects")
|
(perf/end-measure "set-objects")
|
||||||
(process-pending thumbnails full)))
|
(process-pending shapes thumbnails full)))
|
||||||
|
|
||||||
(defn clear-focus-mode
|
(defn clear-focus-mode
|
||||||
[]
|
[]
|
||||||
(h/call wasm/internal-module "_clear_focus_mode")
|
(h/call wasm/internal-module "_clear_focus_mode")
|
||||||
(clear-drawing-cache)
|
|
||||||
(request-render "clear-focus-mode"))
|
(request-render "clear-focus-mode"))
|
||||||
|
|
||||||
(defn set-focus-mode
|
(defn set-focus-mode
|
||||||
@ -924,7 +963,6 @@
|
|||||||
entries)
|
entries)
|
||||||
|
|
||||||
(h/call wasm/internal-module "_set_focus_mode")
|
(h/call wasm/internal-module "_set_focus_mode")
|
||||||
(clear-drawing-cache)
|
|
||||||
(request-render "set-focus-mode"))))
|
(request-render "set-focus-mode"))))
|
||||||
|
|
||||||
(defn set-structure-modifiers
|
(defn set-structure-modifiers
|
||||||
|
|||||||
@ -76,6 +76,15 @@
|
|||||||
(let [variant (font-db-data font-id font-variant-id)]
|
(let [variant (font-db-data font-id font-variant-id)]
|
||||||
(:ttf-url variant))))
|
(:ttf-url variant))))
|
||||||
|
|
||||||
|
(defn update-text-layout
|
||||||
|
[id]
|
||||||
|
(let [shape-id-buffer (uuid/get-u32 id)]
|
||||||
|
(h/call wasm/internal-module "_update_shape_text_layout_for"
|
||||||
|
(aget shape-id-buffer 0)
|
||||||
|
(aget shape-id-buffer 1)
|
||||||
|
(aget shape-id-buffer 2)
|
||||||
|
(aget shape-id-buffer 3))))
|
||||||
|
|
||||||
;; IMPORTANT: It should be noted that only TTF fonts can be stored.
|
;; IMPORTANT: It should be noted that only TTF fonts can be stored.
|
||||||
(defn- store-font-buffer
|
(defn- store-font-buffer
|
||||||
[shape-id font-data font-array-buffer emoji? fallback?]
|
[shape-id font-data font-array-buffer emoji? fallback?]
|
||||||
@ -100,11 +109,7 @@
|
|||||||
emoji?
|
emoji?
|
||||||
fallback?)
|
fallback?)
|
||||||
|
|
||||||
(h/call wasm/internal-module "_update_shape_text_layout_for"
|
(update-text-layout shape-id)
|
||||||
(aget shape-id-buffer 0)
|
|
||||||
(aget shape-id-buffer 1)
|
|
||||||
(aget shape-id-buffer 2)
|
|
||||||
(aget shape-id-buffer 3))
|
|
||||||
|
|
||||||
true))
|
true))
|
||||||
|
|
||||||
|
|||||||
@ -187,7 +187,8 @@
|
|||||||
(api/set-shape-svg-raw-content (api/get-static-markup shape))
|
(api/set-shape-svg-raw-content (api/get-static-markup shape))
|
||||||
|
|
||||||
(= (:type shape) :text)
|
(= (:type shape) :text)
|
||||||
(api/set-shape-text id v false))
|
(do (api/set-shape-text-content id v)
|
||||||
|
(api/set-shape-text-images id v)))
|
||||||
|
|
||||||
:grow-type
|
:grow-type
|
||||||
(api/set-shape-grow-type v)
|
(api/set-shape-grow-type v)
|
||||||
@ -256,11 +257,7 @@
|
|||||||
[objects shape-changes]
|
[objects shape-changes]
|
||||||
(->> (rx/from shape-changes)
|
(->> (rx/from shape-changes)
|
||||||
(rx/mapcat (fn [[shape-id props]] (process-shape! (get objects shape-id) props)))
|
(rx/mapcat (fn [[shape-id props]] (process-shape! (get objects shape-id) props)))
|
||||||
(rx/subs!
|
(rx/subs! #(api/request-render "set-wasm-attrs"))))
|
||||||
(fn [_]
|
|
||||||
(when wasm/context-initialized?
|
|
||||||
(api/update-shape-tiles)
|
|
||||||
(api/request-render "set-wasm-attrs"))))))
|
|
||||||
|
|
||||||
;; `conj` empty set initialization
|
;; `conj` empty set initialization
|
||||||
(def conj* (fnil conj #{}))
|
(def conj* (fnil conj #{}))
|
||||||
|
|||||||
@ -7,7 +7,8 @@
|
|||||||
(ns app.util.functions
|
(ns app.util.functions
|
||||||
"A functions helpers"
|
"A functions helpers"
|
||||||
(:require
|
(:require
|
||||||
["lodash/debounce.js" :as lodash-debounce]))
|
["lodash/debounce.js" :as lodash-debounce]
|
||||||
|
[app.util.rxops :refer [throttle-fn]]))
|
||||||
|
|
||||||
;; NOTE: this is needed because depending on the type of the build and
|
;; NOTE: this is needed because depending on the type of the build and
|
||||||
;; target execution evironment (browser, esm), the real export can be
|
;; target execution evironment (browser, esm), the real export can be
|
||||||
@ -29,3 +30,9 @@
|
|||||||
(debounce f 0))
|
(debounce f 0))
|
||||||
([f timeout]
|
([f timeout]
|
||||||
(ext-debounce f timeout #{:leading false :trailing true})))
|
(ext-debounce f timeout #{:leading false :trailing true})))
|
||||||
|
|
||||||
|
(defn throttle
|
||||||
|
([f]
|
||||||
|
(throttle-fn 0 f))
|
||||||
|
([f timeout]
|
||||||
|
(throttle-fn timeout f)))
|
||||||
|
|||||||
@ -198,7 +198,7 @@
|
|||||||
|
|
||||||
(defn ^:export dump-object
|
(defn ^:export dump-object
|
||||||
[name]
|
[name]
|
||||||
(get-object @st/state name))
|
(clj->js (get-object @st/state name)))
|
||||||
|
|
||||||
(defn get-selected
|
(defn get-selected
|
||||||
[state]
|
[state]
|
||||||
|
|||||||
@ -58,6 +58,9 @@ macro_rules! with_current_shape_mut {
|
|||||||
STATE.as_mut()
|
STATE.as_mut()
|
||||||
}
|
}
|
||||||
.expect("Got an invalid state pointer");
|
.expect("Got an invalid state pointer");
|
||||||
|
|
||||||
|
$state.touch_current();
|
||||||
|
|
||||||
if let Some($shape) = $state.current_shape_mut() {
|
if let Some($shape) = $state.current_shape_mut() {
|
||||||
$block
|
$block
|
||||||
}
|
}
|
||||||
@ -103,17 +106,16 @@ pub extern "C" fn init(width: i32, height: i32) {
|
|||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn clean_up() {
|
pub extern "C" fn clean_up() {
|
||||||
|
with_state_mut!(state, {
|
||||||
|
// Cancel the current animation frame if it exists so
|
||||||
|
// it won't try to render without context
|
||||||
|
let render_state = state.render_state_mut();
|
||||||
|
render_state.cancel_animation_frame();
|
||||||
|
});
|
||||||
unsafe { STATE = None }
|
unsafe { STATE = None }
|
||||||
mem::free_bytes();
|
mem::free_bytes();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "C" fn clear_drawing_cache() {
|
|
||||||
with_state_mut!(state, {
|
|
||||||
state.rebuild_tiles();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn set_render_options(debug: u32, dpr: f32) {
|
pub extern "C" fn set_render_options(debug: u32, dpr: f32) {
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
@ -128,13 +130,14 @@ pub extern "C" fn set_canvas_background(raw_color: u32) {
|
|||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
let color = skia::Color::new(raw_color);
|
let color = skia::Color::new(raw_color);
|
||||||
state.set_background_color(color);
|
state.set_background_color(color);
|
||||||
state.rebuild_tiles();
|
state.rebuild_tiles_shallow();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn render(_: i32) {
|
pub extern "C" fn render(_: i32) {
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
|
state.rebuild_touched_tiles();
|
||||||
state
|
state
|
||||||
.start_render_loop(performance::get_time())
|
.start_render_loop(performance::get_time())
|
||||||
.expect("Error rendering");
|
.expect("Error rendering");
|
||||||
@ -189,15 +192,20 @@ pub extern "C" fn set_view(zoom: f32, x: f32, y: f32) {
|
|||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
let render_state = state.render_state_mut();
|
let render_state = state.render_state_mut();
|
||||||
render_state.viewbox.set_all(zoom, x, y);
|
render_state.viewbox.set_all(zoom, x, y);
|
||||||
with_state_mut!(state, {
|
state.render_from_cache();
|
||||||
// We can have renders in progress
|
});
|
||||||
state.render_state.cancel_animation_frame();
|
}
|
||||||
if state.render_state.options.is_profile_rebuild_tiles() {
|
|
||||||
state.rebuild_tiles();
|
#[no_mangle]
|
||||||
} else {
|
pub extern "C" fn set_view_end() {
|
||||||
state.rebuild_tiles_shallow();
|
with_state_mut!(state, {
|
||||||
}
|
// We can have renders in progress
|
||||||
});
|
state.render_state.cancel_animation_frame();
|
||||||
|
if state.render_state.options.is_profile_rebuild_tiles() {
|
||||||
|
state.rebuild_tiles();
|
||||||
|
} else {
|
||||||
|
state.rebuild_tiles_shallow();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -616,13 +624,6 @@ pub extern "C" fn set_modifiers() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "C" fn update_shape_tiles() {
|
|
||||||
with_state_mut!(state, {
|
|
||||||
state.update_tile_for_current_shape();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
init_gl!();
|
init_gl!();
|
||||||
|
|||||||
@ -238,6 +238,7 @@ pub(crate) struct RenderState {
|
|||||||
pub nested_blurs: Vec<Option<Blur>>, // FIXME: why is this an option?
|
pub nested_blurs: Vec<Option<Blur>>, // FIXME: why is this an option?
|
||||||
pub show_grid: Option<Uuid>,
|
pub show_grid: Option<Uuid>,
|
||||||
pub focus_mode: FocusMode,
|
pub focus_mode: FocusMode,
|
||||||
|
pub touched_ids: HashSet<Uuid>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_cache_size(viewbox: Viewbox, scale: f32) -> skia::ISize {
|
pub fn get_cache_size(viewbox: Viewbox, scale: f32) -> skia::ISize {
|
||||||
@ -307,6 +308,7 @@ impl RenderState {
|
|||||||
nested_blurs: vec![],
|
nested_blurs: vec![],
|
||||||
show_grid: None,
|
show_grid: None,
|
||||||
focus_mode: FocusMode::new(),
|
focus_mode: FocusMode::new(),
|
||||||
|
touched_ids: HashSet::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -327,6 +329,19 @@ impl RenderState {
|
|||||||
self.images.add(id, is_thumbnail, image_data)
|
self.images.add(id, is_thumbnail, image_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Adds an image from an existing WebGL texture, avoiding re-decoding
|
||||||
|
pub fn add_image_from_gl_texture(
|
||||||
|
&mut self,
|
||||||
|
id: Uuid,
|
||||||
|
is_thumbnail: bool,
|
||||||
|
texture_id: u32,
|
||||||
|
width: i32,
|
||||||
|
height: i32,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
self.images
|
||||||
|
.add_image_from_gl_texture(id, is_thumbnail, texture_id, width, height)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn has_image(&self, id: &Uuid, is_thumbnail: bool) -> bool {
|
pub fn has_image(&self, id: &Uuid, is_thumbnail: bool) -> bool {
|
||||||
self.images.contains(id, is_thumbnail)
|
self.images.contains(id, is_thumbnail)
|
||||||
}
|
}
|
||||||
@ -849,7 +864,8 @@ impl RenderState {
|
|||||||
// debug::render_debug_tiles_for_viewbox(self);
|
// debug::render_debug_tiles_for_viewbox(self);
|
||||||
|
|
||||||
performance::begin_measure!("tile_cache");
|
performance::begin_measure!("tile_cache");
|
||||||
self.pending_tiles.update(&self.tile_viewbox);
|
self.pending_tiles
|
||||||
|
.update(&self.tile_viewbox, &self.surfaces);
|
||||||
performance::end_measure!("tile_cache");
|
performance::end_measure!("tile_cache");
|
||||||
|
|
||||||
self.pending_nodes.clear();
|
self.pending_nodes.clear();
|
||||||
@ -1174,10 +1190,9 @@ impl RenderState {
|
|||||||
node_render_state.id
|
node_render_state.id
|
||||||
))?;
|
))?;
|
||||||
|
|
||||||
// If the shape is not in the tile set, then we update
|
// If the shape is not in the tile set, then we add them.
|
||||||
// it.
|
|
||||||
if self.tiles.get_tiles_of(node_id).is_none() {
|
if self.tiles.get_tiles_of(node_id).is_none() {
|
||||||
self.update_tile_for(element, tree);
|
self.add_shape_tiles(element, tree);
|
||||||
}
|
}
|
||||||
|
|
||||||
if visited_children {
|
if visited_children {
|
||||||
@ -1431,6 +1446,7 @@ impl RenderState {
|
|||||||
performance::begin_measure!("render_shape_tree::uncached");
|
performance::begin_measure!("render_shape_tree::uncached");
|
||||||
let (is_empty, early_return) =
|
let (is_empty, early_return) =
|
||||||
self.render_shape_tree_partial_uncached(tree, timestamp)?;
|
self.render_shape_tree_partial_uncached(tree, timestamp)?;
|
||||||
|
|
||||||
if early_return {
|
if early_return {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@ -1513,50 +1529,77 @@ impl RenderState {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Given a shape returns the TileRect with the range of tiles that the shape is in
|
||||||
|
*/
|
||||||
pub fn get_tiles_for_shape(&mut self, shape: &Shape, tree: ShapesPoolRef) -> TileRect {
|
pub fn get_tiles_for_shape(&mut self, shape: &Shape, tree: ShapesPoolRef) -> TileRect {
|
||||||
let extrect = shape.extrect(tree);
|
let extrect = shape.extrect(tree);
|
||||||
let tile_size = tiles::get_tile_size(self.get_scale());
|
let tile_size = tiles::get_tile_size(self.get_scale());
|
||||||
tiles::get_tiles_for_rect(extrect, tile_size)
|
tiles::get_tiles_for_rect(extrect, tile_size)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_tile_for(&mut self, shape: &Shape, tree: ShapesPoolRef) {
|
/*
|
||||||
|
* Given a shape, check the indexes and update it's location in the tile set
|
||||||
|
* returns the tiles that have changed in the process.
|
||||||
|
*/
|
||||||
|
pub fn update_shape_tiles(&mut self, shape: &Shape, tree: ShapesPoolRef) -> Vec<tiles::Tile> {
|
||||||
let TileRect(rsx, rsy, rex, rey) = self.get_tiles_for_shape(shape, tree);
|
let TileRect(rsx, rsy, rex, rey) = self.get_tiles_for_shape(shape, tree);
|
||||||
let old_tiles: HashSet<tiles::Tile> = self
|
|
||||||
|
let old_tiles = self
|
||||||
.tiles
|
.tiles
|
||||||
.get_tiles_of(shape.id)
|
.get_tiles_of(shape.id)
|
||||||
.map_or(HashSet::new(), |tiles| tiles.iter().cloned().collect());
|
.map_or(Vec::new(), |tiles| tiles.iter().copied().collect());
|
||||||
let new_tiles: HashSet<tiles::Tile> = (rsx..=rex)
|
|
||||||
.flat_map(|x| (rsy..=rey).map(move |y| tiles::Tile(x, y)))
|
let new_tiles = (rsx..=rex).flat_map(|x| (rsy..=rey).map(move |y| tiles::Tile(x, y)));
|
||||||
.collect();
|
|
||||||
|
let mut result = HashSet::<tiles::Tile>::new();
|
||||||
|
|
||||||
// First, remove the shape from all tiles where it was previously located
|
// First, remove the shape from all tiles where it was previously located
|
||||||
for tile in old_tiles {
|
for tile in old_tiles {
|
||||||
self.remove_cached_tile_shape(tile, shape.id);
|
self.tiles.remove_shape_at(tile, shape.id);
|
||||||
|
result.insert(tile);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then, add the shape to the new tiles
|
// Then, add the shape to the new tiles
|
||||||
for tile in new_tiles {
|
for tile in new_tiles {
|
||||||
self.remove_cached_tile_shape(tile, shape.id);
|
|
||||||
self.tiles.add_shape_at(tile, shape.id);
|
self.tiles.add_shape_at(tile, shape.id);
|
||||||
|
result.insert(tile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result.iter().copied().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_cached_tile_shape(&mut self, tile: tiles::Tile, id: Uuid) {
|
/*
|
||||||
|
* Add the tiles forthe shape to the index.
|
||||||
|
* returns the tiles that have been updated
|
||||||
|
*/
|
||||||
|
pub fn add_shape_tiles(&mut self, shape: &Shape, tree: ShapesPoolRef) -> Vec<tiles::Tile> {
|
||||||
|
let TileRect(rsx, rsy, rex, rey) = self.get_tiles_for_shape(shape, tree);
|
||||||
|
let tiles: Vec<_> = (rsx..=rex)
|
||||||
|
.flat_map(|x| (rsy..=rey).map(move |y| tiles::Tile(x, y)))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for tile in tiles.iter() {
|
||||||
|
self.tiles.add_shape_at(*tile, shape.id);
|
||||||
|
}
|
||||||
|
tiles
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_cached_tile(&mut self, tile: tiles::Tile) {
|
||||||
let rect = self.get_aligned_tile_bounds(tile);
|
let rect = self.get_aligned_tile_bounds(tile);
|
||||||
self.surfaces
|
self.surfaces
|
||||||
.remove_cached_tile_surface(tile, rect, self.background_color);
|
.remove_cached_tile_surface(tile, rect, self.background_color);
|
||||||
self.tiles.remove_shape_at(tile, id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rebuild_tiles_shallow(&mut self, tree: ShapesPoolRef) {
|
pub fn rebuild_tiles_shallow(&mut self, tree: ShapesPoolRef) {
|
||||||
performance::begin_measure!("rebuild_tiles_shallow");
|
performance::begin_measure!("rebuild_tiles_shallow");
|
||||||
self.tiles.invalidate();
|
|
||||||
self.surfaces.remove_cached_tiles(self.background_color);
|
let mut all_tiles = HashSet::<tiles::Tile>::new();
|
||||||
let mut nodes = vec![Uuid::nil()];
|
let mut nodes = vec![Uuid::nil()];
|
||||||
while let Some(shape_id) = nodes.pop() {
|
while let Some(shape_id) = nodes.pop() {
|
||||||
if let Some(shape) = tree.get(&shape_id) {
|
if let Some(shape) = tree.get(&shape_id) {
|
||||||
if shape_id != Uuid::nil() {
|
if shape_id != Uuid::nil() {
|
||||||
self.update_tile_for(shape, tree);
|
all_tiles.extend(self.update_shape_tiles(shape, tree));
|
||||||
} else {
|
} else {
|
||||||
// We only need to rebuild tiles from the first level.
|
// We only need to rebuild tiles from the first level.
|
||||||
for child_id in shape.children_ids_iter(false) {
|
for child_id in shape.children_ids_iter(false) {
|
||||||
@ -1565,18 +1608,29 @@ impl RenderState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the changed tiles
|
||||||
|
self.surfaces.remove_cached_tiles(self.background_color);
|
||||||
|
for tile in all_tiles {
|
||||||
|
self.remove_cached_tile(tile);
|
||||||
|
}
|
||||||
|
|
||||||
performance::end_measure!("rebuild_tiles_shallow");
|
performance::end_measure!("rebuild_tiles_shallow");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rebuild_tiles(&mut self, tree: ShapesPoolRef) {
|
pub fn rebuild_tiles(&mut self, tree: ShapesPoolRef) {
|
||||||
performance::begin_measure!("rebuild_tiles");
|
performance::begin_measure!("rebuild_tiles");
|
||||||
|
|
||||||
self.tiles.invalidate();
|
self.tiles.invalidate();
|
||||||
self.surfaces.remove_cached_tiles(self.background_color);
|
|
||||||
|
let mut all_tiles = HashSet::<tiles::Tile>::new();
|
||||||
let mut nodes = vec![Uuid::nil()];
|
let mut nodes = vec![Uuid::nil()];
|
||||||
|
|
||||||
while let Some(shape_id) = nodes.pop() {
|
while let Some(shape_id) = nodes.pop() {
|
||||||
if let Some(shape) = tree.get(&shape_id) {
|
if let Some(shape) = tree.get(&shape_id) {
|
||||||
if shape_id != Uuid::nil() {
|
if shape_id != Uuid::nil() {
|
||||||
self.update_tile_for(shape, tree);
|
// We have invalidated the tiles so we only need to add the shape
|
||||||
|
all_tiles.extend(self.add_shape_tiles(shape, tree));
|
||||||
}
|
}
|
||||||
|
|
||||||
for child_id in shape.children_ids_iter(false) {
|
for child_id in shape.children_ids_iter(false) {
|
||||||
@ -1584,9 +1638,45 @@ impl RenderState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the changed tiles
|
||||||
|
self.surfaces.remove_cached_tiles(self.background_color);
|
||||||
|
for tile in all_tiles {
|
||||||
|
self.remove_cached_tile(tile);
|
||||||
|
}
|
||||||
|
|
||||||
performance::end_measure!("rebuild_tiles");
|
performance::end_measure!("rebuild_tiles");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Rebuild the tiles for the shapes that have been modified from the
|
||||||
|
* last time this was executed.
|
||||||
|
*/
|
||||||
|
pub fn rebuild_touched_tiles(&mut self, tree: ShapesPoolRef) {
|
||||||
|
performance::begin_measure!("rebuild_touched_tiles");
|
||||||
|
|
||||||
|
let mut all_tiles = HashSet::<tiles::Tile>::new();
|
||||||
|
|
||||||
|
let ids = self.touched_ids.clone();
|
||||||
|
|
||||||
|
for shape_id in ids.iter() {
|
||||||
|
if let Some(shape) = tree.get(shape_id) {
|
||||||
|
if shape_id != &Uuid::nil() {
|
||||||
|
all_tiles.extend(self.update_shape_tiles(shape, tree));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the changed tiles
|
||||||
|
for tile in all_tiles {
|
||||||
|
self.remove_cached_tile(tile);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.clean_touched();
|
||||||
|
|
||||||
|
performance::end_measure!("rebuild_touched_tiles");
|
||||||
|
}
|
||||||
|
|
||||||
/// Invalidates extended rectangles and updates tiles for a set of shapes
|
/// Invalidates extended rectangles and updates tiles for a set of shapes
|
||||||
///
|
///
|
||||||
/// This function takes a set of shape IDs and for each one:
|
/// This function takes a set of shape IDs and for each one:
|
||||||
@ -1595,18 +1685,18 @@ impl RenderState {
|
|||||||
///
|
///
|
||||||
/// This is useful when you have a pre-computed set of shape IDs that need to be refreshed,
|
/// This is useful when you have a pre-computed set of shape IDs that need to be refreshed,
|
||||||
/// regardless of their relationship to other shapes (e.g., ancestors, descendants, or any other collection).
|
/// regardless of their relationship to other shapes (e.g., ancestors, descendants, or any other collection).
|
||||||
pub fn invalidate_and_update_tiles(
|
pub fn update_tiles_shapes(&mut self, shape_ids: &IndexSet<Uuid>, tree: ShapesPoolMutRef<'_>) {
|
||||||
&mut self,
|
performance::begin_measure!("invalidate_and_update_tiles");
|
||||||
shape_ids: &IndexSet<Uuid>,
|
let mut all_tiles = HashSet::<tiles::Tile>::new();
|
||||||
tree: ShapesPoolMutRef<'_>,
|
|
||||||
) {
|
|
||||||
for shape_id in shape_ids {
|
for shape_id in shape_ids {
|
||||||
if let Some(shape) = tree.get(shape_id) {
|
if let Some(shape) = tree.get(shape_id) {
|
||||||
if !shape.id.is_nil() {
|
all_tiles.extend(self.update_shape_tiles(shape, tree));
|
||||||
self.update_tile_for(shape, tree);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for tile in all_tiles {
|
||||||
|
self.remove_cached_tile(tile);
|
||||||
|
}
|
||||||
|
performance::end_measure!("invalidate_and_update_tiles");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rebuilds tiles for shapes with modifiers and processes their ancestors
|
/// Rebuilds tiles for shapes with modifiers and processes their ancestors
|
||||||
@ -1617,7 +1707,7 @@ impl RenderState {
|
|||||||
/// This is crucial for frames and groups that contain transformed children.
|
/// This is crucial for frames and groups that contain transformed children.
|
||||||
pub fn rebuild_modifier_tiles(&mut self, tree: ShapesPoolMutRef<'_>, ids: Vec<Uuid>) {
|
pub fn rebuild_modifier_tiles(&mut self, tree: ShapesPoolMutRef<'_>, ids: Vec<Uuid>) {
|
||||||
let ancestors = all_with_ancestors(&ids, tree, false);
|
let ancestors = all_with_ancestors(&ids, tree, false);
|
||||||
self.invalidate_and_update_tiles(&ancestors, tree);
|
self.update_tiles_shapes(&ancestors, tree);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_scale(&self) -> f32 {
|
pub fn get_scale(&self) -> f32 {
|
||||||
@ -1627,4 +1717,12 @@ impl RenderState {
|
|||||||
pub fn get_cached_scale(&self) -> f32 {
|
pub fn get_cached_scale(&self) -> f32 {
|
||||||
self.cached_viewbox.zoom() * self.options.dpr()
|
self.cached_viewbox.zoom() * self.options.dpr()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn mark_touched(&mut self, uuid: Uuid) {
|
||||||
|
self.touched_ids.insert(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clean_touched(&mut self) {
|
||||||
|
self.touched_ids.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -62,6 +62,83 @@ pub struct ImageStore {
|
|||||||
context: Box<DirectContext>,
|
context: Box<DirectContext>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a Skia image from an existing WebGL texture.
|
||||||
|
/// This avoids re-decoding the image, as the browser has already decoded
|
||||||
|
/// and uploaded it to the GPU.
|
||||||
|
fn create_image_from_gl_texture(
|
||||||
|
context: &mut Box<DirectContext>,
|
||||||
|
texture_id: u32,
|
||||||
|
width: i32,
|
||||||
|
height: i32,
|
||||||
|
) -> Result<Image, String> {
|
||||||
|
use skia_safe::gpu;
|
||||||
|
use skia_safe::gpu::gl::TextureInfo;
|
||||||
|
|
||||||
|
// Create a TextureInfo describing the existing GL texture
|
||||||
|
let texture_info = TextureInfo {
|
||||||
|
target: gl::TEXTURE_2D,
|
||||||
|
id: texture_id,
|
||||||
|
format: gl::RGBA8,
|
||||||
|
protected: gpu::Protected::No,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a backend texture from the GL texture using the new API
|
||||||
|
let label = format!("shared_texture_{}", texture_id);
|
||||||
|
let backend_texture = unsafe {
|
||||||
|
gpu::backend_textures::make_gl((width, height), gpu::Mipmapped::No, texture_info, label)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a Skia image from the backend texture
|
||||||
|
// Use TopLeft origin because HTML images have their origin at top-left,
|
||||||
|
// while WebGL textures traditionally use bottom-left
|
||||||
|
let image = Image::from_texture(
|
||||||
|
context.as_mut(),
|
||||||
|
&backend_texture,
|
||||||
|
gpu::SurfaceOrigin::TopLeft,
|
||||||
|
skia::ColorType::RGBA8888,
|
||||||
|
skia::AlphaType::Premul,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.ok_or("Failed to create Skia image from GL texture")?;
|
||||||
|
|
||||||
|
Ok(image)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode and upload to GPU
|
||||||
|
fn decode_image(context: &mut Box<DirectContext>, raw_data: &[u8]) -> Option<Image> {
|
||||||
|
let data = unsafe { skia::Data::new_bytes(raw_data) };
|
||||||
|
let codec = Codec::from_data(&data)?;
|
||||||
|
let image = Image::from_encoded(&data)?;
|
||||||
|
|
||||||
|
let mut dimensions = codec.dimensions();
|
||||||
|
if codec.origin().swaps_width_height() {
|
||||||
|
dimensions.width = codec.dimensions().height;
|
||||||
|
dimensions.height = codec.dimensions().width;
|
||||||
|
}
|
||||||
|
|
||||||
|
let image_info = skia::ImageInfo::new_n32_premul(dimensions, None);
|
||||||
|
|
||||||
|
let mut surface = surfaces::render_target(
|
||||||
|
context,
|
||||||
|
Budgeted::Yes,
|
||||||
|
&image_info,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let dest_rect: MathRect =
|
||||||
|
MathRect::from_xywh(0.0, 0.0, dimensions.width as f32, dimensions.height as f32);
|
||||||
|
|
||||||
|
surface
|
||||||
|
.canvas()
|
||||||
|
.draw_image_rect(&image, None, dest_rect, &skia::Paint::default());
|
||||||
|
|
||||||
|
Some(surface.image_snapshot())
|
||||||
|
}
|
||||||
|
|
||||||
impl ImageStore {
|
impl ImageStore {
|
||||||
pub fn new(context: DirectContext) -> Self {
|
pub fn new(context: DirectContext) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@ -77,8 +154,37 @@ impl ImageStore {
|
|||||||
return Err("Image already exists".to_string());
|
return Err("Image already exists".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
self.images
|
let raw_data = image_data.to_vec();
|
||||||
.insert(key, StoredImage::Raw(image_data.to_vec()));
|
|
||||||
|
if let Some(gpu_image) = decode_image(&mut self.context, &raw_data) {
|
||||||
|
self.images.insert(key, StoredImage::Gpu(gpu_image));
|
||||||
|
} else {
|
||||||
|
self.images.insert(key, StoredImage::Raw(raw_data));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a Skia image from an existing WebGL texture, avoiding re-decoding.
|
||||||
|
/// This is much more efficient as it reuses the texture that was already
|
||||||
|
/// decoded and uploaded to GPU by the browser.
|
||||||
|
pub fn add_image_from_gl_texture(
|
||||||
|
&mut self,
|
||||||
|
id: Uuid,
|
||||||
|
is_thumbnail: bool,
|
||||||
|
texture_id: u32,
|
||||||
|
width: i32,
|
||||||
|
height: i32,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let key = (id, is_thumbnail);
|
||||||
|
|
||||||
|
if self.images.contains_key(&key) {
|
||||||
|
return Err("Image already exists".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a Skia image from the existing GL texture
|
||||||
|
let image = create_image_from_gl_texture(&mut self.context, texture_id, width, height)?;
|
||||||
|
self.images.insert(key, StoredImage::Gpu(image));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,48 +209,9 @@ impl ImageStore {
|
|||||||
match entry {
|
match entry {
|
||||||
StoredImage::Gpu(ref img) => Some(img),
|
StoredImage::Gpu(ref img) => Some(img),
|
||||||
StoredImage::Raw(raw_data) => {
|
StoredImage::Raw(raw_data) => {
|
||||||
// Decode and upload to GPU
|
let gpu_image = decode_image(&mut self.context, raw_data)?;
|
||||||
let data = unsafe { skia::Data::new_bytes(raw_data) };
|
|
||||||
let codec = Codec::from_data(data.clone())?;
|
|
||||||
let image = Image::from_encoded(data.clone())?;
|
|
||||||
|
|
||||||
let mut dimensions = codec.dimensions();
|
|
||||||
if codec.origin().swaps_width_height() {
|
|
||||||
dimensions.width = codec.dimensions().height;
|
|
||||||
dimensions.height = codec.dimensions().width;
|
|
||||||
}
|
|
||||||
|
|
||||||
let image_info = skia::ImageInfo::new_n32_premul(dimensions, None);
|
|
||||||
|
|
||||||
let mut surface = surfaces::render_target(
|
|
||||||
&mut self.context,
|
|
||||||
Budgeted::Yes,
|
|
||||||
&image_info,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let dest_rect: MathRect = MathRect::from_xywh(
|
|
||||||
0.0,
|
|
||||||
0.0,
|
|
||||||
dimensions.width as f32,
|
|
||||||
dimensions.height as f32,
|
|
||||||
);
|
|
||||||
|
|
||||||
surface.canvas().draw_image_rect(
|
|
||||||
&image,
|
|
||||||
None,
|
|
||||||
dest_rect,
|
|
||||||
&skia::Paint::default(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let gpu_image = surface.image_snapshot();
|
|
||||||
|
|
||||||
// Replace raw data with GPU image
|
|
||||||
*entry = StoredImage::Gpu(gpu_image);
|
*entry = StoredImage::Gpu(gpu_image);
|
||||||
|
|
||||||
if let StoredImage::Gpu(ref img) = entry {
|
if let StoredImage::Gpu(ref img) = entry {
|
||||||
Some(img)
|
Some(img)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -318,7 +318,7 @@ impl Surfaces {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_cached_tile_surface(&mut self, tile: Tile) -> bool {
|
pub fn has_cached_tile_surface(&self, tile: Tile) -> bool {
|
||||||
self.tiles.has(tile)
|
self.tiles.has(tile)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -365,7 +365,7 @@ impl TileTextureCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has(&mut self, tile: Tile) -> bool {
|
pub fn has(&self, tile: Tile) -> bool {
|
||||||
self.grid.contains_key(&tile)
|
self.grid.contains_key(&tile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -105,7 +105,8 @@ impl<'a> State<'a> {
|
|||||||
for x in rsx..=rex {
|
for x in rsx..=rex {
|
||||||
for y in rsy..=rey {
|
for y in rsy..=rey {
|
||||||
let tile = tiles::Tile(x, y);
|
let tile = tiles::Tile(x, y);
|
||||||
self.render_state.remove_cached_tile_shape(tile, id);
|
self.render_state.remove_cached_tile(tile);
|
||||||
|
self.render_state.tiles.remove_shape_at(tile, shape.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -145,23 +146,6 @@ impl<'a> State<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_tile_for_shape(&mut self, shape_id: Uuid) {
|
|
||||||
if let Some(shape) = self.shapes.get(&shape_id) {
|
|
||||||
self.render_state.update_tile_for(shape, &self.shapes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_tile_for_current_shape(&mut self) {
|
|
||||||
let Some(shape) = self.current_shape() else {
|
|
||||||
panic!("Invalid current shape")
|
|
||||||
};
|
|
||||||
// TODO: Remove this clone
|
|
||||||
if !shape.id.is_nil() {
|
|
||||||
self.render_state
|
|
||||||
.update_tile_for(&shape.clone(), &self.shapes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rebuild_tiles_shallow(&mut self) {
|
pub fn rebuild_tiles_shallow(&mut self) {
|
||||||
self.render_state.rebuild_tiles_shallow(&self.shapes);
|
self.render_state.rebuild_tiles_shallow(&self.shapes);
|
||||||
}
|
}
|
||||||
@ -170,6 +154,10 @@ impl<'a> State<'a> {
|
|||||||
self.render_state.rebuild_tiles(&self.shapes);
|
self.render_state.rebuild_tiles(&self.shapes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn rebuild_touched_tiles(&mut self) {
|
||||||
|
self.render_state.rebuild_touched_tiles(&self.shapes);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn rebuild_modifier_tiles(&mut self, ids: Vec<Uuid>) {
|
pub fn rebuild_modifier_tiles(&mut self, ids: Vec<Uuid>) {
|
||||||
// SAFETY: We're extending the lifetime of the mutable borrow to 'a.
|
// SAFETY: We're extending the lifetime of the mutable borrow to 'a.
|
||||||
// This is safe because:
|
// This is safe because:
|
||||||
@ -215,4 +203,14 @@ impl<'a> State<'a> {
|
|||||||
pub fn set_modifiers(&mut self, modifiers: HashMap<Uuid, skia::Matrix>) {
|
pub fn set_modifiers(&mut self, modifiers: HashMap<Uuid, skia::Matrix>) {
|
||||||
self.shapes.set_modifiers(modifiers);
|
self.shapes.set_modifiers(modifiers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn touch_current(&mut self) {
|
||||||
|
if let Some(current_id) = self.current_id {
|
||||||
|
self.render_state.mark_touched(current_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn touch_shape(&mut self, id: Uuid) {
|
||||||
|
self.render_state.mark_touched(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -265,6 +265,7 @@ impl<'a> ShapesPoolImpl<'a> {
|
|||||||
self.shapes.iter()
|
self.shapes.iter()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, Shape> {
|
pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, Shape> {
|
||||||
self.shapes.iter_mut()
|
self.shapes.iter_mut()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
use crate::render::Surfaces;
|
||||||
use crate::uuid::Uuid;
|
use crate::uuid::Uuid;
|
||||||
use crate::view::Viewbox;
|
use crate::view::Viewbox;
|
||||||
use skia_safe as skia;
|
use skia_safe as skia;
|
||||||
@ -175,7 +176,7 @@ impl PendingTiles {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(&mut self, tile_viewbox: &TileViewbox) {
|
pub fn update(&mut self, tile_viewbox: &TileViewbox, surfaces: &Surfaces) {
|
||||||
self.list.clear();
|
self.list.clear();
|
||||||
|
|
||||||
let columns = tile_viewbox.interest_rect.width();
|
let columns = tile_viewbox.interest_rect.width();
|
||||||
@ -225,6 +226,17 @@ impl PendingTiles {
|
|||||||
current += 1;
|
current += 1;
|
||||||
}
|
}
|
||||||
self.list.reverse();
|
self.list.reverse();
|
||||||
|
|
||||||
|
// Create a new list where the cached tiles go first
|
||||||
|
let iter1 = self
|
||||||
|
.list
|
||||||
|
.iter()
|
||||||
|
.filter(|t| surfaces.has_cached_tile_surface(**t));
|
||||||
|
let iter2 = self
|
||||||
|
.list
|
||||||
|
.iter()
|
||||||
|
.filter(|t| !surfaces.has_cached_tile_surface(**t));
|
||||||
|
self.list = iter1.chain(iter2).copied().collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pop(&mut self) -> Option<Tile> {
|
pub fn pop(&mut self) -> Option<Tile> {
|
||||||
|
|||||||
@ -84,10 +84,59 @@ pub extern "C" fn store_image() {
|
|||||||
{
|
{
|
||||||
eprintln!("{}", msg);
|
eprintln!("{}", msg);
|
||||||
}
|
}
|
||||||
});
|
state.touch_shape(ids.shape_id);
|
||||||
|
});
|
||||||
with_state_mut!(state, {
|
|
||||||
state.update_tile_for_shape(ids.shape_id);
|
mem::free_bytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stores an image from an existing WebGL texture, avoiding re-decoding
|
||||||
|
/// Expected memory layout:
|
||||||
|
/// - bytes 0-15: shape UUID
|
||||||
|
/// - bytes 16-31: image UUID
|
||||||
|
/// - bytes 32-35: is_thumbnail flag (u32)
|
||||||
|
/// - bytes 36-39: GL texture ID (u32)
|
||||||
|
/// - bytes 40-43: width (i32)
|
||||||
|
/// - bytes 44-47: height (i32)
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn store_image_from_texture() {
|
||||||
|
let bytes = mem::bytes();
|
||||||
|
|
||||||
|
if bytes.len() < 48 {
|
||||||
|
eprintln!("store_image_from_texture: insufficient data");
|
||||||
|
mem::free_bytes();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ids = ShapeImageIds::try_from(bytes[0..IMAGE_IDS_SIZE].to_vec()).unwrap();
|
||||||
|
|
||||||
|
// Read is_thumbnail flag (4 bytes as u32)
|
||||||
|
let is_thumbnail_bytes = &bytes[IMAGE_IDS_SIZE..IMAGE_HEADER_SIZE];
|
||||||
|
let is_thumbnail_value = u32::from_le_bytes(is_thumbnail_bytes.try_into().unwrap());
|
||||||
|
let is_thumbnail = is_thumbnail_value != 0;
|
||||||
|
|
||||||
|
// Read GL texture ID (4 bytes as u32)
|
||||||
|
let texture_id_bytes = &bytes[36..40];
|
||||||
|
let texture_id = u32::from_le_bytes(texture_id_bytes.try_into().unwrap());
|
||||||
|
|
||||||
|
// Read width and height (8 bytes as two i32s)
|
||||||
|
let width_bytes = &bytes[40..44];
|
||||||
|
let width = i32::from_le_bytes(width_bytes.try_into().unwrap());
|
||||||
|
|
||||||
|
let height_bytes = &bytes[44..48];
|
||||||
|
let height = i32::from_le_bytes(height_bytes.try_into().unwrap());
|
||||||
|
|
||||||
|
with_state_mut!(state, {
|
||||||
|
if let Err(msg) = state.render_state_mut().add_image_from_gl_texture(
|
||||||
|
ids.image_id,
|
||||||
|
is_thumbnail,
|
||||||
|
texture_id,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
) {
|
||||||
|
eprintln!("store_image_from_texture error: {}", msg);
|
||||||
|
}
|
||||||
|
state.touch_shape(ids.shape_id);
|
||||||
});
|
});
|
||||||
|
|
||||||
mem::free_bytes();
|
mem::free_bytes();
|
||||||
|
|||||||
@ -57,11 +57,9 @@ pub extern "C" fn store_font(
|
|||||||
.add(family, &font_bytes, is_emoji, is_fallback);
|
.add(family, &font_bytes, is_emoji, is_fallback);
|
||||||
|
|
||||||
mem::free_bytes();
|
mem::free_bytes();
|
||||||
});
|
|
||||||
|
|
||||||
with_state_mut!(state, {
|
|
||||||
let shape_id = uuid_from_u32_quartet(a1, b1, c1, d1);
|
let shape_id = uuid_from_u32_quartet(a1, b1, c1, d1);
|
||||||
state.update_tile_for_shape(shape_id);
|
state.touch_shape(shape_id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ use super::{fills::RawFillData, fonts::RawFontStyle};
|
|||||||
use crate::math::{Matrix, Point};
|
use crate::math::{Matrix, Point};
|
||||||
use crate::mem;
|
use crate::mem;
|
||||||
use crate::shapes::{
|
use crate::shapes::{
|
||||||
self, GrowType, TextAlign, TextDecoration, TextDirection, TextTransform, Type,
|
self, GrowType, Shape, TextAlign, TextDecoration, TextDirection, TextTransform, Type,
|
||||||
};
|
};
|
||||||
use crate::utils::{uuid_from_u32, uuid_from_u32_quartet};
|
use crate::utils::{uuid_from_u32, uuid_from_u32_quartet};
|
||||||
use crate::{with_current_shape_mut, with_state_mut, with_state_mut_current_shape, STATE};
|
use crate::{with_current_shape_mut, with_state_mut, with_state_mut_current_shape, STATE};
|
||||||
@ -329,13 +329,17 @@ pub extern "C" fn get_text_dimensions() -> *mut u8 {
|
|||||||
ptr
|
ptr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_text_layout(shape: &mut Shape) {
|
||||||
|
if let Type::Text(text_content) = &mut shape.shape_type {
|
||||||
|
text_content.update_layout(shape.selrect);
|
||||||
|
shape.invalidate_extrect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn update_shape_text_layout() {
|
pub extern "C" fn update_shape_text_layout() {
|
||||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||||
shape.invalidate_extrect();
|
update_text_layout(shape);
|
||||||
if let Type::Text(text_content) = &mut shape.shape_type {
|
|
||||||
text_content.update_layout(shape.selrect);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -344,22 +348,7 @@ pub extern "C" fn update_shape_text_layout_for(a: u32, b: u32, c: u32, d: u32) {
|
|||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
let shape_id = uuid_from_u32_quartet(a, b, c, d);
|
let shape_id = uuid_from_u32_quartet(a, b, c, d);
|
||||||
if let Some(shape) = state.shapes.get_mut(&shape_id) {
|
if let Some(shape) = state.shapes.get_mut(&shape_id) {
|
||||||
shape.invalidate_extrect();
|
update_text_layout(shape);
|
||||||
if let Type::Text(text_content) = &mut shape.shape_type {
|
|
||||||
text_content.update_layout(shape.selrect);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "C" fn update_shape_text_layout_for_all() {
|
|
||||||
with_state_mut!(state, {
|
|
||||||
for shape in state.shapes.iter_mut() {
|
|
||||||
shape.invalidate_extrect();
|
|
||||||
if let Type::Text(text_content) = &mut shape.shape_type {
|
|
||||||
text_content.update_layout(shape.selrect);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user