added multi gender export with hiding

This commit is contained in:
2026-10-01 01:48:19 +02:00
parent a249f1bbdd
commit c1c94a7e16
+135 -10
View File
@@ -26,6 +26,8 @@
let autoplayTransitioning = false;
const POSE_BUTTON_EVENTS = ['select_animation', 'select_project', 'unselect_project', 'load_project', 'undo', 'redo', 'finish_edit', 'add_animation', 'remove_animation'];
const PROP_PREVIEWS_PROPERTY = 'animationdirector_prop_previews';
const MODEL_VARIANTS_PROPERTY = 'animationdirector_model_variants';
let modelVariantsProjectProperty;
const PROP_SLOTS = { left: 'propleft', right: 'propright', floor: 'propfloor' };
let propAction, propProjectProperty, propDialog, propListener;
const propMeshes = new Map();
@@ -2755,6 +2757,75 @@
} catch (error) { Blockbench.showMessageBox({ title: 'Create JEM Model', message: error.message }); }
}
function buildGenderModelVariant(model, hiddenBones) {
const output = clone(model);
for (const geometry of output['minecraft:geometry'] || []) {
const hidden = new Set(hiddenBones);
const bones = geometry.bones || [];
let previousSize;
do {
previousSize = hidden.size;
for (const bone of bones) if (hidden.has(bone.parent)) hidden.add(bone.name);
} while (hidden.size !== previousSize);
// Retain the animated skeleton and locators; only exclude rendered geometry.
for (const bone of bones) if (hidden.has(bone.name)) delete bone.cubes;
}
return output;
}
function hiddenModelAncestor(name, parents, selected) {
const visited = new Set([name]);
let parent = parents.get(name);
while (parent && !visited.has(parent)) {
if (selected.has(parent)) return parent;
visited.add(parent);
parent = parents.get(parent);
}
return null;
}
function refreshModelVariantFields(dialog, bones, result) {
const parents = new Map(bones.map((bone) => [bone.name, bone.parent]));
const updates = {};
for (const gender of ['m', 'f', 'mf']) {
const section = dialog.modelVariantBoneSections?.[gender];
if (section) section.style.display = result[gender] ? '' : 'none';
const selected = new Set(bones.filter((bone, index) => result[`hide_${gender}_${index}`]).map((bone) => bone.name));
bones.forEach((bone, index) => {
const key = `hide_${gender}_${index}`;
const ancestor = hiddenModelAncestor(bone.name, parents, selected);
if (ancestor && result[key]) result[key] = updates[key] = false;
const field = dialog.form?.form_data?.[key];
if (!field?.bar) return;
field.bar.style.opacity = ancestor ? '0.4' : '';
field.bar.title = ancestor ? `Already hidden by ${ancestor}` : '';
for (const input of field.bar.querySelectorAll('input')) input.disabled = !!ancestor;
});
}
if (Object.keys(updates).length) dialog.form.setValues(updates, false);
}
function createModelVariantBoneSections(dialog, bones) {
dialog.modelVariantBoneSections = {};
for (const gender of ['m', 'f', 'mf']) {
const bars = bones.map((bone, index) => dialog.form?.form_data?.[`hide_${gender}_${index}`]?.bar).filter(Boolean);
const first = bars[0];
if (!first?.parentNode || !first.ownerDocument) continue;
const section = first.ownerDocument.createElement('div');
section.className = 'animationdirector_variant_bones';
section.style.maxHeight = '160px';
section.style.overflowY = 'auto';
section.style.overflowX = 'hidden';
section.style.border = '1px solid var(--color-border)';
section.style.padding = '4px 8px';
section.style.marginBottom = '12px';
first.parentNode.insertBefore(section, first);
// Keep the native form fields and their event handlers intact.
for (const bar of bars) section.appendChild(bar);
dialog.modelVariantBoneSections[gender] = section;
}
}
function exportGeckoModel() {
if (!Project) {
Blockbench.showQuickMessage('No project open. Create/open a project first.', 2500);
@@ -2766,16 +2837,66 @@
const afwBoneTextures = buildAfwBoneTextureMap();
const output = addAfwBoneTextureMapToGeckoJson(compiled, afwBoneTextures);
Blockbench.export(
{
type: 'GeckoLib Model JSON',
extensions: ['json', 'geo.json'],
name: `${getModelExportFileName()}.geo`,
content: stringifyCompactModelJson(output, 2),
savetype: 'text',
const sourceProject = Project;
let saved = {};
try { saved = JSON.parse(Project[MODEL_VARIANTS_PROPERTY] || '{}'); } catch (_) { /* Use defaults for invalid saved options. */ }
const bones = [...new Map((output['minecraft:geometry'] || [])
.flatMap((geometry) => geometry.bones || []).map((bone) => [bone.name, bone])).values()];
const form = {
name: { type: 'text', label: 'Base model filename', value: saved.name || getModelExportFileName().replace(/\.(m|f|mf)$/i, '') },
base: { type: 'checkbox', label: 'Export unqualified model (.geo.json)', value: saved.base ?? true },
info: { type: 'info', text: 'Export one or more variants from this model. Hidden bones include their descendants. Bones and locators remain available to animations; the project geometry is unchanged.' },
};
for (const [gender, label] of Object.entries({ m: 'Male', f: 'Female', mf: 'Male + female' })) {
form[`heading_${gender}`] = { type: 'info', text: `### ${label} (.${gender}.geo.json)` };
form[gender] = { type: 'checkbox', label: `Export ${label.toLowerCase()}`, value: !!saved[gender] };
const hidden = Array.isArray(saved[`hide_${gender}`]) ? saved[`hide_${gender}`] : [];
form[`hide_heading_${gender}`] = { type: 'info', text: 'Hide bones (children are automatically included):',
condition: (result) => !!result[gender] };
bones.forEach((bone, index) => {
form[`hide_${gender}_${index}`] = { type: 'checkbox', label: bone.name,
value: hidden.includes(bone.name), condition: (result) => !!result[gender] };
});
}
const dialog = new Dialog({ id: 'animationdirector_model_variants', title: 'Export GeckoLib Models', width: 650, form,
onFormChange(result) { refreshModelVariantFields(this, bones, result); },
onConfirm(result) {
try {
if (Project !== sourceProject) throw new Error('Return to the source model project and retry.');
const name = String(result.name || '').trim().replace(/\.geo\.json$/i, '');
if (!name || /[<>:"/\\|?*]/.test(name)) throw new Error('Choose a valid base model filename without a folder path.');
const genders = ['m', 'f', 'mf'].filter((gender) => result[gender]);
if (!result.base && !genders.length) throw new Error('Select at least one model to export.');
const settings = { name, base: !!result.base };
const parents = new Map(bones.map((bone) => [bone.name, bone.parent]));
for (const gender of ['m', 'f', 'mf']) {
settings[gender] = !!result[gender];
const selected = new Set(bones.filter((bone, index) => result[`hide_${gender}_${index}`]).map((bone) => bone.name));
settings[`hide_${gender}`] = [...selected].filter((name) => !hiddenModelAncestor(name, parents, selected));
}
Project[MODEL_VARIANTS_PROPERTY] = JSON.stringify(settings);
Project.saved = false;
const files = [...(result.base ? [{ name, model: output }] : []), ...genders.map((gender) => ({
name: `${name}.${gender}`, model: buildGenderModelVariant(output, settings[`hide_${gender}`]),
}))];
// Open sequential save dialogs rather than competing simultaneous dialogs.
const exportNext = (index) => {
if (index >= files.length) return;
const file = files[index];
Blockbench.export({ type: 'GeckoLib Model JSON', extensions: ['json', 'geo.json'],
name: `${file.name}.geo`, content: stringifyCompactModelJson(file.model, 2), savetype: 'text' },
() => exportNext(index + 1));
};
exportNext(0);
} catch (error) {
Blockbench.showMessageBox({ title: 'Export GeckoLib Models', message: error.message });
return false;
}
},
() => Blockbench.showQuickMessage('Exported GeckoLib model', 2500)
);
});
dialog.show();
createModelVariantBoneSections(dialog, bones);
refreshModelVariantFields(dialog, bones, dialog.getFormResult());
} catch (e) {
console.error(e);
Blockbench.showMessageBox({
@@ -6938,10 +7059,13 @@ Features without a reference need no additional addon. Whole-animation import an
exportGeckoModelAction = new Action('animationdirector_export_gecko_model', {
name: 'Export GeckoLib Model',
description: 'Export the current model as GeckoLib/Bedrock geometry with AFW bone texture overrides.',
description: 'Export GeckoLib geometry, including m/f/mf variants with separate hidden-bone selections.',
icon: 'file_upload',
click: exportGeckoModel,
});
if (typeof ModelProject !== 'undefined' && !ModelProject.properties[MODEL_VARIANTS_PROPERTY]) {
modelVariantsProjectProperty = new Property(ModelProject, 'string', MODEL_VARIANTS_PROPERTY, { default: '{}' });
}
createJemModelAction = new Action('animationdirector_create_jem_model', {
condition: () => !Blockbench.isWeb,
name: 'Create JEM Model', icon: 'icon-format_optifine',
@@ -7093,6 +7217,7 @@ Features without a reference need no additional addon. Whole-animation import an
if (actorAnimdefMetadataProjectProperty?.delete) actorAnimdefMetadataProjectProperty.delete();
if (animdefAnimatorsProjectProperty?.delete) animdefAnimatorsProjectProperty.delete();
if (animdefDefinitionsProjectProperty?.delete) animdefDefinitionsProjectProperty.delete();
modelVariantsProjectProperty?.delete();
if (blockRequirementsProjectProperty?.delete) blockRequirementsProjectProperty.delete();
},
});