[ts][webcomponents] Sharable cache for AssetManager.

This commit is contained in:
Davide Tantillo 2025-07-08 16:41:17 +02:00
parent 85de53624e
commit 2e5a099ef4
4 changed files with 66 additions and 33 deletions

View File

@ -35,17 +35,16 @@ export class AssetManagerBase implements Disposable {
private pathPrefix: string = ""; private pathPrefix: string = "";
private textureLoader: (image: HTMLImageElement | ImageBitmap) => Texture; private textureLoader: (image: HTMLImageElement | ImageBitmap) => Texture;
private downloader: Downloader; private downloader: Downloader;
private assets: StringMap<any> = {}; private cache: AssetCache;
private assetsRefCount: StringMap<number> = {};
private assetsLoaded: StringMap<Promise<any>> = {};
private errors: StringMap<string> = {}; private errors: StringMap<string> = {};
private toLoad = 0; private toLoad = 0;
private loaded = 0; private loaded = 0;
constructor (textureLoader: (image: HTMLImageElement | ImageBitmap) => Texture, pathPrefix: string = "", downloader: Downloader = new Downloader()) { constructor (textureLoader: (image: HTMLImageElement | ImageBitmap) => Texture, pathPrefix: string = "", downloader = new Downloader(), cache = new AssetCache()) {
this.textureLoader = textureLoader; this.textureLoader = textureLoader;
this.pathPrefix = pathPrefix; this.pathPrefix = pathPrefix;
this.downloader = downloader; this.downloader = downloader;
this.cache = cache;
} }
private start (path: string): string { private start (path: string): string {
@ -56,8 +55,8 @@ export class AssetManagerBase implements Disposable {
private success (callback: (path: string, data: any) => void, path: string, asset: any) { private success (callback: (path: string, data: any) => void, path: string, asset: any) {
this.toLoad--; this.toLoad--;
this.loaded++; this.loaded++;
this.assets[path] = asset; this.cache.assets[path] = asset;
this.assetsRefCount[path] = (this.assetsRefCount[path] || 0) + 1; this.cache.assetsRefCount[path] = (this.cache.assetsRefCount[path] || 0) + 1;
if (callback) callback(path, asset); if (callback) callback(path, asset);
} }
@ -94,7 +93,7 @@ export class AssetManagerBase implements Disposable {
if (this.reuseAssets(path, success, error)) return; if (this.reuseAssets(path, success, error)) return;
this.assetsLoaded[path] = new Promise<any>((resolve, reject) => { this.cache.assetsLoaded[path] = new Promise<any>((resolve, reject) => {
this.downloader.downloadBinary(path, (data: Uint8Array): void => { this.downloader.downloadBinary(path, (data: Uint8Array): void => {
this.success(success, path, data); this.success(success, path, data);
resolve(data); resolve(data);
@ -125,7 +124,7 @@ export class AssetManagerBase implements Disposable {
if (this.reuseAssets(path, success, error)) return; if (this.reuseAssets(path, success, error)) return;
this.assetsLoaded[path] = new Promise<any>((resolve, reject) => { this.cache.assetsLoaded[path] = new Promise<any>((resolve, reject) => {
this.downloader.downloadJson(path, (data: object): void => { this.downloader.downloadJson(path, (data: object): void => {
this.success(success, path, data); this.success(success, path, data);
resolve(data); resolve(data);
@ -140,11 +139,17 @@ export class AssetManagerBase implements Disposable {
reuseAssets (path: string, reuseAssets (path: string,
success: (path: string, data: any) => void = () => { }, success: (path: string, data: any) => void = () => { },
error: (path: string, message: string) => void = () => { }) { error: (path: string, message: string) => void = () => { }) {
const loadedStatus = this.assetsLoaded[path]; const loadedStatus = this.cache.assetsLoaded[path];
const alreadyExistsOrLoading = loadedStatus !== undefined; const alreadyExistsOrLoading = loadedStatus !== undefined;
if (alreadyExistsOrLoading) { if (alreadyExistsOrLoading) {
loadedStatus loadedStatus
.then(data => this.success(success, path, data)) .then(data => {
if (data instanceof Image) {
data = this.textureLoader(data);
this.cache.assetsLoaded[path] = Promise.resolve(data);
}
return this.success(success, path, data)
})
.catch(errorMsg => this.error(error, path, errorMsg)); .catch(errorMsg => this.error(error, path, errorMsg));
} }
return alreadyExistsOrLoading; return alreadyExistsOrLoading;
@ -158,7 +163,7 @@ export class AssetManagerBase implements Disposable {
if (this.reuseAssets(path, success, error)) return; if (this.reuseAssets(path, success, error)) return;
this.assetsLoaded[path] = new Promise<any>((resolve, reject) => { this.cache.assetsLoaded[path] = new Promise<any>((resolve, reject) => {
let isBrowser = !!(typeof window !== 'undefined' && typeof navigator !== 'undefined' && window.document); let isBrowser = !!(typeof window !== 'undefined' && typeof navigator !== 'undefined' && window.document);
let isWebWorker = !isBrowser; // && typeof importScripts !== 'undefined'; let isWebWorker = !isBrowser; // && typeof importScripts !== 'undefined';
if (isWebWorker) { if (isWebWorker) {
@ -171,7 +176,7 @@ export class AssetManagerBase implements Disposable {
return blob ? createImageBitmap(blob, { premultiplyAlpha: "none", colorSpaceConversion: "none" }) : null; return blob ? createImageBitmap(blob, { premultiplyAlpha: "none", colorSpaceConversion: "none" }) : null;
}).then((bitmap) => { }).then((bitmap) => {
if (bitmap) { if (bitmap) {
const texture = this.textureLoader(bitmap) const texture = this.textureLoader(bitmap);
this.success(success, path, texture); this.success(success, path, texture);
resolve(texture); resolve(texture);
}; };
@ -180,7 +185,7 @@ export class AssetManagerBase implements Disposable {
let image = new Image(); let image = new Image();
image.crossOrigin = "anonymous"; image.crossOrigin = "anonymous";
image.onload = () => { image.onload = () => {
const texture = this.textureLoader(image) const texture = this.textureLoader(image);
this.success(success, path, texture); this.success(success, path, texture);
resolve(texture); resolve(texture);
}; };
@ -206,7 +211,7 @@ export class AssetManagerBase implements Disposable {
if (this.reuseAssets(path, success, error)) return; if (this.reuseAssets(path, success, error)) return;
this.assetsLoaded[path] = new Promise<any>((resolve, reject) => { this.cache.assetsLoaded[path] = new Promise<any>((resolve, reject) => {
this.downloader.downloadText(path, (atlasText: string): void => { this.downloader.downloadText(path, (atlasText: string): void => {
try { try {
let atlas = new TextureAtlas(atlasText); let atlas = new TextureAtlas(atlasText);
@ -224,7 +229,7 @@ export class AssetManagerBase implements Disposable {
}, },
(imagePath: string, message: string) => { (imagePath: string, message: string) => {
if (!abort) { if (!abort) {
const errorMsg = `Couldn't load texture atlas ${path} page image: ${imagePath}`; const errorMsg = `Couldn't load texture ${path} page image: ${imagePath}`;
this.error(error, path, errorMsg); this.error(error, path, errorMsg);
reject(errorMsg); reject(errorMsg);
} }
@ -254,7 +259,7 @@ export class AssetManagerBase implements Disposable {
if (this.reuseAssets(path, success, error)) return; if (this.reuseAssets(path, success, error)) return;
this.assetsLoaded[path] = new Promise<any>((resolve, reject) => { this.cache.assetsLoaded[path] = new Promise<any>((resolve, reject) => {
this.downloader.downloadText(path, (atlasText: string): void => { this.downloader.downloadText(path, (atlasText: string): void => {
try { try {
const atlas = new TextureAtlas(atlasText); const atlas = new TextureAtlas(atlasText);
@ -319,13 +324,17 @@ export class AssetManagerBase implements Disposable {
}); });
} }
setCache (cache: AssetCache) {
this.cache = cache;
}
get (path: string) { get (path: string) {
return this.assets[this.pathPrefix + path]; return this.cache.assets[this.pathPrefix + path];
} }
require (path: string) { require (path: string) {
path = this.pathPrefix + path; path = this.pathPrefix + path;
let asset = this.assets[path]; let asset = this.cache.assets[path];
if (asset) return asset; if (asset) return asset;
let error = this.errors[path]; let error = this.errors[path];
throw Error("Asset not found: " + path + (error ? "\n" + error : "")); throw Error("Asset not found: " + path + (error ? "\n" + error : ""));
@ -333,22 +342,22 @@ export class AssetManagerBase implements Disposable {
remove (path: string) { remove (path: string) {
path = this.pathPrefix + path; path = this.pathPrefix + path;
let asset = this.assets[path]; let asset = this.cache.assets[path];
if (asset.dispose) asset.dispose(); if (asset.dispose) asset.dispose();
delete this.assets[path]; delete this.cache.assets[path];
delete this.assetsRefCount[path]; delete this.cache.assetsRefCount[path];
delete this.assetsLoaded[path]; delete this.cache.assetsLoaded[path];
return asset; return asset;
} }
removeAll () { removeAll () {
for (let path in this.assets) { for (let path in this.cache.assets) {
let asset = this.assets[path]; let asset = this.cache.assets[path];
if (asset.dispose) asset.dispose(); if (asset.dispose) asset.dispose();
} }
this.assets = {}; this.cache.assets = {};
this.assetsLoaded = {}; this.cache.assetsLoaded = {};
this.assetsRefCount = {}; this.cache.assetsRefCount = {};
} }
isLoadingComplete (): boolean { isLoadingComplete (): boolean {
@ -369,7 +378,7 @@ export class AssetManagerBase implements Disposable {
// dispose asset only if it's not used by others // dispose asset only if it's not used by others
disposeAsset (path: string) { disposeAsset (path: string) {
if (--this.assetsRefCount[path] === 0) { if (--this.cache.assetsRefCount[path] === 0) {
this.remove(path) this.remove(path)
} }
} }
@ -383,6 +392,27 @@ export class AssetManagerBase implements Disposable {
} }
} }
export class AssetCache {
public assets: StringMap<any> = {};
public assetsRefCount: StringMap<number> = {};
public assetsLoaded: StringMap<Promise<any>> = {};
static AVAILABLE_CACHES = new Map<string, AssetCache>();
static getCache (id: string) {
const cache = AssetCache.AVAILABLE_CACHES.get(id);
if (cache) return cache;
const newCache = new AssetCache();
AssetCache.AVAILABLE_CACHES.set(id, newCache);
return newCache;
}
async addAsset(path: string, asset: any) {
this.assetsLoaded[path] = Promise.resolve(asset);
this.assets[path] = await asset;
}
}
export class Downloader { export class Downloader {
private callbacks: StringMap<Array<Function>> = {}; private callbacks: StringMap<Array<Function>> = {};
rawDataUris: StringMap<string> = {}; rawDataUris: StringMap<string> = {};

View File

@ -27,7 +27,7 @@
* SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
import { AssetManager, Color, Disposable, Input, LoadingScreen, ManagedWebGLRenderingContext, Physics, SceneRenderer, TimeKeeper, Vector2, Vector3 } from "@esotericsoftware/spine-webgl" import { AssetCache, AssetManager, Color, Disposable, Input, LoadingScreen, ManagedWebGLRenderingContext, Physics, SceneRenderer, TimeKeeper, Vector2, Vector3 } from "@esotericsoftware/spine-webgl"
import { SpineWebComponentSkeleton } from "./SpineWebComponentSkeleton.js" import { SpineWebComponentSkeleton } from "./SpineWebComponentSkeleton.js"
import { AttributeTypes, castValue, Point, Rectangle } from "./wcUtils.js" import { AttributeTypes, castValue, Point, Rectangle } from "./wcUtils.js"
@ -48,10 +48,11 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
* @internal * @internal
*/ */
static getOrCreateOverlay (overlayId: string | null): SpineWebComponentOverlay { static getOrCreateOverlay (overlayId: string | null): SpineWebComponentOverlay {
let overlay = SpineWebComponentOverlay.OVERLAY_LIST.get(overlayId || SpineWebComponentOverlay.OVERLAY_ID); const id = overlayId || SpineWebComponentOverlay.OVERLAY_ID;
let overlay = SpineWebComponentOverlay.OVERLAY_LIST.get(id);
if (!overlay) { if (!overlay) {
overlay = document.createElement('spine-overlay') as SpineWebComponentOverlay; overlay = document.createElement('spine-overlay') as SpineWebComponentOverlay;
overlay.setAttribute('overlay-id', SpineWebComponentOverlay.OVERLAY_ID); overlay.setAttribute('overlay-id', id);
document.body.appendChild(overlay); document.body.appendChild(overlay);
} }
return overlay; return overlay;
@ -232,6 +233,9 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
overlayId = SpineWebComponentOverlay.OVERLAY_ID; overlayId = SpineWebComponentOverlay.OVERLAY_ID;
this.setAttribute('overlay-id', overlayId); this.setAttribute('overlay-id', overlayId);
} }
this.assetManager.setCache(AssetCache.getCache(overlayId));
const existingOverlay = SpineWebComponentOverlay.OVERLAY_LIST.get(overlayId); const existingOverlay = SpineWebComponentOverlay.OVERLAY_LIST.get(overlayId);
if (existingOverlay && existingOverlay !== this) { if (existingOverlay && existingOverlay !== this) {
throw new Error(`"SpineWebComponentOverlay - You cannot have two spine-overlay with the same overlay-id: ${overlayId}"`); throw new Error(`"SpineWebComponentOverlay - You cannot have two spine-overlay with the same overlay-id: ${overlayId}"`);

View File

@ -31,7 +31,6 @@ import { AssetManagerBase, Downloader } from "@esotericsoftware/spine-core"
import { ManagedWebGLRenderingContext } from "./WebGL.js"; import { ManagedWebGLRenderingContext } from "./WebGL.js";
import { GLTexture } from "./GLTexture.js"; import { GLTexture } from "./GLTexture.js";
export class AssetManager extends AssetManagerBase { export class AssetManager extends AssetManagerBase {
constructor (context: ManagedWebGLRenderingContext | WebGLRenderingContext, pathPrefix: string = "", downloader: Downloader = new Downloader()) { constructor (context: ManagedWebGLRenderingContext | WebGLRenderingContext, pathPrefix: string = "", downloader: Downloader = new Downloader()) {
super((image: HTMLImageElement | ImageBitmap) => { super((image: HTMLImageElement | ImageBitmap) => {

View File

@ -27,7 +27,7 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
import { Restorable, BlendMode } from "@esotericsoftware/spine-core"; import { Restorable } from "@esotericsoftware/spine-core";
export class ManagedWebGLRenderingContext { export class ManagedWebGLRenderingContext {
public canvas: HTMLCanvasElement | OffscreenCanvas; public canvas: HTMLCanvasElement | OffscreenCanvas;