更新
This commit is contained in:
1 parent
9635428f78
commit
ef01cdd6ee
6 files changed
+172
-66
No files matched your search
+33
-62
@@ -17,12 +17,11 @@ function log(msg) {
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const discoveryJobs = new Map();
|
||||
const backgroundJobs = new Map();
|
||||
const renderJobs = new Map();
|
||||
const refreshJobs = new Map();
|
||||
|
||||
/** 插件代码版本:每次改动递增,用于确认 Eagle 是否加载到最新代码 */
|
||||
const VERSION = 'v13';
|
||||
const VERSION = 'v14';
|
||||
|
||||
function coordinationKey(src) {
|
||||
const root = utils.findLibraryRoot(src) || path.dirname(path.dirname(src));
|
||||
@@ -66,6 +65,7 @@ async function runDiscovery(src, retries, delay) {
|
||||
assets = utils.discoverAssets(src, {
|
||||
strictStem: true,
|
||||
allowCrossBatch: false,
|
||||
refreshIndex: i > 0,
|
||||
});
|
||||
const signature = assetSignature(assets);
|
||||
if (signature && signature === previousSignature) stableCount += 1;
|
||||
@@ -143,15 +143,9 @@ module.exports = async (options) => {
|
||||
}
|
||||
if (changed && item.id) {
|
||||
log('复合后缀元数据已规范化: name=' + item.name + ' ext=' + item.ext);
|
||||
// The item does not enter Eagle's item map until this callback returns.
|
||||
// Reconcile afterwards, using the correctly named persistent cache.
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const assets = utils.discoverAssets(src, { strictStem: true, allowCrossBatch: true });
|
||||
if (assetSignature(assets)) refreshRelatedThumbnails(assets, canonical, item.id).catch(e => log(String(e)));
|
||||
} catch (e) { log('复合后缀补刷失败: ' + String(e)); }
|
||||
}, 1500);
|
||||
}
|
||||
const ext = utils.extOf(path.basename(src));
|
||||
if (!item.isDeleted && (utils.isAtlasFile(src) || (utils.isSkeletonFile(src) && (ext !== '.json' || utils.classifySkeletonJson(src) === 'spine')))) scheduleBackgroundReconcile(src);
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -210,7 +204,6 @@ async function generate({ src, dest, item, deferRefresh }) {
|
||||
// 找不到完整 Spine 资源链(骨骼 + atlas + atlas 引用的全部贴图)
|
||||
if (!assets.skeletonPath || !assets.atlasPath || !assetSignature(assets)) {
|
||||
log('未找到配套资源: ' + src + ' (skeleton=' + !!assets.skeletonPath + ', atlas=' + !!assets.atlasPath + ')');
|
||||
scheduleBackgroundReconcile(src);
|
||||
if (assets.atlasPath && await writeAtlasFallback(assets, dest)) {
|
||||
item.width = 400; item.height = 400;
|
||||
return resolve(item);
|
||||
@@ -358,9 +351,9 @@ async function createWebpCopy(sourcePath, key) {
|
||||
/** 将一份已验证的缩略图主动应用到同一资源链的 atlas/json/skel 条目。 */
|
||||
async function refreshRelatedThumbnails(assets, thumbnailPath, currentItemId) {
|
||||
const signature = assetSignature(assets);
|
||||
if (!signature) return;
|
||||
if (!signature) return false;
|
||||
const ids = [...new Set((assets.relatedDataPaths || []).map(itemIdFromAssetPath).concat(currentItemId || []).filter(Boolean))];
|
||||
if (ids.length === 0) return;
|
||||
if (ids.length === 0) return true;
|
||||
const key = signature + '|' + ids.slice().sort().join(',');
|
||||
if (refreshJobs.has(key)) return refreshJobs.get(key);
|
||||
const job = (async () => {
|
||||
@@ -374,8 +367,10 @@ async function refreshRelatedThumbnails(assets, thumbnailPath, currentItemId) {
|
||||
const failed = results.filter((result) => result.status === 'rejected');
|
||||
if (failed.length) throw failed[0].reason;
|
||||
log('统一补刷完成: ' + ids.length + ' 个条目 [' + ids.join(',') + ']');
|
||||
return true;
|
||||
} catch (e) {
|
||||
log('统一补刷失败: ' + ((e && e.message) || String(e)));
|
||||
return false;
|
||||
} finally {
|
||||
if (apiPath !== thumbnailPath) {
|
||||
try { fs.unlinkSync(apiPath); } catch (e) { /* ignore */ }
|
||||
@@ -384,7 +379,7 @@ async function refreshRelatedThumbnails(assets, thumbnailPath, currentItemId) {
|
||||
})();
|
||||
refreshJobs.set(key, job);
|
||||
try {
|
||||
await job;
|
||||
return await job;
|
||||
} finally {
|
||||
setTimeout(() => { if (refreshJobs.get(key) === job) refreshJobs.delete(key); }, 2000);
|
||||
}
|
||||
@@ -395,56 +390,32 @@ async function refreshRelatedThumbnails(assets, thumbnailPath, currentItemId) {
|
||||
* 在链路补齐后渲染一次,并统一刷新此前已生成占位图的所有数据条目。
|
||||
*/
|
||||
function scheduleBackgroundReconcile(src) {
|
||||
const key = coordinationKey(src);
|
||||
if (backgroundJobs.has(key)) return;
|
||||
const job = (async () => {
|
||||
let previousSignature = null;
|
||||
let stableCount = 0;
|
||||
// 前几秒快速观察,随后降低频率;总计约两分钟。若更晚补拖第三个文件,
|
||||
// 新文件自己的缩略图回调会再次触发同一资源链发现,无需常驻轮询十分钟。
|
||||
for (let i = 0; i < 36; i++) {
|
||||
const delay = i < 8 ? 1000 : (i < 20 ? 2500 : 5000);
|
||||
await sleep(delay);
|
||||
const assets = utils.discoverAssets(src, {
|
||||
strictStem: true,
|
||||
allowCrossBatch: true,
|
||||
});
|
||||
const signature = assetSignature(assets);
|
||||
if (signature && signature === previousSignature) stableCount += 1;
|
||||
else stableCount = signature ? 1 : 0;
|
||||
previousSignature = signature;
|
||||
if (stableCount < 3) continue;
|
||||
|
||||
const tempPng = path.join(os.tmpdir(), 'spineviewer-reconcile-' + process.pid + '-' + Date.now() + '.png');
|
||||
try {
|
||||
const version = utils.detectVersion(assets.skeletonPath);
|
||||
const runtimeDir = utils.chooseRuntimeDir(version, assets.isBinary);
|
||||
let wrote = false;
|
||||
if (runtimeDir) {
|
||||
try {
|
||||
const png = await renderThumbnailShared(assets, { ...assets, version, runtimeDir, size: 400 });
|
||||
fs.writeFileSync(tempPng, png);
|
||||
wrote = true;
|
||||
} catch (e) {
|
||||
wrote = await writeAtlasFallback(assets, tempPng);
|
||||
}
|
||||
}
|
||||
if (wrote) {
|
||||
await refreshRelatedThumbnails(assets, tempPng);
|
||||
log('后台识别到完整资源链并补刷: ' + key);
|
||||
}
|
||||
} finally {
|
||||
try { if (fs.existsSync(tempPng)) fs.unlinkSync(tempPng); } catch (e) { /* ignore */ }
|
||||
}
|
||||
return;
|
||||
}
|
||||
log('后台等待资源链超时: ' + key);
|
||||
})();
|
||||
backgroundJobs.set(key, job);
|
||||
job.catch((e) => log('后台补刷异常: ' + ((e && e.message) || String(e))))
|
||||
.finally(() => { if (backgroundJobs.get(key) === job) backgroundJobs.delete(key); });
|
||||
try { require('../js/spine-reconcile-worker.js').enqueue(src); }
|
||||
catch (error) { log('无法启动后台补刷: ' + String(error)); }
|
||||
}
|
||||
|
||||
// Called by a separate Node worker; Eagle closes the thumbnail window when
|
||||
// its callback resolves, so timers in that window cannot finish this work.
|
||||
module.exports.reconcileOnce = async (src, previousSignature) => {
|
||||
if (!fs.existsSync(src) || utils.isItemDeleted(src)) return { complete: true };
|
||||
const assets = utils.discoverAssets(src, { strictStem: true, allowCrossBatch: true, refreshIndex: true });
|
||||
const signature = assetSignature(assets);
|
||||
if (!signature || signature !== previousSignature) return { signature, complete: false };
|
||||
const version = utils.detectVersion(assets.skeletonPath);
|
||||
const runtimeDir = utils.chooseRuntimeDir(version, assets.isBinary);
|
||||
if (!runtimeDir) return { signature, complete: true };
|
||||
const temp = path.join(os.tmpdir(), 'spineviewer-reconcile-' + crypto.randomBytes(12).toString('hex') + '.png');
|
||||
try {
|
||||
const png = await renderThumbnailShared(assets, { ...assets, version, runtimeDir, size: 400 });
|
||||
fs.writeFileSync(temp, png);
|
||||
const complete = await refreshRelatedThumbnails(assets, temp);
|
||||
return { signature, complete };
|
||||
} catch (error) {
|
||||
log('后台补刷重试: ' + String(error));
|
||||
return { signature, complete: false };
|
||||
} finally { try { fs.unlinkSync(temp); } catch (error) { if (error.code !== 'ENOENT') log(String(error)); } }
|
||||
};
|
||||
|
||||
/** 普通 JSON 使用中性文件图形,避免错误显示 Spine badge。 */
|
||||
async function writeNeutralJsonPlaceholder(dest) {
|
||||
const size = 400;
|
||||
|
||||
Reference in new issue
Block a user