From e7f800aa8d7dc1f65c0bf57d40d08629f6078e1a Mon Sep 17 00:00:00 2001 From: tianmo Date: Fri, 6 Nov 2026 10:44:34 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E8=AF=86=E5=88=AB=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SpineThumbnailGenerateServer/js/plugin.js | 2 +- .../js/spine-utils.js | 8 ++++ .../tests/mixed-atlas-textures.test.js | 47 +++++++++++++++++++ js/spine-utils.js | 8 ++++ .../js/plugin.js | 2 +- .../js/spine-utils.js | 8 ++++ .../UNIVERSAL_SpineViewer/js/spine-utils.js | 8 ++++ tests/atlas-mixed-textures.test.js | 47 +++++++++++++++++++ 8 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 SpineThumbnailGenerateServer/tests/mixed-atlas-textures.test.js create mode 100644 tests/atlas-mixed-textures.test.js diff --git a/SpineThumbnailGenerateServer/js/plugin.js b/SpineThumbnailGenerateServer/js/plugin.js index 4568ab0..61f3295 100644 --- a/SpineThumbnailGenerateServer/js/plugin.js +++ b/SpineThumbnailGenerateServer/js/plugin.js @@ -2,7 +2,7 @@ // 每次改动递增。Eagle 里的 service 插件是启动时加载并常驻的,改完代码必须重新加载插件 // 才会生效;状态栏会显示这个版本号,方便确认加载的到底是不是最新代码。 -const VERSION = 'v72'; +const VERSION = 'v73'; const SHOW_DEV_VERSION = true; const path = require('path'); diff --git a/SpineThumbnailGenerateServer/js/spine-utils.js b/SpineThumbnailGenerateServer/js/spine-utils.js index f3005a9..c3690ac 100644 --- a/SpineThumbnailGenerateServer/js/spine-utils.js +++ b/SpineThumbnailGenerateServer/js/spine-utils.js @@ -176,6 +176,11 @@ function isTextureFile(name) { return TEXTURE_EXTS.indexOf(ext) !== -1; } +function textureFormatOf(name) { + const ext = extOf(name); + return ext === '.jpg' || ext === '.jpeg' ? 'jpeg' : ext; +} + /** 去掉文件名中的扩展名 */ function stemOf(name) { const ext = extOf(name); @@ -749,6 +754,8 @@ function discoverAssets(filePath, options) { const pageFileName = path.posix.basename(pn); let cands = textureFiles.filter((f) => f.name.toLowerCase() === pageFileName.toLowerCase()); if (cands.length === 0) cands = textureFiles.filter((f) => normalizeStem(f.name) === normalizeStem(pageFileName)); + const sameFormat = cands.filter((f) => textureFormatOf(f.name) === textureFormatOf(pageFileName)); + if (sameFormat.length > 0) cands = sameFormat; let hit = selectBest(cands, atlas.fullPath); if (strictStem && hit && Math.abs(importTimeOf(hit.fullPath) - importTimeOf(atlas.fullPath)) > allowedImportDelta) hit = null; if (hit) texturePaths[pn] = hit.fullPath; @@ -830,6 +837,7 @@ const spineUtilsApi = { isSkeletonFile, isAtlasFile, isTextureFile, + textureFormatOf, stemOf, normalizeStem, DEFAULT_SUFFIXES, diff --git a/SpineThumbnailGenerateServer/tests/mixed-atlas-textures.test.js b/SpineThumbnailGenerateServer/tests/mixed-atlas-textures.test.js new file mode 100644 index 0000000..85ee168 --- /dev/null +++ b/SpineThumbnailGenerateServer/tests/mixed-atlas-textures.test.js @@ -0,0 +1,47 @@ +'use strict'; + +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const utils = require('../js/spine-utils'); + +const root = fs.mkdtempSync(path.join(os.tmpdir(), 'spine-mixed-atlas-')); +try { + const images = path.join(root, 'images'); + fs.mkdirSync(images); + fs.writeFileSync(path.join(root, 'metadata.json'), JSON.stringify({ folders: [] })); + const makeItem = (id, name, body) => { + const directory = path.join(images, id + '.info'); + fs.mkdirSync(directory); + fs.writeFileSync(path.join(directory, name), body); + fs.writeFileSync(path.join(directory, 'metadata.json'), JSON.stringify({ folders: [] })); + return path.join(directory, name); + }; + const png = makeItem('png', 'hero.png', 'png'); + const atlas = makeItem('atlas', 'hero.atlas', [ + 'hero.png', + 'size: 64,64', + 'format: RGBA8888', + 'filter: Linear,Linear', + 'repeat: none', + '', + 'hero.jpeg', + 'size: 32,32', + 'format: RGBA8888', + 'filter: Linear,Linear', + 'repeat: none', + '', + ].join('\n')); + const jpeg = makeItem('jpg', 'hero.jpg', 'jpg'); + const assets = utils.discoverAssets(atlas, { + strictStem: true, + searchRoots: [path.dirname(png), path.dirname(atlas), path.dirname(jpeg)], + }); + assert.equal(assets.texturePaths['hero.png'], png); + assert.equal(assets.texturePaths['hero.jpeg'], jpeg, 'JPEG atlas page must prefer .jpg over same-stem .png'); + assert.equal(utils.textureFormatOf('photo.JPEG'), utils.textureFormatOf('photo.jpg')); + console.log('PASS: mixed PNG/JPEG atlas pages resolve to matching texture formats'); +} finally { + fs.rmSync(root, { recursive: true, force: true }); +} diff --git a/js/spine-utils.js b/js/spine-utils.js index f9d1cf2..ccfcd4f 100644 --- a/js/spine-utils.js +++ b/js/spine-utils.js @@ -173,6 +173,11 @@ function isTextureFile(name) { return TEXTURE_EXTS.indexOf(ext) !== -1; } +function textureFormatOf(name) { + const ext = extOf(name); + return ext === '.jpg' || ext === '.jpeg' ? 'jpeg' : ext; +} + /** 去掉文件名中的扩展名 */ function stemOf(name) { const ext = extOf(name); @@ -726,6 +731,8 @@ function discoverAssets(filePath, options) { const pageFileName = path.posix.basename(pn); let cands = textureFiles.filter((f) => f.name.toLowerCase() === pageFileName.toLowerCase()); if (cands.length === 0) cands = textureFiles.filter((f) => normalizeStem(f.name) === normalizeStem(pageFileName)); + const sameFormat = cands.filter((f) => textureFormatOf(f.name) === textureFormatOf(pageFileName)); + if (sameFormat.length > 0) cands = sameFormat; let hit = selectBest(cands, atlas.fullPath); if (strictStem && hit && Math.abs(importTimeOf(hit.fullPath) - importTimeOf(atlas.fullPath)) > allowedImportDelta) hit = null; if (hit) texturePaths[pn] = hit.fullPath; @@ -807,6 +814,7 @@ module.exports = { isSkeletonFile, isAtlasFile, isTextureFile, + textureFormatOf, stemOf, normalizeStem, DEFAULT_SUFFIXES, diff --git a/release/UNIVERSAL_SpineThumbnailGenerateServer/js/plugin.js b/release/UNIVERSAL_SpineThumbnailGenerateServer/js/plugin.js index e28a267..ec3f4e2 100644 --- a/release/UNIVERSAL_SpineThumbnailGenerateServer/js/plugin.js +++ b/release/UNIVERSAL_SpineThumbnailGenerateServer/js/plugin.js @@ -2,7 +2,7 @@ // 每次改动递增。Eagle 里的 service 插件是启动时加载并常驻的,改完代码必须重新加载插件 // 才会生效;状态栏会显示这个版本号,方便确认加载的到底是不是最新代码。 -const VERSION = 'v72'; +const VERSION = 'v73'; const SHOW_DEV_VERSION = false; const path = require('path'); diff --git a/release/UNIVERSAL_SpineThumbnailGenerateServer/js/spine-utils.js b/release/UNIVERSAL_SpineThumbnailGenerateServer/js/spine-utils.js index f3005a9..c3690ac 100644 --- a/release/UNIVERSAL_SpineThumbnailGenerateServer/js/spine-utils.js +++ b/release/UNIVERSAL_SpineThumbnailGenerateServer/js/spine-utils.js @@ -176,6 +176,11 @@ function isTextureFile(name) { return TEXTURE_EXTS.indexOf(ext) !== -1; } +function textureFormatOf(name) { + const ext = extOf(name); + return ext === '.jpg' || ext === '.jpeg' ? 'jpeg' : ext; +} + /** 去掉文件名中的扩展名 */ function stemOf(name) { const ext = extOf(name); @@ -749,6 +754,8 @@ function discoverAssets(filePath, options) { const pageFileName = path.posix.basename(pn); let cands = textureFiles.filter((f) => f.name.toLowerCase() === pageFileName.toLowerCase()); if (cands.length === 0) cands = textureFiles.filter((f) => normalizeStem(f.name) === normalizeStem(pageFileName)); + const sameFormat = cands.filter((f) => textureFormatOf(f.name) === textureFormatOf(pageFileName)); + if (sameFormat.length > 0) cands = sameFormat; let hit = selectBest(cands, atlas.fullPath); if (strictStem && hit && Math.abs(importTimeOf(hit.fullPath) - importTimeOf(atlas.fullPath)) > allowedImportDelta) hit = null; if (hit) texturePaths[pn] = hit.fullPath; @@ -830,6 +837,7 @@ const spineUtilsApi = { isSkeletonFile, isAtlasFile, isTextureFile, + textureFormatOf, stemOf, normalizeStem, DEFAULT_SUFFIXES, diff --git a/release/UNIVERSAL_SpineViewer/js/spine-utils.js b/release/UNIVERSAL_SpineViewer/js/spine-utils.js index f9d1cf2..ccfcd4f 100644 --- a/release/UNIVERSAL_SpineViewer/js/spine-utils.js +++ b/release/UNIVERSAL_SpineViewer/js/spine-utils.js @@ -173,6 +173,11 @@ function isTextureFile(name) { return TEXTURE_EXTS.indexOf(ext) !== -1; } +function textureFormatOf(name) { + const ext = extOf(name); + return ext === '.jpg' || ext === '.jpeg' ? 'jpeg' : ext; +} + /** 去掉文件名中的扩展名 */ function stemOf(name) { const ext = extOf(name); @@ -726,6 +731,8 @@ function discoverAssets(filePath, options) { const pageFileName = path.posix.basename(pn); let cands = textureFiles.filter((f) => f.name.toLowerCase() === pageFileName.toLowerCase()); if (cands.length === 0) cands = textureFiles.filter((f) => normalizeStem(f.name) === normalizeStem(pageFileName)); + const sameFormat = cands.filter((f) => textureFormatOf(f.name) === textureFormatOf(pageFileName)); + if (sameFormat.length > 0) cands = sameFormat; let hit = selectBest(cands, atlas.fullPath); if (strictStem && hit && Math.abs(importTimeOf(hit.fullPath) - importTimeOf(atlas.fullPath)) > allowedImportDelta) hit = null; if (hit) texturePaths[pn] = hit.fullPath; @@ -807,6 +814,7 @@ module.exports = { isSkeletonFile, isAtlasFile, isTextureFile, + textureFormatOf, stemOf, normalizeStem, DEFAULT_SUFFIXES, diff --git a/tests/atlas-mixed-textures.test.js b/tests/atlas-mixed-textures.test.js new file mode 100644 index 0000000..70b8ca1 --- /dev/null +++ b/tests/atlas-mixed-textures.test.js @@ -0,0 +1,47 @@ +'use strict'; + +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const utils = require('../js/spine-utils'); + +const root = fs.mkdtempSync(path.join(os.tmpdir(), 'spineviewer-mixed-atlas-')); +try { + const images = path.join(root, 'images'); + fs.mkdirSync(images); + fs.writeFileSync(path.join(root, 'metadata.json'), JSON.stringify({ folders: [] })); + const makeItem = (id, name, body) => { + const directory = path.join(images, id + '.info'); + fs.mkdirSync(directory); + fs.writeFileSync(path.join(directory, name), body); + fs.writeFileSync(path.join(directory, 'metadata.json'), JSON.stringify({ folders: [] })); + return path.join(directory, name); + }; + const png = makeItem('png', 'hero.png', 'png'); + const atlas = makeItem('atlas', 'hero.atlas', [ + 'hero.png', + 'size: 64,64', + 'format: RGBA8888', + 'filter: Linear,Linear', + 'repeat: none', + '', + 'hero.jpeg', + 'size: 32,32', + 'format: RGBA8888', + 'filter: Linear,Linear', + 'repeat: none', + '', + ].join('\n')); + const jpeg = makeItem('jpg', 'hero.jpg', 'jpg'); + const assets = utils.discoverAssets(atlas, { + strictStem: true, + searchRoots: [path.dirname(png), path.dirname(atlas), path.dirname(jpeg)], + }); + assert.equal(assets.texturePaths['hero.png'], png); + assert.equal(assets.texturePaths['hero.jpeg'], jpeg, 'JPEG atlas page must prefer .jpg over same-stem .png'); + assert.equal(utils.textureFormatOf('photo.JPEG'), utils.textureFormatOf('photo.jpg')); + console.log('PASS: SpineViewer resolves mixed PNG/JPEG atlas pages by texture format'); +} finally { + fs.rmSync(root, { recursive: true, force: true }); +}