This commit is contained in:
2026-09-29 14:54:06 +02:00
parent f7c0c38ec2
commit 5cb0ba631e
6 changed files with 462 additions and 166 deletions
+69 -2
View File
@@ -5,12 +5,36 @@ const path = require('node:path');
const vm = require('node:vm');
function harness() {
// These tests exercise Z-axis rotations; Blockbench supplies full Three.js at runtime.
class Quaternion {
constructor() { this.angle = 0; }
setFromEuler(euler) { this.angle = euler.z; return this; }
multiply(other) { this.angle += other.angle; return this; }
invert() { this.angle = -this.angle; return this; }
toArray() { return [0, 0, Math.sin(this.angle / 2), Math.cos(this.angle / 2)]; }
fromArray(values) { this.angle = 2 * Math.atan2(values[2], values[3]); return this; }
}
class Euler {
constructor(x = 0, y = 0, z = 0) { Object.assign(this, { x, y, z }); }
setFromQuaternion(q) { this.x = this.y = 0; this.z = q.angle; return this; }
}
class Vector3 {
constructor(...values) { this.values = values; }
sub(other) { this.values = this.values.map((v, i) => v - other.values[i]); return this; }
add(other) { this.values = this.values.map((v, i) => v + other.values[i]); return this; }
applyQuaternion(q) {
const [x, y, z] = this.values, c = Math.cos(q.angle), s = Math.sin(q.angle);
this.values = [x * c - y * s, x * s + y * c, z]; return this;
}
toArray() { return this.values; }
}
class Group {
constructor(name, parent) { Object.assign(this, { name, origin: [1, 2, 3], rotation: [0, 0, 0], children: [], parent }); parent?.children.push(this); }
}
const h = { Group, Cube: class {}, Plugin: { register() {} } };
const h = { Group, Cube: class {}, THREE: { Quaternion, Euler, Vector3 },
Format: { id: 'geckolib_model', euler_order: 'ZYX' }, Plugin: { register() {} } };
const source = fs.readFileSync(path.join(__dirname, '..', 'AnimationDirector.js'), 'utf8').replace(' Plugin.register(PLUGIN_ID, {',
' globalThis.api = { cemTemplateCatalog, jemPresetParts, jemTransferSuggestions, validateJemChoices, jemAdditionBones, addJemEnergyVisibility, refreshJemEnergyFields };\n Plugin.register(PLUGIN_ID, {');
' globalThis.api = { cemTemplateCatalog, jemPresetParts, jemTransferSuggestions, validateJemChoices, jemAdditionBones, addJemEnergyVisibility, refreshJemEnergyFields, jemTransferTransform, createJemTransferProject };\n Plugin.register(PLUGIN_ID, {');
vm.runInNewContext(source, h);
return h;
}
@@ -21,6 +45,49 @@ test('JEM preset parent paths include attachment roots and nested groups', () =>
assert.deepEqual(Array.from(parts, (part) => [part.name, part.path]), [['body', '0'], ['fur', '0/0'], ['tip', '0/0/0'], ['head', '1']]);
});
test('JEM transfer compensates different parent rotations without rotating children twice', () => {
const h = harness(), body = new h.Group('actor1_body'), extra = new h.Group('actor1_extra', body);
body.rotation = [0, 0, 90]; extra.origin = [2, 2, 3]; extra.rotation = [0, 0, 10];
const suggestions = h.api.jemTransferSuggestions([body, extra], [{ name: 'body', path: '0' }], 1);
const addition = h.api.validateJemChoices(suggestions, { include_0: true, parent_0: '0' })[0];
const target = new h.Group('body'); target.origin = [10, 20, 30];
const transform = h.api.jemTransferTransform(addition, target);
transform.offset.forEach((value, axis) => assert.ok(Math.abs(value - [8, 19, 27][axis]) < 1e-8));
assert.ok(Math.abs(transform.rotation[2] - 100) < 1e-8);
assert.deepEqual(Array.from(addition.tree.rotation), [0, 0, 10]);
});
test('JEM creation rescales GeckoLib UVs but preserves per-texture UVs in other formats', () => {
for (const projectWide of [false, true]) {
const h = harness(), cubes = [];
h.Group.all = [];
h.Group.prototype.init = function() { h.Group.all.push(this); return this; };
h.Group.prototype.addTo = function(parent) { this.parent = parent; parent.children.push(this); return this; };
h.Cube = class {
constructor(data) { Object.assign(this, data); cubes.push(this); }
addTo(parent) { parent.children.push(this); return this; }
init() { return this; }
};
h.Texture = class {
constructor(data) { Object.assign(this, data, { uuid: 'texture' }); }
fromDataURL() { return this; }
add() { return this; }
};
h.Project = {};
h.Formats = { optifine_entity: { codec: { parse() { new h.Group('body').init(); } } } };
h.newProject = () => true;
h.Canvas = { updateAll() {} }; h.Blockbench = { showQuickMessage() {} };
const addition = { target: '0', parentOrigin: [0, 0, 0], parentRotation: [0, 0, 0, 1], rotationOrder: 'ZYX',
tree: { name: 'extra', origin: [0, 0, 0], rotation: [0, 0, 0], children: [{ cube: {
name: 'cube', from: [0, 0, 0], to: [1, 1, 1], box_uv: false,
faces: { north: { texture: 'old', uv: [0, 0, 16, 16] } } } }] } };
h.api.createJemTransferProject({ name: 'test', model: { models: [] } }, [addition],
[{ id: 'old', data: { uv_width: 32, uv_height: 32 }, image: 'mock' }], 'old',
{ width: 64, height: 64, projectWide }, false);
assert.deepEqual(Array.from(cubes[0].faces.north.uv), projectWide ? [0, 0, 8, 8] : [0, 0, 16, 16]);
}
});
test('suggests added subtrees once, strips actor prefixes and leaves vanilla hierarchy unchanged', () => {
const h = harness(), root = new h.Group('actor1_root'), body = new h.Group('actor1_body', root);
const d = new h.Group('actor1_d', body), tip = new h.Group('actor1_tip', d);