Browse Source

Merged changes from master- into dev-branch #17

- Resulting issues: #29, #30
- Player behaviour where the player stops when a command is executed,
undone or redone removed.
Daniel 10 years ago
parent
commit
472ed080fc
68 changed files with 3300 additions and 237 deletions
  1. 1 0
      .gitignore
  2. 8 1
      editor/css/dark.css
  3. 7 0
      editor/css/light.css
  4. 35 0
      editor/index.html
  5. 28 0
      editor/js/Cmd.js
  6. 68 0
      editor/js/CmdAddObject.js
  7. 70 0
      editor/js/CmdAddScript.js
  8. 99 0
      editor/js/CmdMoveObject.js
  9. 80 0
      editor/js/CmdMultiCmds.js
  10. 103 0
      editor/js/CmdRemoveObject.js
  11. 73 0
      editor/js/CmdRemoveScript.js
  12. 67 0
      editor/js/CmdSetColor.js
  13. 202 0
      editor/js/CmdSetGeometry.js
  14. 64 0
      editor/js/CmdSetGeometryValue.js
  15. 202 0
      editor/js/CmdSetMaterial.js
  16. 62 0
      editor/js/CmdSetMaterialValue.js
  17. 76 0
      editor/js/CmdSetPosition.js
  18. 77 0
      editor/js/CmdSetRotation.js
  19. 76 0
      editor/js/CmdSetScale.js
  20. 94 0
      editor/js/CmdSetScene.js
  21. 66 0
      editor/js/CmdSetScriptName.js
  22. 69 0
      editor/js/CmdSetScriptSource.js
  23. 64 0
      editor/js/CmdSetUuid.js
  24. 71 0
      editor/js/CmdSetValue.js
  25. 53 0
      editor/js/CmdToggleBoolean.js
  26. 33 2
      editor/js/Editor.js
  27. 199 38
      editor/js/History.js
  28. 17 17
      editor/js/Loader.js
  29. 16 16
      editor/js/Menubar.Add.js
  30. 50 10
      editor/js/Menubar.Edit.js
  31. 56 0
      editor/js/Menubar.File.js
  32. 5 9
      editor/js/Sidebar.Geometry.BoxGeometry.js
  33. 3 1
      editor/js/Sidebar.Geometry.BufferGeometry.js
  34. 5 9
      editor/js/Sidebar.Geometry.CircleGeometry.js
  35. 5 9
      editor/js/Sidebar.Geometry.CylinderGeometry.js
  36. 3 1
      editor/js/Sidebar.Geometry.Geometry.js
  37. 5 7
      editor/js/Sidebar.Geometry.IcosahedronGeometry.js
  38. 4 4
      editor/js/Sidebar.Geometry.Modifiers.js
  39. 5 9
      editor/js/Sidebar.Geometry.PlaneGeometry.js
  40. 5 9
      editor/js/Sidebar.Geometry.SphereGeometry.js
  41. 5 9
      editor/js/Sidebar.Geometry.TorusGeometry.js
  42. 5 9
      editor/js/Sidebar.Geometry.TorusKnotGeometry.js
  43. 21 13
      editor/js/Sidebar.Geometry.js
  44. 97 0
      editor/js/Sidebar.History.js
  45. 3 3
      editor/js/Sidebar.Material.js
  46. 64 38
      editor/js/Sidebar.Object3D.js
  47. 4 5
      editor/js/Sidebar.Script.js
  48. 1 0
      editor/js/Sidebar.js
  49. 37 16
      editor/js/Viewport.js
  50. 2 2
      editor/js/libs/ui.three.js
  51. 6 0
      examples/js/controls/TransformControls.js
  52. 69 0
      test/unit/editor/CommonUtilities.js
  53. 20 0
      test/unit/editor/TestCmdAddObject.js
  54. 54 0
      test/unit/editor/TestCmdAddScript.js
  55. 37 0
      test/unit/editor/TestCmdMoveObject.js
  56. 27 0
      test/unit/editor/TestCmdRemoveObject.js
  57. 55 0
      test/unit/editor/TestCmdRemoveScript.js
  58. 40 0
      test/unit/editor/TestCmdSetColor.js
  59. 39 0
      test/unit/editor/TestCmdSetPosition.js
  60. 39 0
      test/unit/editor/TestCmdSetRotation.js
  61. 39 0
      test/unit/editor/TestCmdSetScale.js
  62. 37 0
      test/unit/editor/TestCmdSetScriptName.js
  63. 31 0
      test/unit/editor/TestCmdSetScriptSource.js
  64. 30 0
      test/unit/editor/TestCmdSetUuid.js
  65. 49 0
      test/unit/editor/TestCmdSetValue.js
  66. 40 0
      test/unit/editor/TestCmdToggleBoolean.js
  67. 112 0
      test/unit/editor/TestNestedDoUndoRedo.js
  68. 111 0
      test/unit/unittests_editor.html

+ 1 - 0
.gitignore

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

+ 8 - 1
editor/css/dark.css

@@ -127,7 +127,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

@@ -122,6 +122,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;

+ 35 - 0
editor/index.html

@@ -114,9 +114,30 @@
 		<script src="js/Sidebar.Geometry.TorusKnotGeometry.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/CmdToggleBoolean.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/CmdSetScriptName.js"></script>
+		<script src="js/CmdSetScriptSource.js"></script>
+		<script src="js/CmdSetMaterialValue.js"></script>
+		<script src="js/CmdSetScene.js"></script>
 
 		<script>
 
@@ -218,6 +239,7 @@
 				signals.materialChanged.add( saveState );
 				signals.sceneGraphChanged.add( saveState );
 				signals.scriptChanged.add( saveState );
+				signals.historyChanged.add( saveState );
 
 				/*
 				var showDialog = function ( content ) {
@@ -271,6 +293,19 @@
 						editor.select( parent );
 
 						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;
 
 				}
 

+ 28 - 0
editor/js/Cmd.js

@@ -0,0 +1,28 @@
+/**
+ * Created by Daniel on 20.07.15.
+ */
+
+Cmd = function () {
+
+	this.id = -1;
+	this.serialized = false;
+	this.updatable = false;
+	this.type = '';
+
+};
+
+Cmd.prototype.toJSON = function () {
+
+	var output = {};
+	output.type = this.type;
+	output.id = this.id;
+	return output;
+
+};
+
+Cmd.prototype.fromJSON = function ( json ) {
+
+	this.type = json.type;
+	this.id = json.id;
+
+};

+ 68 - 0
editor/js/CmdAddObject.js

@@ -0,0 +1,68 @@
+/**
+ * Created by Daniel on 20.07.15.
+ */
+
+CmdAddObject = function ( object ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdAddObject';
+
+	this.object = object;
+	if ( object !== undefined ) {
+
+		object.updateMatrixWorld( true );
+		meta = {
+			geometries: {},
+			materials: {},
+			textures: {},
+			images: {}
+		};
+		this.objectJSON = object.toJSON( meta );
+
+	}
+
+};
+
+CmdAddObject.prototype = {
+
+	execute: function () {
+
+		this.editor.addObject( this.object );
+
+	},
+
+	undo: function () {
+
+		this.editor.removeObject( this.object );
+		this.editor.deselect();
+
+	},
+
+	toJSON: function () {
+
+		var output = Cmd.prototype.toJSON.call( this );
+
+		output.object = this.objectJSON;
+
+		return output;
+
+	},
+
+	fromJSON: function ( json ) {
+
+		Cmd.prototype.fromJSON.call( this, json );
+
+		this.objectJSON = json.object;
+		this.object = this.editor.objectByUuid( json.object.object.uuid );
+
+		if ( this.object === undefined ) {
+
+			var loader = new THREE.ObjectLoader();
+			this.object = loader.parse( json.object );
+
+		}
+
+	}
+
+};

+ 70 - 0
editor/js/CmdAddScript.js

@@ -0,0 +1,70 @@
+/**
+ * Created by Daniel on 20.07.15.
+ */
+
+CmdAddScript = function ( object, script ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdAddScript';
+
+	this.object = object;
+	this.objectUuid = object !== undefined ? object.uuid : undefined;
+
+	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.objectUuid;
+		output.script = this.script;
+
+		return output;
+
+	},
+
+	fromJSON: function ( json ) {
+
+		Cmd.prototype.fromJSON.call( this, json );
+
+		this.objectUuid = json.objectUuid;
+		this.script = json.script;
+		this.object = this.editor.objectByUuid( json.objectUuid );
+
+	}
+
+};

+ 99 - 0
editor/js/CmdMoveObject.js

@@ -0,0 +1,99 @@
+/**
+ * Created by Daniel on 20.07.15.
+ */
+
+CmdMoveObject = function ( object, newParent, newBefore ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdMoveObject';
+
+	this.object = object;
+	this.objectUuid = object !== undefined ? object.uuid : undefined;
+
+	this.oldParent = object !== undefined ? object.parent : undefined;
+	this.oldParentUuid = this.oldParent !== undefined ? this.oldParent.uuid : undefined;
+	this.oldIndex = this.oldParent !== undefined ? this.oldParent.children.indexOf( this.object ) : undefined;
+
+	this.newParent = newParent;
+	this.newParentUuid = newParent !== undefined ? newParent.uuid : undefined;
+
+	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.objectUuid;
+		output.newParentUuid = this.newParentUuid;
+		output.oldParentUuid = this.oldParentUuid;
+		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.objectUuid = json.objectUuid;
+
+		this.oldParent = this.editor.objectByUuid( json.oldParentUuid );
+		this.oldParentUuid = json.oldParentUuid;
+
+		this.newParent = this.editor.objectByUuid( json.newParentUuid );
+		this.newParentUuid = json.newParentUuid;
+
+		this.newIndex = json.newIndex;
+		this.oldIndex = json.oldIndex;
+
+	}
+
+};

+ 80 - 0
editor/js/CmdMultiCmds.js

@@ -0,0 +1,80 @@
+/**
+ * Created by Daniel on 20.07.15.
+ */
+
+CmdMultiCmds = function ( cmdArray ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdMultiCmds';
+
+	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 ].editor = this.editor;
+			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.editor = this.editor;
+			cmd.fromJSON( cmds[ i ] );
+			this.cmdArray.push( cmd );
+
+		}
+
+	}
+
+};

+ 103 - 0
editor/js/CmdRemoveObject.js

