Files
AI_spine/tools/verify-spine-runtime.mjs
T
2026-09-22 01:02:21 +08:00

91 lines
4.5 KiB
JavaScript

// Optional, offline compatibility check using a caller-provided official Spine 4.2 core IIFE.
// node tools/verify-spine-runtime.mjs --runtime-file /path/spine-core.js --input-dir output/spine
import assert from 'node:assert/strict';
import {readFileSync} from 'node:fs';
import path from 'node:path';
import vm from 'node:vm';
import {readImage} from '../lib/images.mjs';
const options = {};
for (let i = 2; i < process.argv.length; i += 2) {
const flag = process.argv[i];
if (!['--runtime-file', '--input-dir'].includes(flag) || !process.argv[i + 1]) {
throw new Error('Usage: node tools/verify-spine-runtime.mjs --runtime-file spine-core.js --input-dir output/spine');
}
options[flag] = process.argv[i + 1];
}
if (!options['--runtime-file'] || !options['--input-dir']) throw new Error('Both --runtime-file and --input-dir are required');
const context = vm.createContext({console});
vm.runInContext(readFileSync(options['--runtime-file'], 'utf8'), context, {timeout: 10000});
const spine = context.spine;
const input = path.resolve(options['--input-dir']);
const assembly = JSON.parse(readFileSync(path.join(input, 'assembly.json')));
const json = JSON.parse(readFileSync(path.join(input, 'skeleton.json')));
assert.match(json.skeleton.spine, /^4\.2\./u);
const atlas = new spine.TextureAtlas(readFileSync(path.join(input, 'atlas/skeleton.atlas'), 'utf8'));
for (const page of atlas.pages) {
const png = readImage(path.join(input, 'atlas', page.name), {pngOnly: true});
assert.equal(page.width, png.width);
assert.equal(page.height, png.height);
// CPU geometry needs region metadata, not a WebGL texture upload.
page.setTexture({setFilters() {}, setWraps() {}, getImage() { return {width: png.width, height: png.height}; }});
}
const data = new spine.SkeletonJson(new spine.AtlasAttachmentLoader(atlas)).readSkeletonData(json);
const skeleton = new spine.Skeleton(data);
function vertices(slot) {
const values = new Float32Array(8);
slot.getAttachment().computeWorldVertices(slot, values, 0, 2);
assert.ok([...values].every(Number.isFinite));
return values;
}
function close(actual, expected, label) {
assert.ok(Math.abs(actual - expected) < .002, `${label}: ${actual} != ${expected}`);
}
skeleton.setToSetupPose();
skeleton.updateWorldTransform(spine.Physics.none);
const originals = new Map();
const assets = new Map(assembly.parts.map((p) => [p.id, p]));
for (const slot of skeleton.slots) {
const values = vertices(slot);
originals.set(slot.data.name, values);
const [x0, y0, x1, y1] = assets.get(slot.data.name).crop_xyxy;
const xs = [values[0], values[2], values[4], values[6]];
const ys = [values[1], values[3], values[5], values[7]];
close(Math.min(...xs), x0 - assembly.width / 2, 'setup x0');
close(Math.max(...xs), x1 - assembly.width / 2, 'setup x1');
close(Math.min(...ys), assembly.height - y1, 'setup y0');
close(Math.max(...ys), assembly.height - y0, 'setup y1');
const region = atlas.findRegion(slot.data.name);
assert.equal(region.originalWidth, x1 - x0);
assert.equal(region.originalHeight, y1 - y0);
}
let samples = 0;
for (const animation of data.animations) {
if (animation.name === 'setup') continue;
const boneName = animation.name.replace(/^check__/u, '');
const frames = json.animations[animation.name].bones[boneName].rotate;
for (const time of [.5, 1.5]) {
skeleton.setToSetupPose();
skeleton.updateWorldTransform(spine.Physics.none);
const bone = skeleton.findBone(boneName);
const px = bone.worldX, py = bone.worldY;
const angle = frames.find((f) => f.time === time).value * Math.PI / 180;
animation.apply(skeleton, 0, time, false, [], 1, spine.MixBlend.replace, spine.MixDirection.mixIn);
skeleton.updateWorldTransform(spine.Physics.none);
for (const slot of skeleton.slots) {
let parent = slot.bone;
while (parent && parent !== bone) parent = parent.parent;
const before = originals.get(slot.data.name), after = vertices(slot);
for (let i = 0; i < 8; i += 2) {
const x = before[i] - px, y = before[i + 1] - py;
close(after[i], parent ? px + x * Math.cos(angle) - y * Math.sin(angle) : before[i], 'rotated x');
close(after[i + 1], parent ? py + x * Math.sin(angle) + y * Math.cos(angle) : before[i + 1], 'rotated y');
}
}
samples++;
}
}
console.log(JSON.stringify({input, spine_version: data.version, bones: data.bones.length,
slots: data.slots.length, animations: data.animations.length, rotation_samples: samples,
setup_geometry_verified: true, descendant_rotation_verified: true, browser_render_verified: false}, null, 2));