From dfd8c949fb8d53bd45985ca809938c560e994024 Mon Sep 17 00:00:00 2001 From: Zyie <24736175+Zyie@users.noreply.github.com> Date: Thu, 20 Jun 2024 13:50:40 +0100 Subject: [PATCH] feat: multi-pack spritesheets --- src/pixi/index.ts | 2 - src/texture-packer/index.ts | 1 - src/texture-packer/packer/createJsons.ts | 56 +++--- src/texture-packer/texturePacker.ts | 1 + .../texturePackerCacheBuster.ts | 20 ++- src/texture-packer/texturePackerCompress.ts | 35 +++- .../texturePackerManifestMod.ts | 167 ------------------ test/manifest/Manifest.test.ts | 31 ++-- .../texturePackerCacheBuster.test.ts | 90 ++++++++-- .../texturePackerManifest.test.ts | 32 ++-- 10 files changed, 169 insertions(+), 266 deletions(-) delete mode 100644 src/texture-packer/texturePackerManifestMod.ts diff --git a/src/pixi/index.ts b/src/pixi/index.ts index 1afbc7c..54b22cd 100644 --- a/src/pixi/index.ts +++ b/src/pixi/index.ts @@ -12,7 +12,6 @@ import { spineAtlasMipmap } from '../spine/spineAtlasMipmap.js'; import { texturePacker } from '../texture-packer/texturePacker.js'; import { texturePackerCacheBuster } from '../texture-packer/texturePackerCacheBuster.js'; import { texturePackerCompress } from '../texture-packer/texturePackerCompress.js'; -import { texturePackerManifestMod } from '../texture-packer/texturePackerManifestMod.js'; import { webfont } from '../webfont/webfont.js'; import type { FfmpegOptions } from '../ffmpeg/ffmpeg.js'; @@ -109,7 +108,6 @@ export function pixiAssetPackPipes(config: PixiAssetPack) pipes.push( pixiManifest(manifestOptions), - texturePackerManifestMod(manifestOptions), spineAtlasManifestMod(manifestOptions), ); diff --git a/src/texture-packer/index.ts b/src/texture-packer/index.ts index 8958b40..477af01 100644 --- a/src/texture-packer/index.ts +++ b/src/texture-packer/index.ts @@ -1,4 +1,3 @@ export * from './texturePacker.js'; export * from './texturePackerCacheBuster.js'; export * from './texturePackerCompress.js'; -export * from './texturePackerManifestMod.js'; diff --git a/src/texture-packer/packer/createJsons.ts b/src/texture-packer/packer/createJsons.ts index f42d0ee..c920fd0 100644 --- a/src/texture-packer/packer/createJsons.ts +++ b/src/texture-packer/packer/createJsons.ts @@ -15,12 +15,12 @@ export function createJsons( width: number, height: number, options: { - textureName: string, - resolution: number, - textureFormat: 'png' | 'jpg', - nameStyle: 'short' | 'relative', - removeFileExtension: boolean - } + textureName: string; + resolution: number; + textureFormat: 'png' | 'jpg'; + nameStyle: 'short' | 'relative'; + removeFileExtension: boolean; + }, ) { const bins = packer.bins; @@ -32,7 +32,7 @@ export function createJsons( const bin = bins[i]; const json: any = { - frames: {} + frames: {}, }; for (let j = 0; j < bin.rects.length; j++) @@ -44,7 +44,7 @@ export function createJsons( x: rect.x, y: rect.y, w: rect.width, - h: rect.height + h: rect.height, }, rotated: rect.rot, trimmed: rect.textureData.trimmed, @@ -52,42 +52,42 @@ export function createJsons( x: rect.textureData.trimOffsetLeft, y: rect.textureData.trimOffsetTop, w: rect.width, - h: rect.height + h: rect.height, }, sourceSize: { w: rect.textureData.originalWidth, - h: rect.textureData.originalHeight - } + h: rect.textureData.originalHeight, + }, }; } + const name = createName(options.textureName, i, bins.length !== 1, options.resolution, options.textureFormat); + + let multiPack: string[] | null = null; + + if (bins.length > 1 && i === 0) + { + const binsWithoutFirst = bins.slice(1); + + multiPack = binsWithoutFirst.map((_, i) => name.replace('-0', `-${i + 1}`).replace('.png', `.json`)); + } + json.meta = { app: 'http://github.com/pixijs/assetpack', version: '1.0', - image: createName( - options.textureName, - i, - bins.length !== 1, - options.resolution, - options.textureFormat - ), + image: name, format: 'RGBA8888', size: { w: width, - h: height + h: height, }, - scale: options.resolution + scale: options.resolution, + related_multi_packs: multiPack, }; jsons.push({ - name: createName( - options.textureName, - i, - bins.length !== 1, - options.resolution, - 'json' - ), - json + name: createName(options.textureName, i, bins.length !== 1, options.resolution, 'json'), + json, }); } diff --git a/src/texture-packer/texturePacker.ts b/src/texture-packer/texturePacker.ts index eb08ce0..d494b10 100644 --- a/src/texture-packer/texturePacker.ts +++ b/src/texture-packer/texturePacker.ts @@ -159,6 +159,7 @@ export function texturePacker(_options: TexturePackerOptions = {}): AssetPipe asset.getFinalTransformedChildren()[0]); - jsonAssets.forEach((jsonAsset) => + // loop through all the json files back to front + for (let i = jsonAssets.length - 1; i >= 0; i--) { // we are going to replace the textures in the atlas file with the new cache busted textures // as we do this, the hash of the atlas file will change, so we need to update the path // and also remove the original file. - + const jsonAsset = jsonAssets[i]; const originalHash = jsonAsset.hash; const originalPath = jsonAsset.path; @@ -75,15 +76,24 @@ export function texturePackerCacheBuster( json.meta.image = cacheBustedTexture.filename; - jsonAsset.buffer = Buffer.from(JSON.stringify(json)); + if (json.meta.related_multi_packs) + { + json.meta.related_multi_packs = (json.meta.related_multi_packs as string[]).map((pack) => + { + const foundAssets = findAssets((asset) => + asset.filename === pack, asset, true); - jsonAsset.path = jsonAsset.path.replace(originalHash, jsonAsset.hash); + return foundAssets[0].getFinalTransformedChildren()[0].filename; + }); + } + jsonAsset.buffer = Buffer.from(JSON.stringify(json)); + jsonAsset.path = jsonAsset.path.replace(originalHash, jsonAsset.hash); fs.removeSync(originalPath); // rewrite.. fs.writeFileSync(jsonAsset.path, jsonAsset.buffer); - }); + } textureJsonFilesToFix.length = 0; } diff --git a/src/texture-packer/texturePackerCompress.ts b/src/texture-packer/texturePackerCompress.ts index d7cdecb..54b85fa 100644 --- a/src/texture-packer/texturePackerCompress.ts +++ b/src/texture-packer/texturePackerCompress.ts @@ -5,7 +5,9 @@ import type { CompressOptions } from '../image/compress.js'; export type TexturePackerCompressOptions = PluginOptions<'tps' | 'nc'> & Omit; -export function texturePackerCompress(_options?: TexturePackerCompressOptions): AssetPipe +export function texturePackerCompress( + _options?: TexturePackerCompressOptions, +): AssetPipe { const defaultOptions = { ...{ @@ -17,8 +19,8 @@ export function texturePackerCompress(_options?: TexturePackerCompressOptions): tags: { tps: 'tps', nc: 'nc', - ..._options?.tags - } + ..._options?.tags, + }, }; return { @@ -26,17 +28,19 @@ export function texturePackerCompress(_options?: TexturePackerCompressOptions): defaultOptions, test(asset: Asset, options) { - return (asset.allMetaData[options.tags.tps] + return ( + asset.allMetaData[options.tags.tps] && !asset.allMetaData[options.tags.nc] - && checkExt(asset.path, '.json')); + && checkExt(asset.path, '.json') + ); }, async transform(asset: Asset, options) { const formats = []; - if (options.avif)formats.push('avif'); - if (options.png)formats.push('png'); - if (options.webp)formats.push('webp'); + if (options.avif) formats.push('avif'); + if (options.png) formats.push('png'); + if (options.webp) formats.push('webp'); const json = JSON.parse(asset.buffer.toString()); @@ -49,8 +53,21 @@ export function texturePackerCompress(_options?: TexturePackerCompressOptions): json.meta.image = swapExt(json.meta.image, extension); const newAsset = createNewAssetAt(asset, newFileName); + const newJson = JSON.parse(JSON.stringify(json)); + + if (newJson.meta.related_multi_packs) + { + newJson.meta.related_multi_packs = (newJson.meta.related_multi_packs as string[]).map((pack) => + swapExt(pack, `${extension}.json`), + ); + } + + newAsset.buffer = Buffer.from(JSON.stringify(newJson, null, 2)); - newAsset.buffer = Buffer.from(JSON.stringify(json, null, 2)); + if (!newJson.meta.related_multi_packs) + { + newAsset.metaData.mIgnore = true; + } return newAsset; }); diff --git a/src/texture-packer/texturePackerManifestMod.ts b/src/texture-packer/texturePackerManifestMod.ts deleted file mode 100644 index b6b53aa..0000000 --- a/src/texture-packer/texturePackerManifestMod.ts +++ /dev/null @@ -1,167 +0,0 @@ -import fs from 'fs-extra'; -import { findAssets, Logger, path } from '../core/index.js'; - -import type { Asset, AssetPipe, PluginOptions } from '../core/index.js'; - -export interface TexturePackerManifestOptions extends PluginOptions<'tps'> -{ - output?: string; -} - -/** - * This pipe will modify the manifest generated by 'pixiManifest'. It will remove the entry - * in the manifest - which groups all the textures and jsons under one asset and replace it with - * the assets representing individual pages of the sprite sheet. - * - * Once done, it rewrites the manifest. - * - * This should be added after the `pixiManifest` pipe. - * - * ensure that the same output path is passed to the pipe as the `pixiManifest` pipe. Otherwise - * the manifest will not be found. - * - * As this pipe needs to know about all the textures in the texture files most of the work is done - * in the finish method. - * - * Kind of like applying a patch at the end of the manifest process. - * - * @param _options - * @returns - */ -export function texturePackerManifestMod( - _options: TexturePackerManifestOptions = {} -): AssetPipe -{ - const defaultOptions = { - output: 'manifest.json', - ..._options, - tags: { - tps: 'tps', - ..._options.tags, - }, - }; - - return { - folder: false, - name: 'texture-packer-manifest', - defaultOptions, - - async finish(asset: Asset, options, pipeSystem) - { - const manifestLocation = options.output; - - const newFileName = path.dirname(manifestLocation) === '.' - ? path.joinSafe(pipeSystem.outputPath, manifestLocation) : manifestLocation; - - if (!fs.existsSync(newFileName)) - { - // eslint-disable-next-line max-len - Logger.warn(`[AssetPack][texture-packer-manifest] Texture Packer Manifest could not find the manifest: ${newFileName}. Please ensure that the 'pixiManifest' output and the 'texturePackerManifest' output are the same.`); - - return; - } - - const manifest = fs.readJsonSync(newFileName); - - // used to make sure we don't process the same asset twice. - const duplicateHash: Record = {}; - - const originalJsonAssets = findAssets((asset) => - asset.metaData[options.tags.tps] && !asset.transformParent, asset, true); - - originalJsonAssets.forEach((originalJsonAsset) => - { - // if we have already processed this asset then skip it. - if (duplicateHash[originalJsonAsset.path]) return; - - duplicateHash[originalJsonAsset.path] = true; - - // now get all the final assets that were created for this sprite sheet. - // this will include all the variations of the textures and the jsons. - const finalJsonAssets = originalJsonAsset.getFinalTransformedChildren(); - - // next find the manifestAsset data that was added by the manifest - this is the data that - // we need to remove and replace with the new data for each PAGE of the sprite sheet. - const jsonManifestPath = path.relative(pipeSystem.outputPath, finalJsonAssets[0].path); - - const { manifestAsset, bundle } = findManifestAsset(manifest, jsonManifestPath); - - const texturePackedAssets = getTexturePackedAssets(finalJsonAssets); - - // now we need to get the pages of the sprite sheet and update the manifest with the new pages. - texturePackedAssets.forEach((pages, pageIndex) => - { - bundle.assets.push({ - // use the same alias as the original asset but add the page index to it. - // we don't control what the alias is so we use whats here. - alias: manifestAsset.alias.map((alias: string) => - getAlias(alias, pageIndex, texturePackedAssets.length > 1) - ), - src: pages - .map((finalAsset) => path.relative(pipeSystem.outputPath, finalAsset.path)) - .sort((a, b) => b.localeCompare(a)), - data: manifestAsset.data - }); - }); - - // to wrap up remove the original manifest asset from the bundle. - bundle.assets.splice(bundle.assets.indexOf(manifestAsset), 1); - }); - - // write the new manifest. - fs.writeJSONSync(newFileName, manifest, { spaces: 2 }); - - return; - } - }; -} - -function getTexturePackedAssets(assets: Asset[]) -{ - // first get the jsons.. - const jsonAssets = assets.filter((asset) => asset.extension === '.json'); - - const groupAssets: Asset[][] = []; - - for (let i = 0; i < jsonAssets.length; i++) - { - const jsonAsset = jsonAssets[i]; - - groupAssets[jsonAsset.transformData.page] ??= []; - - groupAssets[jsonAsset.transformData.page].push(jsonAsset); - } - - return groupAssets; -} - -function findManifestAsset(manifest: any, assetPath: string): {bundle: any, manifestAsset: any} -{ - for (let i = 0; i < manifest.bundles.length; i++) - { - const bundle = manifest.bundles[i]; - const assets = bundle.assets; - - const manifestAsset = assets.find((asset: {src: string[]}) => - - asset.src.includes(assetPath) - ); - - if (manifestAsset) - { - return { bundle, manifestAsset }; - } - } - - return { bundle: null, manifestAsset: null }; -} - -function getAlias(alias: string, pageIndex: number, multiPage: boolean) -{ - if (multiPage) - { - return `${alias}-${pageIndex}`; - } - - return alias; -} diff --git a/test/manifest/Manifest.test.ts b/test/manifest/Manifest.test.ts index 96dfbbf..a3dac26 100644 --- a/test/manifest/Manifest.test.ts +++ b/test/manifest/Manifest.test.ts @@ -6,7 +6,7 @@ import { audio } from '../../src/ffmpeg/index.js'; import { compress, mipmap } from '../../src/image/index.js'; import { pixiManifest } from '../../src/manifest/index.js'; import { spineAtlasManifestMod, spineAtlasMipmap } from '../../src/spine/index.js'; -import { texturePacker, texturePackerManifestMod } from '../../src/texture-packer/index.js'; +import { texturePacker, texturePackerCompress } from '../../src/texture-packer/index.js'; import { assetPath, createFolder, getCacheDir, getInputDir, getOutputDir } from '../utils/index.js'; import type { File } from '../utils/index.js'; @@ -115,13 +115,13 @@ describe('Manifest', () => output: outputDir, cache: useCache, pipes: [ + audio(), + spineAtlasMipmap(), texturePacker({ resolutionOptions: { maximumTextureSize: 512, }, }), - audio(), - spineAtlasMipmap(), mipmap(), compress({ png: true, @@ -129,9 +129,9 @@ describe('Manifest', () => webp: true, avif: false, }), + texturePackerCompress(), pixiManifest(), spineAtlasManifestMod(), - texturePackerManifestMod(), ], }); @@ -176,18 +176,11 @@ describe('Manifest', () => }, }, { - alias: ['bundle/tps-0'], - src: ['bundle/tps-0@0.5x.json', 'bundle/tps-0.json'], - data: { - tags: { - tps: true, - m: true, - }, - }, - }, - { - alias: ['bundle/tps-1'], - src: ['bundle/tps-1@0.5x.json', 'bundle/tps-1.json'], + alias: ['bundle/tps'], + src: ['bundle/tps-0@0.5x.webp.json', + 'bundle/tps-0@0.5x.png.json', + 'bundle/tps-0.webp.json', + 'bundle/tps-0.png.json'], data: { tags: { tps: true, @@ -232,7 +225,7 @@ describe('Manifest', () => }, ], }); - }, 30000); + }); it('should copy over files and add them to manifest', async () => { @@ -470,7 +463,6 @@ describe('Manifest', () => includeMetaData: false, }), spineAtlasManifestMod(), - texturePackerManifestMod(), ], }); @@ -568,7 +560,6 @@ describe('Manifest', () => includeMetaData: false, }), spineAtlasManifestMod(), - texturePackerManifestMod(), ], }); @@ -686,7 +677,6 @@ describe('Manifest', () => includeMetaData: false, }), spineAtlasManifestMod(), - texturePackerManifestMod(), ], }); @@ -833,7 +823,6 @@ describe('Manifest', () => includeMetaData: false, }), spineAtlasManifestMod(), - texturePackerManifestMod(), ], }); diff --git a/test/texture-packer/texturePackerCacheBuster.test.ts b/test/texture-packer/texturePackerCacheBuster.test.ts index 8b04212..1ff9b36 100644 --- a/test/texture-packer/texturePackerCacheBuster.test.ts +++ b/test/texture-packer/texturePackerCacheBuster.test.ts @@ -3,8 +3,10 @@ import { glob } from 'glob'; import { describe, expect, it } from 'vitest'; import { cacheBuster } from '../../src/cache-buster/index.js'; import { AssetPack } from '../../src/core/index.js'; +import { compress } from '../../src/image/compress.js'; import { texturePacker } from '../../src/texture-packer/texturePacker.js'; import { texturePackerCacheBuster } from '../../src/texture-packer/texturePackerCacheBuster.js'; +import { texturePackerCompress } from '../../src/texture-packer/texturePackerCompress.js'; import { createTPSFolder } from '../utils/createTPSFolder.js'; import { getCacheDir, getInputDir, getOutputDir } from '../utils/index.js'; @@ -28,6 +30,7 @@ describe('Texture Packer Cache Buster', () => texturePacker({ resolutionOptions: { resolutions: { default: 1 }, + maximumTextureSize: 512, }, }), cacheBuster(), @@ -41,9 +44,9 @@ describe('Texture Packer Cache Buster', () => const files = await glob(globPath); // need two sets of files - expect(files.length).toBe(2); - expect(files.filter((file) => file.endsWith('.json')).length).toBe(1); - expect(files.filter((file) => file.endsWith('.png')).length).toBe(1); + expect(files.length).toBe(4); + expect(files.filter((file) => file.endsWith('.json')).length).toBe(2); + expect(files.filter((file) => file.endsWith('.png')).length).toBe(2); const jsonFiles = files.filter((file) => file.endsWith('.json')); const pngFiles = files.filter((file) => file.endsWith('.png')); @@ -53,18 +56,81 @@ describe('Texture Packer Cache Buster', () => { const rawJson = fs.readJSONSync(jsonFile); - const checkFiles = (fileList: string[]) => + expect(pngFiles.includes(`${outputDir}/${rawJson.meta.image}`)).toBe(true); + + // check if json has related_multi_packs + if (rawJson.meta.related_multi_packs) + { + const relatedMultiPacks = rawJson.meta.related_multi_packs as string[]; + + expect(relatedMultiPacks.length).toBe(1); + expect(jsonFiles.includes(`${outputDir}/${relatedMultiPacks[0]}`)).toBe(true); + } + }); + }); + + it('should create compressed sprite sheet and correctly update json', async () => + { + const testName = 'tp-cache-bust-compress'; + const inputDir = getInputDir(pkg, testName); + const outputDir = getOutputDir(pkg, testName); + + createTPSFolder(testName, pkg); + + const assetpack = new AssetPack({ + entry: inputDir, cacheLocation: getCacheDir(pkg, testName), + output: outputDir, + cache: false, + pipes: [ + texturePacker({ + resolutionOptions: { + resolutions: { default: 1 }, + maximumTextureSize: 512, + }, + }), + compress(), + texturePackerCompress(), + cacheBuster(), + texturePackerCacheBuster() + ] + }); + + await assetpack.run(); + + const globPath = `${outputDir}/*.{json,png,webp}`; + const files = await glob(globPath); + + expect(files.length).toBe(8); + expect(files.filter((file) => file.endsWith('.json')).length).toBe(4); + expect(files.filter((file) => file.endsWith('.png')).length).toBe(2); + expect(files.filter((file) => file.endsWith('.webp')).length).toBe(2); + + const jsonFiles = files.filter((file) => file.endsWith('.json')); + const pngFiles = files.filter((file) => file.endsWith('.png')); + const webpFiles = files.filter((file) => file.endsWith('.webp')); + + // check that the files are correct + jsonFiles.forEach((jsonFile) => + { + const rawJson = fs.readJSONSync(jsonFile); + + if (rawJson.meta.image.includes('.webp')) { - fileList.forEach((file) => - { - // remove the outputDir - file = file.replace(`${outputDir}/`, ''); + expect(webpFiles.includes(`${outputDir}/${rawJson.meta.image}`)).toBe(true); + } + else + { + expect(pngFiles.includes(`${outputDir}/${rawJson.meta.image}`)).toBe(true); + } - expect(rawJson.meta.image).toEqual(file); - }); - }; + // check if json has related_multi_packs + if (rawJson.meta.related_multi_packs) + { + const relatedMultiPacks = rawJson.meta.related_multi_packs as string[]; - checkFiles(pngFiles); + expect(relatedMultiPacks.length).toBe(1); + expect(jsonFiles.includes(`${outputDir}/${relatedMultiPacks[0]}`)).toBe(true); + } }); }); }); diff --git a/test/texture-packer/texturePackerManifest.test.ts b/test/texture-packer/texturePackerManifest.test.ts index b36ee2f..f7c626e 100644 --- a/test/texture-packer/texturePackerManifest.test.ts +++ b/test/texture-packer/texturePackerManifest.test.ts @@ -1,9 +1,9 @@ import fs from 'fs-extra'; import { describe, expect, it } from 'vitest'; import { AssetPack } from '../../src/core/index.js'; +import { compress } from '../../src/image/compress.js'; import { pixiManifest } from '../../src/manifest/index.js'; -import { texturePacker } from '../../src/texture-packer/index.js'; -import { texturePackerManifestMod } from '../../src/texture-packer/texturePackerManifestMod.js'; +import { texturePacker, texturePackerCompress } from '../../src/texture-packer/index.js'; import { createTPSFolder } from '../utils/createTPSFolder.js'; import { getCacheDir, getInputDir, getOutputDir } from '../utils/index.js'; @@ -30,7 +30,6 @@ describe('Texture Packer Compression', () => }, }), pixiManifest(), - texturePackerManifestMod(), ] }); @@ -70,12 +69,13 @@ describe('Texture Packer Compression', () => pipes: [ texturePacker({ resolutionOptions: { - resolutions: { default: 1 }, - maximumTextureSize: 512 + resolutions: { default: 1, low: 0.5 }, + maximumTextureSize: 512, }, }), + compress(), + texturePackerCompress(), pixiManifest(), - texturePackerManifestMod(), ] }); @@ -86,10 +86,13 @@ describe('Texture Packer Compression', () => expect(manifest.bundles[0].assets).toEqual([ { alias: [ - 'sprites-0' + 'sprites' ], src: [ - 'sprites-0.json' + 'sprites-0@0.5x.webp.json', + 'sprites-0@0.5x.png.json', + 'sprites-0.webp.json', + 'sprites-0.png.json', ], data: { tags: { @@ -97,19 +100,6 @@ describe('Texture Packer Compression', () => } } }, - { - alias: [ - 'sprites-1' - ], - src: [ - 'sprites-1.json' - ], - data: { - tags: { - tps: true - } - } - } ]); }); });