@@ -0,0 +1,103 @@
+/**
+ * Created by Daniel on 20.07.15.
+ */
+
+CmdRemoveObject = function ( object ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdRemoveObject';
+
+	meta = {
+		geometries: {},
+		materials: {},
+		textures: {},
+		images: {}
+	};
+
+	this.object = object;
+	this.parent = object !== undefined ? object.parent : undefined;
+	this.objectJSON = object !== undefined ? object.toJSON( meta ) : undefined;
+	this.parentUuid = object !== undefined ? object.parent.uuid : undefined;
+
+};
+
+CmdRemoveObject.prototype = {
+
+	execute: function () {
+
+		this.index = this.parent.children.indexOf( this.object );
+
+		var scope = this.editor;
+		this.object.traverse( function ( child ) {
+
+			scope.removeHelper( child );
+
+		} );
+
+		this.parent.remove( this.object );
+
+		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.signals.objectAdded.dispatch( this.object );
+		this.editor.signals.sceneGraphChanged.dispatch();
+
+	},
+
+	toJSON: function () {
+
+		var output = Cmd.prototype.toJSON.call( this );
+
+		output.object = this.objectJSON;
+		output.index = this.index;
+		output.parentUuid = this.parentUuid;
+
+		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.parentUuid = json.parentUuid;
+
+		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 );
+
+		}
+		this.objectJSON = json.object;
+
+	}
+
+};

+ 73 - 0
editor/js/CmdRemoveScript.js

@@ -0,0 +1,73 @@
+/**
+ * Created by Daniel on 20.07.15.
+ */
+
+CmdRemoveScript = function ( object, script ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdRemoveScript';
+
+	this.object = object;
+	this.objectUuid = object !== undefined ? object.uuid : undefined;
+
+	this.script = script;
+
+};
+
+CmdRemoveScript.prototype = {
+
+	execute: function () {
+
+		if ( this.editor.scripts[ this.object.uuid ] === undefined ) return;
+
+		this.index = this.editor.scripts[ this.object.uuid ].indexOf( this.script );
+
+		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.objectUuid;
+		output.script = this.script;
+		output.index = this.index;
+
+		return output;
+
+	},
+
+	fromJSON: function ( json ) {
+
+		Cmd.prototype.fromJSON.call( this, json );
+
+		this.objectUuid = json.objectUuid;
+		this.script = json.script;
+		this.index = json.index;
+		this.object = this.editor.objectByUuid( json.objectUuid );
+
+	}
+
+};

+ 67 - 0
editor/js/CmdSetColor.js

@@ -0,0 +1,67 @@
+/**
+ * Created by Daniel on 21.07.15.
+ */
+
+CmdSetColor = function ( object, attributeName, newValue ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdSetColor';
+	this.updatable = true;
+
+	this.object = object;
+	this.objectUuid = object !== undefined ? object.uuid : undefined;
+	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.objectUuid;
+		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.objectUuid = json.objectUuid;
+		this.attributeName = json.attributeName;
+		this.oldValue = json.oldValue;
+		this.newValue = json.newValue;
+
+	}
+
+};

+ 202 - 0
editor/js/CmdSetGeometry.js

@@ -0,0 +1,202 @@
+/**
+ * Created by Daniel on 21.07.15.
+ */
+
+CmdSetGeometry = function ( object, newGeometry ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdSetGeometry';
+	this.updatable = true;
+
+	this.object = object;
+	this.objectUuid = object !== undefined ? object.uuid : undefined;
+
+	this.newGeometry = newGeometry;	// only needed for update(cmd)
+
+	this.oldGeometryJSON = object !== undefined ? object.geometry.toJSON() : undefined;
+	this.newGeometryJSON = newGeometry !== undefined ? newGeometry.toJSON() : undefined;
+
+
+};
+
+CmdSetGeometry.prototype = {
+
+	execute: function () {
+
+		this.object.geometry.dispose();
+		this.object.geometry = this.parseGeometry( this.newGeometryJSON );
+		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.parseGeometry( this.oldGeometryJSON );
+		this.object.geometry.computeBoundingSphere();
+
+		this.editor.signals.geometryChanged.dispatch( this.object );
+		this.editor.signals.sceneGraphChanged.dispatch();
+
+	},
+
+	update: function ( cmd ) {
+
+		this.newGeometryJSON = cmd.newGeometry.toJSON();
+
+	},
+
+	toJSON: function () {
+
+		var output = Cmd.prototype.toJSON.call( this );
+
+		output.objectUuid = this.objectUuid;
+		output.oldGeometryJSON = this.oldGeometryJSON;
+		output.newGeometryJSON = this.newGeometryJSON;
+
+		return output;
+
+	},
+
+	fromJSON: function ( json ) {
+
+		Cmd.prototype.fromJSON.call( this, json );
+
+		this.object = this.editor.objectByUuid( json.objectUuid );
+		this.objectUuid = json.objectUuid;
+
+		this.oldGeometryJSON = json.oldGeometryJSON;
+		this.newGeometryJSON = json.newGeometryJSON;
+
+	},
+
+	parseGeometry: function ( data ) {
+
+		var geometryLoader = new THREE.JSONLoader();
+		var bufferGeometryLoader = new THREE.BufferGeometryLoader();
+
+		var geometry;
+
+		switch ( data.type ) {
+
+			case 'PlaneGeometry':
+			case 'PlaneBufferGeometry':
+
+				geometry = new THREE[ data.type ](
+					data.width,
+					data.height,
+					data.widthSegments,
+					data.heightSegments
+				);
+
+				break;
+
+			case 'BoxGeometry':
+			case 'CubeGeometry': // backwards compatible
+
+				geometry = new THREE.BoxGeometry(
+					data.width,
+					data.height,
+					data.depth,
+					data.widthSegments,
+					data.heightSegments,
+					data.depthSegments
+				);
+
+				break;
+
+			case 'CircleGeometry':
+
+				geometry = new THREE.CircleGeometry(
+					data.radius,
+					data.segments
+				);
+
+				break;
+
+			case 'CylinderGeometry':
+
+				geometry = new THREE.CylinderGeometry(
+					data.radiusTop,
+					data.radiusBottom,
+					data.height,
+					data.radialSegments,
+					data.heightSegments,
+					data.openEnded
+				);
+
+				break;
+
+			case 'SphereGeometry':
+
+				geometry = new THREE.SphereGeometry(
+					data.radius,
+					data.widthSegments,
+					data.heightSegments,
+					data.phiStart,
+					data.phiLength,
+					data.thetaStart,
+					data.thetaLength
+				);
+
+				break;
+
+			case 'IcosahedronGeometry':
+
+				geometry = new THREE.IcosahedronGeometry(
+					data.radius,
+					data.detail
+				);
+
+				break;
+
+			case 'TorusGeometry':
+
+				geometry = new THREE.TorusGeometry(
+					data.radius,
+					data.tube,
+					data.radialSegments,
+					data.tubularSegments,
+					data.arc
+				);
+
+				break;
+
+			case 'TorusKnotGeometry':
+
+				geometry = new THREE.TorusKnotGeometry(
+					data.radius,
+					data.tube,
+					data.radialSegments,
+					data.tubularSegments,
+					data.p,
+					data.q,
+					data.heightScale
+				);
+
+				break;
+
+			case 'BufferGeometry':
+
+				geometry = bufferGeometryLoader.parse( data );
+
+				break;
+
+			case 'Geometry':
+
+				geometry = geometryLoader.parse( data.data ).geometry;
+
+				break;
+
+		}
+		geometry.uuid = data.uuid;
+		geometry.name = data.name !== undefined ? data.name : '';
+		return geometry;
+
+	}
+
+};

+ 64 - 0
editor/js/CmdSetGeometryValue.js

@@ -0,0 +1,64 @@
+/**
+ * Created by Daniel on 21.07.15.
+ */
+
+CmdSetGeometryValue = function ( object, attributeName, newValue ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdSetGeometryValue';
+
+	this.object = object;
+	this.attributeName = attributeName;
+	this.oldValue = object !== undefined ? object.geometry[ attributeName ] : undefined;
+	this.newValue = newValue;
+	this.objectUuid = object !== undefined ? object.uuid : undefined;
+
+};
+
+CmdSetGeometryValue.prototype = {
+
+	execute: function () {
+
+		this.object.geometry[ this.attributeName ] = this.newValue;
+		this.editor.signals.objectChanged.dispatch( this.object );
+		this.editor.signals.sceneGraphChanged.dispatch();
+		this.editor.signals.updateSidebar.dispatch();
+
+	},
+
+	undo: function () {
+
+		this.object.geometry[ this.attributeName ] = this.oldValue;
+		this.editor.signals.objectChanged.dispatch( this.object );
+		this.editor.signals.sceneGraphChanged.dispatch();
+		this.editor.signals.updateSidebar.dispatch();
+
+	},
+
+	toJSON: function () {
+
+		var output = Cmd.prototype.toJSON.call( this );
+
+		output.objectUuid = this.objectUuid;
+		output.attributeName = this.attributeName;
+		output.oldValue = this.oldValue;
+		output.newValue = this.newValue;
+
+		return output;
+
+	},
+
+	fromJSON: function ( json ) {
+
+		Cmd.prototype.fromJSON.call( this, json );
+
+		this.objectUuid = json.objectUuid;
+		this.attributeName = json.attributeName;
+		this.oldValue = json.oldValue;
+		this.newValue = json.newValue;
+		this.object = this.editor.objectByUuid( json.objectUuid );
+
+	}
+
+};

+ 202 - 0
editor/js/CmdSetMaterial.js

