@spearwolf/twopoint5d
Version:
Create 2.5D realtime graphics and pixelart with WebGL and three.js
292 lines • 11.2 kB
JavaScript
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