优化识别
This commit is contained in:
1 parent
7263a841cb
commit
5cb2e76471
9 files changed
+302
-66
No files matched your search
+133
-35
@@ -15,33 +15,50 @@ function log(msg) {
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
/** 插件代码版本:每次改动递增,用于确认 Eagle 是否加载到最新代码 */
|
||||
const VERSION = 'v4';
|
||||
const VERSION = 'v6';
|
||||
|
||||
/** 发现配套资源;带重试,以应对 Eagle 导入时“多文件分批写入库”的时序问题。
|
||||
* 同伴文件(骨骼/图集)可能在 Atlas 的缩略图生成后才落盘,故需等待。
|
||||
* 一旦发现即立即返回(不会傻等满窗口),窗口仅作为最坏情况的上限。
|
||||
* 采用 10 次 × 150ms ≈ 1.5 秒;间隔更密、最快感知同伴落盘。 */
|
||||
async function discoverWithRetry(src, retries = 10, delay = 150) {
|
||||
/** 返回资源当前的尺寸/修改时间签名,同时确认 atlas 引用的全部贴图均已落盘。 */
|
||||
function assetSignature(assets) {
|
||||
if (!assets || !assets.skeletonPath || !assets.atlasPath) return null;
|
||||
let atlasText = '';
|
||||
try { atlasText = fs.readFileSync(assets.atlasPath, 'utf8'); } catch (e) { return null; }
|
||||
const pages = utils.parseAtlasPages(atlasText);
|
||||
if (pages.length === 0 || pages.some((name) => !assets.texturePaths[name])) return null;
|
||||
const paths = [assets.skeletonPath, assets.atlasPath].concat(pages.map((name) => assets.texturePaths[name]));
|
||||
const stats = [];
|
||||
for (const file of paths) {
|
||||
try {
|
||||
const stat = fs.statSync(file);
|
||||
if (!stat.isFile() || stat.size === 0) return null;
|
||||
stats.push(file + ':' + stat.size + ':' + Math.round(stat.mtimeMs));
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return stats.join('|');
|
||||
}
|
||||
|
||||
/**
|
||||
* Eagle 拖入一组文件时会并发触发 atlas/json/skel 的缩略图回调,而文件内容和
|
||||
* folders 元数据仍可能继续写入。等同批资源连续三次保持不变后再渲染,避免把
|
||||
* 半截 atlas 或旧库中的同名骨骼渲染成占位图。
|
||||
*/
|
||||
async function discoverWithRetry(src, retries = 48, delay = 250) {
|
||||
let assets = null;
|
||||
let previousSignature = null;
|
||||
let stableCount = 0;
|
||||
for (let i = 0; i < retries; i++) {
|
||||
assets = utils.discoverAssets(src);
|
||||
if (assets.skeletonPath && assets.atlasPath) return assets;
|
||||
assets = utils.discoverAssets(src, { strictStem: true, preferCompatibleJson: true });
|
||||
const signature = assetSignature(assets);
|
||||
if (signature && signature === previousSignature) stableCount += 1;
|
||||
else stableCount = signature ? 1 : 0;
|
||||
previousSignature = signature;
|
||||
if (stableCount >= 3) return assets;
|
||||
await sleep(delay);
|
||||
}
|
||||
return assets;
|
||||
}
|
||||
|
||||
/** 渲染缩略图;失败时重试一次(应对偶发渲染错误) */
|
||||
async function renderWithRetry(opts) {
|
||||
try {
|
||||
return await render.renderThumbnail(opts);
|
||||
} catch (e) {
|
||||
log('renderThumbnail 首次失败: ' + (e && e.message) + ',重试...');
|
||||
await sleep(150);
|
||||
return await render.renderThumbnail(opts);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Eagle 格式扩展缩略图处理函数。
|
||||
* 参数:{ src, dest, item }
|
||||
@@ -73,34 +90,77 @@ module.exports = async ({ src, dest, item }) => {
|
||||
return resolve(item);
|
||||
}
|
||||
|
||||
const assets = await discoverWithRetry(src);
|
||||
let assets = await discoverWithRetry(src);
|
||||
|
||||
// 找不到配套 Spine 资源
|
||||
if (!assets.skeletonPath || !assets.atlasPath) {
|
||||
log('未找到配套资源: ' + src + ' (skeleton=' + !!assets.skeletonPath + ', atlas=' + !!assets.atlasPath + ')');
|
||||
await writePlaceholder(dest, path.basename(src, utils.extOf(path.basename(src)) || ''));
|
||||
if (assets.atlasPath && await writeAtlasFallback(assets, dest)) {
|
||||
item.width = 400; item.height = 400;
|
||||
return resolve(item);
|
||||
}
|
||||
await writePlaceholder(dest);
|
||||
item.width = 400; item.height = 400;
|
||||
return resolve(item);
|
||||
}
|
||||
|
||||
const version = utils.detectVersion(assets.skeletonPath);
|
||||
const runtimeDir = utils.chooseRuntimeDir(version, assets.isBinary);
|
||||
let version = utils.detectVersion(assets.skeletonPath);
|
||||
let runtimeDir = utils.chooseRuntimeDir(version, assets.isBinary);
|
||||
|
||||
// 3.6/3.7 二进制没有对应解析器。若有同批 JSON,资源发现阶段已自动切换;
|
||||
// 走到这里说明没有 JSON,直接显示 atlas 实际贴图,避免无意义地重复解析。
|
||||
if (assets.isBinary && version && parseFloat(version) < 3.8) {
|
||||
if (await writeAtlasFallback(assets, dest)) {
|
||||
log('旧版二进制无 JSON,已使用 atlas 贴图缩略图: ' + src);
|
||||
item.width = 400; item.height = 400;
|
||||
return resolve(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (!runtimeDir) {
|
||||
await writePlaceholder(dest, path.basename(assets.skeletonPath, path.extname(assets.skeletonPath)));
|
||||
if (await writeAtlasFallback(assets, dest)) {
|
||||
item.width = 400; item.height = 400;
|
||||
return resolve(item);
|
||||
}
|
||||
await writePlaceholder(dest);
|
||||
item.width = 400; item.height = 400;
|
||||
return resolve(item);
|
||||
}
|
||||
|
||||
// 渲染首帧到 PNG
|
||||
const png = await renderWithRetry({
|
||||
skeletonPath: assets.skeletonPath,
|
||||
atlasPath: assets.atlasPath,
|
||||
texturePaths: assets.texturePaths,
|
||||
version,
|
||||
runtimeDir,
|
||||
size: 400,
|
||||
});
|
||||
// 渲染首帧到 PNG。若导入过程恰好仍在写大文件,重新发现稳定资源再试,
|
||||
// 每次都会重新匹配同批 atlas/json/skel,而不是重复读取原来的错误组合。
|
||||
let png = null;
|
||||
let lastRenderError = null;
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
try {
|
||||
png = await render.renderThumbnail({
|
||||
skeletonPath: assets.skeletonPath,
|
||||
atlasPath: assets.atlasPath,
|
||||
texturePaths: assets.texturePaths,
|
||||
version,
|
||||
runtimeDir,
|
||||
size: 400,
|
||||
});
|
||||
break;
|
||||
} catch (e) {
|
||||
lastRenderError = e;
|
||||
log('renderThumbnail 第 ' + (attempt + 1) + ' 次失败: ' + (e && e.message) + ',重新等待同批资源...');
|
||||
if (attempt === 2) break;
|
||||
await sleep(500);
|
||||
assets = await discoverWithRetry(src, 16, 250);
|
||||
if (!assets.skeletonPath || !assets.atlasPath || !assetSignature(assets)) continue;
|
||||
version = utils.detectVersion(assets.skeletonPath);
|
||||
runtimeDir = utils.chooseRuntimeDir(version, assets.isBinary);
|
||||
}
|
||||
}
|
||||
if (!png) {
|
||||
if (await writeAtlasFallback(assets, dest)) {
|
||||
log('骨骼无法解析,已使用 atlas 贴图缩略图: ' + src);
|
||||
item.width = 400; item.height = 400;
|
||||
return resolve(item);
|
||||
}
|
||||
throw lastRenderError || new Error('Spine 资源尚未写入完成');
|
||||
}
|
||||
|
||||
fs.writeFileSync(dest, png);
|
||||
|
||||
@@ -115,7 +175,7 @@ module.exports = async ({ src, dest, item }) => {
|
||||
// 兜底:总是生成一张有效 PNG,绝不 reject,避免 Eagle 显示默认图标
|
||||
log('缩略图处理异常: ' + (err && err.message) + '\n' + ((err && err.stack) || ''));
|
||||
try {
|
||||
await writePlaceholder(dest, 'Spine');
|
||||
await writePlaceholder(dest);
|
||||
item.width = 400;
|
||||
item.height = 400;
|
||||
return resolve(item);
|
||||
@@ -153,6 +213,33 @@ async function copyImageAsThumb(src, dest) {
|
||||
fs.copyFileSync(src, dest);
|
||||
}
|
||||
|
||||
/** 骨骼二进制无法解析时,用真实 atlas 贴图生成可辨认的回退缩略图。 */
|
||||
async function writeAtlasFallback(assets, dest) {
|
||||
if (!render.canvasLib || !assets || !assets.texturePaths) return false;
|
||||
const texturePath = Object.values(assets.texturePaths).find((file) => {
|
||||
try { return fs.statSync(file).size > 0; } catch (e) { return false; }
|
||||
});
|
||||
if (!texturePath) return false;
|
||||
try {
|
||||
const img = await render.canvasLib.loadImage(texturePath);
|
||||
const size = 400;
|
||||
const canvas = render.canvasLib.createCanvas(size, size);
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = '#1e1e22';
|
||||
ctx.fillRect(0, 0, size, size);
|
||||
const scale = Math.min((size - 24) / img.width, (size - 24) / img.height);
|
||||
const w = Math.max(1, Math.round(img.width * scale));
|
||||
const h = Math.max(1, Math.round(img.height * scale));
|
||||
ctx.drawImage(img, Math.round((size - w) / 2), Math.round((size - h) / 2), w, h);
|
||||
await render.drawSpineBadge(ctx, size, size);
|
||||
fs.writeFileSync(dest, canvas.toBuffer('image/png'));
|
||||
return true;
|
||||
} catch (e) {
|
||||
log('atlas 贴图回退失败: ' + (e && e.message));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 读取 PNG 宽高(仅支持 PNG 头部) */
|
||||
function imageSizeOf(file) {
|
||||
const buf = fs.readFileSync(file);
|
||||
@@ -163,8 +250,19 @@ function imageSizeOf(file) {
|
||||
}
|
||||
|
||||
/** 生成一个纯 JS 占位 PNG(无需外部依赖),作为兜底 */
|
||||
async function writePlaceholder(dest, text) {
|
||||
async function writePlaceholder(dest) {
|
||||
const w = 400, h = 400;
|
||||
if (render.canvasLib) {
|
||||
try {
|
||||
const canvas = render.canvasLib.createCanvas(w, h);
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = '#1e1e22';
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
await render.drawSpineBadge(ctx, w, h);
|
||||
fs.writeFileSync(dest, canvas.toBuffer('image/png'));
|
||||
return;
|
||||
} catch (e) { /* fall through to pure JS encoder */ }
|
||||
}
|
||||
// 深色底
|
||||
const pixels = Buffer.alloc(w * h * 4);
|
||||
for (let y = 0; y < h; y++) {
|
||||
|
||||
Reference in new issue
Block a user