@@ -0,0 +1,202 @@
+/**
+ * Created by Daniel on 21.07.15.
+ */
+
+CmdSetMaterial = function ( object, newMaterial ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdSetMaterial';
+	this.updatable = true;
+
+	this.object = object;
+	this.objectUuid = object !== undefined ? object.uuid : undefined;
+
+	this.newMaterial = newMaterial;	// only needed for update(cmd)
+
+	this.oldMaterialJSON = object !== undefined ? object.material.toJSON() : undefined;
+	this.newMaterialJSON = newMaterial !== undefined ? newMaterial.toJSON() : undefined;
+
+
+};
+
+CmdSetMaterial.prototype = {
+
+	execute: function () {
+
+		this.object.geometry.dispose();
+		this.object.geometry = this.parseGeometry( this.newMaterialJSON );
+		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.parseGeometry( this.oldMaterialJSON );
+		this.object.geometry.computeBoundingSphere();
+
+		this.editor.signals.geometryChanged.dispatch( this.object );
+		this.editor.signals.sceneGraphChanged.dispatch();
+
+	},
+
+	update: function ( cmd ) {
+
+		this.newMaterialJSON = cmd.newGeometry.toJSON();
+
+	},
+
+	toJSON: function () {
+
+		var output = Cmd.prototype.toJSON.call( this );
+
+		output.objectUuid = this.objectUuid;
+		output.oldMaterialJSON = this.oldMaterialJSON;
+		output.newMaterialJSON = this.newMaterialJSON;
+
+		return output;
+
+	},
+
+	fromJSON: function ( json ) {
+
+		Cmd.prototype.fromJSON.call( this, json );
+
+		this.object = this.editor.objectByUuid( json.objectUuid );
+		this.objectUuid = json.objectUuid;
+
+		this.oldMaterialJSON = json.oldMaterialJSON;
+		this.newMaterialJSON = json.newMaterialJSON;
+
+	},
+
+	parseGeometry: function ( data ) {
+
+		var geometryLoader = new THREE.JSONLoader();
+		var bufferGeometryLoader = new THREE.BufferGeometryLoader();
+
+		var geometry;
+
+		switch ( data.type ) {
+
+			case 'PlaneGeometry':
+			case 'PlaneBufferGeometry':
+
+				geometry = new THREE[ data.type ](
+					data.width,
+					data.height,
+					data.widthSegments,
+					data.heightSegments
+				);
+
+				break;
+
+			case 'BoxGeometry':
+			case 'CubeGeometry': // backwards compatible
+
+				geometry = new THREE.BoxGeometry(
+					data.width,
+					data.height,
+					data.depth,
+					data.widthSegments,
+					data.heightSegments,
+					data.depthSegments
+				);
+
+				break;
+
+			case 'CircleGeometry':
+
+				geometry = new THREE.CircleGeometry(
+					data.radius,
+					data.segments
+				);
+
+				break;
+
+			case 'CylinderGeometry':
+
+				geometry = new THREE.CylinderGeometry(
+					data.radiusTop,
+					data.radiusBottom,
+					data.height,
+					data.radialSegments,
+					data.heightSegments,
+					data.openEnded
+				);
+
+				break;
+
+			case 'SphereGeometry':
+
+				geometry = new THREE.SphereGeometry(
+					data.radius,
+					data.widthSegments,
+					data.heightSegments,
+					data.phiStart,
+					data.phiLength,
+					data.thetaStart,
+					data.thetaLength
+				);
+
+				break;
+
+			case 'IcosahedronGeometry':
+
+				geometry = new THREE.IcosahedronGeometry(
+					data.radius,
+					data.detail
+				);
+
+				break;
+
+			case 'TorusGeometry':
+
+				geometry = new THREE.TorusGeometry(
+					data.radius,
+					data.tube,
+					data.radialSegments,
+					data.tubularSegments,
+					data.arc
+				);
+
+				break;
+
+			case 'TorusKnotGeometry':
+
+				geometry = new THREE.TorusKnotGeometry(
+					data.radius,
+					data.tube,
+					data.radialSegments,
+					data.tubularSegments,
+					data.p,
+					data.q,
+					data.heightScale
+				);
+
+				break;
+
+			case 'BufferGeometry':
+
+				geometry = bufferGeometryLoader.parse( data );
+
+				break;
+
+			case 'Geometry':
+
+				geometry = geometryLoader.parse( data.data ).geometry;
+
+				break;
+
+		}
+		geometry.uuid = data.uuid;
+		geometry.name = data.name !== undefined ? data.name : '';
+		return geometry;
+
+	}
+
+};

+ 62 - 0
editor/js/CmdSetMaterialValue.js

@@ -0,0 +1,62 @@
+/**
+ * Created by Daniel on 21.07.15.
+ */
+
+CmdSetMaterialValue = function ( object, attributeName, newValue ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdSetMaterialValue';
+
+	this.object = object;
+	this.attributeName = attributeName;
+	this.oldValue = object !== undefined ? object.material[ attributeName ] : undefined;
+	this.newValue = newValue;
+	this.objectUuid = object !== undefined ? object.uuid : undefined;
+
+};
+
+CmdSetMaterialValue.prototype = {
+
+	execute: function () {
+
+		this.object.material[ this.attributeName ] = this.newValue;
+		this.editor.signals.objectChanged.dispatch( this.object );
+		this.editor.signals.sceneGraphChanged.dispatch();
+
+	},
+
+	undo: function () {
+
+		this.object.material[ this.attributeName ] = this.oldValue;
+		this.editor.signals.objectChanged.dispatch( this.object );
+		this.editor.signals.sceneGraphChanged.dispatch();
+
+	},
+
+	toJSON: function () {
+
+		var output = Cmd.prototype.toJSON.call( this );
+
+		output.objectUuid = this.objectUuid;
+		output.attributeName = this.attributeName;
+		output.oldValue = this.oldValue;
+		output.newValue = this.newValue;
+
+		return output;
+
+	},
+
+	fromJSON: function ( json ) {
+
+		Cmd.prototype.fromJSON.call( this, json );
+
+		this.objectUuid = json.objectUuid;
+		this.attributeName = json.attributeName;
+		this.oldValue = json.oldValue;
+		this.newValue = json.newValue;
+		this.object = this.editor.objectByUuid( json.objectUuid );
+
+	}
+
+};

+ 76 - 0
editor/js/CmdSetPosition.js

@@ -0,0 +1,76 @@
+/**
+ * Created by Daniel on 23.07.15.
+ */
+
+CmdSetPosition = function ( object, newPositionVector, oldPositionVector ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdSetPosition';
+	this.updatable = true;
+
+	this.object = object;
+	this.objectUuid = object !== undefined ? object.uuid : undefined;
+
+	if (object !== undefined && newPositionVector !== undefined) {
+
+		this.oldPosition = object.position.clone();
+		this.newPosition = newPositionVector.clone();
+
+	}
+
+	if (oldPositionVector !== undefined) {
+
+		this.oldPosition = oldPositionVector.clone();
+
+	}
+
+};
+CmdSetPosition.prototype = {
+
+	execute: function () {
+
+		this.object.position.copy( this.newPosition );
+		this.object.updateMatrix();
+		this.editor.signals.objectChanged.dispatch( this.object );
+
+	},
+
+	undo: function () {
+
+		this.object.position.copy( this.oldPosition );
+		this.object.updateMatrix();
+		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.objectUuid;
+		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.objectUuid = json.objectUuid;
+		this.oldPosition = new THREE.Vector3().fromArray(json.oldPosition);
+		this.newPosition = new THREE.Vector3().fromArray(json.newPosition);
+
+	}
+
+};

+ 77 - 0
editor/js/CmdSetRotation.js

@@ -0,0 +1,77 @@
+/**
+ * Created by Daniel on 23.07.15.
+ */
+
+CmdSetRotation = function ( object, newRotationEuler, oldRotationEuler ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdSetRotation';
+	this.updatable = true;
+
+	this.object = object;
+	this.objectUuid = object !== undefined ? object.uuid : undefined;
+
+	if ( object !== undefined && newRotationEuler !== undefined) {
+
+		this.oldRotation = object.rotation.clone();
+		this.newRotation = newRotationEuler.clone();
+
+	}
+
+	if ( oldRotationEuler !== undefined ) {
+
+		this.oldRotation = oldRotationEuler.clone();
+
+	}
+
+};
+
+CmdSetRotation.prototype = {
+
+	execute: function () {
+
+		this.object.rotation.copy( this.newRotation );
+		this.object.updateMatrix();
+		this.editor.signals.objectChanged.dispatch( this.object );
+
+	},
+
+	undo: function () {
+
+		this.object.rotation.copy( this.oldRotation );
+		this.object.updateMatrix();
+		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.objectUuid;
+		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.objectUuid = json.objectUuid;
+		this.oldRotation = new THREE.Euler().fromArray(json.oldRotation);
+		this.newRotation = new THREE.Euler().fromArray(json.newRotation);
+
+	}
+
+};

+ 76 - 0
editor/js/CmdSetScale.js

@@ -0,0 +1,76 @@
+/**
+ * Created by Daniel on 23.07.15.
+ */
+
+CmdSetScale = function ( object, newScaleVector, oldScaleVector ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdSetScale';
+	this.updatable = true;
+
+	this.object = object;
+	this.objectUuid = object !== undefined ? object.uuid : undefined;
+
+	if ( object !== undefined && newScaleVector !== undefined ) {
+
+		this.oldScale = object.scale.clone();
+		this.newScale = newScaleVector.clone();
+
+	}
+
+	if ( oldScaleVector !== undefined ) {
+
+		this.oldScale = oldScaleVector.clone();
+
+	}
+};
+
+CmdSetScale.prototype = {
+
+	execute: function () {
+
+		this.object.scale.copy( this.newScale );
+		this.object.updateMatrix();
+		this.editor.signals.objectChanged.dispatch( this.object );
+
+	},
+
+	undo: function () {
+
+		this.object.scale.copy( this.oldScale );
+		this.object.updateMatrix();
+		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.objectUuid;
+		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.objectUuid = json.objectUuid;
+		this.oldScale = new THREE.Vector3().fromArray(json.oldScale);
+		this.newScale = new THREE.Vector3().fromArray(json.newScale);
+
+	}
+
+};

+ 94 - 0
editor/js/CmdSetScene.js

@@ -0,0 +1,94 @@
+/**
+ * Created by Daniel on 20.07.15.
+ */
+
+CmdSetScene = function ( oldScene, newScene ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdSetScene';
+
+	this.cmdArray = [];
+
+	if ( newScene !== undefined ) {
+
+		this.cmdArray.push( new CmdSetUuid( oldScene, newScene.uuid ) );
+		this.cmdArray.push( new CmdSetValue( oldScene, 'name', newScene.name ) );
+		this.cmdArray.push( new CmdSetValue( oldScene, 'userData', JSON.parse( JSON.stringify( newScene.userData ) ) ) );
+
+		while ( newScene.children.length > 0 ) {
+
+			var child = newScene.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 ].editor = this.editor;
+			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.editor = this.editor;
+			cmd.fromJSON( cmds[ i ] );
+			this.cmdArray.push( cmd );
+
+		}
+
+	}
+
+};

+ 66 - 0
editor/js/CmdSetScriptName.js

