added multi gender export with exclusion for optifine models
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
const vm = require('node:vm');
|
||||
|
||||
function variantBuilder() {
|
||||
const source = fs.readFileSync(path.join(__dirname, '..', 'AnimationDirector.js'), 'utf8').replace(
|
||||
' Plugin.register(PLUGIN_ID, {',
|
||||
' globalThis.variantTest = { buildGenderModelVariant, refreshModelVariantFields, createModelVariantBoneSections, buildJemModelVariant };\n Plugin.register(PLUGIN_ID, {'
|
||||
);
|
||||
const context = { Plugin: { register() {} }, console };
|
||||
vm.runInNewContext(source, context);
|
||||
return Object.assign(context.variantTest.buildGenderModelVariant, { refresh: context.variantTest.refreshModelVariantFields,
|
||||
sections: context.variantTest.createModelVariantBoneSections, jem: context.variantTest.buildJemModelVariant });
|
||||
}
|
||||
|
||||
test('gender variants omit selected bones, descendants, locators and texture overrides without changing source', () => {
|
||||
const build = variantBuilder();
|
||||
const source = { format_version: '1.12.0', 'minecraft:geometry': [{
|
||||
description: { identifier: 'geometry.example', texture_width: 64 },
|
||||
bones: [
|
||||
{ name: 'detail', parent: 'd', cubes: [{ origin: [0, 0, 0] }] },
|
||||
{ name: 'body', cubes: [{}] },
|
||||
{ name: 'd', parent: 'body', cubes: [{}], locators: { D: [0, 1, 0] } },
|
||||
{ name: 'head', parent: 'body', cubes: [{}] },
|
||||
],
|
||||
}], afw_bone_textures: { d: 'example:textures/custom.png' } };
|
||||
const before = JSON.stringify(source);
|
||||
const female = JSON.parse(JSON.stringify(build(source, ['d'])));
|
||||
assert.deepEqual(female['minecraft:geometry'][0].bones.map((bone) => bone.name), ['body', 'head']);
|
||||
assert.deepEqual(female['minecraft:geometry'][0].bones[0].cubes, [{}]);
|
||||
assert.equal(female.afw_bone_textures, undefined);
|
||||
assert.equal(JSON.stringify(build(source, [])), before);
|
||||
assert.equal(JSON.stringify(source), before);
|
||||
});
|
||||
|
||||
test('variant subtree propagation handles nested children and unknown saved selections', () => {
|
||||
const build = variantBuilder();
|
||||
const source = { 'minecraft:geometry': [{ bones: [
|
||||
{ name: 'grandchild', parent: 'child', cubes: [{}] },
|
||||
{ name: 'child', parent: 'root', cubes: [{}] },
|
||||
{ name: 'root', cubes: [{}] },
|
||||
] }] };
|
||||
const hidden = build(source, ['root', 'deleted']);
|
||||
assert.equal(hidden['minecraft:geometry'][0].bones.length, 0);
|
||||
});
|
||||
|
||||
test('selected hidden parents disable and clear descendant selections separately for each gender', () => {
|
||||
const build = variantBuilder();
|
||||
const bones = [{ name: 'body' }, { name: 'child', parent: 'body' }, { name: 'tip', parent: 'child' }];
|
||||
const fields = {}, updates = [];
|
||||
for (const gender of ['m', 'f', 'mf']) for (let index = 0; index < bones.length; index++) {
|
||||
const input = { disabled: false };
|
||||
fields[`hide_${gender}_${index}`] = { input, bar: { style: {}, querySelectorAll: () => [input] } };
|
||||
}
|
||||
const dialog = { form: { form_data: fields, setValues: (values) => updates.push(values) } };
|
||||
const values = { hide_m_0: true, hide_m_1: true, hide_m_2: true, hide_f_1: true, hide_mf_0: true, hide_mf_2: true };
|
||||
build.refresh(dialog, bones, values);
|
||||
assert.equal(values.hide_m_1, false);
|
||||
assert.equal(values.hide_m_2, false);
|
||||
assert.equal(fields.hide_m_1.input.disabled, true);
|
||||
assert.equal(fields.hide_m_2.input.disabled, true);
|
||||
assert.equal(fields.hide_f_1.input.disabled, false);
|
||||
assert.equal(fields.hide_f_2.input.disabled, true);
|
||||
assert.equal(values.hide_mf_2, false);
|
||||
assert.equal(fields.hide_mf_2.input.disabled, true);
|
||||
assert.match(fields.hide_m_2.bar.title, /Already excluded/);
|
||||
values.hide_m_0 = false;
|
||||
build.refresh(dialog, bones, values);
|
||||
assert.equal(fields.hide_m_2.input.disabled, false);
|
||||
});
|
||||
|
||||
test('each gender bone list gets an independent bounded scroll container', () => {
|
||||
const build = variantBuilder();
|
||||
const nodes = [];
|
||||
const document = { createElement() {
|
||||
return { style: {}, children: [], appendChild(child) {
|
||||
nodes.splice(nodes.indexOf(child), 1);
|
||||
this.children.push(child);
|
||||
child.parentNode = this;
|
||||
} };
|
||||
} };
|
||||
const parent = { insertBefore(node, reference) { nodes.splice(nodes.indexOf(reference), 0, node); } };
|
||||
const fields = {};
|
||||
for (const gender of ['m', 'f', 'mf']) for (let index = 0; index < 2; index++) {
|
||||
const bar = { parentNode: parent, ownerDocument: document, style: {}, querySelectorAll: () => [] };
|
||||
fields[`hide_${gender}_${index}`] = { bar };
|
||||
nodes.push(bar);
|
||||
}
|
||||
const dialog = { form: { form_data: fields } };
|
||||
const bones = [{ name: 'body' }, { name: 'head', parent: 'body' }];
|
||||
build.sections(dialog, bones);
|
||||
assert.equal(nodes.length, 3);
|
||||
for (const gender of ['m', 'f', 'mf']) {
|
||||
const section = dialog.modelVariantBoneSections[gender];
|
||||
assert.equal(section.children.length, 2);
|
||||
assert.equal(section.style.maxHeight, '160px');
|
||||
assert.equal(section.style.overflowY, 'auto');
|
||||
assert.equal(section.children[0], fields[`hide_${gender}_0`].bar);
|
||||
}
|
||||
build.refresh(dialog, bones, { m: true, f: false, mf: true });
|
||||
assert.equal(dialog.modelVariantBoneSections.f.style.display, 'none');
|
||||
assert.equal(dialog.modelVariantBoneSections.mf.style.display, '');
|
||||
});
|
||||
|
||||
test('JEM exclusion removes whole subtrees without changing remaining roots or source', () => {
|
||||
const model = { texture: 'custom.png', textureSize: [64, 64], models: [{ part: 'body', id: 'body',
|
||||
boxes: [{}], submodels: [{ id: 'd', boxes: [{}], sprites: [{}],
|
||||
animations: [{ 'd.visible': 'nbt(NeedsOfNatureEnergy,range:70-200)' }],
|
||||
submodel: { id: 'tip', boxes: [{}] } }, { id: 'detail', boxes: [{}] }] }] };
|
||||
const original = JSON.stringify(model);
|
||||
const output = JSON.parse(JSON.stringify(variantBuilder().jem(model, ['0/0'])));
|
||||
assert.deepEqual(output.models[0].boxes, [{}]);
|
||||
assert.equal(output.models[0].submodels.length, 1);
|
||||
assert.equal(output.models[0].submodels[0].id, 'detail');
|
||||
assert.deepEqual(output.models[0].submodels[0].boxes, [{}]);
|
||||
assert.equal(output.texture, 'custom.png');
|
||||
assert.equal(JSON.stringify(model), original);
|
||||
});
|
||||
|
||||
test('JEM export opens the shared variant screen directly and writes numbered gender files', () => {
|
||||
const source = fs.readFileSync(path.join(__dirname, '..', 'AnimationDirector.js'), 'utf8').replace(
|
||||
' Plugin.register(PLUGIN_ID, {',
|
||||
' globalThis.variantTest = { showCreateJemModel };\n Plugin.register(PLUGIN_ID, {'
|
||||
);
|
||||
const model = { models: [{ part: 'body', id: 'body', boxes: [{}], submodels: [{ id: 'd', boxes: [{}] }] }] };
|
||||
let dialog;
|
||||
const files = [], errors = [];
|
||||
const codec = { compile: () => model, fileName: () => 'wolf' };
|
||||
const context = { Plugin: { register() {} }, Project: { name: 'wolf' },
|
||||
Format: { id: 'optifine_entity', codec }, Formats: { optifine_entity: { codec } }, console,
|
||||
Dialog: class {
|
||||
constructor(options) { dialog = options; this.form = { form_data: {} }; }
|
||||
show() {}
|
||||
getFormResult() { return Object.fromEntries(Object.entries(dialog.form).map(([key, field]) => [key, field.value])); }
|
||||
},
|
||||
Blockbench: { export: (file, callback) => { files.push(file); callback(); },
|
||||
showMessageBox: (error) => errors.push(error.message) },
|
||||
};
|
||||
vm.runInNewContext(source, context);
|
||||
context.variantTest.showCreateJemModel();
|
||||
assert.equal(dialog.title, 'Export JEM Models');
|
||||
assert.equal(dialog.form.hide_mf_1.label, 'd');
|
||||
const result = Object.fromEntries(Object.entries(dialog.form).map(([key, field]) => [key, field.value]));
|
||||
Object.assign(result, { base: false, m: true, f: true, mf: true, hide_f_1: true });
|
||||
dialog.onConfirm(result);
|
||||
assert.deepEqual(errors, []);
|
||||
assert.deepEqual(files.map((file) => file.name), ['wolf1', 'wolf2', 'wolf3']);
|
||||
assert.deepEqual(Array.from(files[0].extensions), ['jem']);
|
||||
assert.equal(JSON.parse(files[1].content).models[0].submodels.length, 0);
|
||||
assert.deepEqual(JSON.parse(files[2].content), model);
|
||||
assert.equal(JSON.parse(context.Project.animationdirector_jem_variants).hide_f[0], '0/0');
|
||||
});
|
||||
|
||||
test('JEM exclusion supports singular submodels and clears animation targets for removed IDs', () => {
|
||||
const model = { models: [{ id: 'body', submodel: { id: 'd', boxes: [{}] },
|
||||
animations: [{ 'd.visible': 'true', 'body.visible': 'true' }] }, { id: 'head' }] };
|
||||
const output = JSON.parse(JSON.stringify(variantBuilder().jem(model, ['0/single', '1'])));
|
||||
assert.equal(output.models.length, 1);
|
||||
assert.equal(output.models[0].submodel, undefined);
|
||||
assert.deepEqual(output.models[0].animations, [{ 'body.visible': 'true' }]);
|
||||
});
|
||||
Reference in New Issue
Block a user