Implement java animation exporter

Closes #1445
This commit is contained in:
JannisX11 2023-09-24 20:44:39 +02:00
parent ca9af96513
commit 5f09911074
4 changed files with 132 additions and 62 deletions

View File

@ -588,7 +588,7 @@ class Animation extends AnimationItem {
if (undo) {
Undo.finishEdit('Remove animation', {animations: []})
if (isApp && remove_from_file && this.path && fs.existsSync(this.path)) {
if (isApp && Format.animation_files && remove_from_file && this.path && fs.existsSync(this.path)) {
Blockbench.showMessageBox({
translateKey: 'delete_animation',
icon: 'movie',
@ -792,52 +792,6 @@ class Animation extends AnimationItem {
{name: 'menu.animation.loop.loop', icon: animation => (animation.loop == 'loop' ? 'radio_button_checked' : 'radio_button_unchecked'), click(animation) {animation.setLoop('loop', true)}},
]},
new MenuSeparator('manage'),
{
name: 'menu.animation.export_java',
id: 'save',
icon: 'save',
condition: () => Format.animation_files,
click(animation) {
let form = {};
let keys = [];
let animations = Animation.all.slice()
if (Format.animation_files) animations.sort((a1, a2) => a1.path.hashCode() - a2.path.hashCode())
animations.forEach(animation => {
let key = animation.name;
keys.push(key)
form[key.hashCode()] = {label: key, type: 'checkbox', value: true};
})
let dialog = new Dialog({
id: 'animation_export',
title: 'dialog.animation_export.title',
form,
onConfirm(form_result) {
dialog.hide();
keys = keys.filter(key => form_result[key.hashCode()]);
let animations = keys.map(k => Animation.all.find(anim => anim.name == k));
let content = Codecs.modded_entity.compileAnimations(animations);
Blockbench.export({
resource_id: 'modded_animation',
type: 'Modded Entity Animation',
extensions: ['java'],
name: (Project.geometry_name||'model'),
content,
})
}
})
form.select_all_none = {
type: 'buttons',
buttons: ['generic.select_all', 'generic.select_none'],
click(index) {
let values = {};
keys.forEach(key => values[key.hashCode()] = (index == 0));
dialog.setFormValues(values);
}
}
dialog.show();
}
},
{
name: 'menu.animation.save',
id: 'save',
@ -2049,6 +2003,7 @@ Interface.definePanels(function() {
'add_animation_controller',
'load_animation_file',
'slider_animation_length',
'export_modded_animations',
]
})
],

View File

@ -181,6 +181,7 @@ const MenuBar = {
'export_obj',
'export_fbx',
'export_collada',
'export_modded_animations',
'upload_sketchfab',
'share_model',
]},

View File

@ -280,6 +280,7 @@ const Templates = {
?(has_no_rotation)%(remove_n), PartPose.offset(%(x), %(y), %(z)));`,
renderer: `%(bone).render(poseStack, vertexConsumer, packedLight, packedOverlay, red, green, blue, alpha);`,
cube: `.texOffs(%(uv_x), %(uv_y)){?(has_mirror).mirror()}.addBox(%(x), %(y), %(z), %(dx), %(dy), %(dz), new CubeDeformation(%(inflate))){?(has_mirror).mirror(false)}`,
animation_template: 'mojang'
},
get(key, version = Project.modded_entity_version) {
@ -295,22 +296,46 @@ const Templates = {
}
}
const AnimationTemplates = {
'mojang': {
name: 'Mojmaps',
file:
`// Save this class in your mod and generate all required imports
/**
* Made with Blockbench %(bb_version)
* Exported for Minecraft version 1.19 or later with Mojang mappings
* @author %(author)
*/
public class %(identifier)Animation {
%(animations)
}`,
animation: `public static final AnimationDefinition %(name) = AnimationDefinition.Builder.withLength(%(length))%(looping)%(channels).build();`,
looping: `.looping()`,
channel: `.addAnimation("%(name)", new AnimationChannel(%(channel_type), %(keyframes)))`,
keyframe_rotation: `new Keyframe(%(time), KeyframeAnimations.degreeVec(%(x), %(y), %(z)), %(interpolation))`,
keyframe_position: `new Keyframe(%(time), KeyframeAnimations.posVec(%(x), %(y), %(z)), %(interpolation))`,
keyframe_scale: `new Keyframe(%(time), KeyframeAnimations.scaleVec(%(x), %(y), %(z)), %(interpolation))`,
channel_types: {
rotation: 'AnimationChannel.Targets.ROTATION',
position: 'AnimationChannel.Targets.POSITION',
scale: 'AnimationChannel.Targets.SCALE',
},
interpolations: {
linear: 'AnimationChannel.Interpolations.LINEAR',
catmullrom: 'AnimationChannel.Interpolations.CATMULLROM',
},
},
get(key, version = Project.modded_entity_version) {
let temp = Templates[version][key];
let mapping = Templates.get('animation_template', version);
let temp = AnimationTemplates[mapping || 'mojang'][key];
if (typeof temp === 'string') temp = temp.replace(/\t\t\t/g, '');
return temp;
},
keepLine(line) {
return line.replace(/\?\(\w+\)/, '');
},
getVariableRegex(name) {
return new RegExp(`%\\(${name}\\)`, 'g');
}
};
function getIdentifier() {
return (Project.geometry_name && Project.geometry_name.replace(/[\s-]+/g, '_')) || Project.name || 'custom_model';
return (Project.geometry_name && Project.geometry_name.replace(/[\s-]+/g, '_')) || Project.name || 'CustomModel';
}
function askToSaveProject() {
@ -884,14 +909,57 @@ Object.defineProperty(codec, 'remember', {
}
})
codec.compileAnimations = function(animations) {
codec.compileAnimations = function(animations = Animation.all) {
let R = Templates.getVariableRegex;
let identifier = getIdentifier();
let interpolations = AnimationTemplates.get('interpolations');
let file = AnimationTemplates.get('file');
model = model.replace(R('bb_version'), Blockbench.version);
model = model.replace(R('identifier'), identifier);
model = model.replace(R('identifier_rl'), identifier.toLowerCase().replace(' ', '_'));
model = model.replace(R('texture_width'), Project.texture_width);
model = model.replace(R('texture_height'), Project.texture_height);
file = file.replace(R('bb_version'), Blockbench.version);
file = file.replace(R('author'), Settings.get('username') || 'Author');
file = file.replace(R('identifier'), identifier);
let anim_strings = [];
animations.forEach(animation => {
let anim_string = AnimationTemplates.get('animation');
anim_string = anim_string.replace(R('name'), animation.name);
anim_string = anim_string.replace(R('length'), F(animation.length));
anim_string = anim_string.replace(R('looping'), animation.loop == 'loop' ? AnimationTemplates.get('looping') : '');
let channel_strings = [];
let channel_types = AnimationTemplates.get('channel_types');
for (let id in animation.animators) {
let animator = animation.animators[id];
if (animator instanceof BoneAnimator == false) continue;
for (let channel_id in channel_types) {
if (!(animator[channel_id] && animator[channel_id].length)) continue;
let keyframes = animator[channel_id].slice().sort((a, b) => a.time - b.time);
let keyframe_strings = keyframes.map(kf => {
let kf_string = AnimationTemplates.get('keyframe_'+channel_id);
kf_string = kf_string.replace(R('time'), F(kf.time));
kf_string = kf_string.replace(R('x'), F(kf.calc('x')));
kf_string = kf_string.replace(R('y'), F(kf.calc('y')));
kf_string = kf_string.replace(R('z'), F(kf.calc('z')));
kf_string = kf_string.replace(R('interpolation'), interpolations[kf.interpolation] || interpolations.catmullrom);
return kf_string;
})
let channel_string = AnimationTemplates.get('channel');
channel_string = channel_string.replace(R('name'), animator.name);
channel_string = channel_string.replace(R('channel_type'), channel_types[channel_id]);
channel_string = channel_string.replace(R('keyframes'), '\n\t\t\t' + keyframe_strings.join(',\n\t\t\t') + '\n\t\t');
channel_strings.push(channel_string);
}
}
anim_string = anim_string.replace(R('channels'), '\n\t\t' + channel_strings.join('\n\t\t') + '\n\t\t');
anim_strings.push(anim_string);
})
file = file.replace(R('animations'), anim_strings.join('\n\n\t'));
return file;
}
var format = new ModelFormat({
@ -931,6 +999,50 @@ BARS.defineActions(function() {
codec.export()
}
})
new Action('export_modded_animations', {
icon: 'free_breakfast',
category: 'file',
condition: () => Format == format,
click() {
let form = {};
let keys = [];
let animations = Animation.all.slice();
if (Format.animation_files) animations.sort((a1, a2) => a1.path.hashCode() - a2.path.hashCode());
animations.forEach(animation => {
let key = animation.name;
keys.push(key)
form[key.hashCode()] = {label: key, type: 'checkbox', value: true};
})
let dialog = new Dialog({
id: 'animation_export',
title: 'dialog.animation_export.title',
form,
onConfirm(form_result) {
dialog.hide();
keys = keys.filter(key => form_result[key.hashCode()]);
let animations = keys.map(k => Animation.all.find(anim => anim.name == k));
let content = Codecs.modded_entity.compileAnimations(animations);
Blockbench.export({
resource_id: 'modded_animation',
type: 'Modded Entity Animation',
extensions: ['java'],
name: (Project.geometry_name||'model'),
content,
})
}
})
form.select_all_none = {
type: 'buttons',
buttons: ['generic.select_all', 'generic.select_none'],
click(index) {
let values = {};
keys.forEach(key => values[key.hashCode()] = (index == 0));
dialog.setFormValues(values);
}
}
dialog.show();
}
})
})
})()

View File

@ -1174,6 +1174,8 @@
"action.export_entity.desc": "Export the model as an entity model for Minecraft Bedrock Edition",
"action.export_class_entity": "Export Java Entity",
"action.export_class_entity.desc": "Export the entity model as a Java class",
"action.export_modded_animations": "Export Modded Entity Animations",
"action.export_modded_animations.desc": "Export animations for a Minecraft Java Edition modded entity model",
"action.import_optifine_part": "Import OptiFine Part",
"action.import_optifine_part.desc": "Import an entity part model for OptiFine",
"action.export_optifine_full": "Export OptiFine JEM",