@@ -0,0 +1,66 @@
+/**
+ * Created by Daniel on 21.07.15.
+ */
+
+CmdSetScriptName = function ( object, script, name ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdSetScriptName';
+
+	this.object = object;
+	this.script = script;
+	this.oldName = script !== undefined ? script.name : undefined;
+	this.newName = name;
+	this.objectUuid = object !== undefined ? object.uuid : undefined;
+
+};
+
+CmdSetScriptName.prototype = {
+
+	execute: function () {
+
+		this.index = this.editor.scripts[ this.objectUuid ].indexOf( this.script );
+		this.script.name = this.newName;
+
+		this.editor.signals.scriptChanged.dispatch();
+		this.editor.signals.refreshScriptEditor.dispatch( this.object, this.script );
+
+	},
+
+	undo: function () {
+
+		this.script.name = this.oldName;
+
+		this.editor.signals.scriptChanged.dispatch();
+		this.editor.signals.refreshScriptEditor.dispatch( this.object, this.script );
+
+	},
+
+	toJSON: function () {
+
+		var output = Cmd.prototype.toJSON.call( this );
+
+		output.objectUuid = this.objectUuid;
+		output.index = this.index;
+		output.oldName = this.oldName;
+		output.newName = this.newName;
+
+		return output;
+
+	},
+
+	fromJSON: function ( json ) {
+
+		Cmd.prototype.fromJSON.call( this, json );
+
+		this.objectUuid = json.objectUuid;
+		this.index = json.index;
+		this.oldName = json.oldName;
+		this.newName = json.newName;
+		this.object = this.editor.objectByUuid( json.objectUuid );
+		this.script = this.editor.scripts[ json.objectUuid ][ json.index ];
+
+	}
+
+};

+ 69 - 0
editor/js/CmdSetScriptSource.js

@@ -0,0 +1,69 @@
+/**
+ * Created by Daniel on 21.07.15.
+ */
+
+CmdSetScriptSource = function ( object, script, source, oldSource, cursorPosition ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdSetScriptSource';
+
+	this.object = object;
+	this.script = script;
+	this.oldSource = oldSource !== undefined ? oldSource : undefined;
+	this.newSource = source;
+	this.objectUuid = object !== undefined ? object.uuid : undefined;
+	this.cursorPosition = cursorPosition; // Format {line: 2, ch: 3}
+
+};
+
+CmdSetScriptSource.prototype = {
+
+	execute: function () {
+
+		this.index = this.editor.scripts[ this.objectUuid ].indexOf( this.script );
+		this.script.source = this.newSource;
+
+		this.editor.signals.scriptChanged.dispatch( this.script );
+		this.editor.signals.refreshScriptEditor.dispatch( this.object, this.script, this.cursorPosition );
+
+	},
+
+	undo: function () {
+
+		this.script.source = this.oldSource;
+
+		this.editor.signals.scriptChanged.dispatch( this.script );
+		this.editor.signals.refreshScriptEditor.dispatch( this.object, this.script, this.cursorPosition );
+
+	},
+
+	toJSON: function () {
+
+		var output = Cmd.prototype.toJSON.call( this );
+
+		output.objectUuid = this.objectUuid;
+		output.index = this.index;
+		output.oldSource = this.oldSource;
+		output.newSource = this.newSource;
+		output.cursorPosition = this.cursorPosition;
+
+		return output;
+
+	},
+
+	fromJSON: function ( json ) {
+
+		Cmd.prototype.fromJSON.call( this, json );
+
+		this.objectUuid = json.objectUuid;
+		this.index = json.index;
+		this.oldSource = json.oldSource;
+		this.newSource = json.newSource;
+		this.object = this.editor.objectByUuid( json.objectUuid );
+		this.script = this.editor.scripts[ json.objectUuid ][ json.index ];
+		this.cursorPosition = json.cursorPosition;
+
+	}
+
+};

+ 64 - 0
editor/js/CmdSetUuid.js

@@ -0,0 +1,64 @@
+/**
+ * Created by Daniel on 21.07.15.
+ */
+
+CmdSetUuid = function ( object, newUuid ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdSetUuid';
+	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();
+		this.editor.signals.updateSidebar.dispatch();
+
+	},
+
+	undo: function () {
+
+		this.object.uuid = this.oldUuid;
+		this.editor.signals.objectChanged.dispatch( this.object );
+		this.editor.signals.sceneGraphChanged.dispatch();
+		this.editor.signals.updateSidebar.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 );
+
+		}
+
+	}
+
+};

+ 71 - 0
editor/js/CmdSetValue.js

@@ -0,0 +1,71 @@
+/**
+ * Created by Daniel on 21.07.15.
+ */
+
+CmdSetValue = function ( object, attributeName, newValue ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdSetValue';
+	this.updatable = true;
+
+	this.object = object;
+	this.attributeName = attributeName;
+	this.oldValue = object !== undefined ? object[ attributeName ] : undefined;
+	this.newValue = newValue;
+	this.objectUuid = object !== undefined ? object.uuid : undefined;
+
+};
+
+CmdSetValue.prototype = {
+
+	execute: function () {
+
+		this.object[ this.attributeName ] = this.newValue;
+		this.editor.signals.objectChanged.dispatch( this.object );
+		this.editor.signals.sceneGraphChanged.dispatch();
+		this.editor.signals.updateSidebar.dispatch();
+
+	},
+
+	undo: function () {
+
+		this.object[ this.attributeName ] = this.oldValue;
+		this.editor.signals.objectChanged.dispatch( this.object );
+		this.editor.signals.sceneGraphChanged.dispatch();
+		this.editor.signals.updateSidebar.dispatch();
+
+	},
+
+	update: function ( cmd ) {
+
+		this.newValue = cmd.newValue;
+
+	},
+
+	toJSON: function () {
+
+		var output = Cmd.prototype.toJSON.call( this );
+
+		output.objectUuid = this.objectUuid;
+		output.attributeName = this.attributeName;
+		output.oldValue = this.oldValue;
+		output.newValue = this.newValue;
+
+		return output;
+
+	},
+
+	fromJSON: function ( json ) {
+
+		Cmd.prototype.fromJSON.call( this, json );
+
+		this.objectUuid = json.objectUuid;
+		this.attributeName = json.attributeName;
+		this.oldValue = json.oldValue;
+		this.newValue = json.newValue;
+		this.object = this.editor.objectByUuid( json.objectUuid );
+
+	}
+
+};

+ 53 - 0
editor/js/CmdToggleBoolean.js

@@ -0,0 +1,53 @@
+/**
+ * Created by Daniel on 21.07.15.
+ */
+
+CmdToggleBoolean = function ( object, attributeName ) {
+
+	Cmd.call( this );
+
+	this.type = 'CmdToggleBoolean';
+
+	this.object = object;
+	this.attributeName = attributeName;
+	this.objectUuid = object !== undefined ? object.uuid : undefined;
+
+};
+
+CmdToggleBoolean.prototype = {
+
+	execute: function () {
+
+		this.object[ this.attributeName ] = !this.object[ this.attributeName ];
+		this.editor.signals.objectChanged.dispatch( this.object );
+
+	},
+
+	undo: function () {
+
+		this.object[ this.attributeName ] = !this.object[ this.attributeName ];
+		this.editor.signals.objectChanged.dispatch( this.object );
+
+	},
+
+	toJSON: function () {
+
+		var output = Cmd.prototype.toJSON.call( this );
+
+		output.objectUuid = this.objectUuid;
+		output.attributeName = this.attributeName;
+
+		return output;
+
+	},
+
+	fromJSON: function ( json ) {
+
+		Cmd.prototype.fromJSON.call( this, json );
+
+		this.object = this.editor.objectByUuid( json.objectUuid );
+		this.attributeName = json.attributeName;
+
+	}
+
+};

+ 33 - 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(),
+		updateSidebar: new SIGNALS.Signal(),
+		historyChanged: new SIGNALS.Signal(),
+		refreshScriptEditor: new SIGNALS.Signal()
 
 	};
 
@@ -430,6 +433,8 @@ Editor.prototype = {
 
 		this.deselect();
 
+		this.history.clear();
+
 		this.signals.editorCleared.dispatch();
 
 	},
@@ -461,6 +466,7 @@ Editor.prototype = {
 
 		this.setScene( loader.parse( json.scene ) );
 		this.scripts = json.scripts;
+		this.history.fromJSON( json.history );
 
 	},
 
@@ -473,10 +479,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 ( command ) {
+
+		this.history.execute( command );
+
+	},
+
+	undo: function () {
+
+		this.history.undo();
+
+	},
+
+	redo: function () {
+
+		this.history.redo();
+
 	}
 
 }

+ 199 - 38
editor/js/History.js

@@ -1,80 +1,241 @@
 /**
  * @author mrdoob / http://mrdoob.com/
+ * edited by dforrer on 20.07.15.
  */
 
-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;
+};
 
