added multi gender export with hiding
This commit is contained in:
+135
-10
@@ -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();
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user