91 lines
4.5 KiB
JavaScript
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));
|