-	//
+History.prototype = {
 
-	var scope = this;
-	var signals = editor.signals;
+	execute: function ( cmd ) {
 
-	signals.objectAdded.add( function ( object ) {
+		var lastCmd = this.undos[ this.undos.length - 1 ];
+		var timeDifference = new Date().getTime() - this.lastCmdTime.getTime();
 
-		if ( scope.isRecording === false ) return;
+		if ( lastCmd != null &&
+			lastCmd.updatable &&
+			lastCmd.object === cmd.object &&
+			lastCmd.type == cmd.type &&
+			timeDifference < 500 ) {
 
-		scope.add(
-			function () {
-				editor.removeObject( object );
-				editor.select( null );
-			},
-			function () {
-				editor.addObject( object );
-				editor.select( object );
-			}
-		);
+			// command objects have the same type and are less than 0.5 second apart
+			lastCmd.update( cmd );
+			cmd = lastCmd;
 
-	} );
+		} else {
 
-};
+			this.undos.push( cmd );
+			cmd.editor = this.editor;
+			cmd.id = ++this.idCounter;
 
-History.prototype = {
+		}
+		cmd.execute();
 
-	add: function ( undo, redo ) {
+		this.lastCmdTime = new Date();
 
-		this.current ++;
+		// clearing all the redo-commands
 
-		this.array[ this.current ] = { undo: undo, redo: redo };
-		this.arrayLength = this.current;
+		this.redos = [];
+		this.editor.signals.historyChanged.dispatch( cmd );
 
 	},
 
 	undo: function () {
 
-		if ( this.current < 0 ) return;
+		var cmd = undefined;
+
+		if ( this.undos.length > 0 ) {
+
+			var cmd = this.undos.pop();
+
+			if ( cmd.serialized ) {
+
+				var json = cmd;
+				cmd = new window[ json.type ]();	// creates a new object of type "json.type"
+				cmd.editor = this.editor;
+				cmd.fromJSON( json );
+
+			}
 
-		this.isRecording = false;
+		}
 
-		this.array[ this.current -- ].undo();
+		if ( cmd !== undefined ) {
 
-		this.isRecording = true;
+			cmd.undo();
+			console.log('Type: Undo ' + cmd.type );
+			this.redos.push( cmd );
+			this.editor.signals.historyChanged.dispatch( cmd );
+
+		}
+
+		return cmd;
 
 	},
 
 	redo: function () {
 
-		if ( this.current === this.arrayLength ) return;
+		var cmd = undefined;
+
+		if ( this.redos.length > 0 ) {
+
+			var cmd = this.redos.pop();
+
+			if ( cmd.serialized ) {
+
+				var json = cmd;
+				cmd = new window[ json.type ]();	// creates a new object of type "json.type"
+				cmd.editor = this.editor;
+				cmd.fromJSON( json );
+
+			}
+
+		}
+
+		if ( cmd !== undefined ) {
+
+			cmd.execute();
+			console.log('Type: Redo ' + cmd.type );
+			this.undos.push( cmd );
+			this.editor.signals.historyChanged.dispatch( cmd );
+
+		}
+
+		return cmd;
+
+	},
+
+	toJSON: function () {
+
+		var history = {};
+
+		// Append Undos to History
+
+		var undos = [];
+
+		for ( var i = 0 ; i < this.undos.length; i++ ) {
+
+			var cmd = this.undos[ i ];
+
+			if ( cmd.serialized ) {
+
+				undos.push( cmd );	// add without serializing
+
+			} else {
+
+				undos.push( cmd.toJSON() );
+
+			}
+
+		}
+
+		history.undos = undos;
+
+		// Append Redos to History
+
+		var redos = [];
 
-		this.isRecording = false;
+		for ( var i = 0 ; i < this.redos.length; i++ ) {
 
-		this.array[ ++ this.current ].redo();
+			var cmd = this.redos[ i ];
 
-		this.isRecording = true;
+			if ( cmd.serialized ) {
+
+				redos.push( cmd );	// add without serializing
+
+			} else {
+
+				redos.push( cmd.toJSON() );
+
+			}
+
+
+		}
+
+		history.redos = redos;
+
+		return history;
+
+	},
+
+	fromJSON: function ( json ) {
+
+		if ( json === undefined ) return;
+
+		for ( var i = 0; i < json.undos.length ; i++ ) {
+
+			json.undos[ i ].serialized = true;
+			this.undos.push( json.undos[ i ] );
+
+			this.idCounter = json.undos[ i ].id > this.idCounter ? json.undos[ i ].id : this.idCounter; // set last used idCounter
+
+		}
+
+		for ( var i = 0; i < json.redos.length ; i++ ) {
+
+			json.redos[ i ].serialized = true;
+			this.redos.push( json.redos[ i ] );
+
+			this.idCounter = json.redos[ i ].id > this.idCounter ? json.redos[ i ].id : this.idCounter; // set last used idCounter
+
+		}
+
+		this.editor.signals.historyChanged.dispatch();
 
 	},
 
 	clear: function () {
 
-		this.array = [];
-		this.arrayLength = -1;
+		this.undos = [];
+		this.redos = [];
+		this.idCounter = 0;
+
+		this.editor.signals.historyChanged.dispatch();
+
+	},
+
+	goToState: function ( id ) {
+
+		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 ( 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.current = -1;
+		this.editor.signals.sceneGraphChanged.dispatch();
+		this.editor.signals.historyChanged.dispatch( cmd );
 
 	}
 

+ 17 - 17
editor/js/Loader.js

@@ -22,7 +22,7 @@ var Loader = function ( editor ) {
 					var loader = new THREE.AWDLoader();
 					var scene = loader.parse( event.target.result );
 
-					editor.setScene( scene );
+					editor.execute( new CmdSetScene( editor.scene, scene ) );
 
 				}, false );
 				reader.readAsArrayBuffer( file );
@@ -40,7 +40,7 @@ var Loader = function ( editor ) {
 					var loader = new THREE.BabylonLoader();
 					var scene = loader.parse( json );
 
-					editor.setScene( scene );
+					editor.execute( new CmdSetScene( editor.scene, scene ) );
 
 				}, false );
 				reader.readAsText( file );
@@ -63,7 +63,7 @@ var Loader = function ( editor ) {
 					var mesh = new THREE.Mesh( geometry, material );
 					mesh.name = filename;
 
-					editor.addObject( mesh );
+					editor.execute( new CmdAddObject( mesh ) );
 					editor.select( mesh );
 
 				}, false );
@@ -92,7 +92,7 @@ var Loader = function ( editor ) {
 						var mesh = new THREE.Mesh( geometry, material );
 						mesh.name = filename;
 
-						editor.addObject( mesh );
+						editor.execute( new CmdAddObject( mesh ) );
 						editor.select( mesh );
 
 					} );
@@ -117,7 +117,7 @@ var Loader = function ( editor ) {
 
 					collada.scene.name = filename;
 
-					editor.addObject( collada.scene );
+					editor.execute( new CmdAddObject( collada.scene ) );
 					editor.select( collada.scene );
 
 				}, false );
@@ -198,7 +198,7 @@ var Loader = function ( editor ) {
 						var object = new THREE.MorphAnimMesh( geometry, material );
 						object.name = filename;
 
-						editor.addObject( object );
+						editor.execute( new CmdAddObject( object ) );
 						editor.select( object );
 
 					}, false );
@@ -216,7 +216,7 @@ var Loader = function ( editor ) {
 					var object = new THREE.OBJLoader().parse( contents );
 					object.name = filename;
 
-					editor.addObject( object );
+					editor.execute( new CmdAddObject( object ) );
 					editor.select( object );
 
 				}, false );
@@ -240,7 +240,7 @@ var Loader = function ( editor ) {
 					var mesh = new THREE.Mesh( geometry, material );
 					mesh.name = filename;
 
-					editor.addObject( mesh );
+					editor.execute( new CmdAddObject( mesh ) );
 					editor.select( mesh );
 
 				}, false );
@@ -264,7 +264,7 @@ var Loader = function ( editor ) {
 					var mesh = new THREE.Mesh( geometry, material );
 					mesh.name = filename;
 
-					editor.addObject( mesh );
+					editor.execute( new CmdAddObject( mesh ) );
 					editor.select( mesh );
 
 				}, false );
@@ -294,7 +294,7 @@ var Loader = function ( editor ) {
 
 					var mesh = new THREE.Mesh( geometry, material );
 
-					editor.addObject( mesh );
+					editor.execute( new CmdAddObject( mesh ) );
 					editor.select( mesh );
 
 				}, false );
@@ -319,7 +319,7 @@ var Loader = function ( editor ) {
 					var mesh = new THREE.Mesh( geometry, material );
 					mesh.name = filename;
 
-					editor.addObject( mesh );
+					editor.execute( new CmdAddObject( mesh ) );
 					editor.select( mesh );
 
 				}, false );
@@ -336,7 +336,7 @@ var Loader = function ( editor ) {
 
 					var result = new THREE.VRMLLoader().parse( contents );
 
-					editor.setScene( result );
+					editor.execute( new CmdSetScene( editor.scene, result ) );
 
 				}, false );
 				reader.readAsText( file );
@@ -380,7 +380,7 @@ var Loader = function ( editor ) {
 
 			var mesh = new THREE.Mesh( result );
 
-			editor.addObject( mesh );
+			editor.execute( new CmdAddObject( mesh ) );
 			editor.select( mesh );
 
 		} else if ( data.metadata.type.toLowerCase() === 'geometry' ) {
@@ -426,7 +426,7 @@ var Loader = function ( editor ) {
 
 			mesh.name = filename;
 
-			editor.addObject( mesh );
+			editor.execute( new CmdAddObject( mesh ) );
 			editor.select( mesh );
 
 		} else if ( data.metadata.type.toLowerCase() === 'object' ) {
@@ -436,11 +436,11 @@ var Loader = function ( editor ) {
 
 			if ( result instanceof THREE.Scene ) {
 
-				editor.setScene( result );
+				editor.execute( new CmdSetScene( editor.scene, result ) );
 
 			} else {
 
-				editor.addObject( result );
+				editor.execute( new CmdAddObject( result ) );
 				editor.select( result );
 
 			}
@@ -452,7 +452,7 @@ var Loader = function ( editor ) {
 			var loader = new THREE.SceneLoader();
 			loader.parse( data, function ( result ) {
 
-				editor.setScene( result.scene );
+				editor.execute( new CmdSetScene( editor.scene, result.scene ) );
 
 			}, '' );
 

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

@@ -40,7 +40,7 @@ Menubar.Add = function ( editor ) {
 		var mesh = new THREE.Group();
 		mesh.name = 'Group ' + ( ++ meshCount );
 
-		editor.addObject( mesh );
+		editor.execute( new CmdAddObject( mesh ) );
 		editor.select( mesh );
 
 	} );
@@ -68,7 +68,7 @@ Menubar.Add = function ( editor ) {
 		var mesh = new THREE.Mesh( geometry, material );
 		mesh.name = 'Plane ' + ( ++ meshCount );
 
-		editor.addObject( mesh );
+		editor.execute( new CmdAddObject( mesh ) );
 		editor.select( mesh );
 
 	} );
@@ -93,7 +93,7 @@ Menubar.Add = function ( editor ) {
 		var mesh = new THREE.Mesh( geometry, new THREE.MeshPhongMaterial() );
 		mesh.name = 'Box ' + ( ++ meshCount );
 
-		editor.addObject( mesh );
+		editor.execute( new CmdAddObject( mesh ) );
 		editor.select( mesh );
 
 	} );
@@ -113,7 +113,7 @@ Menubar.Add = function ( editor ) {
 		var mesh = new THREE.Mesh( geometry, new THREE.MeshPhongMaterial() );
 		mesh.name = 'Circle ' + ( ++ meshCount );
 
-		editor.addObject( mesh );
+		editor.execute( new CmdAddObject( mesh ) );
 		editor.select( mesh );
 
 	} );
@@ -137,7 +137,7 @@ Menubar.Add = function ( editor ) {
 		var mesh = new THREE.Mesh( geometry, new THREE.MeshPhongMaterial() );
 		mesh.name = 'Cylinder ' + ( ++ meshCount );
 
-		editor.addObject( mesh );
+		editor.execute( new CmdAddObject( mesh ) );
 		editor.select( mesh );
 
 	} );
