UNPKG

@bscotch/stitch

Version:

Stitch: The GameMaker Studio 2 Asset Pipeline Development Kit.

217 lines 8.4 kB
import { __decorate, __metadata } from "tslib"; import { pathy } from '@bscotch/pathy'; import { Spritely, SpritelySubimage } from '@bscotch/spritely'; import { memoize, pick } from '@bscotch/utility'; import { SpriteBoundingBoxMode, Yy } from '@bscotch/yy'; import { z } from 'zod'; import { assert, assertIsNumber, StitchError, } from '../../../utility/errors.js'; import { debug, info } from '../../../utility/log.js'; import paths from '../../../utility/paths.js'; import { uuidV4 } from '../../../utility/uuid.js'; /** * Ensure that the dimensions of the sprite and its bounding * box match the dimensions of the source image. If not, assuming * linear scaling and adjust all dims. */ export function setSpriteDims(width, height, isNew) { for (const dim of ['width', 'height']) { const value = { width, height }[dim]; assertIsNumber(value, `${dim} is not a number: ${value}`); assert(value > 0, `${dim} must be > 0: ${value}`); } // Get the old height/width and origin for reference const oldOriginX = this.yyData.sequence.xorigin; const oldOriginY = this.yyData.sequence.yorigin; const oldHeight = this.yyData.height; const oldWidth = this.yyData.width; const oldBbox = pick(this.yyData, [ 'bbox_bottom', 'bbox_right', 'bbox_top', 'bbox_left', ]); this.yyData.height = height; this.yyData.width = width; this.yyData.bbox_bottom ||= height; this.yyData.bbox_right ||= width; const _scaleCoord = (oldPos, oldMax, newMax) => { if ([oldPos, oldMax, newMax].some((val) => val == 0)) { return 0; } return Math.floor((oldPos / oldMax) * newMax); }; const dimsHaveChanged = !isNew && (width != oldWidth || height != oldHeight); if (isNew) { this.yyData.sequence.xorigin = Math.floor(width / 2); this.yyData.sequence.yorigin = Math.floor(height / 2); } else if (dimsHaveChanged) { this.yyData.sequence.xorigin = _scaleCoord(oldOriginX, oldWidth, width); this.yyData.sequence.yorigin = _scaleCoord(oldOriginY, oldHeight, height); } if (dimsHaveChanged && this.yyData.bboxMode == SpriteBoundingBoxMode.FullImage) { // Adjust to dims this.yyData.bbox_left = 0; this.yyData.bbox_top = 0; this.yyData.bbox_right = width; this.yyData.bbox_bottom = height; } else if (dimsHaveChanged) { // Adjust *relatively* const bboxScaleInfo = [ { oldMax: oldWidth, max: width, fields: ['bbox_right', 'bbox_left'], }, { oldMax: oldHeight, max: height, fields: ['bbox_top', 'bbox_bottom'], }, ]; for (const bbox of bboxScaleInfo) { for (const field of bbox.fields) { this.yyData[field] = _scaleCoord(oldBbox[field], bbox.oldMax, bbox.max); } } } } export function deleteExtraneousSpriteImages() { const framePaths = this.storage.listFilesSync(this.yyDirAbsolute, true, [ 'png', ]); // Composite frames for (const framePath of framePaths) { // Since frameIds are GUIDs, we can just check for it as // a substring without worrying about exactly where it appears // in the path. if (this.frameIds.some((frameId) => framePath.includes(frameId))) { continue; } this.storage.deleteFileSync(framePath); debug(`deleted old frame ${framePath}`); } return this; } class SpineSpriteUpdater { srcJson; srcAtlas; srcDir; constructor(jsonSourcePath, name) { this.srcJson = pathy(jsonSourcePath).withValidator(z.object({ skeleton: z.object({ spine: z.string(), }), })); this.srcAtlas = this.srcJson.changeExtension('atlas'); this.srcDir = this.srcJson.up(); } async srcFiles(ext) { return (await this.srcDir.listChildren()).filter((p) => !ext || p.hasExtension(ext)); } async assertValid() { const validations = await Promise.allSettled([ this.assertValidJson(), this.srcAtlas.exists({ assert: true }), // There must be at one image with exactly the same name as the JSON file, // though there may also be additional images. this.srcJson.changeExtension('png').exists({ assert: true }), ]); assert(validations.every((v) => v.status === 'fulfilled'), 'Invalid Spine source files.'); } async assertValidJson() { // Will throw if it cannot be parsed. try { await this.srcJson.read(); } catch (err) { throw new StitchError(`Not a valid Spine JSON file: ${this.srcJson}`); } } } __decorate([ memoize, __metadata("design:type", Function), __metadata("design:paramtypes", [Object]), __metadata("design:returntype", Promise) ], SpineSpriteUpdater.prototype, "srcFiles", null); export async function syncSpineSource(spineSourceJson) { assert(this.isSpine, 'This method can only be used for Spine sprites'); const result = { changed: false, yyChanged: false, filesChanged: [], }; const spineUpdater = new SpineSpriteUpdater(spineSourceJson, this.name); await spineUpdater.assertValid(); const frameId = this.yyData.frames[0]?.name || uuidV4(); this.yyData.frames[0] = { ...this.yyData.frames[0], name: frameId }; this.yyData.frames.splice(1); const srcFiles = await spineUpdater.srcFiles(['png', 'json', 'atlas']); const destDir = pathy(this.yyDirAbsolute); for (const srcFile of srcFiles) { const destFile = srcFile.hasExtension('png') ? destDir.join(srcFile.basename) : destDir.join(`${frameId}${srcFile.extname}`); let overwrite = !(await destFile.exists()); overwrite ||= srcFile.hasExtension('png') && !(await new SpritelySubimage(srcFile.absolute).equals(destFile.absolute)); overwrite ||= !srcFile.hasExtension('png') && !Yy.areEqual(await srcFile.read(), await destFile.read()); if (overwrite) { result.filesChanged.push(destFile); await srcFile.copy(destFile); } } const saveResult = { changed: false }; this.save(saveResult); result.yyChanged = saveResult.changed; result.changed = result.yyChanged || result.filesChanged.length > 0; if (result.changed) { info(`spine sprite ${this.name} changed`, result); } return result; } export async function syncSpriteSource(spriteDirectory, isNew) { assert(!this.isSpine, 'This method cannot be used for Spine sprites'); debug(`syncing sprite with source ${spriteDirectory}`); const result = { changed: false, yyChanged: false, filesChanged: [], }; const sprite = new Spritely(spriteDirectory); // Ensure that the sizes match setSpriteDims.bind(this)(sprite.width, sprite.height, isNew); // Replace all frames, but keep the existing IDs and ID // order where possible. (Minimizes useless git history changes.) const layersRoot = paths.join(this.yyDirAbsolute, 'layers'); this.storage.ensureDirSync(layersRoot); // Remove excess frame data if needed this.yyData.frames.splice(sprite.paths.length); for (const [i, subimagePath] of sprite.paths.entries()) { if (!this.yyData.frames[i]) { this.yyData.frames[i] = { name: uuidV4() }; } const frameId = this.yyData.frames[i].name; debug(`adding frame ${i} using id ${frameId} from image at ${subimagePath}`); const updatedFrame = await this.updateFrameImage(subimagePath, frameId); if (updatedFrame) { result.filesChanged.push(pathy(updatedFrame.path)); } } deleteExtraneousSpriteImages.bind(this)(); const saveResult = { changed: false }; this.save(saveResult); result.yyChanged = saveResult.changed; result.changed = result.yyChanged || result.filesChanged.length > 0; if (result.changed) { info(`sprite ${this.name} changed`, result); } return result; } //# sourceMappingURL=Gms2Sprite.update.js.map