const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); 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 {}, 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, jemTransferTransform, createJemTransferProject };\n Plugin.register(PLUGIN_ID, {'); vm.runInNewContext(source, h); return h; } test('JEM preset parent paths include attachment roots and nested groups', () => { const h = harness(); const parts = h.api.jemPresetParts({ models: [{ part: 'body', submodels: [{ id: 'fur', submodels: [{ id: 'tip' }] }] }, { part: 'head' }] }); 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); const head = new h.Group('actor1_head', root), extra = new h.Group('actor1_extra', root); const suggestions = h.api.jemTransferSuggestions([root, body, d, tip, head, extra], [{ name: 'body', path: '0' }, { name: 'head', path: '1' }], 1); assert.deepEqual(Array.from(suggestions, (item) => [item.name, item.target]), [['d', '0'], ['extra', '']]); assert.equal(suggestions[0].root, d); assert.equal(tip.parent, d); assert.equal(body.name, 'actor1_body'); }); test('selected additions require parent mapping; unchecked additions are omitted', () => { const h = harness(), body = new h.Group('actor1_body'), d = new h.Group('actor1_d', body); const suggestions = h.api.jemTransferSuggestions([body, d], [{ name: 'body', path: '0' }], 1); assert.throws(() => h.api.validateJemChoices(suggestions, { include_0: true, parent_0: '' }), /Choose a target parent/); assert.throws(() => h.api.validateJemChoices(suggestions, { include_0: false }), /at least one/); const chosen = h.api.validateJemChoices(suggestions, { include_0: true, parent_0: '0' }); assert.equal(chosen[0].tree.name, 'd'); assert.equal(chosen[0].target, '0'); assert.deepEqual(Array.from(chosen[0].parentOrigin), [1, 2, 3]); }); test('CEM catalog integration handles its initial undefined return and dependency errors', async () => { const h = harness(); await assert.rejects(h.api.cemTemplateCatalog(), /Install and enable/); let calls = 0; const catalog = { models: { wolf: {} }, entities: [{ id: 'wolf' }] }; h.window = { async loadCEMTemplateModels() { return ++calls === 1 ? undefined : catalog; } }; assert.equal(await h.api.cemTemplateCatalog(), catalog); assert.equal(calls, 2); }); test('energy visibility is opt-in per transferred bone including nested subbones', () => { const h = harness(), body = new h.Group('actor1_body'), d = new h.Group('actor1_d', body); new h.Group('actor1_tip', d); const suggestions = h.api.jemTransferSuggestions([body, d, ...d.children], [{ name: 'body', path: '0' }], 1); assert.deepEqual(Array.from(h.api.jemAdditionBones(suggestions[0]), (bone) => [bone.name, bone.path]), [['d', 'root'], ['tip', 'root_0']]); const selected = h.api.validateJemChoices(suggestions, { include_0: true, parent_0: '0', energy_0_root_0: true }); assert.equal(selected[0].tree.energyVisibility, false); assert.equal(selected[0].tree.children[0].group.energyVisibility, true); }); test('energy expressions use the final unique bone name and preserve existing preset animations', () => { const h = harness(), root = new h.Group('body'), nested = new h.Group('addition', root); const tip = new h.Group('d3_2', nested); root.cem_animations = [{ 'body.rx': 'existing_expression' }]; h.api.addJemEnergyVisibility(tip); assert.equal(root.cem_animations.length, 2); assert.equal(root.cem_animations[0]['body.rx'], 'existing_expression'); assert.equal(root.cem_animations[1]['d3_2.visible'], 'nbt(NeedsOfNatureEnergy,range:70-200)'); assert.equal(tip.cem_animations, undefined); }); test('parent energy visibility disables descendants and omits redundant child expressions without clearing their choices', () => { const h = harness(), body = new h.Group('actor1_body'), d = new h.Group('actor1_d', body); const tip = new h.Group('actor1_tip', d), end = new h.Group('actor1_end', tip); const suggestions = h.api.jemTransferSuggestions([body, d, tip, end], [{ name: 'body', path: '0' }], 1); const fields = Object.fromEntries(['root', 'root_0', 'root_0_0'].map((path) => { const input = {}, bar = { style: {}, querySelectorAll() { return [input]; } }; return [`energy_0_${path}`, { bar, input }]; })); const dialog = { form: { form_data: fields } }; const result = { include_0: true, parent_0: '0', energy_0_root: true, energy_0_root_0: true, energy_0_root_0_0: true }; h.api.refreshJemEnergyFields(dialog, suggestions, result); assert.equal(fields.energy_0_root.input.disabled, false); assert.equal(fields.energy_0_root_0.input.disabled, true); assert.equal(fields.energy_0_root_0_0.input.disabled, true); const tree = h.api.validateJemChoices(suggestions, result)[0].tree; assert.equal(tree.energyVisibility, true); assert.equal(tree.children[0].group.energyVisibility, false); result.energy_0_root = false; h.api.refreshJemEnergyFields(dialog, suggestions, result); assert.equal(fields.energy_0_root_0.input.disabled, false); assert.equal(fields.energy_0_root_0_0.input.disabled, true); assert.equal(h.api.validateJemChoices(suggestions, result)[0].tree.children[0].group.energyVisibility, true); });