新增3.4版本支持

This commit is contained in:
tianmo committed 2026-09-10 13:53:43 +08:00
1 parent 4c5033d2f9
commit 9635428f78
21 files changed
+5425 -51

No files matched your search

+71 -7
View File
@@ -22,7 +22,7 @@ const renderJobs = new Map();
const refreshJobs = new Map();
/** 插件代码版本:每次改动递增,用于确认 Eagle 是否加载到最新代码 */
const VERSION = 'v11';
const VERSION = 'v13';
function coordinationKey(src) {
const root = utils.findLibraryRoot(src) || path.dirname(path.dirname(src));
@@ -125,7 +125,37 @@ async function renderThumbnailShared(assets, options) {
* - dest : 需要生成的缩略图 PNG 路径
* - item : 当前 item 对象(可更新宽高)
*/
module.exports = async ({ src, dest, item }) => {
module.exports = async (options) => {
const { src, dest, item } = options;
const changed = require('../js/eagle-compound.js').normalizeItem(src, item);
// Eagle originally chooses the cache name before invoking the plugin.
// Supply that requested output as well as the corrected compound-name cache.
const canonical = changed && path.basename(dest).endsWith('_thumbnail.png')
? path.join(path.dirname(dest), item.name + '_thumbnail.png') : dest;
const result = await generate({ ...options, deferRefresh: changed });
if (canonical !== dest && fs.existsSync(dest)) {
if (render.canvasLib) {
const image = await render.canvasLib.loadImage(dest);
const canvas = render.canvasLib.createCanvas(image.width, image.height);
canvas.getContext('2d').drawImage(image, 0, 0);
fs.writeFileSync(canonical, await canvas.encode('webp', 90));
} else fs.copyFileSync(dest, canonical);
}
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);
}
return result;
};
async function generate({ src, dest, item, deferRefresh }) {
try {
// 记录 Eagle 传入的关键信息,便于诊断 src/item 的实际形态
log('HIT ' + VERSION + ' src=' + src + ' (isTexture=' + utils.isTextureFile(src) + ')');
@@ -143,6 +173,20 @@ module.exports = async ({ src, dest, item }) => {
// —— 防御:若 src 是图片(png/jpg/jpeg/webp),不生成占位图,
// 而是把原始图片复制/转换为缩略图,交由系统展示真实内容。
const srcExt = utils.extOf(path.basename(src));
// Eagle dispatches compound suffixes as txt/bytes. Only the complete
// .atlas.txt/.skel.bytes names enter Spine discovery; ordinary files exit.
if (!srcExt) {
if (/\.txt$/i.test(src)) {
// Eagle's native text card requires item.text, even for an empty file.
item.text = '';
const fd = fs.openSync(src, 'r');
try { const buffer = Buffer.alloc(32768); item.text = buffer.subarray(0, fs.readSync(fd, buffer, 0, buffer.length, 0)).toString('utf8'); }
finally { fs.closeSync(fd); }
}
await writeNeutralJsonPlaceholder(dest);
item.width = 400; item.height = 400;
return resolve(item);
}
if (utils.isTextureFile(src) || srcExt === '.png' || srcExt === '.jpg' || srcExt === '.jpeg' || srcExt === '.webp') {
await copyImageAsThumb(src, dest);
try { const s = imageSizeOf(dest); item.width = s.width || item.width; item.height = s.height || item.height; } catch (e) {}
@@ -181,7 +225,7 @@ module.exports = async ({ src, dest, item }) => {
if (!runtimeDir) {
if (await writeAtlasFallback(assets, dest)) {
await refreshRelatedThumbnails(assets, dest, item && item.id);
if (!deferRefresh) await refreshRelatedThumbnails(assets, dest, item && item.id);
item.width = 400; item.height = 400;
return resolve(item);
}
@@ -219,7 +263,7 @@ module.exports = async ({ src, dest, item }) => {
if (!png) {
if (await writeAtlasFallback(assets, dest)) {
log('骨骼无法解析,已使用 atlas 贴图缩略图: ' + src);
await refreshRelatedThumbnails(assets, dest, item && item.id);
if (!deferRefresh) await refreshRelatedThumbnails(assets, dest, item && item.id);
item.width = 400; item.height = 400;
return resolve(item);
}
@@ -227,7 +271,7 @@ module.exports = async ({ src, dest, item }) => {
}
fs.writeFileSync(dest, png);
await refreshRelatedThumbnails(assets, dest, item && item.id);
if (!deferRefresh) await refreshRelatedThumbnails(assets, dest, item && item.id);
try {
const size = imageSizeOf(dest);
@@ -262,7 +306,22 @@ function itemIdFromAssetPath(filePath) {
return match ? match[1] : null;
}
function setEagleCustomThumbnail(itemId, thumbnailPath) {
async function setEagleCustomThumbnail(itemId, thumbnailPath, expectedFilename) {
if (expectedFilename) {
// Eagle's foreground may still hold the old name after a type change.
// Do not let setCustomThumbnail persist that stale name back to metadata.
const info = await new Promise((resolveInfo, rejectInfo) => {
const req = http.get('http://127.0.0.1:41595/api/item/info?id=' + encodeURIComponent(itemId), response => {
let body = ''; response.on('data', chunk => { body += chunk; });
response.on('end', () => {
try { const result = JSON.parse(body); if (result.status !== 'success') throw new Error('Eagle 素材尚未入库'); resolveInfo(result.data); }
catch (error) { rejectInfo(error); }
});
});
req.setTimeout(3000, () => req.destroy(new Error('Eagle 素材信息请求超时'))); req.on('error', rejectInfo);
});
if (!info || info.name + '.' + info.ext !== expectedFilename) throw new Error('Eagle 尚未同步复合后缀名称,请重新打开素材库后刷新缩略图');
}
return new Promise((resolveRequest, rejectRequest) => {
const body = JSON.stringify({ id: itemId, thumbnailPath });
const request = http.request({
@@ -280,6 +339,7 @@ function setEagleCustomThumbnail(itemId, thumbnailPath) {
});
});
request.on('error', rejectRequest);
request.setTimeout(12000, () => request.destroy(new Error('Eagle 缩略图请求超时')));
request.end(body);
});
}
@@ -307,7 +367,10 @@ async function refreshRelatedThumbnails(assets, thumbnailPath, currentItemId) {
let apiPath = thumbnailPath;
try {
apiPath = await createWebpCopy(thumbnailPath, utils.normalizeStem(path.basename(assets.skeletonPath || assets.atlasPath)));
const results = await Promise.allSettled(ids.map((id) => setEagleCustomThumbnail(id, apiPath)));
const results = await Promise.allSettled(ids.map((id) => {
const source = (assets.relatedDataPaths || []).find(file => itemIdFromAssetPath(file) === id);
return setEagleCustomThumbnail(id, apiPath, source && path.basename(source));
}));
const failed = results.filter((result) => result.status === 'rejected');
if (failed.length) throw failed[0].reason;
log('统一补刷完成: ' + ids.length + ' 个条目 [' + ids.join(',') + ']');
@@ -458,6 +521,7 @@ async function writeAtlasFallback(assets, dest) {
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);
if (assets.skeletonPath) require('../js/spine-badge.js').drawVersion(ctx, size, size, utils.detectVersion(assets.skeletonPath));
fs.writeFileSync(dest, canvas.toBuffer('image/png'));
return true;
} catch (e) {