diff --git a/js/spine-utils.js b/js/spine-utils.js index b3b90f2..6d47839 100644 --- a/js/spine-utils.js +++ b/js/spine-utils.js @@ -507,10 +507,15 @@ function discoverAssets(filePath, options) { const selected = path.basename(filePath); const selectedStem = normalizeStem(selected); // 先扫码“选中 item 文件夹 + 同级所有 .info”(精确、不跨批次)。 - const roots = getSearchRoots(filePath); - // 若同级范围无法凑齐骨架+图集(常见于 Eagle 导入瞬间,同伴落在别的 .info), - // 则扩展搜索到整个库的 images/ 目录(全库兜底,靠“同家族 + 同文件夹 + 最近 btime”配对)。 - buildRootsWithLibraryRoot(filePath, roots); + let roots; + if (Array.isArray(options.searchRoots) && options.searchRoots.length) { + roots = [...new Set(options.searchRoots.map((root) => path.resolve(root)))]; + } else { + roots = getSearchRoots(filePath); + // 兼容旧的非查询调用:扩展到整个库。查看器会传入 Eagle API + // 明确列举的候选 item 目录,避免每次预览扫描整库。 + buildRootsWithLibraryRoot(filePath, roots); + } // 导入时 Eagle 会先创建各个 .info,再异步写 folders。严格模式必须把尚未写入 // folders 的同批资源也纳入候选,之后用 metadata.json 的 birthtime 精确配对。 diff --git a/release/SpineViewer.eagleplugin b/release/SpineViewer.eagleplugin index a0ad2b3..3995e28 100644 Binary files a/release/SpineViewer.eagleplugin and b/release/SpineViewer.eagleplugin differ diff --git a/release/UNIVERSAL_SpineViewer/js/spine-utils.js b/release/UNIVERSAL_SpineViewer/js/spine-utils.js index b3b90f2..6d47839 100644 --- a/release/UNIVERSAL_SpineViewer/js/spine-utils.js +++ b/release/UNIVERSAL_SpineViewer/js/spine-utils.js @@ -507,10 +507,15 @@ function discoverAssets(filePath, options) { const selected = path.basename(filePath); const selectedStem = normalizeStem(selected); // 先扫码“选中 item 文件夹 + 同级所有 .info”(精确、不跨批次)。 - const roots = getSearchRoots(filePath); - // 若同级范围无法凑齐骨架+图集(常见于 Eagle 导入瞬间,同伴落在别的 .info), - // 则扩展搜索到整个库的 images/ 目录(全库兜底,靠“同家族 + 同文件夹 + 最近 btime”配对)。 - buildRootsWithLibraryRoot(filePath, roots); + let roots; + if (Array.isArray(options.searchRoots) && options.searchRoots.length) { + roots = [...new Set(options.searchRoots.map((root) => path.resolve(root)))]; + } else { + roots = getSearchRoots(filePath); + // 兼容旧的非查询调用:扩展到整个库。查看器会传入 Eagle API + // 明确列举的候选 item 目录,避免每次预览扫描整库。 + buildRootsWithLibraryRoot(filePath, roots); + } // 导入时 Eagle 会先创建各个 .info,再异步写 folders。严格模式必须把尚未写入 // folders 的同批资源也纳入候选,之后用 metadata.json 的 birthtime 精确配对。 diff --git a/release/UNIVERSAL_SpineViewer/viewer/spine.html b/release/UNIVERSAL_SpineViewer/viewer/spine.html index 7bcec35..3847479 100644 --- a/release/UNIVERSAL_SpineViewer/viewer/spine.html +++ b/release/UNIVERSAL_SpineViewer/viewer/spine.html @@ -545,6 +545,37 @@ return false; } + async function getPreviewSearchRoots(sourcePath) { + const currentDir = path.dirname(sourcePath); + const libraryRoot = utils.findLibraryRoot(sourcePath); + if (!libraryRoot) return null; + const folders = utils.foldersOfItem(sourcePath); + const params = new URLSearchParams({ + limit: '1000', + keyword: utils.normalizeStem(path.basename(sourcePath)), + }); + if (folders.length) params.set('folders', folders.join(',')); + try { + const response = await fetch('http://localhost:41595/api/item/list?' + params.toString() + '&_=' + Date.now(), { + cache: 'no-store', + }); + const result = await response.json(); + if (!response.ok || !result || result.status !== 'success' || !Array.isArray(result.data)) return null; + const imagesDir = path.join(libraryRoot, 'images'); + const roots = [currentDir]; + for (const item of result.data) { + if (!item || !/^[A-Za-z0-9_-]+$/.test(item.id || '')) continue; + if (!utils.extOf(String(item.name || '') + '.' + String(item.ext || ''))) continue; + const itemDir = path.join(imagesDir, item.id + '.info'); + if (utils.isEagleItemDir(itemDir)) roots.push(itemDir); + } + return [...new Set(roots.map((root) => path.resolve(root)))]; + } catch (error) { + log('Eagle 候选素材查询失败,回退到本地发现: ' + error.message); + return null; + } + } + function currentResourcePaths() { if (!activeAssets) return []; const groupStems = new Set([ @@ -1142,7 +1173,8 @@ else showTextPreview(name, parsed === undefined ? text : JSON.stringify(parsed, null, 2)); return; } - const assets = utils.discoverAssets(filePath); + const previewSearchRoots = await getPreviewSearchRoots(filePath); + const assets = utils.discoverAssets(filePath, previewSearchRoots ? { searchRoots: previewSearchRoots } : undefined); if (!assets.skeletonPath || !assets.atlasPath) { if (!assets.skeletonPath) { diff --git a/tests/compound-extensions.test.js b/tests/compound-extensions.test.js index 85493ff..de0a81b 100644 --- a/tests/compound-extensions.test.js +++ b/tests/compound-extensions.test.js @@ -35,6 +35,8 @@ try { assert.equal(utils.discoverAssets(lateAtlas, { strictStem: true }).skeletonPath, null); const lateSkeleton = path.join(late, 'hero.skel.bytes'); fs.writeFileSync(lateSkeleton, Buffer.from('\0' + '3.8.1\0')); assert.equal(utils.discoverAssets(lateAtlas, { strictStem: true, refreshIndex: true }).skeletonPath, lateSkeleton); + const scoped = utils.discoverAssets(lateAtlas, { searchRoots: [late] }); + assert.equal(scoped.skeletonPath, lateSkeleton); for (const [name, json, expected] of [ ['spine.json', { skeleton: { spine: '4.2.43' }, bones: [] }, 'spine'], ['empty-version.json', { skeleton: { spine: '' }, bones: [] }, 'other'], diff --git a/viewer/spine.html b/viewer/spine.html index 7bcec35..3847479 100644 --- a/viewer/spine.html +++ b/viewer/spine.html @@ -545,6 +545,37 @@ return false; } + async function getPreviewSearchRoots(sourcePath) { + const currentDir = path.dirname(sourcePath); + const libraryRoot = utils.findLibraryRoot(sourcePath); + if (!libraryRoot) return null; + const folders = utils.foldersOfItem(sourcePath); + const params = new URLSearchParams({ + limit: '1000', + keyword: utils.normalizeStem(path.basename(sourcePath)), + }); + if (folders.length) params.set('folders', folders.join(',')); + try { + const response = await fetch('http://localhost:41595/api/item/list?' + params.toString() + '&_=' + Date.now(), { + cache: 'no-store', + }); + const result = await response.json(); + if (!response.ok || !result || result.status !== 'success' || !Array.isArray(result.data)) return null; + const imagesDir = path.join(libraryRoot, 'images'); + const roots = [currentDir]; + for (const item of result.data) { + if (!item || !/^[A-Za-z0-9_-]+$/.test(item.id || '')) continue; + if (!utils.extOf(String(item.name || '') + '.' + String(item.ext || ''))) continue; + const itemDir = path.join(imagesDir, item.id + '.info'); + if (utils.isEagleItemDir(itemDir)) roots.push(itemDir); + } + return [...new Set(roots.map((root) => path.resolve(root)))]; + } catch (error) { + log('Eagle 候选素材查询失败,回退到本地发现: ' + error.message); + return null; + } + } + function currentResourcePaths() { if (!activeAssets) return []; const groupStems = new Set([ @@ -1142,7 +1173,8 @@ else showTextPreview(name, parsed === undefined ? text : JSON.stringify(parsed, null, 2)); return; } - const assets = utils.discoverAssets(filePath); + const previewSearchRoots = await getPreviewSearchRoots(filePath); + const assets = utils.discoverAssets(filePath, previewSearchRoots ? { searchRoots: previewSearchRoots } : undefined); if (!assets.skeletonPath || !assets.atlasPath) { if (!assets.skeletonPath) {