@@ -162,7 +162,7 @@ Menubar.Add = function ( editor ) {
 		var mesh = new THREE.Mesh( geometry, new THREE.MeshPhongMaterial() );
 		mesh.name = 'Sphere ' + ( ++ meshCount );
 
-		editor.addObject( mesh );
+		editor.execute( new CmdAddObject( mesh ) );
 		editor.select( mesh );
 
 	} );
@@ -182,7 +182,7 @@ Menubar.Add = function ( editor ) {
 		var mesh = new THREE.Mesh( geometry, new THREE.MeshPhongMaterial() );
 		mesh.name = 'Icosahedron ' + ( ++ meshCount );
 
-		editor.addObject( mesh );
+		editor.execute( new CmdAddObject( mesh ) );
 		editor.select( mesh );
 
 	} );
@@ -205,7 +205,7 @@ Menubar.Add = function ( editor ) {
 		var mesh = new THREE.Mesh( geometry, new THREE.MeshPhongMaterial() );
 		mesh.name = 'Torus ' + ( ++ meshCount );
 
-		editor.addObject( mesh );
+		editor.execute( new CmdAddObject( mesh ) );
 		editor.select( mesh );
 
 	} );
@@ -230,7 +230,7 @@ Menubar.Add = function ( editor ) {
 		var mesh = new THREE.Mesh( geometry, new THREE.MeshPhongMaterial() );
 		mesh.name = 'TorusKnot ' + ( ++ meshCount );
 
-		editor.addObject( mesh );
+		editor.execute( new CmdAddObject( mesh ) );
 		editor.select( mesh );
 
 	} );
@@ -246,7 +246,7 @@ Menubar.Add = function ( editor ) {
 		var sprite = new THREE.Sprite( new THREE.SpriteMaterial() );
 		sprite.name = 'Sprite ' + ( ++ meshCount );
 
-		editor.addObject( sprite );
+		editor.execute( new CmdAddObject( sprite ) );
 		editor.select( sprite );
 
 	} );
@@ -270,7 +270,7 @@ Menubar.Add = function ( editor ) {
 		var light = new THREE.PointLight( color, intensity, distance );
 		light.name = 'PointLight ' + ( ++ lightCount );
 
-		editor.addObject( light );
+		editor.execute( new CmdAddObject( light ) );
 		editor.select( light );
 
 	} );
@@ -295,7 +295,7 @@ Menubar.Add = function ( editor ) {
 
 		light.position.set( 0.5, 1, 0.75 ).multiplyScalar( 200 );
 
-		editor.addObject( light );
+		editor.execute( new CmdAddObject( light ) );
 		editor.select( light );
 
 	} );
@@ -317,7 +317,7 @@ Menubar.Add = function ( editor ) {
 
 		light.position.set( 0.5, 1, 0.75 ).multiplyScalar( 200 );
 
-		editor.addObject( light );
+		editor.execute( new CmdAddObject( light ) );
 		editor.select( light );
 
 	} );
@@ -339,7 +339,7 @@ Menubar.Add = function ( editor ) {
 
 		light.position.set( 0.5, 1, 0.75 ).multiplyScalar( 200 );
 
-		editor.addObject( light );
+		editor.execute( new CmdAddObject( light ) );
 		editor.select( light );
 
 	} );
@@ -357,7 +357,7 @@ Menubar.Add = function ( editor ) {
 		var light = new THREE.AmbientLight( color );
 		light.name = 'AmbientLight ' + ( ++ lightCount );
 
-		editor.addObject( light );
+		editor.execute( new CmdAddObject( light ) );
 		editor.select( light );
 
 	} );
@@ -377,7 +377,7 @@ Menubar.Add = function ( editor ) {
 		var camera = new THREE.PerspectiveCamera( 50, 1, 1, 10000 );
 		camera.name = 'PerspectiveCamera ' + ( ++ cameraCount );
 
-		editor.addObject( camera );
+		editor.execute( new CmdAddObject( camera ) );
 		editor.select( camera );
 
 	} );

+ 50 - 10
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,7 +95,7 @@ Menubar.Edit = function ( editor ) {
 
 		object = object.clone();
 
-		editor.addObject( object );
+		editor.execute( new CmdAddObject( object ) );
 		editor.select( object );
 
 	} );
@@ -75,7 +113,9 @@ Menubar.Edit = function ( editor ) {
 		if ( confirm( 'Delete ' + object.name + '?' ) === false ) return;
 
 		var parent = object.parent;
-		editor.removeObject( object );
+		if ( parent === undefined ) return; // avoid deleting the camera or scene
+
+		editor.execute( new CmdRemoveObject( object ) );
 		editor.select( parent );
 
 	} );

+ 56 - 0
editor/js/Menubar.File.js

@@ -32,6 +32,62 @@ Menubar.File = function ( editor ) {
 	} );
 	options.add( option );
 
+	// Open Editor-JSON
+
+	var editorJsonInput = document.createElement( 'input' );
+	editorJsonInput.type = 'file';
+	editorJsonInput.addEventListener( 'change', function ( event ) {
+
+		var reader = new FileReader();
+		reader.addEventListener( 'load', function ( event ) {
+
+			var contents = event.target.result;
+
+			var data;
+			try {
+
+				data = JSON.parse( contents );
+				editor.clear();
+				editor.fromJSON( data );
+
+			} catch ( error ) {
+
+				alert( error );
+				return;
+
+			}
+
+		}, false );
+		reader.readAsText( editorJsonInput.files[ 0 ] );
+
+	} );
+
+	var option = new UI.Panel();
+	option.setClass( 'option' );
+	option.setTextContent( 'Open Editor-JSON...' );
+	option.onClick( function () {
+
+		editorJsonInput.click();
+
+	} );
+	options.add( option );
+
+	// Save Editor-JSON
+
+	var option = new UI.Panel();
+	option.setClass( 'option' );
+	option.setTextContent( 'Save Editor-JSON...' );
+	option.onClick( function () {
+
+		var output = editor.toJSON();
+		output = JSON.stringify( output, null, '\t' );
+		output = output.replace( /[\n\t]+([\d\.e\-\[\]]+)/g, '$1' );
+
+		exportString( output, 'editor.json' );
+
+	} );
+	options.add( option );
+
 	//
 
 	options.add( new UI.HorizontalRule() );

+ 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 - 9
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();
 
@@ -32,16 +34,10 @@ 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()
-		);
-
-		object.geometry.computeBoundingSphere();
-
-		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 );
 

+ 4 - 4
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' );
 
@@ -42,9 +44,7 @@ Sidebar.Geometry.Modifiers = function ( signals, object ) {
 
 			if ( confirm( 'Are you sure?' ) === false ) return;
 
-			object.geometry = new THREE.BufferGeometry().fromGeometry( object.geometry );
-
-			signals.geometryChanged.dispatch( object );
+			editor.execute( new CmdSetGeometry( object, new THREE.BufferGeometry().fromGeometry( object.geometry ) ) );
 
 		} );
 		container.add( button );

+ 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 );
+		) ) );
 
 	}
 

+ 21 - 13
editor/js/Sidebar.Geometry.js

@@ -48,22 +48,29 @@ 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.geometryChanged.dispatch( object );
 				editor.signals.objectChanged.dispatch( object );
 
 				break;
 
 			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.execute( new CmdMultiCmds( cmds ) );
+
+				editor.signals.geometryChanged.dispatch( object );
 				editor.signals.objectChanged.dispatch( object );
 
 				break;
@@ -87,7 +94,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() ) );
 
 	} );
 
@@ -102,7 +109,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() ) );
 
 	} );
 
@@ -113,11 +120,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
 
@@ -148,11 +155,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 ) );
 
 			}
 
@@ -166,6 +173,7 @@ Sidebar.Geometry = function ( editor ) {
 
 	signals.objectSelected.add( build );
 	signals.geometryChanged.add( build );
+	signals.updateSidebar.add( build );
 
 	return container;
 

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

@@ -0,0 +1,97 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+Sidebar.History = function ( editor ) {
+
+	var signals = editor.signals;
+
+	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' ) );
+	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.type.substring( 3, object.type.length ).replace(/([a-z])([A-Z])/g, '$1 $2') + "</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.type.substring( 3, object.type.length ).replace(/([a-z])([A-Z])/g, '$1 $2') + "</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;
+
+};

+ 3 - 3
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() ) );
 
 	} );
 
@@ -386,9 +386,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( editor.selected, 'uuid', materialUUID.getValue() ) );
 
 			}
 

+ 64 - 38
editor/js/Sidebar.Object3D.js

@@ -41,15 +41,15 @@ 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;
 
 		}
@@ -71,7 +71,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 +86,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() ) );
 
 	} );
 
