UNPKG

@spearwolf/twopoint5d

Version:

Create 2.5D realtime graphics and pixelart with WebGL and three.js

292 lines 11.2 kB
import { emit, off, on, once, onceAsync, retain } from '@spearwolf/eventize'; import { batch, createSignal, SignalGroup } from '@spearwolf/signalize'; import { TextureFactory } from './TextureFactory.js'; import { TextureResource } from './TextureResource.js'; export const TextureStoreEvents = { Ready: 'ready', RendererChanged: 'rendererChanged', Resource: 'resource', Dispose: 'dispose', Error: 'error', }; const OnReady = TextureStoreEvents.Ready; const OnRendererChanged = TextureStoreEvents.RendererChanged; const OnResource = TextureStoreEvents.Resource; const OnDispose = TextureStoreEvents.Dispose; const OnError = TextureStoreEvents.Error; const joinTextureClasses = (...classes) => { const all = classes?.filter((c) => c != null); if (all && all.length) { return Array.from(new Set(all.flat()).values()); } return undefined; }; const cmpDefaultClasses = (a, b) => { if (a === b) return true; if (!a || !b) return false; if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false; return true; }; export class TextureStore { static async load(url) { const store = new TextureStore(); store.load(url); return store.whenReady(); } #defaultTextureClasses = createSignal([], { compare: cmpDefaultClasses, attach: this }); get defaultTextureClasses() { return this.#defaultTextureClasses.value; } set defaultTextureClasses(value) { this.#defaultTextureClasses.set(value); } #renderer = createSignal(undefined, { attach: this }); #textureFactory = createSignal(undefined, { attach: this }); get renderer() { return this.#renderer.value; } set renderer(value) { this.#renderer.set(value); } get textureFactory() { return this.#textureFactory.value; } #resources = new Map(); constructor(renderer) { retain(this, [OnReady, OnRendererChanged]); this.#renderer.onChange((renderer) => { this.#textureFactory.set(renderer ? new TextureFactory(renderer, []) : undefined); emit(this, OnRendererChanged, renderer); }); this.#textureFactory.onChange((factory) => { for (const resource of this.#resources.values()) { resource.textureFactory = factory; } }); this.renderer = renderer; } onResource(id, callback) { const resource = this.#resources.get(id); if (resource) { callback(resource); return () => { }; } return on(this, `${OnResource}:${id}`, (resource) => { callback(resource); }); } async whenReady() { await onceAsync(this, OnReady); return this; } async whenResource(id) { const existing = this.#resources.get(id); if (existing) return existing; await onceAsync(this, OnReady); const resource = this.#resources.get(id); if (!resource) { throw new Error(`[TextureStore] No resource with id "${id}" — check your TextureStoreData.items keys.`); } return resource; } load(url) { void (async () => { let response; try { response = await fetch(url); } catch (error) { emit(this, OnError, { source: 'fetch', url, error }); return; } let data; try { data = await response.json(); } catch (error) { emit(this, OnError, { source: 'parse', url, error }); return; } try { this.parse(data); } catch (error) { emit(this, OnError, { source: 'parse', url, error }); } })(); return this; } parse(data) { if (Array.isArray(data.defaultTextureClasses) && data.defaultTextureClasses.length) { this.defaultTextureClasses = data.defaultTextureClasses.slice(); } const updatedResources = []; batch(() => { for (const [id, item] of Object.entries(data.items)) { let resource = this.#resources.get(id); const textureClasses = joinTextureClasses(item.texture, this.defaultTextureClasses); if (item.tileSet) { if (resource) { if (resource.type !== 'tileset') { throw new Error(`Resource ${id} already exists with type "${resource.type}" - cannot change to "tileset"`); } batch(() => { resource.imageUrl = item.imageUrl; resource.tileSetOptions = item.tileSet; resource.textureClasses = textureClasses; resource.frameBasedAnimationsData = item.frameBasedAnimations; }); } else { resource = TextureResource.fromTileSet(id, item.imageUrl, item.tileSet, textureClasses, item.frameBasedAnimations); } } else if (item.atlasUrl) { if (resource) { if (resource.type !== 'atlas') { throw new Error(`Resource ${id} already exists with type "${resource.type}" - cannot change to "atlas"`); } batch(() => { resource.atlasUrl = item.atlasUrl; resource.overrideImageUrl = item.overrideImageUrl; resource.textureClasses = textureClasses; resource.frameBasedAnimationsData = item.frameBasedAnimations; }); } else { resource = TextureResource.fromAtlas(id, item.atlasUrl, item.overrideImageUrl, textureClasses, item.frameBasedAnimations); } } else if (item.imageUrl) { if (resource) { if (resource.type !== 'image') { throw new Error(`Resource ${id} already exists with type "${resource.type}" - cannot change to "image"`); } batch(() => { resource.imageUrl = item.imageUrl; resource.textureClasses = textureClasses; }); } else { resource = TextureResource.fromImage(id, item.imageUrl, textureClasses); } } if (resource) { if (!this.#resources.has(id)) { resource.textureFactory = this.#textureFactory.value; } this.#resources.set(id, resource); updatedResources.push(resource); } } }); emit(this, OnReady, this); updatedResources.forEach((resource) => { emit(this, `${OnResource}:${resource.id}`, resource); }); } on(id, type, callback) { const isMultipleTypes = Array.isArray(type); const values = isMultipleTypes ? new Map() : undefined; const unsubscribeFromSubType = []; let unsubscribeFromResource; let isActiveSubscription = true; const clearSubTypeSubscriptions = () => { unsubscribeFromSubType.forEach((cb) => cb()); unsubscribeFromSubType.length = 0; }; const unsubscribe = () => { isActiveSubscription = false; values?.clear(); unsubscribeFromResource?.(); clearSubTypeSubscriptions(); off(this, OnDispose, unsubscribe); off(this, OnReady, onReadyHandler); }; const onReadyHandler = () => { if (isActiveSubscription) { unsubscribeFromResource = this.onResource(id, (resource) => { clearSubTypeSubscriptions(); resource.load(); if (this.#textureFactory.value && !resource.textureFactory) { resource.textureFactory = this.#textureFactory.value; } resource.refCount++; unsubscribeFromSubType.push(() => { resource.refCount--; }); if (isMultipleTypes) { type.forEach((t) => { unsubscribeFromSubType.push(on(resource, t, (val) => { values.set(t, val); const valuesArg = type.map((t) => values.get(t)).filter((v) => v != null); if (valuesArg.length === type.length) { callback(valuesArg); } })); }); } else { unsubscribeFromSubType.push(on(resource, type, (val) => { callback(val); })); } }); } }; once(this, OnDispose, unsubscribe); once(this, OnReady, onReadyHandler); return unsubscribe; } get(id, type, options) { const signal = options?.signal; return new Promise((resolve, reject) => { if (signal?.aborted) { reject(new DOMException('get() aborted before subscription', 'AbortError')); return; } const unsubscribe = this.on(id, type, (value) => { if (signal) signal.removeEventListener('abort', onAbort); unsubscribe(); resolve(value); }); const onAbort = () => { unsubscribe(); reject(new DOMException(`get(${id}, ${String(type)}) aborted`, 'AbortError')); }; signal?.addEventListener('abort', onAbort, { once: true }); }); } clearUnused() { let removed = 0; for (const [id, resource] of this.#resources) { if (resource.refCount <= 0) { resource.dispose(); this.#resources.delete(id); removed++; } } return removed; } dispose() { emit(this, OnDispose); for (const resource of this.#resources.values()) { resource.dispose(); } this.#resources.clear(); this.#renderer.set(undefined); SignalGroup.delete(this); off(this); } } //# sourceMappingURL=TextureStore.js.map