json放行
This commit is contained in:
1 parent
99d85a9e6d
commit
ba0f48c543
6 files changed
+52
-15
No files matched your search
@@ -68,7 +68,7 @@ SpineViewer/
|
||||
- 将当前帧设为自定义缩略图,并通知 Eagle 立即刷新列表缓存
|
||||
4. **缩略图**:使用 `@napi-rs/canvas` 在 Node 中跨 item 文件夹读取资源并渲染骨骼首帧。自动生成和预览器中手动点击“生成缩略图”都会在右下角添加 Spine 官方 badge,左下角添加完整导出版本号(例如 `3.4.02`),便于识别数据文件。无法识别版本时不显示版本文字。导入时会等待同批文件写入稳定,再按入库批次配对,避免复用旧库里的同名资源。处理器写入 PNG 工作图,最终缩略图缓存格式由 Eagle 决定,可能是体积更小的 WebP。
|
||||
- 同一前缀的 `atlas/json/skel` 共用资源发现和渲染任务,完成后主动补刷这一组的全部数据条目。
|
||||
- JSON 只在存在 Spine 官方导出的 `skeleton.spine` 版本字段时才按骨骼数据处理;普通 JSON、JSONC 和仅含 `bones` / `slots` 同名字段的数据会立即生成中性文件缩略图,不进入全库资源扫描或补刷队列。
|
||||
- JSON 只在 `skeleton.spine` 的值是有效版本号时才按 Spine 骨骼数据处理;普通 JSON、JSONC、空版本字段和仅含 `bones` / `slots` 同名字段的数据会立即放行,不写缩略图,不进入全库资源扫描或补刷队列,以保留 Eagle 或其他 JSON 插件的处理结果。Eagle 查看器不支持内容级转交,所以打开这类文件时会隐藏全部 Spine 控件,仅显示格式化的 JSON 文本。
|
||||
- 暂时缺少骨骼、atlas 或贴图时,前台最多短暂等待约半秒便显示回退图;后台采用逐步降频的方式观察约两分钟。链路补齐且文件稳定后自动生成 WebP 并通知 Eagle 刷新;更晚补拖的文件会通过新的导入回调重新触发识别。
|
||||
- 分批拖入时优先在同一个 Eagle 文件夹内补齐;大型文件夹导入会共享短时文件索引,避免每个条目重复扫描整个库。
|
||||
- Eagle 在缩略图回调结束后会关闭执行窗口,因此补刷使用独立 Node 工作进程和临时文件队列,不依赖窗口内的定时器。每个素材库同时只有一个工作进程;确认资源稳定、素材已经入库后调用 Eagle API 补刷,失败会重试约两分钟,队列完成后退出。不会后台常驻。
|
||||
|
||||
+2
-2
@@ -138,9 +138,9 @@ function classifySkeletonJson(filePath) {
|
||||
try { fs.readSync(fd, buffer, 0, length, 0); } finally { fs.closeSync(fd); }
|
||||
const text = buffer.toString('utf8');
|
||||
const hasSkeletonKey = /"skeleton"\s*:/i.test(text);
|
||||
const hasSkeletonVersion = /"skeleton"\s*:\s*\{[\s\S]{0,8192}?"spine"\s*:/i.test(text);
|
||||
const versionMatch = text.match(/"skeleton"\s*:\s*\{[\s\S]{0,8192}?"spine"\s*:\s*"(\d+\.\d+(?:\.\d+)?(?:[^"\\]|\\.)*)"/i);
|
||||
let kind;
|
||||
if (hasSkeletonVersion) {
|
||||
if (versionMatch) {
|
||||
kind = 'spine';
|
||||
} else if (stat.size > JSON_PROBE_BYTES) {
|
||||
kind = 'other';
|
||||
|
||||
@@ -35,5 +35,15 @@ 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);
|
||||
for (const [name, json, expected] of [
|
||||
['spine.json', { skeleton: { spine: '4.2.43' }, bones: [] }, 'spine'],
|
||||
['empty-version.json', { skeleton: { spine: '' }, bones: [] }, 'other'],
|
||||
['null-version.json', { skeleton: { spine: null }, bones: [] }, 'other'],
|
||||
['fake-version.json', { skeleton: { spine: 'latest' }, bones: [] }, 'other'],
|
||||
['ordinary.json', { bones: [], slots: [] }, 'other'],
|
||||
]) {
|
||||
const file = path.join(root, name); fs.writeFileSync(file, JSON.stringify(json));
|
||||
assert.equal(utils.classifySkeletonJson(file), expected, name);
|
||||
}
|
||||
console.log('PASS: Eagle routing, case handling, ordinary-file exclusion, cache isolation and late-arriving skeleton');
|
||||
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
||||
@@ -24,6 +24,13 @@ const fixtures = path.resolve(process.argv[2] || path.join(__dirname, '.fixtures
|
||||
const result = await generate({ src, dest: path.join(root, 'notes_thumbnail.png'), item: { name: 'notes', ext: 'txt' } });
|
||||
assert.equal(result.text, text); assert.equal(result.ext, 'txt'); assert.equal(result.name, 'notes');
|
||||
}
|
||||
const ordinaryJson = path.join(root, 'config.json');
|
||||
const ordinaryJsonDest = path.join(root, 'config_thumbnail.png');
|
||||
fs.writeFileSync(ordinaryJson, JSON.stringify({ skeleton: { spine: null }, enabled: true }));
|
||||
const ordinaryJsonItem = { name: 'config', ext: 'json' };
|
||||
assert.equal(await generate({ src: ordinaryJson, dest: ordinaryJsonDest, item: ordinaryJsonItem }), ordinaryJsonItem);
|
||||
assert.equal(fs.existsSync(ordinaryJsonDest), false, 'non-Spine JSON must leave thumbnail destination untouched');
|
||||
assert.equal(ordinaryJsonItem.width, undefined); assert.equal(ordinaryJsonItem.height, undefined);
|
||||
for (const [source, target] of [['spineboy-mesh.skel', 'spineboy.skel.bytes'], ['spineboy.atlas', 'spineboy.atlas.txt'], ['spineboy.png', 'spineboy.png']])
|
||||
fs.copyFileSync(path.join(fixtures, source), path.join(root, target));
|
||||
for (const [name, ext] of [['spineboy.atlas', 'txt'], ['spineboy.skel', 'bytes']]) {
|
||||
|
||||
+3
-4
@@ -188,13 +188,12 @@ async function generate({ src, dest, item, deferRefresh }) {
|
||||
}
|
||||
|
||||
// manifest 只能按扩展名接管 JSON,不能按文件内容过滤。因此先做低成本内容判定:
|
||||
// 普通 JSON 立即生成中性缩略图,不扫描资源库,也不启动后台补刷。
|
||||
// 普通 JSON 不写 dest,不修改尺寸,也不启动资源扫描和后台补刷。
|
||||
// 这样可以保留 Eagle 或其他 JSON 插件生成的缩略图,避免本插件的回退图覆盖它。
|
||||
if (srcExt === '.json') {
|
||||
const jsonKind = await classifyJsonBriefly(src);
|
||||
if (jsonKind === 'other') {
|
||||
log('普通 JSON,跳过 Spine 资源发现: ' + src);
|
||||
await writeNeutralJsonPlaceholder(dest);
|
||||
item.width = 400; item.height = 400;
|
||||
log('普通 JSON,放行给 Eagle/其他 JSON 插件: ' + src);
|
||||
return resolve(item);
|
||||
}
|
||||
}
|
||||
|
||||
+29
-8
@@ -175,6 +175,13 @@
|
||||
#overlay .box { max-width: 520px; padding: 20px; }
|
||||
#overlay .title { font-size: 16px; margin-bottom: 8px; color: var(--error); }
|
||||
#overlay .msg { color: var(--text-dim); white-space: pre-wrap; word-break: break-all; }
|
||||
#overlay.text-preview { align-items: stretch; justify-content: stretch; text-align: left; }
|
||||
#overlay.text-preview .box { width: 100%; max-width: none; padding: 24px; overflow: auto; }
|
||||
#overlay.text-preview .title { color: var(--text); font-size: 13px; font-weight: 600; }
|
||||
#overlay.text-preview .msg {
|
||||
font: 12px/1.6 "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
||||
color: var(--text-dim); white-space: pre; word-break: normal;
|
||||
}
|
||||
#overlay.hidden { display: none; }
|
||||
#loading { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 4; background: var(--bg); }
|
||||
#loading.hidden { display: none; }
|
||||
@@ -350,6 +357,16 @@
|
||||
document.getElementById('errorMsg').textContent = msg;
|
||||
}
|
||||
|
||||
function showTextPreview(title, text) {
|
||||
running = false;
|
||||
toolbarEl.style.display = 'none';
|
||||
timelineBarEl.style.display = 'none';
|
||||
animationPanel.style.display = 'none';
|
||||
statusEl.style.display = 'none';
|
||||
overlay.classList.add('text-preview');
|
||||
showError(title, text);
|
||||
}
|
||||
|
||||
function setStatus(msg, holdMs = 0) {
|
||||
statusEl.textContent = msg;
|
||||
statusHoldUntil = holdMs === Infinity ? Infinity : (holdMs > 0 ? performance.now() + holdMs : 0);
|
||||
@@ -936,9 +953,6 @@
|
||||
if (!utils.extOf(path.basename(filePath))) {
|
||||
// Registering txt/bytes is required by Eagle's extension dispatch.
|
||||
// Ordinary text remains readable and never starts Spine discovery.
|
||||
document.getElementById('toolbar').style.display = 'none';
|
||||
document.getElementById('timelineBar').style.display = 'none';
|
||||
document.getElementById('animationPanel').style.display = 'none';
|
||||
let message = '此文件不是 .atlas.txt 或 .skel.bytes 格式的 Spine 资源。';
|
||||
if (/\.txt$/i.test(filePath)) {
|
||||
const fd = fs.openSync(filePath, 'r');
|
||||
@@ -949,16 +963,23 @@
|
||||
if (fs.fstatSync(fd).size > count) message += '\n\n(仅显示前 256 KB)';
|
||||
} finally { fs.closeSync(fd); }
|
||||
}
|
||||
showError(path.basename(filePath), message);
|
||||
const box = overlay.querySelector('.box');
|
||||
box.style.cssText = 'max-width:none;width:100%;height:100%;overflow:auto;text-align:left';
|
||||
document.getElementById('errorTitle').style.color = 'var(--text)';
|
||||
showTextPreview(path.basename(filePath), message);
|
||||
return;
|
||||
}
|
||||
// Eagle 只能按 .json 扩展名把文件交给格式扩展。查看器必须再次检查内容,
|
||||
// 防止普通 JSON 被拿去匹配库中其他 Spine 骨骼资源。
|
||||
if (utils.extOf(path.basename(filePath)) === '.json' && utils.classifySkeletonJson(filePath) !== 'spine') {
|
||||
showError('普通 JSON 文件', '该文件不包含 Spine 导出的 skeleton.spine 版本字段,因此不会作为 Spine 骨骼数据加载。');
|
||||
const maxPreviewBytes = 1024 * 1024;
|
||||
const stat = fs.statSync(filePath);
|
||||
const fd = fs.openSync(filePath, 'r');
|
||||
const buffer = Buffer.alloc(Math.min(stat.size, maxPreviewBytes));
|
||||
let count = 0;
|
||||
try { count = fs.readSync(fd, buffer, 0, buffer.length, 0); } finally { fs.closeSync(fd); }
|
||||
let text = buffer.subarray(0, count).toString('utf8');
|
||||
if (stat.size <= maxPreviewBytes) {
|
||||
try { text = JSON.stringify(JSON.parse(text), null, 2); } catch (e) { /* 保留 JSONC/原始文本 */ }
|
||||
} else text += '\n\n(文件较大,仅显示前 1 MB)';
|
||||
showTextPreview(path.basename(filePath), text);
|
||||
return;
|
||||
}
|
||||
const assets = utils.discoverAssets(filePath);
|
||||
|
||||
Reference in new issue
Block a user