UNPKG

@bscotch/sprite-source

Version:

Art pipeline scripting module for GameMaker sprites.

139 lines 5.45 kB
import { pathy } from '@bscotch/pathy'; import { computePngChecksums } from '@bscotch/pixel-checksum'; import { cacheVersion, spritesInfoSchema, } from './SpriteCache.schemas.js'; import { SpriteDir } from './SpriteDir.js'; import { computeStringChecksum } from './checksum.js'; import { retryOptions, spriteCacheFilename } from './constants.js'; import { SpriteSourceError, getDirs } from './utility.js'; export class SpriteCache { maxDepth; issues = []; logs = []; spritesRoot; /** * @param spritesRoot The path to the root directory containing * sprites. For a SpriteSource, this is the root of a set * of nested folders-of-images. For a SpriteDest (a project), * this is the `{project}/sprites` folder. * @param maxDepth The maximum depth to search for sprites. For a SpriteSource this is probably Infinity. For a SpriteDest, this should be 1. */ constructor(spritesRoot, maxDepth = Infinity) { this.maxDepth = maxDepth; this.spritesRoot = pathy(spritesRoot); } get stitchDir() { return this.spritesRoot.join('.stitch'); } get cacheFile() { return this.stitchDir .join(spriteCacheFilename) .withValidator(spritesInfoSchema); } async getSpriteDirs(dirs) { const waits = []; const spriteDirs = []; for (const dir of dirs) { waits.push(SpriteDir.from(dir, this.logs, this.issues) .then((sprite) => { if (sprite) { spriteDirs.push(sprite); } }) .catch((err) => { this.issues.push(new SpriteSourceError(`Error processing "${dir.relative}"`, err)); })); } await Promise.all(waits); return spriteDirs; } async loadCache() { let cache; try { cache = await this.cacheFile.read({ fallback: {}, ...retryOptions, }); if (cache.version !== cacheVersion) { cache = { version: cacheVersion, info: {}, }; this.issues.push(new SpriteSourceError(`Sprite cache version is out of date. Will rebuild.`)); } } catch (err) { cache = { version: cacheVersion, info: {}, }; this.issues.push(new SpriteSourceError(`Could not load sprite cache. Will rebuild.`, err)); } return cache; } /** * Update the sprite-info cache. */ async updateSpriteInfo(ignore) { const ignorePatterns = ignore?.map((pattern) => new RegExp(pattern)); // Load the current cache and sprite dirs const [cache, allSpriteDirs] = await Promise.all([ this.loadCache(), getDirs(this.spritesRoot.absolute, this.maxDepth).then((dirs) => this.getSpriteDirs(dirs)), ]); // Filter out ignored spriteDirs const spriteDirs = !ignore?.length ? allSpriteDirs : allSpriteDirs.filter((dir) => !ignorePatterns?.some((pattern) => dir.path.relative.match(pattern))); // For each sprite, update the cache with its size, frames (checksums, changedAt, etc) const waits = []; for (const sprite of spriteDirs) { waits.push(sprite.updateCache(cache)); } await Promise.all(waits); // Add any missing checksums const checksumsToCompute = []; for (const [sprite, info] of Object.entries(cache.info)) { if (info.spine) continue; for (const [frame, frameInfo] of Object.entries(info.frames)) { if (frameInfo.checksum) continue; checksumsToCompute.push([ sprite, frame, this.spritesRoot.join(sprite, frame).absolute, ]); } } const checksums = computePngChecksums(checksumsToCompute.map(([_s, _f, framePath]) => framePath)); checksumsToCompute.forEach(([sprite, frame], idx) => { const spriteCache = cache.info[sprite]; if (spriteCache.spine) return; // Shouldn't happen spriteCache.frames[frame].checksum = checksums[idx]; spriteCache.checksum = ''; // Will be recomputed below }); // Ensure cumulative checksums for all non-spine sprites for (const [, spriteCache] of Object.entries(cache.info)) { if (spriteCache.spine || spriteCache.checksum) continue; const frameChecksums = Object.values(spriteCache.frames) .map((f) => f.checksum) .sort(); spriteCache.checksum = computeStringChecksum(frameChecksums.join('-')); } // Remove any sprite info that no longer exists const existingSpriteDirs = new Set(spriteDirs.map((dir) => dir.path.relative)); for (const spriteDir of Object.keys(cache.info)) { if (!existingSpriteDirs.has(spriteDir)) { delete cache.info[spriteDir]; } } // Save and return the updated cache await this.cacheFile.write(cache, { ...retryOptions, }); return cache; } } //# sourceMappingURL=SpriteCache.js.map