blockbench/js/undo.js

639 lines
16 KiB
JavaScript
Raw Normal View History

2021-05-05 19:54:41 +08:00
class UndoSystem {
constructor() {
this.index = 0;
this.history = [];
}
2021-12-08 01:31:16 +08:00
initEdit(aspects, amended = false) {
2019-07-18 00:02:07 +08:00
if (aspects && aspects.cubes) {
console.warn('Aspect "cubes" is deprecated. Please use "elements" instead.');
aspects.elements = aspects.cubes;
}
/*
if (
2021-05-05 19:54:41 +08:00
aspects && this.current_save &&
Objector.equalKeys(aspects, this.current_save.aspects) &&
2019-07-18 00:02:07 +08:00
aspects.elements !== selected &&
2021-05-05 19:54:41 +08:00
this.history.length == this.index
2019-07-18 00:02:07 +08:00
) {
return;
2018-10-18 01:50:25 +08:00
}
2019-07-18 00:02:07 +08:00
- This still causes issues, for example with different texture selections
*/
2021-12-08 01:31:16 +08:00
if (!amended && this.amend_edit_menu) {
this.closeAmendEditMenu();
}
2021-05-05 19:54:41 +08:00
this.current_save = new UndoSystem.save(aspects)
return this.current_save;
}
2019-08-18 00:26:14 +08:00
finishEdit(action, aspects) {
2019-07-18 00:02:07 +08:00
if (aspects && aspects.cubes) {
console.warn('Aspect "cubes" is deprecated. Please use "elements" instead.');
aspects.elements = aspects.cubes;
}
2021-05-05 19:54:41 +08:00
if (!this.current_save) return;
aspects = aspects || this.current_save.aspects
2018-10-18 01:50:25 +08:00
//After
2019-02-04 04:09:35 +08:00
Blockbench.dispatchEvent('finish_edit', {aspects})
2018-10-18 01:50:25 +08:00
var entry = {
2021-05-05 19:54:41 +08:00
before: this.current_save,
post: new UndoSystem.save(aspects),
2021-06-02 03:08:41 +08:00
action: action,
time: Date.now()
2018-10-18 01:50:25 +08:00
}
2021-05-05 19:54:41 +08:00
this.current_save = entry.post
if (this.history.length > this.index) {
this.history.length = this.index;
delete this.current_save;
2018-10-18 01:50:25 +08:00
}
2021-05-05 19:54:41 +08:00
this.history.push(entry)
2018-10-18 01:50:25 +08:00
2021-05-05 19:54:41 +08:00
if (this.history.length > settings.undo_limit.value) {
this.history.shift()
2018-10-18 01:50:25 +08:00
}
2021-05-05 19:54:41 +08:00
this.index = this.history.length
2018-12-16 23:18:20 +08:00
if (!aspects || !aspects.keep_saved) {
2021-05-05 19:54:41 +08:00
Project.saved = false;
2018-12-16 23:18:20 +08:00
}
2019-02-04 04:09:35 +08:00
Blockbench.dispatchEvent('finished_edit', {aspects})
if (Project.EditSession && Project.EditSession.active) {
Project.EditSession.sendEdit(entry)
2019-04-08 00:53:33 +08:00
}
2019-07-18 00:02:07 +08:00
return entry;
2021-05-05 19:54:41 +08:00
}
2019-08-18 00:26:14 +08:00
cancelEdit() {
2021-05-05 19:54:41 +08:00
if (!this.current_save) return;
2021-07-26 23:07:51 +08:00
Canvas.outlines.children.empty();
if (this.amend_edit_menu) {
2021-12-09 03:33:13 +08:00
this.closeAmendEditMenu();
}
2021-05-05 19:54:41 +08:00
this.loadSave(this.current_save, new UndoSystem.save(this.current_save.aspects))
delete this.current_save;
}
2021-12-08 01:31:16 +08:00
closeAmendEditMenu() {
if (this.amend_edit_menu) {
this.amend_edit_menu.remove();
delete this.amend_edit_menu;
}
}
amendEdit(form, callback) {
let input_elements = {};
let dialog = document.createElement('div');
dialog.id = 'amend_edit_menu';
this.amend_edit_menu = dialog;
let close_button = document.createElement('div');
close_button.append(Blockbench.getIconNode('clear'));
close_button.className = 'amend_edit_close_button';
close_button.title = tl('dialog.close')
close_button.addEventListener('click', (event) => {
Undo.closeAmendEditMenu();
})
dialog.append(close_button);
function updateValue() {
let form_values = {};
for (let key in form) {
form_values[key] = input_elements[key].get();
}
2021-12-09 03:33:13 +08:00
Undo.undo(null, true);
2021-12-08 01:31:16 +08:00
callback(form_values);
}
for (let key in form) {
let form_line = form[key];
let line = document.createElement('div');
line.className = 'amend_edit_line';
dialog.append(line);
let slider = new NumSlider({
id: 'amend_edit_slider',
private: true,
onChange: updateValue,
settings: {
default: form_line.value || 0,
min: form_line.min,
max: form_line.max,
step: form_line.step||1,
},
});
line.append(slider.node);
input_elements[key] = slider;
slider.update();
let label = document.createElement('label');
label.innerText = tl(form_line.label);
line.append(label);
}
let preview_container = document.getElementById('preview');
preview_container.append(dialog);
}
2019-08-18 00:26:14 +08:00
addKeyframeCasualties(arr) {
if (!arr || arr.length == 0) return;
2021-05-05 19:54:41 +08:00
if (!this.current_save.keyframes) {
this.current_save.keyframes = {
animation: Animation.selected.uuid
2019-08-18 00:26:14 +08:00
}
}
arr.forEach(kf => {
2021-05-05 19:54:41 +08:00
this.current_save.affected = true
this.current_save.keyframes[kf.uuid] = kf.getUndoCopy();
2019-08-18 00:26:14 +08:00
})
2021-05-05 19:54:41 +08:00
}
2021-12-09 03:33:13 +08:00
undo(remote, amended) {
if (!amended && this.amend_edit_menu) {
this.closeAmendEditMenu();
}
2021-05-05 19:54:41 +08:00
if (this.history.length <= 0 || this.index < 1) return;
2018-10-18 01:50:25 +08:00
2021-05-05 19:54:41 +08:00
Project.saved = false;
this.index--;
2018-10-18 01:50:25 +08:00
2021-05-05 19:54:41 +08:00
var entry = this.history[this.index]
this.loadSave(entry.before, entry.post)
if (Project.EditSession && remote !== true) {
Project.EditSession.sendAll('command', 'undo')
2019-04-08 00:53:33 +08:00
}
2019-02-04 04:09:35 +08:00
Blockbench.dispatchEvent('undo', {entry})
2021-05-05 19:54:41 +08:00
}
2021-12-09 03:33:13 +08:00
redo(remote, amended) {
if (!amended && this.amend_edit_menu) {
this.closeAmendEditMenu();
}
2021-05-05 19:54:41 +08:00
if (this.history.length <= 0) return;
if (this.index >= this.history.length) {
2018-10-18 01:50:25 +08:00
return;
}
2021-05-05 19:54:41 +08:00
Project.saved = false;
2018-10-18 01:50:25 +08:00
2021-05-05 19:54:41 +08:00
var entry = this.history[this.index]
this.index++;
this.loadSave(entry.post, entry.before)
if (Project.EditSession && remote !== true) {
Project.EditSession.sendAll('command', 'redo')
2019-04-08 00:53:33 +08:00
}
2019-02-04 04:09:35 +08:00
Blockbench.dispatchEvent('redo', {entry})
2021-05-05 19:54:41 +08:00
}
2019-08-18 00:26:14 +08:00
remoteEdit(entry) {
2021-05-05 19:54:41 +08:00
this.loadSave(entry.post, entry.before, 'session')
2019-04-08 00:53:33 +08:00
if (entry.save_history !== false) {
2021-05-05 19:54:41 +08:00
delete this.current_save;
this.history.push(entry)
if (this.history.length > settings.undo_limit.value) {
this.history.shift()
2019-04-08 00:53:33 +08:00
}
2021-05-05 19:54:41 +08:00
this.index = this.history.length
Project.saved = false;
2019-04-08 00:53:33 +08:00
Blockbench.dispatchEvent('finished_edit', {remote: true})
}
2021-05-05 19:54:41 +08:00
}
2019-08-18 00:26:14 +08:00
getItemByUUID(list, uuid) {
2018-10-18 01:50:25 +08:00
if (!list || typeof list !== 'object' || !list.length) {return false;}
var i = 0;
while (i < list.length) {
if (list[i].uuid === uuid) {
return list[i]
}
i++;
}
return false;
2021-05-05 19:54:41 +08:00
}
2019-08-18 00:26:14 +08:00
loadSave(save, reference, mode) {
2019-04-08 00:53:33 +08:00
var is_session = mode === 'session';
2020-05-31 21:54:04 +08:00
if (save.uv_mode) {
Project.box_uv = save.uv_mode.box_uv;
Project.texture_width = save.uv_mode.width;
Project.texture_height = save.uv_mode.height;
Canvas.updateAllUVs()
}
2019-07-18 00:02:07 +08:00
if (save.elements) {
for (var uuid in save.elements) {
if (save.elements.hasOwnProperty(uuid)) {
var element = save.elements[uuid]
var new_element = OutlinerNode.uuids[uuid]
2019-07-18 00:02:07 +08:00
if (new_element) {
for (var face in new_element.faces) {
new_element.faces[face].reset()
}
new_element.extend(element)
2021-08-22 07:25:50 +08:00
new_element.preview_controller.updateAll(new_element);
2018-10-18 01:50:25 +08:00
} else {
new_element = OutlinerElement.fromSave(element, true);
2018-10-18 01:50:25 +08:00
}
}
}
2019-07-18 00:02:07 +08:00
for (var uuid in reference.elements) {
if (reference.elements.hasOwnProperty(uuid) && !save.elements.hasOwnProperty(uuid)) {
var obj = OutlinerNode.uuids[uuid]
2018-10-18 01:50:25 +08:00
if (obj) {
2019-04-08 00:53:33 +08:00
obj.remove()
2018-10-18 01:50:25 +08:00
}
}
}
Canvas.updateVisibility()
}
if (save.outliner) {
2019-07-18 00:02:07 +08:00
Group.selected = undefined
2018-10-18 01:50:25 +08:00
parseGroups(save.outliner)
2019-04-08 00:53:33 +08:00
if (is_session) {
function iterate(arr) {
arr.forEach((obj) => {
delete obj.isOpen;
if (obj.children) {
iterate(obj.children)
}
})
}
iterate(save.outliner)
}
2019-07-18 00:02:07 +08:00
if (Format.bone_rig) {
2018-10-18 01:50:25 +08:00
Canvas.updateAllPositions()
}
}
2019-04-08 00:53:33 +08:00
if (save.selection_group && !is_session) {
2019-07-18 00:02:07 +08:00
Group.selected = undefined
var sel_group = OutlinerNode.uuids[save.selection_group]
2018-10-18 01:50:25 +08:00
if (sel_group) {
sel_group.select()
}
}
2019-04-08 00:53:33 +08:00
if (save.selection && !is_session) {
2018-10-18 01:50:25 +08:00
selected.length = 0;
elements.forEach(function(obj) {
if (save.selection.includes(obj.uuid)) {
2019-07-18 00:02:07 +08:00
obj.selectLow()
2021-12-08 01:31:16 +08:00
if (save.selected_vertices[obj.uuid]) {
Project.selected_vertices[obj.uuid] = save.selected_vertices[obj.uuid];
}
2018-10-18 01:50:25 +08:00
}
})
}
if (save.group) {
var group = OutlinerNode.uuids[save.group.uuid]
2018-10-18 01:50:25 +08:00
if (group) {
2019-04-08 00:53:33 +08:00
if (is_session) {
delete save.group.isOpen;
}
2018-10-18 01:50:25 +08:00
group.extend(save.group)
2019-07-18 00:02:07 +08:00
if (Format.bone_rig) {
2018-10-18 01:50:25 +08:00
group.forEachChild(function(obj) {
if (obj.preview_controller) obj.preview_controller.updateTransform(obj);
})
2018-10-18 01:50:25 +08:00
}
}
}
if (save.textures) {
Painter.current = {}
for (var uuid in save.textures) {
if (reference.textures[uuid]) {
var tex = Texture.all.find(tex => tex.uuid == uuid)
2018-10-18 01:50:25 +08:00
if (tex) {
2019-04-08 00:53:33 +08:00
var require_reload = tex.mode !== save.textures[uuid].mode;
2020-03-05 03:56:17 +08:00
tex.extend(save.textures[uuid]).updateSource()
tex.keep_size = true;
2019-04-08 00:53:33 +08:00
if (require_reload || reference.textures[uuid] === true) {
tex.load()
}
2018-10-18 01:50:25 +08:00
}
} else {
2019-04-08 00:53:33 +08:00
var tex = new Texture(save.textures[uuid], uuid)
tex.load().add(false)
2018-10-18 01:50:25 +08:00
}
}
for (var uuid in reference.textures) {
if (!save.textures[uuid]) {
var tex = Texture.all.find(tex => tex.uuid == uuid)
2018-10-18 01:50:25 +08:00
if (tex) {
2020-07-16 15:32:59 +08:00
Texture.all.splice(Texture.all.indexOf(tex), 1)
}
if (Texture.selected == tex) {
Texture.selected = undefined;
2018-10-18 01:50:25 +08:00
}
}
}
Canvas.updateAllFaces()
}
2020-07-16 15:32:59 +08:00
if (save.texture_order) {
Texture.all.sort((a, b) => {
return save.texture_order.indexOf(a.uuid) - save.texture_order.indexOf(b.uuid);
})
Canvas.updateLayeredTextures()
}
2020-07-18 19:31:46 +08:00
if (save.selected_texture) {
let tex = Texture.all.find(tex => tex.uuid == save.selected_texture);
if (tex instanceof Texture) tex.select()
} else if (save.selected_texture === null) {
2020-07-18 19:31:46 +08:00
unselectTextures()
}
2018-10-18 01:50:25 +08:00
if (save.settings) {
for (var key in save.settings) {
settings[key].value = save.settings[key]
}
}
2018-12-03 02:37:06 +08:00
2019-04-08 00:53:33 +08:00
if (save.animations) {
for (var uuid in save.animations) {
2018-12-03 02:37:06 +08:00
2021-05-05 19:54:41 +08:00
var animation = (reference.animations && reference.animations[uuid]) ? this.getItemByUUID(Animator.animations, uuid) : null;
2019-04-08 00:53:33 +08:00
if (!animation) {
animation = new Animation()
animation.uuid = uuid
}
animation.extend(save.animations[uuid]).add(false)
if (save.animations[uuid].selected) {
animation.select()
}
2018-12-03 02:37:06 +08:00
}
2019-04-08 00:53:33 +08:00
for (var uuid in reference.animations) {
if (!save.animations[uuid]) {
2021-05-05 19:54:41 +08:00
var animation = this.getItemByUUID(Animator.animations, uuid)
2019-04-08 00:53:33 +08:00
if (animation) {
animation.remove(false)
}
}
2018-12-03 02:37:06 +08:00
}
}
2019-04-08 00:53:33 +08:00
if (save.keyframes) {
var animation = Animation.selected;
2019-04-08 00:53:33 +08:00
if (!animation || animation.uuid !== save.keyframes.animation) {
2018-12-03 02:37:06 +08:00
animation = Animator.animations.findInArray('uuid', save.keyframes.animation)
2019-04-08 00:53:33 +08:00
if (animation.select && Animator.open && is_session) {
2019-01-09 22:54:35 +08:00
animation.select()
}
2018-12-03 02:37:06 +08:00
}
2019-04-08 00:53:33 +08:00
if (animation) {
2018-12-03 02:37:06 +08:00
2019-08-18 00:26:14 +08:00
function getKeyframe(uuid, animator) {
var i = 0;
while (i < animator.keyframes.length) {
if (animator.keyframes[i].uuid === uuid) {
return animator.keyframes[i];
2019-04-08 00:53:33 +08:00
}
2019-08-18 00:26:14 +08:00
i++;
2018-12-03 02:37:06 +08:00
}
2019-08-18 00:26:14 +08:00
}
for (var uuid in save.keyframes) {
if (uuid.length === 36 && save.keyframes.hasOwnProperty(uuid)) {
var data = save.keyframes[uuid];
var animator = animation.animators[data.animator];
if (!animator) continue;
var kf = getKeyframe(uuid, animator);
if (kf) {
kf.extend(data)
} else {
animator.addKeyframe(data, uuid);
2019-04-08 00:53:33 +08:00
}
2018-12-03 02:37:06 +08:00
}
2019-08-18 00:26:14 +08:00
}
for (var uuid in reference.keyframes) {
if (uuid.length === 36 && reference.keyframes.hasOwnProperty(uuid) && !save.keyframes.hasOwnProperty(uuid)) {
var data = reference.keyframes[uuid];
var animator = animation.animators[data.animator];
if (!animator) continue;
var kf = getKeyframe(uuid, animator)
if (kf) {
kf.remove()
2019-04-08 00:53:33 +08:00
}
2018-12-03 02:37:06 +08:00
}
}
2019-08-18 00:26:14 +08:00
updateKeyframeSelection()
2018-12-03 02:37:06 +08:00
}
}
2018-12-27 21:03:04 +08:00
if (save.display_slots) {
for (var slot in save.display_slots) {
2019-01-09 22:54:35 +08:00
var data = save.display_slots[slot]
2021-07-11 04:22:02 +08:00
if (!Project.display_settings[slot] && data) {
Project.display_settings[slot] = new DisplaySlot()
} else if (data === null && Project.display_settings[slot]) {
Project.display_settings[slot].default()
2018-12-27 21:03:04 +08:00
}
2021-07-11 04:22:02 +08:00
Project.display_settings[slot].extend(data).update()
2018-12-27 21:03:04 +08:00
}
}
Blockbench.dispatchEvent('load_undo_save', {save, reference, mode})
2018-10-18 01:50:25 +08:00
updateSelection()
2019-12-16 03:04:31 +08:00
if ((save.outliner || save.group) && Format.bone_rig) {
Canvas.updateAllBones();
}
if (Modes.animate) {
Animator.preview();
}
2018-10-18 01:50:25 +08:00
}
2018-12-27 21:03:04 +08:00
}
2021-05-05 19:54:41 +08:00
UndoSystem.save = class {
constructor(aspects) {
var scope = this;
this.aspects = aspects;
if (aspects.selection) {
2021-12-08 01:31:16 +08:00
this.selection = [];
this.selected_vertices = {};
selected.forEach(obj => {
this.selection.push(obj.uuid);
if (obj instanceof Mesh) {
this.selected_vertices[obj.uuid] = Mesh.selected[0].getSelectedVertices().slice();
}
2021-05-05 19:54:41 +08:00
})
if (Group.selected) {
this.selection_group = Group.selected.uuid
}
2021-12-08 01:31:16 +08:00
2021-05-05 19:54:41 +08:00
}
if (aspects.elements) {
this.elements = {}
aspects.elements.forEach(function(obj) {
scope.elements[obj.uuid] = obj.getUndoCopy(aspects)
})
}
if (aspects.outliner) {
this.outliner = compileGroups(true)
}
if (aspects.group) {
this.group = aspects.group.getChildlessCopy(true)
}
if (aspects.textures) {
this.textures = {}
aspects.textures.forEach(t => {
var tex = t.getUndoCopy(aspects.bitmap)
this.textures[t.uuid] = tex
})
}
if (aspects.texture_order && Texture.all.length) {
this.texture_order = [];
Texture.all.forEach(tex => {
this.texture_order.push(tex.uuid);
})
}
if (aspects.selected_texture && Texture.all.length) {
this.selected_texture = Texture.selected ? Texture.selected.uuid : null;
}
if (aspects.settings) {
this.settings = aspects.settings
}
if (aspects.uv_mode) {
this.uv_mode = {
box_uv: Project.box_uv,
width: Project.texture_width,
height: Project.texture_height
}
}
if (aspects.animations) {
this.animations = {}
aspects.animations.forEach(a => {
scope.animations[a.uuid] = a.getUndoCopy();
})
}
if (aspects.keyframes && Animation.selected && Timeline.animators.length) {
this.keyframes = {
animation: Animation.selected.uuid
}
aspects.keyframes.forEach(kf => {
scope.keyframes[kf.uuid] = kf.getUndoCopy()
})
}
if (aspects.display_slots) {
scope.display_slots = {}
aspects.display_slots.forEach(slot => {
2021-07-11 04:22:02 +08:00
if (Project.display_settings[slot]) {
scope.display_slots[slot] = Project.display_settings[slot].copy()
2021-05-05 19:54:41 +08:00
} else {
scope.display_slots[slot] = null
}
})
}
2021-05-08 17:50:19 +08:00
if (aspects.exploded_view !== undefined) {
this.exploded_view = !!aspects.exploded_view;
}
2021-05-05 19:54:41 +08:00
}
addTexture(texture) {
if (!this.textures) return;
if (this.aspects.textures.safePush(texture)) {
this.textures[texture.uuid] = texture.getUndoCopy(this.aspects.bitmap)
}
2018-12-27 21:03:04 +08:00
}
}
2021-05-05 19:54:41 +08:00
let Undo = null;
2018-12-27 21:03:04 +08:00
BARS.defineActions(function() {
2019-08-18 00:26:14 +08:00
new Action('undo', {
2018-12-27 21:03:04 +08:00
icon: 'undo',
category: 'edit',
2021-08-18 04:02:23 +08:00
condition: () => Project,
2019-03-10 05:06:35 +08:00
work_in_dialog: true,
keybind: new Keybind({key: 'z', ctrl: true}),
2021-05-05 19:54:41 +08:00
click(e) {
Project.undo.undo(e);
}
2018-12-27 21:03:04 +08:00
})
2019-08-18 00:26:14 +08:00
new Action('redo', {
2018-12-27 21:03:04 +08:00
icon: 'redo',
category: 'edit',
2021-08-18 04:02:23 +08:00
condition: () => Project,
2019-03-10 05:06:35 +08:00
work_in_dialog: true,
keybind: new Keybind({key: 'y', ctrl: true}),
2021-05-05 19:54:41 +08:00
click(e) {
Project.undo.redo(e);
}
2018-12-27 21:03:04 +08:00
})
2021-06-02 03:08:41 +08:00
new Action('edit_history', {
icon: 'history',
category: 'edit',
2021-08-14 18:57:17 +08:00
condition: () => Project,
2021-06-02 03:08:41 +08:00
click() {
let steps = [];
Undo.history.forEachReverse((entry, index) => {
index++;
step = {
name: entry.action,
time: new Date(entry.time).toLocaleTimeString(),
index,
current: index == Undo.index
};
steps.push(step);
})
steps.push({
2021-06-06 15:28:22 +08:00
name: 'Original',
2021-06-02 03:08:41 +08:00
time: '',
index: 0,
current: Undo.index == 0
})
let step_selected = null;
const dialog = new Dialog({
id: 'edit_history',
title: 'action.edit_history',
component: {
data() {return {
steps,
selected: null
}},
methods: {
select(index) {
this.selected = step_selected = index;
},
confirm() {
dialog.confirm();
}
},
template: `
<div id="edit_history_list">
<ul>
<li v-for="step in steps" :class="{current: step.current, selected: step.index == selected}" @click="select(step.index)" @dblclick="confirm()">
{{ step.name }}
<div class="edit_history_time">{{ step.time }}</div>
</li>
</ul>
</div>
`
},
onConfirm() {
if (step_selected === null) return;
let difference = step_selected - Undo.index;
if (step_selected < Undo.index) {
for (let i = 0; i < -difference; i++) {
Undo.undo();
}
} else if (step_selected > Undo.index) {
for (let i = 0; i < difference; i++) {
Undo.redo();
}
}
}
}).show();
}
})
2018-12-27 21:03:04 +08:00
})