@@ -342,96 +342,114 @@ Sidebar.Object3D = function ( editor ) {
 
 		if ( object !== null ) {
 
-			if ( object.parent !== null ) {
+			if ( object.parent !== undefined ) {
 
 				var newParentId = parseInt( objectParent.getValue() );
 
 				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();
+			}
+
+			var newRotation = new THREE.Euler( objectRotationX.getValue(), objectRotationY.getValue(), objectRotationZ.getValue() );
+			if ( object.rotation.toVector3().distanceTo( newRotation.toVector3() ) >= 0.01 ) {
+
+				editor.execute( new CmdSetRotation( object, newRotation ) );
+
+			}
+
+			var newScale = new THREE.Vector3( objectScaleX.getValue(), objectScaleY.getValue(), objectScaleZ.getValue() );
+			if ( object.scale.distanceTo( newScale ) >= 0.01 ) {
 
-			if ( object.fov !== undefined ) {
+				editor.execute( new CmdSetScale( object, newScale ) );
 
-				object.fov = objectFov.getValue();
+			}
+
+			if ( object.fov !== undefined && Math.abs( object.fov - objectFov.getValue() ) >= 0.01 ) {
+
+				editor.execute( new CmdSetValue( object, 'fov', objectFov.getValue() ) );
 				object.updateProjectionMatrix();
 
 			}
 
-			if ( object.near !== undefined ) {
+			if ( object.near !== undefined && Math.abs( object.near - objectNear.getValue() ) >= 0.01 ) {
 
-				object.near = objectNear.getValue();
+				editor.execute( new CmdSetValue( object, 'near', objectNear.getValue() ) );
 
 			}
 
-			if ( object.far !== undefined ) {
+			if ( object.far !== undefined && Math.abs( object.far - objectFar.getValue() ) >= 0.01 ) {
 
-				object.far = objectFar.getValue();
+				editor.execute( new CmdSetValue( object, 'far', objectFar.getValue() ) );
 
 			}
 
-			if ( object.intensity !== undefined ) {
+			if ( object.intensity !== undefined && Math.abs( object.intensity - objectIntensity.getValue() ) >= 0.01 ) {
 
-				object.intensity = objectIntensity.getValue();
+				editor.execute( new CmdSetValue( object, 'intensity', objectIntensity.getValue() ) );
 
 			}
 
-			if ( object.color !== undefined ) {
+			if ( object.color !== undefined && object.color.getHex() !== objectColor.getHexValue() ) {
 
-				object.color.setHex( objectColor.getHexValue() );
+				editor.execute( new CmdSetColor( object, 'color', objectColor.getHexValue() ) );
 
 			}
 
-			if ( object.groundColor !== undefined ) {
+			if ( object.groundColor !== undefined && object.groundColor.getHex() !== objectGroundColor.getHexValue() ) {
 
-				object.groundColor.setHex( objectGroundColor.getHexValue() );
+				editor.execute( new CmdSetColor( object, 'groundColor', objectGroundColor.getHexValue() ) );
 
 			}
 
-			if ( object.distance !== undefined ) {
+			if ( object.distance !== undefined && Math.abs( object.distance - objectDistance.getValue() ) >= 0.01 ) {
 
-				object.distance = objectDistance.getValue();
+				editor.execute( new CmdSetValue( object, 'distance', objectDistance.getValue() ) );
 
 			}
 
-			if ( object.angle !== undefined ) {
+			if ( object.angle !== undefined && Math.abs( object.angle - objectAngle.getValue() ) >= 0.01 ) {
 
-				object.angle = objectAngle.getValue();
+				editor.execute( new CmdSetValue( object, 'angle', objectAngle.getValue() ) );
 
 			}
 
-			if ( object.exponent !== undefined ) {
+			if ( object.exponent !== undefined && Math.abs( object.exponent - objectExponent.getValue() ) >= 0.01 ) {
 
-				object.exponent = objectExponent.getValue();
+				editor.execute( new CmdSetValue( object, 'exponent', objectExponent.getValue() ) );
 
 			}
 
-			if ( object.decay !== undefined ) {
+			if ( object.decay !== undefined && Math.abs( object.decay - objectDecay.getValue() ) >= 0.01 ) {
 
-				object.decay = objectDecay.getValue();
+				editor.execute( new CmdSetValue( object, 'decay', objectDecay.getValue() ) );
 
 			}
 
-			object.visible = objectVisible.getValue();
+			if ( object.visible !== objectVisible.getValue() ) {
+
+				editor.execute( new CmdToggleBoolean( object, 'visible' ) );
+
+			}
 
 			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 ) {
 
@@ -528,6 +546,14 @@ Sidebar.Object3D = function ( editor ) {
 
 	} );
 
+	signals.updateSidebar.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 CmdSetScriptName( editor.selected, script, 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 ) );

+ 37 - 16
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.updateSidebar.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,22 +75,33 @@ 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 ));
 
-			( function ( matrix1, matrix2 ) {
+					}
+					break;
+				case 'rotate':
+					if (!objectRotationOnDown.equals(object.rotation)) {
+
+						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)) {
 
-			} )( matrix.clone(), object.matrix.clone() );
+						editor.execute(new CmdSetScale( object, object.scale, objectScaleOnDown ));
+
+					}
+					break;
+
+			}
 
 		}
 
@@ -358,9 +375,13 @@ var Viewport = function ( editor ) {
 
 	} );
 
-	signals.geometryChanged.add( function ( geometry ) {
+	signals.geometryChanged.add( function ( object ) {
+
+		if ( object !== null ) {
 
-		selectionBox.update( editor.selected );
+			selectionBox.update( object );
+
+		}
 
 		render();
 

+ 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

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

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

@@ -0,0 +1,69 @@
+// module( "CommonUtilities" );
+
+function aBox( name ) {
+
+	var width = 100;
+	var height = 100;
+	var depth = 100;
+
+	var widthSegments = 1;
+	var heightSegments = 1;
+	var depthSegments = 1;
+
+	var geometry = new THREE.BoxGeometry( width, height, depth, widthSegments, heightSegments, depthSegments );
+	var mesh = new THREE.Mesh( geometry, new THREE.MeshPhongMaterial() );
+	mesh.name = name || "Box 1";
+
+	return mesh;
+
+}
+
+function aSphere( name ) {
+
+	var width = 100;
+	var height = 100;
+	var depth = 100;
+
+	var widthSegments = 1;
+	var heightSegments = 1;
+	var depthSegments = 1;
+
+	var geometry = new THREE.SphereGeometry( width, height, depth, widthSegments, heightSegments, depthSegments );
+	var mesh = new THREE.Mesh( geometry, new THREE.MeshPhongMaterial() );
+	mesh.name = name || "Sphere 1";
+
+	return mesh;
+
+}
+
+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;
+}

+ 20 - 0
test/unit/editor/TestCmdAddObject.js

@@ -0,0 +1,20 @@
+module( "CmdAddObject" );
+
+test( "Test CmdAddObject (Undo and Redo)", function() {
+
+	var editor = new Editor();
+
+	var theName = "This awesome box";
+
+	var mesh = aBox( theName );
+
+	editor.execute( new CmdAddObject( mesh ) );
+	ok( editor.scene.children.length == 1, "OK, adding object was successful " );
+
+	editor.undo();
+	ok( editor.scene.children.length == 0, "OK, adding object is undone (was removed)" );
+
+	editor.redo();
+	ok( editor.scene.children[0].name == theName , "OK, removed object was added again (redo)" );
+
+});

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

@@ -0,0 +1,54 @@
+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" );
+
+	}
+
+
+});

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

@@ -0,0 +1,37 @@
+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 ");
+
+});

+ 27 - 0
test/unit/editor/TestCmdRemoveObject.js

@@ -0,0 +1,27 @@
+module( "CmdRemoveObject" );
+
+test( "Test CmdRemoveObject (Undo and Redo)", function() {
+
+	var editor = new Editor();
+
+	var theName = "Come back!" ;
+
+	var mesh = aBox( theName );
+
+	editor.execute( new CmdAddObject( mesh ) );
+	editor.select( mesh );
+
+	// var object = editor.selected;
+	var parent = mesh.parent;
+
+	editor.execute( new CmdRemoveObject( mesh ) );
+	editor.select( parent );
+	ok( editor.scene.children.length == 0, "OK, object removal was successful" );
+
+	editor.undo();
+	ok( editor.scene.children[0].name == theName, "OK, removal was undone successfully, object exists again" );
+
+	editor.redo();
+	ok( editor.scene.children.length == 0, "OK, object was removed again (redo removal)" );
+
+});

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

@@ -0,0 +1,55 @@
+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 @@
+module( "CmdSetColor" );
+
+test("Test CmdSetColor (Undo and Redo)", function() {
+
+	var editor = new Editor();
+
+	var object = aPointlight( "The light Light" );
+
+	var green   = 12581843; // bffbd3
+	var blue    = 14152447; // d7f2ff
+	var yellow  = 16775383; // fff8d7
+
+	editor.execute( new CmdAddObject( object ) );
+
+	// set color to green
+	var cmd = new CmdSetColor( object, 'color', green );
+	cmd.updatable = false;	// Because otherwise the commands are merged into one command
+	editor.execute( cmd );
+	ok( object.color.getHex() == green , "OK, color has been set successfully, Expected: '" + green + "', Actual: '" + object.color.getHex() + "'" );
+
+	// set color to blue
+	var cmd = new CmdSetColor( object, 'color', blue );
+	cmd.updatable = false;	// Because otherwise the commands are merged into one command
+	editor.execute( cmd );
+	ok( object.color.getHex() == blue , "OK, color has been set successfully, Expected: '" + blue + "', Actual: '" + object.color.getHex() + "'" );
+
+	// set color to yellow
+	var cmd = new CmdSetColor( object, 'color', yellow );
+	cmd.updatable = false;	// Because otherwise the commands are merged into one command
+	editor.execute( cmd );
+	ok( object.color.getHex() == yellow , "OK, color has been set successfully, Expected: '" + yellow + "', Actual: '" + object.color.getHex() + "'" );
+
+
+	editor.undo();
+	ok( object.color.getHex() == blue, "OK, changing color has been undone, Expected: '" + blue + "', Actual: '" + object.color.getHex() + "'" );
+
+	editor.redo();
+	ok( object.color.getHex() == yellow , "OK, changing color has been redone, Expected: '" + yellow + "', Actual: '" + object.color.getHex() + "'" );
+
+});

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

@@ -0,0 +1,39 @@
+module( "CmdSetPosition" );
+
+test( "Test CmdSetPosition (Undo and Redo)", function() {
+
+	var editor = new Editor();
+
+	var mesh = aBox();
+	var initPosX =  50 ;
+	var initPosY = -80 ;
+	var initPosZ =  30 ;
+	mesh.position.x = initPosX ;
+	mesh.position.y = initPosY ;
+	mesh.position.z = initPosZ ;
+
+	editor.execute( new CmdAddObject( mesh ) );
+	editor.select( mesh );
+
+	// translate the object
+	var newPosX = 100 ;
+	var newPosY = 200 ;
+	var newPosZ = 500 ;
+	var newPosition = new THREE.Vector3( newPosX, newPosY, newPosZ );
+	editor.execute( new CmdSetPosition( mesh, newPosition ) );
+
+	ok( mesh.position.x != initPosX, "OK, changing X position was successful" );
+	ok( mesh.position.y != initPosY, "OK, changing Y position was successful" );
+	ok( mesh.position.z != initPosZ, "OK, changing Z position was successful" );
+
+	editor.undo();
+	ok( mesh.position.x == initPosX, "OK, changing X position value is undone" );
+	ok( mesh.position.y == initPosY, "OK, changing Y position value is undone" );
+	ok( mesh.position.z == initPosZ, "OK, changing Z position value is undone" );
+
+	editor.redo();
+	ok( mesh.position.x == newPosX, "OK, changing X position value is redone" );
+	ok( mesh.position.y == newPosY, "OK, changing Y position value is redone" );
+	ok( mesh.position.z == newPosZ, "OK, changing Z position value is redone" );
+
+});

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

@@ -0,0 +1,39 @@
+module( "CmdSetRotation" );
+
+test( "Test CmdSetRotation (Undo and Redo)", function() {
+
+	var editor = new Editor();
+
+	var mesh = aBox();
+	var initRotationX =  1.1 ;
+	var initRotationY =  0.4 ;
+	var initRotationZ = -2.0 ;
+	mesh.rotation.x = initRotationX ;
+	mesh.rotation.y = initRotationY ;
+	mesh.rotation.z = initRotationZ ;
+
+	editor.execute( new CmdAddObject( mesh ) );
+	editor.select( mesh );
+
+	// rotate the object
+	var newRotationX =  -3.2 ;
+	var newRotationY =   0.8 ;
+	var newRotationZ =   1.5 ;
+	var newRotation = new THREE.Euler( newRotationX, newRotationY, newRotationZ );
+	editor.execute ( new CmdSetRotation( mesh, newRotation ) );
+
+	ok( mesh.rotation.x != initRotationX, "OK, changing X rotation was successful" );
+	ok( mesh.rotation.y != initRotationY, "OK, changing Y rotation was successful" );
+	ok( mesh.rotation.z != initRotationZ, "OK, changing Z rotation was successful" );
+
+	editor.undo();
+	ok( mesh.rotation.x == initRotationX, "OK, changing X rotation value is undone" );
+	ok( mesh.rotation.y == initRotationY, "OK, changing Y rotation value is undone" );
+	ok( mesh.rotation.z == initRotationZ, "OK, changing Z rotation value is undone" );
+
+	editor.redo();
+	ok( mesh.rotation.x == newRotationX, "OK, changing X rotation value is redone" );
+	ok( mesh.rotation.y == newRotationY, "OK, changing Y rotation value is redone" );
+	ok( mesh.rotation.z == newRotationZ, "OK, changing Z rotation value is redone" );
+
+});

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

@@ -0,0 +1,39 @@
+module( "CmdSetScale" );
+
+test( "Test CmdSetScale (Undo and Redo)", function() {
+
+	var editor = new Editor();
+
+	var mesh = aBox();
+	var initScaleX =  1.4 ;
+	var initScaleY =  2.7 ;
+	var initScaleZ =  0.4 ;
+	mesh.scale.x = initScaleX ;
+	mesh.scale.y = initScaleY ;
+	mesh.scale.z = initScaleZ ;
+
+	editor.execute( new CmdAddObject( mesh ) );
+	editor.select( mesh );
+
+	// (re)scale the object
+	var newScaleX = 0.1 ;
+	var newScaleY = 5.3 ;
+	var newScaleZ = 1.0 ;
+	var newScale = new THREE.Vector3( newScaleX, newScaleY, newScaleZ );
+	editor.execute ( new CmdSetScale( mesh, newScale ) );
+
+	ok( mesh.scale.x != initScaleX, "OK, changing X scale was successful" );
+	ok( mesh.scale.y != initScaleY, "OK, changing Y scale was successful" );
+	ok( mesh.scale.z != initScaleZ, "OK, changing Z scale was successful" );
+
+	editor.undo();
+	ok( mesh.scale.x == initScaleX, "OK, changing X scale value is undone" );
+	ok( mesh.scale.y == initScaleY, "OK, changing Y scale value is undone" );
+	ok( mesh.scale.z == initScaleZ, "OK, changing Z scale value is undone" );
+
+	editor.redo();
+	ok( mesh.scale.x == newScaleX, "OK, changing X scale value is redone" );
+	ok( mesh.scale.y == newScaleY, "OK, changing Y scale value is redone" );
+	ok( mesh.scale.z == newScaleZ, "OK, changing Z scale value is redone" );
+
+});

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

@@ -0,0 +1,37 @@
+module("CmdSetScriptName");
+
+test( "Test CmdSetScriptName", function() {
+
+	var editor = new Editor();
+
+	var box    = aBox( "The scripted box" );
+	var xMove  = { name: "", source: "function update( event ) { this.position.x = this.position.x + 1; }" };
+
+	var names = [ "name 1", "name 2" ];
+
+	editor.execute( new CmdAddObject( box ) );
+
+	var cmd = new CmdAddScript( box, xMove );
+	editor.execute( cmd );
+
+	ok( Object.keys( editor.scripts ).length == 1, "OK, script has been added" );
+
+	names.map( function( name ) {
+
+		cmd = new CmdSetScriptName( box, xMove, 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();
+	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, again" );
+
+});

+ 31 - 0
test/unit/editor/TestCmdSetScriptSource.js

@@ -0,0 +1,31 @@
+module( "CmdSetScriptSource" );
+
+test( "Test CmdSetScriptSource (Undo and Redo)", function() {
+
+	var editor = new Editor();
+
+	var box    = aBox( "The scripted box" );
+	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 ];
+
+	editor.execute( new CmdAddObject( box ) );
+
+ 	var cmd = new CmdAddScript( box, scripts[0] );
+ 	cmd.updatable = false;
+ 	editor.execute( cmd );
+
+ 	cmd = new CmdSetScriptSource( box, xMove, yMove['source'], xMove['source'], 0 );
+ 	cmd.updatable = false;
+ 	editor.execute( cmd );
+	ok( editor.scripts[ box.uuid ][0][ 'source' ] == yMove[ 'source' ], "OK, script source has been set successfully");
+
+ 	console.log(  editor.scripts );
+
+	editor.undo();
+	ok( editor.scripts[ box.uuid ][0][ 'source' ] == xMove[ 'source' ], "OK, script source has been set to previous state");
+
+	editor.redo();
+	ok( editor.scripts[ box.uuid ][0][ 'source' ] == yMove[ 'source' ], "OK, script source has been reverted successfully");
+
+});

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

@@ -0,0 +1,30 @@
+module( "CmdSetUuid" );
+
+test( "Test CmdSetUuid (Undo and Redo)", function(){
+
+	var editor = new Editor();
+	var theName = "Initial name";
+	var object = aBox( theName );
+
+	var uuidBefore = THREE.Math.generateUUID();
+	var uuidAfter  = THREE.Math.generateUUID();
+
+	editor.execute( new CmdAddObject( object ) );
+
+	var cmd = new CmdSetUuid( object, uuidBefore );
+	cmd.updatable = false;
+	editor.execute( cmd );
+	ok( object[ 'uuid' ] == uuidBefore, "OK, UUID is correct after first execute ");
+
+	var cmd = new CmdSetUuid( object, uuidAfter );
+	cmd.updatable = false;
+	editor.execute( cmd );
+	ok( object[ 'uuid' ] == uuidAfter, "OK, UUID is correct after second execute ");
+
+	editor.undo();
+	ok( object[ 'uuid' ] == uuidBefore, "OK, UUID is correct after undo ");
+
+	editor.redo();
+	ok( object[ 'uuid' ] == uuidAfter, "OK, UUID is correct after redo ");
+
+});

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

@@ -0,0 +1,49 @@
+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' ].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 ] + "')");
+
+			}
+
+		});
+
+	});
+
+
+
+
+});

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

