新增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

+40 -15
View File
@@ -24,22 +24,38 @@ function runtime(version) {
vm.runInContext(fs.readFileSync(path.join(__dirname, '..', 'lib', version, 'spine-canvas.js'), 'utf8'), context);
return install(context.spine, version);
}
for (const version of ['3.6', '3.7']) {
const versions = process.argv[3] === '3.4' ? ['3.4'] : ['3.6', '3.7'];
let pairs = 0;
for (const version of versions) {
const spine = runtime(version);
// 3.4 JSON export truncates to two decimal places instead of rounding.
const quantization = version === '3.4' ? 0.0102 : 0.0052;
assert.equal(utils.chooseRuntimeDir(version + '.32', true), version);
for (const name of names) {
const directory = path.join(fixtures, version, name);
const bytes = fs.readFileSync(path.join(directory, name + '-pro.skel'));
const json = JSON.parse(fs.readFileSync(path.join(directory, name + '-pro.json'), 'utf8'));
for (const name of version === '3.4' ? ['spineboy', 'spineboy-mesh', 'spineboy-hover', 'goblins', 'goblins-mesh', 'vine', 'stretchyman', 'tank', 'raptor'] : names) {
const directory = path.join(fixtures, version, version === '3.4' ? name.split('-')[0] : name);
const file = name + (version === '3.4' ? '' : '-pro');
const bytes = fs.readFileSync(path.join(directory, file + '.skel'));
const json = JSON.parse(fs.readFileSync(path.join(directory, file + '.json'), 'utf8'));
const decoded = decode(bytes, version);
assert.equal(decoded.skeleton.hash, json.skeleton.hash);
assert.equal(decoded.skeleton.spine, json.skeleton.spine);
assert.equal(utils.detectVersion(path.join(directory, name + '-pro.skel')), json.skeleton.spine);
assert.equal(utils.detectVersion(path.join(directory, file + '.skel')), json.skeleton.spine);
// Region geometry uses a real official atlas; no native Canvas dependency
// is needed to verify animation poses and renderer triangle output.
const texture = { getImage: () => ({ width: 1024, height: 1024 }), setFilters() {}, setWraps() {} };
const atlas = new spine.TextureAtlas(fs.readFileSync(path.join(directory, name + '.atlas'), 'utf8'), () => texture);
const atlas = new spine.TextureAtlas(fs.readFileSync(path.join(directory, (version === '3.4' ? name.split('-')[0] : name) + '.atlas'), 'utf8'), () => texture);
const attachmentLoader = new spine.AtlasAttachmentLoader(atlas);
// 3.4 Goblins binary optimizes three constant segments to linear;
// JSON keeps stepped. Their equal endpoints produce identical poses.
if (version === '3.4' && name.startsWith('goblins')) {
for (const [bone, type, index] of [['left lower leg', 'translate', 2], ['right lower leg', 'translate', 0], ['hip', 'rotate', 0]]) {
const timeline = json.animations.walk.bones[bone][type];
assert.equal(timeline[index].curve, 'stepped');
assert.equal(decoded.animations.walk.bones[bone][type][index].curve, undefined);
for (const field of type === 'rotate' ? ['angle'] : ['x', 'y']) assert.equal(timeline[index][field], timeline[index + 1][field]);
delete timeline[index].curve;
}
}
// In the pinned 3.6 raptor exports, the same-hash JSON puts the stepped
// IK curve on frame 0, while the SKEL stores it on frame 1. Assert the
// exact discrepancy, then align only this JSON comparison oracle.
@@ -60,7 +76,7 @@ for (const version of ['3.6', '3.7']) {
assert.deepEqual(Array.from(data.animations, a => a.name), Array.from(expected.animations, a => a.name));
for (let i = 0; i < data.bones.length; i++) {
for (const k of ['x', 'y', 'rotation', 'scaleX', 'scaleY', 'shearX', 'shearY', 'length'])
near(data.bones[i][k], expected.bones[i][k], 0.0052, name + '/bone/' + k);
near(data.bones[i][k], expected.bones[i][k], quantization, name + '/bone/' + k);
}
for (const skin of data.skins) {
const reference = expected.findSkin(skin.name);
@@ -69,20 +85,24 @@ for (const version of ['3.6', '3.7']) {
const b = reference.getAttachment(Number(slot), key);
assert(b, key); assert.equal(a.constructor.name, b.constructor.name);
for (const field of ['vertices', 'regionUVs', 'triangles', 'bones', 'lengths']) {
if (a[field]) arrayNear(a[field], b[field], field === 'triangles' || field === 'bones' ? 0 : 0.0052, name + '/' + key + '/' + field);
if (a[field]) arrayNear(a[field], b[field], field === 'triangles' || field === 'bones' ? 0 : quantization, name + '/' + key + '/' + field);
}
}
}
}
for (const animation of data.animations) {
const refAnimation = expected.findAnimation(animation.name);
near(animation.duration, refAnimation.duration, 0.00006, 'duration');
near(animation.duration, refAnimation.duration, version === '3.4' ? 0.00011 : 0.00006, 'duration');
assert.equal(animation.timelines.length, refAnimation.timelines.length);
// Older JSON and binary exports order independent slot timelines differently.
const timelineKey = t => [t.constructor.name, t.slotIndex, t.boneIndex, t.ikConstraintIndex, t.transformConstraintIndex, t.pathConstraintIndex, t.attachment && t.attachment.name].join('/');
const references = new Map(refAnimation.timelines.map(t => [timelineKey(t), t]));
for (let i = 0; i < animation.timelines.length; i++) {
const a = animation.timelines[i], b = refAnimation.timelines[i];
const a = animation.timelines[i], b = references.get(timelineKey(a));
assert(b, timelineKey(a));
assert.equal(a.constructor.name, b.constructor.name);
arrayNear(a.frames, b.frames, 0.0052, name + '/' + animation.name + '/frames');
if (a.curves) arrayNear(a.curves, b.curves, 0.0052, name + '/' + animation.name + '/curves');
arrayNear(a.frames, b.frames, quantization, name + '/' + animation.name + '/frames');
if (a.curves) arrayNear(a.curves, b.curves, quantization, name + '/' + animation.name + '/curves');
if (a.attachmentNames) assert.deepEqual(Array.from(a.attachmentNames), Array.from(b.attachmentNames));
if (a.frameVertices) for (let f = 0; f < a.frameVertices.length; f++) arrayNear(a.frameVertices[f], b.frameVertices[f], 0.0104, 'deform');
}
@@ -100,7 +120,11 @@ for (const version of ['3.6', '3.7']) {
assert.deepEqual(Array.from(pair[0].slots, s => s.getAttachment()?.name), Array.from(pair[1].slots, s => s.getAttachment()?.name));
for (let i = 0; i < pair[0].bones.length; i++) for (const field of ['a', 'b', 'c', 'd', 'worldX', 'worldY']) {
// Quantized JSON weights/curves compound along bone chains.
near(pair[0].bones[i][field], pair[1].bones[i][field], 2.1, name + '/' + animation.name + '/' + field);
// 3.4 truncates weights/scales by up to 0.01. Bound accumulated
// world-position error by 1% of export size; local data is checked above.
const tolerance = version === '3.4' && field.startsWith('world')
? Math.max(2.1, Math.max(json.skeleton.width, json.skeleton.height) * scale * 0.01) : 2.1;
near(pair[0].bones[i][field], pair[1].bones[i][field], tolerance, name + '/' + animation.name + '/' + field);
}
const renderer = clipping.createRenderer(spine, { globalAlpha: 1 }, version);
let triangles = 0;
@@ -116,6 +140,7 @@ for (const version of ['3.6', '3.7']) {
const wrapped = Buffer.concat([Buffer.from([9, 8, 7]), bytes, Buffer.from([6])]);
assert.equal(decode(wrapped.subarray(3, -1), version).skeleton.hash, json.skeleton.hash);
console.log('PASS ' + version + ' / ' + name);
pairs++;
}
}
for (const version of ['3.8', '4.0', '4.1', '4.2', '4.3']) assert.equal(utils.chooseRuntimeDir(version + '.1', true), version);
@@ -174,4 +199,4 @@ for (const version of ['3.6', '3.7']) for (const nonessential of [false, true])
assert.equal(data.bones[0].name, '根😀'); assert.equal(data.events[0].intValue, -123);
assert.equal(data.animations[0].duration, 0.5);
}
console.log(`PASS: 14 official JSON/SKEL pairs, ${animations} animation/scale checks, ${poses} pose/render checks`);
console.log(`PASS: ${pairs} official JSON/SKEL pairs, ${animations} animation/scale checks, ${poses} pose/render checks`);