const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const vm = require('node:vm'); const test = require('node:test'); function harness() { const context = { Plugin: { register() {} }, Format: { id: 'geckolib_model' }, Project: {}, Animation: { all: [], selected: null }, Group: { all: [] }, Timeline: { time: 0.7 }, Animator: { resetLastValues() {}, preview() {} }, edits: [], Undo: { initEdit(edit) { context.edits.push(edit); }, finishEdit(label) { context.label = label; } }, Blockbench: { showQuickMessage(message) { context.message = message; } }, console, }; const code = fs.readFileSync(path.join(__dirname, '..', 'AnimationDirector.js'), 'utf8') .replace(' Plugin.register(PLUGIN_ID, {', ' globalThis.api = { previousStagePoseSource, copyPreviousStagePose, copyFirstPoseToEnd, loopPoseUnavailableReason, refreshPreviousStagePoseButton, setButton(button) { previousStagePoseAction = button; } };\n Plugin.register(PLUGIN_ID, {'); vm.runInNewContext(code, context); return context; } function animation(name, length = 2) { return { name, length, animators: {}, getBoneAnimator(group) { return this.animators[group.uuid] ||= animator(); } }; } function animator() { return { position: [], rotation: [], scale: [], addKeyframe(data) { return this.createKeyframe(data, data.time, data.channel); }, createKeyframe(data, time, channel) { const frame = { ...data, time, channel, extend(data) { Object.assign(this, data); } }; this[channel].push(frame); return frame; } }; } test('previous pose button stays visible and explains why it cannot be used', () => { const h = harness(), states = [], label = {}; const button = { nodes: [{ classList: { toggle(name, value) { states.push(value); } }, setAttribute() {}, querySelector() { return label; } }] }; h.api.setButton(button); h.api.refreshPreviousStagePoseButton(); assert.match(label.textContent, /Select an animation stage/); h.Animation.selected = animation('example.p1'); h.Animation.all = [h.Animation.selected]; h.api.refreshPreviousStagePoseButton(); assert.match(label.textContent, /no previous stage/); assert.deepEqual(states, [true, true]); }); test('previous stage uses numeric ordering, same animation ID and saved reused stages', () => { const h = harness(), first = animation('example.p1'), current = animation('example.p3'); h.Animation.selected = current; h.Animation.all = [animation('unrelated.p2'), current, first]; h.Project.animationdirector_animdef_definitions = JSON.stringify({ example: { stages: [{ stage: 1 }, { stage: 2, use_stage: 1 }, { stage: 3 }] } }); assert.equal(h.api.previousStagePoseSource().source, first); assert.equal(h.api.previousStagePoseSource().previous, 2); h.Project.animationdirector_animdef_definitions = JSON.stringify({ example: { stages: [{ stage: 2, use_stage: 9 }] } }); assert.match(h.api.previousStagePoseSource().reason, /missing animation p9/); }); test('copies evaluated endpoint for all actors, resets absent channels and preserves later frames and effects', () => { const h = harness(), source = animation('example.p1'), target = animation('example.p2'); h.Animation.selected = target; h.Animation.all = [target, source]; const groups = [{ uuid: 'a', name: 'actor1_body' }, { uuid: 'b', name: 'actor2_head' }, { uuid: 'guide', name: '__ad_block_requirement_x' }]; h.Group.all = groups; source.animators.a = animator(); source.animators.a.position = [{ time: 1 }]; source.animators.a.interpolate = (channel, allowExpression) => { assert.equal(h.Timeline.time, 2); assert.equal(allowExpression, false); return [4, 5, 6]; }; target.animators.a = animator(); const initial = target.animators.a.createKeyframe({ data_points: [{ x: 99 }] }, 0, 'position'); const later = target.animators.a.createKeyframe({ data_points: [{ x: 77 }] }, 1, 'position'); target.animators.effects = { sound: [{ time: 0, effect: 'test' }] }; h.api.copyPreviousStagePose(); assert.equal(h.Timeline.time, 0.7); assert.equal(initial.data_points[0].x, 4); assert.equal(later.data_points[0].x, 77); assert.equal(target.animators.b.scale[0].data_points[0].x, 1); assert.equal(target.animators.b.rotation[0].data_points[0].x, 0); assert.equal(target.animators.effects.sound[0].effect, 'test'); assert.equal(target.animators.guide, undefined); assert.equal(h.edits.length, 1); }); test('failed endpoint evaluation restores playhead and leaves target unmodified', () => { const h = harness(), source = animation('example.p1'), target = animation('example.p2'); h.Animation.selected = target; h.Animation.all = [source, target]; h.Group.all = [{ uuid: 'a', name: 'actor1_body' }]; source.animators.a = { position: [{}], interpolate() { return [NaN, 0, 0]; } }; assert.throws(() => h.api.copyPreviousStagePose(), /Cannot evaluate/); assert.equal(h.Timeline.time, 0.7); assert.equal(h.edits.length, 0); assert.deepEqual(target.animators, {}); }); test('loop pose copies time zero to exact endpoint without modifying intermediate keys or source pose', () => { const h = harness(), clip = animation('example.p1', 2.1234); h.Animation.selected = clip; h.Animation.all = [clip]; const group = { uuid: 'a', name: 'actor1_body' }; h.Group.all = [group, { uuid: 'empty', name: 'actor2_head' }]; const bone = clip.animators.a = animator(); const first = bone.createKeyframe({ data_points: [{ x: 5, y: 6, z: 7 }] }, 0, 'position'); const middle = bone.createKeyframe({ data_points: [{ x: 99 }] }, 1, 'position'); const last = bone.createKeyframe({ data_points: [{ x: 88 }] }, clip.length, 'position'); bone.interpolate = () => { assert.equal(h.Timeline.time, 0); return [5, 6, 7]; }; h.api.copyFirstPoseToEnd(); assert.equal(first.data_points[0].x, 5); assert.equal(middle.data_points[0].x, 99); assert.equal(last.data_points[0].x, 5); assert.equal(bone.scale.length, 0); assert.equal(bone.rotation.length, 0); assert.equal(clip.animators.empty, undefined); assert.equal(clip.length, 2.1234); assert.equal(h.Timeline.time, 0.7); assert.equal(h.edits.length, 1); }); test('loop pose on an unkeyed clip does not create animators or an undo edit', () => { const h = harness(), clip = animation('example.p1'); h.Animation.selected = clip; h.Animation.all = [clip]; h.Group.all = [{ uuid: 'a', name: 'actor1_body' }]; h.api.copyFirstPoseToEnd(); assert.deepEqual(clip.animators, {}); assert.equal(h.edits.length, 0); }); test('loop pose is unavailable without a clip or with a zero length', () => { const h = harness(); assert.match(h.api.loopPoseUnavailableReason(), /Select an animation/); h.Animation.selected = animation('example.p1', 0); h.api.copyFirstPoseToEnd(); assert.match(h.message, /positive animation clip length/); assert.equal(h.edits.length, 0); });