mirror of
https://github.com/JannisX11/blockbench.git
synced 2025-01-30 15:42:42 +08:00
Add export settings API
Add FBX export settings
This commit is contained in:
parent
c984c93ab8
commit
e7fcf8245e
@ -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',
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -17,6 +17,7 @@ class ModelProject {
|
||||
|
||||
this.save_path = '';
|
||||
this.export_path = '';
|
||||
this.export_options = {};
|
||||
this.added_models = 0;
|
||||
|
||||
this.undo = new UndoSystem();
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user