particle library

This commit is contained in:
2026-09-29 16:36:08 +02:00
parent 2780f8aebb
commit e7bb16df64
4 changed files with 187 additions and 15 deletions
+2 -1
View File
@@ -46,6 +46,7 @@ function harness() {
remove() { Cube.all = Cube.all.filter((cube) => cube !== this); }
}
const context = {
localStorage: { getItem() { return null; }, setItem() {} },
Project: project, Group, Cube, Plugin: { register(id, definition) { context.registration = definition; } },
Action: class { constructor(id, options) { Object.assign(this, options, { id }); } },
Toggle: class { constructor(id, options) { Object.assign(this, options, { id, value: false }); } },
@@ -155,7 +156,7 @@ test('animation menu orders import and creation actions and has no deprecated sp
assert.deepEqual(Array.from(menu.children, (action) => action === '_' ? '_' : action.name), [
'Import Whole Animation', 'Import Actor', 'Import Animation', 'Import Animation Definition', 'Pack Folders',
'_', 'Actor Preferences', 'Replace Actor Model',
'_', 'Block Requirements', 'Item Props', 'NoN Sound Library', 'NoN Particle Library',
'_', 'Block Requirements', 'Item Props', 'NoN Sound Library', 'NoN Particle Library', 'Particle Previews',
'_', 'Animation Definition', 'Export Animations',
]);
const source = fs.readFileSync(path.join(__dirname, '..', 'AnimationDirector.js'), 'utf8');
+27 -3
View File
@@ -13,13 +13,13 @@ function harness() {
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, {');
.replace(' Plugin.register(PLUGIN_ID, {', ' globalThis.api = { previousStagePoseSource, copyPreviousStagePose, copyFirstPoseToEnd, loopPoseUnavailableReason, refreshPreviousStagePoseButton, refreshLoopPoseButton, setButton(button) { previousStagePoseAction = button; }, setLoopButton(button) { loopPoseAction = button; } };\n Plugin.register(PLUGIN_ID, {');
vm.runInNewContext(code, context);
return context;
}
function animation(name, length = 2) {
return { name, length, animators: {}, getBoneAnimator(group) {
return { name, length, loop: 'loop', animators: {}, getBoneAnimator(group) {
return this.animators[group.uuid] ||= animator();
} };
}
@@ -35,7 +35,8 @@ function animator() {
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; } }] };
const node = { style: {}, classList: { toggle(name, value) { states.push(value); } }, setAttribute() {}, querySelector() { return label; } };
const button = { node, nodes: [node] };
h.api.setButton(button);
h.api.refreshPreviousStagePoseButton();
assert.match(label.textContent, /Select an animation stage/);
@@ -43,6 +44,12 @@ test('previous pose button stays visible and explains why it cannot be used', ()
h.api.refreshPreviousStagePoseButton();
assert.match(label.textContent, /no previous stage/);
assert.deepEqual(states, [true, true]);
assert.equal(node.style.filter, 'grayscale(1)');
assert.equal(node.style.opacity, '0.35');
const second = animation('example.p2'); h.Animation.all.push(second); h.Animation.selected = second;
h.api.refreshPreviousStagePoseButton();
assert.equal(node.style.filter, '');
assert.equal(node.style.opacity, '');
});
test('previous stage uses numeric ordering, same animation ID and saved reused stages', () => {
@@ -132,3 +139,20 @@ test('loop pose is unavailable without a clip or with a zero length', () => {
assert.match(h.message, /positive animation clip length/);
assert.equal(h.edits.length, 0);
});
test('loop pose button is visibly disabled for non-looping animations and cannot edit them', () => {
const h = harness(), clip = animation('example.p1'), node = {
style: {}, classList: { toggle() {} }, setAttribute() {}, querySelector() { return null; },
};
h.api.setLoopButton({ node, nodes: [node] });
h.Animation.selected = clip;
for (const loop of ['once', 'hold']) {
clip.loop = loop; h.api.refreshLoopPoseButton();
assert.equal(node.style.filter, 'grayscale(1)');
assert.match(h.api.loopPoseUnavailableReason(), /Set this animation to Loop/);
h.api.copyFirstPoseToEnd(); assert.equal(h.edits.length, 0);
}
clip.loop = 'loop'; h.api.refreshLoopPoseButton();
assert.equal(node.style.filter, '');
assert.equal(h.api.loopPoseUnavailableReason(), null);
});