You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
133 lines
4.2 KiB
133 lines
4.2 KiB
2 years ago
|
How to implement additional commands for undo/redo functionality?
|
||
|
===
|
||
|
|
||
|
### Basics ###
|
||
|
|
||
|
After evaluating different design patterns for undo/redo we decided to use the [command-pattern](http://en.wikipedia.org/wiki/Command_pattern) for implementing undo/redo functionality in the three.js-editor.
|
||
|
|
||
|
This means that every action is encapsulated in a command-object which contains all the relevant information to restore the previous state.
|
||
|
|
||
|
In our implementation we store the old and the new state separately (we don't store the complete state but rather the attribute and value which has changed).
|
||
|
It would also be possible to only store the difference between the old and the new state.
|
||
|
|
||
|
**Before implementing your own command you should look if you can't reuse one of the already existing ones.**
|
||
|
|
||
|
For numbers, strings or booleans the Set...ValueCommand-commands can be used.
|
||
|
Then there are separate commands for:
|
||
|
- setting a color property (THREE.Color)
|
||
|
- setting maps (THREE.Texture)
|
||
|
- setting geometries
|
||
|
- setting materials
|
||
|
- setting position, rotation and scale
|
||
|
|
||
|
### Template for new commands ###
|
||
|
|
||
|
Every command needs a constructor. In the constructor
|
||
|
|
||
|
```javascript
|
||
|
|
||
|
function DoSomethingCommand( editor ) {
|
||
|
|
||
|
Command.call( this, editor ); // Required: Call default constructor
|
||
|
|
||
|
this.type = 'DoSomethingCommand'; // Required: has to match the object-name!
|
||
|
this.name = 'Set/Do/Update Something'; // Required: description of the command, used in Sidebar.History
|
||
|
|
||
|
// TODO: store all the relevant information needed to
|
||
|
// restore the old and the new state
|
||
|
|
||
|
}
|
||
|
```
|
||
|
|
||
|
And as part of the prototype you need to implement four functions
|
||
|
- **execute:** which is also used for redo
|
||
|
- **undo:** which reverts the changes made by 'execute'
|
||
|
- **toJSON:** which serializes the command so that the undo/redo-history can be preserved across a browser refresh
|
||
|
- **fromJSON:** which deserializes the command
|
||
|
|
||
|
```javascript
|
||
|
DoSomethingCommand.prototype = {
|
||
|
|
||
|
execute: function () {
|
||
|
|
||
|
// TODO: apply changes to 'object' to reach the new state
|
||
|
|
||
|
},
|
||
|
|
||
|
undo: function () {
|
||
|
|
||
|
// TODO: restore 'object' to old state
|
||
|
|
||
|
},
|
||
|
|
||
|
toJSON: function () {
|
||
|
|
||
|
var output = Command.prototype.toJSON.call( this ); // Required: Call 'toJSON'-method of prototype 'Command'
|
||
|
|
||
|
// TODO: serialize all the necessary information as part of 'output' (JSON-format)
|
||
|
// so that it can be restored in 'fromJSON'
|
||
|
|
||
|
return output;
|
||
|
|
||
|
},
|
||
|
|
||
|
fromJSON: function ( json ) {
|
||
|
|
||
|
Command.prototype.fromJSON.call( this, json ); // Required: Call 'fromJSON'-method of prototype 'Command'
|
||
|
|
||
|
// TODO: restore command from json
|
||
|
|
||
|
}
|
||
|
|
||
|
};
|
||
|
|
||
|
```
|
||
|
|
||
|
### Executing a command ###
|
||
|
|
||
|
To execute a command we need an instance of the main editor-object. The editor-object functions as the only entry point through which all commands have to go to be added as part of the undo/redo-history.
|
||
|
On **editor** we then call **.execute(...)*** with the new command-object which in turn calls **history.execute(...)** and adds the command to the undo-stack.
|
||
|
|
||
|
```javascript
|
||
|
|
||
|
editor.execute( new DoSomethingCommand() );
|
||
|
|
||
|
```
|
||
|
|
||
|
### Updatable commands ###
|
||
|
|
||
|
Some commands are also **updatable**. By default a command is not updatable. Making a command updatable means that you
|
||
|
have to implement a fifth function 'update' as part of the prototype. In it only the 'new' state gets updated while the old one stays the same.
|
||
|
|
||
|
Here as an example is the update-function of **SetColorCommand**:
|
||
|
|
||
|
```javascript
|
||
|
update: function ( cmd ) {
|
||
|
|
||
|
this.newValue = cmd.newValue;
|
||
|
|
||
|
},
|
||
|
|
||
|
```
|
||
|
|
||
|
#### List of updatable commands
|
||
|
|
||
|
- SetColorCommand
|
||
|
- SetGeometryCommand
|
||
|
- SetMaterialColorCommand
|
||
|
- SetMaterialValueCommand
|
||
|
- SetPositionCommand
|
||
|
- SetRotationCommand
|
||
|
- SetScaleCommand
|
||
|
- SetValueCommand
|
||
|
- SetScriptValueCommand
|
||
|
|
||
|
The idea behind 'updatable commands' is that two commands of the same type which occur
|
||
|
within a short period of time should be merged into one.
|
||
|
**For example:** Dragging with your mouse over the x-position field in the sidebar
|
||
|
leads to hundreds of minor changes to the x-position.
|
||
|
The user expectation is not to undo every single change that happened while they dragged
|
||
|
the mouse cursor but rather to go back to the position before they started to drag their mouse.
|
||
|
|
||
|
When editing a script the changes are also merged into one undo-step.
|