[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 textureLoader: (image: HTMLImageElement | ImageBitmap) => Texture;
private downloader: Downloader;
private assets: StringMap<any> = {};
private assetsRefCount: StringMap<number> = {};
private assetsLoaded: StringMap<Promise<any>> = {};
private cache: AssetCache;
private errors: StringMap<string> = {};
private toLoad = 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.pathPrefix = pathPrefix;
this.downloader = downloader;
this.cache = cache;
}
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) {
this.toLoad--;
this.loaded++;
this.assets[path] = asset;
this.assetsRefCount[path] = (this.assetsRefCount[path] || 0) + 1;
this.cache.assets[path] = asset;
this.cache.assetsRefCount[path] = (this.cache.assetsRefCount[path] || 0) + 1;
if (callback) callback(path, asset);
}
@ -94,7 +93,7 @@ export class AssetManagerBase implements Disposable {
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.success(success, path, data);
resolve(data);
@ -125,7 +124,7 @@ export class AssetManagerBase implements Disposable {
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.success(success, path, data);
resolve(data);
@ -140,11 +139,17 @@ export class AssetManagerBase implements Disposable {
reuseAssets (path: string,
success: (path: string, data: any) => void = () => { },
error: (path: string, message: string) => void = () => { }) {
const loadedStatus = this.assetsLoaded[path];
const loadedStatus = this.cache.assetsLoaded[path];
const alreadyExistsOrLoading = loadedStatus !== undefined;
if (alreadyExistsOrLoading) {
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));
}
return alreadyExistsOrLoading;
@ -158,7 +163,7 @@ export class AssetManagerBase implements Disposable {
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 isWebWorker = !isBrowser; // && typeof importScripts !== 'undefined';
if (isWebWorker) {
@ -171,7 +176,7 @@ export class AssetManagerBase implements Disposable {
return blob ? createImageBitmap(blob, { premultiplyAlpha: "none", colorSpaceConversion: "none" }) : null;
}).then((bitmap) => {
if (bitmap) {
const texture = this.textureLoader(bitmap)
const texture = this.textureLoader(bitmap);
this.success(success, path, texture);
resolve(texture);
};
@ -180,7 +185,7 @@ export class AssetManagerBase implements Disposable {
let image = new Image();
image.crossOrigin = "anonymous";
image.onload = () => {
const texture = this.textureLoader(image)
const texture = this.textureLoader(image);
this.success(success, path, texture);
resolve(texture);
};
@ -206,7 +211,7 @@ export class AssetManagerBase implements Disposable {
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 => {
try {
let atlas = new TextureAtlas(atlasText);
@ -224,7 +229,7 @@ export class AssetManagerBase implements Disposable {
},
(imagePath: string, message: string) => {
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);
reject(errorMsg);
}
@ -254,7 +259,7 @@ export class AssetManagerBase implements Disposable {
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 => {
try {
const atlas = new TextureAtlas(atlasText);
@ -319,13 +324,17 @@ export class AssetManagerBase implements Disposable {
});
}
setCache (cache: AssetCache) {
this.cache = cache;
}
get (path: string) {
return this.assets[this.pathPrefix + path];
return this.cache.assets[this.pathPrefix + path];
}
require (path: string) {
path = this.pathPrefix + path;
let asset = this.assets[path];
let asset = this.cache.assets[path];
if (asset) return asset;
let error = this.errors[path];
throw Error("Asset not found: " + path + (error ? "\n" + error : ""));
@ -333,22 +342,22 @@ export class AssetManagerBase implements Disposable {
remove (path: string) {
path = this.pathPrefix + path;
let asset = this.assets[path];
let asset = this.cache.assets[path];
if (asset.dispose) asset.dispose();
delete this.assets[path];
delete this.assetsRefCount[path];
delete this.assetsLoaded[path];
delete this.cache.assets[path];
delete this.cache.assetsRefCount[path];
delete this.cache.assetsLoaded[path];
return asset;
}
removeAll () {
for (let path in this.assets) {
let asset = this.assets[path];
for (let path in this.cache.assets) {
let asset = this.cache.assets[path];
if (asset.dispose) asset.dispose();
}
this.assets = {};
this.assetsLoaded = {};
this.assetsRefCount = {};
this.cache.assets = {};
this.cache.assetsLoaded = {};
this.cache.assetsRefCount = {};
}
isLoadingComplete (): boolean {
@ -369,7 +378,7 @@ export class AssetManagerBase implements Disposable {
// dispose asset only if it's not used by others
disposeAsset (path: string) {
if (--this.assetsRefCount[path] === 0) {
if (--this.cache.assetsRefCount[path] === 0) {
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 {
private callbacks: StringMap<Array<Function>> = {};
rawDataUris: StringMap<string> = {};

View File

@ -27,7 +27,7 @@
* 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 { AttributeTypes, castValue, Point, Rectangle } from "./wcUtils.js"
@ -48,10 +48,11 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
* @internal
*/
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) {
overlay = document.createElement('spine-overlay') as SpineWebComponentOverlay;
overlay.setAttribute('overlay-id', SpineWebComponentOverlay.OVERLAY_ID);
overlay.setAttribute('overlay-id', id);
document.body.appendChild(overlay);
}
return overlay;
@ -232,6 +233,9 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
overlayId = SpineWebComponentOverlay.OVERLAY_ID;
this.setAttribute('overlay-id', overlayId);
}
this.assetManager.setCache(AssetCache.getCache(overlayId));
const existingOverlay = SpineWebComponentOverlay.OVERLAY_LIST.get(overlayId);
if (existingOverlay && existingOverlay !== this) {
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 { GLTexture } from "./GLTexture.js";
export class AssetManager extends AssetManagerBase {
constructor (context: ManagedWebGLRenderingContext | WebGLRenderingContext, pathPrefix: string = "", downloader: Downloader = new Downloader()) {
super((image: HTMLImageElement | ImageBitmap) => {

View File

@ -27,7 +27,7 @@
* 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 {
public canvas: HTMLCanvasElement | OffscreenCanvas;