소스 검색

Merge pull request #7337 from lxxxvi/dev

Implementation of Undo/Redo functionality for threejs-editor
Mr.doob 9 년 전
부모
커밋
bdc336befd
79개의 변경된 파일4899개의 추가작업 그리고 358개의 파일을 삭제
  1. 1 0
      .gitignore
  2. 8 1
      editor/css/dark.css
  3. 7 0
      editor/css/light.css
  4. 132 0
      editor/docs/Implementing additional commands for undo-redo.md
  5. 94 0
      editor/docs/Writing unit tests for undo-redo commands.md
  6. 37 2
      editor/index.html
  7. 47 0
      editor/js/Cmd.js
  8. 66 0
      editor/js/CmdAddObject.js
  9. 76 0
      editor/js/CmdAddScript.js
  10. 107 0
      editor/js/CmdMoveObject.js
  11. 85 0
      editor/js/CmdMultiCmds.js
  12. 103 0
      editor/js/CmdRemoveObject.js
  13. 81 0
      editor/js/CmdRemoveScript.js
  14. 74 0
      editor/js/CmdSetColor.js
  15. 86 0
      editor/js/CmdSetGeometry.js
  16. 71 0
      editor/js/CmdSetGeometryValue.js
  17. 74 0
      editor/js/CmdSetMaterial.js
  18. 74 0
      editor/js/CmdSetMaterialColor.js
  19. 125 0
      editor/js/CmdSetMaterialMap.js
  20. 76 0
      editor/js/CmdSetMaterialValue.js
  21. 83 0
      editor/js/CmdSetPosition.js
  22. 84 0
      editor/js/CmdSetRotation.js
  23. 84 0
      editor/js/CmdSetScale.js
  24. 100 0
      editor/js/CmdSetScene.js
  25. 88 0
      editor/js/CmdSetScriptValue.js
  26. 71 0
      editor/js/CmdSetUuid.js
  27. 76 0
      editor/js/CmdSetValue.js
  28. 2 0
      editor/js/Config.js
  29. 31 2
      editor/js/Editor.js
  30. 277 35
      editor/js/History.js
  31. 19 33
      editor/js/Loader.js
  32. 16 32
      editor/js/Menubar.Add.js
  33. 59 14
      editor/js/Menubar.Edit.js
  34. 66 17
      editor/js/Script.js
  35. 5 9
      editor/js/Sidebar.Geometry.BoxGeometry.js
  36. 3 1
      editor/js/Sidebar.Geometry.BufferGeometry.js
  37. 5 7
      editor/js/Sidebar.Geometry.CircleGeometry.js
  38. 5 9
      editor/js/Sidebar.Geometry.CylinderGeometry.js
  39. 3 1
      editor/js/Sidebar.Geometry.Geometry.js
  40. 5 7
      editor/js/Sidebar.Geometry.IcosahedronGeometry.js
  41. 3 1
      editor/js/Sidebar.Geometry.Modifiers.js
  42. 5 9
      editor/js/Sidebar.Geometry.PlaneGeometry.js
  43. 5 9
      editor/js/Sidebar.Geometry.SphereGeometry.js
  44. 5 9
      editor/js/Sidebar.Geometry.TorusGeometry.js
  45. 5 9
      editor/js/Sidebar.Geometry.TorusKnotGeometry.js
  46. 19 20
      editor/js/Sidebar.Geometry.js
  47. 136 0
      editor/js/Sidebar.History.js
  48. 129 55
      editor/js/Sidebar.Material.js
  49. 66 50
      editor/js/Sidebar.Object3D.js
  50. 4 5
      editor/js/Sidebar.Script.js
  51. 1 0
      editor/js/Sidebar.js
  52. 43 19
      editor/js/Viewport.js
  53. 2 2
      editor/js/libs/ui.three.js
  54. 6 0
      examples/js/controls/TransformControls.js
  55. 171 0
      test/unit/editor/CommonUtilities.js
  56. 56 0
      test/unit/editor/TestCmdAddObjectAndCmdRemoveObject.js
  57. 62 0
      test/unit/editor/TestCmdAddScript.js
  58. 42 0
      test/unit/editor/TestCmdMoveObject.js
  59. 80 0
      test/unit/editor/TestCmdMultiCmds.js
  60. 64 0
      test/unit/editor/TestCmdRemoveScript.js
  61. 40 0
      test/unit/editor/TestCmdSetColor.js
  62. 63 0
      test/unit/editor/TestCmdSetGeometry.js
  63. 51 0
      test/unit/editor/TestCmdSetGeometryValue.js
  64. 66 0
      test/unit/editor/TestCmdSetMaterial.js
  65. 45 0
      test/unit/editor/TestCmdSetMaterialColor.js
  66. 70 0
      test/unit/editor/TestCmdSetMaterialMap.js
  67. 63 0
      test/unit/editor/TestCmdSetMaterialValue.js
  68. 48 0
      test/unit/editor/TestCmdSetPosition.js
  69. 51 0
      test/unit/editor/TestCmdSetRotation.js
  70. 51 0
      test/unit/editor/TestCmdSetScale.js
  71. 73 0
      test/unit/editor/TestCmdSetScene.js
  72. 77 0
      test/unit/editor/TestCmdSetScriptValue.js
  73. 37 0
      test/unit/editor/TestCmdSetUuid.js
  74. 54 0
      test/unit/editor/TestCmdSetValue.js
  75. 55 0
      test/unit/editor/TestMassUndoAndRedo.js
  76. 55 0
      test/unit/editor/TestNegativeCases.js
  77. 108 0
      test/unit/editor/TestNestedDoUndoRedo.js
  78. 358 0
      test/unit/editor/TestSerialization.js
  79. 124 0
      test/unit/unittests_editor.html

+ 1 - 0
.gitignore

@@ -2,3 +2,4 @@
 *.swp
 .project
 node_modules
+.idea/

+ 8 - 1
editor/css/dark.css

@@ -128,7 +128,14 @@ input.Number {
 				#menubar .menu .options .option:active {
 					background: transparent;
 				}
-
+				
+		#menubar .menu .options .inactive {
+			color: #444;
+			background-color: transparent;
+			padding: 5px 10px;
+			margin: 0px !important;
+		}
+				
 #sidebar {
 	position: absolute;
 	right: 0px;

+ 7 - 0
editor/css/light.css

@@ -123,6 +123,13 @@ input.Number {
 					background: transparent;
 				}
 
+		#menubar .menu .options .inactive {
+			color: #bbb;
+			background-color: transparent;
+			padding: 5px 10px;
+			margin: 0px !important;
+		}
+
 #sidebar {
 	position: absolute;
 	right: 0px;

+ 132 - 0
editor/docs/Implementing additional commands for undo-redo.md

