Add export settings API

Add FBX export settings
This commit is contained in:
JannisX11 2023-02-23 21:13:20 +01:00
parent c984c93ab8
commit e7fcf8245e
7 changed files with 196 additions and 124 deletions

View File

@ -170,7 +170,7 @@ const MenuBar = {
'import_obj',
'extrude_texture'
]},
{name: 'generic.export', id: 'export', icon: 'insert_drive_file', children: [
{name: 'generic.export', id: 'export', icon: 'insert_drive_file', condition: () => Project, children: [
'export_blockmodel',
'export_bedrock',
'export_entity',

View File

@ -6,6 +6,7 @@ class Codec {
Codecs[id] = this;
this.name = data.name || 'Unknown Format';
this.events = {};
this.export_options = data.export_options || {};
Merge.function(this, data, 'load');
Merge.function(this, data, 'compile');
Merge.function(this, data, 'parse');
@ -23,6 +24,9 @@ class Codec {
this.load_filter = data.load_filter;
this.export_action = data.export_action;
}
getExportOptions() {
return Project.export_options[this.id] || {};
}
//Import
load(model, file, add) {
if (!this.parse) return false;
@ -54,22 +58,59 @@ class Codec {
}
//parse(model, path)
//Export
compile() {
compile(options = this.getExportOptions()) {
this.dispatchEvent('compile', {content: ''})
return '';
}
export() {
var scope = this;
async promptExportOptions() {
let codec = this;
return await new Promise((resolve, reject) => {
let form = {};
let opts_in_project = Project.export_options[codec.id];
for (let form_id in this.export_options) {
if (!Condition(this.export_options[form_id].condition)) continue;
form[form_id] = {};
for (let key in this.export_options[form_id]) {
form[form_id][key] = this.export_options[form_id][key];
}
if (opts_in_project && opts_in_project[form_id] != undefined) {
form[form_id].value = opts_in_project[form_id];
}
}
new Dialog('export_options', {
title: 'dialog.export_options.title',
width: 480,
form,
onConfirm(result) {
if (!Project.export_options[codec.id]) Project.export_options[codec.id] = {};
for (let key in result) {
let value = result[key];
if (value !== form[key].value) {
Project.export_options[codec.id][key] = value;
}
}
resolve(result);
},
onCancel() {
resolve(null);
}
}).show();
})
}
async export() {
if (Object.keys(this.export_options).length) {
await this.promptExportOptions();
}
Blockbench.export({
resource_id: 'model',
type: scope.name,
extensions: [scope.extension],
name: scope.fileName(),
startpath: scope.startPath(),
content: scope.compile(),
custom_writer: isApp ? (a, b) => scope.write(a, b) : null,
}, path => scope.afterDownload(path))
type: this.name,
extensions: [this.extension],
name: this.fileName(),
startpath: this.startPath(),
content: this.compile(),
custom_writer: isApp ? (a, b) => this.write(a, b) : null,
}, path => this.afterDownload(path))
}
fileName() {
return Project.name||'model';
@ -78,11 +119,10 @@ class Codec {
return Project.export_path;
}
write(content, path) {
var scope = this;
if (fs.existsSync(path) && this.overwrite) {
this.overwrite(content, path, path => scope.afterSave(path))
this.overwrite(content, path, path => this.afterSave(path))
} else {
Blockbench.writeFile(path, {content}, path => scope.afterSave(path));
Blockbench.writeFile(path, {content}, path => this.afterSave(path));
}
}
//overwrite(content, path, cb)

View File

@ -228,6 +228,15 @@ var codec = new Codec('project', {
}
}
if (Object.keys(Project.export_options).length) {
model.export_options = {};
for (let codec_id in Project.export_options) {
if (Object.keys(Project.export_options[codec_id]).length) {
model.export_options[codec_id] = Object.assign({}, Project.export_options[codec_id]);
}
}
}
if (options.history) {
model.history = [];
Undo.history.forEach(h => {
@ -385,6 +394,11 @@ var codec = new Codec('project', {
}
})
}
if (model.export_options) {
for (let codec_id in model.export_options) {
Project.export_options[codec_id] = Object.assign({}, model.export_options[codec_id]);
}
}
if (model.history) {
Undo.history = model.history.slice()
Undo.index = model.history_index;
@ -630,6 +644,7 @@ BARS.defineActions(function() {
icon: 'save',
category: 'file',
keybind: new Keybind({key: 's', ctrl: true, alt: true}),
condition: () => Project,
click: function () {
saveTextures(true)
if (isApp && Project.save_path) {
@ -644,6 +659,7 @@ BARS.defineActions(function() {
icon: 'save',
category: 'file',
keybind: new Keybind({key: 's', ctrl: true, alt: true, shift: true}),
condition: () => Project,
click: function () {
saveTextures(true)
codec.export()

View File

@ -4,9 +4,9 @@
var codec = new Codec('fbx', {
name: 'FBX Model',
extension: 'fbx',
compile(options = 0) {
compile(options = this.getExportOptions()) {
let scope = this;
let export_scale = Settings.get('model_export_scale') / 100;
let export_scale = options.scale / 100;
let model = [
'; FBX 7.3.0 project file',
'; Created by the Blockbench FBX Exporter',
@ -539,125 +539,127 @@ var codec = new Codec('fbx', {
})
// Animations
let anim_clips = Codecs.gltf.buildAnimationTracks(false); // Handles sampling of math based curves etc.
let time_factor = 46186158000; // Arbitrary factor, found in three.js FBX importer
anim_clips.forEach(clip => {
DefinitionCounter.animation_stack++;
DefinitionCounter.animation_layer++;
if (options.include_animations) {
let anim_clips = Codecs.gltf.buildAnimationTracks(false); // Handles sampling of math based curves etc.
let time_factor = 46186158000; // Arbitrary factor, found in three.js FBX importer
anim_clips.forEach(clip => {
DefinitionCounter.animation_stack++;
DefinitionCounter.animation_layer++;
let stack_id = getID(clip.uuid+'_s');
let layer_id = getID(clip.uuid+'_l');
let unique_name = getUniqueName('animation', clip.uuid, clip.name);
let fbx_duration = Math.round(clip.duration * time_factor);
let stack_id = getID(clip.uuid+'_s');
let layer_id = getID(clip.uuid+'_l');
let unique_name = getUniqueName('animation', clip.uuid, clip.name);
let fbx_duration = Math.round(clip.duration * time_factor);
let stack = {
_key: 'AnimationStack',
_values: [stack_id, `AnimStack::${unique_name}`, ''],
Properties70: {
p1: {_key: 'P', _values: ['LocalStop', 'KTime', 'Time', '', fbx_duration]},
p2: {_key: 'P', _values: ['ReferenceStop', 'KTime', 'Time', '', fbx_duration]},
}
};
let layer = {
_key: 'AnimationLayer',
_values: [layer_id, `AnimLayer::${unique_name}`, ''],
_force_compound: true
};
Objects[clip.uuid+'_s'] = stack;
Objects[clip.uuid+'_l'] = layer;
Connections.push({
name: [`AnimLayer::${unique_name}`, `AnimStack::${unique_name}`],
id: [layer_id, stack_id],
});
clip.tracks.forEach(track => {
// Track = CurveNode
DefinitionCounter.animation_curve_node++;
let track_id = getID(clip.uuid + '.' + track.name)
let track_name = `AnimCurveNode::${unique_name}.${track.channel[0].toUpperCase()}`;
let curve_node = {
_key: 'AnimationCurveNode',
_values: [track_id, track_name, ''],
let stack = {
_key: 'AnimationStack',
_values: [stack_id, `AnimStack::${unique_name}`, ''],
Properties70: {
p1: {_key: 'P', _values: [`d|X`, 'Number', '', 'A', 1]},
p2: {_key: 'P', _values: [`d|Y`, 'Number', '', 'A', 1]},
p3: {_key: 'P', _values: [`d|Z`, 'Number', '', 'A', 1]},
p1: {_key: 'P', _values: ['LocalStop', 'KTime', 'Time', '', fbx_duration]},
p2: {_key: 'P', _values: ['ReferenceStop', 'KTime', 'Time', '', fbx_duration]},
}
};
let timecodes = track.times.map(second => Math.round(second * time_factor));
Objects[clip.uuid + '.' + track.name] = curve_node;
// Connect to bone
let layer = {
_key: 'AnimationLayer',
_values: [layer_id, `AnimLayer::${unique_name}`, ''],
_force_compound: true
};
Objects[clip.uuid+'_s'] = stack;
Objects[clip.uuid+'_l'] = layer;
Connections.push({
name: [track_name, `Model::${getUniqueName('object', track.group_uuid)}`],
id: [track_id, getID(track.group_uuid)],
property: track.channel == 'position' ? "Lcl Translation" : (track.channel == 'rotation' ? "Lcl Rotation" : "Lcl Scaling")
});
// Connect to layer
Connections.push({
name: [track_name, `AnimLayer::${unique_name}`],
id: [track_id, layer_id],
name: [`AnimLayer::${unique_name}`, `AnimStack::${unique_name}`],
id: [layer_id, stack_id],
});
['X', 'Y', 'Z'].forEach((axis_letter, axis_number) => {
DefinitionCounter.animation_curve++;
let curve_id = getID(clip.uuid + '.' + track.name + '.' + axis_letter);
let curve_name = `AnimCurve::${unique_name}.${track.channel[0].toUpperCase()}${axis_letter}`;
let values = track.values.filter((val, i) => (i % 3) == axis_number);
if (track.channel == 'position') {
values.forEach((v, i) => values[i] = v * 100);
}
if (track.channel == 'rotation') {
values.forEach((v, i) => values[i] = Math.radToDeg(v));
}
let curve = {
_key: 'AnimationCurve',
_values: [curve_id, curve_name, ''],
Default: 0,
KeyVer: 4008,
KeyTime: {
_values: [`_*${timecodes.length}`],
a: timecodes
},
KeyValueFloat: {
_values: [`_*${values.length}`],
a: values
},
KeyAttrFlags: {
_values: [`_*${1}`],
a: [24836]
},
KeyAttrDataFloat: {
_values: [`_*${4}`],
a: [0,0,255790911,0]
},
KeyAttrRefCount: {
_values: [`_*${1}`],
a: [timecodes.length]
},
clip.tracks.forEach(track => {
// Track = CurveNode
DefinitionCounter.animation_curve_node++;
let track_id = getID(clip.uuid + '.' + track.name)
let track_name = `AnimCurveNode::${unique_name}.${track.channel[0].toUpperCase()}`;
let curve_node = {
_key: 'AnimationCurveNode',
_values: [track_id, track_name, ''],
Properties70: {
p1: {_key: 'P', _values: [`d|X`, 'Number', '', 'A', 1]},
p2: {_key: 'P', _values: [`d|Y`, 'Number', '', 'A', 1]},
p3: {_key: 'P', _values: [`d|Z`, 'Number', '', 'A', 1]},
}
};
Objects[clip.uuid + '.' + track.name + axis_letter] = curve;
let timecodes = track.times.map(second => Math.round(second * time_factor));
Objects[clip.uuid + '.' + track.name] = curve_node;
// Connect to track
// Connect to bone
Connections.push({
name: [curve_name, track_name],
id: [curve_id, track_id],
property: `d|${axis_letter}`
name: [track_name, `Model::${getUniqueName('object', track.group_uuid)}`],
id: [track_id, getID(track.group_uuid)],
property: track.channel == 'position' ? "Lcl Translation" : (track.channel == 'rotation' ? "Lcl Rotation" : "Lcl Scaling")
});
// Connect to layer
Connections.push({
name: [track_name, `AnimLayer::${unique_name}`],
id: [track_id, layer_id],
});
});
})
Takes[clip.uuid] = {
_key: 'Take',
_values: [unique_name],
FileName: `${unique_name}.tak`,
LocalTime: [0, fbx_duration],
ReferenceTime: [0, fbx_duration],
};
})
['X', 'Y', 'Z'].forEach((axis_letter, axis_number) => {
DefinitionCounter.animation_curve++;
let curve_id = getID(clip.uuid + '.' + track.name + '.' + axis_letter);
let curve_name = `AnimCurve::${unique_name}.${track.channel[0].toUpperCase()}${axis_letter}`;
let values = track.values.filter((val, i) => (i % 3) == axis_number);
if (track.channel == 'position') {
values.forEach((v, i) => values[i] = v * 100);
}
if (track.channel == 'rotation') {
values.forEach((v, i) => values[i] = Math.radToDeg(v));
}
let curve = {
_key: 'AnimationCurve',
_values: [curve_id, curve_name, ''],
Default: 0,
KeyVer: 4008,
KeyTime: {
_values: [`_*${timecodes.length}`],
a: timecodes
},
KeyValueFloat: {
_values: [`_*${values.length}`],
a: values
},
KeyAttrFlags: {
_values: [`_*${1}`],
a: [24836]
},
KeyAttrDataFloat: {
_values: [`_*${4}`],
a: [0,0,255790911,0]
},
KeyAttrRefCount: {
_values: [`_*${1}`],
a: [timecodes.length]
},
};
Objects[clip.uuid + '.' + track.name + axis_letter] = curve;
// Connect to track
Connections.push({
name: [curve_name, track_name],
id: [curve_id, track_id],
property: `d|${axis_letter}`
});
});
})
Takes[clip.uuid] = {
_key: 'Take',
_values: [unique_name],
FileName: `${unique_name}.tak`,
LocalTime: [0, fbx_duration],
ReferenceTime: [0, fbx_duration],
};
})
}
// Object definitions
model += formatFBXComment('Object definitions');
@ -949,7 +951,14 @@ var codec = new Codec('fbx', {
})
})
},
export() {
export_options: {
scale: {label: 'settings.model_export_scale', type: 'number', value: Settings.get('model_export_scale')},
include_animations: {label: 'codec.fbx.export_animations', type: 'checkbox', value: true}
},
async export() {
if (Object.keys(this.export_options).length) {
await this.promptExportOptions();
}
var scope = this;
if (isApp) {
Blockbench.export({
@ -994,6 +1003,7 @@ BARS.defineActions(function() {
id: 'export_fbx',
icon: 'icon-fbx',
category: 'file',
condition: () => Project,
click: function () {
codec.export()
}

View File

@ -606,6 +606,7 @@ BARS.defineActions(function() {
icon: 'save',
category: 'file',
keybind: new Keybind({key: 's', ctrl: true}),
condition: () => Project,
click: async function() {
if (isApp) {
saveTextures()

View File

@ -17,6 +17,7 @@ class ModelProject {
this.save_path = '';
this.export_path = '';
this.export_options = {};
this.added_models = 0;
this.undo = new UndoSystem();

View File

@ -382,6 +382,8 @@
"dialog.select_texture.import_all": "Import All",
"dialog.export_options.title": "Export Options",
"dialog.texture.title": "Texture",
"dialog.texture.variable": "Variable",
"dialog.texture.namespace": "Namespace",
@ -1811,6 +1813,8 @@
"texture.error.ratio": "Invalid aspect ratio",
"texture.error.parent": "Texture file provided by parent model",
"codec.fbx.export_animations": "Export Animations",
"panel.uv": "UV",
"panel.display": "Display",
"panel.textures": "Textures",