@@ -0,0 +1,40 @@
+module( "CmdToggleBoolean" );
+
+test( "Test CmdToggleBoolean (Undo and Redo)", function(){
+
+	var editor = new Editor();
+
+	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 + "'" );
+
+		[ 'visible' ].map( function( item ) {
+
+			if( object[ item ] !== undefined ) {
+
+				var beforeState =  object[ item ];
+				var afterState  = !object[ item ];
+				ok( 0 == 0, " Initial state of '" + item  + "' is '" + object[ item ] + "'" );
+
+				var cmd = new CmdToggleBoolean( object, item );
+				cmd.updatable = false;
+				editor.execute( cmd );
+				ok( object[ item ] == afterState , " OK, toggling boolean of '" + item + "' has been executed (expected: '" + afterState + "', actual: '" + object[ item ] + "')" );
+
+				editor.undo();
+				ok( object[ item ] == beforeState, " OK, toggling boolean of '" + item + "' has been undone (expected: '" + beforeState + "', actual: '" + object[ item ] + "')" );
+
+				editor.redo();
+				ok( object[ item ] == afterState , " OK, toggling boolean of '" + item + "' has been redone (expected: '" + afterState + "', actual: '" + object[ item ] + "')" );
+
+			}
+
+		});
+	});
+
+});

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

@@ -0,0 +1,112 @@
+module( "NestedDoUndoRedo" );
+
+test( "Test nested Do's, Undo's and Redo's ", function() {
+
+	// TODO: replace CmdNameObject by CmdSetValue
+
+	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 ) );
+
+//	editor.execute( new CmdNameObject( editor, mesh, 'Nothing is as it was before' ) );
+
+	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.name == "Nothing is as it was before", "OK, name is correct" );
+
+	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
+//	editor.undo();  // renaming undone
+
+	/* full check */
+//	ok( mesh.name == "One box unlike all others", "OK, name is correct" );
+
+	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();  // renaming redone
+	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.name == "Nothing is as it was before", "OK, name is correct" );
+
+	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 " );
+
+
+});

+ 111 - 0
test/unit/unittests_editor.html

@@ -0,0 +1,111 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>ThreeJS Unit Tests - Using Files in /editor</title>
+  <link rel="stylesheet" href="http://code.jquery.com/qunit/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/jszip.min.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/CmdRemoveObject.js"></script>
+<script src="../../editor/js/CmdRemoveScript.js"></script>
+<script src="../../editor/js/CmdSetColor.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/CmdSetScriptName.js"></script>
+<script src="../../editor/js/CmdSetScriptSource.js"></script>
+<script src="../../editor/js/CmdSetUuid.js"></script>
+<script src="../../editor/js/CmdSetValue.js"></script>
+<script src="../../editor/js/CmdToggleBoolean.js"></script>
+
+
+<!-- add class-based unit tests below -->
+<script src="editor/CommonUtilities.js"></script>
+
+<!-- Undo-Redo tests -->
+<script src="editor/TestCmdAddObject.js"></script>
+<script src="editor/TestCmdAddScript.js"></script>
+<script src="editor/TestCmdMoveObject.js"></script>
+<script src="editor/TestCmdRemoveObject.js"></script>
+<script src="editor/TestCmdRemoveScript.js"></script>
+<script src="editor/TestCmdSetColor.js"></script>
+<script src="editor/TestCmdSetPosition.js"></script>
+<script src="editor/TestCmdSetRotation.js"></script>
+<script src="editor/TestCmdSetScale.js"></script>
+<script src="editor/TestCmdSetScriptName.js"></script>
+<script src="editor/TestCmdSetScriptSource.js"></script>
+<script src="editor/TestCmdSetUuid.js"></script>
+<script src="editor/TestCmdSetValue.js"></script>
+<script src="editor/TestCmdToggleBoolean.js"></script>
+<script src="editor/TestNestedDoUndoRedo.js"></script>
+
+</body>
+</html>