@@ -0,0 +1,132 @@
+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 CmdSet...Value-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
+	
+CmdXXX = function () {
+
+	Cmd.call( this ); // Required: Call default constructor
+
+	this.type = 'CmdXXX';            // Required: has to match the object-name!
+	this.name = 'Set/Do/Update XXX'; // 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
+CmdXXX.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 = Cmd.prototype.toJSON.call( this ); // Required: Call 'toJSON'-method of prototype 'Cmd'
+
+		// 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 ) {
+
+		Cmd.prototype.fromJSON.call( this, json ); // Required: Call 'fromJSON'-method of prototype 'Cmd'
+		
+		// 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 CmdXXX() );
+		
+```
+
+### 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 **CmdSetColor**:
+
+```javascript
+update: function ( cmd ) {
+
+	this.newValue = cmd.newValue;
+
+},
+
+```
+
+#### List of updatable commands
+
+- CmdSetColor
+- CmdSetGeometry
+- CmdSetMaterialColor
+- CmdSetMaterialValue
+- CmdSetPosition
+- CmdSetRotation
+- CmdSetScale
+- CmdSetValue
+- CmdSetScriptValue
+
+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 he dragged
+the mouse cursor but rather to go back to the position before he started to drag his mouse.
+
+When editing a script the changes are also merged into one undo-step.

+ 94 - 0
editor/docs/Writing unit tests for undo-redo commands.md

@@ -0,0 +1,94 @@
+Writing unit tests for undo-redo commands
+===
+
+### Overview ###
+
+Writing unit tests for undo/redo commands is easy.
+The main idea to simulate a scene, execute actions and perform undo and redo.
+Following steps are required.
+
+1. Create a new unit test file
+2. Include the new command and the unit test file in the editor's test suite
+3. Write the test
+4. Execute the test
+
+Each of the listed steps will now be described in detail.
+
+### 1. Create a new unit test file ###
+
+Create a new file in path `test/unit/editor/TestCmdXXX.js`.
+
+### 2. Include the new command in the editor test suite ###
+
+Navigate to the editor test suite `test/unit/unittests_editor.html` and open it.
+Within the file, go to the `<!-- command object classes -->` and include the new command:
+
+```javascript
+// <!-- command object classes -->
+//...
+<script src="../../editor/js/CmdSetUuid.js"></script>
+<script src="../../editor/js/CmdSetValue.js"></script>
+<script src="../../editor/js/CmdXXX.js"></script>         // add this line
+//...
+```
+
+It is recommended to keep the script inclusions in alphabetical order, if possible.
+
+Next, in the same file, go to `<!-- Undo-Redo tests -->` and include the test file for the new command:
+
+```javascript
+// <!-- Undo-Redo tests -->
+//...
+<script src="editor/TestCmdSetValue.js"></script>
+<script src="editor/TestCmdXXX.js"></script>              // add this line
+<script src="editor/TestNestedDoUndoRedo.js"></script>
+//...
+```
+
+Again, keeping the alphabetical order is recommended.
+
+### 3. Write the test ###
+
+#### Template ####
+
+Open the unit test file `test/unit/editor/TestCmdXXX.js` and paste following code:
+
+```javascript
+module( "CmdXXX" );
+
+test("Test CmdXXX (Undo and Redo)", function() {
+
+    var editor = new Editor();
+
+    var box = aBox( 'Name your box' );
+
+    // other available objects from "CommonUtilities.js"
+    // var sphere = aSphere( 'Name your sphere' );
+    // var pointLight = aPointLight( 'Name your pointLight' );
+    // var perspectiveCamera = aPerspectiveCamera( 'Name your perspectiveCamera' );
+
+    // in most cases you'll need to add the object to work with
+    editor.execute( new CmdAddObject( box ) );
+
+
+    // your test begins here...
+
+
+});
+```
+
+The predefined code is just meant to ease the development, you do not have to stick with it.
+However, the test should cover at least one `editor.execute()`, one `editor.undo()` and one `editor.redo()` call.
+
+Best practice is to call `editor.execute( new CmdXXX( {custom parameters} ) )` **twice**. Since you'll have to do one undo (go one step back), it is recommended to have a custom state for comparison. Try to avoid assertions `ok()` against default values.
+
+#### Assertions ####
+After performing `editor.execute()` twice, you can do your first assertion to check whether the executes are done correctly.
+
+Next, you perform `editor.undo()` and check if the last action was undone.
+
+Finally, perform `editor.redo()` and verify if the values are as expected.
+
+### 4. Execute the test ###
+
+Open the editor's unit test suite `test/unit/unittests_editor.html` in your browser and check the results from the test framework.

+ 37 - 2
editor/index.html

@@ -119,9 +119,31 @@
 		<script src="js/Sidebar.Geometry.TeapotBufferGeometry.js"></script>
 		<script src="js/Sidebar.Material.js"></script>
 		<script src="js/Sidebar.Script.js"></script>
+		<script src="js/Sidebar.History.js"></script>
 		<script src="js/Toolbar.js"></script>
 		<script src="js/Viewport.js"></script>
 		<script src="js/Viewport.Info.js"></script>
+		<script src="js/Cmd.js"></script>
+		<script src="js/CmdAddObject.js"></script>
+		<script src="js/CmdRemoveObject.js"></script>
+		<script src="js/CmdMoveObject.js"></script>
+		<script src="js/CmdSetPosition.js"></script>
+		<script src="js/CmdSetRotation.js"></script>
+		<script src="js/CmdSetScale.js"></script>
+		<script src="js/CmdSetValue.js"></script>
+		<script src="js/CmdSetUuid.js"></script>
+		<script src="js/CmdSetColor.js"></script>
+		<script src="js/CmdSetGeometry.js"></script>
+		<script src="js/CmdSetGeometryValue.js"></script>
+		<script src="js/CmdMultiCmds.js"></script>
+		<script src="js/CmdAddScript.js"></script>
+		<script src="js/CmdRemoveScript.js"></script>
+		<script src="js/CmdSetScriptValue.js"></script>
+		<script src="js/CmdSetMaterial.js"></script>
+		<script src="js/CmdSetMaterialValue.js"></script>
+		<script src="js/CmdSetMaterialColor.js"></script>
+		<script src="js/CmdSetMaterialMap.js"></script>
+		<script src="js/CmdSetScene.js"></script>
 
 		<script>
 
@@ -220,6 +242,7 @@
 				signals.materialChanged.add( saveState );
 				signals.sceneGraphChanged.add( saveState );
 				signals.scriptChanged.add( saveState );
+				signals.historyChanged.add( saveState );
 
 				signals.showModal.add( function ( content ) {
 
@@ -262,10 +285,22 @@
 						if ( confirm( 'Delete ' + object.name + '?' ) === false ) return;
 
 						var parent = object.parent;
-						editor.removeObject( object );
-						editor.select( parent );
+						if ( parent !== null ) editor.execute( new CmdRemoveObject( object ) );
 
 						break;
+						
+					case 90: // Register Ctrl-Z for Undo, Ctrl-Shift-Z for Redo
+
+						if ( event.ctrlKey && event.shiftKey ) {
+
+							editor.redo();
+
+						} else if ( event.ctrlKey ) {
+
+							editor.undo();
+
+						}
+						break;
 
 				}
 

+ 47 - 0
editor/js/Cmd.js

@@ -0,0 +1,47 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param editorRef pointer to main editor object used to initialize
+ *        each command object with a reference to the editor
+ * @constructor
+ */
+
+Cmd = function ( editorRef ) {
+
+	this.id = - 1;
+	this.inMemory = false;
+	this.updatable = false;
+	this.type = '';
+	this.name = '';
+
+	if ( editorRef !== undefined ) {
+
+		Cmd.editor = editorRef;
+
+	}
+	this.editor = Cmd.editor;
+
+
+};
+
+Cmd.prototype.toJSON = function () {
+
+	var output = {};
+	output.type = this.type;
+	output.id = this.id;
+	output.name = this.name;
+	return output;
+
+};
+
+Cmd.prototype.fromJSON = function ( json ) {
+
+	this.inMemory = true;
+	this.type = json.type;
+	this.id = json.id;
+	this.name = json.name;
+
+};

+ 66 - 0
editor/js/CmdAddObject.js

@@ -0,0 +1,66 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param object THREE.Object3D
+ * @constructor
+ */
+
+CmdAddObject = function ( object ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdAddObject';
+
+	this.object = object;
+	if ( object !== undefined ) {
+
+		this.name = 'Add Object: ' + object.name;
+
+	}
+
+};
+
+CmdAddObject.prototype = {
+
+	execute: function () {
+
+		this.editor.addObject( this.object );
+		this.editor.select( this.object );
+
+	},
+
+	undo: function () {
+
+		this.editor.removeObject( this.object );
+		this.editor.deselect();
+
+	},
+
+	toJSON: function () {
+
+		var output = Cmd.prototype.toJSON.call( this );
+		output.object = this.object.toJSON();
+
+		return output;
+
+	},
+
+	fromJSON: function ( json ) {
+
+		Cmd.prototype.fromJSON.call( this, json );
+
+		this.object = this.editor.objectByUuid( json.object.object.uuid );
+
+		if ( this.object === undefined ) {
+
+			var loader = new THREE.ObjectLoader();
+			this.object = loader.parse( json.object );
+
+		}
+
+	}
+
+};

+ 76 - 0
editor/js/CmdAddScript.js

@@ -0,0 +1,76 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param object THREE.Object3D
+ * @param script javascript object
+ * @constructor
+ */
+
+CmdAddScript = function ( object, script ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdAddScript';
+	this.name = 'Add Script';
+
+	this.object = object;
+	this.script = script;
+
+};
+
+CmdAddScript.prototype = {
+
+	execute: function () {
+
+		if ( this.editor.scripts[ this.object.uuid ] === undefined ) {
+
+			this.editor.scripts[ this.object.uuid ] = [];
+
+		}
+
+		this.editor.scripts[ this.object.uuid ].push( this.script );
+
+		this.editor.signals.scriptAdded.dispatch( this.script );
+
+	},
+
+	undo: function () {
+
+		if ( this.editor.scripts[ this.object.uuid ] === undefined ) return;
+
+		var index = this.editor.scripts[ this.object.uuid ].indexOf( this.script );
+
+		if ( index !== - 1 ) {
+
+			this.editor.scripts[ this.object.uuid ].splice( index, 1 );
+
+		}
+
+		this.editor.signals.scriptRemoved.dispatch( this.script );
+
+	},
+
+	toJSON: function () {
+
+		var output = Cmd.prototype.toJSON.call( this );
+
+		output.objectUuid = this.object.uuid;
+		output.script = this.script;
+
+		return output;
+
+	},
+
+	fromJSON: function ( json ) {
+
+		Cmd.prototype.fromJSON.call( this, json );
+
+		this.script = json.script;
+		this.object = this.editor.objectByUuid( json.objectUuid );
+
+	}
+
+};

+ 107 - 0
editor/js/CmdMoveObject.js

@@ -0,0 +1,107 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param object THREE.Object3D
+ * @param newParent THREE.Object3D
+ * @param newBefore THREE.Object3D
+ * @constructor
+ */
+
+CmdMoveObject = function ( object, newParent, newBefore ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdMoveObject';
+	this.name = 'Move Object';
+
+	this.object = object;
+	this.oldParent = ( object !== undefined ) ? object.parent : undefined;
+	this.oldIndex = ( this.oldParent !== undefined ) ? this.oldParent.children.indexOf( this.object ) : undefined;
+	this.newParent = newParent;
+
+	if ( newBefore !== undefined ) {
+
+		this.newIndex = ( newParent !== undefined ) ? newParent.children.indexOf( newBefore ) : undefined;
+
+	} else {
+
+		this.newIndex = ( newParent !== undefined ) ? newParent.children.length : undefined;
+
+	}
+
+	if ( this.oldParent === this.newParent && this.newIndex > this.oldIndex ) {
+
+		this.newIndex --;
+
+	}
+
+	this.newBefore = newBefore;
+
+};
+
+CmdMoveObject.prototype = {
+
+	execute: function () {
+
+		this.oldParent.remove( this.object );
+
+		var children = this.newParent.children;
+		children.splice( this.newIndex, 0, this.object );
+		this.object.parent = this.newParent;
+
+		this.editor.signals.sceneGraphChanged.dispatch();
+
+	},
+
+	undo: function () {
+
+		this.newParent.remove( this.object );
+
+		var children = this.oldParent.children;
+		children.splice( this.oldIndex, 0, this.object );
+		this.object.parent = this.oldParent;
+
+		this.editor.signals.sceneGraphChanged.dispatch();
+
+	},
+
+	toJSON: function () {
+
+		var output = Cmd.prototype.toJSON.call( this );
+
+		output.objectUuid = this.object.uuid;
+		output.newParentUuid = this.newParent.uuid;
+		output.oldParentUuid = this.oldParent.uuid;
+		output.newIndex = this.newIndex;
+		output.oldIndex = this.oldIndex;
+
+		return output;
+
+	},
+
+	fromJSON: function ( json ) {
+
+		Cmd.prototype.fromJSON.call( this, json );
+
+		this.object = this.editor.objectByUuid( json.objectUuid );
+		this.oldParent = this.editor.objectByUuid( json.oldParentUuid );
+		if ( this.oldParent === undefined ) {
+
+			this.oldParent = this.editor.scene;
+
+		}
+		this.newParent = this.editor.objectByUuid( json.newParentUuid );
+		if ( this.newParent === undefined ) {
+
+			this.newParent = this.editor.scene;
+
+		}
+		this.newIndex = json.newIndex;
+		this.oldIndex = json.oldIndex;
+
+	}
+
+};

+ 85 - 0
editor/js/CmdMultiCmds.js

@@ -0,0 +1,85 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param cmdArray array containing command objects
+ * @constructor
+ */
+
+CmdMultiCmds = function ( cmdArray ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdMultiCmds';
+	this.name = 'Multiple Changes';
+
+	this.cmdArray = ( cmdArray !== undefined ) ? cmdArray : [];
+
+};
+
+CmdMultiCmds.prototype = {
+
+	execute: function () {
+
+		this.editor.signals.sceneGraphChanged.active = false;
+
+		for ( var i = 0; i < this.cmdArray.length; i ++ ) {
+
+			this.cmdArray[ i ].execute();
+
+		}
+
+		this.editor.signals.sceneGraphChanged.active = true;
+		this.editor.signals.sceneGraphChanged.dispatch();
+
+	},
+
+	undo: function () {
+
+		this.editor.signals.sceneGraphChanged.active = false;
+
+		for ( var i = this.cmdArray.length - 1; i >= 0; i -- ) {
+
+			this.cmdArray[ i ].undo();
+
+		}
+
+		this.editor.signals.sceneGraphChanged.active = true;
+		this.editor.signals.sceneGraphChanged.dispatch();
+
+	},
+
+	toJSON: function () {
+
+		var output = Cmd.prototype.toJSON.call( this );
+
+		var cmds = [];
+		for ( var i = 0; i < this.cmdArray.length; i ++ ) {
+
+			cmds.push( this.cmdArray[ i ].toJSON() );
+
+		}
+		output.cmds = cmds;
+
+		return output;
+
+	},
+
+	fromJSON: function ( json ) {
+
+		Cmd.prototype.fromJSON.call( this, json );
+
+		var cmds = json.cmds;
+		for ( var i = 0; i < cmds.length; i ++ ) {
+
+			var cmd = new window[ cmds[ i ].type ]();	// creates a new object of type "json.type"
+			cmd.fromJSON( cmds[ i ] );
+			this.cmdArray.push( cmd );
+
+		}
+
+	}
+
+};

+ 103 - 0
editor/js/CmdRemoveObject.js

@@ -0,0 +1,103 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param object THREE.Object3D
+ * @constructor
+ */
+
+CmdRemoveObject = function ( object ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdRemoveObject';
+	this.name = 'Remove Object';
+
+	this.object = object;
+	this.parent = ( object !== undefined ) ? object.parent : undefined;
+	if ( this.parent !== undefined ) {
+
+		this.index = this.parent.children.indexOf( this.object );
+
+	}
+
+};
+
+CmdRemoveObject.prototype = {
+
+	execute: function () {
+
+		var scope = this.editor;
+		this.object.traverse( function ( child ) {
+
+			scope.removeHelper( child );
+
+		} );
+
+		this.parent.remove( this.object );
+		this.editor.select( this.parent );
+
+		this.editor.signals.objectRemoved.dispatch( this.object );
+		this.editor.signals.sceneGraphChanged.dispatch();
+
+	},
+
+	undo: function () {
+
+		var scope = this.editor;
+
+		this.object.traverse( function ( child ) {
+
+			if ( child.geometry !== undefined ) scope.addGeometry( child.geometry );
+			if ( child.material !== undefined ) scope.addMaterial( child.material );
+
+			scope.addHelper( child );
+
+		} );
+
+		this.parent.children.splice( this.index, 0, this.object );
+		this.object.parent = this.parent;
+		this.editor.select( this.object );
+
+		this.editor.signals.objectAdded.dispatch( this.object );
+		this.editor.signals.sceneGraphChanged.dispatch();
+
+	},
+
+	toJSON: function () {
+
+		var output = Cmd.prototype.toJSON.call( this );
+		output.object = this.object.toJSON();
+		output.index = this.index;
+		output.parentUuid = this.parent.uuid;
+
+		return output;
+
+	},
+
+	fromJSON: function ( json ) {
+
+		Cmd.prototype.fromJSON.call( this, json );
+
+		this.parent = this.editor.objectByUuid( json.parentUuid );
+		if ( this.parent === undefined ) {
+
+			this.parent = this.editor.scene;
+
+		}
+
+		this.index = json.index;
+
+		this.object = this.editor.objectByUuid( json.object.object.uuid );
+		if ( this.object === undefined ) {
+
+			var loader = new THREE.ObjectLoader();
+			this.object = loader.parse( json.object );
+
+		}
+
+	}
+
+};

+ 81 - 0
editor/js/CmdRemoveScript.js

@@ -0,0 +1,81 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param object THREE.Object3D
+ * @param script javascript object
+ * @constructor
+ */
+
+CmdRemoveScript = function ( object, script ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdRemoveScript';
+	this.name = 'Remove Script';
+
+	this.object = object;
+	this.script = script;
+	if ( this.object && this.script ) {
+
+		this.index = this.editor.scripts[ this.object.uuid ].indexOf( this.script );
+
+	}
+
+};
+
+CmdRemoveScript.prototype = {
+
+	execute: function () {
+
+		if ( this.editor.scripts[ this.object.uuid ] === undefined ) return;
+
+		if ( this.index !== - 1 ) {
+
+			this.editor.scripts[ this.object.uuid ].splice( this.index, 1 );
+
+		}
+
+		this.editor.signals.scriptRemoved.dispatch( this.script );
+
+	},
+
+	undo: function () {
+
+		if ( this.editor.scripts[ this.object.uuid ] === undefined ) {
+
+			this.editor.scripts[ this.object.uuid ] = [];
+
+		}
+
+		this.editor.scripts[ this.object.uuid ].splice( this.index, 0, this.script );
+
+		this.editor.signals.scriptAdded.dispatch( this.script );
+
+	},
+
+	toJSON: function () {
+
+		var output = Cmd.prototype.toJSON.call( this );
+
+		output.objectUuid = this.object.uuid;
+		output.script = this.script;
+		output.index = this.index;
+
+		return output;
+
+	},
+
+	fromJSON: function ( json ) {
+
+		Cmd.prototype.fromJSON.call( this, json );
+
+		this.script = json.script;
+		this.index = json.index;
+		this.object = this.editor.objectByUuid( json.objectUuid );
+
+	}
+
+};

+ 74 - 0
editor/js/CmdSetColor.js

@@ -0,0 +1,74 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param object THREE.Object3D
+ * @param attributeName string
+ * @param newValue integer representing a hex color value
+ * @constructor
+ */
+
+CmdSetColor = function ( object, attributeName, newValue ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdSetColor';
+	this.name = 'Set ' + attributeName;
+	this.updatable = true;
+
+	this.object = object;
+	this.attributeName = attributeName;
+	this.oldValue = ( object !== undefined ) ? this.object[ this.attributeName ].getHex() : undefined;
+	this.newValue = newValue;
+
+};
+
+CmdSetColor.prototype = {
+
+	execute: function () {
+
+		this.object[ this.attributeName ].setHex( this.newValue );
+		this.editor.signals.objectChanged.dispatch( this.object );
+
+	},
+
+	undo: function () {
+
+		this.object[ this.attributeName ].setHex( this.oldValue );
+		this.editor.signals.objectChanged.dispatch( this.object );
+
+	},
+
+	update: function ( cmd ) {
+
+		this.newValue = cmd.newValue;
+
+	},
+
+	toJSON: function () {
+
+		var output = Cmd.prototype.toJSON.call( this );
+
+		output.objectUuid = this.object.uuid;
+		output.attributeName = this.attributeName;
+		output.oldValue = this.oldValue;
+		output.newValue = this.newValue;
+
+		return output;
+
+	},
+
+	fromJSON: function ( json ) {
+
+		Cmd.prototype.fromJSON.call( this, json );
+
+		this.object = this.editor.objectByUuid( json.objectUuid );
+		this.attributeName = json.attributeName;
+		this.oldValue = json.oldValue;
+		this.newValue = json.newValue;
+
+	}
+
+};

+ 86 - 0
editor/js/CmdSetGeometry.js

@@ -0,0 +1,86 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param object THREE.Object3D
+ * @param newGeometry THREE.Geometry
+ * @constructor
+ */
+
+CmdSetGeometry = function ( object, newGeometry ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdSetGeometry';
+	this.name = 'Set Geometry';
+	this.updatable = true;
+
+	this.object = object;
+	this.oldGeometry = ( object !== undefined ) ? object.geometry : undefined;
+	this.newGeometry = newGeometry;
+
+};
+
+CmdSetGeometry.prototype = {
+
+	execute: function () {
+
+		this.object.geometry.dispose();
+		this.object.geometry = this.newGeometry;
+		this.object.geometry.computeBoundingSphere();
+
+		this.editor.signals.geometryChanged.dispatch( this.object );
+		this.editor.signals.sceneGraphChanged.dispatch();
+
+	},
+
+	undo: function () {
+
+		this.object.geometry.dispose();
+		this.object.geometry = this.oldGeometry;
+		this.object.geometry.computeBoundingSphere();
+
+		this.editor.signals.geometryChanged.dispatch( this.object );
+		this.editor.signals.sceneGraphChanged.dispatch();
+
+	},
+
+	update: function ( cmd ) {
+
+		this.newGeometry = cmd.newGeometry;
+
+	},
+
+	toJSON: function () {
+
+		var output = Cmd.prototype.toJSON.call( this );
+
+		output.objectUuid = this.object.uuid;
+		output.oldGeometry = this.object.geometry.toJSON();
+		output.newGeometry = this.newGeometry.toJSON();
+
+		return output;
+
+	},
+
+	fromJSON: function ( json ) {
+
+		Cmd.prototype.fromJSON.call( this, json );
+
+		this.object = this.editor.objectByUuid( json.objectUuid );
+
+		this.oldGeometry = parseGeometry( json.oldGeometry );
+		this.newGeometry = parseGeometry( json.newGeometry );
+
+		function parseGeometry ( data ) {
+
+			var loader = new THREE.ObjectLoader();
+			return loader.parseGeometries( [ data ] )[ data.uuid ];
+
+		}
+
+	}
+
+};

+ 71 - 0
editor/js/CmdSetGeometryValue.js

@@ -0,0 +1,71 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param object THREE.Object3D
+ * @param attributeName string
+ * @param newValue number, string, boolean or object
+ * @constructor
+ */
+
+CmdSetGeometryValue = function ( object, attributeName, newValue ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdSetGeometryValue';
+	this.name = 'Set Geometry.' + attributeName;
+
+	this.object = object;
+	this.attributeName = attributeName;
+	this.oldValue = ( object !== undefined ) ? object.geometry[ attributeName ] : undefined;
+	this.newValue = newValue;
+
+};
+
+CmdSetGeometryValue.prototype = {
+
+	execute: function () {
+
+		this.object.geometry[ this.attributeName ] = this.newValue;
+		this.editor.signals.objectChanged.dispatch( this.object );
+		this.editor.signals.geometryChanged.dispatch();
+		this.editor.signals.sceneGraphChanged.dispatch();
+
+	},
+
+	undo: function () {
+
+		this.object.geometry[ this.attributeName ] = this.oldValue;
+		this.editor.signals.objectChanged.dispatch( this.object );
+		this.editor.signals.geometryChanged.dispatch();
+		this.editor.signals.sceneGraphChanged.dispatch();
+
+	},
+
+	toJSON: function () {
+
+		var output = Cmd.prototype.toJSON.call( this );
+
+		output.objectUuid = this.object.uuid;
+		output.attributeName = this.attributeName;
+		output.oldValue = this.oldValue;
+		output.newValue = this.newValue;
+
+		return output;
+
+	},
+
+	fromJSON: function ( json ) {
+
+		Cmd.prototype.fromJSON.call( this, json );
+
+		this.object = this.editor.objectByUuid( json.objectUuid );
+		this.attributeName = json.attributeName;
+		this.oldValue = json.oldValue;
+		this.newValue = json.newValue;
+
+	}
+
+};

+ 74 - 0
editor/js/CmdSetMaterial.js

@@ -0,0 +1,74 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param object THREE.Object3D
+ * @param newMaterial THREE.Material
+ * @constructor
+ */
+
+CmdSetMaterial = function ( object, newMaterial ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdSetMaterial';
+	this.name = 'New Material';
+
+	this.object = object;
+	this.oldMaterial = ( object !== undefined ) ? object.material : undefined;
+	this.newMaterial = newMaterial;
+
+};
+
+CmdSetMaterial.prototype = {
+
+	execute: function () {
+
+		this.object.material = this.newMaterial;
+		this.editor.signals.materialChanged.dispatch( this.newMaterial );
+
+	},
+
+	undo: function () {
+
+		this.object.material = this.oldMaterial;
+		this.editor.signals.materialChanged.dispatch( this.oldMaterial );
+
+	},
+
+	toJSON: function () {
+
+		var output = Cmd.prototype.toJSON.call( this );
+
+		output.objectUuid = this.object.uuid;
+		output.oldMaterial = this.oldMaterial.toJSON();
+		output.newMaterial = this.newMaterial.toJSON();
+
+		return output;
+
+	},
+
+	fromJSON: function ( json ) {
+
+		Cmd.prototype.fromJSON.call( this, json );
+
+		this.object = this.editor.objectByUuid( json.objectUuid );
+		this.oldMaterial = parseMaterial( json.oldMaterial );
+		this.newMaterial = parseMaterial( json.newMaterial );
+
+
+		function parseMaterial ( json ) {
+
+			var loader = new THREE.ObjectLoader();
+			var images = loader.parseImages( json.images );
+			var textures  = loader.parseTextures( json.textures, images );
+			var materials = loader.parseMaterials( [ json ], textures );
+			return materials[ json.uuid ];
+
+		}
+
+	}
+
+};

+ 74 - 0
editor/js/CmdSetMaterialColor.js

@@ -0,0 +1,74 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param object THREE.Object3D
+ * @param attributeName string
+ * @param newValue integer representing a hex color value
+ * @constructor
+ */
+
+CmdSetMaterialColor = function ( object, attributeName, newValue ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdSetMaterialColor';
+	this.name = 'Set Material.' + attributeName;
+	this.updatable = true;
+
+	this.object = object;
+	this.attributeName = attributeName;
+	this.oldValue = ( object !== undefined ) ? this.object.material[ this.attributeName ].getHex() : undefined;
+	this.newValue = newValue;
+
+};
+
+CmdSetMaterialColor.prototype = {
+
+	execute: function () {
+
+		this.object.material[ this.attributeName ].setHex( this.newValue );
+		this.editor.signals.materialChanged.dispatch( this.object.material );
+
+	},
+
+	undo: function () {
+
+		this.object.material[ this.attributeName ].setHex( this.oldValue );
+		this.editor.signals.materialChanged.dispatch( this.object.material );
+
+	},
+
+	update: function ( cmd ) {
+
+		this.newValue = cmd.newValue;
+
+	},
+
+	toJSON: function () {
+
+		var output = Cmd.prototype.toJSON.call( this );
+
+		output.objectUuid = this.object.uuid;
+		output.attributeName = this.attributeName;
+		output.oldValue = this.oldValue;
+		output.newValue = this.newValue;
+
+		return output;
+
+	},
+
+	fromJSON: function ( json ) {
+
+		Cmd.prototype.fromJSON.call( this, json );
+
+		this.object = this.editor.objectByUuid( json.objectUuid );
+		this.attributeName = json.attributeName;
+		this.oldValue = json.oldValue;
+		this.newValue = json.newValue;
+
+	}
+
+};

+ 125 - 0
editor/js/CmdSetMaterialMap.js

@@ -0,0 +1,125 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param object THREE.Object3D
+ * @param mapName string
+ * @param newMap THREE.Texture
+ * @constructor
+ */
+
+CmdSetMaterialMap = function ( object, mapName, newMap ) {
+
+	Cmd.call( this );
+	this.type = 'CmdSetMaterialMap';
+	this.name = 'Set Material.' + mapName;
+
+	this.object = object;
+	this.mapName = mapName;
+	this.oldMap = ( object !== undefined ) ? object.material[ mapName ] : undefined;
+	this.newMap = newMap;
+
+};
+
+CmdSetMaterialMap.prototype = {
+
+	execute: function () {
+
+		this.object.material[ this.mapName ] = this.newMap;
+		this.object.material.needsUpdate = true;
+		this.editor.signals.materialChanged.dispatch( this.object.material );
+
+	},
+
+	undo: function () {
+
+		this.object.material[ this.mapName ] = this.oldMap;
+		this.object.material.needsUpdate = true;
+		this.editor.signals.materialChanged.dispatch( this.object.material );
+
+	},
+
+	toJSON: function () {
+
+		var output = Cmd.prototype.toJSON.call( this );
+
+		output.objectUuid = this.object.uuid;
+		output.mapName = this.mapName;
+		output.newMap = serializeMap( this.newMap );
+		output.oldMap = serializeMap( this.oldMap );
+
+		return output;
+
+		// serializes a map (THREE.Texture)
+
+		function serializeMap ( map ) {
+
+			if ( map === null || map === undefined ) return null;
+
+			var meta = {
+				geometries: {},
+				materials: {},
+				textures: {},
+				images: {}
+			};
+
+			var json = map.toJSON( meta );
+			var images = extractFromCache( meta.images );
+			if ( images.length > 0 ) json.images = images;
+			json.sourceFile = map.sourceFile;
+
+			return json;
+
+		}
+
+		// Note: The function 'extractFromCache' is copied from Object3D.toJSON()
+
+		// extract data from the cache hash
+		// remove metadata on each item
+		// and return as array
+		function extractFromCache ( cache ) {
+
+			var values = [];
+			for ( var key in cache ) {
+
+				var data = cache[ key ];
+				delete data.metadata;
+				values.push( data );
+
+			}
+			return values;
+
+		}
+
+	},
+
+	fromJSON: function ( json ) {
+
+		Cmd.prototype.fromJSON.call( this, json );
+
+		this.object = this.editor.objectByUuid( json.objectUuid );
+		this.mapName = json.mapName;
+		this.oldMap = parseTexture( json.oldMap );
+		this.newMap = parseTexture( json.newMap );
+
+		function parseTexture ( json ) {
+
+			var map = null;
+			if ( json !== null ) {
+
+				var loader = new THREE.ObjectLoader();
+				var images = loader.parseImages( json.images );
+				var textures  = loader.parseTextures( [ json ], images );
+				map = textures[ json.uuid ];
+				map.sourceFile = json.sourceFile;
+
+			}
+			return map;
+
+		}
+
+	}
+
+};

+ 76 - 0
editor/js/CmdSetMaterialValue.js

@@ -0,0 +1,76 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param object THREE.Object3D
+ * @param attributeName string
+ * @param newValue number, string, boolean or object
+ * @constructor
+ */
+
+CmdSetMaterialValue = function ( object, attributeName, newValue ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdSetMaterialValue';
+	this.name = 'Set Material.' + attributeName;
+	this.updatable = true;
+
+	this.object = object;
+	this.oldValue = ( object !== undefined ) ? object.material[ attributeName ] : undefined;
+	this.newValue = newValue;
+	this.attributeName = attributeName;
+
+};
+
+CmdSetMaterialValue.prototype = {
+
+	execute: function () {
+
+		this.object.material[ this.attributeName ] = this.newValue;
+		this.object.material.needsUpdate = true;
+		this.editor.signals.materialChanged.dispatch( this.object.material );
+
+	},
+
+	undo: function () {
+
+		this.object.material[ this.attributeName ] = this.oldValue;
+		this.object.material.needsUpdate = true;
+		this.editor.signals.materialChanged.dispatch( this.object.material );
+
+	},
+
+	update: function ( cmd ) {
+
+		this.newValue = cmd.newValue;
+
+	},
+
+	toJSON: function () {
+
+		var output = Cmd.prototype.toJSON.call( this );
+
+		output.objectUuid = this.object.uuid;
+		output.attributeName = this.attributeName;
+		output.oldValue = this.oldValue;
+		output.newValue = this.newValue;
+
+		return output;
+
+	},
+
+	fromJSON: function ( json ) {
+
+		Cmd.prototype.fromJSON.call( this, json );
+
+		this.attributeName = json.attributeName;
+		this.oldValue = json.oldValue;
+		this.newValue = json.newValue;
+		this.object = this.editor.objectByUuid( json.objectUuid );
+
+	}
+
+};

+ 83 - 0
editor/js/CmdSetPosition.js

@@ -0,0 +1,83 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param object THREE.Object3D
+ * @param newPosition THREE.Vector3
+ * @param optionalOldPosition THREE.Vector3
+ * @constructor
+ */
+
+CmdSetPosition = function ( object, newPosition, optionalOldPosition ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdSetPosition';
+	this.name = 'Set Position';
+	this.updatable = true;
+
+	this.object = object;
+
+	if ( object !== undefined && newPosition !== undefined ) {
+
+		this.oldPosition = object.position.clone();
+		this.newPosition = newPosition.clone();
+
+	}
+
+	if ( optionalOldPosition !== undefined ) {
+
+		this.oldPosition = optionalOldPosition.clone();
+
+	}
+
+};
+CmdSetPosition.prototype = {
+
+	execute: function () {
+
+		this.object.position.copy( this.newPosition );
+		this.object.updateMatrixWorld( true );
+		this.editor.signals.objectChanged.dispatch( this.object );
+
+	},
+
+	undo: function () {
+
+		this.object.position.copy( this.oldPosition );
+		this.object.updateMatrixWorld( true );
+		this.editor.signals.objectChanged.dispatch( this.object );
+
+	},
+
+	update: function ( command ) {
+
+		this.newPosition.copy( command.newPosition );
+
+	},
+
+	toJSON: function () {
+
+		var output = Cmd.prototype.toJSON.call( this );
+
+		output.objectUuid = this.object.uuid;
+		output.oldPosition = this.oldPosition.toArray();
+		output.newPosition = this.newPosition.toArray();
+
+		return output;
+
+	},
+
+	fromJSON: function ( json ) {
+
+		Cmd.prototype.fromJSON.call( this, json );
+
+		this.object = this.editor.objectByUuid( json.objectUuid );
+		this.oldPosition = new THREE.Vector3().fromArray( json.oldPosition );
+		this.newPosition = new THREE.Vector3().fromArray( json.newPosition );
+
+	}
+
+};

+ 84 - 0
editor/js/CmdSetRotation.js

@@ -0,0 +1,84 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param object THREE.Object3D
+ * @param newRotation THREE.Euler
+ * @param optionalOldRotation THREE.Euler
+ * @constructor
+ */
+
+CmdSetRotation = function ( object, newRotation, optionalOldRotation ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdSetRotation';
+	this.name = 'Set Rotation';
+	this.updatable = true;
+
+	this.object = object;
+
+	if ( object !== undefined && newRotation !== undefined ) {
+
+		this.oldRotation = object.rotation.clone();
+		this.newRotation = newRotation.clone();
+
+	}
+
+	if ( optionalOldRotation !== undefined ) {
+
+		this.oldRotation = optionalOldRotation.clone();
+
+	}
+
+};
+
+CmdSetRotation.prototype = {
+
+	execute: function () {
+
+		this.object.rotation.copy( this.newRotation );
+		this.object.updateMatrixWorld( true );
+		this.editor.signals.objectChanged.dispatch( this.object );
+
+	},
+
+	undo: function () {
+
+		this.object.rotation.copy( this.oldRotation );
+		this.object.updateMatrixWorld( true );
+		this.editor.signals.objectChanged.dispatch( this.object );
+
+	},
+
+	update: function ( command ) {
+
+		this.newRotation.copy( command.newRotation );
+
+	},
+
+	toJSON: function () {
+
+		var output = Cmd.prototype.toJSON.call( this );
+
+		output.objectUuid = this.object.uuid;
+		output.oldRotation = this.oldRotation.toArray();
+		output.newRotation = this.newRotation.toArray();
+
+		return output;
+
+	},
+
+	fromJSON: function ( json ) {
+
+		Cmd.prototype.fromJSON.call( this, json );
+
+		this.object = this.editor.objectByUuid( json.objectUuid );
+		this.oldRotation = new THREE.Euler().fromArray( json.oldRotation );
+		this.newRotation = new THREE.Euler().fromArray( json.newRotation );
+
+	}
+
+};

+ 84 - 0
editor/js/CmdSetScale.js

@@ -0,0 +1,84 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param object THREE.Object3D
+ * @param newScale THREE.Vector3
+ * @param optionalOldScale THREE.Vector3
+ * @constructor
+ */
+
+CmdSetScale = function ( object, newScale, optionalOldScale ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdSetScale';
+	this.name = 'Set Scale';
+	this.updatable = true;
+
+	this.object = object;
+
+	if ( object !== undefined && newScale !== undefined ) {
+
+		this.oldScale = object.scale.clone();
+		this.newScale = newScale.clone();
+
+	}
+
+	if ( optionalOldScale !== undefined ) {
+
+		this.oldScale = optionalOldScale.clone();
+
+	}
+
+};
+
+CmdSetScale.prototype = {
+
+	execute: function () {
+
+		this.object.scale.copy( this.newScale );
+		this.object.updateMatrixWorld( true );
+		this.editor.signals.objectChanged.dispatch( this.object );
+
+	},
+
+	undo: function () {
+
+		this.object.scale.copy( this.oldScale );
+		this.object.updateMatrixWorld( true );
+		this.editor.signals.objectChanged.dispatch( this.object );
+
+	},
+
+	update: function ( command ) {
+
+		this.newScale.copy( command.newScale );
+
+	},
+
+	toJSON: function () {
+
+		var output = Cmd.prototype.toJSON.call( this );
+
+		output.objectUuid = this.object.uuid;
+		output.oldScale = this.oldScale.toArray();
+		output.newScale = this.newScale.toArray();
+
+		return output;
+
+	},
+
+	fromJSON: function ( json ) {
+
+		Cmd.prototype.fromJSON.call( this, json );
+
+		this.object = this.editor.objectByUuid( json.objectUuid );
+		this.oldScale = new THREE.Vector3().fromArray( json.oldScale );
+		this.newScale = new THREE.Vector3().fromArray( json.newScale );
+
+	}
+
+};

+ 100 - 0
editor/js/CmdSetScene.js

@@ -0,0 +1,100 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param scene containing children to import
+ * @constructor
+ */
+
+CmdSetScene = function ( scene ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdSetScene';
+	this.name = 'Set Scene';
+
+	this.cmdArray = [];
+
+	if ( scene !== undefined ) {
+
+		this.cmdArray.push( new CmdSetUuid( this.editor.scene, scene.uuid ) );
+		this.cmdArray.push( new CmdSetValue( this.editor.scene, 'name', scene.name ) );
+		this.cmdArray.push( new CmdSetValue( this.editor.scene, 'userData', JSON.parse( JSON.stringify( scene.userData ) ) ) );
+
+		while ( scene.children.length > 0 ) {
+
+			var child = scene.children.pop();
+			this.cmdArray.push( new CmdAddObject( child ) );
+
+		}
+
+	}
+
+};
+
+CmdSetScene.prototype = {
+
+	execute: function () {
+
+		this.editor.signals.sceneGraphChanged.active = false;
+
+		for ( var i = 0; i < this.cmdArray.length; i ++ ) {
+
+			this.cmdArray[ i ].execute();
+
+		}
+
+		this.editor.signals.sceneGraphChanged.active = true;
+		this.editor.signals.sceneGraphChanged.dispatch();
+
+	},
+
+	undo: function () {
+
+		this.editor.signals.sceneGraphChanged.active = false;
+
+		for ( var i = this.cmdArray.length - 1; i >= 0; i -- ) {
+
+			this.cmdArray[ i ].undo();
+
+		}
+
+		this.editor.signals.sceneGraphChanged.active = true;
+		this.editor.signals.sceneGraphChanged.dispatch();
+
+	},
+
+	toJSON: function () {
+
+		var output = Cmd.prototype.toJSON.call( this );
+
+		var cmds = [];
+		for ( var i = 0; i < this.cmdArray.length; i ++ ) {
+
+			cmds.push( this.cmdArray[ i ].toJSON() );
+
+		}
+		output.cmds = cmds;
+
+		return output;
+
+	},
+
+	fromJSON: function ( json ) {
+
+		Cmd.prototype.fromJSON.call( this, json );
+
+		var cmds = json.cmds;
+		for ( var i = 0; i < cmds.length; i ++ ) {
+
+			var cmd = new window[ cmds[ i ].type ]();	// creates a new object of type "json.type"
+			cmd.fromJSON( cmds[ i ] );
+			this.cmdArray.push( cmd );
+
+		}
+
+	}
+
+};

+ 88 - 0
editor/js/CmdSetScriptValue.js

@@ -0,0 +1,88 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param object THREE.Object3D
+ * @param script javascript object
+ * @param attributeName string
+ * @param newValue string, object
+ * @param cursorPosition javascript object with format {line: 2, ch: 3}
+ * @constructor
+ */
+
+CmdSetScriptValue = function ( object, script, attributeName, newValue, cursorPosition ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdSetScriptValue';
+	this.name = 'Set Script.' + attributeName;
+	this.updatable = true;
+
+	this.object = object;
+	this.script = script;
+
+	this.attributeName = attributeName;
+	this.oldValue = ( script !== undefined ) ? script[ this.attributeName ] : undefined;
+	this.newValue = newValue;
+	this.cursorPosition = cursorPosition;
+
+};
+
+CmdSetScriptValue.prototype = {
+
+	execute: function () {
+
+		this.script[ this.attributeName ] = this.newValue;
+
+		this.editor.signals.scriptChanged.dispatch();
+		this.editor.signals.refreshScriptEditor.dispatch( this.object, this.script, this.cursorPosition );
+
+	},
+
+	undo: function () {
+
+		this.script[ this.attributeName ] = this.oldValue;
+
+		this.editor.signals.scriptChanged.dispatch();
+		this.editor.signals.refreshScriptEditor.dispatch( this.object, this.script, this.cursorPosition );
+
+	},
+
+	update: function ( cmd ) {
+
+		this.cursorPosition = cmd.cursorPosition;
+		this.newValue = cmd.newValue;
+
+	},
+
+	toJSON: function () {
+
+		var output = Cmd.prototype.toJSON.call( this );
+
+		output.objectUuid = this.object.uuid;
+		output.index = this.editor.scripts[ this.object.uuid ].indexOf( this.script );
+		output.attributeName = this.attributeName;
+		output.oldValue = this.oldValue;
+		output.newValue = this.newValue;
+		output.cursorPosition = this.cursorPosition;
+
+		return output;
+
+	},
+
+	fromJSON: function ( json ) {
+
+		Cmd.prototype.fromJSON.call( this, json );
+
+		this.oldValue = json.oldValue;
+		this.newValue = json.newValue;
+		this.attributeName = json.attributeName;
+		this.object = this.editor.objectByUuid( json.objectUuid );
+		this.script = this.editor.scripts[ json.objectUuid ][ json.index ];
+		this.cursorPosition = json.cursorPosition;
+
+	}
+
+};

+ 71 - 0
editor/js/CmdSetUuid.js

@@ -0,0 +1,71 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param object THREE.Object3D
+ * @param newUuid string
+ * @constructor
+ */
+
+CmdSetUuid = function ( object, newUuid ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdSetUuid';
+	this.name = 'Update UUID';
+
+	this.object = object;
+
+	this.oldUuid = ( object !== undefined ) ? object.uuid : undefined;
+	this.newUuid = newUuid;
+
+};
+
+CmdSetUuid.prototype = {
+
+	execute: function () {
+
+		this.object.uuid = this.newUuid;
+		this.editor.signals.objectChanged.dispatch( this.object );
+		this.editor.signals.sceneGraphChanged.dispatch();
+
+	},
+
+	undo: function () {
+
+		this.object.uuid = this.oldUuid;
+		this.editor.signals.objectChanged.dispatch( this.object );
+		this.editor.signals.sceneGraphChanged.dispatch();
+
+	},
+
+	toJSON: function () {
+
+		var output = Cmd.prototype.toJSON.call( this );
+
+		output.oldUuid = this.oldUuid;
+		output.newUuid = this.newUuid;
+
+		return output;
+
+	},
+
+	fromJSON: function ( json ) {
+
+		Cmd.prototype.fromJSON.call( this, json );
+
+		this.oldUuid = json.oldUuid;
+		this.newUuid = json.newUuid;
+		this.object = this.editor.objectByUuid( json.oldUuid );
+
+		if ( this.object === undefined ) {
+
+			this.object = this.editor.objectByUuid( json.newUuid );
+
+		}
+
+	}
+
+};

+ 76 - 0
editor/js/CmdSetValue.js

@@ -0,0 +1,76 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param object THREE.Object3D
+ * @param attributeName string
+ * @param newValue number, string, boolean or object
+ * @constructor
+ */
+
+CmdSetValue = function ( object, attributeName, newValue ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdSetValue';
+	this.name = 'Set ' + attributeName;
+	this.updatable = true;
+
+	this.object = object;
+	this.attributeName = attributeName;
+	this.oldValue = ( object !== undefined ) ? object[ attributeName ] : undefined;
+	this.newValue = newValue;
+
+};
+
+CmdSetValue.prototype = {
+
+	execute: function () {
+
+		this.object[ this.attributeName ] = this.newValue;
+		this.editor.signals.objectChanged.dispatch( this.object );
+		this.editor.signals.sceneGraphChanged.dispatch();
+
+	},
+
+	undo: function () {
+
+		this.object[ this.attributeName ] = this.oldValue;
+		this.editor.signals.objectChanged.dispatch( this.object );
+		this.editor.signals.sceneGraphChanged.dispatch();
+
+	},
+
+	update: function ( cmd ) {
+
+		this.newValue = cmd.newValue;
+
+	},
+
+	toJSON: function () {
+
+		var output = Cmd.prototype.toJSON.call( this );
+
+		output.objectUuid = this.object.uuid;
+		output.attributeName = this.attributeName;
+		output.oldValue = this.oldValue;
+		output.newValue = this.newValue;
+
+		return output;
+
+	},
+
+	fromJSON: function ( json ) {
+
+		Cmd.prototype.fromJSON.call( this, json );
+
+		this.attributeName = json.attributeName;
+		this.oldValue = json.oldValue;
+		this.newValue = json.newValue;
+		this.object = this.editor.objectByUuid( json.objectUuid );
+
+	}
+
+};

+ 2 - 0
editor/js/Config.js

@@ -10,6 +10,7 @@ var Config = function () {
 		'autosave': true,
 		'theme': 'css/light.css',
 
+		'project/history/stored': true,
 		'project/renderer': 'WebGLRenderer',
 		'project/renderer/antialias': true,
 		'project/renderer/shadows': true,
@@ -17,6 +18,7 @@ var Config = function () {
 
 		'ui/sidebar/animation/collapsed': true,
 		'ui/sidebar/geometry/collapsed': true,
+		'ui/sidebar/history/collapsed': true,
 		'ui/sidebar/material/collapsed': true,
 		'ui/sidebar/object3d/collapsed': false,
 		'ui/sidebar/project/collapsed': true,

+ 31 - 2
editor/js/Editor.js

@@ -62,7 +62,10 @@ var Editor = function () {
 		fogParametersChanged: new SIGNALS.Signal(),
 		windowResize: new SIGNALS.Signal(),
 
-		showGridChanged: new SIGNALS.Signal()
+		showGridChanged: new SIGNALS.Signal(),
+		refreshSidebarObject3D: new SIGNALS.Signal(),
+		historyChanged: new SIGNALS.Signal(),
+		refreshScriptEditor: new SIGNALS.Signal()
 
 	};
 
@@ -454,6 +457,7 @@ Editor.prototype = {
 
 		this.setScene( loader.parse( json.scene ) );
 		this.scripts = json.scripts;
+		this.history.fromJSON( json.history );
 
 	},
 
@@ -467,10 +471,35 @@ Editor.prototype = {
 			},
 			camera: this.camera.toJSON(),
 			scene: this.scene.toJSON(),
-			scripts: this.scripts
+			scripts: this.scripts,
+			history: this.history.toJSON()
 
 		};
 
+	},
+
+	objectByUuid: function ( uuid ) {
+
+		return this.scene.getObjectByProperty( 'uuid', uuid, true );
+
+	},
+
+	execute: function ( cmd, optionalName ) {
+
+		this.history.execute( cmd, optionalName );
+
+	},
+
+	undo: function () {
+
+		this.history.undo();
+
+	},
+
+	redo: function () {
+
+		this.history.redo();
+
 	}
 
 }

+ 277 - 35
editor/js/History.js

@@ -1,34 +1,36 @@
 /**
- * @author mrdoob / http://mrdoob.com/
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
  */
 
-var History = function ( editor ) {
+History = function ( editor ) {
 
-	this.array = [];
-	this.arrayLength = -1;
+	this.editor = editor;
+	this.undos = [];
+	this.redos = [];
+	this.lastCmdTime = new Date();
+	this.idCounter = 0;
 
-	this.current = -1;
-	this.isRecording = true;
+	this.historyDisabled = false;
+	this.config = editor.config;
 
-	//
+	//Set editor-reference in Cmd
+
+	Cmd( editor );
+
+	// signals
 
 	var scope = this;
-	var signals = editor.signals;
 
-	signals.objectAdded.add( function ( object ) {
+	this.editor.signals.startPlayer.add( function () {
 
-		if ( scope.isRecording === false ) return;
+		scope.historyDisabled = true;
 
-		scope.add(
-			function () {
-				editor.removeObject( object );
-				editor.select( null );
-			},
-			function () {
-				editor.addObject( object );
-				editor.select( object );
-			}
-		);
+	} );
+
+	this.editor.signals.stopPlayer.add( function () {
+
+		scope.historyDisabled = false;
 
 	} );
 
@@ -36,45 +38,285 @@ var History = function ( editor ) {
 
 History.prototype = {
 
-	add: function ( undo, redo ) {
+	execute: function ( cmd, optionalName ) {
+
+		var lastCmd = this.undos[ this.undos.length - 1 ];
+		var timeDifference = new Date().getTime() - this.lastCmdTime.getTime();
+
+		var isUpdatableCmd = lastCmd &&
+			lastCmd.updatable &&
+			cmd.updatable &&
+			lastCmd.object === cmd.object &&
+			lastCmd.type === cmd.type &&
+			lastCmd.script === cmd.script &&
+			lastCmd.attributeName === cmd.attributeName;
+
+		if ( isUpdatableCmd && cmd.type === "CmdSetScriptValue" ) {
+
+			// When the cmd.type is "CmdSetScriptValue" the timeDifference is ignored
+
+			lastCmd.update( cmd );
+			cmd = lastCmd;
+
+		} else if ( isUpdatableCmd && timeDifference < 500 ) {
+
+			lastCmd.update( cmd );
+			cmd = lastCmd;
+
+		} else {
 
-		this.current ++;
+			// the command is not updatable and is added as a new part of the history
 
-		this.array[ this.current ] = { undo: undo, redo: redo };
-		this.arrayLength = this.current;
+			this.undos.push( cmd );
+			cmd.id = ++ this.idCounter;
+
+		}
+		cmd.name = ( optionalName !== undefined ) ? optionalName : cmd.name;
+		cmd.execute();
+		cmd.inMemory = true;
+
+		if ( this.config.getKey( 'project/history/stored' ) ) {
+
+			cmd.json = cmd.toJSON();	// serialize the cmd immediately after execution and append the json to the cmd
+
+		}
+		this.lastCmdTime = new Date();
+
+		// clearing all the redo-commands
+
+		this.redos = [];
+		this.editor.signals.historyChanged.dispatch( cmd );
 
 	},
 
 	undo: function () {
 
-		if ( this.current < 0 ) return;
+		if ( this.historyDisabled ) {
+
+			alert( "Undo/Redo disabled while scene is playing." );
+			return;
+
+		}
+
+		var cmd = undefined;
+
+		if ( this.undos.length > 0 ) {
+
+			cmd = this.undos.pop();
+
+			if ( cmd.inMemory === false ) {
+
+				cmd.fromJSON( cmd.json );
+
+			}
+
+		}
 
-		this.isRecording = false;
+		if ( cmd !== undefined ) {
 
-		this.array[ this.current -- ].undo();
+			cmd.undo();
+			this.redos.push( cmd );
+			this.editor.signals.historyChanged.dispatch( cmd );
 
-		this.isRecording = true;
+		}
+
+		return cmd;
 
 	},
 
 	redo: function () {
 
-		if ( this.current === this.arrayLength ) return;
+		if ( this.historyDisabled ) {
+
+			alert( "Undo/Redo disabled while scene is playing." );
+			return;
+
+		}
+
+		var cmd = undefined;
+
+		if ( this.redos.length > 0 ) {
+
+			cmd = this.redos.pop();
+
+			if ( cmd.inMemory === false ) {
+
+				cmd.fromJSON( cmd.json );
+
+			}
+
+		}
+
+		if ( cmd !== undefined ) {
+
+			cmd.execute();
+			this.undos.push( cmd );
+			this.editor.signals.historyChanged.dispatch( cmd );
+
+		}
+
+		return cmd;
+
+	},
+
+	toJSON: function () {
+
+		var history = {};
+		history.undos = [];
+		history.redos = [];
+
+		if ( ! this.config.getKey( 'project/history/stored' ) ) {
+
+			return history;
+
+		}
+
+		// Append Undos to History
+
+		for ( var i = 0 ; i < this.undos.length; i ++ ) {
+
+			if ( this.undos[ i ].hasOwnProperty( "json" ) ) {
+
+				history.undos.push( this.undos[ i ].json );
 
-		this.isRecording = false;
+			}
+
+		}
+
+		// Append Redos to History
+
+		for ( var i = 0 ; i < this.redos.length; i ++ ) {
+
+			if ( this.redos[ i ].hasOwnProperty( "json" ) ) {
+
+				history.redos.push( this.redos[ i ].json );
+
+			}
+
+		}
+
+		return history;
+
+	},
+
+	fromJSON: function ( json ) {
+
+		if ( json === undefined ) return;
 
-		this.array[ ++ this.current ].redo();
+		for ( var i = 0; i < json.undos.length; i ++ ) {
 
-		this.isRecording = true;
+			var cmdJSON = json.undos[ i ];
+			var cmd = new window[ cmdJSON.type ]();	// creates a new object of type "json.type"
+			cmd.json = cmdJSON;
+			cmd.id = cmdJSON.id;
+			cmd.name = cmdJSON.name;
+			this.undos.push( cmd );
+			this.idCounter = ( cmdJSON.id > this.idCounter ) ? cmdJSON.id : this.idCounter; // set last used idCounter
+
+		}
+
+		for ( var i = 0; i < json.redos.length; i ++ ) {
+
+			var cmdJSON = json.redos[ i ];
+			var cmd = new window[ cmdJSON.type ]();	// creates a new object of type "json.type"
+			cmd.json = cmdJSON;
+			cmd.id = cmdJSON.id;
+			cmd.name = cmdJSON.name;
+			this.redos.push( cmd );
+			this.idCounter = ( cmdJSON.id > this.idCounter ) ? cmdJSON.id : this.idCounter; // set last used idCounter
+
+		}
+
+		// Select the last executed undo-command
+		this.editor.signals.historyChanged.dispatch( this.undos[ this.undos.length - 1 ] );
 
 	},
 
 	clear: function () {
 
-		this.array = [];
-		this.arrayLength = -1;
+		this.undos = [];
+		this.redos = [];
+		this.idCounter = 0;
+
+		this.editor.signals.historyChanged.dispatch();
+
+	},
+
+	goToState: function ( id ) {
+
+		if ( this.historyDisabled ) {
+
+			alert( "Undo/Redo disabled while scene is playing." );
+			return;
+
+		}
+
+		this.editor.signals.sceneGraphChanged.active = false;
+		this.editor.signals.historyChanged.active = false;
+
+		var cmd = this.undos.length > 0 ? this.undos[ this.undos.length - 1 ] : undefined;	// next cmd to pop
+
+		if ( cmd === undefined || id > cmd.id ) {
+
+			cmd = this.redo();
+			while ( cmd !== undefined && id > cmd.id ) {
+
+				cmd = this.redo();
+
+			}
+
+		} else {
+
+			while ( true ) {
+
+				cmd = this.undos[ this.undos.length - 1 ];	// next cmd to pop
+
+				if ( cmd === undefined || id === cmd.id ) break;
+
+				cmd = this.undo();
+
+			}
+
+		}
+
+		this.editor.signals.sceneGraphChanged.active = true;
+		this.editor.signals.historyChanged.active = true;
+
+		this.editor.signals.sceneGraphChanged.dispatch();
+		this.editor.signals.historyChanged.dispatch( cmd );
+
+	},
+
+	enableSerialization: function ( id ) {
+
+		/**
+		 * because there might be commands in this.undos and this.redos
+		 * which have not been serialized with .toJSON() we go back
+		 * to the oldest command and redo one command after the other
+		 * while also calling .toJSON() on them.
+		 */
+
+		this.goToState( - 1 );
+
+		this.editor.signals.sceneGraphChanged.active = false;
+		this.editor.signals.historyChanged.active = false;
+
+		var cmd = this.redo();
+		while ( cmd !== undefined ) {
+
+			if ( ! cmd.hasOwnProperty( "json" ) ) {
+
+				cmd.json = cmd.toJSON();
+
+			}
+			cmd = this.redo();
+
+		}
+
+		this.editor.signals.sceneGraphChanged.active = true;
+		this.editor.signals.historyChanged.active = true;
 
-		this.current = -1;
+		this.goToState( id );
 
 	}
 

+ 19 - 33
editor/js/Loader.js

@@ -24,8 +24,7 @@ var Loader = function ( editor ) {
 					var loader = new THREE.AMFLoader();
 					var amfobject = loader.parse( event.target.result );
 
-					editor.addObject( amfobject );
-					editor.select( amfobject );
+					editor.execute( new CmdAddObject( amfobject ) );
 
 				}, false );
 				reader.readAsArrayBuffer( file );
@@ -40,7 +39,7 @@ var Loader = function ( editor ) {
 					var loader = new THREE.AWDLoader();
 					var scene = loader.parse( event.target.result );
 
-					editor.setScene( scene );
+					editor.execute( new CmdSetScene( scene ) );
 
 				}, false );
 				reader.readAsArrayBuffer( file );
@@ -58,7 +57,7 @@ var Loader = function ( editor ) {
 					var loader = new THREE.BabylonLoader();
 					var scene = loader.parse( json );
 
-					editor.setScene( scene );
+					editor.execute( new CmdSetScene( scene ) );
 
 				}, false );
 				reader.readAsText( file );
@@ -81,8 +80,7 @@ var Loader = function ( editor ) {
 					var mesh = new THREE.Mesh( geometry, material );
 					mesh.name = filename;
 
-					editor.addObject( mesh );
-					editor.select( mesh );
+					editor.execute( new CmdAddObject( mesh ) );
 
 				}, false );
 				reader.readAsText( file );
@@ -110,8 +108,7 @@ var Loader = function ( editor ) {
 						var mesh = new THREE.Mesh( geometry, material );
 						mesh.name = filename;
 
-						editor.addObject( mesh );
-						editor.select( mesh );
+						editor.execute( new CmdAddObject( mesh ) );
 
 					} );
 
@@ -132,8 +129,7 @@ var Loader = function ( editor ) {
 
 					collada.scene.name = filename;
 
-					editor.addObject( collada.scene );
-					editor.select( collada.scene );
+					editor.execute( new CmdAddObject( collada.scene ) );
 
 				}, false );
 				reader.readAsText( file );
@@ -208,8 +204,7 @@ var Loader = function ( editor ) {
 
 						collada.scene.name = filename;
 
-						editor.addObject( collada.scene );
-						editor.select( collada.scene );
+						editor.execute( new CmdAddObject( collada.scene ) );
 
 					}, false );
 					reader.readAsArrayBuffer( file );
@@ -233,8 +228,7 @@ var Loader = function ( editor ) {
 						mesh.mixer = new THREE.AnimationMixer( mesh )
 						mesh.name = filename;
 
-						editor.addObject( mesh );
-						editor.select( mesh );
+						editor.execute( new CmdAddObject( mesh ) );
 
 					}, false );
 					reader.readAsArrayBuffer( file );
@@ -251,8 +245,7 @@ var Loader = function ( editor ) {
 					var object = new THREE.OBJLoader().parse( contents );
 					object.name = filename;
 
-					editor.addObject( object );
-					editor.select( object );
+					editor.execute( new CmdAddObject( object ) );
 
 				}, false );
 				reader.readAsText( file );
@@ -275,8 +268,7 @@ var Loader = function ( editor ) {
 					var mesh = new THREE.Mesh( geometry, material );
 					mesh.name = filename;
 
-					editor.addObject( mesh );
-					editor.select( mesh );
+					editor.execute( new CmdAddObject( mesh ) );
 
 				}, false );
 				reader.readAsText( file );
@@ -299,8 +291,7 @@ var Loader = function ( editor ) {
 					var mesh = new THREE.Mesh( geometry, material );
 					mesh.name = filename;
 
-					editor.addObject( mesh );
-					editor.select( mesh );
+					editor.execute( new CmdAddObject( mesh ) );
 
 				}, false );
 
@@ -329,8 +320,7 @@ var Loader = function ( editor ) {
 
 					var mesh = new THREE.Mesh( geometry, material );
 
-					editor.addObject( mesh );
-					editor.select( mesh );
+					editor.execute( new CmdAddObject( mesh ) );
 
 				}, false );
 				reader.readAsBinaryString( file );
@@ -354,8 +344,7 @@ var Loader = function ( editor ) {
 					var mesh = new THREE.Mesh( geometry, material );
 					mesh.name = filename;
 
-					editor.addObject( mesh );
-					editor.select( mesh );
+					editor.execute( new CmdAddObject( mesh ) );
 
 				}, false );
 				reader.readAsText( file );
@@ -371,7 +360,7 @@ var Loader = function ( editor ) {
 
 					var result = new THREE.VRMLLoader().parse( contents );
 
-					editor.setScene( result );
+					editor.execute( new CmdSetScene( result ) );
 
 				}, false );
 				reader.readAsText( file );
@@ -415,8 +404,7 @@ var Loader = function ( editor ) {
 
 			var mesh = new THREE.Mesh( result );
 
-			editor.addObject( mesh );
-			editor.select( mesh );
+			editor.execute( new CmdAddObject( mesh ) );
 
 		} else if ( data.metadata.type.toLowerCase() === 'geometry' ) {
 
@@ -463,8 +451,7 @@ var Loader = function ( editor ) {
 
 			mesh.name = filename;
 
-			editor.addObject( mesh );
-			editor.select( mesh );
+			editor.execute( new CmdAddObject( mesh ) );
 
 		} else if ( data.metadata.type.toLowerCase() === 'object' ) {
 
@@ -475,12 +462,11 @@ var Loader = function ( editor ) {
 
 			if ( result instanceof THREE.Scene ) {
 
-				editor.setScene( result );
+				editor.execute( new CmdSetScene( result ) );
 
 			} else {
 
-				editor.addObject( result );
-				editor.select( result );
+				editor.execute( new CmdAddObject( result ) );
 
 			}
 
@@ -491,7 +477,7 @@ var Loader = function ( editor ) {
 			var loader = new THREE.SceneLoader();
 			loader.parse( data, function ( result ) {
 
-				editor.setScene( result.scene );
+				editor.execute( new CmdSetScene( result.scene ) );
 
 			}, '' );
 

+ 16 - 32
editor/js/Menubar.Add.js

@@ -40,8 +40,7 @@ Menubar.Add = function ( editor ) {
 		var mesh = new THREE.Group();
 		mesh.name = 'Group ' + ( ++ meshCount );
 
-		editor.addObject( mesh );
-		editor.select( mesh );
+		editor.execute( new CmdAddObject( mesh ) );
 
 	} );
 	options.add( option );
@@ -68,8 +67,7 @@ Menubar.Add = function ( editor ) {
 		var mesh = new THREE.Mesh( geometry, material );
 		mesh.name = 'Plane ' + ( ++ meshCount );
 
-		editor.addObject( mesh );
-		editor.select( mesh );
+		editor.execute( new CmdAddObject( mesh ) );
 
 	} );
 	options.add( option );
@@ -93,8 +91,7 @@ Menubar.Add = function ( editor ) {
 		var mesh = new THREE.Mesh( geometry, new THREE.MeshPhongMaterial() );
 		mesh.name = 'Box ' + ( ++ meshCount );
 
-		editor.addObject( mesh );
-		editor.select( mesh );
+		editor.execute( new CmdAddObject( mesh ) );
 
 	} );
 	options.add( option );
@@ -113,8 +110,7 @@ Menubar.Add = function ( editor ) {
 		var mesh = new THREE.Mesh( geometry, new THREE.MeshPhongMaterial() );
 		mesh.name = 'Circle ' + ( ++ meshCount );
 
-		editor.addObject( mesh );
-		editor.select( mesh );
+		editor.execute( new CmdAddObject( mesh ) );
 
 	} );
 	options.add( option );
@@ -137,8 +133,7 @@ Menubar.Add = function ( editor ) {
 		var mesh = new THREE.Mesh( geometry, new THREE.MeshPhongMaterial() );
 		mesh.name = 'Cylinder ' + ( ++ meshCount );
 
-		editor.addObject( mesh );
-		editor.select( mesh );
+		editor.execute( new CmdAddObject( mesh ) );
 
 	} );
 	options.add( option );
@@ -162,8 +157,7 @@ Menubar.Add = function ( editor ) {
 		var mesh = new THREE.Mesh( geometry, new THREE.MeshPhongMaterial() );
 		mesh.name = 'Sphere ' + ( ++ meshCount );
 
-		editor.addObject( mesh );
-		editor.select( mesh );
+		editor.execute( new CmdAddObject( mesh ) );
 
 	} );
 	options.add( option );
@@ -182,8 +176,7 @@ Menubar.Add = function ( editor ) {
 		var mesh = new THREE.Mesh( geometry, new THREE.MeshPhongMaterial() );
 		mesh.name = 'Icosahedron ' + ( ++ meshCount );
 
-		editor.addObject( mesh );
-		editor.select( mesh );
+		editor.execute( new CmdAddObject( mesh ) );
 
 	} );
 	options.add( option );
@@ -205,8 +198,7 @@ Menubar.Add = function ( editor ) {
 		var mesh = new THREE.Mesh( geometry, new THREE.MeshPhongMaterial() );
 		mesh.name = 'Torus ' + ( ++ meshCount );
 
-		editor.addObject( mesh );
-		editor.select( mesh );
+		editor.execute( new CmdAddObject( mesh ) );
 
 	} );
 	options.add( option );
@@ -230,8 +222,7 @@ Menubar.Add = function ( editor ) {
 		var mesh = new THREE.Mesh( geometry, new THREE.MeshPhongMaterial() );
 		mesh.name = 'TorusKnot ' + ( ++ meshCount );
 
-		editor.addObject( mesh );
-		editor.select( mesh );
+		editor.execute( new CmdAddObject( mesh ) );
 
 	} );
 	options.add( option );
@@ -276,8 +267,7 @@ Menubar.Add = function ( editor ) {
 		var sprite = new THREE.Sprite( new THREE.SpriteMaterial() );
 		sprite.name = 'Sprite ' + ( ++ meshCount );
 
-		editor.addObject( sprite );
-		editor.select( sprite );
+		editor.execute( new CmdAddObject( sprite ) );
 
 	} );
 	options.add( option );
@@ -300,8 +290,7 @@ Menubar.Add = function ( editor ) {
 		var light = new THREE.PointLight( color, intensity, distance );
 		light.name = 'PointLight ' + ( ++ lightCount );
 
-		editor.addObject( light );
-		editor.select( light );
+		editor.execute( new CmdAddObject( light ) );
 
 	} );
 	options.add( option );
@@ -325,8 +314,7 @@ Menubar.Add = function ( editor ) {
 
 		light.position.set( 0.5, 1, 0.75 ).multiplyScalar( 200 );
 
-		editor.addObject( light );
-		editor.select( light );
+		editor.execute( new CmdAddObject( light ) );
 
 	} );
 	options.add( option );
@@ -347,8 +335,7 @@ Menubar.Add = function ( editor ) {
 
 		light.position.set( 0.5, 1, 0.75 ).multiplyScalar( 200 );
 
-		editor.addObject( light );
-		editor.select( light );
+		editor.execute( new CmdAddObject( light ) );
 
 	} );
 	options.add( option );
@@ -369,8 +356,7 @@ Menubar.Add = function ( editor ) {
 
 		light.position.set( 0.5, 1, 0.75 ).multiplyScalar( 200 );
 
-		editor.addObject( light );
-		editor.select( light );
+		editor.execute( new CmdAddObject( light ) );
 
 	} );
 	options.add( option );
@@ -387,8 +373,7 @@ Menubar.Add = function ( editor ) {
 		var light = new THREE.AmbientLight( color );
 		light.name = 'AmbientLight ' + ( ++ lightCount );
 
-		editor.addObject( light );
-		editor.select( light );
+		editor.execute( new CmdAddObject( light ) );
 
 	} );
 	options.add( option );
@@ -407,8 +392,7 @@ Menubar.Add = function ( editor ) {
 		var camera = new THREE.PerspectiveCamera( 50, 1, 1, 10000 );
 		camera.name = 'PerspectiveCamera ' + ( ++ cameraCount );
 
-		editor.addObject( camera );
-		editor.select( camera );
+		editor.execute( new CmdAddObject( camera ) );
 
 	} );
 	options.add( option );

+ 59 - 14
editor/js/Menubar.Edit.js

@@ -18,28 +18,66 @@ Menubar.Edit = function ( editor ) {
 
 	// Undo
 
-	var option = new UI.Panel();
-	option.setClass( 'option' );
-	option.setTextContent( 'Undo' );
-	option.onClick( function () {
+	var undo = new UI.Panel();
+	undo.setClass( 'option' );
+	undo.setTextContent( 'Undo (Ctrl+Z)' );
+	undo.onClick( function () {
 
-		editor.history.undo();
+		editor.undo();
 
 	} );
-	options.add( option );
+	options.add( undo );
 
 	// Redo
 
+	var redo = new UI.Panel();
+	redo.setClass( 'option' );
+	redo.setTextContent( 'Redo (Ctrl+Shift+Z)' );
+	redo.onClick( function () {
+
+		editor.redo();
+
+	} );
+	options.add( redo );
+
+	// Clear History
+
 	var option = new UI.Panel();
 	option.setClass( 'option' );
-	option.setTextContent( 'Redo' );
+	option.setTextContent( 'Clear History' );
 	option.onClick( function () {
 
-		editor.history.redo();
+		if ( confirm( 'The Undo/Redo History will be cleared. Are you sure?' ) ) {
+
+			editor.history.clear();
+
+		}
 
 	} );
 	options.add( option );
 
+
+	editor.signals.historyChanged.add( function () {
+
+		var history = editor.history;
+
+		undo.setClass( 'option' );
+		redo.setClass( 'option' );
+
+		if ( history.undos.length == 0 ) {
+
+			undo.setClass( 'inactive' );
+
+		}
+
+		if ( history.redos.length == 0 ) {
+
+			redo.setClass( 'inactive' );
+
+		}
+
+	} );
+
 	// ---
 
 	options.add( new UI.HorizontalRule() );
@@ -57,8 +95,7 @@ Menubar.Edit = function ( editor ) {
 
 		object = object.clone();
 
-		editor.addObject( object );
-		editor.select( object );
+		editor.execute( new CmdAddObject( object ) );
 
 	} );
 	options.add( option );
@@ -75,8 +112,9 @@ Menubar.Edit = function ( editor ) {
 		if ( confirm( 'Delete ' + object.name + '?' ) === false ) return;
 
 		var parent = object.parent;
-		editor.removeObject( object );
-		editor.select( parent );
+		if ( parent === undefined ) return; // avoid deleting the camera or scene
+
+		editor.execute( new CmdRemoveObject( object ) );
 
 	} );
 	options.add( option );
@@ -108,6 +146,7 @@ Menubar.Edit = function ( editor ) {
 
 		}
 
+		var cmds = [];
 		root.traverse( function ( object ) {
 
 			var material = object.material;
@@ -119,8 +158,8 @@ Menubar.Edit = function ( editor ) {
 					var shader = glslprep.minifyGlsl( [
 							material.vertexShader, material.fragmentShader ] );
 
-					material.vertexShader = shader[ 0 ];
-					material.fragmentShader = shader[ 1 ];
+					cmds.push( new CmdSetMaterialValue( object, 'vertexShader', shader[ 0 ] ) );
+					cmds.push( new CmdSetMaterialValue( object, 'fragmentShader', shader[ 1 ] ) );
 
 					++nMaterialsChanged;
 
@@ -148,6 +187,12 @@ Menubar.Edit = function ( editor ) {
 
 		} );
 
+		if ( nMaterialsChanged > 0 ) {
+
+			editor.execute( new CmdMultiCmds( cmds ), 'Minify Shaders' );
+
+		}
+
 		window.alert( nMaterialsChanged +
 				" material(s) were changed.\n" + errors.join( "\n" ) );
 

+ 66 - 17
editor/js/Script.js

@@ -80,20 +80,39 @@ var Script = function ( editor ) {
 
 			if ( typeof( currentScript ) === 'object' ) {
 
-				currentScript.source = value;
-				signals.scriptChanged.dispatch( currentScript );
+				if ( value !== currentScript.source ) {
+
+					editor.execute( new CmdSetScriptValue( currentObject, currentScript, 'source', value, codemirror.getCursor() ) );
+
+				}
 				return;
 			}
 
 			if ( currentScript !== 'programInfo' ) return;
 
 			var json = JSON.parse( value );
-			currentObject.defines = json.defines;
-			currentObject.uniforms = json.uniforms;
-			currentObject.attributes = json.attributes;
 
-			currentObject.needsUpdate = true;
-			signals.materialChanged.dispatch( currentObject );
+			if ( JSON.stringify( currentObject.material.defines ) !== JSON.stringify( json.defines ) ) {
+
+				var cmd = new CmdSetMaterialValue( currentObject, 'defines', json.defines );
+				cmd.updatable = false;
+				editor.execute( cmd );
+
+			}
+			if ( JSON.stringify( currentObject.material.uniforms ) !== JSON.stringify( json.uniforms ) ) {
+
+				var cmd = new CmdSetMaterialValue( currentObject, 'uniforms', json.uniforms );
+				cmd.updatable = false;
+				editor.execute( cmd );
+
+			}
+			if ( JSON.stringify( currentObject.material.attributes ) !== JSON.stringify( json.attributes ) ) {
+
+				var cmd = new CmdSetMaterialValue( currentObject, 'attributes', json.attributes );
+				cmd.updatable = false;
+				editor.execute( cmd );
+
+			}
 
 		}, 300 );
 
@@ -222,9 +241,9 @@ var Script = function ( editor ) {
 					if ( errors.length !== 0 ) break;
 					if ( renderer instanceof THREE.WebGLRenderer === false ) break;
 
-					currentObject[ currentScript ] = string;
-					currentObject.needsUpdate = true;
-					signals.materialChanged.dispatch( currentObject );
+					currentObject.material[ currentScript ] = string;
+					currentObject.material.needsUpdate = true;
+					signals.materialChanged.dispatch( currentObject.material );
 
 					var programs = renderer.info.programs;
 
@@ -236,7 +255,7 @@ var Script = function ( editor ) {
 						var diagnostics = programs[i].diagnostics;
 
 						if ( diagnostics === undefined ||
-								diagnostics.material !== currentObject ) continue;
+								diagnostics.material !== currentObject.material ) continue;
 
 						if ( ! diagnostics.runnable ) valid = false;
 
@@ -342,6 +361,7 @@ var Script = function ( editor ) {
 			mode = 'javascript';
 			name = script.name;
 			source = script.source;
+			title.setValue( object.name + ' / ' + name );
 
 		} else {
 
@@ -351,7 +371,7 @@ var Script = function ( editor ) {
 
 					mode = 'glsl';
 					name = 'Vertex Shader';
-					source = object.vertexShader || "";
+					source = object.material.vertexShader || "";
 
 					break;
 
@@ -359,7 +379,7 @@ var Script = function ( editor ) {
 
 					mode = 'glsl';
 					name = 'Fragment Shader';
-					source = object.fragmentShader || "";
+					source = object.material.fragmentShader || "";
 
 					break;
 
@@ -368,13 +388,14 @@ var Script = function ( editor ) {
 					mode = 'json';
 					name = 'Program Properties';
 					var json = {
-						defines: object.defines,
-						uniforms: object.uniforms,
-						attributes: object.attributes
+						defines: object.material.defines,
+						uniforms: object.material.uniforms,
+						attributes: object.material.attributes
 					};
 					source = JSON.stringify( json, null, '\t' );
 
 			}
+			title.setValue( object.material.name + ' / ' + name );
 
 		}
 
@@ -382,7 +403,6 @@ var Script = function ( editor ) {
 		currentScript = script;
 		currentObject = object;
 
-		title.setValue( object.name + ' / ' + name );
 		container.setDisplay( '' );
 		codemirror.setValue( source );
 		if (mode === 'json' ) mode = { name: 'javascript', json: true };
@@ -390,6 +410,35 @@ var Script = function ( editor ) {
 
 	} );
 
+	signals.scriptRemoved.add( function ( script ) {
+
+		if ( currentScript === script ) {
+
+			container.setDisplay( 'none' );
+
+		}
+
+	} );
+
+	signals.refreshScriptEditor.add( function ( object, script, cursorPosition ) {
+
+		if ( currentScript !== script ) return;
+
+		// copying the codemirror history because "codemirror.setValue(...)" alters its history
+
+		var history = codemirror.getHistory();
+		title.setValue( object.name + ' / ' + script.name );
+		codemirror.setValue( script.source );
+
+		if ( cursorPosition !== undefined ) {
+
+			codemirror.setCursor( cursorPosition );
+
+		}
+		codemirror.setHistory( history ); // setting the history to previous state
+
+	} );
+
 	return container;
 
 };

+ 5 - 9
editor/js/Sidebar.Geometry.BoxGeometry.js

@@ -2,7 +2,9 @@
  * @author mrdoob / http://mrdoob.com/
  */
 
-Sidebar.Geometry.BoxGeometry = function ( signals, object ) {
+Sidebar.Geometry.BoxGeometry = function ( editor, object ) {
+
+	var signals = editor.signals;
 
 	var container = new UI.Panel();
 
@@ -72,20 +74,14 @@ Sidebar.Geometry.BoxGeometry = function ( signals, object ) {
 
 	function update() {
 
-		object.geometry.dispose();
-
-		object.geometry = new THREE.BoxGeometry(
+		editor.execute( new CmdSetGeometry( object, new THREE.BoxGeometry(
 			width.getValue(),
 			height.getValue(),
 			depth.getValue(),
 			widthSegments.getValue(),
 			heightSegments.getValue(),
 			depthSegments.getValue()
-		);
-
-		object.geometry.computeBoundingSphere();
-
-		signals.geometryChanged.dispatch( object );
+		) ) );
 
 	}
 

+ 3 - 1
editor/js/Sidebar.Geometry.BufferGeometry.js

@@ -2,7 +2,9 @@
  * @author mrdoob / http://mrdoob.com/
  */
 
-Sidebar.Geometry.BufferGeometry = function ( signals ) {
+Sidebar.Geometry.BufferGeometry = function ( editor ) {
+
+	var signals = editor.signals;
 
 	var container = new UI.Panel();
 

+ 5 - 7
editor/js/Sidebar.Geometry.CircleGeometry.js

@@ -2,7 +2,9 @@
  * @author mrdoob / http://mrdoob.com/
  */
 
-Sidebar.Geometry.CircleGeometry = function ( signals, object ) {
+Sidebar.Geometry.CircleGeometry = function ( editor, object ) {
+
+	var signals = editor.signals;
 
 	var container = new UI.Panel();
 
@@ -52,16 +54,12 @@ Sidebar.Geometry.CircleGeometry = function ( signals, object ) {
 
 	function update() {
 
-		object.geometry.dispose();
-
-		object.geometry = new THREE.CircleGeometry(
+		editor.execute( new CmdSetGeometry( object, new THREE.CircleGeometry(
 			radius.getValue(),
 			segments.getValue(),
 			thetaStart.getValue(),
 			thetaLength.getValue()
-		);
-
-		signals.geometryChanged.dispatch( object );
+		) ) );
 
 	}
 

+ 5 - 9
editor/js/Sidebar.Geometry.CylinderGeometry.js

@@ -2,7 +2,9 @@
  * @author mrdoob / http://mrdoob.com/
  */
 
-Sidebar.Geometry.CylinderGeometry = function ( signals, object ) {
+Sidebar.Geometry.CylinderGeometry = function ( editor, object ) {
+
+	var signals = editor.signals;
 
 	var container = new UI.Panel();
 
@@ -72,20 +74,14 @@ Sidebar.Geometry.CylinderGeometry = function ( signals, object ) {
 
 	function update() {
 
-		object.geometry.dispose();
-
-		object.geometry = new THREE.CylinderGeometry(
+		editor.execute( new CmdSetGeometry( object, new THREE.CylinderGeometry(
 			radiusTop.getValue(),
 			radiusBottom.getValue(),
 			height.getValue(),
 			radialSegments.getValue(),
 			heightSegments.getValue(),
 			openEnded.getValue()
-		);
-
-		object.geometry.computeBoundingSphere();
-
-		signals.geometryChanged.dispatch( object );
+		) ) );
 
 	}
 

+ 3 - 1
editor/js/Sidebar.Geometry.Geometry.js

@@ -2,7 +2,9 @@
  * @author mrdoob / http://mrdoob.com/
  */
 
-Sidebar.Geometry.Geometry = function ( signals ) {
+Sidebar.Geometry.Geometry = function ( editor ) {
+
+	var signals = editor.signals;
 
 	var container = new UI.Panel();
 

+ 5 - 7
editor/js/Sidebar.Geometry.IcosahedronGeometry.js

@@ -2,7 +2,9 @@
  * @author mrdoob / http://mrdoob.com/
  */
 
-Sidebar.Geometry.IcosahedronGeometry = function ( signals, object ) {
+Sidebar.Geometry.IcosahedronGeometry = function ( editor, object ) {
+
+	var signals = editor.signals;
 
 	var container = new UI.Panel();
 
@@ -33,14 +35,10 @@ Sidebar.Geometry.IcosahedronGeometry = function ( signals, object ) {
 
 	function update() {
 
-		object.geometry.dispose();
-
-		object.geometry = new THREE.IcosahedronGeometry(
+		editor.execute( new CmdSetGeometry( object, new THREE.IcosahedronGeometry(
 			radius.getValue(),
 			detail.getValue()
-		);
-
-		object.geometry.computeBoundingSphere();
+		) ) );
 
 		signals.objectChanged.dispatch( object );
 

+ 3 - 1
editor/js/Sidebar.Geometry.Modifiers.js

@@ -2,7 +2,9 @@
  * @author mrdoob / http://mrdoob.com/
  */
 
-Sidebar.Geometry.Modifiers = function ( signals, object ) {
+Sidebar.Geometry.Modifiers = function ( editor, object ) {
+
+	var signals = editor.signals;
 
 	var container = new UI.Panel().setPaddingLeft( '90px' );
 

+ 5 - 9
editor/js/Sidebar.Geometry.PlaneGeometry.js

@@ -2,7 +2,9 @@
  * @author mrdoob / http://mrdoob.com/
  */
 
-Sidebar.Geometry.PlaneGeometry = function ( signals, object ) {
+Sidebar.Geometry.PlaneGeometry = function ( editor, object ) {
+
+	var signals = editor.signals;
 
 	var container = new UI.Panel();
 
@@ -52,19 +54,13 @@ Sidebar.Geometry.PlaneGeometry = function ( signals, object ) {
 	//
 
 	function update() {
-		
-		object.geometry.dispose();
 
-		object.geometry = new THREE.PlaneGeometry(
+		editor.execute( new CmdSetGeometry( object, new THREE.PlaneGeometry(
 			width.getValue(),
 			height.getValue(),
 			widthSegments.getValue(),
 			heightSegments.getValue()
-		);
-
-		object.geometry.computeBoundingSphere();
-
-		signals.geometryChanged.dispatch( object );
+		) ) );
 
 	}
 

+ 5 - 9
editor/js/Sidebar.Geometry.SphereGeometry.js

@@ -2,7 +2,9 @@
  * @author mrdoob / http://mrdoob.com/
  */
 
-Sidebar.Geometry.SphereGeometry = function ( signals, object ) {
+Sidebar.Geometry.SphereGeometry = function ( editor, object ) {
+
+	var signals = editor.signals;
 
 	var container = new UI.Panel();
 
@@ -83,9 +85,7 @@ Sidebar.Geometry.SphereGeometry = function ( signals, object ) {
 
 	function update() {
 
-		object.geometry.dispose();
-
-		object.geometry = new THREE.SphereGeometry(
+		editor.execute( new CmdSetGeometry( object, new THREE.SphereGeometry(
 			radius.getValue(),
 			widthSegments.getValue(),
 			heightSegments.getValue(),
@@ -93,11 +93,7 @@ Sidebar.Geometry.SphereGeometry = function ( signals, object ) {
 			phiLength.getValue(),
 			thetaStart.getValue(),
 			thetaLength.getValue()
-		);
-
-		object.geometry.computeBoundingSphere();
-
-		signals.geometryChanged.dispatch( object );
+		) ) );
 
 	}
 

+ 5 - 9
editor/js/Sidebar.Geometry.TorusGeometry.js

@@ -2,7 +2,9 @@
  * @author mrdoob / http://mrdoob.com/
  */
 
-Sidebar.Geometry.TorusGeometry = function ( signals, object ) {
+Sidebar.Geometry.TorusGeometry = function ( editor, object ) {
+
+	var signals = editor.signals;
 
 	var container = new UI.Panel();
 
@@ -63,19 +65,13 @@ Sidebar.Geometry.TorusGeometry = function ( signals, object ) {
 
 	function update() {
 
-		object.geometry.dispose();
-
-		object.geometry = new THREE.TorusGeometry(
+		editor.execute( new CmdSetGeometry( object, new THREE.TorusGeometry(
 			radius.getValue(),
 			tube.getValue(),
 			radialSegments.getValue(),
 			tubularSegments.getValue(),
 			arc.getValue()
-		);
-
-		object.geometry.computeBoundingSphere();
-
-		signals.geometryChanged.dispatch( object );
+		) ) );
 
 	}
 

+ 5 - 9
editor/js/Sidebar.Geometry.TorusKnotGeometry.js

@@ -2,7 +2,9 @@
  * @author mrdoob / http://mrdoob.com/
  */
 
-Sidebar.Geometry.TorusKnotGeometry = function ( signals, object ) {
+Sidebar.Geometry.TorusKnotGeometry = function ( editor, object ) {
+
+	var signals = editor.signals;
 
 	var container = new UI.Panel();
 
@@ -83,9 +85,7 @@ Sidebar.Geometry.TorusKnotGeometry = function ( signals, object ) {
 
 	function update() {
 
-		object.geometry.dispose();
-
-		object.geometry = new THREE.TorusKnotGeometry(
+		editor.execute( new CmdSetGeometry( object, new THREE.TorusKnotGeometry(
 			radius.getValue(),
 			tube.getValue(),
 			radialSegments.getValue(),
@@ -93,11 +93,7 @@ Sidebar.Geometry.TorusKnotGeometry = function ( signals, object ) {
 			p.getValue(),
 			q.getValue(),
 			heightScale.getValue()
-		);
-
-		object.geometry.computeBoundingSphere();
-
-		signals.geometryChanged.dispatch( object );
+		) ) );
 
 	}
 

+ 19 - 20
editor/js/Sidebar.Geometry.js

@@ -49,10 +49,11 @@ Sidebar.Geometry = function ( editor ) {
 
 				var offset = geometry.center();
 
-				object.position.sub( offset );
+				var newPosition = object.position.clone();
+				newPosition.sub( offset );
+				editor.execute( new CmdSetPosition( object, newPosition ) );
 
-				editor.signals.geometryChanged.dispatch( geometry );
-				editor.signals.objectChanged.dispatch( object );
+				editor.signals.geometryChanged.dispatch( object );
 
 				break;
 
@@ -60,9 +61,7 @@ Sidebar.Geometry = function ( editor ) {
 
 				if ( geometry instanceof THREE.Geometry ) {
 
-					object.geometry = new THREE.BufferGeometry().fromGeometry( geometry );
-
-					signals.geometryChanged.dispatch( object );
+					editor.execute( new CmdSetGeometry( object, new THREE.BufferGeometry().fromGeometry( geometry ) ) );
 
 				}
 
@@ -70,14 +69,16 @@ Sidebar.Geometry = function ( editor ) {
 
 			case 'Flatten':
 
-				geometry.applyMatrix( object.matrix );
+				var newGeometry = geometry.clone();
+				newGeometry.uuid = geometry.uuid;
+				newGeometry.applyMatrix( object.matrix );
 
-				object.position.set( 0, 0, 0 );
-				object.rotation.set( 0, 0, 0 );
-				object.scale.set( 1, 1, 1 );
+				var cmds = [ new CmdSetGeometry( object, newGeometry ),
+					new CmdSetPosition( object, new THREE.Vector3( 0, 0, 0 ) ),
+					new CmdSetRotation( object, new THREE.Euler( 0, 0, 0 ) ),
+					new CmdSetScale( object, new THREE.Vector3( 1, 1, 1 ) ) ];
 
-				editor.signals.geometryChanged.dispatch( geometry );
-				editor.signals.objectChanged.dispatch( object );
+				editor.execute( new CmdMultiCmds( cmds ), 'Flatten Geometry' );
 
 				break;
 
@@ -85,8 +86,6 @@ Sidebar.Geometry = function ( editor ) {
 
 		this.setValue( 'Actions' );
 
-		signals.objectChanged.dispatch( object );
-
 	} );
 	container.addStatic( objectActions );
 
@@ -100,7 +99,7 @@ Sidebar.Geometry = function ( editor ) {
 
 		geometryUUID.setValue( THREE.Math.generateUUID() );
 
-		editor.selected.geometry.uuid = geometryUUID.getValue();
+		editor.execute( new CmdSetGeometryValue( editor.selected, 'uuid', geometryUUID.getValue() ) );
 
 	} );
 
@@ -115,7 +114,7 @@ Sidebar.Geometry = function ( editor ) {
 	var geometryNameRow = new UI.Panel();
 	var geometryName = new UI.Input().setWidth( '150px' ).setFontSize( '12px' ).onChange( function () {
 
-		editor.setGeometryName( editor.selected.geometry, geometryName.getValue() );
+		editor.execute( new CmdSetGeometryValue( editor.selected, 'name', geometryName.getValue() ) );
 
 	} );
 
@@ -126,11 +125,11 @@ Sidebar.Geometry = function ( editor ) {
 
 	// geometry
 
-	container.add( new Sidebar.Geometry.Geometry( signals ) );
+	container.add( new Sidebar.Geometry.Geometry( editor ) );
 
 	// buffergeometry
 
-	container.add( new Sidebar.Geometry.BufferGeometry( signals ) );
+	container.add( new Sidebar.Geometry.BufferGeometry( editor ) );
 
 	// parameters
 
@@ -161,11 +160,11 @@ Sidebar.Geometry = function ( editor ) {
 
 			if ( geometry.type === 'BufferGeometry' || geometry.type === 'Geometry' ) {
 
-				parameters.add( new Sidebar.Geometry.Modifiers( signals, object ) );
+				parameters.add( new Sidebar.Geometry.Modifiers( editor, object ) );
 
 			} else if ( Sidebar.Geometry[ geometry.type ] !== undefined ) {
 
-				parameters.add( new Sidebar.Geometry[ geometry.type ]( signals, object ) );
+				parameters.add( new Sidebar.Geometry[ geometry.type ]( editor, object ) );
 
 			}
 

+ 136 - 0
editor/js/Sidebar.History.js

@@ -0,0 +1,136 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+Sidebar.History = function ( editor ) {
+
+	var signals = editor.signals;
+
+	var config = editor.config;
+
+	var history = editor.history;
+
+	var container = new UI.CollapsiblePanel();
+	container.setCollapsed( editor.config.getKey( 'ui/sidebar/history/collapsed' ) );
+	container.onCollapsedChange( function ( boolean ) {
+
+		editor.config.setKey( 'ui/sidebar/history/collapsed', boolean );
+
+	} );
+
+	container.addStatic( new UI.Text( 'HISTORY' ) );
+
+	// Checkbox 'Save History'
+
+	var saveHistorySpan = new UI.Span().setPosition( 'absolute' ).setLeft( '180px' ).setFontSize( '13px' );
+	var saveHistoryCheckbox = new UI.Checkbox( config.getKey( 'project/history/stored' ) ).setLeft( '50px' ).onChange( function () {
+
+		config.setKey( 'project/history/stored', this.getValue() );
+		var saveHistory = this.getValue();
+
+		if ( saveHistory ) {
+
+			alert( 'The history will be preserved across a browser refresh.\nThis can have an impact on performance (mainly when working with textures)!' );
+
+			var lastUndoCmd = history.undos[ history.undos.length - 1 ];
+			var lastUndoId = ( lastUndoCmd !== undefined ) ? lastUndoCmd.id : 0;
+			editor.history.enableSerialization( lastUndoId );
+
+		} else {
+
+			signals.historyChanged.dispatch();
+
+		}
+
+	} );
+
+	saveHistorySpan.add( saveHistoryCheckbox );
+	saveHistorySpan.add( new UI.Text( 'Save History' ).setPosition( 'relative' ).setLeft( '5px' ) );
+
+	saveHistorySpan.onClick( function ( event ) {
+
+		event.stopPropagation(); // Avoid panel collapsing
+
+	} );
+
+	container.addStatic( saveHistorySpan );
+
+	container.add( new UI.Break() );
+
+	var ignoreObjectSelectedSignal = false;
+
+	var outliner = new UI.Outliner( editor );
+	outliner.onChange( function () {
+
+		ignoreObjectSelectedSignal = true;
+
+		editor.history.goToState( parseInt( outliner.getValue() ) );
+
+		ignoreObjectSelectedSignal = false;
+
+	} );
+	outliner.onDblClick( function () {
+
+		//editor.focusById( parseInt( outliner.getValue() ) );
+
+	} );
+	container.add( outliner );
+
+	//
+
+	var refreshUI = function () {
+
+		var options = [];
+		var enumerator = 1;
+
+		( function addObjects( objects, pad ) {
+
+			for ( var i = 0, l = objects.length; i < l; i ++ ) {
+
+				var object = objects[ i ];
+
+				var html = pad + "<span style='color: #0000cc '>" + enumerator ++ + ". Undo: " + object.name + "</span>";
+
+				options.push( { value: object.id, html: html } );
+
+			}
+
+		} )( history.undos, '&nbsp;' );
+
+
+		( function addObjects( objects, pad ) {
+
+			for ( var i = objects.length - 1; i >= 0; i -- ) {
+
+				var object = objects[ i ];
+
+				var html = pad + "<span style='color: #71544e'>" + enumerator ++ + ". Redo: " +  object.name + "</span>";
+
+				options.push( { value: object.id, html: html } );
+
+			}
+
+		} )( history.redos, '&nbsp;' );
+
+		outliner.setOptions( options );
+
+	};
+
+	refreshUI();
+
+	// events
+
+	signals.editorCleared.add( refreshUI );
+
+	signals.historyChanged.add( refreshUI );
+	signals.historyChanged.add( function ( cmd ) {
+		
+		outliner.setValue( cmd !== undefined ? cmd.id : null );
+
+	} );
+
+
+	return container;
+
+};

+ 129 - 55
editor/js/Sidebar.Material.js

@@ -42,7 +42,7 @@ Sidebar.Material = function ( editor ) {
 	var materialNameRow = new UI.Panel();
 	var materialName = new UI.Input().setWidth( '150px' ).setFontSize( '12px' ).onChange( function () {
 
-		editor.setMaterialName( editor.selected.material, materialName.getValue() );
+		editor.execute( new CmdSetMaterialValue( editor.selected, 'name', materialName.getValue() ) );
 
 	} );
 
@@ -82,7 +82,7 @@ Sidebar.Material = function ( editor ) {
 	materialProgramInfo.setMarginLeft( '4px' );
 	materialProgramInfo.onClick( function () {
 
-		signals.editScript.dispatch( currentObject.material, 'programInfo' );
+		signals.editScript.dispatch( currentObject, 'programInfo' );
 
 	} );
 	materialProgramRow.add( materialProgramInfo );
@@ -91,7 +91,7 @@ Sidebar.Material = function ( editor ) {
 	materialProgramVertex.setMarginLeft( '4px' );
 	materialProgramVertex.onClick( function () {
 
-		signals.editScript.dispatch( currentObject.material, 'vertexShader' );
+		signals.editScript.dispatch( currentObject, 'vertexShader' );
 
 	} );
 	materialProgramRow.add( materialProgramVertex );
@@ -100,7 +100,7 @@ Sidebar.Material = function ( editor ) {
 	materialProgramFragment.setMarginLeft( '4px' );
 	materialProgramFragment.onClick( function () {
 
-		signals.editScript.dispatch( currentObject.material, 'fragmentShader' );
+		signals.editScript.dispatch( currentObject, 'fragmentShader' );
 
 	} );
 	materialProgramRow.add( materialProgramFragment );
@@ -400,9 +400,9 @@ Sidebar.Material = function ( editor ) {
 
 		if ( material ) {
 
-			if ( material.uuid !== undefined ) {
+			if ( material.uuid !== undefined && material.uuid !== materialUUID.getValue() ) {
 
-				material.uuid = materialUUID.getValue();
+				editor.execute( new CmdSetMaterialValue( currentObject, 'uuid', materialUUID.getValue() ) );
 
 			}
 
@@ -410,7 +410,7 @@ Sidebar.Material = function ( editor ) {
 
 				material = new THREE[ materialClass.getValue() ]();
 
-				object.material = material;
+				editor.execute( new CmdSetMaterial( currentObject, material ), 'New Material: ' + materialClass.getValue() );
 				// TODO Copy other references in the scene graph
 				// keeping name and UUID then.
 				// Also there should be means to create a unique
@@ -419,27 +419,27 @@ Sidebar.Material = function ( editor ) {
 
 			}
 
-			if ( material.color !== undefined ) {
+			if ( material.color !== undefined && material.color.getHex() !== materialColor.getHexValue() ) {
 
-				material.color.setHex( materialColor.getHexValue() );
+				editor.execute( new CmdSetMaterialColor( currentObject, 'color', materialColor.getHexValue() ) );
 
 			}
 
-			if ( material.emissive !== undefined ) {
+			if ( material.emissive !== undefined && material.emissive.getHex() !== materialEmissive.getHexValue() ) {
 
-				material.emissive.setHex( materialEmissive.getHexValue() );
+				editor.execute( new CmdSetMaterialColor( currentObject, 'emissive', materialEmissive.getHexValue() ) );
 
 			}
 
-			if ( material.specular !== undefined ) {
+			if ( material.specular !== undefined && material.specular.getHex() !== materialSpecular.getHexValue() ) {
 
-				material.specular.setHex( materialSpecular.getHexValue() );
+				editor.execute( new CmdSetMaterialColor( currentObject, 'specular', materialSpecular.getHexValue() ) );
 
 			}
 
-			if ( material.shininess !== undefined ) {
+			if ( material.shininess !== undefined && Math.abs( material.shininess - materialShininess.getValue() ) >= 0.01 ) {
 
-				material.shininess = materialShininess.getValue();
+				editor.execute( new CmdSetMaterialValue( currentObject, 'shininess', materialShininess.getValue() ) );
 
 			}
 
@@ -449,16 +449,15 @@ Sidebar.Material = function ( editor ) {
 
 				if ( material.vertexColors !== vertexColors ) {
 
-					material.vertexColors = vertexColors;
-					material.needsUpdate = true;
+					editor.execute( new CmdSetMaterialValue( currentObject, 'vertexColors', vertexColors ) );
 
 				}
 
 			}
 
-			if ( material.skinning !== undefined ) {
+			if ( material.skinning !== undefined && material.skinning !== materialSkinning.getValue() ) {
 
-				material.skinning = materialSkinning.getValue();
+				editor.execute( new CmdSetMaterialValue( currentObject, 'skinning', materialSkinning.getValue() ) );
 
 			}
 
@@ -468,8 +467,12 @@ Sidebar.Material = function ( editor ) {
 
 				if ( objectHasUvs ) {
 
-					material.map = mapEnabled ? materialMap.getValue() : null;
-					material.needsUpdate = true;
+					var map = mapEnabled ? materialMap.getValue() : null;
+					if ( material.map !== map ) {
+
+						editor.execute( new CmdSetMaterialMap( currentObject, 'map', map ) );
+
+					}
 
 				} else {
 
@@ -485,8 +488,12 @@ Sidebar.Material = function ( editor ) {
 
 				if ( objectHasUvs ) {
 
-					material.alphaMap = mapEnabled ? materialAlphaMap.getValue() : null;
-					material.needsUpdate = true;
+					var alphaMap = mapEnabled ? materialAlphaMap.getValue() : null;
+					if ( material.alphaMap !== alphaMap ) {
+
+						editor.execute( new CmdSetMaterialMap( currentObject, 'alphaMap', alphaMap ) );
+
+					}
 
 				} else {
 
@@ -502,9 +509,18 @@ Sidebar.Material = function ( editor ) {
 
 				if ( objectHasUvs ) {
 
-					material.bumpMap = bumpMapEnabled ? materialBumpMap.getValue() : null;
-					material.bumpScale = materialBumpScale.getValue();
-					material.needsUpdate = true;
+					var bumpMap = bumpMapEnabled ? materialBumpMap.getValue() : null;
+					if ( material.bumpMap !== bumpMap ) {
+
+						editor.execute( new CmdSetMaterialMap( currentObject, 'bumpMap', bumpMap ) );
+
+					}
+
+					if ( material.bumpScale !== materialBumpScale.getValue() ) {
+
+						editor.execute( new CmdSetMaterialValue( currentObject, 'bumpScale', materialBumpScale.getValue() ) );
+
+					}
 
 				} else {
 
@@ -520,8 +536,12 @@ Sidebar.Material = function ( editor ) {
 
 				if ( objectHasUvs ) {
 
-					material.normalMap = normalMapEnabled ? materialNormalMap.getValue() : null;
-					material.needsUpdate = true;
+					var normalMap = normalMapEnabled ? materialNormalMap.getValue() : null;
+					if ( material.normalMap !== normalMap ) {
+
+						editor.execute( new CmdSetMaterialMap( currentObject, 'normalMap', normalMap ) );
+
+					}
 
 				} else {
 
@@ -537,9 +557,18 @@ Sidebar.Material = function ( editor ) {
 
 				if ( objectHasUvs ) {
 
-					material.displacementMap = displacementMapEnabled ? materialDisplacementMap.getValue() : null;
-					material.displacementScale = materialDisplacementScale.getValue();
-					material.needsUpdate = true;
+					var displacementMap = displacementMapEnabled ? materialDisplacementMap.getValue() : null;
+					if ( material.displacementMap !== displacementMap ) {
+
+						editor.execute( new CmdSetMaterialMap( currentObject, 'displacementMap', displacementMap ) );
+
+					}
+
+					if ( material.displacementScale !== materialDisplacementScale.getValue() ) {
+
+						editor.execute( new CmdSetMaterialValue( currentObject, 'displacementScale', materialDisplacementScale.getValue() ) );
+
+					}
 
 				} else {
 
@@ -555,8 +584,12 @@ Sidebar.Material = function ( editor ) {
 
 				if ( objectHasUvs ) {
 
-					material.specularMap = specularMapEnabled ? materialSpecularMap.getValue() : null;
-					material.needsUpdate = true;
+					var specularMap = specularMapEnabled ? materialSpecularMap.getValue() : null;
+					if ( material.specularMap !== specularMap ) {
+
+						editor.execute( new CmdSetMaterialMap( currentObject, 'specularMap', specularMap ) );
+
+					}
 
 				} else {
 
@@ -570,12 +603,21 @@ Sidebar.Material = function ( editor ) {
 
 				var envMapEnabled = materialEnvMapEnabled.getValue() === true;
 
-				material.envMap = envMapEnabled ? materialEnvMap.getValue() : null;
-				material.reflectivity = materialReflectivity.getValue();
-				material.needsUpdate = true;
+				var envMap = envMapEnabled ? materialEnvMap.getValue() : null;
 
-			}
+				if ( material.envMap !== envMap ) {
+
+					editor.execute( new CmdSetMaterialMap( currentObject, 'envMap', envMap ) );
+
+				}
+
+				if ( material.reflectivity !== materialReflectivity.getValue() ) {
 
+					editor.execute( new CmdSetMaterialValue( currentObject, 'reflectivity', materialReflectivity.getValue() ) );
+
+				}
+
+			}
 
 			if ( material.lightMap !== undefined ) {
 
@@ -583,8 +625,12 @@ Sidebar.Material = function ( editor ) {
 
 				if ( objectHasUvs ) {
 
-					material.lightMap = lightMapEnabled ? materialLightMap.getValue() : null;
-					material.needsUpdate = true;
+					var lightMap = lightMapEnabled ? materialLightMap.getValue() : null;
+					if ( material.lightMap !== lightMap ) {
+
+						editor.execute( new CmdSetMaterialMap( currentObject, 'lightMap', lightMap ) );
+
+					}
 
 				} else {
 
@@ -600,9 +646,18 @@ Sidebar.Material = function ( editor ) {
 
 				if ( objectHasUvs ) {
 
-					material.aoMap = aoMapEnabled ? materialAOMap.getValue() : null;
-					material.aoMapIntensity = materialAOScale.getValue();
-					material.needsUpdate = true;
+					var aoMap = aoMapEnabled ? materialAOMap.getValue() : null;
+					if ( material.aoMap !== aoMap ) {
+
+						editor.execute( new CmdSetMaterialMap( currentObject, 'aoMap', aoMap ) );
+
+					}
+
+					if ( material.aoMapIntensity !== materialAOScale.getValue() ) {
+
+						editor.execute( new CmdSetMaterialValue( currentObject, 'aoMapIntensity', materialAOScale.getValue() ) );
+
+					}
 
 				} else {
 
@@ -614,49 +669,65 @@ Sidebar.Material = function ( editor ) {
 
 			if ( material.side !== undefined ) {
 
-				material.side = parseInt( materialSide.getValue() );
+				var side = parseInt( materialSide.getValue() );
+				if ( material.side !== side ) {
+
+					editor.execute( new CmdSetMaterialValue( currentObject, 'side', side ) );
+
+				}
+
 
 			}
 
 			if ( material.shading !== undefined ) {
 
-				material.shading = parseInt( materialShading.getValue() );
+				var shading = parseInt( materialShading.getValue() );
+				if ( material.shading !== shading ) {
+
+					editor.execute( new CmdSetMaterialValue( currentObject, 'shading', shading ) );
+
+				}
 
 			}
 
 			if ( material.blending !== undefined ) {
 
-				material.blending = parseInt( materialBlending.getValue() );
+				var blending = parseInt( materialBlending.getValue() );
+				if ( material.blending !== blending ) {
+
+					editor.execute( new CmdSetMaterialValue( currentObject, 'blending', blending ) );
+
+				}
 
 			}
 
-			if ( material.opacity !== undefined ) {
+			if ( material.opacity !== undefined && Math.abs( material.opacity - materialOpacity.getValue() ) >= 0.01 ) {
 
-				material.opacity = materialOpacity.getValue();
+				editor.execute( new CmdSetMaterialValue( currentObject, 'opacity', materialOpacity.getValue() ) );
 
 			}
 
-			if ( material.transparent !== undefined ) {
+			if ( material.transparent !== undefined && material.transparent !== materialTransparent.getValue() ) {
 
-				material.transparent = materialTransparent.getValue();
+				editor.execute( new CmdSetMaterialValue( currentObject, 'transparent', materialTransparent.getValue() ) );
 
 			}
 
-			if ( material.alphaTest !== undefined ) {
+			if ( material.alphaTest !== undefined && Math.abs( material.alphaTest - materialAlphaTest.getValue() ) >= 0.01 ) {
 
-				material.alphaTest = materialAlphaTest.getValue();
+				editor.execute( new CmdSetMaterialValue( currentObject, 'alphaTest', materialAlphaTest.getValue() ) );
 
 			}
 
-			if ( material.wireframe !== undefined ) {
+			if ( material.wireframe !== undefined && material.wireframe !== materialWireframe.getValue() ) {
 
-				material.wireframe = materialWireframe.getValue();
+				editor.execute( new CmdSetMaterialValue( currentObject, 'wireframe', materialWireframe.getValue() ) );
 
 			}
 
-			if ( material.wireframeLinewidth !== undefined ) {
+			if ( material.wireframeLinewidth !== undefined && Math.abs( material.wireframeLinewidth - materialWireframeLinewidth.getValue() ) >= 0.01 ) {
 
-				material.wireframeLinewidth = materialWireframeLinewidth.getValue();
+				editor.execute( new CmdSetMaterialValue( currentObject, 'wireframeLinewidth', materialWireframeLinewidth.getValue() ) );
 
 			}
 
@@ -718,6 +789,8 @@ Sidebar.Material = function ( editor ) {
 
 	function refreshUi( resetTextureSelectors ) {
 
+		if ( !currentObject ) return;
+
 		var material = currentObject.material;
 
 		if ( material.uuid !== undefined ) {
@@ -959,6 +1032,7 @@ Sidebar.Material = function ( editor ) {
 
 	} );
 
+	signals.materialChanged.add( function () { refreshUi() } );
 	return container;
 
 }

+ 66 - 50
editor/js/Sidebar.Object3D.js

@@ -41,23 +41,21 @@ Sidebar.Object3D = function ( editor ) {
 		switch ( this.getValue() ) {
 
 			case 'Reset Position':
-				object.position.set( 0, 0, 0 );
+				editor.execute( new CmdSetPosition( object, new THREE.Vector3( 0, 0, 0 ) ) );
 				break;
 
 			case 'Reset Rotation':
-				object.rotation.set( 0, 0, 0 );
+				editor.execute( new CmdSetRotation( object, new THREE.Euler( 0, 0, 0 ) ) );
 				break;
 
 			case 'Reset Scale':
-				object.scale.set( 1, 1, 1 );
+				editor.execute( new CmdSetScale( object, new THREE.Vector3( 1, 1, 1 ) ) );
 				break;
 
 		}
 
 		this.setValue( 'Actions' );
 
-		signals.objectChanged.dispatch( object );
-
 	} );
 	container.addStatic( objectActions );
 
@@ -71,7 +69,7 @@ Sidebar.Object3D = function ( editor ) {
 
 		objectUUID.setValue( THREE.Math.generateUUID() );
 
-		editor.selected.uuid = objectUUID.getValue();
+		editor.execute( new CmdSetUuid( editor.selected, objectUUID.getValue() ) );
 
 	} );
 
@@ -86,7 +84,7 @@ Sidebar.Object3D = function ( editor ) {
 	var objectNameRow = new UI.Panel();
 	var objectName = new UI.Input().setWidth( '150px' ).setFontSize( '12px' ).onChange( function () {
 
-		editor.nameObject( editor.selected, objectName.getValue() );
+		editor.execute( new CmdSetValue( editor.selected, 'name', objectName.getValue() ) );
 
 	} );
 
@@ -375,110 +373,122 @@ Sidebar.Object3D = function ( editor ) {
 
 				if ( object.parent.id !== newParentId && object.id !== newParentId ) {
 
-					editor.moveObject( object, editor.scene.getObjectById( newParentId ) );
+					editor.execute( new CmdMoveObject( object, editor.scene.getObjectById( newParentId ) ) );
 
 				}
 
 			}
 			*/
 
-			object.position.x = objectPositionX.getValue();
-			object.position.y = objectPositionY.getValue();
-			object.position.z = objectPositionZ.getValue();
+			var newPosition = new THREE.Vector3( objectPositionX.getValue(), objectPositionY.getValue(), objectPositionZ.getValue() );
+			if ( object.position.distanceTo( newPosition ) >= 0.01 ) {
 
-			object.rotation.x = objectRotationX.getValue();
-			object.rotation.y = objectRotationY.getValue();
-			object.rotation.z = objectRotationZ.getValue();
+				editor.execute( new CmdSetPosition( object, newPosition ) );
 
-			object.scale.x = objectScaleX.getValue();
-			object.scale.y = objectScaleY.getValue();
-			object.scale.z = objectScaleZ.getValue();
+			}
 
-			if ( object.fov !== undefined ) {
+			var newRotation = new THREE.Euler( objectRotationX.getValue(), objectRotationY.getValue(), objectRotationZ.getValue() );
+			if ( object.rotation.toVector3().distanceTo( newRotation.toVector3() ) >= 0.01 ) {
 
-				object.fov = objectFov.getValue();
-				object.updateProjectionMatrix();
+				editor.execute( new CmdSetRotation( object, newRotation ) );
 
 			}
 
-			if ( object.near !== undefined ) {
+			var newScale = new THREE.Vector3( objectScaleX.getValue(), objectScaleY.getValue(), objectScaleZ.getValue() );
+			if ( object.scale.distanceTo( newScale ) >= 0.01 ) {
 
-				object.near = objectNear.getValue();
+				editor.execute( new CmdSetScale( object, newScale ) );
 
 			}
 
-			if ( object.far !== undefined ) {
+			if ( object.fov !== undefined && Math.abs( object.fov - objectFov.getValue() ) >= 0.01 ) {
 
-				object.far = objectFar.getValue();
+				editor.execute( new CmdSetValue( object, 'fov', objectFov.getValue() ) );
+				object.updateProjectionMatrix();
 
 			}
 
-			if ( object.intensity !== undefined ) {
+			if ( object.near !== undefined && Math.abs( object.near - objectNear.getValue() ) >= 0.01 ) {
 
-				object.intensity = objectIntensity.getValue();
+				editor.execute( new CmdSetValue( object, 'near', objectNear.getValue() ) );
 
 			}
 
-			if ( object.color !== undefined ) {
+			if ( object.far !== undefined && Math.abs( object.far - objectFar.getValue() ) >= 0.01 ) {
 
-				object.color.setHex( objectColor.getHexValue() );
+				editor.execute( new CmdSetValue( object, 'far', objectFar.getValue() ) );
 
 			}
 
-			if ( object.groundColor !== undefined ) {
+			if ( object.intensity !== undefined && Math.abs( object.intensity - objectIntensity.getValue() ) >= 0.01 ) {
 
-				object.groundColor.setHex( objectGroundColor.getHexValue() );
+				editor.execute( new CmdSetValue( object, 'intensity', objectIntensity.getValue() ) );
 
 			}
 
-			if ( object.distance !== undefined ) {
+			if ( object.color !== undefined && object.color.getHex() !== objectColor.getHexValue() ) {
 
-				object.distance = objectDistance.getValue();
+				editor.execute( new CmdSetColor( object, 'color', objectColor.getHexValue() ) );
 
 			}
 
-			if ( object.angle !== undefined ) {
+			if ( object.groundColor !== undefined && object.groundColor.getHex() !== objectGroundColor.getHexValue() ) {
 
-				object.angle = objectAngle.getValue();
+				editor.execute( new CmdSetColor( object, 'groundColor', objectGroundColor.getHexValue() ) );
 
 			}
 
-			if ( object.exponent !== undefined ) {
+			if ( object.distance !== undefined && Math.abs( object.distance - objectDistance.getValue() ) >= 0.01 ) {
 
-				object.exponent = objectExponent.getValue();
+				editor.execute( new CmdSetValue( object, 'distance', objectDistance.getValue() ) );
 
 			}
 
-			if ( object.decay !== undefined ) {
+			if ( object.angle !== undefined && Math.abs( object.angle - objectAngle.getValue() ) >= 0.01 ) {
 
-				object.decay = objectDecay.getValue();
+				editor.execute( new CmdSetValue( object, 'angle', objectAngle.getValue() ) );
 
 			}
 
-			if ( object.castShadow !== undefined ) {
+			if ( object.exponent !== undefined && Math.abs( object.exponent - objectExponent.getValue() ) >= 0.01 ) {
 
-				object.castShadow = objectCastShadow.getValue();
+				editor.execute( new CmdSetValue( object, 'exponent', objectExponent.getValue() ) );
 
 			}
 
-			if ( object.receiveShadow !== undefined ) {
+			if ( object.decay !== undefined && Math.abs( object.decay - objectDecay.getValue() ) >= 0.01 ) {
 
-				var value = objectReceiveShadow.getValue();
+				editor.execute( new CmdSetValue( object, 'decay', objectDecay.getValue() ) );
 
-				if ( value !== object.receiveShadow ) {
+			}
 
-					object.receiveShadow = value;
-					object.material.needsUpdate = true;
+			if ( object.visible !== objectVisible.getValue() ) {
 
-				}
+				editor.execute( new CmdSetValue( object, 'visible', objectVisible.getValue() ) );
 
 			}
 
-			object.visible = objectVisible.getValue();
+			if ( object.castShadow !== objectCastShadow.getValue() ) {
+
+				editor.execute( new CmdSetValue( object, 'castShadow', objectCastShadow.getValue() ) );
+
+			}
+
+			if ( object.receiveShadow !== objectReceiveShadow.getValue() ) {
+
+				editor.execute( new CmdSetValue( object, 'receiveShadow', objectReceiveShadow.getValue() ) );
+				object.material.needsUpdate = true;
+
+			}
 
 			try {
 
-				object.userData = JSON.parse( objectUserData.getValue() );
+				var userData = JSON.parse( objectUserData.getValue() );
+				if ( JSON.stringify( object.userData ) != JSON.stringify( userData ) ) {
+
+					editor.execute( new CmdSetValue( object, 'userData', userData ) );
+
+				}
 
 			} catch ( exception ) {
 
@@ -486,8 +496,6 @@ Sidebar.Object3D = function ( editor ) {
 
 			}
 
-			signals.objectChanged.dispatch( object );
-
 		}
 
 	}
@@ -579,6 +587,14 @@ Sidebar.Object3D = function ( editor ) {
 
 	} );
 
+	signals.refreshSidebarObject3D.add( function ( object ) {
+
+		if ( object !== editor.selected ) return;
+
+		updateUI( object );
+
+	} );
+
 	function updateUI( object ) {
 
 		objectType.setValue( object.type );

+ 4 - 5
editor/js/Sidebar.Script.js

@@ -27,7 +27,7 @@ Sidebar.Script = function ( editor ) {
 	newScript.onClick( function () {
 
 		var script = { name: '', source: 'function update( event ) {}' };
-		editor.addScript( editor.selected, script );
+		editor.execute( new CmdAddScript( editor.selected, script ) );
 
 	} );
 	container.add( newScript );
@@ -63,9 +63,7 @@ Sidebar.Script = function ( editor ) {
 					var name = new UI.Input( script.name ).setWidth( '130px' ).setFontSize( '12px' );
 					name.onChange( function () {
 
-						script.name = this.getValue();
-
-						signals.scriptChanged.dispatch();
+						editor.execute( new CmdSetScriptValue( editor.selected, script, 'name', this.getValue() ) );
 
 					} );
 					scriptsContainer.add( name );
@@ -85,7 +83,7 @@ Sidebar.Script = function ( editor ) {
 
 						if ( confirm( 'Are you sure?' ) ) {
 
-							editor.removeScript( editor.selected, script );
+							editor.execute( new CmdRemoveScript( editor.selected, script ) );
 
 						}
 
@@ -122,6 +120,7 @@ Sidebar.Script = function ( editor ) {
 
 	signals.scriptAdded.add( update );
 	signals.scriptRemoved.add( update );
+	signals.scriptChanged.add( update );
 
 	return container;
 

+ 1 - 0
editor/js/Sidebar.js

@@ -8,6 +8,7 @@ var Sidebar = function ( editor ) {
 	container.setId( 'sidebar' );
 
 	container.add( new Sidebar.Project( editor ) );
+	container.add( new Sidebar.History( editor ) );
 	container.add( new Sidebar.Scene( editor ) );
 	container.add( new Sidebar.Object3D( editor ) );
 	container.add( new Sidebar.Geometry( editor ) );

+ 43 - 19
editor/js/Viewport.js

@@ -34,7 +34,9 @@ var Viewport = function ( editor ) {
 	selectionBox.visible = false;
 	sceneHelpers.add( selectionBox );
 
-	var matrix = new THREE.Matrix4();
+	var objectPositionOnDown = null;
+	var objectRotationOnDown = null;
+	var objectScaleOnDown = null;
 
 	var transformControls = new THREE.TransformControls( camera, container.dom );
 	transformControls.addEventListener( 'change', function () {
@@ -51,6 +53,8 @@ var Viewport = function ( editor ) {
 
 			}
 
+			signals.refreshSidebarObject3D.dispatch( object );
+
 		}
 
 		render();
@@ -60,7 +64,9 @@ var Viewport = function ( editor ) {
 
 		var object = transformControls.object;
 
-		matrix.copy( object.matrix );
+		objectPositionOnDown = object.position.clone();
+		objectRotationOnDown = object.rotation.clone();
+		objectScaleOnDown = object.scale.clone();
 
 		controls.enabled = false;
 
@@ -69,26 +75,36 @@ var Viewport = function ( editor ) {
 
 		var object = transformControls.object;
 
-		if ( matrix.equals( object.matrix ) === false ) {
+		if ( object != null ) {
+
+			switch ( transformControls.getMode() ) {
+
+				case 'translate':
+					if (!objectPositionOnDown.equals(object.position)) {
+
+						editor.execute(new CmdSetPosition( object, object.position, objectPositionOnDown ));
+
+					}
+					break;
+				case 'rotate':
+					if (!objectRotationOnDown.equals(object.rotation)) {
 
-			( function ( matrix1, matrix2 ) {
+						editor.execute(new CmdSetRotation( object, object.rotation, objectRotationOnDown ));
 
-				editor.history.add(
-					function () {
-						matrix1.decompose( object.position, object.quaternion, object.scale );
-						signals.objectChanged.dispatch( object );
-					},
-					function () {
-						matrix2.decompose( object.position, object.quaternion, object.scale );
-						signals.objectChanged.dispatch( object );
 					}
-				);
+					break;
+				case 'scale':
+					if (!objectScaleOnDown.equals(object.scale)) {
+
+						editor.execute(new CmdSetScale( object, object.scale, objectScaleOnDown ));
 
-			} )( matrix.clone(), object.matrix.clone() );
+					}
+					break;
+
+			}
 
 		}
 
-		signals.objectChanged.dispatch( object );
 		controls.enabled = true;
 
 	} );
@@ -358,9 +374,13 @@ var Viewport = function ( editor ) {
 
 	} );
 
-	signals.geometryChanged.add( function ( geometry ) {
+	signals.geometryChanged.add( function ( object ) {
 
-		selectionBox.update( editor.selected );
+		if ( object !== null ) {
+
+			selectionBox.update( object );
+
+		}
 
 		render();
 
@@ -384,8 +404,12 @@ var Viewport = function ( editor ) {
 
 	signals.objectChanged.add( function ( object ) {
 
-		selectionBox.update( object );
-		transformControls.update();
+		if ( editor.selected === object ) {
+
+			selectionBox.update( object );
+			transformControls.update();
+
+		}
 
 		if ( object instanceof THREE.PerspectiveCamera ) {
 

+ 2 - 2
editor/js/libs/ui.three.js

@@ -158,12 +158,12 @@ UI.Outliner = function ( editor ) {
 
 			if ( item.nextSibling === null ) {
 
-				editor.moveObject( object, editor.scene );
+				editor.execute( new CmdMoveObject( object, editor.scene ) );
 
 			} else {
 
 				var nextObject = scene.getObjectById( item.nextSibling.value );
-				editor.moveObject( object, nextObject.parent, nextObject );
+				editor.execute( new CmdMoveObject( object, nextObject.parent, nextObject ) );
 
 			}
 

+ 6 - 0
examples/js/controls/TransformControls.js

@@ -738,6 +738,12 @@
 
 		};
 
+		this.getMode = function () {
+
+			return _mode;
+
+		};
+
 		this.setMode = function ( mode ) {
 
 			_mode = mode ? mode : _mode;

+ 171 - 0
test/unit/editor/CommonUtilities.js

@@ -0,0 +1,171 @@
+/**
+ * @author lxxxvi / https://github.com/lxxxvi
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+function mergeParams( defaults, customParams ) {
+
+	if ( typeof customParams == "undefined" ) return defaults;
+
+	var defaultKeys = Object.keys( defaults );
+	var params = {};
+
+	defaultKeys.map( function( key ) {
+
+		params[ key ] = customParams[ key ] || defaultKeys[ key ];
+
+	} );
+
+	return params;
+
+}
+
+
+function getGeometryParams( type, customParams ) {
+
+	if ( typeof customParams != "undefined" &&
+		typeof customParams.geometry != "undefined" &&
+		typeof customParams.geometry.parameters != "undefined" ) {
+
+		var customGeometryParams = customParams.geometry.parameters;
+
+	}
+
+	var defaults = {};
+
+	switch ( type ) {
+
+		case "BoxGeometry":
+
+			defaults = { width: 100, height: 100, depth: 100, widthSegments: 1, heightSegments: 1, depthSegments: 1 };
+			break;
+
+		case "SphereGeometry":
+
+			defaults = { radius: 75, widthSegments: 32, heightSegments: 16, phiStart: 0, phiLength: 6.28, thetaStart: 0.00, thetaLength: 3.14 };
+			break;
+
+		default:
+
+			console.error( "Type '" + type + "' is not known while creating params" );
+			return false;
+
+	}
+
+	return mergeParams( defaults, customGeometryParams );
+
+}
+
+function getGeometry( type, customParams ) {
+
+	var params = getGeometryParams( type, customParams );
+
+	switch ( type ) {
+
+		case "BoxGeometry":
+
+			return new THREE.BoxGeometry(
+				params[ 'width' ],
+				params[ 'height' ],
+				params[ 'depth' ],
+				params[ 'widthSegments' ],
+				params[ 'heightSegments' ],
+				params[ 'depthSegments' ]
+			);
+
+		case "SphereGeometry":
+
+			return new THREE.SphereGeometry(
+				params[ 'radius' ],
+				params[ 'widthSegments' ],
+				params[ 'heightSegments' ],
+				params[ 'phiStart' ],
+				params[ 'phiLength' ],
+				params[ 'thetaStart' ],
+				params[ 'thetaLength' ]
+			);
+
+		default:
+
+			console.error( "Type '" + type + "' is not known while creating geometry " );
+			return false;
+
+	}
+
+}
+
+function getObject( name, type, customParams ) {
+
+	var geometry = getGeometry( type, customParams );
+
+	var object = new THREE.Mesh( geometry, new THREE.MeshPhongMaterial() );
+	object.name = name || type + " 1";
+
+	return object;
+
+}
+
+
+function aBox( name, customParams ) {
+
+	return getObject( name, "BoxGeometry", customParams );
+
+}
+
+function aSphere( name, customParams ) {
+
+	return getObject( name, "SphereGeometry", customParams );
+
+}
+
+function aPointlight( name ) {
+
+	var object = new THREE.PointLight( 54321, 1.0, 0.0, 1.0 );
+	object.name = name || "PointLight 1";
+
+	return object;
+
+}
+
+function aPerspectiveCamera( name ) {
+
+	var object = new THREE.PerspectiveCamera( 50.1, 0.4, 1.03, 999.05 );
+	object.name = name || "PerspectiveCamera 1";
+
+	return object;
+
+}
+
+function getScriptCount( editor ) {
+
+	var scriptsKeys = Object.keys( editor.scripts );
+	var scriptCount = 0;
+
+	for ( var i = 0; i < scriptsKeys.length; i ++ ) {
+
+		scriptCount += editor.scripts[ scriptsKeys[ i ] ].length;
+
+	}
+
+	return scriptCount;
+
+}
+
+function exportScene( editor ) {
+
+	var output = editor.scene.toJSON();
+	output = JSON.stringify( output, null, '\t' );
+	output = output.replace( /[\n\t]+([\d\.e\-\[\]]+)/g, '$1' );
+	return output;
+
+}
+
+function importScene( data ) {
+
+	var json = JSON.parse( data );
+	var loader = new THREE.ObjectLoader();
+	var result = loader.parse( json );
+
+	return result;
+
+}

+ 56 - 0
test/unit/editor/TestCmdAddObjectAndCmdRemoveObject.js

@@ -0,0 +1,56 @@
+/**
+ * @author lxxxvi / https://github.com/lxxxvi
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+module( "CmdAddObjectAndCmdRemoveObject" );
+
+test( "Test CmdAddObject and CmdRemoveObject (Undo and Redo)", function() {
+
+	// setup
+	var editor = new Editor();
+
+	var box = aBox( 'The Box' );
+	var light = aPointlight( 'The PointLight' );
+	var camera = aPerspectiveCamera( 'The Camera' );
+
+	var objects = [ box , light, camera ];
+
+	objects.map( function( object ) {
+
+		// Test Add
+		var cmd = new CmdAddObject( object );
+		cmd.updatable = false;
+
+		editor.execute( cmd );
+		ok( editor.scene.children.length == 1, "OK, adding '" + object.type + "' was successful " );
+
+		editor.undo();
+		ok( editor.scene.children.length == 0, "OK, adding '" + object.type + "' is undone (was removed)" );
+
+		editor.redo();
+		ok( editor.scene.children[ 0 ].name == object.name, "OK, removed '" + object.type + "' was added again (redo)" );
+
+		ok( editor.selected == object, "OK, focus was set on recovered object after Add-Redo" );
+
+
+		// Test Remove
+		var cmd = new CmdRemoveObject( object );
+		cmd.updatable = false;
+
+		editor.execute( cmd );
+		ok( editor.scene.children.length == 0, "OK, removing object was successful" );
+
+		editor.undo();
+		ok( editor.scene.children[ 0 ].name == object.name, "OK, removed object was added again (undo)" );
+
+		ok( editor.selected == object, "OK, focus was set on recovered object after Delete-Undo" );
+
+		editor.redo();
+		ok( editor.scene.children.length == 0, "OK, object was removed again (redo)" );
+
+
+	} );
+
+
+} );

+ 62 - 0
test/unit/editor/TestCmdAddScript.js

@@ -0,0 +1,62 @@
+/**
+ * @author lxxxvi / https://github.com/lxxxvi
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+module( "CmdAddScript" );
+
+test( "Test CmdAddScript (Undo and Redo)", function() {
+
+	var editor = new Editor();
+
+	// prepare
+	var box    = aBox( "The scripted box" );
+	var sphere = aSphere( "The scripted sphere" );
+	var objects = [ box, sphere ];
+
+	var xMove  = { name: "", source: "function update( event ) { this.position.x = this.position.x + 1; }" };
+	var yMove  = { name: "", source: "function update( event ) { this.position.y = this.position.y + 1; }" };
+	var scripts = [ xMove, yMove ];
+
+	// add objects to editor
+	objects.map( function( item ) {
+
+		editor.execute( new CmdAddObject( item ) );
+
+	} );
+	ok( editor.scene.children.length == 2, "OK, the box and the sphere have been added" );
+
+	// add scripts to the objects
+	for ( var i = 0; i < scripts.length; i ++ ) {
+
+		var cmd = new CmdAddScript( objects[ i ], scripts[ i ] );
+		cmd.updatable = false;
+		editor.execute( cmd );
+
+	}
+
+	var scriptsKeys = Object.keys( editor.scripts );
+	ok( getScriptCount( editor ) == scripts.length, "OK, correct number of scripts have been added" );
+
+	for ( var i = 0; i < objects.length; i ++ ) {
+
+		ok( objects[ i ].uuid == scriptsKeys[ i ], "OK, script key #" + i + " matches the object's UUID" );
+
+	}
+
+	editor.undo();
+	ok( getScriptCount( editor ) == scripts.length - 1, "OK, one script has been removed by undo" );
+
+	editor.redo();
+	ok( getScriptCount( editor ) == scripts.length, "OK, one script has been added again by redo" );
+
+
+	for ( var i = 0; i < scriptsKeys.length; i ++ ) {
+
+		ok( editor.scripts[ scriptsKeys[ i ] ][ 0 ] == scripts[ i ], "OK, script #" + i + " is still assigned correctly" );
+
+	}
+
+
+} );
+

+ 42 - 0
test/unit/editor/TestCmdMoveObject.js

@@ -0,0 +1,42 @@
+/**
+ * @author lxxxvi / https://github.com/lxxxvi
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+module( "CmdMoveObject" );
+
+test( "Test CmdMoveObject (Undo and Redo)", function() {
+
+	var editor = new Editor();
+
+	// create some objects
+	var anakinsName = 'Anakin Skywalker';
+	var lukesName   = 'Luke Skywalker';
+	var anakinSkywalker = aSphere( anakinsName );
+	var lukeSkywalker   = aBox( lukesName );
+
+	editor.execute( new CmdAddObject( anakinSkywalker ) );
+	editor.execute( new CmdAddObject( lukeSkywalker ) );
+
+
+	ok( anakinSkywalker.parent.name == "Scene", "OK, Anakin's parent is 'Scene' " );
+	ok( lukeSkywalker.parent.name   == "Scene", "OK, Luke's parent is 'Scene' " );
+
+	// Tell Luke, Anakin is his father
+	editor.execute( new CmdMoveObject( lukeSkywalker, anakinSkywalker ) );
+
+	ok( true === true, "(Luke has been told who his father is)" );
+	ok( anakinSkywalker.parent.name == "Scene", "OK, Anakin's parent is still 'Scene' " );
+	ok( lukeSkywalker.parent.name   == anakinsName, "OK, Luke's parent is '" + anakinsName + "' " );
+
+	editor.undo();
+	ok( true === true, "(Statement undone)" );
+	ok( anakinSkywalker.parent.name == "Scene", "OK, Anakin's parent is still 'Scene' " );
+	ok( lukeSkywalker.parent.name   == "Scene", "OK, Luke's parent is 'Scene' again " );
+
+	editor.redo();
+	ok( true === true, "(Statement redone)" );
+	ok( anakinSkywalker.parent.name == "Scene", "OK, Anakin's parent is still 'Scene' " );
+	ok( lukeSkywalker.parent.name   == anakinsName, "OK, Luke's parent is '" + anakinsName + "' again " );
+
+} );

+ 80 - 0
test/unit/editor/TestCmdMultiCmds.js

@@ -0,0 +1,80 @@
+/**
+ * @author lxxxvi / https://github.com/lxxxvi
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+module( "CmdMultiCmds" );
+
+test( "Test CmdMultiCmds (Undo and Redo)", function() {
+
+	var editor = new Editor();
+	var box = aBox( 'Multi Command Box' );
+	var boxGeometry1 = { geometry: { parameters: { width: 200, height: 201, depth: 202, widthSegments: 2, heightSegments: 3, depthSegments: 4 } } };
+	var boxGeometry2 = { geometry: { parameters: { width:  50, height:  51, depth:  52, widthSegments: 7, heightSegments: 8, depthSegments: 9 } } };
+	var boxGeometries = [ getGeometry( "BoxGeometry", boxGeometry1 ), getGeometry( "BoxGeometry", boxGeometry2 ) ];
+
+	var cmd = new CmdAddObject( box );
+	cmd.updatable = false;
+	editor.execute( cmd );
+
+	// setup first multi commands
+	var firstMultiCmds = [
+
+		new CmdSetGeometry( box, boxGeometries[ 0 ] ),
+		new CmdSetPosition( box, new THREE.Vector3( 1, 2, 3 ) ),
+		new CmdSetRotation( box, new THREE.Euler( 0.1, 0.2, 0.2 ) ),
+		new CmdSetScale( box, new THREE.Vector3( 1.1, 1.2, 1.3 ) )
+
+	];
+
+	firstMultiCmds.map( function( cmd ) {
+
+		cmd.updatable = false;
+
+	} );
+
+	var firstMultiCmd = new CmdMultiCmds( firstMultiCmds );
+	firstMultiCmd.updatable = false;
+	editor.execute( firstMultiCmd );
+
+
+	// setup second multi commands
+	var secondMultiCmds = [
+
+		new CmdSetGeometry( box, boxGeometries[ 1 ] ),
+		new CmdSetPosition( box, new THREE.Vector3( 4, 5, 6 ) ),
+		new CmdSetRotation( box, new THREE.Euler( 0.4, 0.5, 0.6 ) ),
+		new CmdSetScale( box, new THREE.Vector3( 1.4, 1.5, 1.6 ) )
+
+	];
+
+	secondMultiCmds.map( function( cmd ) {
+
+		cmd.updatable = false;
+
+	} );
+
+	var secondMultiCmd = new CmdMultiCmds( secondMultiCmds );
+	secondMultiCmd.updatable = false;
+	editor.execute( secondMultiCmd );
+
+
+	// test one modified value for each command
+	ok( box.geometry.parameters.widthSegments == 7, "OK, widthSegments has been modified accordingly after two multi executes (expected: 7, actual: " + box.geometry.parameters.widthSegments + ")" );
+	ok( box.position.y == 5, "OK, y position has been modified accordingly after two multi executes (expected: 5, actual: " + box.position.y + ")" );
+	ok( box.rotation.x == 0.4, "OK, x rotation has been modified accordingly after two multi executes (expected: 0.4, actual: " + box.rotation.x + ") " );
+	ok( box.scale.z == 1.6, "OK, z scale has been modified accordingly after two multi executes (expected: 1.6, actual: " + box.scale.z + ")" );
+
+	editor.undo();
+	ok( box.geometry.parameters.widthSegments == 2, "OK, widthSegments has been modified accordingly after undo (expected: 2, actual: " + box.geometry.parameters.widthSegments + ")" );
+	ok( box.position.y == 2, "OK, y position has been modified accordingly after undo (expected: 2, actual: " + box.position.y + ")" );
+	ok( box.rotation.x == 0.1, "OK, x rotation has been modified accordingly after undo (expected: 0.1, actual: " + box.rotation.x + ")" );
+	ok( box.scale.z == 1.3, "OK, z scale has been modified accordingly after undo (expected: 1.3, actual: " + box.scale.z + ")" );
+
+	editor.redo();
+	ok( box.geometry.parameters.widthSegments == 7, "OK, widthSegments has been modified accordingly after two multi executes (expected: 7, actual: " + box.geometry.parameters.widthSegments + ")" );
+	ok( box.position.y == 5, "OK, y position has been modified accordingly after two multi executes (expected: 5, actual: " + box.position.y + ")" );
+	ok( box.rotation.x == 0.4, "OK, x rotation has been modified accordingly after two multi executes (expected: 0.4, actual: " + box.rotation.x + ") " );
+	ok( box.scale.z == 1.6, "OK, z scale has been modified accordingly after two multi executes (expected: 1.6, actual: " + box.scale.z + ")" );
+
+} );

+ 64 - 0
test/unit/editor/TestCmdRemoveScript.js

@@ -0,0 +1,64 @@
+/**
+ * @author lxxxvi / https://github.com/lxxxvi
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+module( "CmdRemoveScript" );
+
+test( "Test CmdRemoveScript (Undo and Redo)", function() {
+
+	var editor = new Editor();
+
+	// prepare
+	var box    = aBox( "The scripted box" );
+	var sphere = aSphere( "The scripted sphere" );
+	var objects = [ box, sphere ];
+
+	var xMove  = { name: "", source: "function update( event ) { this.position.x = this.position.x + 1; }" };
+	var yMove  = { name: "", source: "function update( event ) { this.position.y = this.position.y + 1; }" };
+	var scripts = [ xMove, yMove ];
+
+	// add objects to editor
+	objects.map( function( item ) {
+
+		editor.execute( new CmdAddObject( item ) );
+
+	} );
+	ok( editor.scene.children.length == 2, "OK, the box and the sphere have been added" );
+
+	// add scripts to the objects
+	for ( var i = 0; i < scripts.length; i ++ ) {
+
+		var cmd = new CmdAddScript( objects[ i ], scripts[ i ] );
+		cmd.updatable = false;
+		editor.execute( cmd );
+
+	}
+
+	for ( var i = 0; i < scripts.length; i ++ ) {
+
+		var cmd = new CmdRemoveScript( objects[ i ], scripts[ i ] );
+		cmd.updatable = false;
+		editor.execute( cmd );
+
+	}
+	ok( getScriptCount( editor ) == 0, "OK, all scripts have been removed" );
+
+	scripts.map( function() {
+
+		editor.undo();
+
+	} );
+	ok( getScriptCount( editor ) == scripts.length, "OK, all scripts have been added again by undo(s)" );
+
+	var scriptsKeys = Object.keys( editor.scripts );
+	for ( var i = 0; i < scriptsKeys.length; i ++ ) {
+
+		ok( editor.scripts[ scriptsKeys[ i ] ][ 0 ] == scripts[ i ], "OK, script #" + i + " is still assigned correctly" );
+
+	}
+
+	editor.redo();
+	ok( getScriptCount( editor ) == scripts.length - 1, "OK, one script has been removed again by redo" );
+
+} );

+ 40 - 0
test/unit/editor/TestCmdSetColor.js

@@ -0,0 +1,40 @@
+/**
+ * @author lxxxvi / https://github.com/lxxxvi
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+module( "CmdSetColor" );
+
+test( "Test CmdSetColor (Undo and Redo)", function() {
+
+	var editor = new Editor();
+	var pointLight = aPointlight( "The light Light" );
+	editor.execute( new CmdAddObject( pointLight ) );
+
+	var green   = 12581843; // bffbd3
+	var blue    = 14152447; // d7f2ff
+	var yellow  = 16775383; // fff8d7
+
+	var colors = [ green, blue, yellow ];
+
+	colors.map( function( color ) {
+
+		var cmd = new CmdSetColor( pointLight, 'color', color );
+		cmd.updatable = false;
+		editor.execute( cmd );
+
+	} );
+
+	ok( pointLight.color.getHex() == colors[ colors.length - 1 ],
+		"OK, color has been set successfully (expected: '" + colors[ colors.length - 1 ] + "', actual: '" + pointLight.color.getHex() + "')" );
+
+	editor.undo();
+	ok( pointLight.color.getHex() == colors[ colors.length - 2 ],
+		"OK, color has been set successfully after undo (expected: '" + colors[ colors.length - 2 ] + "', actual: '" + pointLight.color.getHex() + "')" );
+
+	editor.redo();
+	ok( pointLight.color.getHex() == colors[ colors.length - 1 ],
+		"OK, color has been set successfully after redo (expected: '" + colors[ colors.length - 1 ] + "', actual: '" + pointLight.color.getHex() + "')" );
+
+
+} );

+ 63 - 0
test/unit/editor/TestCmdSetGeometry.js

@@ -0,0 +1,63 @@
+/**
+ * @author lxxxvi / https://github.com/lxxxvi
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+module( "CmdSetGeometry" );
+
+test( "Test CmdSetGeometry (Undo and Redo)", function() {
+
+	var editor = new Editor();
+
+	// initialize objects and geometries
+	var box = aBox( 'Guinea Pig' ); // default ( 100, 100, 100, 1, 1, 1 )
+	var boxGeometry1 = { geometry: { parameters: { width: 200, height: 201, depth: 202, widthSegments: 2, heightSegments: 3, depthSegments: 4 } } };
+	var boxGeometry2 = { geometry: { parameters: { width:  50, height:  51, depth:  52, widthSegments: 7, heightSegments: 8, depthSegments: 9 } } };
+	var geometryParams = [ boxGeometry1, boxGeometry2 ];
+
+
+	// add the object
+	var cmd = new CmdAddObject( box );
+	cmd.updatable = false;
+	editor.execute( cmd );
+
+	for ( var i = 0; i < geometryParams.length; i ++ ) {
+
+		var cmd = new CmdSetGeometry( box, getGeometry( "BoxGeometry", geometryParams[ i ] ) );
+		cmd.updatable = false;
+		editor.execute( cmd );
+
+		var actualParams = box.geometry.parameters;
+		var expectedParams = geometryParams[ i ].geometry.parameters;
+
+		ok( actualParams.width == expectedParams.width, "OK, box width matches the corresponding value from boxGeometry"  + ( i + 1 ) );
+		ok( actualParams.height == expectedParams.height, "OK, box height matches the corresponding value from boxGeometry" + ( i + 1 ) );
+		ok( actualParams.depth == expectedParams.depth, "OK, box depth matches the corresponding value from boxGeometry"  + ( i + 1 ) );
+		ok( actualParams.widthSegments == expectedParams.widthSegments, "OK, box widthSegments matches the corresponding value from boxGeometry"  + ( i + 1 ) );
+		ok( actualParams.heightSegments == expectedParams.heightSegments, "OK, box heightSegments matches the corresponding value from boxGeometry"  + ( i + 1 ) );
+		ok( actualParams.depthSegments == expectedParams.depthSegments, "OK, box depthSegments matches the corresponding value from boxGeometry"  + ( i + 1 ) );
+
+	}
+
+	editor.undo();
+	var actualParams = box.geometry.parameters;
+	var expectedParams = geometryParams[ 0 ].geometry.parameters;
+	ok( actualParams.width == expectedParams.width, "OK, box width matches the corresponding value from boxGeometry1 (after undo)" );
+	ok( actualParams.height == expectedParams.height, "OK, box height matches the corresponding value from boxGeometry1 (after undo)" );
+	ok( actualParams.depth == expectedParams.depth, "OK, box depth matches the corresponding value from boxGeometry1 (after undo)" );
+	ok( actualParams.widthSegments == expectedParams.widthSegments, "OK, box widthSegments matches the corresponding value from boxGeometry1 (after undo)" );
+	ok( actualParams.heightSegments == expectedParams.heightSegments, "OK, box heightSegments matches the corresponding value from boxGeometry1 (after undo)" );
+	ok( actualParams.depthSegments == expectedParams.depthSegments, "OK, box depthSegments matches the corresponding value from boxGeometry1 (after undo)" );
+
+	editor.redo();
+	var actualParams = box.geometry.parameters;
+	var expectedParams = geometryParams[ 1 ].geometry.parameters;
+	ok( actualParams.width == expectedParams.width, "OK, box width matches the corresponding value from boxGeometry2 (after redo)" );
+	ok( actualParams.height == expectedParams.height, "OK, box height matches the corresponding value from boxGeometry2 (after redo)" );
+	ok( actualParams.depth == expectedParams.depth, "OK, box depth matches the corresponding value from boxGeometry2 (after redo)" );
+	ok( actualParams.widthSegments == expectedParams.widthSegments, "OK, box widthSegments matches the corresponding value from boxGeometry2 (after redo)" );
+	ok( actualParams.heightSegments == expectedParams.heightSegments, "OK, box heightSegments matches the corresponding value from boxGeometry2 (after redo)" );
+	ok( actualParams.depthSegments == expectedParams.depthSegments, "OK, box depthSegments matches the corresponding value from boxGeometry2 (after redo)" );
+
+
+} );

+ 51 - 0
test/unit/editor/TestCmdSetGeometryValue.js

@@ -0,0 +1,51 @@
+/**
+ * @author lxxxvi / https://github.com/lxxxvi
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+module( "CmdSetGeometryValue" );
+
+test( "Test CmdSetGeometryValue (Undo and Redo)", function() {
+
+	var editor = new Editor();
+	var box = aBox( 'The Box' );
+
+	var cmd = new CmdAddObject( box );
+	cmd.updatable = false;
+	editor.execute( cmd );
+
+	var testData = [
+		{ uuid: THREE.Math.generateUUID(), name: 'Bruno' },
+		{ uuid: THREE.Math.generateUUID(), name: 'Jack' }
+	];
+
+	for ( var i = 0; i < testData.length; i ++ ) {
+
+		var keys = Object.keys( testData[ i ] );
+
+		keys.map( function( key ) {
+
+			cmd = new CmdSetGeometryValue( box, key, testData[ i ][ key ] );
+			cmd.updatable = false;
+			editor.execute( cmd );
+
+		} );
+
+	}
+
+	ok( box.geometry.name == testData[ 1 ].name, "OK, box.geometry.name is correct after executes" );
+	ok( box.geometry.uuid == testData[ 1 ].uuid, "OK, box.geometry.uuid is correct after executes" );
+
+	editor.undo();
+	editor.undo();
+
+	ok( box.geometry.name == testData[ 0 ].name, "OK, box.geometry.name is correct after undos" );
+	ok( box.geometry.uuid == testData[ 0 ].uuid, "OK, box.geometry.uuid is correct after undos" );
+
+	editor.redo();
+	editor.redo();
+
+	ok( box.geometry.name == testData[ 1 ].name, "OK, box.geometry.name is correct after executes" );
+	ok( box.geometry.uuid == testData[ 1 ].uuid, "OK, box.geometry.uuid is correct after executes" );
+
+} );

+ 66 - 0
test/unit/editor/TestCmdSetMaterial.js

@@ -0,0 +1,66 @@
+/**
+ * @author lxxxvi / https://github.com/lxxxvi
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+module( "CmdSetMaterial" );
+
+test( "Test for CmdSetMaterial (Undo and Redo)", function() {
+
+	// setup
+	var editor = new Editor();
+	var box = aBox( 'Material girl in a material world' );
+	var cmd = new CmdAddObject( box );
+	cmd.updatable = false;
+	editor.execute( cmd );
+
+	materialClasses = [
+
+		'LineBasicMaterial',
+		'LineDashedMaterial',
+		'MeshBasicMaterial',
+		'MeshDepthMaterial',
+		'MeshLambertMaterial',
+		'MeshNormalMaterial',
+		'MeshPhongMaterial',
+		'ShaderMaterial',
+		'SpriteMaterial'
+
+	];
+
+	materialClasses.map( function( materialClass ) {
+
+		material = new THREE[ materialClass ]();
+		editor.execute( new CmdSetMaterial( box, material ) );
+
+	} );
+
+	var i = materialClasses.length - 1;
+
+	// initial test
+	ok( box.material.type == materialClasses[ i ],
+		"OK, initial material type was set correctly (expected: '" + materialClasses[ i ] + "', actual: '" + box.material.type + "')" );
+
+
+
+	// test undos
+	while ( i > 0 ) {
+
+		editor.undo();
+		-- i;
+		ok( box.material.type == materialClasses[ i ],
+			"OK, material type was set correctly after undo (expected: '" + materialClasses[ i ] + "', actual: '" + box.material.type + "')" );
+
+	}
+
+	// test redos
+	while ( i < materialClasses.length - 1 ) {
+
+		editor.redo();
+		++ i;
+		ok( box.material.type == materialClasses[ i ],
+			"OK, material type was set correctly after redo (expected: '" + materialClasses[ i ] + "', actual: '" + box.material.type + "')" );
+
+	}
+
+} );

+ 45 - 0
test/unit/editor/TestCmdSetMaterialColor.js

@@ -0,0 +1,45 @@
+/**
+ * @author lxxxvi / https://github.com/lxxxvi
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+module( "CmdSetMaterialColor" );
+
+test( "Test for CmdSetMaterialColor (Undo and Redo)", function() {
+
+	// Setup scene
+	var editor = new Editor();
+	var box = aBox();
+	var cmd = new CmdAddObject( box );
+	cmd.updatable = false;
+	editor.execute( cmd );
+
+	var green   = 12581843; // bffbd3
+	var blue    = 14152447; // d7f2ff
+	var yellow  = 16775383; // fff8d7
+
+	// there have to be at least 2 colors !
+	colors = [ green, blue, yellow ];
+
+	[ 'color', 'emissive', 'specular' ].map( function( attributeName ) {
+
+		colors.map( function ( color )  {
+
+			var cmd = new CmdSetMaterialColor( box, attributeName, color );
+			cmd.updatable = false;
+			editor.execute( cmd );
+
+		} );
+
+		ok( box.material[ attributeName ].getHex() == colors[ colors.length - 1 ], "OK, " + attributeName + " was set correctly to last color " );
+
+		editor.undo();
+		ok( box.material[ attributeName ].getHex() == colors[ colors.length - 2 ], "OK, " + attributeName + " is set correctly to second to last color after undo" );
+
+		editor.redo();
+		ok( box.material[ attributeName ].getHex() == colors[ colors.length - 1 ], "OK, " + attributeName + " is set correctly to last color after redo" );
+
+
+	} );
+
+} );

+ 70 - 0
test/unit/editor/TestCmdSetMaterialMap.js

@@ -0,0 +1,70 @@
+/**
+ * @author lxxxvi / https://github.com/lxxxvi
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+module( "CmdSetMaterialMap" );
+
+test( "Test for CmdSetMaterialMap (Undo and Redo)", function() {
+
+	// setup
+	var editor = new Editor();
+	var box = aBox( 'Material mapped box' );
+	var cmd = new CmdAddObject( box );
+	cmd.updatable = false;
+	editor.execute( cmd );
+
+	var mapNames = [ 'map', 'alphaMap', 'bumpMap', 'normalMap', 'displacementMap', 'specularMap', 'envMap', 'lightMap', 'aoMap' ];
+
+	// define files
+	var dirt  = { name: 'dirt.png' , data: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpDMjYxMEI4MzVENDMxMUU1OTdEQUY4QkNGNUVENjg4MyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpDMjYxMEI4NDVENDMxMUU1OTdEQUY4QkNGNUVENjg4MyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkMyNjEwQjgxNUQ0MzExRTU5N0RBRjhCQ0Y1RUQ2ODgzIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkMyNjEwQjgyNUQ0MzExRTU5N0RBRjhCQ0Y1RUQ2ODgzIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+txizaQAAABVQTFRFh4eHbGxsdFhEWT0puYVclmxKeVU6ppwr+AAAAHtJREFUeNosjgEWBCEIQplFuP+RB5h9lZn2EZxkLzC3D1YSgSlmk7i0ctzDZNBz/VSoX1KwjlFI8WmA2R7JqUa0LJJcd1rLNWRRaMyi+3Y16qMKHhdE48XLsDyHKJ0nSMazY1fxHyriXxV584tmEedcfGNrA/5cmK8AAwCT9ATehDDyzwAAAABJRU5ErkJggg==' };
+	var stone = { name: 'stone.png', data: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpDMjYxMEI4NzVENDMxMUU1OTdEQUY4QkNGNUVENjg4MyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpDMjYxMEI4ODVENDMxMUU1OTdEQUY4QkNGNUVENjg4MyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkMyNjEwQjg1NUQ0MzExRTU5N0RBRjhCQ0Y1RUQ2ODgzIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkMyNjEwQjg2NUQ0MzExRTU5N0RBRjhCQ0Y1RUQ2ODgzIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+WCxVbAAAAA9QTFRFgICAaGhoj4+Pf39/dHR0lCmHpAAAAG9JREFUeNo8TkkSBDEIEuH/bx7A9HigYlhkKA93sfcaetn10whSQ0jILpqjFQYiqGepXuexaxRpqAQInF2rzJKNx/CZm6yGuoKOzszjL0LbYjlgxzZokJA6CvgsenUaGm3PRPI/W1MUrXC9+v0TYACSUwL7JYc6IAAAAABJRU5ErkJggg==' };
+	var files = [ dirt, stone ];
+
+	// define images for given files
+	var images = files.map( function( file ) {
+
+		var i = new Image();
+		i.src = file.data;
+		return { name: file.name, image: i };
+
+	} );
+
+
+	// test all maps
+	mapNames.map( function( mapName ) {
+
+
+		// define textures for given images
+		var textures = images.map( function( img ) {
+
+			var texture = new THREE.Texture( img.image, mapName );
+			texture.sourceFile = img.name;
+			return texture;
+
+		} );
+
+		// apply the textures
+		textures.map( function( texture ) {
+
+			var cmd = new CmdSetMaterialMap( box, mapName, texture );
+			cmd.updatable = false;
+			editor.execute( cmd );
+
+		} );
+
+
+		ok( box.material[ mapName ].image.src == images[ images.length - 1 ].image.src,
+			"OK, " + mapName + " set correctly " );
+
+		editor.undo();
+		ok( box.material[ mapName ].image.src == images[ images.length - 2 ].image.src,
+			"OK, " + mapName + " set correctly after undo " );
+
+		editor.redo();
+		ok( box.material[ mapName ].image.src == images[ images.length - 1 ].image.src,
+			"OK, " + mapName + " set correctly after redo" );
+
+	} );
+
+} );

+ 63 - 0
test/unit/editor/TestCmdSetMaterialValue.js

@@ -0,0 +1,63 @@
+/**
+ * @author lxxxvi / https://github.com/lxxxvi
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+module( "CmdSetMaterialValue" );
+
+test( "Test for CmdSetMaterialValue (Undo and Redo)", function() {
+
+	// setup scene
+	var editor = new Editor();
+	var box = aBox();
+	var cmd = new CmdAddObject( box );
+	cmd.updatable = false;
+	editor.execute( cmd );
+
+	// every attribute gets three test values
+	var testData = {
+
+		uuid: [ THREE.Math.generateUUID(), THREE.Math.generateUUID(), THREE.Math.generateUUID() ],
+		name: [ 'Alpha', 'Bravo', 'Charlie' ],
+		shininess: [ 11.1, 22.2, 33.3 ],
+		vertexColors: [ 'No', 'Face', 'Vertex' ],
+		bumpScale: [ 1.1, 2.2, 3.3 ],
+		reflectivity: [ - 1.3, 2.1, 5.0 ],
+		aoMapIntensity: [ 0.1, 0.4, 0.7 ],
+		side: [ 'Front', 'Back', 'Double' ],
+		shading: [ 'No', 'Flat', 'Smooth' ],
+		blending: [ 'No', 'Normal', 'Additive' ],
+		opacity: [ 0.2, 0.5, 0.8 ],
+		alphaTest: [ 0.1, 0.6, 0.9 ],
+		wirefrimeLinewidth: [ 1.2, 3.4, 5.6 ]
+
+	};
+
+	var testDataKeys = Object.keys( testData );
+
+	testDataKeys.map( function( attributeName ) {
+
+		testData[ attributeName ].map( function( value ) {
+
+			var cmd = new CmdSetMaterialValue( box, attributeName, value );
+			cmd.updatable = false;
+			editor.execute( cmd );
+
+		} );
+
+		var length = testData[ attributeName ].length;
+		ok( box.material[ attributeName ] == testData[ attributeName ][ length - 1 ],
+			"OK, " + attributeName + " was set correctly to the last value (expected: '" + testData[ attributeName ][ length - 1 ] + "', actual: '" + box.material[ attributeName ] + "')" );
+
+		editor.undo();
+		ok( box.material[ attributeName ] == testData[ attributeName ][ length - 2 ],
+			"OK, " + attributeName + " was set correctly to the second to the last value after undo (expected: '" + testData[ attributeName ][ length - 2 ] + "', actual: '" + box.material[ attributeName ] + "')" );
+
+		editor.redo();
+		ok( box.material[ attributeName ] == testData[ attributeName ][ length - 1 ],
+			"OK, " + attributeName + " was set correctly to the last value again after redo (expected: '" + testData[ attributeName ][ length - 1 ] + "', actual: '" + box.material[ attributeName ] + "')" );
+
+	} );
+
+
+} );

+ 48 - 0
test/unit/editor/TestCmdSetPosition.js

@@ -0,0 +1,48 @@
+/**
+ * @author lxxxvi / https://github.com/lxxxvi
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+module( "CmdSetPosition" );
+
+test( "Test CmdSetPosition (Undo and Redo)", function() {
+
+	var editor = new Editor();
+	var box = aBox();
+	var cmd = new CmdAddObject( box );
+	editor.execute( cmd );
+
+	var positions = [
+
+		{ x:   50, y: - 80, z: 30 },
+		{ x: - 10, y:  100, z:  0 },
+		{ x:   44, y: - 20, z: 90 }
+
+	];
+
+	positions.map( function( position ) {
+
+		var newPosition = new THREE.Vector3( position.x, position.y, position.z );
+		var cmd = new CmdSetPosition( box, newPosition );
+		cmd.updatable = false;
+		editor.execute( cmd );
+
+	} );
+
+	ok( box.position.x == positions[ positions.length - 1 ].x, "OK, changing X position was successful" );
+	ok( box.position.y == positions[ positions.length - 1 ].y, "OK, changing Y position was successful" );
+	ok( box.position.z == positions[ positions.length - 1 ].z, "OK, changing Z position was successful" );
+
+
+	editor.undo();
+	ok( box.position.x == positions[ positions.length - 2 ].x, "OK, changing X position was successful (after undo)" );
+	ok( box.position.y == positions[ positions.length - 2 ].y, "OK, changing Y position was successful (after undo)" );
+	ok( box.position.z == positions[ positions.length - 2 ].z, "OK, changing Z position was successful (after undo)" );
+
+	editor.redo();
+	ok( box.position.x == positions[ positions.length - 1 ].x, "OK, changing X position was successful (after redo)" );
+	ok( box.position.y == positions[ positions.length - 1 ].y, "OK, changing Y position was successful (after redo)" );
+	ok( box.position.z == positions[ positions.length - 1 ].z, "OK, changing Z position was successful (after redo)" );
+
+
+} );

+ 51 - 0
test/unit/editor/TestCmdSetRotation.js

@@ -0,0 +1,51 @@
+/**
+ * @author lxxxvi / https://github.com/lxxxvi
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+module( "CmdSetRotation" );
+
+test( "Test CmdSetRotation (Undo and Redo)", function() {
+
+	// setup
+	var editor = new Editor();
+	var box = aBox();
+	editor.execute( new CmdAddObject( box ) );
+
+
+	var rotations = [
+
+		{ x: 1.1, y:   0.4, z: - 2.0 },
+		{ x: 2.2, y: - 1.3, z:   1.3 },
+		{ x: 0.3, y: - 0.1, z: - 1.9 }
+
+	];
+
+
+	rotations.map( function( rotation ) {
+
+		var newRotation = new THREE.Euler( rotation.x, rotation.y, rotation.z );
+		var cmd = new CmdSetRotation( box, newRotation );
+		cmd.updatable = false;
+		editor.execute ( cmd );
+
+	} );
+
+
+	ok( box.rotation.x == rotations[ rotations.length - 1 ].x, "OK, changing X rotation was successful" );
+	ok( box.rotation.y == rotations[ rotations.length - 1 ].y, "OK, changing Y rotation was successful" );
+	ok( box.rotation.z == rotations[ rotations.length - 1 ].z, "OK, changing Z rotation was successful" );
+
+	editor.undo();
+	ok( box.rotation.x == rotations[ rotations.length - 2 ].x, "OK, changing X rotation was successful (after undo)" );
+	ok( box.rotation.y == rotations[ rotations.length - 2 ].y, "OK, changing Y rotation was successful (after undo)" );
+	ok( box.rotation.z == rotations[ rotations.length - 2 ].z, "OK, changing Z rotation was successful (after undo)" );
+
+	editor.redo();
+	ok( box.rotation.x == rotations[ rotations.length - 1 ].x, "OK, changing X rotation was successful (after redo)" );
+	ok( box.rotation.y == rotations[ rotations.length - 1 ].y, "OK, changing Y rotation was successful (after redo)" );
+	ok( box.rotation.z == rotations[ rotations.length - 1 ].z, "OK, changing Z rotation was successful (after redo)" );
+
+
+
+} );

+ 51 - 0
test/unit/editor/TestCmdSetScale.js

@@ -0,0 +1,51 @@
+/**
+ * @author lxxxvi / https://github.com/lxxxvi
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+module( "CmdSetScale" );
+
+test( "Test CmdSetScale (Undo and Redo)", function() {
+
+	// setup
+	var editor = new Editor();
+	var box = aBox();
+	editor.execute( new CmdAddObject( box ) );
+
+
+	// scales
+	var scales = [
+
+		{ x: 1.4, y: 2.7, z: 0.4 },
+		{ x: 0.1, y: 1.3, z: 2.9 },
+		{ x: 3.2, y: 0.3, z: 2.0 }
+
+	];
+
+	scales.map( function( scale ) {
+
+		var newScale = new THREE.Vector3( scale.x, scale.y, scale.z );
+		var cmd = new CmdSetScale( box, newScale );
+		cmd.updatable = false;
+		editor.execute( cmd );
+
+	} );
+
+	ok( box.scale.x == scales[ scales.length - 1 ].x, "OK, setting X scale value was successful" );
+	ok( box.scale.y == scales[ scales.length - 1 ].y, "OK, setting Y scale value was successful" );
+	ok( box.scale.z == scales[ scales.length - 1 ].z, "OK, setting Z scale value was successful" );
+
+
+	editor.undo();
+	ok( box.scale.x == scales[ scales.length - 2 ].x, "OK, X scale is correct after undo" );
+	ok( box.scale.y == scales[ scales.length - 2 ].y, "OK, Y scale is correct after undo" );
+	ok( box.scale.z == scales[ scales.length - 2 ].z, "OK, Z scale is correct after undo" );
+
+
+	editor.redo();
+	ok( box.scale.x == scales[ scales.length - 1 ].x, "OK, X scale is correct after redo" );
+	ok( box.scale.y == scales[ scales.length - 1 ].y, "OK, Y scale is correct after redo" );
+	ok( box.scale.z == scales[ scales.length - 1 ].z, "OK, Z scale is correct after redo" );
+
+
+} );

+ 73 - 0
test/unit/editor/TestCmdSetScene.js

@@ -0,0 +1,73 @@
+/**
+ * @author lxxxvi / https://github.com/lxxxvi
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+module( "TestCmdSetScene" );
+
+test( "Test for CmdSetScene (Undo and Redo)", function() {
+
+	// setup
+	var editor = new Editor();
+	objects = [ aBox(), aSphere(), aPointlight() ];
+
+
+	// create multiple editors (scenes) and save the output
+	var scenes = objects.map( function( object ) {
+
+		editor = new Editor();
+		var cmd = new CmdAddObject( object );
+		cmd.updatable = false;
+		editor.execute( cmd );
+		return { obj: object, exportedData: exportScene( editor ) };
+
+	} );
+
+
+	// create new empty editor (scene), merge the other editors (scenes)
+	editor = new Editor();
+	scenes.map( function( scene ) {
+
+		var importedScene = importScene( scene.exportedData );
+		var cmd = new CmdSetScene( importedScene );
+		cmd.updatable = false;
+		editor.execute( cmd );
+
+	} );
+
+	// tests
+	ok( editor.scene.children.length = scenes.length,
+		"OK, all scenes have been merged" );
+
+	var i = 0;
+	while ( i < editor.scene.children.length ) {
+
+		ok( editor.scene.children[ i ].name == scenes[ i ].obj.name,
+			"OK, editor.scene.children[ " + i + " ].name matches scenes[ " + i + " ].obj.name" );
+		i ++;
+
+	}
+
+	editor.undo();
+	var i = 0;
+	while ( i < editor.scene.children.length ) {
+
+		ok( editor.scene.children[ i ].name == scenes[ i ].obj.name,
+			"OK, editor.scene.children[ " + i + " ].name matches scenes[ " + i + " ].obj.name after undo" );
+		i ++;
+
+	}
+
+
+	editor.redo();
+	var i = 0;
+	while ( i < editor.scene.children.length ) {
+
+		ok( editor.scene.children[ i ].name == scenes[ i ].obj.name,
+			"OK, editor.scene.children[ " + i + " ].name matches scenes[ " + i + " ].obj.name after redo" );
+		i ++;
+
+	}
+
+
+} );

+ 77 - 0
test/unit/editor/TestCmdSetScriptValue.js

@@ -0,0 +1,77 @@
+/**
+ * @author lxxxvi / https://github.com/lxxxvi
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+module( "CmdSetScriptValue" );
+
+test( "Test CmdSetScriptValue for source (Undo and Redo)", function() {
+
+
+	// setup
+	var editor = new Editor();
+	var box    = aBox( "The scripted box" );
+	var cmd = new CmdAddObject( box );
+	cmd.updatable = false;
+	editor.execute( cmd );
+
+	var translateScript = { name: "Translate", source: "function( update ) {}" };
+	cmd = new CmdAddScript( box, translateScript );
+	cmd.updatable = false;
+	editor.execute( cmd );
+
+
+	var testSourceData = [
+
+		{ name: "Translate", source: "function update( event ) { this.position.x = this.position.x + 1; }" },
+		{ name: "Translate", source: "function update( event ) { this.position.y = this.position.y + 1; }" },
+		{ name: "Translate", source: "function update( event ) { this.position.z = this.position.z + 1; }" }
+
+	];
+
+
+	// test source
+
+	testSourceData.map( function( script ) {
+
+		var cmd = new CmdSetScriptValue( box, translateScript, 'source', script.source, 0 );
+		cmd.updatable = false;
+		editor.execute( cmd );
+
+	} );
+
+	var length = testSourceData.length;
+	ok( editor.scripts[ box.uuid ][ 0 ][ 'source' ] == testSourceData[ length - 1 ].source,
+		"OK, 'source' was set correctly to the last value (expected: '" + testSourceData[ length - 1 ].source + "', actual: '" + editor.scripts[ box.uuid ][ 0 ][ 'source' ] + "')" );
+
+	editor.undo();
+	ok( editor.scripts[ box.uuid ][ 0 ][ 'source' ] == testSourceData[ length - 2 ].source,
+		"OK, 'source' was set correctly to the second to the last value after undo (expected: '" + testSourceData[ length - 2 ].source + "', actual: '" + editor.scripts[ box.uuid ][ 0 ][ 'source' ] + "')" );
+
+	editor.redo();
+	ok( editor.scripts[ box.uuid ][ 0 ][ 'source' ] == testSourceData[ length - 1 ].source,
+		"OK, 'source' was set correctly to the last value again after redo (expected: '" + testSourceData[ length - 1 ].source + "', actual: '" + editor.scripts[ box.uuid ][ 0 ][ 'source' ]	 + "')" );
+
+
+	var names = [ "X Script", "Y Script", "Z Script" ];
+
+	names.map( function( name ) {
+
+		cmd = new CmdSetScriptValue( box, translateScript, 'name', name );
+		cmd.updatable = false;
+		editor.execute( cmd );
+
+	} );
+
+	var scriptName = editor.scripts[ box.uuid ][ 0 ][ "name" ];
+	ok( scriptName == names[ names.length - 1 ], "OK, the script name corresponds to the last script name that was assigned" );
+
+	editor.undo();
+	scriptName = editor.scripts[ box.uuid ][ 0 ][ "name" ];
+	ok( scriptName == names[ names.length - 2 ], "OK, the script name corresponds to the second last script name that was assigned" );
+
+	editor.redo();
+	scriptName = editor.scripts[ box.uuid ][ 0 ][ "name" ];
+	ok( scriptName == names[ names.length - 1 ], "OK, the script name corresponds to the last script name that was assigned, again" );
+
+} );

+ 37 - 0
test/unit/editor/TestCmdSetUuid.js

@@ -0,0 +1,37 @@
+/**
+ * @author lxxxvi / https://github.com/lxxxvi
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+module( "CmdSetUuid" );
+
+test( "Test CmdSetUuid (Undo and Redo)", function() {
+
+	var editor = new Editor();
+	var object = aBox( 'UUID test box' );
+	editor.execute( new CmdAddObject( object ) );
+
+
+	var uuids = [ THREE.Math.generateUUID(), THREE.Math.generateUUID(), THREE.Math.generateUUID() ];
+
+	uuids.map( function( uuid ) {
+
+		var cmd = new CmdSetUuid( object, uuid );
+		cmd.updatable = false;
+		editor.execute( cmd );
+
+	} );
+
+	ok( object.uuid == uuids[ uuids.length - 1 ],
+		"OK, UUID on actual object matches last UUID in the test data array " );
+
+	editor.undo();
+	ok( object.uuid == uuids[ uuids.length - 2 ],
+		"OK, UUID on actual object matches second to the last UUID in the test data array (after undo)" );
+
+	editor.redo();
+	ok( object.uuid == uuids[ uuids.length - 1 ],
+		"OK, UUID on actual object matches last UUID in the test data array again (after redo) " );
+
+
+} );

+ 54 - 0
test/unit/editor/TestCmdSetValue.js

@@ -0,0 +1,54 @@
+/**
+ * @author lxxxvi / https://github.com/lxxxvi
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+module( "CmdSetValue" );
+
+test( "Test CmdSetValue (Undo and Redo)", function() {
+
+	var editor = new Editor();
+
+	var valueBefore = 1.10;
+	var valueAfter  = 2.20;
+
+	var box   = aBox( 'A Box' );
+	var light = aPointlight( 'A PointLight' );
+	var cam   = aPerspectiveCamera( 'A PerspectiveCamera' );
+
+	[ box, light, cam ].map( function( object ) {
+
+		editor.execute( new CmdAddObject( object ) );
+
+		ok( 0 == 0, "Testing object of type '" + object.type + "'" );
+
+		[ 'name', 'fov', 'near', 'far', 'intensity', 'distance', 'angle', 'exponent', 'decay', 'visible', 'userData' ].map( function( item ) {
+
+			if ( object[ item ] !== undefined ) {
+
+				var cmd = new CmdSetValue( object, item, valueBefore );
+				cmd.updatable = false;
+				editor.execute( cmd );
+				ok( object[ item ] == valueBefore, " OK, the attribute '" + item + "' is correct after first execute (expected: '" + valueBefore + "', actual: '" + object[ item ] + "')" );
+
+				var cmd = new CmdSetValue( object, item, valueAfter );
+				cmd.updatable = false;
+				editor.execute( cmd );
+				ok( object[ item ] == valueAfter, " OK, the attribute '" + item + "' is correct after second execute (expected: '" + valueAfter + "', actual: '" + object[ item ] + "')" );
+
+				editor.undo();
+				ok( object[ item ] == valueBefore, " OK, the attribute '" + item + "' is correct after undo (expected: '" + valueBefore + "', actual: '" + object[ item ] + "')" );
+
+				editor.redo();
+				ok( object[ item ] == valueAfter, " OK, the attribute '" + item + "' is correct after redo (expected: '" + valueAfter + "', actual: '" + object[ item ] + "')" );
+
+			}
+
+		} );
+
+	} );
+
+
+
+
+} );

+ 55 - 0
test/unit/editor/TestMassUndoAndRedo.js

@@ -0,0 +1,55 @@
+/**
+ * @author lxxxvi / https://github.com/lxxxvi
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+module( "MassUndoAndRedo" );
+
+test( "MassUndoAndRedo (stress test)", function() {
+
+	var editor = new Editor();
+
+	var MAX_OBJECTS = 100;
+
+	// add objects
+	var i = 0;
+	while ( i < MAX_OBJECTS ) {
+
+		var object = aSphere( 'Sphere #' + i );
+		var cmd = new CmdAddObject( object );
+		cmd.updatable = false;
+		editor.execute( cmd );
+
+		i ++;
+
+	}
+
+	ok( editor.scene.children.lenght = MAX_OBJECTS,
+		"OK, " + MAX_OBJECTS + " objects have been added" );
+
+	// remove all objects
+	i = 0;
+	while ( i < MAX_OBJECTS ) {
+
+		editor.undo();
+		i ++;
+
+	}
+
+
+	ok( editor.scene.children.length == 0,
+		"OK, all objects have been removed by undos" );
+
+
+	i = 0;
+	while ( i < MAX_OBJECTS ) {
+
+		editor.redo();
+		i ++;
+
+	}
+
+	ok( editor.scene.children.lenght = MAX_OBJECTS,
+		"OK, " + MAX_OBJECTS + " objects have been added again by redos" );
+
+} );

+ 55 - 0
test/unit/editor/TestNegativeCases.js

@@ -0,0 +1,55 @@
+/**
+ * @author lxxxvi / https://github.com/lxxxvi
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+module( "NegativeCases" );
+
+test( "Test unwanted situations ", function() {
+
+	var editor = new Editor();
+
+	// illegal
+	editor.undo();
+	ok( editor.history.undos.length == 0, "OK, (illegal) undo did not affect the undo history" );
+	ok( editor.history.redos.length == 0, "OK, (illegal) undo did not affect the redo history" );
+
+	// illegal
+	editor.redo();
+	ok( editor.history.undos.length == 0, "OK, (illegal) redo did not affect the undo history" );
+	ok( editor.history.redos.length == 0, "OK, (illegal) redo did not affect the redo history" );
+
+
+	var box = aBox();
+	var cmd = new CmdAddObject( box );
+	cmd.updatable = false;
+	editor.execute( cmd );
+
+	ok( editor.history.undos.length == 1, "OK, execute changed undo history" );
+	ok( editor.history.redos.length == 0, "OK, execute did not change redo history" );
+
+	// illegal
+	editor.redo();
+	ok( editor.history.undos.length == 1, "OK, (illegal) redo did not affect the undo history" );
+	ok( editor.history.redos.length == 0, "OK, (illegal) redo did not affect the redo history" );
+
+
+	editor.undo();
+	ok( editor.history.undos.length == 0, "OK, undo changed the undo history" );
+	ok( editor.history.redos.length == 1, "OK, undo changed the redo history" );
+
+	// illegal
+	editor.undo();
+	ok( editor.history.undos.length == 0, "OK, (illegal) undo did not affect the undo history" );
+	ok( editor.history.redos.length == 1, "OK, (illegal) undo did not affect the redo history" );
+
+	editor.redo();
+	ok( editor.history.undos.length == 1, "OK, redo changed the undo history" );
+	ok( editor.history.redos.length == 0, "OK, undo changed the redo history" );
+
+	// illegal
+	editor.redo();
+	ok( editor.history.undos.length == 1, "OK, (illegal) did not affect the undo history" );
+	ok( editor.history.redos.length == 0, "OK, (illegal) did not affect the redo history" );
+
+} );

+ 108 - 0
test/unit/editor/TestNestedDoUndoRedo.js

@@ -0,0 +1,108 @@
+/**
+ * @author lxxxvi / https://github.com/lxxxvi
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+module( "NestedDoUndoRedo" );
+
+test( "Test nested Do's, Undo's and Redo's", function() {
+
+	var editor = new Editor();
+
+	var mesh = aBox( 'One box unlike all others' );
+
+	var initPosX      =  2 ;
+	var initPosY      =  3 ;
+	var initPosZ      =  4 ;
+	var initRotationX = 12 ;
+	var initRotationY = 13 ;
+	var initRotationZ = 14 ;
+	var initScaleX    = 22 ;
+	var initScaleY    = 23 ;
+	var initScaleZ    = 24 ;
+
+	mesh.position.x = initPosX ;
+	mesh.position.y = initPosY ;
+	mesh.position.z = initPosZ ;
+	mesh.rotation.x = initRotationX ;
+	mesh.rotation.y = initRotationY ;
+	mesh.rotation.z = initRotationZ ;
+	mesh.scale.x    = initScaleX ;
+	mesh.scale.y    = initScaleY ;
+	mesh.scale.z    = initScaleZ ;
+
+	// let's begin
+	editor.execute( new CmdAddObject( mesh ) );
+
+	var newPos = new THREE.Vector3( initPosX + 100, initPosY, initPosZ );
+	editor.execute( new CmdSetPosition( mesh, newPos ) );
+
+	var newRotation = new THREE.Euler( initRotationX, initRotationY + 1000, initRotationZ );
+	editor.execute( new CmdSetRotation( mesh, newRotation ) );
+
+	var newScale = new THREE.Vector3( initScaleX, initScaleY, initScaleZ + 10000 );
+	editor.execute( new CmdSetScale( mesh, newScale ) );
+
+
+	/* full check */
+
+	ok( mesh.position.x ==   102, "OK, X position is correct " );
+	ok( mesh.position.y ==     3, "OK, Y position is correct " );
+	ok( mesh.position.z ==     4, "OK, Z position is correct " );
+
+	ok( mesh.rotation.x ==    12, "OK, X rotation is correct " );
+	ok( mesh.rotation.y ==  1013, "OK, Y rotation is correct " );
+	ok( mesh.rotation.z ==    14, "OK, Z rotation is correct " );
+
+	ok( mesh.scale.x    ==    22, "OK, X scale is correct " );
+	ok( mesh.scale.y    ==    23, "OK, Y scale is correct " );
+	ok( mesh.scale.z    == 10024, "OK, Z scale is correct " );
+
+
+	editor.undo();  // rescaling undone
+	editor.undo();  // rotation undone
+	editor.undo();  // translation undone
+
+	/* full check */
+
+	ok( mesh.position.x ==     2, "OK, X position is correct " );
+	ok( mesh.position.y ==     3, "OK, Y position is correct " );
+	ok( mesh.position.z ==     4, "OK, Z position is correct " );
+
+	ok( mesh.rotation.x ==    12, "OK, X rotation is correct " );
+	ok( mesh.rotation.y ==    13, "OK, Y rotation is correct " );
+	ok( mesh.rotation.z ==    14, "OK, Z rotation is correct " );
+
+	ok( mesh.scale.x    ==    22, "OK, X scale is correct " );
+	ok( mesh.scale.y    ==    23, "OK, Y scale is correct " );
+	ok( mesh.scale.z    ==    24, "OK, Z scale is correct " );
+
+
+	editor.redo();  // translation redone
+	editor.redo();  // rotation redone
+
+	editor.execute( new CmdRemoveObject( mesh ) );
+	ok( editor.scene.children.length == 0, "OK, object removal was successful" );
+
+	editor.undo();  // removal undone
+	ok( mesh.rotation.y ==    1013, "OK, Y rotation is correct " );
+
+
+	editor.undo();  // rotation undone (expected!)
+
+	/* full check */
+
+	ok( mesh.position.x ==   102, "OK, X position is correct " );
+	ok( mesh.position.y ==     3, "OK, Y position is correct " );
+	ok( mesh.position.z ==     4, "OK, Z position is correct " );
+
+	ok( mesh.rotation.x ==    12, "OK, X rotation is correct " );
+	ok( mesh.rotation.y ==    13, "OK, Y rotation is correct " );
+	ok( mesh.rotation.z ==    14, "OK, Z rotation is correct " );
+
+	ok( mesh.scale.x    ==    22, "OK, X scale is correct " );
+	ok( mesh.scale.y    ==    23, "OK, Y scale is correct " );
+	ok( mesh.scale.z    ==    24, "OK, Z scale is correct " );
+
+
+} );

+ 358 - 0
test/unit/editor/TestSerialization.js

@@ -0,0 +1,358 @@
+/**
+ * @author lxxxvi / https://github.com/lxxxvi
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+module( "Serialization" );
+
+test( "Test Serialization", function( assert ) {
+
+	// setup
+	var editor = new Editor();
+	var done = assert.async();
+	editor.storage.init( function () {
+
+		performTests();
+		done(); // continue running other tests
+
+	} );
+
+	var green   = 12581843; // bffbd3
+
+	var addObject = function () {
+
+		// setup
+		var box = aBox( 'The Box' );
+
+		// Test Add
+		var cmd = new CmdAddObject( box );
+		cmd.updatable = false;
+
+		editor.execute( cmd );
+
+		return "addObject";
+
+	};
+
+	var addScript = function () {
+
+		// setup
+		var box = aBox( 'The Box' );
+
+		// Test Add
+
+		var cmd = new CmdAddObject( box );
+		editor.execute( cmd );
+
+		var cmd = new CmdAddScript( box, { "name": "test", "source": "console.log(\"hello world\");" } );
+		cmd.updatable = false;
+
+		editor.execute( cmd );
+
+		return "addScript";
+
+	};
+
+	var moveObject = function () {
+
+		// create some objects
+		var anakinsName = 'Anakin Skywalker';
+		var lukesName   = 'Luke Skywalker';
+		var anakinSkywalker = aSphere( anakinsName );
+		var lukeSkywalker   = aBox( lukesName );
+
+		editor.execute( new CmdAddObject( anakinSkywalker ) );
+		editor.execute( new CmdAddObject( lukeSkywalker ) );
+
+		// Tell Luke, Anakin is his father
+		editor.execute( new CmdMoveObject( lukeSkywalker, anakinSkywalker ) );
+
+		return "moveObject";
+
+	};
+
+	var removeScript = function () {
+
+		var box = aBox( 'Box with no script' );
+		editor.execute( new CmdAddObject( box ) );
+
+		var script = { "name": "test", "source": "console.log(\"hello world\");" } ;
+		var cmd = new CmdAddScript( box, script );
+		cmd.updatable = false;
+
+		editor.execute( cmd );
+
+		cmd = new CmdRemoveScript( box, script );
+		editor.execute( cmd );
+
+		return "removeScript";
+
+	};
+
+	var setColor = function () {
+
+		var pointLight = aPointlight( "The light Light" );
+
+		editor.execute( new CmdAddObject( pointLight ) );
+		var cmd = new CmdSetColor( pointLight, 'color', green );
+		cmd.updatable = false;
+		editor.execute( cmd );
+
+		return "setColor";
+
+	};
+
+	var setGeometry = function () {
+
+		var box = aBox( 'Guinea Pig' ); // default ( 100, 100, 100, 1, 1, 1 )
+		var boxGeometry = { geometry: { parameters: { width: 200, height: 201, depth: 202, widthSegments: 2, heightSegments: 3, depthSegments: 4 } } };
+
+		editor.execute( new CmdAddObject( box ) );
+
+		var cmd = new CmdSetGeometry( box, getGeometry( "BoxGeometry", boxGeometry ) );
+		cmd.updatable = false;
+		editor.execute( cmd );
+
+		return "setGeometry";
+
+	};
+
+	var setGeometryValue = function() {
+
+		var box = aBox( 'Geometry Value Box' );
+		editor.execute( new CmdAddObject( box ) );
+
+		cmd = new CmdSetGeometryValue( box, 'uuid', THREE.Math.generateUUID() );
+		cmd.updatable = false;
+		editor.execute( cmd );
+
+		return "setGeometryValue";
+
+	};
+
+	var setMaterial = function () {
+
+		var sphere = aSphere( 'The Sun' );
+		editor.execute( new CmdAddObject( sphere ) );
+
+		var material = new THREE[ 'MeshPhongMaterial' ]();
+		var cmd = new CmdSetMaterial( sphere, material );
+		cmd.updatable = false;
+		editor.execute( cmd );
+
+		return "setMaterial";
+
+	};
+
+	var setMaterialColor = function () {
+
+		var box = aBox( 'Box with colored material' );
+		editor.execute( new CmdAddObject( box ) );
+
+		var cmd = new CmdSetMaterialColor( box, 'color', green );
+		cmd.updatable = false;
+		editor.execute( cmd );
+
+		return "setMaterialColor";
+
+	};
+
+	var setMaterialMap = function () {
+
+		var sphere = aSphere( 'Sphere with texture' );
+		editor.execute( new CmdAddObject( sphere ) );
+
+		// dirt.png
+		var data = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpDMjYxMEI4MzVENDMxMUU1OTdEQUY4QkNGNUVENjg4MyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpDMjYxMEI4NDVENDMxMUU1OTdEQUY4QkNGNUVENjg4MyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkMyNjEwQjgxNUQ0MzExRTU5N0RBRjhCQ0Y1RUQ2ODgzIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkMyNjEwQjgyNUQ0MzExRTU5N0RBRjhCQ0Y1RUQ2ODgzIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+txizaQAAABVQTFRFh4eHbGxsdFhEWT0puYVclmxKeVU6ppwr+AAAAHtJREFUeNosjgEWBCEIQplFuP+RB5h9lZn2EZxkLzC3D1YSgSlmk7i0ctzDZNBz/VSoX1KwjlFI8WmA2R7JqUa0LJJcd1rLNWRRaMyi+3Y16qMKHhdE48XLsDyHKJ0nSMazY1fxHyriXxV584tmEedcfGNrA/5cmK8AAwCT9ATehDDyzwAAAABJRU5ErkJggg==';
+		var img = new Image();
+		img.src = data;
+
+		var texture = new THREE.Texture( img, 'map' );
+		texture.sourceFile = 'dirt.png';
+
+		var cmd = new CmdSetMaterialMap( sphere, 'map', texture );
+		cmd.updatable = false;
+		editor.execute( cmd );
+
+		return "setMaterialMap";
+
+	};
+
+	var setMaterialValue = function () {
+
+		var box = aBox( 'Box with values' );
+		editor.execute( new CmdAddObject( box ) );
+
+		var cmd = new CmdSetMaterialValue( box, 'name', 'Bravo' );
+		cmd.updatable = false;
+		editor.execute( cmd );
+
+		return "setMaterialValue";
+
+	};
+
+	var setPosition = function () {
+
+		var sphere = aSphere( 'Sphere with position' );
+		editor.execute( new CmdAddObject( sphere ) );
+
+		var newPosition = new THREE.Vector3( 101, 202, 303 );
+		var cmd = new CmdSetPosition( sphere, newPosition );
+		cmd.updatable = false;
+		editor.execute( cmd );
+
+		return "setPosition";
+
+	};
+
+	var setRotation = function () {
+
+		var box = aBox( 'Box with rotation' );
+		editor.execute( new CmdAddObject( box ) );
+
+		var newRotation = new THREE.Euler( 0.3, - 1.7, 2 );
+		var cmd = new CmdSetRotation( box, newRotation );
+		cmd.updatable = false;
+		editor.execute ( cmd );
+
+		return "setRotation";
+
+	};
+
+	var setScale = function () {
+
+		var sphere = aSphere( 'Sphere with scale' );
+		editor.execute( new CmdAddObject( sphere ) );
+
+		var newScale = new THREE.Vector3( 1.2, 3.3, 4.6 );
+		var cmd = new CmdSetScale( sphere, newScale );
+		cmd.updatable = false;
+		editor.execute( cmd );
+
+		return "setScale";
+
+	};
+
+	var setScriptValue = function () {
+
+		var box = aBox( 'Box with script' );
+		editor.execute( new CmdAddObject( box ) );
+		var script = { name: "Alert", source: "alert( null );" };
+		editor.execute( new CmdAddScript( box, script ) );
+
+		var newScript = { name: "Console", source: "console.log( null );" };
+		var cmd = new CmdSetScriptValue( box, script, 'source', newScript.source, 0 );
+		cmd.updatable = false;
+		editor.execute( cmd );
+
+		return "setScriptValue";
+
+	};
+
+	var setUuid = function () {
+
+		var sphere = aSphere( 'Sphere with UUID' );
+		editor.execute( new CmdAddObject( sphere ) );
+
+		var cmd = new CmdSetUuid( sphere, THREE.Math.generateUUID() );
+		cmd.updatable = false;
+		editor.execute( cmd );
+
+		return "setUuid";
+
+	};
+
+	var setValue = function () {
+
+		var box = aBox( 'Box with value' );
+		editor.execute( new CmdAddObject( box ) );
+
+		var cmd = new CmdSetValue( box, 'intensity', 2.3 );
+		cmd.updatable = false;
+		editor.execute( cmd );
+
+		return "setValue";
+
+	};
+
+	var setups = [
+
+		addObject,
+		addScript,
+		moveObject,
+		removeScript,
+		setColor,
+		setGeometry,
+		setGeometryValue,
+		setMaterial,
+		setMaterialColor,
+		setMaterialMap,
+		setMaterialValue,
+		setPosition,
+		setRotation,
+		setScale,
+		setScriptValue,
+		setUuid,
+		setValue
+
+	];
+
+	function performTests() {
+
+		// Forward tests
+
+		for ( var i = 0; i < setups.length ; i ++ ) {
+
+			var name = setups[ i ]();
+
+			// Check for correct serialization
+
+			editor.history.goToState( 0 );
+			editor.history.goToState( 1000 );
+
+			var history = JSON.stringify( editor.history.toJSON() );
+
+			editor.history.clear();
+
+			editor.history.fromJSON( JSON.parse( history ) );
+
+			editor.history.goToState( 0 );
+			editor.history.goToState( 1000 );
+
+			var history2 = JSON.stringify( editor.history.toJSON() );
+
+			ok( history == history2, "OK, forward serializing was successful for " + name );
+
+			editor.clear();
+
+		}
+
+		// Backward tests
+
+		for ( var i = 0; i < setups.length ; i ++ ) {
+
+			var name = setups[ i ]();
+
+			editor.history.goToState( 0 );
+
+			var history = JSON.stringify( editor.history.toJSON() );
+
+			editor.history.clear();
+
+			editor.history.fromJSON( JSON.parse( history ) );
+
+			editor.history.goToState( 1000 );
+			editor.history.goToState( 0 );
+
+			var history2 = JSON.stringify( editor.history.toJSON() );
+
+			ok( history == history2, "OK, backward serializing was successful for " + name );
+
+			editor.clear();
+
+		}
+
+	}
+
+} );
+

+ 124 - 0
test/unit/unittests_editor.html

@@ -0,0 +1,124 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>ThreeJS Unit Tests - Using Files in /editor</title>
+  <link rel="stylesheet" href="qunit-1.18.0.css">
+</head>
+<body>
+<div id="qunit"></div>
+<script src="../unit/qunit-1.18.0.js"></script>
+
+<!-- add sources to test below -->
+<script src="../../build/three.min.js"></script>
+
+<script src="../../editor/js/libs/codemirror/codemirror.js"></script>
+<script src="../../editor/js/libs/codemirror/mode/javascript.js"></script>
+<script src="../../editor/js/libs/esprima.js"></script>
+
+<script src="../../editor/js/libs/sortable.min.js"></script>
+<script src="../../editor/js/libs/signals.min.js"></script>
+<script src="../../editor/js/libs/ui.js"></script>
+<script src="../../editor/js/libs/ui.three.js"></script>
+
+<script src="../../editor/js/libs/app.js"></script>
+<script src="../../editor/js/Player.js"></script>
+<script src="../../editor/js/Script.js"></script>
+
+<!--
+<script src="../examples/js/effects/VREffect.js"></script>
+<script src="../examples/js/controls/VRControls.js"></script>
+-->
+
+<script src="../../editor/js/Storage.js"></script>
+
+<script src="../../editor/js/Editor.js"></script>
+<script src="../../editor/js/Config.js"></script>
+<script src="../../editor/js/Loader.js"></script>
+<script src="../../editor/js/Menubar.js"></script>
+<script src="../../editor/js/Menubar.File.js"></script>
+<script src="../../editor/js/Menubar.Edit.js"></script>
+<script src="../../editor/js/Menubar.Add.js"></script>
+<script src="../../editor/js/Menubar.Play.js"></script>
+<script src="../../editor/js/Menubar.View.js"></script>
+<script src="../../editor/js/Menubar.Examples.js"></script>
+<script src="../../editor/js/Menubar.Help.js"></script>
+<script src="../../editor/js/Menubar.Status.js"></script>
+<script src="../../editor/js/Sidebar.js"></script>
+<script src="../../editor/js/Sidebar.Project.js"></script>
+<script src="../../editor/js/Sidebar.Scene.js"></script>
+<script src="../../editor/js/Sidebar.Object3D.js"></script>
+<script src="../../editor/js/Sidebar.Animation.js"></script>
+<script src="../../editor/js/Sidebar.Geometry.js"></script>
+<script src="../../editor/js/Sidebar.Geometry.Geometry.js"></script>
+<script src="../../editor/js/Sidebar.Geometry.BufferGeometry.js"></script>
+<script src="../../editor/js/Sidebar.Geometry.Modifiers.js"></script>
+<script src="../../editor/js/Sidebar.Geometry.BoxGeometry.js"></script>
+<script src="../../editor/js/Sidebar.Geometry.CircleGeometry.js"></script>
+<script src="../../editor/js/Sidebar.Geometry.CylinderGeometry.js"></script>
+<script src="../../editor/js/Sidebar.Geometry.IcosahedronGeometry.js"></script>
+<script src="../../editor/js/Sidebar.Geometry.PlaneGeometry.js"></script>
+<script src="../../editor/js/Sidebar.Geometry.SphereGeometry.js"></script>
+<script src="../../editor/js/Sidebar.Geometry.TorusGeometry.js"></script>
+<script src="../../editor/js/Sidebar.Geometry.TorusKnotGeometry.js"></script>
+<script src="../../editor/js/Sidebar.Material.js"></script>
+<script src="../../editor/js/Sidebar.Script.js"></script>
+<script src="../../editor/js/Toolbar.js"></script>
+<script src="../../editor/js/Viewport.js"></script>
+<script src="../../editor/js/Viewport.Info.js"></script>
+<script src="../../editor/js/History.js"></script>
+
+<!-- command object classes -->
+<script src="../../editor/js/Cmd.js"></script>
+<script src="../../editor/js/CmdAddObject.js"></script>
+<script src="../../editor/js/CmdAddScript.js"></script>
+<script src="../../editor/js/CmdMoveObject.js"></script>
+<script src="../../editor/js/CmdMultiCmds.js"></script>
+<script src="../../editor/js/CmdRemoveObject.js"></script>
+<script src="../../editor/js/CmdRemoveScript.js"></script>
+<script src="../../editor/js/CmdSetColor.js"></script>
+<script src="../../editor/js/CmdSetGeometry.js"></script>
+<script src="../../editor/js/CmdSetGeometryValue.js"></script>
+<script src="../../editor/js/CmdSetMaterial.js"></script>
+<script src="../../editor/js/CmdSetMaterialColor.js"></script>
+<script src="../../editor/js/CmdSetMaterialMap.js"></script>
+<script src="../../editor/js/CmdSetMaterialValue.js"></script>
+<script src="../../editor/js/CmdSetPosition.js"></script>
+<script src="../../editor/js/CmdSetRotation.js"></script>
+<script src="../../editor/js/CmdSetScale.js"></script>
+<script src="../../editor/js/CmdSetScene.js"></script>
+<script src="../../editor/js/CmdSetScriptValue.js"></script>
+<script src="../../editor/js/CmdSetUuid.js"></script>
+<script src="../../editor/js/CmdSetValue.js"></script>
+
+
+<!-- add class-based unit tests below -->
+<script src="editor/CommonUtilities.js"></script>
+
+<!-- Undo-Redo tests -->
+<script src="editor/TestCmdAddObjectAndCmdRemoveObject.js"></script>
+<script src="editor/TestCmdAddScript.js"></script>
+<script src="editor/TestCmdMoveObject.js"></script>
+<script src="editor/TestCmdMultiCmds.js"></script>
+<script src="editor/TestCmdRemoveScript.js"></script>
+<script src="editor/TestCmdSetColor.js"></script>
+<script src="editor/TestCmdSetGeometry.js"></script>
+<script src="editor/TestCmdSetGeometryValue.js"></script>
+<script src="editor/TestCmdSetMaterial.js"></script>
+<script src="editor/TestCmdSetMaterialColor.js"></script>
+<script src="editor/TestCmdSetMaterialMap.js"></script>
+<script src="editor/TestCmdSetMaterialValue.js"></script>
+<script src="editor/TestCmdSetPosition.js"></script>
+<script src="editor/TestCmdSetRotation.js"></script>
+<script src="editor/TestCmdSetScale.js"></script>
+<script src="editor/TestCmdSetScene.js"></script>
+<script src="editor/TestCmdSetScriptValue.js"></script>
+<script src="editor/TestCmdSetUuid.js"></script>
+<script src="editor/TestCmdSetValue.js"></script>
+<script src="editor/TestNestedDoUndoRedo.js"></script>
+<script src="editor/TestSerialization.js"></script>
+<script src="editor/TestNegativeCases.js"></script>
+<script src="editor/TestMassUndoAndRedo.js"></script>
+
+</body>
+</html>