Browse Source

Merge branch 'dev' of https://github.com/leonsal/three.js into dev

Leonel 9 years ago
parent
commit
b4eb677632
100 changed files with 5239 additions and 2237 deletions
  1. 1 0
      .gitignore
  2. 2 1
      .npmignore
  3. 8 8
      bower.json
  4. 368 277
      build/three.js
  5. 411 366
      build/three.min.js
  6. 26 8
      docs/api/core/Geometry.html
  7. 1 1
      docs/api/core/Raycaster.html
  8. 2 2
      docs/api/examples/SpriteCanvasMaterial.html
  9. 0 88
      docs/api/extras/ImageUtils.html
  10. 0 20
      docs/api/extras/geometries/CubeGeometry.html
  11. 1 1
      docs/api/loaders/MTLLoader.html
  12. 0 8
      docs/api/loaders/MaterialLoader.html
  13. 0 5
      docs/api/loaders/XHRLoader.html
  14. 62 62
      docs/api/materials/LineBasicMaterial.html
  15. 19 7
      docs/api/materials/MeshPhongMaterial.html
  16. 9 9
      docs/api/math/Box2.html
  17. 14 14
      docs/api/math/Box3.html
  18. 4 4
      docs/api/math/Matrix3.html
  19. 3 3
      docs/api/math/Matrix4.html
  20. 4 4
      docs/api/math/Plane.html
  21. 9 9
      docs/api/math/Ray.html
  22. 5 0
      docs/api/renderers/WebGLRenderer.html
  23. 11 13
      docs/api/textures/CubeTexture.html
  24. 2 4
      docs/list.js
  25. 14 14
      docs/page.js
  26. 34 33
      docs/scenes/bones-browser.html
  27. 2 1
      docs/scenes/geometry-browser.html
  28. 28 27
      docs/scenes/material-browser.html
  29. 49 28
      editor/css/dark.css
  30. 47 24
      editor/css/light.css
  31. 29 21
      editor/css/main.css
  32. 132 0
      editor/docs/Implementing additional commands for undo-redo.md
  33. 94 0
      editor/docs/Writing unit tests for undo-redo commands.md
  34. 50 13
      editor/index.html
  35. 47 0
      editor/js/Command.js
  36. 3 6
      editor/js/Config.js
  37. 77 21
      editor/js/Editor.js
  38. 277 35
      editor/js/History.js
  39. 132 119
      editor/js/Loader.js
  40. 60 91
      editor/js/Menubar.Add.js
  41. 63 18
      editor/js/Menubar.Edit.js
  42. 1 1
      editor/js/Menubar.Examples.js
  43. 45 71
      editor/js/Menubar.File.js
  44. 2 2
      editor/js/Menubar.Help.js
  45. 6 9
      editor/js/Menubar.Status.js
  46. 0 81
      editor/js/Menubar.View.js
  47. 0 1
      editor/js/Menubar.js
  48. 66 17
      editor/js/Script.js
  49. 1 1
      editor/js/Sidebar.Animation.js
  50. 12 16
      editor/js/Sidebar.Geometry.BoxGeometry.js
  51. 11 7
      editor/js/Sidebar.Geometry.BufferGeometry.js
  52. 10 12
      editor/js/Sidebar.Geometry.CircleGeometry.js
  53. 12 16
      editor/js/Sidebar.Geometry.CylinderGeometry.js
  54. 9 7
      editor/js/Sidebar.Geometry.Geometry.js
  55. 8 10
      editor/js/Sidebar.Geometry.IcosahedronGeometry.js
  56. 5 3
      editor/js/Sidebar.Geometry.Modifiers.js
  57. 10 14
      editor/js/Sidebar.Geometry.PlaneGeometry.js
  58. 13 17
      editor/js/Sidebar.Geometry.SphereGeometry.js
  59. 8 8
      editor/js/Sidebar.Geometry.TeapotBufferGeometry.js
  60. 11 15
      editor/js/Sidebar.Geometry.TorusGeometry.js
  61. 13 17
      editor/js/Sidebar.Geometry.TorusKnotGeometry.js
  62. 37 38
      editor/js/Sidebar.Geometry.js
  63. 117 0
      editor/js/Sidebar.History.js
  64. 385 118
      editor/js/Sidebar.Material.js
  65. 108 97
      editor/js/Sidebar.Object.js
  66. 13 37
      editor/js/Sidebar.Project.js
  67. 76 0
      editor/js/Sidebar.Properties.js
  68. 25 17
      editor/js/Sidebar.Scene.js
  69. 5 6
      editor/js/Sidebar.Script.js
  70. 47 0
      editor/js/Sidebar.Settings.js
  71. 68 7
      editor/js/Sidebar.js
  72. 4 7
      editor/js/Toolbar.js
  73. 67 68
      editor/js/Viewport.js
  74. 66 0
      editor/js/commands/AddObjectCommand.js
  75. 76 0
      editor/js/commands/AddScriptCommand.js
  76. 107 0
      editor/js/commands/MoveObjectCommand.js
  77. 85 0
      editor/js/commands/MultiCmdsCommand.js
  78. 103 0
      editor/js/commands/RemoveObjectCommand.js
  79. 81 0
      editor/js/commands/RemoveScriptCommand.js
  80. 74 0
      editor/js/commands/SetColorCommand.js
  81. 86 0
      editor/js/commands/SetGeometryCommand.js
  82. 71 0
      editor/js/commands/SetGeometryValueCommand.js
  83. 74 0
      editor/js/commands/SetMaterialColorCommand.js
  84. 74 0
      editor/js/commands/SetMaterialCommand.js
  85. 125 0
      editor/js/commands/SetMaterialMapCommand.js
  86. 76 0
      editor/js/commands/SetMaterialValueCommand.js
  87. 83 0
      editor/js/commands/SetPositionCommand.js
  88. 84 0
      editor/js/commands/SetRotationCommand.js
  89. 84 0
      editor/js/commands/SetScaleCommand.js
  90. 100 0
      editor/js/commands/SetSceneCommand.js
  91. 88 0
      editor/js/commands/SetScriptValueCommand.js
  92. 71 0
      editor/js/commands/SetUuidCommand.js
  93. 76 0
      editor/js/commands/SetValueCommand.js
  94. 20 9
      editor/js/libs/app.js
  95. 38 0
      editor/js/libs/app/index.html
  96. 100 50
      editor/js/libs/ui.js
  97. 59 22
      editor/js/libs/ui.three.js
  98. 1 1
      examples/canvas_morphtargets_horse.html
  99. 7 57
      examples/css3d_youtube.html
  100. 35 13
      examples/index.html

+ 1 - 0
.gitignore

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

+ 2 - 1
.npmignore

@@ -1,4 +1,5 @@
-examples/
+examples/*
+!examples/js/
 src/
 test/
 utils/

+ 8 - 8
bower.json

@@ -14,13 +14,13 @@
 	"ignore": [
 		"**/.*",
 		"*.md",
-		"docs",
-		"editor",
-		"examples/*",
-		"!examples/js",
-		"src",
-		"test",
-		"utils",
-		"LICENSE"
+		"/docs",
+		"/editor",
+		"/examples/*",
+		"!/examples/js",
+		"/src",
+		"/test",
+		"/utils",
+		"/LICENSE"
 	]
 }

File diff suppressed because it is too large
+ 368 - 277
build/three.js


File diff suppressed because it is too large
+ 411 - 366
build/three.min.js


+ 26 - 8
docs/api/core/Geometry.html

@@ -96,16 +96,32 @@
 
 		<h3>[property:Array skinWeights]</h3>
 		<div>
-		Array of [page:Vector4 Vector4s] representing the skinning weights as used in a [page:SkinnedMesh],
-		The weights match the number and order of vertices in the geometry. The weighted values
-		are typically between the values of 0 and 1 and affect the amount that the individuals bones affect
-		a given vertex.
+		When working with a [page:SkinnedMesh], each vertex can have up to 4 [page:Bone bones] affecting it.
+		The skinWeights property is an array of weight values that correspond to the order of the vertices in
+		the geometry. So for instance, the first skinWeight would correspond to the first vertex in the geometry.
+		Since each vertex can be modified by 4 bones, a [page:Vector4] is used to represent the skin weights
+		for that vertex.
+		</div>
+		<div>
+		The values of the vector should typically be between 0 and 1. For instance when set to 0 the bone
+		transformation will have no affect. When set to 0.5 it will have 50% affect. When set to 100%, it will
+		have 100% affect. If there is only 1 bone associated with the vertex then you only need to worry about
+		the first component of the vector, the rest can be ignored and set to 0.
 		</div>
 
 		<h3>[property:Array skinIndices]</h3>
 		<div>
-		Array of [page:Vector4 Vector4s] representing the indices of individual bones in the [page:Skeleton.bones] array,
-		The indices match the number and order of vertices in the geometry.
+		Just like the skinWeights property, the skinIndices' values correspond to the geometry's vertices.
+		Each vertex can have up to 4 bones associated with it. So if you look at the first vertex, and the
+		first skinIndex, this will tell you the bones associated with that vertex. For example the first vertex
+		could have a value of <strong>( 10.05, 30.10, 12.12 )</strong>. Then the first skin index could have the
+		value of <strong>( 10, 2, 0, 0 )</strong>. The first skin weight could have the value of
+		<strong>( 0.8, 0.2, 0, 0 )</strong>. In affect this would take the first vertex, and then the bone
+		<strong>mesh.bones[10]</strong> and apply it 80% of the way. Then it would take the bone <strong>skeleton.bones[2]</strong>
+		and apply it 20% of the way. The next two values have a weight of 0, so they would have no affect.
+		</div>
+		<div>
+		In code another example could look like this:
 		<code>
 		// e.g.
 		geometry.skinIndices[15] = new THREE.Vector4(   0,   5,   9, 0 );
@@ -118,7 +134,7 @@
 		skeleton.bones[0]; // weight of 0.2
 		skeleton.bones[5]; // weight of 0.5
 		skeleton.bones[9]; // weight of 0.3
-		skeleton.bones[0]; // weight of 0
+		skeleton.bones[10]; // weight of 0
 		</code>
 		</div>
 
@@ -271,13 +287,15 @@
 		<h3>[method:null normalize]()</h3>
 		<div>
 		Normalize the geometry. <br />
-		Make the geometry centered and has a bounding sphere whose raidus equals to 1.0.
+		Make the geometry centered and has a bounding sphere whose radius equals to 1.0.
 		</div>
 
 		<h3>[method:Geometry clone]()</h3>
 		<div>
 		Creates a new clone of the Geometry.
 		</div>
+		
+		<div>This method copies only vertices, faces and uvs. It does not copy any other properties of the geometry.</div>
 
 		<h3>[method:null dispose]()</h3>
 		<div>

+ 1 - 1
docs/api/core/Raycaster.html

@@ -56,7 +56,7 @@
 			[example:webgl_interactive_cubes_ortho Raycasting to a Mesh in using an OrthographicCamera], 
 			[example:webgl_interactive_buffergeometry Raycasting to a Mesh with BufferGeometry], 
 			[example:webgl_interactive_lines Raycasting to a Line], 
-			[example:webgl_interactive_raycasting_pointcloud Raycasting to a Points], 
+			[example:webgl_interactive_raycasting_points Raycasting to Points], 
 			[example:webgl_geometry_terrain_raycast Terrain raycasting], 
 			[example:webgl_octree_raycasting Raycasting using an octree],
 			[example:webgl_interactive_voxelpainter Raycasting to paint voxels]</div>

+ 2 - 2
docs/api/materials/SpriteCanvasMaterial.html → docs/api/examples/SpriteCanvasMaterial.html

@@ -1,7 +1,7 @@
 <!DOCTYPE html>
 <html lang="en">
 	<head>
-		<meta charset="utf-8" />
+		<meta charset="utf-8" />
 		<base href="../../" />
 		<script src="list.js"></script>
 		<script src="page.js"></script>
@@ -52,6 +52,6 @@
 
 		<h2>Source</h2>
 
-		[link:https://github.com/mrdoob/three.js/blob/master/src/[path].js src/[path].js]
+		[link:https://github.com/mrdoob/three.js/blob/master/examples/js/renderers/CanvasRenderer.js examples/js/renderers/CanvasRenderer.js]
 	</body>
 </html>

+ 0 - 88
docs/api/extras/ImageUtils.html

@@ -1,88 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-	<head>
-		<meta charset="utf-8" />
-		<base href="../../" />
-		<script src="list.js"></script>
-		<script src="page.js"></script>
-		<link type="text/css" rel="stylesheet" href="page.css" />
-	</head>
-	<body>
-		<h1>[name]</h1>
-
-		<div class="desc">A Helper class to ease the loading of images of different types.</div>
-		
-
-		<h2>Properties</h2>
-
-
-
-		<h3>[property:string crossOrigin]</h3>
-		<div>
-		The crossOrigin string to implement CORS for loading the image from a different domain that allows CORS.
-		</div> 
-
-		<h2>Methods</h2>
-
-
-
-		<h3>[method:DataTexture generateDataTexture]([page:Number width], [page:Number height], [page:Number color])</h3>
-		<div>
-		width -- The width of the texture. <br />
-		height -- The height of the texture. <br />
-		color -- The hexadecimal value of the color.
-		</div>
-		<div>
-		Generates a texture of a single color. It is a DataTexture with format, RGBFormat.
-		</div>
-
-		<h3>[method:CompressedTexture parseDDS]([page:String buffer], [page:boolean loadMipmaps])</h3>
-		<div>
-		buffer -- A string containing the data of the dds. <br />
-		loadMipmaps -- A boolean to indicate if you need to load the mipmaps. Default is True.
-		</div>
-		<div>
-		Parses a DDS Image from the string into a CompressedTexture. 
-		</div>
-
-		<h3>[method:Texture loadTexture]([page:String url], [page:UVMapping mapping], [page:Function onLoad], [page:Function onError])</h3>
-		<div>
-		url -- the url of the texture<br />
-		mapping -- Can be an instance of [page:UVMapping THREE.UVMapping], [page:CubeReflectionMapping THREE.CubeReflectionMapping] or [page:SphericalReflectionMapping THREE.SphericalReflectionMapping]. Describes how the image is applied to the object.<br />Use undefined instead of null as a default value. See mapping property of [page:Texture texture] for more details. <br/>
-		onLoad -- callback function<br />
-		onError -- callback function
-		</div>
-		<div>
-		A helper function to generates a [page:Texture THREE.Texture] from an image URL. Provides a load and error
-		callback.
-		</div>
-
-		<h3>[method:canvas getNormalMap]([page:Image image], [page:Float depth])</h3>
-		<div>
-		image -- A loaded image element <br />
-		depth -- The depth of the normal map. Defaults to between -1 and 1.
-		</div>
-		<div>
-		Translates an image element into a normal map with the range (-1, -1, -1) to (1, 1, 1) multiplied by the depth.
-		Returns a canvas element.
-		</div>
-
-		<h3>[method:CubeTexture loadTextureCube]([page:Array array], [page:Textures mapping], [page:function onLoad], [page:function onError])</h3>
-		<div>
-		array -- An array of 6 images <br />
-		mapping -- Either [page:Textures THREE.CubeReflectionMapping] or [page:Textures THREE.CubeRefractionMapping]<br />
-		onLoad -- callback <br />
-		onError -- callback
-		</div>
-		<div>
-		Creates a [page:CubeTexture] from 6 images.<br /><br />
-		
-		The images are loaded in the following order where p is positiive and n is negative: [ px, nx, py, ny, pz, nz ].
-		See [page:CubeTexture] for an example in code.
-		</div>
-
-		<h2>Source</h2>
-
-		[link:https://github.com/mrdoob/three.js/blob/master/src/[path].js src/[path].js]
-	</body>
-</html>

+ 0 - 20
docs/api/extras/geometries/CubeGeometry.html

@@ -1,20 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-	<head>
-		<meta charset="utf-8" />
-		<base href="../../../" />
-		<script src="list.js"></script>
-		<script src="page.js"></script>
-		<link type="text/css" rel="stylesheet" href="page.css" />
-	</head>
-	<body>
-		[page:Geometry] &rarr;
-
-		<h1>[name]</h1>
-
-		<div class="desc">Renamed CubeGeometry to BoxGeometry. see [page:BoxGeometry].</div>
-
-
-		[link:https://github.com/mrdoob/three.js/blob/master/src/[path].js src/[path].js]
-	</body>
-</html>

+ 1 - 1
docs/api/loaders/MTLLoader.html

@@ -19,7 +19,7 @@
 		<h3>[name]( [page:String baseUrl], [page:Object options], [page:String crossOrigin] )</h3>
 		<div>
 		[page:String baseUrl] — The base url from which to find subsequent resources.<br />
-		[page:Object options] — Options passed to the created material (side, wrap, normalizeRGB, ignoreZeroRGBs, invertTransparency).<br />
+		[page:Object options] — Options passed to the created material (side, wrap, normalizeRGB, ignoreZeroRGBs).<br />
 		[page:String crossOrigin] — The crossOrigin string to implement CORS for loading the url from a different domain that allows CORS.<br />
 		</div>
 		<div>

+ 0 - 8
docs/api/loaders/MaterialLoader.html

@@ -37,14 +37,6 @@
 		Begin loading from url and return the [page:Material] object that will contain the data.
 		</div>
 
-		<h3>[method:null setCrossOrigin]( [page:String value] )</h3>
-		<div>
-		[page:String value] — The crossOrigin string.
-		</div>
-		<div>
-		The crossOrigin string to implement CORS for loading the url from a different domain that allows CORS.
-		</div>
-
 		<h3>[method:Material parse]( [page:Object json] )</h3>
 		<div>
 		[page:Object json] — The json object containing the parameters of the Material.

+ 0 - 5
docs/api/loaders/XHRLoader.html

@@ -32,11 +32,6 @@
 		A [page:Cache cache] instance that hold the response from each request made through this loader, so each file is requested once.
 		</div>
 
-		<h3>[property:String crossOrigin]</h3>
-		<div>
-		The crossOrigin string to implement CORS for loading the url from a different domain that allows CORS.
-		</div>
-
 		<h3>[property:String responseType]</h3>
 		<div>
 		Can be set to change the response type.

+ 62 - 62
docs/api/materials/LineBasicMaterial.html

@@ -1,63 +1,63 @@
-<!DOCTYPE html>
-<html lang="en">
-	<head>
+<!DOCTYPE html>
+<html lang="en">
+	<head>
 		<meta charset="utf-8" />
-		<base href="../../" />
-		<script src="list.js"></script>
-		<script src="page.js"></script>
-		<link type="text/css" rel="stylesheet" href="page.css" />
-	</head>
-	<body>
-		[page:Material] &rarr;
-
-		<h1>[name]</h1>
-
-		<div class="desc">A material for drawing wireframe-style geometries.</div>
-
-
-		<h2>Constructor</h2>
-
-
-		<h3>[name]( [page:Object parameters] )</h3>
-
-		<div>parameters is an object with one or more properties defining the material's appearance.</div>
-		<div>
-		color — Line color in hexadecimal. Default is 0xffffff.<br />
-		linewidth — Line thickness. Default is 1.<br />
-		linecap — Define appearance of line ends. Default is 'round'.<br />
-		linejoin — Define appearance of line joints. Default is 'round'.<br />
-		vertexColors — Define how the vertices gets colored. Default is THREE.NoColors.<br />
-		fog — Define whether the material color is affected by global fog settings. Default is false.
-		</div>
-
-		<h2>Properties</h2>
-
-		<h3>[property:Integer color]</h3>
-		<div>Sets the color of the line. Default is 0xffffff.</div>
-
-		<h3>[property:Float linewidth]</h3>
-		<div>Controls line thickness. Default is 1.</div>
-		<div>Due to limitations in the <a href="https://code.google.com/p/angleproject/" target="_blank">ANGLE layer</a>, on Windows platforms linewidth will always be 1 regardless of the set value.</div>
-
-		<h3>[property:String linecap]</h3>
-		<div>Define appearance of line ends. Possible values are "butt", "round" and "square". Default is 'round'.</div>
-		<div>This setting might not have any effect when used with certain renderers. For example, it is ignored with the [page:WebGLRenderer WebGL] renderer, but does work with the [page:CanvasRenderer Canvas] renderer.</div>
-
-		<h3>[property:String linejoin]</h3>
-		<div>Define appearance of line joints. Possible values are "round", "bevel" and "miter". Default is 'round'.</div>
-		<div>This setting might not have any effect when used with certain renderers. For example, it is ignored with the [page:WebGLRenderer WebGL] renderer, but does work with the [page:CanvasRenderer Canvas] renderer.</div>
-
-		<h3>[property:Integer vertexColors]</h3>
-		<div>Define how the vertices gets colored. Possible values are THREE.NoColors, THREE.FaceColors and THREE.VertexColors. Default is THREE.NoColors.</div>
-		<div>This setting might not have any effect when used with certain renderers.</div>
-
-		<h3>[property:Boolean fog]</h3>
-		<div>Define whether the material color is affected by global fog settings.</div>
-		<div>This setting might not have any effect when used with certain renderers. For example, it is ignored with the [page:CanvasRenderer Canvas] renderer, but does work with the [page:WebGLRenderer WebGL] renderer.</div>
-
-		
-		<h2>Source</h2>
-
-		[link:https://github.com/mrdoob/three.js/blob/master/src/[path].js src/[path].js]
-	</body>
-</html>
+		<base href="../../" />
+		<script src="list.js"></script>
+		<script src="page.js"></script>
+		<link type="text/css" rel="stylesheet" href="page.css" />
+	</head>
+	<body>
+		[page:Material] &rarr;
+
+		<h1>[name]</h1>
+
+		<div class="desc">A material for drawing wireframe-style geometries.</div>
+
+
+		<h2>Constructor</h2>
+
+
+		<h3>[name]( [page:Object parameters] )</h3>
+
+		<div>parameters is an object with one or more properties defining the material's appearance.</div>
+		<div>
+		color — Line color in hexadecimal. Default is 0xffffff.<br />
+		linewidth — Line thickness. Default is 1.<br />
+		linecap — Define appearance of line ends. Default is 'round'.<br />
+		linejoin — Define appearance of line joints. Default is 'round'.<br />
+		vertexColors — Define how the vertices gets colored. Default is THREE.NoColors.<br />
+		fog — Define whether the material color is affected by global fog settings. Default is false.
+		</div>
+
+		<h2>Properties</h2>
+
+		<h3>[property:Integer color]</h3>
+		<div>Sets the color of the line. Default is 0xffffff.</div>
+
+		<h3>[property:Float linewidth]</h3>
+		<div>Controls line thickness. Default is 1.</div>
+		<div>Due to limitations in the <a href="https://code.google.com/p/angleproject/" target="_blank">ANGLE layer</a>, with the [page:WebGLRenderer WebGL] renderer on Windows platforms linewidth will always be 1 regardless of the set value.</div>
+
+		<h3>[property:String linecap]</h3>
+		<div>Define appearance of line ends. Possible values are "butt", "round" and "square". Default is 'round'.</div>
+		<div>This setting might not have any effect when used with certain renderers. For example, it is ignored with the [page:WebGLRenderer WebGL] renderer, but does work with the [page:CanvasRenderer Canvas] renderer.</div>
+
+		<h3>[property:String linejoin]</h3>
+		<div>Define appearance of line joints. Possible values are "round", "bevel" and "miter". Default is 'round'.</div>
+		<div>This setting might not have any effect when used with certain renderers. For example, it is ignored with the [page:WebGLRenderer WebGL] renderer, but does work with the [page:CanvasRenderer Canvas] renderer.</div>
+
+		<h3>[property:Integer vertexColors]</h3>
+		<div>Define how the vertices gets colored. Possible values are THREE.NoColors, THREE.FaceColors and THREE.VertexColors. Default is THREE.NoColors.</div>
+		<div>This setting might not have any effect when used with certain renderers.</div>
+
+		<h3>[property:Boolean fog]</h3>
+		<div>Define whether the material color is affected by global fog settings.</div>
+		<div>This setting might not have any effect when used with certain renderers. For example, it is ignored with the [page:CanvasRenderer Canvas] renderer, but does work with the [page:WebGLRenderer WebGL] renderer.</div>
+
+		
+		<h2>Source</h2>
+
+		[link:https://github.com/mrdoob/three.js/blob/master/src/[path].js src/[path].js]
+	</body>
+</html>

+ 19 - 7
docs/api/materials/MeshPhongMaterial.html

@@ -31,6 +31,9 @@
 		emissiveMap — Set emissive map. Default is null.<br />
 		specularMap — Set specular map. Default is null.<br />
 		alphaMap — Set alpha map. Default is null.<br />
+		displacementMap — Set displacement map. Default is null.<br />
+		displacementScale — Set displacement scale. Default is 1.<br />
+		displacementBias — Set displacement offset. Default is 0.<br />
 		envMap — Set env map. Default is null.<br />
 		fog — Define whether the material color is affected by global fog settings. Default is true.<br />
 		shading — Define shading type. Default is THREE.SmoothShading.<br />
@@ -70,13 +73,6 @@
 		<h3>[property:Float shininess]</h3>
 		<div>How shiny the specular highlight is; a higher value gives a sharper highlight. Default is *30*.</div>
 
-		<h3>[property:boolean metal]</h3>
-		<div>
-			If set to true the shader multiplies the specular highlight by the underlying color of the object, making
-			it appear to be more metal-like and darker. If set to false the specular highlight is added ontop of the
-			underlying colors.
-		</div>
-
 		<h3>[property:Texture map]</h3>
 		<div>Set color texture map. Default is null. The texture map color is modulated by the diffuse color.</div>
 
@@ -119,6 +115,22 @@
 		<div>The alpha map is a grayscale texture that controls the opacity across the surface (black: fully transparent; white: fully opaque). Default is null.</div>
 		<div>Only the color of the texture is used, ignoring the alpha channel if one exists. For RGB and RGBA textures, the [page:WebGLRenderer WebGL] renderer will use the green channel when sampling this texture due to the extra bit of precision provided for green in DXT-compressed and uncompressed RGB 565 formats. Luminance-only and luminance/alpha textures will also still work as expected.</div>
 
+		<h3>[property:Texture displacementMap]</h3>
+		<div>
+			The displacement map affects the position of the mesh's vertices. Unlike other maps which only affect the light and shade of the material the displaced vertices can cast shadows, block other objects, and otherwise act as real geometry. 
+			The displacement texture is an image where the value of each pixel (white being the highest) is mapped against, and repositions, the vertices of the mesh.
+		</div>
+		
+		<h3>[property:Float displacementScale]</h3>
+		<div>
+			How much the displacement map affects the mesh (where black is no displacement, and white is maximum displacement). Without a displacement map set, this value is not applied. Default is 1.
+		</div>
+		
+		<h3>[property:Float displacementBias]</h3>
+		<div>
+			The offset of the displacement map's values on the mesh's vertices. Without a displacement map set, this value is not applied. Default is 0.
+		</div>
+		
 		<h3>[property:TextureCube envMap]</h3>
 		<div>Set env map. Default is null.</div>
 

+ 9 - 9
docs/api/math/Box2.html

@@ -22,7 +22,7 @@
 		max -- Upper (x, y) boundary of the box.
 		</div>
 		<div>
-		Creates a box bounded by min and max. 
+		Creates a box bounded by min and max.
 		</div>
 
 
@@ -33,12 +33,12 @@
 		<h3>[property:Vector2 min]</h3>
 		<div>
 		Lower (x, y) boundary of this box.
-		</div> 
+		</div>
 
 		<h3>[property:Vector2 max]</h3>
 		<div>
 		Upper (x, y) boundary of this box.
-		</div> 
+		</div>
 
 		<h2>Methods</h2>
 
@@ -70,7 +70,7 @@
 		Clamps *point* within the bounds of this box.
 		</div>
 
-		<h3>[method:Boolean isIntersectionBox]([page:Box2 box])</h3>
+		<h3>[method:Boolean intersectsBox]([page:Box2 box])</h3>
 		<div>
 		box -- Box to check for intersection against.
 		</div>
@@ -99,7 +99,7 @@
 		box -- Box that will be unioned with this box.
 		</div>
 		<div>
-		Unions this box with *box* setting the upper bound of this box to the greater of the 
+		Unions this box with *box* setting the upper bound of this box to the greater of the
 		two boxes' upper bounds and the lower bound of this box to the lesser of the two boxes'
 		lower bounds.
 		</div>
@@ -108,7 +108,7 @@
 		<div>
 		point -- [page:Vector2]<br/>
 		optionalTarget -- [page:Vector2]<br/>
-		
+
 		</div>
 		<div>
 		Returns a point as a proportion of this box's width and height.
@@ -139,7 +139,7 @@
 		</div>
 		<div>
 		Returns true if this box includes the entirety of *box*. If this and *box* overlap exactly,</br>
-		this function also returns true. 
+		this function also returns true.
 		</div>
 
 		<h3>[method:Box2 translate]([page:Vector2 offset]) [page:Box2 this]</h3>
@@ -177,7 +177,7 @@
 		</div>
 		<div>
 		Expands this box equilaterally by *vector*. The width of this box will be
-		expanded by the x component of *vector* in both directions. The height of 
+		expanded by the x component of *vector* in both directions. The height of
 		this box will be expanded by the y component of *vector* in both directions.
 		</div>
 
@@ -222,7 +222,7 @@
 		<h3>[method:Box2 setFromCenterAndSize]([page:Vector2 center], [page:Vector2 size]) [page:Box2 this]</h3>
 		<div>
 		center -- Desired center position of the box. <br />
-		size -- Desired x and y dimensions of the box. 
+		size -- Desired x and y dimensions of the box.
 		</div>
 		<div>
 		Centers this box on *center* and sets this box's width and height to the values specified

+ 14 - 14
docs/api/math/Box3.html

@@ -22,7 +22,7 @@
 		max -- Upper (x, y, z) boundary of the box.
 		</div>
 		<div>
-		Creates a box bounded by min and max. 
+		Creates a box bounded by min and max.
 		</div>
 
 		<h2>Properties</h2>
@@ -37,7 +37,7 @@
 		<h3>[property:Vector3 max]</h3>
 		<div>
 		Upper (x, y, z) boundary of this box.
-		</div> 
+		</div>
 
 		<h2>Methods</h2>
 
@@ -51,7 +51,7 @@
 		<div>
 		Sets the lower and upper (x, y, z) boundaries of this box.
 		</div>
-		
+
 		<h3>[method:Box3 applyMatrix4]([page:Matrix4 matrix]) [page:Box3 this]</h3>
 		<div>
 		matrix -- The [page:Matrix4] to apply
@@ -69,7 +69,7 @@
 		Clamps *point* within the bounds of this box.
 		</div>
 
-		<h3>[method:Boolean isIntersectionBox]([page:Box3 box])</h3>
+		<h3>[method:Boolean intersectsBox]([page:Box3 box])</h3>
 		<div>
 		box -- Box to check for intersection against.
 		</div>
@@ -84,7 +84,7 @@
 		<div>
 		Sets the upper and lower bounds of this box to include all of the points in *points*.
 		</div>
-		
+
 		<h3>[method:Box3 setFromObject]([page:Object3D object]) [page:Box3 this]</h3>
 		<div>
 		object -- [page:Object3D] to compute the bounding box for.
@@ -93,8 +93,8 @@
 		Computes the world-axis-aligned bounding box of an object (including its children),
 		accounting for both the object's, and childrens', world transforms
 		</div>
-		
-		
+
+
 
 		<h3>[method:Vector3 size]([page:Vector3 optionalTarget])</h3>
 		<div>
@@ -109,7 +109,7 @@
 		box -- Box that will be unioned with this box.
 		</div>
 		<div>
-		Unions this box with *box* setting the upper bound of this box to the greater of the 
+		Unions this box with *box* setting the upper bound of this box to the greater of the
 		two boxes' upper bounds and the lower bound of this box to the lesser of the two boxes'
 		lower bounds.
 		</div>
@@ -139,7 +139,7 @@
 		</div>
 		<div>
 		Returns true if this box includes the entirety of *box*. If this and *box* overlap exactly,</br>
-		this function also returns true. 
+		this function also returns true.
 		</div>
 
 		<h3>[method:Boolean containsPoint]([page:Vector3 point])</h3>
@@ -149,7 +149,7 @@
 		<div>
 		Returns true if the specified point lies within the boundaries of this box.
 		</div>
-		
+
 		<h3>[method:Box3 translate]([page:Vector3 offset]) [page:Box3 this]</h3>
 		<div>
 		offset -- Direction and distance of offset.
@@ -186,7 +186,7 @@
 		<div>
 		Expands the boundaries of this box to include *point*.
 		</div>
-		
+
 		<h3>[method:Box3 expandByScalar]([page:float scalar]) [page:Box3 this]</h3>
 		<div>
 		scalar -- Distance to expand.
@@ -195,14 +195,14 @@
 		Expands each dimension of the box by *scalar*. If negative, the dimensions of the box <br/>
 		will be contracted.
 		</div>
-		
+
 		<h3>[method:Box3 expandByVector]([page:Vector3 vector]) [page:Box3 this]</h3>
 		<div>
 		vector -- Amount to expand this box in each dimension.
 		</div>
 		<div>
 		Expands this box equilaterally by *vector*. The width of this box will be
-		expanded by the x component of *vector* in both directions. The height of 
+		expanded by the x component of *vector* in both directions. The height of
 		this box will be expanded by the y component of *vector* in both directions.
 		The depth of this box will be expanded by the z component of *vector* in
 		both directions.
@@ -249,7 +249,7 @@
 		<h3>[method:Box3 setFromCenterAndSize]([page:Vector3 center], [page:Vector3 size]) [page:Box3 this]</h3>
 		<div>
 		center -- Desired center position of the box. <br />
-		size -- Desired x and y dimensions of the box. 
+		size -- Desired x and y dimensions of the box.
 		</div>
 		<div>
 		Centers this box on *center* and sets this box's width and height to the values specified

+ 4 - 4
docs/api/math/Matrix3.html

@@ -1,7 +1,7 @@
 <!DOCTYPE html>
 <html lang="en">
 	<head>
-		<meta charset="utf-8" />
+		<meta charset="utf-8" />
 		<base href="../../" />
 		<script src="list.js"></script>
 		<script src="page.js"></script>
@@ -92,10 +92,10 @@
 		Sets this matrix as the normal matrix (upper left 3x3)of the passed [page:Matrix4 matrix4]. The normal matrix is the inverse transpose of the matrix *m*.
 		</div>
 
-		<h3>[method:Matrix3 getInverse]([page:Matrix4 m], [page:Boolean throwOnInvertible]) [page:Matrix3 this]</h3>
+		<h3>[method:Matrix3 getInverse]([page:Matrix4 m], [page:Boolean throwOnDegenerate]) [page:Matrix3 this]</h3>
 		<div>
 		m -- [page:Matrix4]<br />
-		throwOnInvertible -- [Page:Boolean] If true, throw an error if the matrix is invertible.
+		throwOnDegenerate -- [Page:Boolean] If true, throw an error if the matrix is degenerate (not invertible).
 		</div>
 		<div>
 		Set this matrix to the inverse of the passed matrix.
@@ -117,7 +117,7 @@
 		<h3>[method:Matrix3 identity]() [page:Matrix3 this]</h3>
 		<div>
 		Resets this matrix to identity.<br/><br/>
-		
+
 		1, 0, 0<br/>
 		0, 1, 0<br/>
 		0, 0, 1<br/>

+ 3 - 3
docs/api/math/Matrix4.html

@@ -1,7 +1,7 @@
 <!DOCTYPE html>
 <html lang="en">
 	<head>
-		<meta charset="utf-8" />
+		<meta charset="utf-8" />
 		<base href="../../" />
 		<script src="list.js"></script>
 		<script src="page.js"></script>
@@ -72,12 +72,12 @@
 		Copies the translation component of the supplied matrix *m* into this matrix translation component.
 		</div>
 
-		<h3>[method:Matrix4 makeBasis]( [page:Vector3 xAxis], [page:Vector3 zAxis], [page:Vector3 zAxis] ) [page:Matrix4 this]</h3>
+		<h3>[method:Matrix4 makeBasis]( [page:Vector3 xAxis], [page:Vector3 yAxis], [page:Vector3 zAxis] ) [page:Matrix4 this]</h3>
 		<div>
 		Creates the basis matrix consisting of the three provided axis vectors.  Returns the current matrix.
 		</div>
 
-		<h3>[method:Matrix4 extractBasis]( [page:Vector3 xAxis], [page:Vector3 zAxis], [page:Vector3 zAxis] ) [page:Matrix4 this]</h3>
+		<h3>[method:Matrix4 extractBasis]( [page:Vector3 xAxis], [page:Vector3 yAxis], [page:Vector3 zAxis] ) [page:Matrix4 this]</h3>
 		<div>
 		Extracts basis of into the three axis vectors provided.  Returns the current matrix.
 		</div>

+ 4 - 4
docs/api/math/Plane.html

@@ -28,7 +28,7 @@
 		<h3>[property:Vector3 normal]</h3>
 
 		<h3>[property:Float constant]</h3>
-		
+
 		<h2>Methods</h2>
 
 
@@ -62,9 +62,9 @@
 		</div>
 		<div>
 		Apply a Matrix4 to the plane. The second parameter is optional.
-		
+
 		<code>
-		var optionalNormalMatrix = new THREE.Matrix3().getNormalMatrix( matrix ) 
+		var optionalNormalMatrix = new THREE.Matrix3().getNormalMatrix( matrix )
 		</code>
 		</div>
 
@@ -77,7 +77,7 @@
 		Returns a vector in the same direction as the Plane's normal, but the magnitude is passed point's original distance to the plane.
 		</div>
 
-		<h3>[method:Boolean isIntersectionLine]([page:Line3 line])</h3>
+		<h3>[method:Boolean intersectsLine]([page:Line3 line])</h3>
 		<div>
 		line -- [page:Line3]
 		</div>

+ 9 - 9
docs/api/math/Ray.html

@@ -30,12 +30,12 @@
 		<h3>[property:Vector3 origin]</h3>
 		<div>
 		The origin of the [page:Ray].
-		</div> 
+		</div>
 
 		<h3>[property:Vector3 direction]</h3>
 		<div>
 		The direction of the [page:Ray]. This must be normalized (with [page:Vector3].normalize) for the methods to operate properly.
-		</div> 
+		</div>
 
 		<h2>Methods</h2>
 
@@ -77,7 +77,7 @@
 		<div>
 		Copy the properties of the provided [page:Ray], then return this [page:Ray].
 		</div>
-		
+
 		<h3>.distanceSqToSegment([page:Vector3 v0], [page:Vector3 v1], [page:Vector3 optionalPointOnRay] = null, [page:Vector3 optionalPointOnSegment] = null) [page:Float]</h3>
 		<div>
 		v0 -- [page:Vector3] The start of the line segment.
@@ -129,7 +129,7 @@
 		<div>
 		Intersect this [page:Ray] with a [page:Box3], returning the intersection point or *null* if there is no intersection.
 		</div>
-		
+
 		<h3>.intersectPlane([page:Plane plane], [page:Vector3 optionalTarget] = null) [page:Vector3]?</h3>
 		<div>
 		plane -- [page:Plane] The [page:Plane] to intersect with.<br />
@@ -139,7 +139,7 @@
 		Intersect this [page:Ray] with a [page:Plane], returning the intersection point or *null* if there is no intersection.
 		</div>
 		function ( a, b, c, backfaceCulling, optionalTarget )
-		
+
 		<h3>.intersectTriangle([page:Vector3 a], [page:Vector3 b], [page:Vector3 c], [page:Boolean backfaceCulling], [page:Vector3 optionalTarget] = null) [page:Vector3]?</h3>
 		<div>
 		a, b, c -- [page:Vector3] The [page:Vector3] points on the triangle.<br />
@@ -149,8 +149,8 @@
 		<div>
 		Intersect this [page:Ray] with a triangle, returning the intersection point or *null* if there is no intersection.
 		</div>
-		
-		<h3>[method:Boolean isIntersectionBox]([page:Box3 box])</h3>
+
+		<h3>[method:Boolean intersectsBox]([page:Box3 box])</h3>
 		<div>
 		box -- [page:Box3] The [page:Box3] to intersect with.
 		</div>
@@ -158,7 +158,7 @@
 		Return whether or not this [page:Ray] intersects with the [page:Box3].
 		</div>
 
-		<h3>[method:Boolean isIntersectionPlane]([page:Plane plane])</h3>
+		<h3>[method:Boolean intersectsPlane]([page:Plane plane])</h3>
 		<div>
 		plane -- [page:Plane] The [page:Plane] to intersect with.
 		</div>
@@ -166,7 +166,7 @@
 		Return whether or not this [page:Ray] intersects with the [page:Plane].
 		</div>
 
-		<h3>[method:Boolean isIntersectionSphere]([page:Sphere sphere])</h3>
+		<h3>[method:Boolean intersectsSphere]([page:Sphere sphere])</h3>
 		<div>
 		sphere -- [page:Sphere] The [page:Sphere] to intersect with.
 		</div>

+ 5 - 0
docs/api/renderers/WebGLRenderer.html

@@ -178,6 +178,8 @@
 
 		<h3>[method:null setScissor]( [page:Integer x], [page:Integer y], [page:Integer width], [page:Integer height] )</h3>
 		<div>Sets the scissor area from (x, y) to (x + width, y + height).</div>
+		
+		<div>NOTE: The point (x, y) is the lower left corner of the area to be set for both of these methods. The area is defined from left to right in width but bottom to top in height. The sense of the vertical definition is opposite to the fill direction of an HTML canvas element.</div>
 
 		<h3>[method:null enableScissorTest]( [page:Boolean enable] )</h3>
 		<div>Enable the scissor test. When this is enabled, only the pixels within the defined scissor area will be affected by further renderer actions.</div>
@@ -225,6 +227,9 @@
 		<div>If forceClear is true, the depth, stencil and color buffers will be cleared before rendering even if the renderer's autoClear property is false.</div>
 		<div>Even with forceClear set to true you can prevent certain buffers being cleared by setting either the .autoClearColor, .autoClearStencil or .autoClearDepth properties to false.</div>
 
+		<h3>[method:null readRenderTargetPixels]( [page:WebGLRenderTarget renderTarget], [page:Float x], [page:Float y], [page:Float width], [page:Float height], buffer )</h3>
+		<div>Reads the pixel data from the renderTarget into the buffer you pass in. Buffer should be a Javascript Uint8Array instantiated with new Uint8Array( renderTargetWidth * renderTargetWidth * 4 ) to account for size and color information. This is a wrapper around gl.readPixels.</div>
+
 		<h3>[method:null renderImmediateObject]( camera, lights, fog, material, object )</h3>
 		<div>Renders an immediate Object using a camera.</div>
 

+ 11 - 13
docs/api/textures/CubeTexture.html

@@ -1,7 +1,7 @@
 <!DOCTYPE html>
 <html lang="en">
 	<head>
-		<meta charset="utf-8" />
+		<meta charset="utf-8" />
 		<base href="../../" />
 		<script src="list.js"></script>
 		<script src="page.js"></script>
@@ -17,15 +17,15 @@
 		<h2>Example</h2>
 
 		<code>
-		var path = "textures/cube/pisa/";
-		var format = '.png';
-		var urls = [
-			path + 'px' + format, path + 'nx' + format,
-			path + 'py' + format, path + 'ny' + format,
-			path + 'pz' + format, path + 'nz' + format
-		];
-
-		var textureCube = THREE.ImageUtils.loadTextureCube( urls );
+		var loader = new THREE.CubeTextureLoader();
+		loader.setPath( 'textures/cube/pisa/' );
+		
+		var textureCube = loader.load( [
+			'px.png', 'nx.png',
+			'py.png', 'ny.png',
+			'pz.png', 'nz.png'
+		] );
+
 		var material = new THREE.MeshBasicMaterial( { color: 0xffffff, envMap: textureCube } );
 		</code>
 
@@ -37,9 +37,7 @@
 		<div>
 		CubeTexture is almost equivalent in functionality and usage to [page:Texture]. The only differences are that the
 		images are an array of 6 images as opposed to a single image, and the mapping options are
-		[page:Textures THREE.CubeReflectionMapping] (default) or [page:Textures THREE.CubeRefractionMapping]<br />
-		<br />
-		Generally [page:ImageUtils.loadTextureCube] is used instead of using CubeTexture directly.
+		[page:Textures THREE.CubeReflectionMapping] (default) or [page:Textures THREE.CubeRefractionMapping]
 		</div>
 
 

+ 2 - 4
docs/list.js

@@ -79,7 +79,6 @@ var list = {
 			[ "PointsMaterial", "api/materials/PointsMaterial" ],
 			[ "RawShaderMaterial", "api/materials/RawShaderMaterial" ],
 			[ "ShaderMaterial", "api/materials/ShaderMaterial" ],
-			[ "SpriteCanvasMaterial", "api/materials/SpriteCanvasMaterial" ],
 			[ "SpriteMaterial", "api/materials/SpriteMaterial" ]
 		],
 
@@ -160,7 +159,6 @@ var list = {
 		"Extras": [
 			[ "FontUtils", "api/extras/FontUtils" ],
 			[ "GeometryUtils", "api/extras/GeometryUtils" ],
-			[ "ImageUtils", "api/extras/ImageUtils" ],
 			[ "SceneUtils", "api/extras/SceneUtils" ]
 		],
 
@@ -195,7 +193,6 @@ var list = {
 		"Extras / Geometries": [
 			[ "BoxGeometry", "api/extras/geometries/BoxGeometry" ],
 			[ "CircleGeometry", "api/extras/geometries/CircleGeometry" ],
-			[ "CubeGeometry", "api/extras/geometries/CubeGeometry" ],
 			[ "CylinderGeometry", "api/extras/geometries/CylinderGeometry" ],
 			[ "DodecahedronGeometry", "api/extras/geometries/DodecahedronGeometry" ],
 			[ "ExtrudeGeometry", "api/extras/geometries/ExtrudeGeometry" ],
@@ -239,7 +236,8 @@ var list = {
 
 		"Examples" : [
 			[ "CombinedCamera", "api/examples/cameras/CombinedCamera" ],
-			[ "LookupTable", "api/examples/Lut" ]
+			[ "LookupTable", "api/examples/Lut" ],
+			[ "SpriteCanvasMaterial", "api/examples/SpriteCanvasMaterial" ]
 
 		]
 

+ 14 - 14
docs/page.js

@@ -7,7 +7,7 @@ var onDocumentLoad = function ( event ) {
 
 	if ( section == 'manual' ) {
 
-		name = name.replace(/\-/g, ' ');
+		name = name.replace( /\-/g, ' ' );
 
 		path = pathname.replace( /\ /g, '-' );
 		path = /\/manual\/[-A-z0-9\/]+/.exec( path ).toString().substr( 8 );
@@ -20,22 +20,22 @@ var onDocumentLoad = function ( event ) {
 
 	var text = document.body.innerHTML;
 
-	text = text.replace(/\[name\]/gi, name);
-	text = text.replace(/\[path\]/gi, path);
-	text = text.replace(/\[page:([\w\.]+)\]/gi, "[page:$1 $1]" ); // [page:name] to [page:name title]
-	text = text.replace(/\[page:\.([\w\.]+) ([\w\.\s]+)\]/gi, "[page:"+name+".$1 $2]" ); // [page:.member title] to [page:name.member title]
-	text = text.replace(/\[page:([\w\.]+) ([\w\.\s]+)\]/gi, "<a href=\"javascript:window.parent.goTo('$1')\" title=\"$1\">$2</a>" ); // [page:name title]
-	// text = text.replace(/\[member:.([\w]+) ([\w\.\s]+)\]/gi, "<a href=\"javascript:window.parent.goTo('" + name + ".$1')\" title=\"$1\">$2</a>" );
+	text = text.replace( /\[name\]/gi, name );
+	text = text.replace( /\[path\]/gi, path );
+	text = text.replace( /\[page:([\w\.]+)\]/gi, "[page:$1 $1]" ); // [page:name] to [page:name title]
+	text = text.replace( /\[page:\.([\w\.]+) ([\w\.\s]+)\]/gi, "[page:" + name + ".$1 $2]" ); // [page:.member title] to [page:name.member title]
+	text = text.replace( /\[page:([\w\.]+) ([\w\.\s]+)\]/gi, "<a href=\"javascript:window.parent.goTo('$1')\" title=\"$1\">$2</a>" ); // [page:name title]
+	// text = text.replace( /\[member:.([\w]+) ([\w\.\s]+)\]/gi, "<a href=\"javascript:window.parent.goTo('" + name + ".$1')\" title=\"$1\">$2</a>" );
 
-	text = text.replace(/\[(?:member|property|method):([\w]+)\]/gi, "[member:$1 $1]" ); // [member:name] to [member:name title]
-	text = text.replace(/\[(?:member|property|method):([\w]+) ([\w\.\s]+)\]/gi, "<a href=\"javascript:window.parent.goTo('"+name+".$2')\" target=\"_parent\" title=\""+name+".$2\" class=\"permalink\">#</a> .<a href=\"javascript:window.parent.goTo('$1')\" title=\"$1\" id=\"$2\">$2</a> " );
+	text = text.replace( /\[(?:member|property|method):([\w]+)\]/gi, "[member:$1 $1]" ); // [member:name] to [member:name title]
+	text = text.replace( /\[(?:member|property|method):([\w]+) ([\w\.\s]+)\]/gi, "<a href=\"javascript:window.parent.goTo('" + name + ".$2')\" target=\"_parent\" title=\"" + name + ".$2\" class=\"permalink\">#</a> .<a href=\"javascript:window.parent.goTo('$1')\" title=\"$1\" id=\"$2\">$2</a> " );
 
-	text = text.replace(/\[link:([\w|\:|\/|\.|\-|\_]+)\]/gi, "[link:$1 $1]" ); // [link:url] to [link:url title]
-	text = text.replace(/\[link:([\w|\:|\/|\.|\-|\_|\(|\)|\#]+) ([\w|\:|\/|\.|\-|\_|\s]+)\]/gi, "<a href=\"$1\"  target=\"_blank\">$2</a>" ); // [link:url title]
-	text = text.replace(/\*([\w|\d|\"|\-|\(][\w|\d|\ |\-|\/|\+|\-|\(|\)|\=|\,|\.\"]*[\w|\d|\"|\)]|\w)\*/gi, "<strong>$1</strong>" ); // *
+	text = text.replace( /\[link:([\w|\:|\/|\.|\-|\_]+)\]/gi, "[link:$1 $1]" ); // [link:url] to [link:url title]
+	text = text.replace( /\[link:([\w|\:|\/|\.|\-|\_|\(|\)|\#]+) ([\w|\:|\/|\.|\-|\_|\s]+)\]/gi, "<a href=\"$1\"  target=\"_blank\">$2</a>" ); // [link:url title]
+	text = text.replace( /\*([\w|\d|\"|\-|\(][\w|\d|\ |\-|\/|\+|\-|\(|\)|\=|\,|\.\"]*[\w|\d|\"|\)]|\w)\*/gi, "<strong>$1</strong>" ); // *
 
-	text = text.replace(/\[example:([\w\_]+)\]/gi, "[example:$1 $1]" ); // [example:name] to [example:name title]
-	text = text.replace(/\[example:([\w\_]+) ([\w\:\/\.\-\_ \s]+)\]/gi, "<a href=\"../../../../examples/#$1\"  target=\"_blank\">$2</a>" ); // [example:name title]
+	text = text.replace( /\[example:([\w\_]+)\]/gi, "[example:$1 $1]" ); // [example:name] to [example:name title]
+	text = text.replace( /\[example:([\w\_]+) ([\w\:\/\.\-\_ \s]+)\]/gi, "<a href=\"../examples/#$1\"  target=\"_blank\">$2</a>" ); // [example:name title]
 
 
 	document.body.innerHTML = text;

+ 34 - 33
docs/scenes/bones-browser.html

@@ -10,7 +10,7 @@
 				font-weight: normal;
 				font-style: normal;
 			}
-			
+
 			body {
 				margin:0;
 				font-family: 'inconsolata';
@@ -18,9 +18,9 @@
 				line-height: 18px;
 				overflow: hidden;
 			}
-			
+
 			canvas { width: 100%; height: 100% }
-			
+
 			#newWindow {
 				display: block;
 				position: absolute;
@@ -31,13 +31,13 @@
 		</style>
 	</head>
 	<body>
-		
+
 		<a id='newWindow' href='./bones-browser.html' target='_blank'>Open in New Window</a>
-		
+
 		<script src="../../build/three.min.js"></script>
 		<script src='../../examples/js/libs/dat.gui.min.js'></script>
 		<script src="../../examples/js/controls/OrbitControls.js"></script>
-		
+
 		<script>
 
 			var gui, scene, camera, renderer, orbit, ambientLight, lights, mesh, bones, skeletonHelper;
@@ -57,6 +57,7 @@
 				camera.position.y = 30;
 
 				renderer = new THREE.WebGLRenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );				
 				renderer.setSize( window.innerWidth, window.innerHeight );
 				document.body.appendChild( renderer.domElement );
 
@@ -155,9 +156,9 @@
 
 				var mesh = new THREE.SkinnedMesh( geometry,	material );
 				var skeleton = new THREE.Skeleton( bones );
-	
+
 				mesh.add( bones[ 0 ] );
-	
+
 				mesh.bind( skeleton );
 
 				skeletonHelper = new THREE.SkeletonHelper( mesh );
@@ -165,27 +166,27 @@
 				scene.add( skeletonHelper );
 
 				return mesh;
-	
+
 			};
 
 			function setupDatGui () {
-	
+
 				var folder = gui.addFolder( "General Options" );
-	
+
 				folder.add( state, "animateBones" );
 				folder.__controllers[ 0 ].name( "Animate Bones" );
 
 				folder.add( mesh, "pose" );
 				folder.__controllers[ 1 ].name( ".pose()" );
-	
+
 				var bones = mesh.skeleton.bones;
-	
+
 				for ( var i = 0; i < bones.length; i ++ ) {
-		
+
 					var bone = bones[ i ];
-		
+
 					folder = gui.addFolder( "Bone " + i );
-	
+
 					folder.add( bone.position, 'x', -10 + bone.position.x, 10 + bone.position.x );
 					folder.add( bone.position, 'y', -10 + bone.position.y, 10 + bone.position.y );
 					folder.add( bone.position, 'z', -10 + bone.position.z, 10 + bone.position.z );
@@ -193,7 +194,7 @@
 					folder.add( bone.rotation, 'x', -Math.PI * 0.5, Math.PI * 0.5 );
 					folder.add( bone.rotation, 'y', -Math.PI * 0.5, Math.PI * 0.5 );
 					folder.add( bone.rotation, 'z', -Math.PI * 0.5, Math.PI * 0.5 );
-		
+
 					folder.add( bone.scale, 'x', 0, 2 );
 					folder.add( bone.scale, 'y', 0, 2 );
 					folder.add( bone.scale, 'z', 0, 2 );
@@ -201,21 +202,21 @@
 					folder.__controllers[ 0 ].name( "position.x" );
 					folder.__controllers[ 1 ].name( "position.y" );
 					folder.__controllers[ 2 ].name( "position.z" );
-		
+
 					folder.__controllers[ 3 ].name( "rotation.x" );
 					folder.__controllers[ 4 ].name( "rotation.y" );
 					folder.__controllers[ 5 ].name( "rotation.z" );
-		
+
 					folder.__controllers[ 6 ].name( "scale.x" );
 					folder.__controllers[ 7 ].name( "scale.y" );
 					folder.__controllers[ 8 ].name( "scale.z" );
-		
+
 				}
-				
+
 			}
 
 			function initBones () {
-	
+
 				var segmentHeight = 8;
 				var segmentCount = 4;
 				var height = segmentHeight * segmentCount;
@@ -238,34 +239,34 @@
 			};
 
 			function render () {
-	
+
 				requestAnimationFrame( render );
 
 				var time = Date.now() * 0.001;
-	
+
 				var bone = mesh;
 
-	
+
 				//Wiggle the bones
 				if ( state.animateBones ) {
-		
+
 					for ( var i = 0; i < mesh.skeleton.bones.length; i ++ ) {
-		
+
 						mesh.skeleton.bones[ i ].rotation.z = Math.sin( time ) * 2 / mesh.skeleton.bones.length;
-		
+
 					}
-		
+
 				}
 
 				skeletonHelper.update();
-	
+
 				renderer.render( scene, camera );
-	
+
 			};
 
 			initScene();
 			render();
-			
+
 		</script>
 	</body>
-</html>
+</html>

+ 2 - 1
docs/scenes/geometry-browser.html

@@ -59,7 +59,8 @@
 			var camera = new THREE.PerspectiveCamera( 75, window.innerWidth/window.innerHeight, 0.1, 50 );
 			camera.position.z = 30;
 
-			var renderer = new THREE.WebGLRenderer({antialias: true});
+			var renderer = new THREE.WebGLRenderer( { antialias: true } );
+			renderer.setPixelRatio( window.devicePixelRatio );			
 			renderer.setSize( window.innerWidth, window.innerHeight );
 			document.body.appendChild( renderer.domElement );
 

+ 28 - 27
docs/scenes/material-browser.html

@@ -10,7 +10,7 @@
 				font-weight: normal;
 				font-style: normal;
 			}
-			
+
 			body {
 				margin:0;
 				font-family: 'inconsolata';
@@ -18,9 +18,9 @@
 				line-height: 18px;
 				overflow: hidden;
 			}
-			
+
 			canvas { width: 100%; height: 100% }
-			
+
 			#newWindow {
 				display: block;
 				position: absolute;
@@ -31,23 +31,24 @@
 		</style>
 	</head>
 	<body>
-		
+
 		<a id='newWindow' href='./material-browser.html' target='_blank'>Open in New Window</a>
-		
+
 		<script src="../../build/three.min.js"></script>
 		<script src='../../examples/js/libs/dat.gui.min.js'></script>
 		<script src='js/material.js'></script>
-		
+
 		<script>
-			
+
 			document.getElementById('newWindow').href += window.location.hash;
-			
+
 			var gui = new dat.GUI();
 			var scene = new THREE.Scene();
 			var camera = new THREE.PerspectiveCamera( 75, window.innerWidth/window.innerHeight, 0.1, 50 );
 			camera.position.z = 30;
-			
-			var renderer = new THREE.WebGLRenderer();
+
+			var renderer = new THREE.WebGLRenderer( { antialias: true } );
+			renderer.setPixelRatio( window.devicePixelRatio );
 			renderer.setSize( window.innerWidth, window.innerHeight );
 			document.body.appendChild( renderer.domElement );
 
@@ -58,7 +59,7 @@
 			lights[0] = new THREE.PointLight( 0xffffff, 1, 0 );
 			lights[1] = new THREE.PointLight( 0xffffff, 1, 0 );
 			lights[2] = new THREE.PointLight( 0xffffff, 1, 0 );
-			
+
 			lights[0].position.set( 0, 200, 0 );
 			lights[1].position.set( 100, 200, 100 );
 			lights[2].position.set( -100, -200, -100 );
@@ -71,54 +72,54 @@
 
 			var geometry = new THREE.TorusKnotGeometry( 10, 3, 100, 16 );
 			var mesh = new THREE.Mesh( geometry );
-			
+
 			generateVertexColors( geometry );
-			
+
 			mesh.material = chooseFromHash( gui, mesh, geometry );
 
 			generateMorphTargets( mesh, geometry );
 
 			scene.add( mesh );
-			
+
 			var prevFog = false;
-			
+
 			var render = function () {
-				
+
 				requestAnimationFrame( render );
 
 				var time = Date.now() * 0.001;
 
 				mesh.rotation.x += 0.005;
 				mesh.rotation.y += 0.005;
-				
+
 				if ( prevFog !== scene.fog ) {
-					
+
 					mesh.material.needsUpdate = true;
 					prevFog = scene.fog;
-					
+
 				}
-				
+
 				if ( mesh.morphTargetInfluences ) {
-					
+
 					mesh.morphTargetInfluences[ 0 ] = ( 1 + Math.sin( 4 * time ) ) / 2;
-					
+
 				}
 
 				renderer.render( scene, camera );
-				
+
 			};
-			
+
 			window.addEventListener( 'resize', function () {
-				
+
 				camera.aspect = window.innerWidth / window.innerHeight;
 				camera.updateProjectionMatrix();
 
 				renderer.setSize( window.innerWidth, window.innerHeight );
-				
+
 			}, false );
 
 			render();
-			
+
 		</script>
 	</body>
 </html>

+ 49 - 28
editor/css/dark.css

@@ -29,9 +29,8 @@
 
 input.Number {
 	color: #2A75B7!important;
-	font-size: 12px;							/** TODO: Use of !imporant is not ideal **/
-	background-color: transparent!important;	/* For now this is a quick fix a rendering issue due to inherited background */
-	border: 1px solid transparent;
+	font-size: 12px;
+	border: 0px;
 	padding: 2px;
 	cursor: col-resize;
 }
@@ -39,20 +38,20 @@ input.Number {
 #viewport {
 	position: absolute;
 	top: 32px;
-	left: 0px;
+	left: 0;
 	right: 300px;
 	bottom: 32px;
 }
 
 	#viewport #info {
-		text-shadow: 1px 1px 0px rgba(0,0,0,0.25);
+		text-shadow: 1px 1px 0 rgba(0,0,0,0.25);
 		pointer-events: none;
 	}
 
 #script {
 	position: absolute;
 	top: 32px;
-	left: 0px;
+	left: 0;
 	right: 300px;
 	bottom: 32px;
 	opacity: 0.9;
@@ -61,7 +60,7 @@ input.Number {
 #player {
 	position: absolute;
 	top: 32px;
-	left: 0px;
+	left: 0;
 	right: 300px;
 	bottom: 32px;
 }
@@ -71,10 +70,10 @@ input.Number {
 	width: 100%;
 	height: 32px;
 	background: #111;
-	padding: 0px;
-	margin: 0px;
-	right: 0px;
-	top: 0px;
+	padding: 0;
+	margin: 0;
+	right: 0;
+	top: 0;
 }
 
 	#menubar .menu {
@@ -86,21 +85,21 @@ input.Number {
 	#menubar .menu.right {
 		float: right;
 		cursor: auto;
-		padding-right: 0px;
+		padding-right: 0;
 		text-align: right;
 	}
 
 		#menubar .menu .title {
 			display: inline-block;
 			color: #888;
-			margin: 0px;
+			margin: 0;
 			padding: 8px;
 		}
 
 		#menubar .menu .options {
 			position: absolute;
 			display: none;
-			padding: 5px 0px;
+			padding: 5px 0;
 			background: #111;
 			width: 150px;
 		}
@@ -110,14 +109,14 @@ input.Number {
 		}
 
 			#menubar .menu .options hr {
-				border-color: #333;
+				border-color: #222;
 			}
 
 			#menubar .menu .options .option {
 				color: #888;
 				background-color: transparent;
 				padding: 5px 10px;
-				margin: 0px !important;
+				margin: 0 !important;
 			}
 
 				#menubar .menu .options .option:hover {
@@ -129,11 +128,18 @@ input.Number {
 					background: transparent;
 				}
 
+		#menubar .menu .options .inactive {
+			color: #444;
+			background-color: transparent;
+			padding: 5px 10px;
+			margin: 0 !important;
+		}
+
 #sidebar {
 	position: absolute;
-	right: 0px;
+	right: 0;
 	top: 32px;
-	bottom: 0px;
+	bottom: 0;
 	width: 300px;
 	background-color: #111;
 	overflow: auto;
@@ -152,30 +158,45 @@ input.Number {
 	}
 
 	#sidebar .Panel {
-		margin-bottom: 10px;
+		color: #888;
+		padding: 10px;
+		border-top: 1px solid #222;
 	}
 
 	#sidebar .Panel.collapsed {
-		margin-bottom: 0px;
+		margin-bottom: 0;
 	}
 
-	#sidebar > .Panel {
-		color: #888;
-		padding: 10px;
-		border-top: 1px solid #333;
+	#sidebar .Panel.Material canvas {
+		border: solid 1px #5A5A5A;
 	}
 
-	#sidebar .Panel.Material canvas {
+	#sidebar .Row {
+		min-height: 20px;
+		margin-bottom: 10px;
+	}
 
-		border: solid 1px #5A5A5A;
+#tabs {
+	background-color: #1b1b1b;
+	border-top: 1px solid #222;
+}
 
+	#tabs span {
+		color: #555;
+		border-right: 1px solid #222;
+		padding: 10px;
+	}
+
+	#tabs span.selected {
+		color: #888;
+		background-color: #111;
 	}
 
 #toolbar {
 	position: absolute;
-	left: 0px;
+	left: 0;
 	right: 300px;
-	bottom: 0px;
+	bottom: 0;
 	height: 32px;
 	background-color: #111;
 	color: #333;

+ 47 - 24
editor/css/light.css

@@ -22,9 +22,8 @@
 
 input.Number {
 	color: #0080f0!important;
-	font-size: 12px;							/** TODO: Use of !imporant is not ideal **/
-	background-color: transparent!important;	/* For now this is a quick fix a rendering issue due to inherited background */
-	border: 1px solid transparent;
+	font-size: 12px;
+	border: 0px;
 	padding: 2px;
 	cursor: col-resize;
 }
@@ -32,20 +31,20 @@ input.Number {
 #viewport {
 	position: absolute;
 	top: 32px;
-	left: 0px;
+	left: 0;
 	right: 300px;
 	bottom: 32px;
 }
 
 	#viewport #info {
-		text-shadow: 1px 1px 0px rgba(0,0,0,0.25);
+		text-shadow: 1px 1px 0 rgba(0,0,0,0.25);
 		pointer-events: none;
 	}
 
 #script {
 	position: absolute;
 	top: 32px;
-	left: 0px;
+	left: 0;
 	right: 300px;
 	bottom: 32px;
 	opacity: 0.9;
@@ -54,7 +53,7 @@ input.Number {
 #player {
 	position: absolute;
 	top: 32px;
-	left: 0px;
+	left: 0;
 	right: 300px;
 	bottom: 32px;
 }
@@ -64,10 +63,10 @@ input.Number {
 	width: 100%;
 	height: 32px;
 	background: #eee;
-	padding: 0px;
-	margin: 0px;
-	right: 0px;
-	top: 0px;
+	padding: 0;
+	margin: 0;
+	right: 0;
+	top: 0;
 }
 
 	#menubar .menu {
@@ -79,21 +78,21 @@ input.Number {
 	#menubar .menu.right {
 		float: right;
 		cursor: auto;
-		padding-right: 0px;
+		padding-right: 0;
 		text-align: right;
 	}
 
 		#menubar .menu .title {
 			display: inline-block;
 			color: #888;
-			margin: 0px;
+			margin: 0;
 			padding: 8px;
 		}
 
 		#menubar .menu .options {
 			position: absolute;
 			display: none;
-			padding: 5px 0px;
+			padding: 5px 0;
 			background: #eee;
 			width: 150px;
 		}
@@ -110,7 +109,7 @@ input.Number {
 				color: #666;
 				background-color: transparent;
 				padding: 5px 10px;
-				margin: 0px !important;
+				margin: 0 !important;
 			}
 
 				#menubar .menu .options .option:hover {
@@ -123,11 +122,18 @@ input.Number {
 					background: transparent;
 				}
 
+		#menubar .menu .options .inactive {
+			color: #bbb;
+			background-color: transparent;
+			padding: 5px 10px;
+			margin: 0 !important;
+		}
+
 #sidebar {
 	position: absolute;
-	right: 0px;
+	right: 0;
 	top: 32px;
-	bottom: 0px;
+	bottom: 0;
 	width: 300px;
 	background: #eee;
 	overflow: auto;
@@ -145,24 +151,41 @@ input.Number {
 	}
 
 	#sidebar .Panel {
-		margin-bottom: 10px;
+		color: #888;
+		padding: 10px;
+		border-top: 1px solid #ccc;
 	}
 
 	#sidebar .Panel.collapsed {
-		margin-bottom: 0px;
+		margin-bottom: 0;
 	}
 
-	#sidebar > .Panel {
-		color: #888;
+	#sidebar .Row {
+		min-height: 20px;
+		margin-bottom: 10px;
+	}
+
+#tabs {
+	background-color: #ddd;
+	border-top: 1px solid #ccc;
+}
+
+	#tabs span {
+		color: #aaa;
+		border-right: 1px solid #ccc;
 		padding: 10px;
-		border-top: 1px solid #ccc;
+	}
+
+	#tabs span.selected {
+		color: #888;
+		background-color: #eee;
 	}
 
 #toolbar {
 	position: absolute;
-	left: 0px;
+	left: 0;
 	right: 300px;
-	bottom: 0px;
+	bottom: 0;
 	height: 32px;
 	background: #eee;
 	color: #333;

+ 29 - 21
editor/css/main.css

@@ -6,7 +6,7 @@ body {
 }
 
 hr {
-	border: 0px;
+	border: 0;
 	border-top: 1px solid #ccc;
 }
 
@@ -42,14 +42,14 @@ textarea, input { outline: none; } /* osx */
 }
 
 	.Panel.Collapsible .Static {
-		margin: 0px;
+		margin: 0;
 	}
 
 	.Panel.Collapsible .Static .Button {
 		float: left;
 		margin-right: 6px;
-		width: 0px;
-		height: 0px;
+		width: 0;
+		height: 0;
 		border: 6px solid transparent;
 	}
 
@@ -88,71 +88,79 @@ textarea, input { outline: none; } /* osx */
 
 		color: #f00;
 		text-align: right;
-		padding: 0px 20px;
+		padding: 0 20px;
 
 	}
 
-/* scene types */
+/* outliner */
 
-.type {
+#outliner .type {
 	position:relative;
 	top:-2px;
-	padding: 0px 2px;
+	padding: 0 2px;
 	color: #ddd;
 }
-.type:after {
+
+#outliner .type:after {
 	content: '■';
 }
 
-.Scene {
+#outliner .Scene {
 	color: #ccccff;
 }
 
-.Object3D {
+#outliner .Object3D {
 	color: #aaaaee;
 }
 
-.Mesh {
+#outliner .Mesh {
 	color: #8888ee;
 }
 
-.Line {
+#outliner .Line {
 	color: #88ee88;
 }
 
-.LineSegments {
+#outliner .LineSegments {
 	color: #88ee88;
 }
 
-.Points {
+#outliner .Points {
 	color: #ee8888;
 }
 
 /* */
 
-.PointLight {
+#outliner .PointLight {
 	color: #dddd00;
 }
 
 /* */
 
-.Geometry {
+#outliner .Geometry {
 	color: #88ff88;
 }
 
-.BoxGeometry {
+#outliner .BoxGeometry {
 	color: #bbeebb;
 }
-.TorusGeometry {
+
+#outliner .TorusGeometry {
 	color: #aaeeaa;
 }
 
 /* */
 
-.Material {
+#outliner .Material {
 	color: #ff8888;
 }
 
-.MeshPhongMaterial {
+#outliner .MeshPhongMaterial {
 	color: #ffaa88;
 }
+
+/* */
+
+#outliner .Script:after {
+	content: '{...}' /* ❮/❯ */
+}

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

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

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

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

+ 50 - 13
editor/index.html

@@ -23,6 +23,7 @@
 		<script src="../examples/js/loaders/KMZLoader.js"></script>
 		<script src="../examples/js/loaders/MD2Loader.js"></script>
 		<script src="../examples/js/loaders/OBJLoader.js"></script>
+		<script src="../examples/js/loaders/PlayCanvasLoader.js"></script>
 		<script src="../examples/js/loaders/PLYLoader.js"></script>
 		<script src="../examples/js/loaders/STLLoader.js"></script>
 		<script src="../examples/js/loaders/UTF8Loader.js"></script>
@@ -94,15 +95,15 @@
 		<script src="js/Menubar.Edit.js"></script>
 		<script src="js/Menubar.Add.js"></script>
 		<script src="js/Menubar.Play.js"></script>
-		<script src="js/Menubar.View.js"></script>
 		<script src="js/Menubar.Examples.js"></script>
 		<script src="js/Menubar.Help.js"></script>
 		<script src="js/Menubar.Status.js"></script>
 		<script src="js/Sidebar.js"></script>
-		<script src="js/Sidebar.Project.js"></script>
 		<script src="js/Sidebar.Scene.js"></script>
-		<script src="js/Sidebar.Object3D.js"></script>
-		<script src="js/Sidebar.Animation.js"></script>
+		<script src="js/Sidebar.Project.js"></script>
+		<script src="js/Sidebar.Settings.js"></script>
+		<script src="js/Sidebar.Properties.js"></script>
+		<script src="js/Sidebar.Object.js"></script>
 		<script src="js/Sidebar.Geometry.js"></script>
 		<script src="js/Sidebar.Geometry.Geometry.js"></script>
 		<script src="js/Sidebar.Geometry.BufferGeometry.js"></script>
@@ -118,10 +119,33 @@
 		<script src="../examples/js/geometries/TeapotBufferGeometry.js"></script>
 		<script src="js/Sidebar.Geometry.TeapotBufferGeometry.js"></script>
 		<script src="js/Sidebar.Material.js"></script>
+		<script src="js/Sidebar.Animation.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/Command.js"></script>
+		<script src="js/commands/AddObjectCommand.js"></script>
+		<script src="js/commands/RemoveObjectCommand.js"></script>
+		<script src="js/commands/MoveObjectCommand.js"></script>
+		<script src="js/commands/SetPositionCommand.js"></script>
+		<script src="js/commands/SetRotationCommand.js"></script>
+		<script src="js/commands/SetScaleCommand.js"></script>
+		<script src="js/commands/SetValueCommand.js"></script>
+		<script src="js/commands/SetUuidCommand.js"></script>
+		<script src="js/commands/SetColorCommand.js"></script>
+		<script src="js/commands/SetGeometryCommand.js"></script>
+		<script src="js/commands/SetGeometryValueCommand.js"></script>
+		<script src="js/commands/MultiCmdsCommand.js"></script>
+		<script src="js/commands/AddScriptCommand.js"></script>
+		<script src="js/commands/RemoveScriptCommand.js"></script>
+		<script src="js/commands/SetScriptValueCommand.js"></script>
+		<script src="js/commands/SetMaterialCommand.js"></script>
+		<script src="js/commands/SetMaterialValueCommand.js"></script>
+		<script src="js/commands/SetMaterialColorCommand.js"></script>
+		<script src="js/commands/SetMaterialMapCommand.js"></script>
+		<script src="js/commands/SetSceneCommand.js"></script>
 
 		<script>
 
@@ -139,12 +163,12 @@
 			var viewport = new Viewport( editor );
 			document.body.appendChild( viewport.dom );
 
-			var player = new Player( editor );
-			document.body.appendChild( player.dom );
-
 			var script = new Script( editor );
 			document.body.appendChild( script.dom );
 
+			var player = new Player( editor );
+			document.body.appendChild( player.dom );
+
 			var toolbar = new Toolbar( editor );
 			document.body.appendChild( toolbar.dom );
 
@@ -220,6 +244,7 @@
 				signals.materialChanged.add( saveState );
 				signals.sceneGraphChanged.add( saveState );
 				signals.scriptChanged.add( saveState );
+				signals.historyChanged.add( saveState );
 
 				signals.showModal.add( function ( content ) {
 
@@ -262,9 +287,21 @@
 						if ( confirm( 'Delete ' + object.name + '?' ) === false ) return;
 
 						var parent = object.parent;
-						editor.removeObject( object );
-						editor.select( parent );
+						if ( parent !== null ) editor.execute( new RemoveObjectCommand( object ) );
+
+						break;
+
+					case 90: // Register Ctrl-Z for Undo, Ctrl-Shift-Z for Redo
+
+						if ( event.ctrlKey && event.shiftKey ) {
+
+							editor.redo();
+
+						} else if ( event.ctrlKey ) {
+
+							editor.undo();
 
+						}
 						break;
 
 				}
@@ -283,13 +320,11 @@
 
 			//
 
-			var file = null;
 			var hash = window.location.hash;
 
-			if ( hash.substr( 1, 4 ) === 'app=' ) file = hash.substr( 5 );
-			if ( hash.substr( 1, 6 ) === 'scene=' ) file = hash.substr( 7 );
+			if ( hash.substr( 1, 5 ) === 'file=' ) {
 
-			if ( file !== null ) {
+				var file = hash.substr( 6 );
 
 				if ( confirm( 'Any unsaved data will be lost. Are you sure?' ) ) {
 
@@ -308,12 +343,14 @@
 
 			}
 
+			/*
 			window.addEventListener( 'message', function ( event ) {
 
 				editor.clear();
 				editor.fromJSON( event.data );
 
 			}, false );
+			*/
 
 		</script>
 	</body>

+ 47 - 0
editor/js/Command.js

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

+ 3 - 6
editor/js/Config.js

@@ -15,12 +15,9 @@ var Config = function () {
 		'project/renderer/shadows': true,
 		'project/vr': false,
 
+		'settings/history': false,
+
 		'ui/sidebar/animation/collapsed': true,
-		'ui/sidebar/geometry/collapsed': true,
-		'ui/sidebar/material/collapsed': true,
-		'ui/sidebar/object3d/collapsed': false,
-		'ui/sidebar/project/collapsed': true,
-		'ui/sidebar/scene/collapsed': false,
 		'ui/sidebar/script/collapsed': true
 	};
 
@@ -68,6 +65,6 @@ var Config = function () {
 
 		}
 
-	}
+	};
 
 };

+ 77 - 21
editor/js/Editor.js

@@ -6,6 +6,11 @@ var Editor = function () {
 
 	var SIGNALS = signals;
 
+	this.DEFAULT_CAMERA = new THREE.PerspectiveCamera( 50, 1, 0.1, 10000 );
+	this.DEFAULT_CAMERA.name = 'Camera';
+	this.DEFAULT_CAMERA.position.set( 20, 10, 20 );
+	this.DEFAULT_CAMERA.lookAt( new THREE.Vector3() );
+
 	this.signals = {
 
 		// script
@@ -62,7 +67,10 @@ var Editor = function () {
 		fogParametersChanged: new SIGNALS.Signal(),
 		windowResize: new SIGNALS.Signal(),
 
-		showGridChanged: new SIGNALS.Signal()
+		showGridChanged: new SIGNALS.Signal(),
+		refreshSidebarObject3D: new SIGNALS.Signal(),
+		historyChanged: new SIGNALS.Signal(),
+		refreshScriptEditor: new SIGNALS.Signal()
 
 	};
 
@@ -71,10 +79,7 @@ var Editor = function () {
 	this.storage = new Storage();
 	this.loader = new Loader( this );
 
-	this.camera = new THREE.PerspectiveCamera( 50, 1, 1, 100000 );
-	this.camera.position.set( 500, 250, 500 );
-	this.camera.lookAt( new THREE.Vector3() );
-	this.camera.name = 'Camera';
+	this.camera = this.DEFAULT_CAMERA.clone();
 
 	this.scene = new THREE.Scene();
 	this.scene.name = 'Scene';
@@ -233,7 +238,7 @@ Editor.prototype = {
 
 	addHelper: function () {
 
-		var geometry = new THREE.SphereBufferGeometry( 20, 4, 2 );
+		var geometry = new THREE.SphereBufferGeometry( 2, 4, 2 );
 		var material = new THREE.MeshBasicMaterial( { color: 0xff0000, visible: false } );
 
 		return function ( object ) {
@@ -242,23 +247,23 @@ Editor.prototype = {
 
 			if ( object instanceof THREE.Camera ) {
 
-				helper = new THREE.CameraHelper( object, 10 );
+				helper = new THREE.CameraHelper( object, 1 );
 
 			} else if ( object instanceof THREE.PointLight ) {
 
-				helper = new THREE.PointLightHelper( object, 10 );
+				helper = new THREE.PointLightHelper( object, 1 );
 
 			} else if ( object instanceof THREE.DirectionalLight ) {
 
-				helper = new THREE.DirectionalLightHelper( object, 20 );
+				helper = new THREE.DirectionalLightHelper( object, 1 );
 
 			} else if ( object instanceof THREE.SpotLight ) {
 
-				helper = new THREE.SpotLightHelper( object, 10 );
+				helper = new THREE.SpotLightHelper( object, 1 );
 
 			} else if ( object instanceof THREE.HemisphereLight ) {
 
-				helper = new THREE.HemisphereLightHelper( object, 10 );
+				helper = new THREE.HemisphereLightHelper( object, 1 );
 
 			} else if ( object instanceof THREE.SkinnedMesh ) {
 
@@ -405,8 +410,7 @@ Editor.prototype = {
 		this.history.clear();
 		this.storage.clear();
 
-		this.camera.position.set( 500, 250, 500 );
-		this.camera.lookAt( new THREE.Vector3() );
+		this.camera.copy( this.DEFAULT_CAMERA );
 
 		var objects = this.scene.children;
 
@@ -444,33 +448,85 @@ Editor.prototype = {
 
 		// TODO: Clean this up somehow
 
+		if ( json.project !== undefined ) {
+
+			this.config.setKey( 'project/renderer/shadows', json.project.shadows );
+			this.config.setKey( 'project/vr', json.project.vr );
+
+		}
+
 		var camera = loader.parse( json.camera );
 
-		this.camera.position.copy( camera.position );
-		this.camera.rotation.copy( camera.rotation );
-		this.camera.aspect = camera.aspect;
-		this.camera.near = camera.near;
-		this.camera.far = camera.far;
+		this.camera.copy( camera );
+		this.camera.aspect = this.DEFAULT_CAMERA.aspect;
+		this.camera.updateProjectionMatrix();
 
-		this.setScene( loader.parse( json.scene ) );
+		this.history.fromJSON( json.history );
 		this.scripts = json.scripts;
 
+		this.setScene( loader.parse( json.scene ) );
+
 	},
 
 	toJSON: function () {
 
+		// scripts clean up
+
+		var scene = this.scene;
+		var scripts = this.scripts;
+
+		for ( var key in scripts ) {
+
+			var script = scripts[ key ];
+
+			if ( script.length === 0 || scene.getObjectByProperty( 'uuid', key ) === undefined ) {
+
+				delete scripts[ key ];
+
+			}
+
+		}
+
+		//
+
 		return {
 
+			metadata: {},
 			project: {
 				shadows: this.config.getKey( 'project/renderer/shadows' ),
 				vr: this.config.getKey( 'project/vr' )
 			},
 			camera: this.camera.toJSON(),
 			scene: this.scene.toJSON(),
-			scripts: this.scripts
+			scripts: this.scripts,
+			history: this.history.toJSON()
 
 		};
 
+	},
+
+	objectByUuid: function ( uuid ) {
+
+		return this.scene.getObjectByProperty( 'uuid', uuid, true );
+
+	},
+
+	execute: function ( cmd, optionalName ) {
+
+		this.history.execute( cmd, optionalName );
+
+	},
+
+	undo: function () {
+
+		this.history.undo();
+
+	},
+
+	redo: function () {
+
+		this.history.redo();
+
 	}
 
-}
+};

+ 277 - 35
editor/js/History.js

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

+ 132 - 119
editor/js/Loader.js

@@ -14,18 +14,25 @@ var Loader = function ( editor ) {
 		var filename = file.name;
 		var extension = filename.split( '.' ).pop().toLowerCase();
 
+		var reader = new FileReader();
+		reader.addEventListener( 'progress', function ( event ) {
+
+			var size = '(' + Math.floor( event.total / 1000 ).format() + ' KB)';
+			var progress = Math.floor( ( event.loaded / event.total ) * 100 ) + '%'
+			console.log( 'Loading', filename, size, progress );
+
+		} );
+
 		switch ( extension ) {
 
 			case 'amf':
 
-				var reader = new FileReader();
 				reader.addEventListener( 'load', function ( event ) {
 
 					var loader = new THREE.AMFLoader();
 					var amfobject = loader.parse( event.target.result );
 
-					editor.addObject( amfobject );
-					editor.select( amfobject );
+					editor.execute( new AddObjectCommand( amfobject ) );
 
 				}, false );
 				reader.readAsArrayBuffer( file );
@@ -34,13 +41,12 @@ var Loader = function ( editor ) {
 
 			case 'awd':
 
-				var reader = new FileReader();
 				reader.addEventListener( 'load', function ( event ) {
 
 					var loader = new THREE.AWDLoader();
 					var scene = loader.parse( event.target.result );
 
-					editor.setScene( scene );
+					editor.execute( new SetSceneCommand( scene ) );
 
 				}, false );
 				reader.readAsArrayBuffer( file );
@@ -49,7 +55,6 @@ var Loader = function ( editor ) {
 
 			case 'babylon':
 
-				var reader = new FileReader();
 				reader.addEventListener( 'load', function ( event ) {
 
 					var contents = event.target.result;
@@ -58,7 +63,7 @@ var Loader = function ( editor ) {
 					var loader = new THREE.BabylonLoader();
 					var scene = loader.parse( json );
 
-					editor.setScene( scene );
+					editor.execute( new SetSceneCommand( scene ) );
 
 				}, false );
 				reader.readAsText( file );
@@ -67,7 +72,6 @@ var Loader = function ( editor ) {
 
 			case 'babylonmeshdata':
 
-				var reader = new FileReader();
 				reader.addEventListener( 'load', function ( event ) {
 
 					var contents = event.target.result;
@@ -76,13 +80,12 @@ var Loader = function ( editor ) {
 					var loader = new THREE.BabylonLoader();
 
 					var geometry = loader.parseGeometry( json );
-					var material = new THREE.MeshPhongMaterial();
+					var material = new THREE.MeshStandardMaterial();
 
 					var mesh = new THREE.Mesh( geometry, material );
 					mesh.name = filename;
 
-					editor.addObject( mesh );
-					editor.select( mesh );
+					editor.execute( new AddObjectCommand( mesh ) );
 
 				}, false );
 				reader.readAsText( file );
@@ -91,7 +94,6 @@ var Loader = function ( editor ) {
 
 			case 'ctm':
 
-				var reader = new FileReader();
 				reader.addEventListener( 'load', function ( event ) {
 
 					var data = new Uint8Array( event.target.result );
@@ -105,13 +107,12 @@ var Loader = function ( editor ) {
 						geometry.sourceType = "ctm";
 						geometry.sourceFile = file.name;
 
-						var material = new THREE.MeshPhongMaterial();
+						var material = new THREE.MeshStandardMaterial();
 
 						var mesh = new THREE.Mesh( geometry, material );
 						mesh.name = filename;
 
-						editor.addObject( mesh );
-						editor.select( mesh );
+						editor.execute( new AddObjectCommand( mesh ) );
 
 					} );
 
@@ -122,7 +123,6 @@ var Loader = function ( editor ) {
 
 			case 'dae':
 
-				var reader = new FileReader();
 				reader.addEventListener( 'load', function ( event ) {
 
 					var contents = event.target.result;
@@ -132,8 +132,7 @@ var Loader = function ( editor ) {
 
 					collada.scene.name = filename;
 
-					editor.addObject( collada.scene );
-					editor.select( collada.scene );
+					editor.execute( new AddObjectCommand( collada.scene ) );
 
 				}, false );
 				reader.readAsText( file );
@@ -148,14 +147,13 @@ var Loader = function ( editor ) {
 			case '3obj':
 			case '3scn':
 
-				var reader = new FileReader();
 				reader.addEventListener( 'load', function ( event ) {
 
 					var contents = event.target.result;
 
 					// 2.0
 
-					if ( contents.indexOf( 'postMessage' ) !== -1 ) {
+					if ( contents.indexOf( 'postMessage' ) !== - 1 ) {
 
 						var blob = new Blob( [ contents ], { type: 'text/javascript' } );
 						var url = URL.createObjectURL( blob );
@@ -198,52 +196,47 @@ var Loader = function ( editor ) {
 				break;
 
 
-				case 'kmz':
+			case 'kmz':
 
-					var reader = new FileReader();
-					reader.addEventListener( 'load', function ( event ) {
+				reader.addEventListener( 'load', function ( event ) {
 
-						var loader = new THREE.KMZLoader();
-						var collada = loader.parse( event.target.result );
+					var loader = new THREE.KMZLoader();
+					var collada = loader.parse( event.target.result );
 
-						collada.scene.name = filename;
+					collada.scene.name = filename;
 
-						editor.addObject( collada.scene );
-						editor.select( collada.scene );
+					editor.execute( new AddObjectCommand( collada.scene ) );
 
-					}, false );
-					reader.readAsArrayBuffer( file );
+				}, false );
+				reader.readAsArrayBuffer( file );
 
-					break;
+				break;
 
-				case 'md2':
+			case 'md2':
 
-					var reader = new FileReader();
-					reader.addEventListener( 'load', function ( event ) {
+				reader.addEventListener( 'load', function ( event ) {
 
-						var contents = event.target.result;
+					var contents = event.target.result;
 
-						var geometry = new THREE.MD2Loader().parse( contents );
-						var material = new THREE.MeshPhongMaterial( {
-							morphTargets: true,
-							morphNormals: true
-						} );
+					var geometry = new THREE.MD2Loader().parse( contents );
+					var material = new THREE.MeshStandardMaterial( {
+						morphTargets: true,
+						morphNormals: true
+					} );
 
-						var mesh = new THREE.Mesh( geometry, material );
-						mesh.mixer = new THREE.AnimationMixer( mesh )
-						mesh.name = filename;
+					var mesh = new THREE.Mesh( geometry, material );
+					mesh.mixer = new THREE.AnimationMixer( mesh );
+					mesh.name = filename;
 
-						editor.addObject( mesh );
-						editor.select( mesh );
+					editor.execute( new AddObjectCommand( mesh ) );
 
-					}, false );
-					reader.readAsArrayBuffer( file );
+				}, false );
+				reader.readAsArrayBuffer( file );
 
-					break;
+				break;
 
 			case 'obj':
 
-				var reader = new FileReader();
 				reader.addEventListener( 'load', function ( event ) {
 
 					var contents = event.target.result;
@@ -251,8 +244,24 @@ var Loader = function ( editor ) {
 					var object = new THREE.OBJLoader().parse( contents );
 					object.name = filename;
 
-					editor.addObject( object );
-					editor.select( object );
+					editor.execute( new AddObjectCommand( object ) );
+
+				}, false );
+				reader.readAsText( file );
+
+				break;
+
+			case 'playcanvas':
+
+				reader.addEventListener( 'load', function ( event ) {
+
+					var contents = event.target.result;
+					var json = JSON.parse( contents );
+
+					var loader = new THREE.PlayCanvasLoader();
+					var object = loader.parse( json );
+
+					editor.execute( new AddObjectCommand( object ) );
 
 				}, false );
 				reader.readAsText( file );
@@ -261,7 +270,6 @@ var Loader = function ( editor ) {
 
 			case 'ply':
 
-				var reader = new FileReader();
 				reader.addEventListener( 'load', function ( event ) {
 
 					var contents = event.target.result;
@@ -270,13 +278,12 @@ var Loader = function ( editor ) {
 					geometry.sourceType = "ply";
 					geometry.sourceFile = file.name;
 
-					var material = new THREE.MeshPhongMaterial();
+					var material = new THREE.MeshStandardMaterial();
 
 					var mesh = new THREE.Mesh( geometry, material );
 					mesh.name = filename;
 
-					editor.addObject( mesh );
-					editor.select( mesh );
+					editor.execute( new AddObjectCommand( mesh ) );
 
 				}, false );
 				reader.readAsText( file );
@@ -285,7 +292,6 @@ var Loader = function ( editor ) {
 
 			case 'stl':
 
-				var reader = new FileReader();
 				reader.addEventListener( 'load', function ( event ) {
 
 					var contents = event.target.result;
@@ -294,13 +300,12 @@ var Loader = function ( editor ) {
 					geometry.sourceType = "stl";
 					geometry.sourceFile = file.name;
 
-					var material = new THREE.MeshPhongMaterial();
+					var material = new THREE.MeshStandardMaterial();
 
 					var mesh = new THREE.Mesh( geometry, material );
 					mesh.name = filename;
 
-					editor.addObject( mesh );
-					editor.select( mesh );
+					editor.execute( new AddObjectCommand( mesh ) );
 
 				}, false );
 
@@ -319,7 +324,6 @@ var Loader = function ( editor ) {
 			/*
 			case 'utf8':
 
-				var reader = new FileReader();
 				reader.addEventListener( 'load', function ( event ) {
 
 					var contents = event.target.result;
@@ -329,8 +333,7 @@ var Loader = function ( editor ) {
 
 					var mesh = new THREE.Mesh( geometry, material );
 
-					editor.addObject( mesh );
-					editor.select( mesh );
+					editor.execute( new AddObjectCommand( mesh ) );
 
 				}, false );
 				reader.readAsBinaryString( file );
@@ -340,7 +343,6 @@ var Loader = function ( editor ) {
 
 			case 'vtk':
 
-				var reader = new FileReader();
 				reader.addEventListener( 'load', function ( event ) {
 
 					var contents = event.target.result;
@@ -349,13 +351,12 @@ var Loader = function ( editor ) {
 					geometry.sourceType = "vtk";
 					geometry.sourceFile = file.name;
 
-					var material = new THREE.MeshPhongMaterial();
+					var material = new THREE.MeshStandardMaterial();
 
 					var mesh = new THREE.Mesh( geometry, material );
 					mesh.name = filename;
 
-					editor.addObject( mesh );
-					editor.select( mesh );
+					editor.execute( new AddObjectCommand( mesh ) );
 
 				}, false );
 				reader.readAsText( file );
@@ -364,14 +365,13 @@ var Loader = function ( editor ) {
 
 			case 'wrl':
 
-				var reader = new FileReader();
 				reader.addEventListener( 'load', function ( event ) {
 
 					var contents = event.target.result;
 
 					var result = new THREE.VRMLLoader().parse( contents );
 
-					editor.setScene( result );
+					editor.execute( new SetSceneCommand( result ) );
 
 				}, false );
 				reader.readAsText( file );
@@ -386,9 +386,9 @@ var Loader = function ( editor ) {
 
 		}
 
-	}
+	};
 
-	var handleJSON = function ( data, file, filename ) {
+	function handleJSON( data, file, filename ) {
 
 		if ( data.metadata === undefined ) { // 2.0
 
@@ -402,101 +402,114 @@ var Loader = function ( editor ) {
 
 		}
 
-		if ( data.metadata.version === undefined ) {
+		if ( data.metadata.formatVersion !== undefined ) {
 
 			data.metadata.version = data.metadata.formatVersion;
 
 		}
 
-		if ( data.metadata.type === 'BufferGeometry' ) {
+		switch ( data.metadata.type.toLowerCase() ) {
 
-			var loader = new THREE.BufferGeometryLoader();
-			var result = loader.parse( data );
+			case 'buffergeometry':
 
-			var mesh = new THREE.Mesh( result );
+				var loader = new THREE.BufferGeometryLoader();
+				var result = loader.parse( data );
 
-			editor.addObject( mesh );
-			editor.select( mesh );
+				var mesh = new THREE.Mesh( result );
 
-		} else if ( data.metadata.type.toLowerCase() === 'geometry' ) {
+				editor.execute( new AddObjectCommand( mesh ) );
 
-			var loader = new THREE.JSONLoader();
-			loader.setTexturePath( scope.texturePath );
+				break;
+
+			case 'geometry':
+
+				var loader = new THREE.JSONLoader();
+				loader.setTexturePath( scope.texturePath );
+
+				var result = loader.parse( data );
 
-			var result = loader.parse( data );
+				var geometry = result.geometry;
+				var material;
 
-			var geometry = result.geometry;
-			var material;
+				if ( result.materials !== undefined ) {
 
-			if ( result.materials !== undefined ) {
+					if ( result.materials.length > 1 ) {
 
-				if ( result.materials.length > 1 ) {
+						material = new THREE.MeshFaceMaterial( result.materials );
 
-					material = new THREE.MeshFaceMaterial( result.materials );
+					} else {
+
+						material = result.materials[ 0 ];
+
+					}
 
 				} else {
 
-					material = result.materials[ 0 ];
+					material = new THREE.MeshStandardMaterial();
 
 				}
 
-			} else {
+				geometry.sourceType = "ascii";
+				geometry.sourceFile = file.name;
 
-				material = new THREE.MeshPhongMaterial();
+				var mesh;
 
-			}
+				if ( geometry.animation && geometry.animation.hierarchy ) {
 
-			geometry.sourceType = "ascii";
-			geometry.sourceFile = file.name;
+					mesh = new THREE.SkinnedMesh( geometry, material );
 
-			var mesh;
+				} else {
 
-			if ( geometry.animation && geometry.animation.hierarchy ) {
+					mesh = new THREE.Mesh( geometry, material );
 
-				mesh = new THREE.SkinnedMesh( geometry, material );
+				}
 
-			} else {
+				mesh.name = filename;
 
-				mesh = new THREE.Mesh( geometry, material );
+				editor.execute( new AddObjectCommand( mesh ) );
 
-			}
+				break;
 
-			mesh.name = filename;
+			case 'object':
 
-			editor.addObject( mesh );
-			editor.select( mesh );
+				var loader = new THREE.ObjectLoader();
+				loader.setTexturePath( scope.texturePath );
 
-		} else if ( data.metadata.type.toLowerCase() === 'object' ) {
+				var result = loader.parse( data );
 
-			var loader = new THREE.ObjectLoader();
-			loader.setTexturePath( scope.texturePath );
+				if ( result instanceof THREE.Scene ) {
 
-			var result = loader.parse( data );
+					editor.execute( new SetSceneCommand( result ) );
 
-			if ( result instanceof THREE.Scene ) {
+				} else {
 
-				editor.setScene( result );
+					editor.execute( new AddObjectCommand( result ) );
 
-			} else {
+				}
 
-				editor.addObject( result );
-				editor.select( result );
+				break;
 
-			}
+			case 'scene':
 
-		} else if ( data.metadata.type.toLowerCase() === 'scene' ) {
+				// DEPRECATED
 
-			// DEPRECATED
+				var loader = new THREE.SceneLoader();
+				loader.parse( data, function ( result ) {
 
-			var loader = new THREE.SceneLoader();
-			loader.parse( data, function ( result ) {
+					editor.execute( new SetSceneCommand( result.scene ) );
 
-				editor.setScene( result.scene );
+				}, '' );
 
-			}, '' );
+				break;
+
+			case 'app':
+
+				editor.fromJSON( data );
+
+				break;
 
 		}
 
-	};
+	}
 
-}
+};

+ 60 - 91
editor/js/Menubar.Add.js

@@ -32,7 +32,7 @@ Menubar.Add = function ( editor ) {
 
 	// Group
 
-	var option = new UI.Panel();
+	var option = new UI.Row();
 	option.setClass( 'option' );
 	option.setTextContent( 'Group' );
 	option.onClick( function () {
@@ -40,8 +40,7 @@ Menubar.Add = function ( editor ) {
 		var mesh = new THREE.Group();
 		mesh.name = 'Group ' + ( ++ meshCount );
 
-		editor.addObject( mesh );
-		editor.select( mesh );
+		editor.execute( new AddObjectCommand( mesh ) );
 
 	} );
 	options.add( option );
@@ -52,105 +51,87 @@ Menubar.Add = function ( editor ) {
 
 	// Plane
 
-	var option = new UI.Panel();
+	var option = new UI.Row();
 	option.setClass( 'option' );
 	option.setTextContent( 'Plane' );
 	option.onClick( function () {
 
-		var width = 200;
-		var height = 200;
-
-		var widthSegments = 1;
-		var heightSegments = 1;
-
-		var geometry = new THREE.PlaneGeometry( width, height, widthSegments, heightSegments );
-		var material = new THREE.MeshPhongMaterial();
+		var geometry = new THREE.PlaneGeometry( 2, 2 );
+		var material = new THREE.MeshStandardMaterial();
 		var mesh = new THREE.Mesh( geometry, material );
 		mesh.name = 'Plane ' + ( ++ meshCount );
 
-		editor.addObject( mesh );
-		editor.select( mesh );
+		editor.execute( new AddObjectCommand( mesh ) );
 
 	} );
 	options.add( option );
 
 	// Box
 
-	var option = new UI.Panel();
+	var option = new UI.Row();
 	option.setClass( 'option' );
 	option.setTextContent( 'Box' );
 	option.onClick( function () {
 
-		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() );
+		var geometry = new THREE.BoxGeometry( 1, 1, 1 );
+		var mesh = new THREE.Mesh( geometry, new THREE.MeshStandardMaterial() );
 		mesh.name = 'Box ' + ( ++ meshCount );
 
-		editor.addObject( mesh );
-		editor.select( mesh );
+		editor.execute( new AddObjectCommand( mesh ) );
 
 	} );
 	options.add( option );
 
 	// Circle
 
-	var option = new UI.Panel();
+	var option = new UI.Row();
 	option.setClass( 'option' );
 	option.setTextContent( 'Circle' );
 	option.onClick( function () {
 
-		var radius = 20;
+		var radius = 1;
 		var segments = 32;
 
 		var geometry = new THREE.CircleGeometry( radius, segments );
-		var mesh = new THREE.Mesh( geometry, new THREE.MeshPhongMaterial() );
+		var mesh = new THREE.Mesh( geometry, new THREE.MeshStandardMaterial() );
 		mesh.name = 'Circle ' + ( ++ meshCount );
 
-		editor.addObject( mesh );
-		editor.select( mesh );
+		editor.execute( new AddObjectCommand( mesh ) );
 
 	} );
 	options.add( option );
 
 	// Cylinder
 
-	var option = new UI.Panel();
+	var option = new UI.Row();
 	option.setClass( 'option' );
 	option.setTextContent( 'Cylinder' );
 	option.onClick( function () {
 
-		var radiusTop = 20;
-		var radiusBottom = 20;
-		var height = 100;
+		var radiusTop = 1;
+		var radiusBottom = 1;
+		var height = 2;
 		var radiusSegments = 32;
 		var heightSegments = 1;
 		var openEnded = false;
 
 		var geometry = new THREE.CylinderGeometry( radiusTop, radiusBottom, height, radiusSegments, heightSegments, openEnded );
-		var mesh = new THREE.Mesh( geometry, new THREE.MeshPhongMaterial() );
+		var mesh = new THREE.Mesh( geometry, new THREE.MeshStandardMaterial() );
 		mesh.name = 'Cylinder ' + ( ++ meshCount );
 
-		editor.addObject( mesh );
-		editor.select( mesh );
+		editor.execute( new AddObjectCommand( mesh ) );
 
 	} );
 	options.add( option );
 
 	// Sphere
 
-	var option = new UI.Panel();
+	var option = new UI.Row();
 	option.setClass( 'option' );
 	option.setTextContent( 'Sphere' );
 	option.onClick( function () {
 
-		var radius = 75;
+		var radius = 1;
 		var widthSegments = 32;
 		var heightSegments = 16;
 		var phiStart = 0;
@@ -159,79 +140,75 @@ Menubar.Add = function ( editor ) {
 		var thetaLength = Math.PI;
 
 		var geometry = new THREE.SphereGeometry( radius, widthSegments, heightSegments, phiStart, phiLength, thetaStart, thetaLength );
-		var mesh = new THREE.Mesh( geometry, new THREE.MeshPhongMaterial() );
+		var mesh = new THREE.Mesh( geometry, new THREE.MeshStandardMaterial() );
 		mesh.name = 'Sphere ' + ( ++ meshCount );
 
-		editor.addObject( mesh );
-		editor.select( mesh );
+		editor.execute( new AddObjectCommand( mesh ) );
 
 	} );
 	options.add( option );
 
 	// Icosahedron
 
-	var option = new UI.Panel();
+	var option = new UI.Row();
 	option.setClass( 'option' );
 	option.setTextContent( 'Icosahedron' );
 	option.onClick( function () {
 
-		var radius = 75;
+		var radius = 1;
 		var detail = 2;
 
 		var geometry = new THREE.IcosahedronGeometry( radius, detail );
-		var mesh = new THREE.Mesh( geometry, new THREE.MeshPhongMaterial() );
+		var mesh = new THREE.Mesh( geometry, new THREE.MeshStandardMaterial() );
 		mesh.name = 'Icosahedron ' + ( ++ meshCount );
 
-		editor.addObject( mesh );
-		editor.select( mesh );
+		editor.execute( new AddObjectCommand( mesh ) );
 
 	} );
 	options.add( option );
 
 	// Torus
 
-	var option = new UI.Panel();
+	var option = new UI.Row();
 	option.setClass( 'option' );
 	option.setTextContent( 'Torus' );
 	option.onClick( function () {
 
-		var radius = 100;
-		var tube = 40;
-		var radialSegments = 8;
-		var tubularSegments = 6;
+		var radius = 2;
+		var tube = 1;
+		var radialSegments = 32;
+		var tubularSegments = 12;
 		var arc = Math.PI * 2;
 
 		var geometry = new THREE.TorusGeometry( radius, tube, radialSegments, tubularSegments, arc );
-		var mesh = new THREE.Mesh( geometry, new THREE.MeshPhongMaterial() );
+		var mesh = new THREE.Mesh( geometry, new THREE.MeshStandardMaterial() );
 		mesh.name = 'Torus ' + ( ++ meshCount );
 
-		editor.addObject( mesh );
-		editor.select( mesh );
+		editor.execute( new AddObjectCommand( mesh ) );
 
 	} );
 	options.add( option );
 
 	// TorusKnot
 
-	var option = new UI.Panel();
+	var option = new UI.Row();
 	option.setClass( 'option' );
 	option.setTextContent( 'TorusKnot' );
 	option.onClick( function () {
 
-		var radius = 100;
-		var tube = 40;
+		var radius = 2;
+		var tube = 0.8;
 		var radialSegments = 64;
-		var tubularSegments = 8;
+		var tubularSegments = 12;
 		var p = 2;
 		var q = 3;
 		var heightScale = 1;
 
 		var geometry = new THREE.TorusKnotGeometry( radius, tube, radialSegments, tubularSegments, p, q, heightScale );
-		var mesh = new THREE.Mesh( geometry, new THREE.MeshPhongMaterial() );
+		var mesh = new THREE.Mesh( geometry, new THREE.MeshStandardMaterial() );
 		mesh.name = 'TorusKnot ' + ( ++ meshCount );
 
-		editor.addObject( mesh );
-		editor.select( mesh );
+		editor.execute( new AddObjectCommand( mesh ) );
 
 	} );
 	options.add( option );
@@ -239,7 +216,7 @@ Menubar.Add = function ( editor ) {
 	/*
 	// Teapot
 
-	var option = new UI.Panel();
+	var option = new UI.Row();
 	option.setClass( 'option' );
 	option.setTextContent( 'Teapot' );
 	option.onClick( function () {
@@ -252,8 +229,7 @@ Menubar.Add = function ( editor ) {
 		var fitLid = false;
 		var blinnScale = true;
 
-		var material = new THREE.MeshPhongMaterial();
-		material.side = 2;
+		var material = new THREE.MeshStandardMaterial();
 
 		var geometry = new THREE.TeapotBufferGeometry( size, segments, bottom, lid, body, fitLid, blinnScale );
 		var mesh = new THREE.Mesh( geometry, material );
@@ -268,7 +244,7 @@ Menubar.Add = function ( editor ) {
 
 	// Sprite
 
-	var option = new UI.Panel();
+	var option = new UI.Row();
 	option.setClass( 'option' );
 	option.setTextContent( 'Sprite' );
 	option.onClick( function () {
@@ -276,8 +252,7 @@ Menubar.Add = function ( editor ) {
 		var sprite = new THREE.Sprite( new THREE.SpriteMaterial() );
 		sprite.name = 'Sprite ' + ( ++ meshCount );
 
-		editor.addObject( sprite );
-		editor.select( sprite );
+		editor.execute( new AddObjectCommand( sprite ) );
 
 	} );
 	options.add( option );
@@ -288,7 +263,7 @@ Menubar.Add = function ( editor ) {
 
 	// PointLight
 
-	var option = new UI.Panel();
+	var option = new UI.Row();
 	option.setClass( 'option' );
 	option.setTextContent( 'PointLight' );
 	option.onClick( function () {
@@ -300,15 +275,14 @@ Menubar.Add = function ( editor ) {
 		var light = new THREE.PointLight( color, intensity, distance );
 		light.name = 'PointLight ' + ( ++ lightCount );
 
-		editor.addObject( light );
-		editor.select( light );
+		editor.execute( new AddObjectCommand( light ) );
 
 	} );
 	options.add( option );
 
 	// SpotLight
 
-	var option = new UI.Panel();
+	var option = new UI.Row();
 	option.setClass( 'option' );
 	option.setTextContent( 'SpotLight' );
 	option.onClick( function () {
@@ -323,17 +297,16 @@ Menubar.Add = function ( editor ) {
 		light.name = 'SpotLight ' + ( ++ lightCount );
 		light.target.name = 'SpotLight ' + ( lightCount ) + ' Target';
 
-		light.position.set( 0.5, 1, 0.75 ).multiplyScalar( 200 );
+		light.position.set( 5, 10, 7.5 );
 
-		editor.addObject( light );
-		editor.select( light );
+		editor.execute( new AddObjectCommand( light ) );
 
 	} );
 	options.add( option );
 
 	// DirectionalLight
 
-	var option = new UI.Panel();
+	var option = new UI.Row();
 	option.setClass( 'option' );
 	option.setTextContent( 'DirectionalLight' );
 	option.onClick( function () {
@@ -345,17 +318,16 @@ Menubar.Add = function ( editor ) {
 		light.name = 'DirectionalLight ' + ( ++ lightCount );
 		light.target.name = 'DirectionalLight ' + ( lightCount ) + ' Target';
 
-		light.position.set( 0.5, 1, 0.75 ).multiplyScalar( 200 );
+		light.position.set( 5, 10, 7.5 );
 
-		editor.addObject( light );
-		editor.select( light );
+		editor.execute( new AddObjectCommand( light ) );
 
 	} );
 	options.add( option );
 
 	// HemisphereLight
 
-	var option = new UI.Panel();
+	var option = new UI.Row();
 	option.setClass( 'option' );
 	option.setTextContent( 'HemisphereLight' );
 	option.onClick( function () {
@@ -367,17 +339,16 @@ Menubar.Add = function ( editor ) {
 		var light = new THREE.HemisphereLight( skyColor, groundColor, intensity );
 		light.name = 'HemisphereLight ' + ( ++ lightCount );
 
-		light.position.set( 0.5, 1, 0.75 ).multiplyScalar( 200 );
+		light.position.set( 0, 10, 0 );
 
-		editor.addObject( light );
-		editor.select( light );
+		editor.execute( new AddObjectCommand( light ) );
 
 	} );
 	options.add( option );
 
 	// AmbientLight
 
-	var option = new UI.Panel();
+	var option = new UI.Row();
 	option.setClass( 'option' );
 	option.setTextContent( 'AmbientLight' );
 	option.onClick( function() {
@@ -387,8 +358,7 @@ Menubar.Add = function ( editor ) {
 		var light = new THREE.AmbientLight( color );
 		light.name = 'AmbientLight ' + ( ++ lightCount );
 
-		editor.addObject( light );
-		editor.select( light );
+		editor.execute( new AddObjectCommand( light ) );
 
 	} );
 	options.add( option );
@@ -399,7 +369,7 @@ Menubar.Add = function ( editor ) {
 
 	// PerspectiveCamera
 
-	var option = new UI.Panel();
+	var option = new UI.Row();
 	option.setClass( 'option' );
 	option.setTextContent( 'PerspectiveCamera' );
 	option.onClick( function() {
@@ -407,8 +377,7 @@ Menubar.Add = function ( editor ) {
 		var camera = new THREE.PerspectiveCamera( 50, 1, 1, 10000 );
 		camera.name = 'PerspectiveCamera ' + ( ++ cameraCount );
 
-		editor.addObject( camera );
-		editor.select( camera );
+		editor.execute( new AddObjectCommand( camera ) );
 
 	} );
 	options.add( option );

+ 63 - 18
editor/js/Menubar.Edit.js

@@ -18,35 +18,73 @@ Menubar.Edit = function ( editor ) {
 
 	// Undo
 
-	var option = new UI.Panel();
-	option.setClass( 'option' );
-	option.setTextContent( 'Undo' );
-	option.onClick( function () {
+	var undo = new UI.Row();
+	undo.setClass( 'option' );
+	undo.setTextContent( 'Undo (Ctrl+Z)' );
+	undo.onClick( function () {
 
-		editor.history.undo();
+		editor.undo();
 
 	} );
-	options.add( option );
+	options.add( undo );
 
 	// Redo
 
-	var option = new UI.Panel();
+	var redo = new UI.Row();
+	redo.setClass( 'option' );
+	redo.setTextContent( 'Redo (Ctrl+Shift+Z)' );
+	redo.onClick( function () {
+
+		editor.redo();
+
+	} );
+	options.add( redo );
+
+	// Clear History
+
+	var option = new UI.Row();
 	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() );
 
 	// Clone
 
-	var option = new UI.Panel();
+	var option = new UI.Row();
 	option.setClass( 'option' );
 	option.setTextContent( 'Clone' );
 	option.onClick( function () {
@@ -57,15 +95,14 @@ Menubar.Edit = function ( editor ) {
 
 		object = object.clone();
 
-		editor.addObject( object );
-		editor.select( object );
+		editor.execute( new AddObjectCommand( object ) );
 
 	} );
 	options.add( option );
 
 	// Delete
 
-	var option = new UI.Panel();
+	var option = new UI.Row();
 	option.setClass( 'option' );
 	option.setTextContent( 'Delete' );
 	option.onClick( function () {
@@ -75,15 +112,16 @@ Menubar.Edit = function ( editor ) {
 		if ( confirm( 'Delete ' + object.name + '?' ) === false ) return;
 
 		var parent = object.parent;
-		editor.removeObject( object );
-		editor.select( parent );
+		if ( parent === undefined ) return; // avoid deleting the camera or scene
+
+		editor.execute( new RemoveObjectCommand( object ) );
 
 	} );
 	options.add( option );
 
 	// Minify shaders
 
-	var option = new UI.Panel();
+	var option = new UI.Row();
 	option.setClass( 'option' );
 	option.setTextContent( 'Minify Shaders' );
 	option.onClick( function() {
@@ -108,6 +146,7 @@ Menubar.Edit = function ( editor ) {
 
 		}
 
+		var cmds = [];
 		root.traverse( function ( object ) {
 
 			var material = object.material;
@@ -119,8 +158,8 @@ Menubar.Edit = function ( editor ) {
 					var shader = glslprep.minifyGlsl( [
 							material.vertexShader, material.fragmentShader ] );
 
-					material.vertexShader = shader[ 0 ];
-					material.fragmentShader = shader[ 1 ];
+					cmds.push( new SetMaterialValueCommand( object, 'vertexShader', shader[ 0 ] ) );
+					cmds.push( new SetMaterialValueCommand( object, 'fragmentShader', shader[ 1 ] ) );
 
 					++nMaterialsChanged;
 
@@ -148,6 +187,12 @@ Menubar.Edit = function ( editor ) {
 
 		} );
 
+		if ( nMaterialsChanged > 0 ) {
+
+			editor.execute( new MultiCmdsCommand( cmds ), 'Minify Shaders' );
+
+		}
+
 		window.alert( nMaterialsChanged +
 				" material(s) were changed.\n" + errors.join( "\n" ) );
 

+ 1 - 1
editor/js/Menubar.Examples.js

@@ -33,7 +33,7 @@ Menubar.Examples = function ( editor ) {
 
 			var item = items[ i ];
 
-			var option = new UI.Panel();
+			var option = new UI.Row();
 			option.setClass( 'option' );
 			option.setTextContent( item.title );
 			option.onClick( function () {

+ 45 - 71
editor/js/Menubar.File.js

@@ -18,7 +18,7 @@ Menubar.File = function ( editor ) {
 
 	// New
 
-	var option = new UI.Panel();
+	var option = new UI.Row();
 	option.setClass( 'option' );
 	option.setTextContent( 'New' );
 	option.onClick( function () {
@@ -46,7 +46,7 @@ Menubar.File = function ( editor ) {
 
 	} );
 
-	var option = new UI.Panel();
+	var option = new UI.Row();
 	option.setClass( 'option' );
 	option.setTextContent( 'Import' );
 	option.onClick( function () {
@@ -62,7 +62,7 @@ Menubar.File = function ( editor ) {
 
 	// Export Geometry
 
-	var option = new UI.Panel();
+	var option = new UI.Row();
 	option.setClass( 'option' );
 	option.setTextContent( 'Export Geometry' );
 	option.onClick( function () {
@@ -88,20 +88,24 @@ Menubar.File = function ( editor ) {
 		var output = geometry.toJSON();
 
 		try {
+
 			output = JSON.stringify( output, null, '\t' );
 			output = output.replace( /[\n\t]+([\d\.e\-\[\]]+)/g, '$1' );
+
 		} catch ( e ) {
+
 			output = JSON.stringify( output );
+
 		}
 
-		exportString( output, 'geometry.json' );
+		saveString( output, 'geometry.json' );
 
 	} );
 	options.add( option );
 
 	// Export Object
 
-	var option = new UI.Panel();
+	var option = new UI.Row();
 	option.setClass( 'option' );
 	option.setTextContent( 'Export Object' );
 	option.onClick( function () {
@@ -118,20 +122,24 @@ Menubar.File = function ( editor ) {
 		var output = object.toJSON();
 
 		try {
+
 			output = JSON.stringify( output, null, '\t' );
 			output = output.replace( /[\n\t]+([\d\.e\-\[\]]+)/g, '$1' );
+
 		} catch ( e ) {
+
 			output = JSON.stringify( output );
+
 		}
 
-		exportString( output, 'model.json' );
+		saveString( output, 'model.json' );
 
 	} );
 	options.add( option );
 
 	// Export Scene
 
-	var option = new UI.Panel();
+	var option = new UI.Row();
 	option.setClass( 'option' );
 	option.setTextContent( 'Export Scene' );
 	option.onClick( function () {
@@ -139,20 +147,24 @@ Menubar.File = function ( editor ) {
 		var output = editor.scene.toJSON();
 
 		try {
+
 			output = JSON.stringify( output, null, '\t' );
 			output = output.replace( /[\n\t]+([\d\.e\-\[\]]+)/g, '$1' );
+
 		} catch ( e ) {
+
 			output = JSON.stringify( output );
+
 		}
 
-		exportString( output, 'scene.json' );
+		saveString( output, 'scene.json' );
 
 	} );
 	options.add( option );
 
 	// Export OBJ
 
-	var option = new UI.Panel();
+	var option = new UI.Row();
 	option.setClass( 'option' );
 	option.setTextContent( 'Export OBJ' );
 	option.onClick( function () {
@@ -168,21 +180,21 @@ Menubar.File = function ( editor ) {
 
 		var exporter = new THREE.OBJExporter();
 
-		exportString( exporter.parse( object ), 'model.obj' );
+		saveString( exporter.parse( object ), 'model.obj' );
 
 	} );
 	options.add( option );
 
 	// Export STL
 
-	var option = new UI.Panel();
+	var option = new UI.Row();
 	option.setClass( 'option' );
 	option.setTextContent( 'Export STL' );
 	option.onClick( function () {
 
 		var exporter = new THREE.STLExporter();
 
-		exportString( exporter.parse( editor.scene ), 'model.stl' );
+		saveString( exporter.parse( editor.scene ), 'model.stl' );
 
 	} );
 	options.add( option );
@@ -193,60 +205,19 @@ Menubar.File = function ( editor ) {
 
 	// Publish
 
-	var option = new UI.Panel();
+	var option = new UI.Row();
 	option.setClass( 'option' );
 	option.setTextContent( 'Publish' );
 	option.onClick( function () {
 
-		var camera = editor.camera;
-
 		var zip = new JSZip();
 
-		zip.file( 'index.html', [
-
-			'<!DOCTYPE html>',
-			'<html lang="en">',
-			'	<head>',
-			'		<title>three.js</title>',
-			'		<meta charset="utf-8">',
-			'		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">',
-			'		<style>',
-			'		body {',
-			'			margin: 0px;',
-			'			overflow: hidden;',
-			'		}',
-			'		</style>',
-			'	</head>',
-			'	<body ontouchstart="">',
-			'		<script src="js/three.min.js"></script>',
-			'		<script src="js/app.js"></script>',
-			'		<script>',
-			'',
-			'			var loader = new THREE.XHRLoader();',
-			'			loader.load( \'app.json\', function ( text ) {',
-			'',
-			'				var player = new APP.Player();',
-			'				player.load( JSON.parse( text ) );',
-			'				player.setSize( window.innerWidth, window.innerHeight );',
-			'				player.play();',
-			'',
-			'				document.body.appendChild( player.dom );',
-			'',
-			'				window.addEventListener( \'resize\', function () {',
-			'					player.setSize( window.innerWidth, window.innerHeight );',
-			'				} );',
-			'',
-			'			} );',
-			'',
-			'		</script>',
-			'	</body>',
-			'</html>'
-
-		].join( '\n' ) );
-
 		//
 
 		var output = editor.toJSON();
+		output.metadata.type = 'App';
+		delete output.history;
+
 		output = JSON.stringify( output, null, '\t' );
 		output = output.replace( /[\n\t]+([\d\.e\-\[\]]+)/g, '$1' );
 
@@ -256,11 +227,16 @@ Menubar.File = function ( editor ) {
 
 		var manager = new THREE.LoadingManager( function () {
 
-			location.href = 'data:application/zip;base64,' + zip.generate();
+			save( zip.generate( { type: 'blob' } ), 'download.zip' );
 
 		} );
 
 		var loader = new THREE.XHRLoader( manager );
+		loader.load( 'js/libs/app/index.html', function ( content ) {
+
+			zip.file( 'index.html', content );
+
+		} );
 		loader.load( 'js/libs/app.js', function ( content ) {
 
 			zip.file( 'js/app.js', content );
@@ -282,23 +258,21 @@ Menubar.File = function ( editor ) {
 	link.style.display = 'none';
 	document.body.appendChild( link ); // Firefox workaround, see #6594
 
-	var exportString = function ( output, filename ) {
-
-		var blob = new Blob( [ output ], { type: 'text/plain' } );
-		var objectURL = URL.createObjectURL( blob );
+	function save( blob, filename ) {
 
-		link.href = objectURL;
+		link.href = URL.createObjectURL( blob );
 		link.download = filename || 'data.json';
-		link.target = '_blank';
+		link.click();
+
+		// URL.revokeObjectURL( url ); breaks Firefox...
+
+	}
+
+	function saveString( text, filename ) {
 
-		var event = document.createEvent("MouseEvents");
-		event.initMouseEvent(
-			"click", true, false, window, 0, 0, 0, 0, 0
-			, false, false, false, false, 0, null
-		);
-		link.dispatchEvent(event);
+		save( new Blob( [ text ], { type: 'text/plain' } ), filename );
 
-	};
+	}
 
 	return container;
 

+ 2 - 2
editor/js/Menubar.Help.js

@@ -18,7 +18,7 @@ Menubar.Help = function ( editor ) {
 
 	// Source code
 
-	var option = new UI.Panel();
+	var option = new UI.Row();
 	option.setClass( 'option' );
 	option.setTextContent( 'Source code' );
 	option.onClick( function () {
@@ -30,7 +30,7 @@ Menubar.Help = function ( editor ) {
 
 	// About
 
-	var option = new UI.Panel();
+	var option = new UI.Row();
 	option.setClass( 'option' );
 	option.setTextContent( 'About' );
 	option.onClick( function () {

+ 6 - 9
editor/js/Menubar.Status.js

@@ -7,8 +7,9 @@ Menubar.Status = function ( editor ) {
 	var container = new UI.Panel();
 	container.setClass( 'menu right' );
 
-	var checkbox = new UI.Checkbox( editor.config.getKey( 'autosave' ) );
-	checkbox.onChange( function () {
+	var autosave = new UI.THREE.Boolean( editor.config.getKey( 'autosave' ), 'autosave' );
+	autosave.text.setColor( '#888' );
+	autosave.onChange( function () {
 
 		var value = this.getValue();
 
@@ -21,21 +22,17 @@ Menubar.Status = function ( editor ) {
 		}
 
 	} );
-	container.add( checkbox );
-
-	var text = new UI.Text( 'autosave' );
-	text.setClass( 'title' );
-	container.add( text );
+	container.add( autosave );
 
 	editor.signals.savingStarted.add( function () {
 
-		text.setTextDecoration( 'underline' );
+		autosave.text.setTextDecoration( 'underline' );
 
 	} );
 
 	editor.signals.savingFinished.add( function () {
 
-		text.setTextDecoration( 'none' );
+		autosave.text.setTextDecoration( 'none' );
 
 	} );
 

+ 0 - 81
editor/js/Menubar.View.js

@@ -1,81 +0,0 @@
-/**
- * @author mrdoob / http://mrdoob.com/
- */
-
-Menubar.View = function ( editor ) {
-
-	var container = new UI.Panel();
-	container.setClass( 'menu' );
-
-	var title = new UI.Panel();
-	title.setClass( 'title' );
-	title.setTextContent( 'View' );
-	container.add( title );
-
-	var options = new UI.Panel();
-	options.setClass( 'options' );
-	container.add( options );
-
-	// Light theme
-
-	var option = new UI.Panel();
-	option.setClass( 'option' );
-	option.setTextContent( 'Light theme' );
-	option.onClick( function () {
-
-		editor.setTheme( 'css/light.css' );
-		editor.config.setKey( 'theme', 'css/light.css' );
-
-	} );
-	options.add( option );
-
-	// Dark theme
-
-	var option = new UI.Panel();
-	option.setClass( 'option' );
-	option.setTextContent( 'Dark theme' );
-	option.onClick( function () {
-
-		editor.setTheme( 'css/dark.css' );
-		editor.config.setKey( 'theme', 'css/dark.css' );
-
-	} );
-	options.add( option );
-
-	//
-
-	options.add( new UI.HorizontalRule() );
-
-	// fullscreen
-
-	var option = new UI.Panel();
-	option.setClass( 'option' );
-	option.setTextContent( 'Fullscreen' );
-	option.onClick( function () {
-
-		var element = document.body;
-
-		if ( element.requestFullscreen ) {
-
-			element.requestFullscreen();
-
-		} else if ( element.mozRequestFullScreen ) {
-
-			element.mozRequestFullScreen();
-
-		} else if ( element.webkitRequestFullscreen ) {
-
-			element.webkitRequestFullscreen();
-
-		} else if ( element.msRequestFullscreen ) {
-
-			element.msRequestFullscreen();
-
-		}
-
-	} );
-	options.add( option );
-
-	return container;
-
-};

+ 0 - 1
editor/js/Menubar.js

@@ -12,7 +12,6 @@ var Menubar = function ( editor ) {
 	container.add( new Menubar.Add( editor ) );
 	container.add( new Menubar.Play( editor ) );
 	container.add( new Menubar.Examples( editor ) );
-	container.add( new Menubar.View( editor ) );
 	container.add( new Menubar.Help( editor ) );
 
 	container.add( new Menubar.Status( editor ) );

+ 66 - 17
editor/js/Script.js

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

+ 1 - 1
editor/js/Sidebar.Animation.js

@@ -21,7 +21,7 @@ Sidebar.Animation = function ( editor ) {
 	container.addStatic( new UI.Text( 'Animation' ).setTextTransform( 'uppercase' ) );
 	container.add( new UI.Break() );
 
-	var animationsRow = new UI.Panel();
+	var animationsRow = new UI.Row();
 	container.add( animationsRow );
 
 	/*

+ 12 - 16
editor/js/Sidebar.Geometry.BoxGeometry.js

@@ -2,15 +2,17 @@
  * @author mrdoob / http://mrdoob.com/
  */
 
-Sidebar.Geometry.BoxGeometry = function ( signals, object ) {
+Sidebar.Geometry.BoxGeometry = function ( editor, object ) {
 
-	var container = new UI.Panel();
+	var signals = editor.signals;
+
+	var container = new UI.Row();
 
 	var parameters = object.geometry.parameters;
 
 	// width
 
-	var widthRow = new UI.Panel();
+	var widthRow = new UI.Row();
 	var width = new UI.Number( parameters.width ).onChange( update );
 
 	widthRow.add( new UI.Text( 'Width' ).setWidth( '90px' ) );
@@ -20,7 +22,7 @@ Sidebar.Geometry.BoxGeometry = function ( signals, object ) {
 
 	// height
 
-	var heightRow = new UI.Panel();
+	var heightRow = new UI.Row();
 	var height = new UI.Number( parameters.height ).onChange( update );
 
 	heightRow.add( new UI.Text( 'Height' ).setWidth( '90px' ) );
@@ -30,7 +32,7 @@ Sidebar.Geometry.BoxGeometry = function ( signals, object ) {
 
 	// depth
 
-	var depthRow = new UI.Panel();
+	var depthRow = new UI.Row();
 	var depth = new UI.Number( parameters.depth ).onChange( update );
 
 	depthRow.add( new UI.Text( 'Depth' ).setWidth( '90px' ) );
@@ -40,7 +42,7 @@ Sidebar.Geometry.BoxGeometry = function ( signals, object ) {
 
 	// widthSegments
 
-	var widthSegmentsRow = new UI.Panel();
+	var widthSegmentsRow = new UI.Row();
 	var widthSegments = new UI.Integer( parameters.widthSegments ).setRange( 1, Infinity ).onChange( update );
 
 	widthSegmentsRow.add( new UI.Text( 'Width segments' ).setWidth( '90px' ) );
@@ -50,7 +52,7 @@ Sidebar.Geometry.BoxGeometry = function ( signals, object ) {
 
 	// heightSegments
 
-	var heightSegmentsRow = new UI.Panel();
+	var heightSegmentsRow = new UI.Row();
 	var heightSegments = new UI.Integer( parameters.heightSegments ).setRange( 1, Infinity ).onChange( update );
 
 	heightSegmentsRow.add( new UI.Text( 'Height segments' ).setWidth( '90px' ) );
@@ -60,7 +62,7 @@ Sidebar.Geometry.BoxGeometry = function ( signals, object ) {
 
 	// depthSegments
 
-	var depthSegmentsRow = new UI.Panel();
+	var depthSegmentsRow = new UI.Row();
 	var depthSegments = new UI.Integer( parameters.depthSegments ).setRange( 1, Infinity ).onChange( update );
 
 	depthSegmentsRow.add( new UI.Text( 'Height segments' ).setWidth( '90px' ) );
@@ -72,20 +74,14 @@ Sidebar.Geometry.BoxGeometry = function ( signals, object ) {
 
 	function update() {
 
-		object.geometry.dispose();
-
-		object.geometry = new THREE.BoxGeometry(
+		editor.execute( new SetGeometryCommand( object, new THREE.BoxGeometry(
 			width.getValue(),
 			height.getValue(),
 			depth.getValue(),
 			widthSegments.getValue(),
 			heightSegments.getValue(),
 			depthSegments.getValue()
-		);
-
-		object.geometry.computeBoundingSphere();
-
-		signals.geometryChanged.dispatch( object );
+		) ) );
 
 	}
 

+ 11 - 7
editor/js/Sidebar.Geometry.BufferGeometry.js

@@ -2,9 +2,11 @@
  * @author mrdoob / http://mrdoob.com/
  */
 
-Sidebar.Geometry.BufferGeometry = function ( signals ) {
+Sidebar.Geometry.BufferGeometry = function ( editor ) {
 
-	var container = new UI.Panel();
+	var signals = editor.signals;
+
+	var container = new UI.Row();
 
 	function update( object ) {
 
@@ -21,7 +23,7 @@ Sidebar.Geometry.BufferGeometry = function ( signals ) {
 
 			if ( index !== null ) {
 
-				var panel = new UI.Panel();
+				var panel = new UI.Row();
 				panel.add( new UI.Text( 'index' ).setWidth( '90px' ) );
 				panel.add( new UI.Text( ( index.count ).format() ).setFontSize( '12px' ) );
 				container.add( panel );
@@ -32,9 +34,11 @@ Sidebar.Geometry.BufferGeometry = function ( signals ) {
 
 			for ( var name in attributes ) {
 
-				var panel = new UI.Panel();
+				var attribute = attributes[ name ];
+
+				var panel = new UI.Row();
 				panel.add( new UI.Text( name ).setWidth( '90px' ) );
-				panel.add( new UI.Text( ( attributes[ name ].count ).format() ).setFontSize( '12px' ) );
+				panel.add( new UI.Text( ( attribute.count ).format() + ' (' + attribute.itemSize + ')' ).setFontSize( '12px' ) );
 				container.add( panel );
 
 			}
@@ -45,11 +49,11 @@ Sidebar.Geometry.BufferGeometry = function ( signals ) {
 
 		}
 
-	};
+	}
 
 	signals.objectSelected.add( update );
 	signals.geometryChanged.add( update );
 
 	return container;
 
-}
+};

+ 10 - 12
editor/js/Sidebar.Geometry.CircleGeometry.js

@@ -2,15 +2,17 @@
  * @author mrdoob / http://mrdoob.com/
  */
 
-Sidebar.Geometry.CircleGeometry = function ( signals, object ) {
+Sidebar.Geometry.CircleGeometry = function ( editor, object ) {
 
-	var container = new UI.Panel();
+	var signals = editor.signals;
+
+	var container = new UI.Row();
 
 	var parameters = object.geometry.parameters;
 
 	// radius
 
-	var radiusRow = new UI.Panel();
+	var radiusRow = new UI.Row();
 	var radius = new UI.Number( parameters.radius ).onChange( update );
 
 	radiusRow.add( new UI.Text( 'Radius' ).setWidth( '90px' ) );
@@ -20,7 +22,7 @@ Sidebar.Geometry.CircleGeometry = function ( signals, object ) {
 
 	// segments
 
-	var segmentsRow = new UI.Panel();
+	var segmentsRow = new UI.Row();
 	var segments = new UI.Integer( parameters.segments ).setRange( 3, Infinity ).onChange( update );
 
 	segmentsRow.add( new UI.Text( 'Segments' ).setWidth( '90px' ) );
@@ -30,7 +32,7 @@ Sidebar.Geometry.CircleGeometry = function ( signals, object ) {
 
 	// thetaStart
 
-	var thetaStartRow = new UI.Panel();
+	var thetaStartRow = new UI.Row();
 	var thetaStart = new UI.Number( parameters.thetaStart ).onChange( update );
 
 	thetaStartRow.add( new UI.Text( 'Theta start' ).setWidth( '90px' ) );
@@ -40,7 +42,7 @@ Sidebar.Geometry.CircleGeometry = function ( signals, object ) {
 
 	// thetaLength
 
-	var thetaLengthRow = new UI.Panel();
+	var thetaLengthRow = new UI.Row();
 	var thetaLength = new UI.Number( parameters.thetaLength ).onChange( update );
 
 	thetaLengthRow.add( new UI.Text( 'Theta length' ).setWidth( '90px' ) );
@@ -52,16 +54,12 @@ Sidebar.Geometry.CircleGeometry = function ( signals, object ) {
 
 	function update() {
 
-		object.geometry.dispose();
-
-		object.geometry = new THREE.CircleGeometry(
+		editor.execute( new SetGeometryCommand( object, new THREE.CircleGeometry(
 			radius.getValue(),
 			segments.getValue(),
 			thetaStart.getValue(),
 			thetaLength.getValue()
-		);
-
-		signals.geometryChanged.dispatch( object );
+		) ) );
 
 	}
 

+ 12 - 16
editor/js/Sidebar.Geometry.CylinderGeometry.js

@@ -2,15 +2,17 @@
  * @author mrdoob / http://mrdoob.com/
  */
 
-Sidebar.Geometry.CylinderGeometry = function ( signals, object ) {
+Sidebar.Geometry.CylinderGeometry = function ( editor, object ) {
 
-	var container = new UI.Panel();
+	var signals = editor.signals;
+
+	var container = new UI.Row();
 
 	var parameters = object.geometry.parameters;
 
 	// radiusTop
 
-	var radiusTopRow = new UI.Panel();
+	var radiusTopRow = new UI.Row();
 	var radiusTop = new UI.Number( parameters.radiusTop ).onChange( update );
 
 	radiusTopRow.add( new UI.Text( 'Radius top' ).setWidth( '90px' ) );
@@ -20,7 +22,7 @@ Sidebar.Geometry.CylinderGeometry = function ( signals, object ) {
 
 	// radiusBottom
 
-	var radiusBottomRow = new UI.Panel();
+	var radiusBottomRow = new UI.Row();
 	var radiusBottom = new UI.Number( parameters.radiusBottom ).onChange( update );
 
 	radiusBottomRow.add( new UI.Text( 'Radius bottom' ).setWidth( '90px' ) );
@@ -30,7 +32,7 @@ Sidebar.Geometry.CylinderGeometry = function ( signals, object ) {
 
 	// height
 
-	var heightRow = new UI.Panel();
+	var heightRow = new UI.Row();
 	var height = new UI.Number( parameters.height ).onChange( update );
 
 	heightRow.add( new UI.Text( 'Height' ).setWidth( '90px' ) );
@@ -40,7 +42,7 @@ Sidebar.Geometry.CylinderGeometry = function ( signals, object ) {
 
 	// radialSegments
 
-	var radialSegmentsRow = new UI.Panel();
+	var radialSegmentsRow = new UI.Row();
 	var radialSegments = new UI.Integer( parameters.radialSegments ).setRange( 1, Infinity ).onChange( update );
 
 	radialSegmentsRow.add( new UI.Text( 'Radial segments' ).setWidth( '90px' ) );
@@ -50,7 +52,7 @@ Sidebar.Geometry.CylinderGeometry = function ( signals, object ) {
 
 	// heightSegments
 
-	var heightSegmentsRow = new UI.Panel();
+	var heightSegmentsRow = new UI.Row();
 	var heightSegments = new UI.Integer( parameters.heightSegments ).setRange( 1, Infinity ).onChange( update );
 
 	heightSegmentsRow.add( new UI.Text( 'Height segments' ).setWidth( '90px' ) );
@@ -60,7 +62,7 @@ Sidebar.Geometry.CylinderGeometry = function ( signals, object ) {
 
 	// openEnded
 
-	var openEndedRow = new UI.Panel();
+	var openEndedRow = new UI.Row();
 	var openEnded = new UI.Checkbox( parameters.openEnded ).onChange( update );
 
 	openEndedRow.add( new UI.Text( 'Open ended' ).setWidth( '90px' ) );
@@ -72,20 +74,14 @@ Sidebar.Geometry.CylinderGeometry = function ( signals, object ) {
 
 	function update() {
 
-		object.geometry.dispose();
-
-		object.geometry = new THREE.CylinderGeometry(
+		editor.execute( new SetGeometryCommand( object, new THREE.CylinderGeometry(
 			radiusTop.getValue(),
 			radiusBottom.getValue(),
 			height.getValue(),
 			radialSegments.getValue(),
 			heightSegments.getValue(),
 			openEnded.getValue()
-		);
-
-		object.geometry.computeBoundingSphere();
-
-		signals.geometryChanged.dispatch( object );
+		) ) );
 
 	}
 

+ 9 - 7
editor/js/Sidebar.Geometry.Geometry.js

@@ -2,14 +2,16 @@
  * @author mrdoob / http://mrdoob.com/
  */
 
-Sidebar.Geometry.Geometry = function ( signals ) {
+Sidebar.Geometry.Geometry = function ( editor ) {
 
-	var container = new UI.Panel();
+	var signals = editor.signals;
+
+	var container = new UI.Row();
 
 	// vertices
 
-	var verticesRow = new UI.Panel();
-	var vertices = new UI.Text().setFontSize( '12px' );
+	var verticesRow = new UI.Row();
+	var vertices = new UI.Text();
 
 	verticesRow.add( new UI.Text( 'Vertices' ).setWidth( '90px' ) );
 	verticesRow.add( vertices );
@@ -18,8 +20,8 @@ Sidebar.Geometry.Geometry = function ( signals ) {
 
 	// faces
 
-	var facesRow = new UI.Panel();
-	var faces = new UI.Text().setFontSize( '12px' );
+	var facesRow = new UI.Row();
+	var faces = new UI.Text();
 
 	facesRow.add( new UI.Text( 'Faces' ).setWidth( '90px' ) );
 	facesRow.add( faces );
@@ -34,7 +36,7 @@ Sidebar.Geometry.Geometry = function ( signals ) {
 
 		var geometry = object.geometry;
 
-		if ( geometry instanceof THREE.Geometry ) { 
+		if ( geometry instanceof THREE.Geometry ) {
 
 			container.setDisplay( 'block' );
 

+ 8 - 10
editor/js/Sidebar.Geometry.IcosahedronGeometry.js

@@ -2,15 +2,17 @@
  * @author mrdoob / http://mrdoob.com/
  */
 
-Sidebar.Geometry.IcosahedronGeometry = function ( signals, object ) {
+Sidebar.Geometry.IcosahedronGeometry = function ( editor, object ) {
 
-	var container = new UI.Panel();
+	var signals = editor.signals;
+
+	var container = new UI.Row();
 
 	var parameters = object.geometry.parameters;
 
 	// radius
 
-	var radiusRow = new UI.Panel();
+	var radiusRow = new UI.Row();
 	var radius = new UI.Number( parameters.radius ).onChange( update );
 
 	radiusRow.add( new UI.Text( 'Radius' ).setWidth( '90px' ) );
@@ -20,7 +22,7 @@ Sidebar.Geometry.IcosahedronGeometry = function ( signals, object ) {
 
 	// detail
 
-	var detailRow = new UI.Panel();
+	var detailRow = new UI.Row();
 	var detail = new UI.Integer( parameters.detail ).setRange( 0, Infinity ).onChange( update );
 
 	detailRow.add( new UI.Text( 'Detail' ).setWidth( '90px' ) );
@@ -33,14 +35,10 @@ Sidebar.Geometry.IcosahedronGeometry = function ( signals, object ) {
 
 	function update() {
 
-		object.geometry.dispose();
-
-		object.geometry = new THREE.IcosahedronGeometry(
+		editor.execute( new SetGeometryCommand( object, new THREE.IcosahedronGeometry(
 			radius.getValue(),
 			detail.getValue()
-		);
-
-		object.geometry.computeBoundingSphere();
+		) ) );
 
 		signals.objectChanged.dispatch( object );
 

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

@@ -2,9 +2,11 @@
  * @author mrdoob / http://mrdoob.com/
  */
 
-Sidebar.Geometry.Modifiers = function ( signals, object ) {
+Sidebar.Geometry.Modifiers = function ( editor, object ) {
 
-	var container = new UI.Panel().setPaddingLeft( '90px' );
+	var signals = editor.signals;
+
+	var container = new UI.Row().setPaddingLeft( '90px' );
 
 	var geometry = object.geometry;
 
@@ -35,4 +37,4 @@ Sidebar.Geometry.Modifiers = function ( signals, object ) {
 
 	return container;
 
-}
+};

+ 10 - 14
editor/js/Sidebar.Geometry.PlaneGeometry.js

@@ -2,15 +2,17 @@
  * @author mrdoob / http://mrdoob.com/
  */
 
-Sidebar.Geometry.PlaneGeometry = function ( signals, object ) {
+Sidebar.Geometry.PlaneGeometry = function ( editor, object ) {
 
-	var container = new UI.Panel();
+	var signals = editor.signals;
+
+	var container = new UI.Row();
 
 	var parameters = object.geometry.parameters;
 
 	// width
 
-	var widthRow = new UI.Panel();
+	var widthRow = new UI.Row();
 	var width = new UI.Number( parameters.width ).onChange( update );
 
 	widthRow.add( new UI.Text( 'Width' ).setWidth( '90px' ) );
@@ -20,7 +22,7 @@ Sidebar.Geometry.PlaneGeometry = function ( signals, object ) {
 
 	// height
 
-	var heightRow = new UI.Panel();
+	var heightRow = new UI.Row();
 	var height = new UI.Number( parameters.height ).onChange( update );
 
 	heightRow.add( new UI.Text( 'Height' ).setWidth( '90px' ) );
@@ -30,7 +32,7 @@ Sidebar.Geometry.PlaneGeometry = function ( signals, object ) {
 
 	// widthSegments
 
-	var widthSegmentsRow = new UI.Panel();
+	var widthSegmentsRow = new UI.Row();
 	var widthSegments = new UI.Integer( parameters.widthSegments ).setRange( 1, Infinity ).onChange( update );
 
 	widthSegmentsRow.add( new UI.Text( 'Width segments' ).setWidth( '90px' ) );
@@ -40,7 +42,7 @@ Sidebar.Geometry.PlaneGeometry = function ( signals, object ) {
 
 	// heightSegments
 
-	var heightSegmentsRow = new UI.Panel();
+	var heightSegmentsRow = new UI.Row();
 	var heightSegments = new UI.Integer( parameters.heightSegments ).setRange( 1, Infinity ).onChange( update );
 
 	heightSegmentsRow.add( new UI.Text( 'Height segments' ).setWidth( '90px' ) );
@@ -52,19 +54,13 @@ Sidebar.Geometry.PlaneGeometry = function ( signals, object ) {
 	//
 
 	function update() {
-		
-		object.geometry.dispose();
 
-		object.geometry = new THREE.PlaneGeometry(
+		editor.execute( new SetGeometryCommand( object, new THREE.PlaneGeometry(
 			width.getValue(),
 			height.getValue(),
 			widthSegments.getValue(),
 			heightSegments.getValue()
-		);
-
-		object.geometry.computeBoundingSphere();
-
-		signals.geometryChanged.dispatch( object );
+		) ) );
 
 	}
 

+ 13 - 17
editor/js/Sidebar.Geometry.SphereGeometry.js

@@ -2,15 +2,17 @@
  * @author mrdoob / http://mrdoob.com/
  */
 
-Sidebar.Geometry.SphereGeometry = function ( signals, object ) {
+Sidebar.Geometry.SphereGeometry = function ( editor, object ) {
 
-	var container = new UI.Panel();
+	var signals = editor.signals;
+
+	var container = new UI.Row();
 
 	var parameters = object.geometry.parameters;
 
 	// radius
 
-	var radiusRow = new UI.Panel();
+	var radiusRow = new UI.Row();
 	var radius = new UI.Number( parameters.radius ).onChange( update );
 
 	radiusRow.add( new UI.Text( 'Radius' ).setWidth( '90px' ) );
@@ -20,7 +22,7 @@ Sidebar.Geometry.SphereGeometry = function ( signals, object ) {
 
 	// widthSegments
 
-	var widthSegmentsRow = new UI.Panel();
+	var widthSegmentsRow = new UI.Row();
 	var widthSegments = new UI.Integer( parameters.widthSegments ).setRange( 1, Infinity ).onChange( update );
 
 	widthSegmentsRow.add( new UI.Text( 'Width segments' ).setWidth( '90px' ) );
@@ -30,7 +32,7 @@ Sidebar.Geometry.SphereGeometry = function ( signals, object ) {
 
 	// heightSegments
 
-	var heightSegmentsRow = new UI.Panel();
+	var heightSegmentsRow = new UI.Row();
 	var heightSegments = new UI.Integer( parameters.heightSegments ).setRange( 1, Infinity ).onChange( update );
 
 	heightSegmentsRow.add( new UI.Text( 'Height segments' ).setWidth( '90px' ) );
@@ -40,7 +42,7 @@ Sidebar.Geometry.SphereGeometry = function ( signals, object ) {
 
 	// phiStart
 
-	var phiStartRow = new UI.Panel();
+	var phiStartRow = new UI.Row();
 	var phiStart = new UI.Number( parameters.phiStart ).onChange( update );
 
 	phiStartRow.add( new UI.Text( 'Phi start' ).setWidth( '90px' ) );
@@ -50,7 +52,7 @@ Sidebar.Geometry.SphereGeometry = function ( signals, object ) {
 
 	// phiLength
 
-	var phiLengthRow = new UI.Panel();
+	var phiLengthRow = new UI.Row();
 	var phiLength = new UI.Number( parameters.phiLength ).onChange( update );
 
 	phiLengthRow.add( new UI.Text( 'Phi length' ).setWidth( '90px' ) );
@@ -60,7 +62,7 @@ Sidebar.Geometry.SphereGeometry = function ( signals, object ) {
 
 	// thetaStart
 
-	var thetaStartRow = new UI.Panel();
+	var thetaStartRow = new UI.Row();
 	var thetaStart = new UI.Number( parameters.thetaStart ).onChange( update );
 
 	thetaStartRow.add( new UI.Text( 'Theta start' ).setWidth( '90px' ) );
@@ -70,7 +72,7 @@ Sidebar.Geometry.SphereGeometry = function ( signals, object ) {
 
 	// thetaLength
 
-	var thetaLengthRow = new UI.Panel();
+	var thetaLengthRow = new UI.Row();
 	var thetaLength = new UI.Number( parameters.thetaLength ).onChange( update );
 
 	thetaLengthRow.add( new UI.Text( 'Theta length' ).setWidth( '90px' ) );
@@ -83,9 +85,7 @@ Sidebar.Geometry.SphereGeometry = function ( signals, object ) {
 
 	function update() {
 
-		object.geometry.dispose();
-
-		object.geometry = new THREE.SphereGeometry(
+		editor.execute( new SetGeometryCommand( 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 );
+		) ) );
 
 	}
 

+ 8 - 8
editor/js/Sidebar.Geometry.TeapotBufferGeometry.js

@@ -4,13 +4,13 @@
 
 Sidebar.Geometry.TeapotBufferGeometry = function ( signals, object ) {
 
-	var container = new UI.Panel();
+	var container = new UI.Row();
 
 	var parameters = object.geometry.parameters;
 
 	// size
 
-	var sizeRow = new UI.Panel();
+	var sizeRow = new UI.Row();
 	var size = new UI.Number( parameters.size ).onChange( update );
 
 	sizeRow.add( new UI.Text( 'Size' ).setWidth( '90px' ) );
@@ -20,7 +20,7 @@ Sidebar.Geometry.TeapotBufferGeometry = function ( signals, object ) {
 
 	// segments
 
-	var segmentsRow = new UI.Panel();
+	var segmentsRow = new UI.Row();
 	var segments = new UI.Integer( parameters.segments ).setRange( 1, Infinity ).onChange( update );
 
 	segmentsRow.add( new UI.Text( 'Segments' ).setWidth( '90px' ) );
@@ -30,7 +30,7 @@ Sidebar.Geometry.TeapotBufferGeometry = function ( signals, object ) {
 
 	// bottom
 
-	var bottomRow = new UI.Panel();
+	var bottomRow = new UI.Row();
 	var bottom = new UI.Checkbox( parameters.bottom ).onChange( update );
 
 	bottomRow.add( new UI.Text( 'Bottom' ).setWidth( '90px' ) );
@@ -40,7 +40,7 @@ Sidebar.Geometry.TeapotBufferGeometry = function ( signals, object ) {
 
 	// lid
 
-	var lidRow = new UI.Panel();
+	var lidRow = new UI.Row();
 	var lid = new UI.Checkbox( parameters.lid ).onChange( update );
 
 	lidRow.add( new UI.Text( 'Lid' ).setWidth( '90px' ) );
@@ -50,7 +50,7 @@ Sidebar.Geometry.TeapotBufferGeometry = function ( signals, object ) {
 
 	// body
 
-	var bodyRow = new UI.Panel();
+	var bodyRow = new UI.Row();
 	var body = new UI.Checkbox( parameters.body ).onChange( update );
 
 	bodyRow.add( new UI.Text( 'Body' ).setWidth( '90px' ) );
@@ -60,7 +60,7 @@ Sidebar.Geometry.TeapotBufferGeometry = function ( signals, object ) {
 
 	// fitted lid
 
-	var fitLidRow = new UI.Panel();
+	var fitLidRow = new UI.Row();
 	var fitLid = new UI.Checkbox( parameters.fitLid ).onChange( update );
 
 	fitLidRow.add( new UI.Text( 'Fitted Lid' ).setWidth( '90px' ) );
@@ -70,7 +70,7 @@ Sidebar.Geometry.TeapotBufferGeometry = function ( signals, object ) {
 
 	// blinn-sized
 
-	var blinnRow = new UI.Panel();
+	var blinnRow = new UI.Row();
 	var blinn = new UI.Checkbox( parameters.blinn ).onChange( update );
 
 	blinnRow.add( new UI.Text( 'Blinn-scaled' ).setWidth( '90px' ) );

+ 11 - 15
editor/js/Sidebar.Geometry.TorusGeometry.js

@@ -2,15 +2,17 @@
  * @author mrdoob / http://mrdoob.com/
  */
 
-Sidebar.Geometry.TorusGeometry = function ( signals, object ) {
+Sidebar.Geometry.TorusGeometry = function ( editor, object ) {
 
-	var container = new UI.Panel();
+	var signals = editor.signals;
+
+	var container = new UI.Row();
 
 	var parameters = object.geometry.parameters;
 
 	// radius
 
-	var radiusRow = new UI.Panel();
+	var radiusRow = new UI.Row();
 	var radius = new UI.Number( parameters.radius ).onChange( update );
 
 	radiusRow.add( new UI.Text( 'Radius' ).setWidth( '90px' ) );
@@ -20,7 +22,7 @@ Sidebar.Geometry.TorusGeometry = function ( signals, object ) {
 
 	// tube
 
-	var tubeRow = new UI.Panel();
+	var tubeRow = new UI.Row();
 	var tube = new UI.Number( parameters.tube ).onChange( update );
 
 	tubeRow.add( new UI.Text( 'Tube' ).setWidth( '90px' ) );
@@ -30,7 +32,7 @@ Sidebar.Geometry.TorusGeometry = function ( signals, object ) {
 
 	// radialSegments
 
-	var radialSegmentsRow = new UI.Panel();
+	var radialSegmentsRow = new UI.Row();
 	var radialSegments = new UI.Integer( parameters.radialSegments ).setRange( 1, Infinity ).onChange( update );
 
 	radialSegmentsRow.add( new UI.Text( 'Radial segments' ).setWidth( '90px' ) );
@@ -40,7 +42,7 @@ Sidebar.Geometry.TorusGeometry = function ( signals, object ) {
 
 	// tubularSegments
 
-	var tubularSegmentsRow = new UI.Panel();
+	var tubularSegmentsRow = new UI.Row();
 	var tubularSegments = new UI.Integer( parameters.tubularSegments ).setRange( 1, Infinity ).onChange( update );
 
 	tubularSegmentsRow.add( new UI.Text( 'Tubular segments' ).setWidth( '90px' ) );
@@ -50,7 +52,7 @@ Sidebar.Geometry.TorusGeometry = function ( signals, object ) {
 
 	// arc
 
-	var arcRow = new UI.Panel();
+	var arcRow = new UI.Row();
 	var arc = new UI.Number( parameters.arc ).onChange( update );
 
 	arcRow.add( new UI.Text( 'Arc' ).setWidth( '90px' ) );
@@ -63,19 +65,13 @@ Sidebar.Geometry.TorusGeometry = function ( signals, object ) {
 
 	function update() {
 
-		object.geometry.dispose();
-
-		object.geometry = new THREE.TorusGeometry(
+		editor.execute( new SetGeometryCommand( object, new THREE.TorusGeometry(
 			radius.getValue(),
 			tube.getValue(),
 			radialSegments.getValue(),
 			tubularSegments.getValue(),
 			arc.getValue()
-		);
-
-		object.geometry.computeBoundingSphere();
-
-		signals.geometryChanged.dispatch( object );
+		) ) );
 
 	}
 

+ 13 - 17
editor/js/Sidebar.Geometry.TorusKnotGeometry.js

@@ -2,15 +2,17 @@
  * @author mrdoob / http://mrdoob.com/
  */
 
-Sidebar.Geometry.TorusKnotGeometry = function ( signals, object ) {
+Sidebar.Geometry.TorusKnotGeometry = function ( editor, object ) {
 
-	var container = new UI.Panel();
+	var signals = editor.signals;
+
+	var container = new UI.Row();
 
 	var parameters = object.geometry.parameters;
 
 	// radius
 
-	var radiusRow = new UI.Panel();
+	var radiusRow = new UI.Row();
 	var radius = new UI.Number( parameters.radius ).onChange( update );
 
 	radiusRow.add( new UI.Text( 'Radius' ).setWidth( '90px' ) );
@@ -20,7 +22,7 @@ Sidebar.Geometry.TorusKnotGeometry = function ( signals, object ) {
 
 	// tube
 
-	var tubeRow = new UI.Panel();
+	var tubeRow = new UI.Row();
 	var tube = new UI.Number( parameters.tube ).onChange( update );
 
 	tubeRow.add( new UI.Text( 'Tube' ).setWidth( '90px' ) );
@@ -30,7 +32,7 @@ Sidebar.Geometry.TorusKnotGeometry = function ( signals, object ) {
 
 	// radialSegments
 
-	var radialSegmentsRow = new UI.Panel();
+	var radialSegmentsRow = new UI.Row();
 	var radialSegments = new UI.Integer( parameters.radialSegments ).setRange( 1, Infinity ).onChange( update );
 
 	radialSegmentsRow.add( new UI.Text( 'Radial segments' ).setWidth( '90px' ) );
@@ -40,7 +42,7 @@ Sidebar.Geometry.TorusKnotGeometry = function ( signals, object ) {
 
 	// tubularSegments
 
-	var tubularSegmentsRow = new UI.Panel();
+	var tubularSegmentsRow = new UI.Row();
 	var tubularSegments = new UI.Integer( parameters.tubularSegments ).setRange( 1, Infinity ).onChange( update );
 
 	tubularSegmentsRow.add( new UI.Text( 'Tubular segments' ).setWidth( '90px' ) );
@@ -50,7 +52,7 @@ Sidebar.Geometry.TorusKnotGeometry = function ( signals, object ) {
 
 	// p
 
-	var pRow = new UI.Panel();
+	var pRow = new UI.Row();
 	var p = new UI.Number( parameters.p ).onChange( update );
 
 	pRow.add( new UI.Text( 'P' ).setWidth( '90px' ) );
@@ -60,7 +62,7 @@ Sidebar.Geometry.TorusKnotGeometry = function ( signals, object ) {
 
 	// q
 
-	var qRow = new UI.Panel();
+	var qRow = new UI.Row();
 	var q = new UI.Number( parameters.q ).onChange( update );
 
 	pRow.add( new UI.Text( 'Q' ).setWidth( '90px' ) );
@@ -70,7 +72,7 @@ Sidebar.Geometry.TorusKnotGeometry = function ( signals, object ) {
 
 	// heightScale
 
-	var heightScaleRow = new UI.Panel();
+	var heightScaleRow = new UI.Row();
 	var heightScale = new UI.Number( parameters.heightScale ).onChange( update );
 
 	pRow.add( new UI.Text( 'Height scale' ).setWidth( '90px' ) );
@@ -83,9 +85,7 @@ Sidebar.Geometry.TorusKnotGeometry = function ( signals, object ) {
 
 	function update() {
 
-		object.geometry.dispose();
-
-		object.geometry = new THREE.TorusKnotGeometry(
+		editor.execute( new SetGeometryCommand( 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 );
+		) ) );
 
 	}
 

+ 37 - 38
editor/js/Sidebar.Geometry.js

@@ -6,21 +6,13 @@ Sidebar.Geometry = function ( editor ) {
 
 	var signals = editor.signals;
 
-	var container = new UI.CollapsiblePanel();
-	container.setCollapsed( editor.config.getKey( 'ui/sidebar/geometry/collapsed' ) );
-	container.onCollapsedChange( function ( boolean ) {
-
-		editor.config.setKey( 'ui/sidebar/geometry/collapsed', boolean );
-
-	} );
-	container.setDisplay( 'none' );
-
-	var geometryType = new UI.Text().setTextTransform( 'uppercase' );
-	container.addStatic( geometryType );
+	var container = new UI.Panel();
+	container.setBorderTop( '0' );
+	container.setPaddingTop( '20px' );
 
 	// Actions
 
-	var objectActions = new UI.Select().setPosition('absolute').setRight( '8px' ).setFontSize( '11px' );
+	var objectActions = new UI.Select().setPosition( 'absolute' ).setRight( '8px' ).setFontSize( '11px' );
 	objectActions.setOptions( {
 
 		'Actions': 'Actions',
@@ -49,10 +41,11 @@ Sidebar.Geometry = function ( editor ) {
 
 				var offset = geometry.center();
 
-				object.position.sub( offset );
+				var newPosition = object.position.clone();
+				newPosition.sub( offset );
+				editor.execute( new SetPositionCommand( object, newPosition ) );
 
-				editor.signals.geometryChanged.dispatch( geometry );
-				editor.signals.objectChanged.dispatch( object );
+				editor.signals.geometryChanged.dispatch( object );
 
 				break;
 
@@ -60,9 +53,7 @@ Sidebar.Geometry = function ( editor ) {
 
 				if ( geometry instanceof THREE.Geometry ) {
 
-					object.geometry = new THREE.BufferGeometry().fromGeometry( geometry );
-
-					signals.geometryChanged.dispatch( object );
+					editor.execute( new SetGeometryCommand( object, new THREE.BufferGeometry().fromGeometry( geometry ) ) );
 
 				}
 
@@ -70,14 +61,16 @@ Sidebar.Geometry = function ( editor ) {
 
 			case 'Flatten':
 
-				geometry.applyMatrix( object.matrix );
+				var newGeometry = geometry.clone();
+				newGeometry.uuid = geometry.uuid;
+				newGeometry.applyMatrix( object.matrix );
 
-				object.position.set( 0, 0, 0 );
-				object.rotation.set( 0, 0, 0 );
-				object.scale.set( 1, 1, 1 );
+				var cmds = [ new SetGeometryCommand( object, newGeometry ),
+					new SetPositionCommand( object, new THREE.Vector3( 0, 0, 0 ) ),
+					new SetRotationCommand( object, new THREE.Euler( 0, 0, 0 ) ),
+					new SetScaleCommand( object, new THREE.Vector3( 1, 1, 1 ) ) ];
 
-				editor.signals.geometryChanged.dispatch( geometry );
-				editor.signals.objectChanged.dispatch( object );
+				editor.execute( new MultiCmdsCommand( cmds ), 'Flatten Geometry' );
 
 				break;
 
@@ -85,22 +78,28 @@ Sidebar.Geometry = function ( editor ) {
 
 		this.setValue( 'Actions' );
 
-		signals.objectChanged.dispatch( object );
-
 	} );
-	container.addStatic( objectActions );
+	// container.addStatic( objectActions );
+
+	// type
+
+	var geometryTypeRow = new UI.Row();
+	var geometryType = new UI.Text();
+
+	geometryTypeRow.add( new UI.Text( 'Type' ).setWidth( '90px' ) );
+	geometryTypeRow.add( geometryType );
 
-	container.add( new UI.Break() );
+	container.add( geometryTypeRow );
 
 	// uuid
 
-	var geometryUUIDRow = new UI.Panel();
+	var geometryUUIDRow = new UI.Row();
 	var geometryUUID = new UI.Input().setWidth( '115px' ).setFontSize( '12px' ).setDisabled( true );
 	var geometryUUIDRenew = new UI.Button( '⟳' ).setMarginLeft( '7px' ).onClick( function () {
 
 		geometryUUID.setValue( THREE.Math.generateUUID() );
 
-		editor.selected.geometry.uuid = geometryUUID.getValue();
+		editor.execute( new SetGeometryValueCommand( editor.selected, 'uuid', geometryUUID.getValue() ) );
 
 	} );
 
@@ -112,10 +111,10 @@ Sidebar.Geometry = function ( editor ) {
 
 	// name
 
-	var geometryNameRow = new UI.Panel();
+	var geometryNameRow = new UI.Row();
 	var geometryName = new UI.Input().setWidth( '150px' ).setFontSize( '12px' ).onChange( function () {
 
-		editor.setGeometryName( editor.selected.geometry, geometryName.getValue() );
+		editor.execute( new SetGeometryValueCommand( editor.selected, 'name', geometryName.getValue() ) );
 
 	} );
 
@@ -126,15 +125,15 @@ 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
 
-	var parameters = new UI.Panel();
+	var parameters = new UI.Span();
 	container.add( parameters );
 
 
@@ -161,11 +160,11 @@ Sidebar.Geometry = function ( editor ) {
 
 			if ( geometry.type === 'BufferGeometry' || geometry.type === 'Geometry' ) {
 
-				parameters.add( new Sidebar.Geometry.Modifiers( signals, object ) );
+				parameters.add( new Sidebar.Geometry.Modifiers( editor, object ) );
 
 			} else if ( Sidebar.Geometry[ geometry.type ] !== undefined ) {
 
-				parameters.add( new Sidebar.Geometry[ geometry.type ]( signals, object ) );
+				parameters.add( new Sidebar.Geometry[ geometry.type ]( editor, object ) );
 
 			}
 
@@ -182,4 +181,4 @@ Sidebar.Geometry = function ( editor ) {
 
 	return container;
 
-}
+};

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

@@ -0,0 +1,117 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+Sidebar.History = function ( editor ) {
+
+	var signals = editor.signals;
+
+	var config = editor.config;
+
+	var history = editor.history;
+
+	var container = new UI.Panel();
+
+	container.add( new UI.Text( 'HISTORY' ) );
+
+	//
+
+	var persistent = new UI.THREE.Boolean( config.getKey( 'settings/history' ), 'persistent' );
+	persistent.setPosition( 'absolute' ).setRight( '8px' );
+	persistent.onChange( function () {
+
+		var value = this.getValue();
+
+		config.setKey( 'settings/history', value );
+
+		if ( value ) {
+
+			alert( 'The history will be preserved across sessions.\nThis can have an impact on performance when working with textures.' );
+
+			var lastUndoCmd = history.undos[ history.undos.length - 1 ];
+			var lastUndoId = ( lastUndoCmd !== undefined ) ? lastUndoCmd.id : 0;
+			editor.history.enableSerialization( lastUndoId );
+
+		} else {
+
+			signals.historyChanged.dispatch();
+
+		}
+
+	} );
+	container.add( persistent );
+
+	container.add( new UI.Break(), 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;
+
+	} );
+	container.add( outliner );
+
+	//
+
+	var refreshUI = function () {
+
+		var options = [];
+		var enumerator = 1;
+
+		( function addObjects( objects, pad ) {
+
+			for ( var i = 0, l = objects.length; i < l; i ++ ) {
+
+				var object = objects[ i ];
+
+				var html = pad + "<span style='color: #0000cc '>" + enumerator ++ + ". Undo: " + object.name + "</span>";
+
+				options.push( { value: object.id, html: html } );
+
+			}
+
+		} )( history.undos, '&nbsp;' );
+
+
+		( function addObjects( objects, pad ) {
+
+			for ( var i = objects.length - 1; i >= 0; i -- ) {
+
+				var object = objects[ i ];
+
+				var html = pad + "<span style='color: #71544e'>" + enumerator ++ + ". Redo: " +  object.name + "</span>";
+
+				options.push( { value: object.id, html: html } );
+
+			}
+
+		} )( history.redos, '&nbsp;' );
+
+		outliner.setOptions( options );
+
+	};
+
+	refreshUI();
+
+	// events
+
+	signals.editorCleared.add( refreshUI );
+
+	signals.historyChanged.add( refreshUI );
+	signals.historyChanged.add( function ( cmd ) {
+
+		outliner.setValue( cmd !== undefined ? cmd.id : null );
+
+	} );
+
+
+	return container;
+
+};

+ 385 - 118
editor/js/Sidebar.Material.js

@@ -7,22 +7,36 @@ Sidebar.Material = function ( editor ) {
 	var signals = editor.signals;
 	var currentObject;
 
-	var container = new UI.CollapsiblePanel();
-	container.setCollapsed( editor.config.getKey( 'ui/sidebar/material/collapsed' ) );
-	container.onCollapsedChange( function ( boolean ) {
+	var container = new UI.Panel();
+	container.setBorderTop( '0' );
+	container.setPaddingTop( '20px' );
 
-		editor.config.setKey( 'ui/sidebar/material/collapsed', boolean );
+	// type
 
-	} );
-	container.setDisplay( 'none' );
-	container.dom.classList.add( 'Material' );
+	var materialClassRow = new UI.Row();
+	var materialClass = new UI.Select().setOptions( {
 
-	container.addStatic( new UI.Text().setValue( 'MATERIAL' ) );
-	container.add( new UI.Break() );
+		'LineBasicMaterial': 'LineBasicMaterial',
+		'LineDashedMaterial': 'LineDashedMaterial',
+		'MeshBasicMaterial': 'MeshBasicMaterial',
+		'MeshDepthMaterial': 'MeshDepthMaterial',
+		'MeshNormalMaterial': 'MeshNormalMaterial',
+		'MeshLambertMaterial': 'MeshLambertMaterial',
+		'MeshPhongMaterial': 'MeshPhongMaterial',
+		'MeshStandardMaterial': 'MeshStandardMaterial',
+		'ShaderMaterial': 'ShaderMaterial',
+		'SpriteMaterial': 'SpriteMaterial'
+
+	} ).setWidth( '150px' ).setFontSize( '12px' ).onChange( update );
+
+	materialClassRow.add( new UI.Text( 'Type' ).setWidth( '90px' ) );
+	materialClassRow.add( materialClass );
+
+	container.add( materialClassRow );
 
 	// uuid
 
-	var materialUUIDRow = new UI.Panel();
+	var materialUUIDRow = new UI.Row();
 	var materialUUID = new UI.Input().setWidth( '115px' ).setFontSize( '12px' ).setDisabled( true );
 	var materialUUIDRenew = new UI.Button( '⟳' ).setMarginLeft( '7px' ).onClick( function () {
 
@@ -39,10 +53,10 @@ Sidebar.Material = function ( editor ) {
 
 	// name
 
-	var materialNameRow = new UI.Panel();
+	var materialNameRow = new UI.Row();
 	var materialName = new UI.Input().setWidth( '150px' ).setFontSize( '12px' ).onChange( function () {
 
-		editor.setMaterialName( editor.selected.material, materialName.getValue() );
+		editor.execute( new SetMaterialValueCommand( editor.selected, 'name', materialName.getValue() ) );
 
 	} );
 
@@ -51,38 +65,16 @@ Sidebar.Material = function ( editor ) {
 
 	container.add( materialNameRow );
 
-	// class
-
-	var materialClassRow = new UI.Panel();
-	var materialClass = new UI.Select().setOptions( {
-
-		'LineBasicMaterial': 'LineBasicMaterial',
-		'LineDashedMaterial': 'LineDashedMaterial',
-		'MeshBasicMaterial': 'MeshBasicMaterial',
-		'MeshDepthMaterial': 'MeshDepthMaterial',
-		'MeshLambertMaterial': 'MeshLambertMaterial',
-		'MeshNormalMaterial': 'MeshNormalMaterial',
-		'MeshPhongMaterial': 'MeshPhongMaterial',
-		'ShaderMaterial': 'ShaderMaterial',
-		'SpriteMaterial': 'SpriteMaterial'
-
-	} ).setWidth( '150px' ).setFontSize( '12px' ).onChange( update );
-
-	materialClassRow.add( new UI.Text( 'Type' ).setWidth( '90px' ) );
-	materialClassRow.add( materialClass );
-
-	container.add( materialClassRow );
-
 	// program
 
-	var materialProgramRow = new UI.Panel();
+	var materialProgramRow = new UI.Row();
 	materialProgramRow.add( new UI.Text( 'Program' ).setWidth( '90px' ) );
 
 	var materialProgramInfo = new UI.Button( 'Info' );
 	materialProgramInfo.setMarginLeft( '4px' );
 	materialProgramInfo.onClick( function () {
 
-		signals.editScript.dispatch( currentObject.material, 'programInfo' );
+		signals.editScript.dispatch( currentObject, 'programInfo' );
 
 	} );
 	materialProgramRow.add( materialProgramInfo );
@@ -91,7 +83,7 @@ Sidebar.Material = function ( editor ) {
 	materialProgramVertex.setMarginLeft( '4px' );
 	materialProgramVertex.onClick( function () {
 
-		signals.editScript.dispatch( currentObject.material, 'vertexShader' );
+		signals.editScript.dispatch( currentObject, 'vertexShader' );
 
 	} );
 	materialProgramRow.add( materialProgramVertex );
@@ -100,7 +92,7 @@ Sidebar.Material = function ( editor ) {
 	materialProgramFragment.setMarginLeft( '4px' );
 	materialProgramFragment.onClick( function () {
 
-		signals.editScript.dispatch( currentObject.material, 'fragmentShader' );
+		signals.editScript.dispatch( currentObject, 'fragmentShader' );
 
 	} );
 	materialProgramRow.add( materialProgramFragment );
@@ -109,7 +101,7 @@ Sidebar.Material = function ( editor ) {
 
 	// color
 
-	var materialColorRow = new UI.Panel();
+	var materialColorRow = new UI.Row();
 	var materialColor = new UI.Color().onChange( update );
 
 	materialColorRow.add( new UI.Text( 'Color' ).setWidth( '90px' ) );
@@ -117,9 +109,29 @@ Sidebar.Material = function ( editor ) {
 
 	container.add( materialColorRow );
 
+	// roughness
+
+	var materialRoughnessRow = new UI.Row();
+	var materialRoughness = new UI.Number( 0.5 ).setWidth( '60px' ).setRange( 0, 1 ).onChange( update );
+
+	materialRoughnessRow.add( new UI.Text( 'Roughness' ).setWidth( '90px' ) );
+	materialRoughnessRow.add( materialRoughness );
+
+	container.add( materialRoughnessRow );
+
+	// metalness
+
+	var materialMetalnessRow = new UI.Row();
+	var materialMetalness = new UI.Number( 0.5 ).setWidth( '60px' ).setRange( 0, 1 ).onChange( update );
+
+	materialMetalnessRow.add( new UI.Text( 'Metalness' ).setWidth( '90px' ) );
+	materialMetalnessRow.add( materialMetalness );
+
+	container.add( materialMetalnessRow );
+
 	// emissive
 
-	var materialEmissiveRow = new UI.Panel();
+	var materialEmissiveRow = new UI.Row();
 	var materialEmissive = new UI.Color().setHexValue( 0x000000 ).onChange( update );
 
 	materialEmissiveRow.add( new UI.Text( 'Emissive' ).setWidth( '90px' ) );
@@ -129,7 +141,7 @@ Sidebar.Material = function ( editor ) {
 
 	// specular
 
-	var materialSpecularRow = new UI.Panel();
+	var materialSpecularRow = new UI.Row();
 	var materialSpecular = new UI.Color().setHexValue( 0x111111 ).onChange( update );
 
 	materialSpecularRow.add( new UI.Text( 'Specular' ).setWidth( '90px' ) );
@@ -139,7 +151,7 @@ Sidebar.Material = function ( editor ) {
 
 	// shininess
 
-	var materialShininessRow = new UI.Panel();
+	var materialShininessRow = new UI.Row();
 	var materialShininess = new UI.Number( 30 ).onChange( update );
 
 	materialShininessRow.add( new UI.Text( 'Shininess' ).setWidth( '90px' ) );
@@ -149,7 +161,7 @@ Sidebar.Material = function ( editor ) {
 
 	// vertex colors
 
-	var materialVertexColorsRow = new UI.Panel();
+	var materialVertexColorsRow = new UI.Row();
 	var materialVertexColors = new UI.Select().setOptions( {
 
 		0: 'No',
@@ -165,7 +177,7 @@ Sidebar.Material = function ( editor ) {
 
 	// skinning
 
-	var materialSkinningRow = new UI.Panel();
+	var materialSkinningRow = new UI.Row();
 	var materialSkinning = new UI.Checkbox( false ).onChange( update );
 
 	materialSkinningRow.add( new UI.Text( 'Skinning' ).setWidth( '90px' ) );
@@ -175,7 +187,7 @@ Sidebar.Material = function ( editor ) {
 
 	// map
 
-	var materialMapRow = new UI.Panel();
+	var materialMapRow = new UI.Row();
 	var materialMapEnabled = new UI.Checkbox( false ).onChange( update );
 	var materialMap = new UI.Texture().onChange( update );
 
@@ -187,7 +199,7 @@ Sidebar.Material = function ( editor ) {
 
 	// alpha map
 
-	var materialAlphaMapRow = new UI.Panel();
+	var materialAlphaMapRow = new UI.Row();
 	var materialAlphaMapEnabled = new UI.Checkbox( false ).onChange( update );
 	var materialAlphaMap = new UI.Texture().onChange( update );
 
@@ -199,7 +211,7 @@ Sidebar.Material = function ( editor ) {
 
 	// bump map
 
-	var materialBumpMapRow = new UI.Panel();
+	var materialBumpMapRow = new UI.Row();
 	var materialBumpMapEnabled = new UI.Checkbox( false ).onChange( update );
 	var materialBumpMap = new UI.Texture().onChange( update );
 	var materialBumpScale = new UI.Number( 1 ).setWidth( '30px' ).onChange( update );
@@ -213,7 +225,7 @@ Sidebar.Material = function ( editor ) {
 
 	// normal map
 
-	var materialNormalMapRow = new UI.Panel();
+	var materialNormalMapRow = new UI.Row();
 	var materialNormalMapEnabled = new UI.Checkbox( false ).onChange( update );
 	var materialNormalMap = new UI.Texture().onChange( update );
 
@@ -225,7 +237,7 @@ Sidebar.Material = function ( editor ) {
 
 	// displacement map
 
-	var materialDisplacementMapRow = new UI.Panel();
+	var materialDisplacementMapRow = new UI.Row();
 	var materialDisplacementMapEnabled = new UI.Checkbox( false ).onChange( update );
 	var materialDisplacementMap = new UI.Texture().onChange( update );
 	var materialDisplacementScale = new UI.Number( 1 ).setWidth( '30px' ).onChange( update );
@@ -237,9 +249,33 @@ Sidebar.Material = function ( editor ) {
 
 	container.add( materialDisplacementMapRow );
 
+	// roughness map
+
+	var materialRoughnessMapRow = new UI.Row();
+	var materialRoughnessMapEnabled = new UI.Checkbox( false ).onChange( update );
+	var materialRoughnessMap = new UI.Texture().onChange( update );
+
+	materialRoughnessMapRow.add( new UI.Text( 'Rough. Map' ).setWidth( '90px' ) );
+	materialRoughnessMapRow.add( materialRoughnessMapEnabled );
+	materialRoughnessMapRow.add( materialRoughnessMap );
+
+	container.add( materialRoughnessMapRow );
+
+	// metalness map
+
+	var materialMetalnessMapRow = new UI.Row();
+	var materialMetalnessMapEnabled = new UI.Checkbox( false ).onChange( update );
+	var materialMetalnessMap = new UI.Texture().onChange( update );
+
+	materialMetalnessMapRow.add( new UI.Text( 'Metal. Map' ).setWidth( '90px' ) );
+	materialMetalnessMapRow.add( materialMetalnessMapEnabled );
+	materialMetalnessMapRow.add( materialMetalnessMap );
+
+	container.add( materialMetalnessMapRow );
+
 	// specular map
 
-	var materialSpecularMapRow = new UI.Panel();
+	var materialSpecularMapRow = new UI.Row();
 	var materialSpecularMapEnabled = new UI.Checkbox( false ).onChange( update );
 	var materialSpecularMap = new UI.Texture().onChange( update );
 
@@ -251,7 +287,7 @@ Sidebar.Material = function ( editor ) {
 
 	// env map
 
-	var materialEnvMapRow = new UI.Panel();
+	var materialEnvMapRow = new UI.Row();
 	var materialEnvMapEnabled = new UI.Checkbox( false ).onChange( update );
 	var materialEnvMap = new UI.Texture( THREE.SphericalReflectionMapping ).onChange( update );
 	var materialReflectivity = new UI.Number( 1 ).setWidth( '30px' ).onChange( update );
@@ -265,7 +301,7 @@ Sidebar.Material = function ( editor ) {
 
 	// light map
 
-	var materialLightMapRow = new UI.Panel();
+	var materialLightMapRow = new UI.Row();
 	var materialLightMapEnabled = new UI.Checkbox( false ).onChange( update );
 	var materialLightMap = new UI.Texture().onChange( update );
 
@@ -277,7 +313,7 @@ Sidebar.Material = function ( editor ) {
 
 	// ambient occlusion map
 
-	var materialAOMapRow = new UI.Panel();
+	var materialAOMapRow = new UI.Row();
 	var materialAOMapEnabled = new UI.Checkbox( false ).onChange( update );
 	var materialAOMap = new UI.Texture().onChange( update );
 	var materialAOScale = new UI.Number( 1 ).setRange( 0, 1 ).setWidth( '30px' ).onChange( update );
@@ -289,9 +325,21 @@ Sidebar.Material = function ( editor ) {
 
 	container.add( materialAOMapRow );
 
+	// emissive map
+
+	var materialEmissiveMapRow = new UI.Row();
+	var materialEmissiveMapEnabled = new UI.Checkbox( false ).onChange( update );
+	var materialEmissiveMap = new UI.Texture().onChange( update );
+
+	materialEmissiveMapRow.add( new UI.Text( 'Emissive Map' ).setWidth( '90px' ) );
+	materialEmissiveMapRow.add( materialEmissiveMapEnabled );
+	materialEmissiveMapRow.add( materialEmissiveMap );
+
+	container.add( materialEmissiveMapRow );
+
 	// side
 
-	var materialSideRow = new UI.Panel();
+	var materialSideRow = new UI.Row();
 	var materialSide = new UI.Select().setOptions( {
 
 		0: 'Front',
@@ -307,7 +355,7 @@ Sidebar.Material = function ( editor ) {
 
 	// shading
 
-	var materialShadingRow = new UI.Panel();
+	var materialShadingRow = new UI.Row();
 	var materialShading = new UI.Select().setOptions( {
 
 		0: 'No',
@@ -323,7 +371,7 @@ Sidebar.Material = function ( editor ) {
 
 	// blending
 
-	var materialBlendingRow = new UI.Panel();
+	var materialBlendingRow = new UI.Row();
 	var materialBlending = new UI.Select().setOptions( {
 
 		0: 'No',
@@ -342,8 +390,8 @@ Sidebar.Material = function ( editor ) {
 
 	// opacity
 
-	var materialOpacityRow = new UI.Panel();
-	var materialOpacity = new UI.Number().setWidth( '60px' ).setRange( 0, 1 ).onChange( update );
+	var materialOpacityRow = new UI.Row();
+	var materialOpacity = new UI.Number( 1 ).setWidth( '60px' ).setRange( 0, 1 ).onChange( update );
 
 	materialOpacityRow.add( new UI.Text( 'Opacity' ).setWidth( '90px' ) );
 	materialOpacityRow.add( materialOpacity );
@@ -352,7 +400,7 @@ Sidebar.Material = function ( editor ) {
 
 	// transparent
 
-	var materialTransparentRow = new UI.Panel();
+	var materialTransparentRow = new UI.Row();
 	var materialTransparent = new UI.Checkbox().setLeft( '100px' ).onChange( update );
 
 	materialTransparentRow.add( new UI.Text( 'Transparent' ).setWidth( '90px' ) );
@@ -362,7 +410,7 @@ Sidebar.Material = function ( editor ) {
 
 	// alpha test
 
-	var materialAlphaTestRow = new UI.Panel();
+	var materialAlphaTestRow = new UI.Row();
 	var materialAlphaTest = new UI.Number().setWidth( '60px' ).setRange( 0, 1 ).onChange( update );
 
 	materialAlphaTestRow.add( new UI.Text( 'Alpha Test' ).setWidth( '90px' ) );
@@ -372,7 +420,7 @@ Sidebar.Material = function ( editor ) {
 
 	// wireframe
 
-	var materialWireframeRow = new UI.Panel();
+	var materialWireframeRow = new UI.Row();
 	var materialWireframe = new UI.Checkbox( false ).onChange( update );
 	var materialWireframeLinewidth = new UI.Number( 1 ).setWidth( '60px' ).setRange( 0, 100 ).onChange( update );
 
@@ -400,9 +448,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 SetMaterialValueCommand( currentObject, 'uuid', materialUUID.getValue() ) );
 
 			}
 
@@ -410,7 +458,7 @@ Sidebar.Material = function ( editor ) {
 
 				material = new THREE[ materialClass.getValue() ]();
 
-				object.material = material;
+				editor.execute( new SetMaterialCommand( currentObject, material ), 'New Material: ' + materialClass.getValue() );
 				// TODO Copy other references in the scene graph
 				// keeping name and UUID then.
 				// Also there should be means to create a unique
@@ -419,27 +467,39 @@ Sidebar.Material = function ( editor ) {
 
 			}
 
-			if ( material.color !== undefined ) {
+			if ( material.color !== undefined && material.color.getHex() !== materialColor.getHexValue() ) {
+
+				editor.execute( new SetMaterialColorCommand( currentObject, 'color', materialColor.getHexValue() ) );
+
+			}
+
+			if ( material.roughness !== undefined && Math.abs( material.roughness - materialRoughness.getValue() ) >= 0.01 ) {
 
-				material.color.setHex( materialColor.getHexValue() );
+				editor.execute( new SetMaterialValueCommand( currentObject, 'roughness', materialRoughness.getValue() ) );
 
 			}
 
-			if ( material.emissive !== undefined ) {
+			if ( material.metalness !== undefined && Math.abs( material.metalness - materialMetalness.getValue() ) >= 0.01 ) {
 
-				material.emissive.setHex( materialEmissive.getHexValue() );
+				editor.execute( new SetMaterialValueCommand( currentObject, 'metalness', materialMetalness.getValue() ) );
 
 			}
 
-			if ( material.specular !== undefined ) {
+			if ( material.emissive !== undefined && material.emissive.getHex() !== materialEmissive.getHexValue() ) {
 
-				material.specular.setHex( materialSpecular.getHexValue() );
+				editor.execute( new SetMaterialColorCommand( currentObject, 'emissive', materialEmissive.getHexValue() ) );
 
 			}
 
-			if ( material.shininess !== undefined ) {
+			if ( material.specular !== undefined && material.specular.getHex() !== materialSpecular.getHexValue() ) {
 
-				material.shininess = materialShininess.getValue();
+				editor.execute( new SetMaterialColorCommand( currentObject, 'specular', materialSpecular.getHexValue() ) );
+
+			}
+
+			if ( material.shininess !== undefined && Math.abs( material.shininess - materialShininess.getValue() ) >= 0.01 ) {
+
+				editor.execute( new SetMaterialValueCommand( currentObject, 'shininess', materialShininess.getValue() ) );
 
 			}
 
@@ -449,16 +509,15 @@ Sidebar.Material = function ( editor ) {
 
 				if ( material.vertexColors !== vertexColors ) {
 
-					material.vertexColors = vertexColors;
-					material.needsUpdate = true;
+					editor.execute( new SetMaterialValueCommand( currentObject, 'vertexColors', vertexColors ) );
 
 				}
 
 			}
 
-			if ( material.skinning !== undefined ) {
+			if ( material.skinning !== undefined && material.skinning !== materialSkinning.getValue() ) {
 
-				material.skinning = materialSkinning.getValue();
+				editor.execute( new SetMaterialValueCommand( currentObject, 'skinning', materialSkinning.getValue() ) );
 
 			}
 
@@ -468,8 +527,12 @@ Sidebar.Material = function ( editor ) {
 
 				if ( objectHasUvs ) {
 
-					material.map = mapEnabled ? materialMap.getValue() : null;
-					material.needsUpdate = true;
+					var map = mapEnabled ? materialMap.getValue() : null;
+					if ( material.map !== map ) {
+
+						editor.execute( new SetMaterialMapCommand( currentObject, 'map', map ) );
+
+					}
 
 				} else {
 
@@ -485,8 +548,12 @@ Sidebar.Material = function ( editor ) {
 
 				if ( objectHasUvs ) {
 
-					material.alphaMap = mapEnabled ? materialAlphaMap.getValue() : null;
-					material.needsUpdate = true;
+					var alphaMap = mapEnabled ? materialAlphaMap.getValue() : null;
+					if ( material.alphaMap !== alphaMap ) {
+
+						editor.execute( new SetMaterialMapCommand( currentObject, 'alphaMap', alphaMap ) );
+
+					}
 
 				} else {
 
@@ -502,9 +569,18 @@ Sidebar.Material = function ( editor ) {
 
 				if ( objectHasUvs ) {
 
-					material.bumpMap = bumpMapEnabled ? materialBumpMap.getValue() : null;
-					material.bumpScale = materialBumpScale.getValue();
-					material.needsUpdate = true;
+					var bumpMap = bumpMapEnabled ? materialBumpMap.getValue() : null;
+					if ( material.bumpMap !== bumpMap ) {
+
+						editor.execute( new SetMaterialMapCommand( currentObject, 'bumpMap', bumpMap ) );
+
+					}
+
+					if ( material.bumpScale !== materialBumpScale.getValue() ) {
+
+						editor.execute( new SetMaterialValueCommand( currentObject, 'bumpScale', materialBumpScale.getValue() ) );
+
+					}
 
 				} else {
 
@@ -520,8 +596,12 @@ Sidebar.Material = function ( editor ) {
 
 				if ( objectHasUvs ) {
 
-					material.normalMap = normalMapEnabled ? materialNormalMap.getValue() : null;
-					material.needsUpdate = true;
+					var normalMap = normalMapEnabled ? materialNormalMap.getValue() : null;
+					if ( material.normalMap !== normalMap ) {
+
+						editor.execute( new SetMaterialMapCommand( currentObject, 'normalMap', normalMap ) );
+
+					}
 
 				} else {
 
@@ -537,9 +617,18 @@ Sidebar.Material = function ( editor ) {
 
 				if ( objectHasUvs ) {
 
-					material.displacementMap = displacementMapEnabled ? materialDisplacementMap.getValue() : null;
-					material.displacementScale = materialDisplacementScale.getValue();
-					material.needsUpdate = true;
+					var displacementMap = displacementMapEnabled ? materialDisplacementMap.getValue() : null;
+					if ( material.displacementMap !== displacementMap ) {
+
+						editor.execute( new SetMaterialMapCommand( currentObject, 'displacementMap', displacementMap ) );
+
+					}
+
+					if ( material.displacementScale !== materialDisplacementScale.getValue() ) {
+
+						editor.execute( new SetMaterialValueCommand( currentObject, 'displacementScale', materialDisplacementScale.getValue() ) );
+
+					}
 
 				} else {
 
@@ -549,14 +638,72 @@ Sidebar.Material = function ( editor ) {
 
 			}
 
+			if ( material.roughnessMap !== undefined ) {
+
+				var roughnessMapEnabled = materialRoughnessMapEnabled.getValue() === true;
+
+				if ( objectHasUvs ) {
+
+					var roughnessMap = roughnessMapEnabled ? materialRoughnessMap.getValue() : null;
+					if ( material.roughnessMap !== roughnessMap ) {
+
+						editor.execute( new SetMaterialMapCommand( currentObject, 'roughnessMap', roughnessMap ) );
+
+					}
+
+					if ( material.displacementScale !== materialDisplacementScale.getValue() ) {
+
+						editor.execute( new SetMaterialValueCommand( currentObject, 'displacementScale', materialDisplacementScale.getValue() ) );
+
+					}
+
+				} else {
+
+					if ( roughnessMapEnabled ) textureWarning = true;
+
+				}
+
+			}
+
+			if ( material.metalnessMap !== undefined ) {
+
+				var metalnessMapEnabled = materialMetalnessMapEnabled.getValue() === true;
+
+				if ( objectHasUvs ) {
+
+					var metalnessMap = metalnessMapEnabled ? materialMetalnessMap.getValue() : null;
+					if ( material.metalnessMap !== metalnessMap ) {
+
+						editor.execute( new SetMaterialMapCommand( currentObject, 'metalnessMap', metalnessMap ) );
+
+					}
+
+					if ( material.displacementScale !== materialDisplacementScale.getValue() ) {
+
+						editor.execute( new SetMaterialValueCommand( currentObject, 'displacementScale', materialDisplacementScale.getValue() ) );
+
+					}
+
+				} else {
+
+					if ( metalnessMapEnabled ) textureWarning = true;
+
+				}
+
+			}
+
 			if ( material.specularMap !== undefined ) {
 
 				var specularMapEnabled = materialSpecularMapEnabled.getValue() === true;
 
 				if ( objectHasUvs ) {
 
-					material.specularMap = specularMapEnabled ? materialSpecularMap.getValue() : null;
-					material.needsUpdate = true;
+					var specularMap = specularMapEnabled ? materialSpecularMap.getValue() : null;
+					if ( material.specularMap !== specularMap ) {
+
+						editor.execute( new SetMaterialMapCommand( currentObject, 'specularMap', specularMap ) );
+
+					}
 
 				} else {
 
@@ -570,12 +717,21 @@ Sidebar.Material = function ( editor ) {
 
 				var envMapEnabled = materialEnvMapEnabled.getValue() === true;
 
-				material.envMap = envMapEnabled ? materialEnvMap.getValue() : null;
-				material.reflectivity = materialReflectivity.getValue();
-				material.needsUpdate = true;
+				var envMap = envMapEnabled ? materialEnvMap.getValue() : null;
 
-			}
+				if ( material.envMap !== envMap ) {
+
+					editor.execute( new SetMaterialMapCommand( currentObject, 'envMap', envMap ) );
+
+				}
+
+				if ( material.reflectivity !== materialReflectivity.getValue() ) {
 
+					editor.execute( new SetMaterialValueCommand( currentObject, 'reflectivity', materialReflectivity.getValue() ) );
+
+				}
+
+			}
 
 			if ( material.lightMap !== undefined ) {
 
@@ -583,8 +739,12 @@ Sidebar.Material = function ( editor ) {
 
 				if ( objectHasUvs ) {
 
-					material.lightMap = lightMapEnabled ? materialLightMap.getValue() : null;
-					material.needsUpdate = true;
+					var lightMap = lightMapEnabled ? materialLightMap.getValue() : null;
+					if ( material.lightMap !== lightMap ) {
+
+						editor.execute( new SetMaterialMapCommand( currentObject, 'lightMap', lightMap ) );
+
+					}
 
 				} else {
 
@@ -600,9 +760,18 @@ Sidebar.Material = function ( editor ) {
 
 				if ( objectHasUvs ) {
 
-					material.aoMap = aoMapEnabled ? materialAOMap.getValue() : null;
-					material.aoMapIntensity = materialAOScale.getValue();
-					material.needsUpdate = true;
+					var aoMap = aoMapEnabled ? materialAOMap.getValue() : null;
+					if ( material.aoMap !== aoMap ) {
+
+						editor.execute( new SetMaterialMapCommand( currentObject, 'aoMap', aoMap ) );
+
+					}
+
+					if ( material.aoMapIntensity !== materialAOScale.getValue() ) {
+
+						editor.execute( new SetMaterialValueCommand( currentObject, 'aoMapIntensity', materialAOScale.getValue() ) );
+
+					}
 
 				} else {
 
@@ -612,55 +781,92 @@ Sidebar.Material = function ( editor ) {
 
 			}
 
+			if ( material.emissiveMap !== undefined ) {
+
+				var emissiveMapEnabled = materialEmissiveMapEnabled.getValue() === true;
+
+				if ( objectHasUvs ) {
+
+					var emissiveMap = emissiveMapEnabled ? materialEmissiveMap.getValue() : null;
+					if ( material.emissiveMap !== emissiveMap ) {
+
+						editor.execute( new SetMaterialMapCommand( currentObject, 'emissiveMap', emissiveMap ) );
+
+					}
+
+				} else {
+
+					if ( emissiveMapEnabled ) textureWarning = true;
+
+				}
+
+			}
+
 			if ( material.side !== undefined ) {
 
-				material.side = parseInt( materialSide.getValue() );
+				var side = parseInt( materialSide.getValue() );
+				if ( material.side !== side ) {
+
+					editor.execute( new SetMaterialValueCommand( currentObject, 'side', side ) );
+
+				}
+
 
 			}
 
 			if ( material.shading !== undefined ) {
 
-				material.shading = parseInt( materialShading.getValue() );
+				var shading = parseInt( materialShading.getValue() );
+				if ( material.shading !== shading ) {
+
+					editor.execute( new SetMaterialValueCommand( currentObject, 'shading', shading ) );
+
+				}
 
 			}
 
 			if ( material.blending !== undefined ) {
 
-				material.blending = parseInt( materialBlending.getValue() );
+				var blending = parseInt( materialBlending.getValue() );
+				if ( material.blending !== blending ) {
+
+					editor.execute( new SetMaterialValueCommand( currentObject, 'blending', blending ) );
+
+				}
 
 			}
 
-			if ( material.opacity !== undefined ) {
+			if ( material.opacity !== undefined && Math.abs( material.opacity - materialOpacity.getValue() ) >= 0.01 ) {
 
-				material.opacity = materialOpacity.getValue();
+				editor.execute( new SetMaterialValueCommand( currentObject, 'opacity', materialOpacity.getValue() ) );
 
 			}
 
-			if ( material.transparent !== undefined ) {
+			if ( material.transparent !== undefined && material.transparent !== materialTransparent.getValue() ) {
 
-				material.transparent = materialTransparent.getValue();
+				editor.execute( new SetMaterialValueCommand( currentObject, 'transparent', materialTransparent.getValue() ) );
 
 			}
 
-			if ( material.alphaTest !== undefined ) {
+			if ( material.alphaTest !== undefined && Math.abs( material.alphaTest - materialAlphaTest.getValue() ) >= 0.01 ) {
 
-				material.alphaTest = materialAlphaTest.getValue();
+				editor.execute( new SetMaterialValueCommand( currentObject, 'alphaTest', materialAlphaTest.getValue() ) );
 
 			}
 
-			if ( material.wireframe !== undefined ) {
+			if ( material.wireframe !== undefined && material.wireframe !== materialWireframe.getValue() ) {
 
-				material.wireframe = materialWireframe.getValue();
+				editor.execute( new SetMaterialValueCommand( currentObject, 'wireframe', materialWireframe.getValue() ) );
 
 			}
 
-			if ( material.wireframeLinewidth !== undefined ) {
+			if ( material.wireframeLinewidth !== undefined && Math.abs( material.wireframeLinewidth - materialWireframeLinewidth.getValue() ) >= 0.01 ) {
 
-				material.wireframeLinewidth = materialWireframeLinewidth.getValue();
+				editor.execute( new SetMaterialValueCommand( currentObject, 'wireframeLinewidth', materialWireframeLinewidth.getValue() ) );
 
 			}
 
-			refreshUi(false);
+			refreshUI( false );
 
 			signals.materialChanged.dispatch( material );
 
@@ -672,7 +878,7 @@ Sidebar.Material = function ( editor ) {
 
 		}
 
-	};
+	}
 
 	//
 
@@ -681,6 +887,8 @@ Sidebar.Material = function ( editor ) {
 		var properties = {
 			'name': materialNameRow,
 			'color': materialColorRow,
+			'roughness': materialRoughnessRow,
+			'metalness': materialMetalnessRow,
 			'emissive': materialEmissiveRow,
 			'specular': materialSpecularRow,
 			'shininess': materialShininessRow,
@@ -692,10 +900,13 @@ Sidebar.Material = function ( editor ) {
 			'bumpMap': materialBumpMapRow,
 			'normalMap': materialNormalMapRow,
 			'displacementMap': materialDisplacementMapRow,
+			'roughnessMap': materialRoughnessMapRow,
+			'metalnessMap': materialMetalnessMapRow,
 			'specularMap': materialSpecularMapRow,
 			'envMap': materialEnvMapRow,
 			'lightMap': materialLightMapRow,
 			'aoMap': materialAOMapRow,
+			'emissiveMap': materialEmissiveMapRow,
 			'side': materialSideRow,
 			'shading': materialShadingRow,
 			'blending': materialBlendingRow,
@@ -713,10 +924,12 @@ Sidebar.Material = function ( editor ) {
 
 		}
 
-	};
+	}
+
 
+	function refreshUI( resetTextureSelectors ) {
 
-	function refreshUi( resetTextureSelectors ) {
+		if ( ! currentObject ) return;
 
 		var material = currentObject.material;
 
@@ -740,6 +953,18 @@ Sidebar.Material = function ( editor ) {
 
 		}
 
+		if ( material.roughness !== undefined ) {
+
+			materialRoughness.setValue( material.roughness );
+
+		}
+
+		if ( material.metalness !== undefined ) {
+
+			materialMetalness.setValue( material.metalness );
+
+		}
+
 		if ( material.emissive !== undefined ) {
 
 			materialEmissive.setHexValue( material.emissive.getHexString() );
@@ -834,6 +1059,30 @@ Sidebar.Material = function ( editor ) {
 
 		}
 
+		if ( material.roughnessMap !== undefined ) {
+
+			materialRoughnessMapEnabled.setValue( material.roughnessMap !== null );
+
+			if ( material.roughnessMap !== null || resetTextureSelectors ) {
+
+				materialRoughnessMap.setValue( material.roughnessMap );
+
+			}
+
+		}
+
+		if ( material.metalnessMap !== undefined ) {
+
+			materialMetalnessMapEnabled.setValue( material.metalnessMap !== null );
+
+			if ( material.metalnessMap !== null || resetTextureSelectors ) {
+
+				materialMetalnessMap.setValue( material.metalnessMap );
+
+			}
+
+		}
+
 		if ( material.specularMap !== undefined ) {
 
 			materialSpecularMapEnabled.setValue( material.specularMap !== null );
@@ -886,6 +1135,18 @@ Sidebar.Material = function ( editor ) {
 
 		}
 
+		if ( material.emissiveMap !== undefined ) {
+
+			materialEmissiveMapEnabled.setValue( material.emissiveMap !== null );
+
+			if ( material.emissiveMap !== null || resetTextureSelectors ) {
+
+				materialEmissiveMap.setValue( material.emissiveMap );
+
+			}
+
+		}
+
 		if ( material.side !== undefined ) {
 
 			materialSide.setValue( material.side );
@@ -947,7 +1208,7 @@ Sidebar.Material = function ( editor ) {
 			var objectChanged = object !== currentObject;
 
 			currentObject = object;
-			refreshUi(objectChanged);
+			refreshUI( objectChanged );
 			container.setDisplay( '' );
 
 		} else {
@@ -959,6 +1220,12 @@ Sidebar.Material = function ( editor ) {
 
 	} );
 
+	signals.materialChanged.add( function () {
+
+		refreshUI();
+
+	} );
+
 	return container;
 
-}
+};

+ 108 - 97
editor/js/Sidebar.Object3D.js → editor/js/Sidebar.Object.js

@@ -2,25 +2,18 @@
  * @author mrdoob / http://mrdoob.com/
  */
 
-Sidebar.Object3D = function ( editor ) {
+Sidebar.Object = function ( editor ) {
 
 	var signals = editor.signals;
 
-	var container = new UI.CollapsiblePanel();
-	container.setCollapsed( editor.config.getKey( 'ui/sidebar/object3d/collapsed' ) );
-	container.onCollapsedChange( function ( boolean ) {
-
-		editor.config.setKey( 'ui/sidebar/object3d/collapsed', boolean );
-
-	} );
+	var container = new UI.Panel();
+	container.setBorderTop( '0' );
+	container.setPaddingTop( '20px' );
 	container.setDisplay( 'none' );
 
-	var objectType = new UI.Text().setTextTransform( 'uppercase' );
-	container.addStatic( objectType );
-
 	// Actions
 
-	var objectActions = new UI.Select().setPosition('absolute').setRight( '8px' ).setFontSize( '11px' );
+	var objectActions = new UI.Select().setPosition( 'absolute' ).setRight( '8px' ).setFontSize( '11px' );
 	objectActions.setOptions( {
 
 		'Actions': 'Actions',
@@ -41,37 +34,43 @@ Sidebar.Object3D = function ( editor ) {
 		switch ( this.getValue() ) {
 
 			case 'Reset Position':
-				object.position.set( 0, 0, 0 );
+				editor.execute( new SetPositionCommand( object, new THREE.Vector3( 0, 0, 0 ) ) );
 				break;
 
 			case 'Reset Rotation':
-				object.rotation.set( 0, 0, 0 );
+				editor.execute( new SetRotationCommand( object, new THREE.Euler( 0, 0, 0 ) ) );
 				break;
 
 			case 'Reset Scale':
-				object.scale.set( 1, 1, 1 );
+				editor.execute( new SetScaleCommand( object, new THREE.Vector3( 1, 1, 1 ) ) );
 				break;
 
 		}
 
 		this.setValue( 'Actions' );
 
-		signals.objectChanged.dispatch( object );
-
 	} );
-	container.addStatic( objectActions );
+	// container.addStatic( objectActions );
 
-	container.add( new UI.Break() );
+	// type
+
+	var objectTypeRow = new UI.Row();
+	var objectType = new UI.Text();
+
+	objectTypeRow.add( new UI.Text( 'Type' ).setWidth( '90px' ) );
+	objectTypeRow.add( objectType );
+
+	container.add( objectTypeRow );
 
 	// uuid
 
-	var objectUUIDRow = new UI.Panel();
+	var objectUUIDRow = new UI.Row();
 	var objectUUID = new UI.Input().setWidth( '115px' ).setFontSize( '12px' ).setDisabled( true );
 	var objectUUIDRenew = new UI.Button( '⟳' ).setMarginLeft( '7px' ).onClick( function () {
 
 		objectUUID.setValue( THREE.Math.generateUUID() );
 
-		editor.selected.uuid = objectUUID.getValue();
+		editor.execute( new SetUuidCommand( editor.selected, objectUUID.getValue() ) );
 
 	} );
 
@@ -83,10 +82,10 @@ Sidebar.Object3D = function ( editor ) {
 
 	// name
 
-	var objectNameRow = new UI.Panel();
+	var objectNameRow = new UI.Row();
 	var objectName = new UI.Input().setWidth( '150px' ).setFontSize( '12px' ).onChange( function () {
 
-		editor.nameObject( editor.selected, objectName.getValue() );
+		editor.execute( new SetValueCommand( editor.selected, 'name', objectName.getValue() ) );
 
 	} );
 
@@ -98,7 +97,7 @@ Sidebar.Object3D = function ( editor ) {
 	/*
 	// parent
 
-	var objectParentRow = new UI.Panel();
+	var objectParentRow = new UI.Row();
 	var objectParent = new UI.Select().setWidth( '150px' ).setFontSize( '12px' ).onChange( update );
 
 	objectParentRow.add( new UI.Text( 'Parent' ).setWidth( '90px' ) );
@@ -109,7 +108,7 @@ Sidebar.Object3D = function ( editor ) {
 
 	// position
 
-	var objectPositionRow = new UI.Panel();
+	var objectPositionRow = new UI.Row();
 	var objectPositionX = new UI.Number().setWidth( '50px' ).onChange( update );
 	var objectPositionY = new UI.Number().setWidth( '50px' ).onChange( update );
 	var objectPositionZ = new UI.Number().setWidth( '50px' ).onChange( update );
@@ -121,7 +120,7 @@ Sidebar.Object3D = function ( editor ) {
 
 	// rotation
 
-	var objectRotationRow = new UI.Panel();
+	var objectRotationRow = new UI.Row();
 	var objectRotationX = new UI.Number().setWidth( '50px' ).onChange( update );
 	var objectRotationY = new UI.Number().setWidth( '50px' ).onChange( update );
 	var objectRotationZ = new UI.Number().setWidth( '50px' ).onChange( update );
@@ -133,7 +132,7 @@ Sidebar.Object3D = function ( editor ) {
 
 	// scale
 
-	var objectScaleRow = new UI.Panel();
+	var objectScaleRow = new UI.Row();
 	var objectScaleLock = new UI.Checkbox( true ).setPosition( 'absolute' ).setLeft( '75px' );
 	var objectScaleX = new UI.Number( 1 ).setRange( 0.01, Infinity ).setWidth( '50px' ).onChange( updateScaleX );
 	var objectScaleY = new UI.Number( 1 ).setRange( 0.01, Infinity ).setWidth( '50px' ).onChange( updateScaleY );
@@ -147,7 +146,7 @@ Sidebar.Object3D = function ( editor ) {
 
 	// fov
 
-	var objectFovRow = new UI.Panel();
+	var objectFovRow = new UI.Row();
 	var objectFov = new UI.Number().onChange( update );
 
 	objectFovRow.add( new UI.Text( 'Fov' ).setWidth( '90px' ) );
@@ -157,7 +156,7 @@ Sidebar.Object3D = function ( editor ) {
 
 	// near
 
-	var objectNearRow = new UI.Panel();
+	var objectNearRow = new UI.Row();
 	var objectNear = new UI.Number().onChange( update );
 
 	objectNearRow.add( new UI.Text( 'Near' ).setWidth( '90px' ) );
@@ -167,7 +166,7 @@ Sidebar.Object3D = function ( editor ) {
 
 	// far
 
-	var objectFarRow = new UI.Panel();
+	var objectFarRow = new UI.Row();
 	var objectFar = new UI.Number().onChange( update );
 
 	objectFarRow.add( new UI.Text( 'Far' ).setWidth( '90px' ) );
@@ -177,7 +176,7 @@ Sidebar.Object3D = function ( editor ) {
 
 	// intensity
 
-	var objectIntensityRow = new UI.Panel();
+	var objectIntensityRow = new UI.Row();
 	var objectIntensity = new UI.Number().setRange( 0, Infinity ).onChange( update );
 
 	objectIntensityRow.add( new UI.Text( 'Intensity' ).setWidth( '90px' ) );
@@ -187,7 +186,7 @@ Sidebar.Object3D = function ( editor ) {
 
 	// color
 
-	var objectColorRow = new UI.Panel();
+	var objectColorRow = new UI.Row();
 	var objectColor = new UI.Color().onChange( update );
 
 	objectColorRow.add( new UI.Text( 'Color' ).setWidth( '90px' ) );
@@ -197,7 +196,7 @@ Sidebar.Object3D = function ( editor ) {
 
 	// ground color
 
-	var objectGroundColorRow = new UI.Panel();
+	var objectGroundColorRow = new UI.Row();
 	var objectGroundColor = new UI.Color().onChange( update );
 
 	objectGroundColorRow.add( new UI.Text( 'Ground color' ).setWidth( '90px' ) );
@@ -207,7 +206,7 @@ Sidebar.Object3D = function ( editor ) {
 
 	// distance
 
-	var objectDistanceRow = new UI.Panel();
+	var objectDistanceRow = new UI.Row();
 	var objectDistance = new UI.Number().setRange( 0, Infinity ).onChange( update );
 
 	objectDistanceRow.add( new UI.Text( 'Distance' ).setWidth( '90px' ) );
@@ -217,7 +216,7 @@ Sidebar.Object3D = function ( editor ) {
 
 	// angle
 
-	var objectAngleRow = new UI.Panel();
+	var objectAngleRow = new UI.Row();
 	var objectAngle = new UI.Number().setPrecision( 3 ).setRange( 0, Math.PI / 2 ).onChange( update );
 
 	objectAngleRow.add( new UI.Text( 'Angle' ).setWidth( '90px' ) );
@@ -227,7 +226,7 @@ Sidebar.Object3D = function ( editor ) {
 
 	// exponent
 
-	var objectExponentRow = new UI.Panel();
+	var objectExponentRow = new UI.Row();
 	var objectExponent = new UI.Number().setRange( 0, Infinity ).onChange( update );
 
 	objectExponentRow.add( new UI.Text( 'Exponent' ).setWidth( '90px' ) );
@@ -237,7 +236,7 @@ Sidebar.Object3D = function ( editor ) {
 
 	// decay
 
-	var objectDecayRow = new UI.Panel();
+	var objectDecayRow = new UI.Row();
 	var objectDecay = new UI.Number().setRange( 0, Infinity ).onChange( update );
 
 	objectDecayRow.add( new UI.Text( 'Decay' ).setWidth( '90px' ) );
@@ -247,31 +246,21 @@ Sidebar.Object3D = function ( editor ) {
 
 	// shadow
 
-	var objectShadowRow = new UI.Panel();
+	var objectShadowRow = new UI.Row();
 
 	objectShadowRow.add( new UI.Text( 'Shadow' ).setWidth( '90px' ) );
 
-	var objectCastShadowSpan = new UI.Span().setMarginRight( '10px' );
-	var objectCastShadow = new UI.Checkbox().onChange( update );
-
-	objectCastShadowSpan.add( objectCastShadow );
-	objectCastShadowSpan.add( new UI.Text( 'cast' ).setMarginLeft( '3px' ) );
-
-	objectShadowRow.add( objectCastShadowSpan );
+	var objectCastShadow = new UI.THREE.Boolean( false, 'cast' ).onChange( update );
+	objectShadowRow.add( objectCastShadow );
 
-	var objectReceiveShadowSpan = new UI.Span();
-	var objectReceiveShadow = new UI.Checkbox().onChange( update );
-
-	objectReceiveShadowSpan.add( objectReceiveShadow );
-	objectReceiveShadowSpan.add( new UI.Text( 'receive' ).setMarginLeft( '3px' ) );
-
-	objectShadowRow.add( objectReceiveShadowSpan );
+	var objectReceiveShadow = new UI.THREE.Boolean( false, 'receive' ).onChange( update );
+	objectShadowRow.add( objectReceiveShadow );
 
 	container.add( objectShadowRow );
 
 	// visible
 
-	var objectVisibleRow = new UI.Panel();
+	var objectVisibleRow = new UI.Row();
 	var objectVisible = new UI.Checkbox().onChange( update );
 
 	objectVisibleRow.add( new UI.Text( 'Visible' ).setWidth( '90px' ) );
@@ -283,7 +272,7 @@ Sidebar.Object3D = function ( editor ) {
 
 	var timeout;
 
-	var objectUserDataRow = new UI.Panel();
+	var objectUserDataRow = new UI.Row();
 	var objectUserData = new UI.TextArea().setWidth( '150px' ).setHeight( '40px' ).setFontSize( '12px' ).onChange( update );
 	objectUserData.onKeyUp( function () {
 
@@ -375,110 +364,126 @@ Sidebar.Object3D = function ( editor ) {
 
 				if ( object.parent.id !== newParentId && object.id !== newParentId ) {
 
-					editor.moveObject( object, editor.scene.getObjectById( newParentId ) );
+					editor.execute( new MoveObjectCommand( 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 ) {
+
+				editor.execute( new SetPositionCommand( object, newPosition ) );
+
+			}
+
+			var newRotation = new THREE.Euler( objectRotationX.getValue(), objectRotationY.getValue(), objectRotationZ.getValue() );
+			if ( object.rotation.toVector3().distanceTo( newRotation.toVector3() ) >= 0.01 ) {
+
+				editor.execute( new SetRotationCommand( object, newRotation ) );
+
+			}
+
+			var newScale = new THREE.Vector3( objectScaleX.getValue(), objectScaleY.getValue(), objectScaleZ.getValue() );
+			if ( object.scale.distanceTo( newScale ) >= 0.01 ) {
 
-			object.rotation.x = objectRotationX.getValue();
-			object.rotation.y = objectRotationY.getValue();
-			object.rotation.z = objectRotationZ.getValue();
+				editor.execute( new SetScaleCommand( object, newScale ) );
 
-			object.scale.x = objectScaleX.getValue();
-			object.scale.y = objectScaleY.getValue();
-			object.scale.z = objectScaleZ.getValue();
+			}
 
-			if ( object.fov !== undefined ) {
+			if ( object.fov !== undefined && Math.abs( object.fov - objectFov.getValue() ) >= 0.01 ) {
 
-				object.fov = objectFov.getValue();
+				editor.execute( new SetValueCommand( 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 SetValueCommand( 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 SetValueCommand( 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 SetValueCommand( 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 SetColorCommand( 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 SetColorCommand( 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 SetValueCommand( 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 SetValueCommand( 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 SetValueCommand( 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 SetValueCommand( object, 'decay', objectDecay.getValue() ) );
 
 			}
 
-			if ( object.castShadow !== undefined ) {
+			if ( object.visible !== objectVisible.getValue() ) {
 
-				object.castShadow = objectCastShadow.getValue();
+				editor.execute( new SetValueCommand( object, 'visible', objectVisible.getValue() ) );
 
 			}
 
-			if ( object.receiveShadow !== undefined ) {
+			if ( object.castShadow !== objectCastShadow.getValue() ) {
+
+				editor.execute( new SetValueCommand( object, 'castShadow', objectCastShadow.getValue() ) );
+
+			}
 
-				var value = objectReceiveShadow.getValue();
+			if ( object.receiveShadow !== undefined ) {
 
-				if ( value !== object.receiveShadow ) {
+				if ( object.receiveShadow !== objectReceiveShadow.getValue() ) {
 
-					object.receiveShadow = value;
+					editor.execute( new SetValueCommand( object, 'receiveShadow', objectReceiveShadow.getValue() ) );
 					object.material.needsUpdate = true;
 
 				}
 
 			}
 
-			object.visible = objectVisible.getValue();
-
 			try {
 
-				object.userData = JSON.parse( objectUserData.getValue() );
+				var userData = JSON.parse( objectUserData.getValue() );
+				if ( JSON.stringify( object.userData ) != JSON.stringify( userData ) ) {
+
+					editor.execute( new SetValueCommand( object, 'userData', userData ) );
+
+				}
 
 			} catch ( exception ) {
 
@@ -486,8 +491,6 @@ Sidebar.Object3D = function ( editor ) {
 
 			}
 
-			signals.objectChanged.dispatch( object );
-
 		}
 
 	}
@@ -507,7 +510,7 @@ Sidebar.Object3D = function ( editor ) {
 			'exponent' : objectExponentRow,
 			'decay' : objectDecayRow,
 			'castShadow' : objectShadowRow,
-			'receiveShadow' : objectReceiveShadowSpan
+			'receiveShadow' : objectReceiveShadow
 		};
 
 		for ( var property in properties ) {
@@ -579,6 +582,14 @@ Sidebar.Object3D = function ( editor ) {
 
 	} );
 
+	signals.refreshSidebarObject3D.add( function ( object ) {
+
+		if ( object !== editor.selected ) return;
+
+		updateUI( object );
+
+	} );
+
 	function updateUI( object ) {
 
 		objectType.setValue( object.type );
@@ -699,4 +710,4 @@ Sidebar.Object3D = function ( editor ) {
 
 	return container;
 
-}
+};

+ 13 - 37
editor/js/Sidebar.Project.js

@@ -17,16 +17,9 @@ Sidebar.Project = function ( editor ) {
 
 	};
 
-	var container = new UI.CollapsiblePanel();
-	container.setCollapsed( config.getKey( 'ui/sidebar/project/collapsed' ) );
-	container.onCollapsedChange( function ( boolean ) {
-
-		config.setKey( 'ui/sidebar/project/collapsed', boolean );
-
-	} );
-
-	container.addStatic( new UI.Text( 'PROJECT' ) );
-	container.add( new UI.Break() );
+	var container = new UI.Panel();
+	container.setBorderTop( '0' );
+	container.setPaddingTop( '20px' );
 
 	// class
 
@@ -40,22 +33,13 @@ Sidebar.Project = function ( editor ) {
 
 	}
 
-	var rendererTypeRow = new UI.Panel();
+	var rendererTypeRow = new UI.Row();
 	var rendererType = new UI.Select().setOptions( options ).setWidth( '150px' ).onChange( function () {
 
 		var value = this.getValue();
 
-		if ( value === 'WebGLRenderer' ) {
-
-			rendererPropertiesRow.setDisplay( '' );
-
-		} else {
-
-			rendererPropertiesRow.setDisplay( 'none' );
-
-		}
-
 		config.setKey( 'project/renderer', value );
+
 		updateRenderer();
 
 	} );
@@ -73,42 +57,32 @@ Sidebar.Project = function ( editor ) {
 
 	// antialiasing
 
-	var rendererPropertiesRow = new UI.Panel();
+	var rendererPropertiesRow = new UI.Row();
 	rendererPropertiesRow.add( new UI.Text( '' ).setWidth( '90px' ) );
 
-	var rendererAntialiasSpan = new UI.Span().setMarginRight( '10px' );
-	var rendererAntialias = new UI.Checkbox( config.getKey( 'project/renderer/antialias' ) ).setLeft( '100px' ).onChange( function () {
+	var rendererAntialias = new UI.THREE.Boolean( config.getKey( 'project/renderer/antialias' ), 'antialias' ).onChange( function () {
 
 		config.setKey( 'project/renderer/antialias', this.getValue() );
 		updateRenderer();
 
 	} );
-
-	rendererAntialiasSpan.add( rendererAntialias );
-	rendererAntialiasSpan.add( new UI.Text( 'antialias' ).setMarginLeft( '3px' ) );
-
-	rendererPropertiesRow.add( rendererAntialiasSpan );
+	rendererPropertiesRow.add( rendererAntialias );
 
 	// shadow
 
-	var rendererShadowsSpan = new UI.Span();
-	var rendererShadows = new UI.Checkbox( config.getKey( 'project/renderer/shadows' ) ).setLeft( '100px' ).onChange( function () {
+	var rendererShadows = new UI.THREE.Boolean( config.getKey( 'project/renderer/shadows' ), 'shadows' ).onChange( function () {
 
 		config.setKey( 'project/renderer/shadows', this.getValue() );
 		updateRenderer();
 
 	} );
-
-	rendererShadowsSpan.add( rendererShadows );
-	rendererShadowsSpan.add( new UI.Text( 'shadows' ).setMarginLeft( '3px' ) );
-
-	rendererPropertiesRow.add( rendererShadowsSpan );
+	rendererPropertiesRow.add( rendererShadows );
 
 	container.add( rendererPropertiesRow );
 
 	// VR
 
-	var vrRow = new UI.Panel();
+	var vrRow = new UI.Row();
 	var vr = new UI.Checkbox( config.getKey( 'project/vr' ) ).setLeft( '100px' ).onChange( function () {
 
 		config.setKey( 'project/vr', this.getValue() );
@@ -137,6 +111,8 @@ Sidebar.Project = function ( editor ) {
 
 		}
 
+		rendererPropertiesRow.setDisplay( type === 'WebGLRenderer' ? '' : 'none' );
+
 		var renderer = new rendererTypes[ type ]( { antialias: antialias } );
 		if ( shadows && renderer.shadowMap ) renderer.shadowMap.enabled = true;
 		signals.rendererChanged.dispatch( renderer );

+ 76 - 0
editor/js/Sidebar.Properties.js

@@ -0,0 +1,76 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+Sidebar.Properties = function ( editor ) {
+
+	var signals = editor.signals;
+
+	var container = new UI.Span();
+
+	var objectTab = new UI.Text( 'OBJECT' ).onClick( onClick );
+	var geometryTab = new UI.Text( 'GEOMETRY' ).onClick( onClick );
+	var materialTab = new UI.Text( 'MATERIAL' ).onClick( onClick );
+
+	var tabs = new UI.Div();
+	tabs.setId( 'tabs' );
+	tabs.add( objectTab, geometryTab, materialTab );
+	container.add( tabs );
+
+	function onClick( event ) {
+
+		select( event.target.textContent );
+
+	}
+
+	//
+
+	var object = new UI.Span().add(
+		new Sidebar.Object( editor )
+	);
+	container.add( object );
+
+	var geometry = new UI.Span().add(
+		new Sidebar.Geometry( editor )
+	);
+	container.add( geometry );
+
+	var material = new UI.Span().add(
+		new Sidebar.Material( editor )
+	);
+	container.add( material );
+
+	//
+
+	function select( section ) {
+
+		objectTab.setClass( '' );
+		geometryTab.setClass( '' );
+		materialTab.setClass( '' );
+
+		object.setDisplay( 'none' );
+		geometry.setDisplay( 'none' );
+		material.setDisplay( 'none' );
+
+		switch ( section ) {
+			case 'OBJECT':
+				objectTab.setClass( 'selected' );
+				object.setDisplay( '' );
+				break;
+			case 'GEOMETRY':
+				geometryTab.setClass( 'selected' );
+				geometry.setDisplay( '' );
+				break;
+			case 'MATERIAL':
+				materialTab.setClass( 'selected' );
+				material.setDisplay( '' );
+				break;
+		}
+
+	}
+
+	select( 'OBJECT' );
+
+	return container;
+
+};

+ 25 - 17
editor/js/Sidebar.Scene.js

@@ -6,20 +6,14 @@ Sidebar.Scene = function ( editor ) {
 
 	var signals = editor.signals;
 
-	var container = new UI.CollapsiblePanel();
-	container.setCollapsed( editor.config.getKey( 'ui/sidebar/scene/collapsed' ) );
-	container.onCollapsedChange( function ( boolean ) {
-
-		editor.config.setKey( 'ui/sidebar/scene/collapsed', boolean );
-
-	} );
-
-	container.addStatic( new UI.Text( 'SCENE' ) );
-	container.add( new UI.Break() );
+	var container = new UI.Panel();
+	container.setBorderTop( '0' );
+	container.setPaddingTop( '20px' );
 
 	var ignoreObjectSelectedSignal = false;
 
 	var outliner = new UI.Outliner( editor );
+	outliner.setId( 'outliner' );
 	outliner.onChange( function () {
 
 		ignoreObjectSelectedSignal = true;
@@ -49,7 +43,7 @@ Sidebar.Scene = function ( editor ) {
 
 	};
 
-	var fogTypeRow = new UI.Panel();
+	var fogTypeRow = new UI.Row();
 	var fogType = new UI.Select().setOptions( {
 
 		'None': 'None',
@@ -73,7 +67,7 @@ Sidebar.Scene = function ( editor ) {
 
 	// fog color
 
-	var fogColorRow = new UI.Panel();
+	var fogColorRow = new UI.Row();
 	fogColorRow.setDisplay( 'none' );
 
 	var fogColor = new UI.Color().setValue( '#aaaaaa' )
@@ -90,7 +84,7 @@ Sidebar.Scene = function ( editor ) {
 
 	// fog near
 
-	var fogNearRow = new UI.Panel();
+	var fogNearRow = new UI.Row();
 	fogNearRow.setDisplay( 'none' );
 
 	var fogNear = new UI.Number( 1 ).setWidth( '60px' ).setRange( 0, Infinity ).onChange( updateFogParameters );
@@ -100,7 +94,7 @@ Sidebar.Scene = function ( editor ) {
 
 	container.add( fogNearRow );
 
-	var fogFarRow = new UI.Panel();
+	var fogFarRow = new UI.Row();
 	fogFarRow.setDisplay( 'none' );
 
 	// fog far
@@ -114,7 +108,7 @@ Sidebar.Scene = function ( editor ) {
 
 	// fog density
 
-	var fogDensityRow = new UI.Panel();
+	var fogDensityRow = new UI.Row();
 	fogDensityRow.setDisplay( 'none' );
 
 	var fogDensity = new UI.Number( 0.00025 ).setWidth( '60px' ).setRange( 0, 0.1 ).setPrecision( 5 ).onChange( updateFogParameters );
@@ -133,8 +127,20 @@ Sidebar.Scene = function ( editor ) {
 
 		var options = [];
 
-		// options.push( { value: camera.id, html: '<span class="type ' + camera.type + '"></span> ' + camera.name } );
-		options.push( { static: true, value: scene.id, html: '<span class="type ' + scene.type + '"></span> ' + scene.name } );
+		options.push( { static: true, value: camera.id, html: '<span class="type ' + camera.type + '"></span> ' + camera.name } );
+		options.push( { static: true, value: scene.id, html: '<span class="type ' + scene.type + '"></span> ' + scene.name + getScript( scene.uuid ) } );
+
+		function getScript( uuid ) {
+
+			if ( editor.scripts[ uuid ] !== undefined ) {
+
+				return ' <span class="type Script"></span>';
+
+			}
+
+			return '';
+
+		}
 
 		( function addObjects( objects, pad ) {
 
@@ -154,6 +160,8 @@ Sidebar.Scene = function ( editor ) {
 
 				}
 
+				html += getScript( object.uuid );
+
 				options.push( { value: object.id, html: html } );
 
 				addObjects( object.children, pad + '&nbsp;&nbsp;&nbsp;' );

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

@@ -20,14 +20,14 @@ Sidebar.Script = function ( editor ) {
 
 	//
 
-	var scriptsContainer = new UI.Panel();
+	var scriptsContainer = new UI.Row();
 	container.add( scriptsContainer );
 
 	var newScript = new UI.Button( 'New' );
 	newScript.onClick( function () {
 
 		var script = { name: '', source: 'function update( event ) {}' };
-		editor.addScript( editor.selected, script );
+		editor.execute( new AddScriptCommand( 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 SetScriptValueCommand( editor.selected, script, 'name', this.getValue() ) );
 
 					} );
 					scriptsContainer.add( name );
@@ -85,7 +83,7 @@ Sidebar.Script = function ( editor ) {
 
 						if ( confirm( 'Are you sure?' ) ) {
 
-							editor.removeScript( editor.selected, script );
+							editor.execute( new RemoveScriptCommand( 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;
 

+ 47 - 0
editor/js/Sidebar.Settings.js

@@ -0,0 +1,47 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+Sidebar.Settings = function ( editor ) {
+
+	var config = editor.config;
+	var signals = editor.signals;
+
+	var container = new UI.Panel();
+	container.setBorderTop( '0' );
+	container.setPaddingTop( '20px' );
+
+	// class
+
+	var options = {
+		'css/light.css': 'light',
+		'css/dark.css': 'dark'
+	};
+
+	var themeRow = new UI.Row();
+	var theme = new UI.Select().setWidth( '150px' );
+	theme.setOptions( options );
+
+	if ( config.getKey( 'theme' ) !== undefined ) {
+
+		theme.setValue( config.getKey( 'theme' ) );
+
+	}
+
+	theme.onChange( function () {
+
+		var value = this.getValue();
+
+		editor.setTheme( value );
+		editor.config.setKey( 'theme', value );
+
+	} );
+
+	themeRow.add( new UI.Text( 'Theme' ).setWidth( '90px' ) );
+	themeRow.add( theme );
+
+	container.add( themeRow );
+
+	return container;
+
+};

+ 68 - 7
editor/js/Sidebar.js

@@ -7,13 +7,74 @@ var Sidebar = function ( editor ) {
 	var container = new UI.Panel();
 	container.setId( 'sidebar' );
 
-	container.add( new Sidebar.Project( editor ) );
-	container.add( new Sidebar.Scene( editor ) );
-	container.add( new Sidebar.Object3D( editor ) );
-	container.add( new Sidebar.Geometry( editor ) );
-	container.add( new Sidebar.Material( editor ) );
-	container.add( new Sidebar.Animation( editor ) );
-	container.add( new Sidebar.Script( editor ) );
+	//
+
+	var sceneTab = new UI.Text( 'SCENE' ).onClick( onClick );
+	var projectTab = new UI.Text( 'PROJECT' ).onClick( onClick );
+	var settingsTab = new UI.Text( 'SETTINGS' ).onClick( onClick );
+
+	var tabs = new UI.Div();
+	tabs.setId( 'tabs' );
+	tabs.add( sceneTab, projectTab, settingsTab );
+	container.add( tabs );
+
+	function onClick( event ) {
+
+		select( event.target.textContent );
+
+	}
+
+	//
+
+	var scene = new UI.Span().add(
+		new Sidebar.Scene( editor ),
+		new Sidebar.Properties( editor ),
+		new Sidebar.Animation( editor ),
+		new Sidebar.Script( editor )
+	);
+	container.add( scene );
+
+	var project = new UI.Span().add(
+		new Sidebar.Project( editor )
+	);
+	container.add( project );
+
+	var settings = new UI.Span().add(
+		new Sidebar.Settings( editor ),
+		new Sidebar.History( editor )
+	);
+	container.add( settings );
+
+	//
+
+	function select( section ) {
+
+		sceneTab.setClass( '' );
+		projectTab.setClass( '' );
+		settingsTab.setClass( '' );
+
+		scene.setDisplay( 'none' );
+		project.setDisplay( 'none' );
+		settings.setDisplay( 'none' );
+
+		switch ( section ) {
+			case 'SCENE':
+				sceneTab.setClass( 'selected' );
+				scene.setDisplay( '' );
+				break;
+			case 'PROJECT':
+				projectTab.setClass( 'selected' );
+				project.setDisplay( '' );
+				break;
+			case 'SETTINGS':
+				settingsTab.setClass( 'selected' );
+				settings.setDisplay( '' );
+				break;
+		}
+
+	}
+
+	select( 'SCENE' );
 
 	return container;
 

+ 4 - 7
editor/js/Toolbar.js

@@ -38,20 +38,17 @@ var Toolbar = function ( editor ) {
 	// grid
 
 	var grid = new UI.Number( 25 ).setWidth( '40px' ).onChange( update );
-	buttons.add( new UI.Text( 'Grid: ' ) );
+	buttons.add( new UI.Text( 'grid: ' ) );
 	buttons.add( grid );
 
-	var snap = new UI.Checkbox( false ).onChange( update ).setMarginLeft( '10px' );
+	var snap = new UI.THREE.Boolean( false, 'snap' ).onChange( update );
 	buttons.add( snap );
-	buttons.add( new UI.Text( 'snap' ).setMarginLeft( '3px' ) );
 
-	var local = new UI.Checkbox( false ).onChange( update ).setMarginLeft( '10px' );
+	var local = new UI.THREE.Boolean( false, 'local' ).onChange( update );
 	buttons.add( local );
-	buttons.add( new UI.Text( 'local' ).setMarginLeft( '3px' ) );
 
-	var showGrid = new UI.Checkbox().onChange( update ).setValue( true ).setMarginLeft( '10px' );
+	var showGrid = new UI.THREE.Boolean( true, 'show' ).onChange( update );
 	buttons.add( showGrid );
-	buttons.add( new UI.Text( 'show' ).setMarginLeft( '3px' ) );
 
 	function update() {
 

+ 67 - 68
editor/js/Viewport.js

@@ -19,7 +19,7 @@ var Viewport = function ( editor ) {
 
 	// helpers
 
-	var grid = new THREE.GridHelper( 500, 25 );
+	var grid = new THREE.GridHelper( 30, 1 );
 	sceneHelpers.add( grid );
 
 	//
@@ -34,7 +34,9 @@ var Viewport = function ( editor ) {
 	selectionBox.visible = false;
 	sceneHelpers.add( selectionBox );
 
-	var matrix = new THREE.Matrix4();
+	var objectPositionOnDown = null;
+	var objectRotationOnDown = null;
+	var objectScaleOnDown = null;
 
 	var transformControls = new THREE.TransformControls( camera, container.dom );
 	transformControls.addEventListener( 'change', function () {
@@ -51,6 +53,8 @@ var Viewport = function ( editor ) {
 
 			}
 
+			signals.refreshSidebarObject3D.dispatch( object );
+
 		}
 
 		render();
@@ -60,7 +64,9 @@ var Viewport = function ( editor ) {
 
 		var object = transformControls.object;
 
-		matrix.copy( object.matrix );
+		objectPositionOnDown = object.position.clone();
+		objectRotationOnDown = object.rotation.clone();
+		objectScaleOnDown = object.scale.clone();
 
 		controls.enabled = false;
 
@@ -69,26 +75,44 @@ 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 SetPositionCommand( object, object.position, objectPositionOnDown ) );
+
+					}
+
+					break;
+
+				case 'rotate':
+
+					if ( ! objectRotationOnDown.equals( object.rotation ) ) {
+
+						editor.execute( new SetRotationCommand( object, object.rotation, objectRotationOnDown ) );
+
+					}
+
+					break;
+
+				case 'scale':
 
-			( function ( matrix1, matrix2 ) {
+					if ( ! objectScaleOnDown.equals( object.scale ) ) {
+
+						editor.execute( new SetScaleCommand( object, object.scale, objectScaleOnDown ) );
 
-				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 );
 					}
-				);
 
-			} )( matrix.clone(), object.matrix.clone() );
+					break;
+
+			}
 
 		}
 
-		signals.objectChanged.dispatch( object );
 		controls.enabled = true;
 
 	} );
@@ -118,7 +142,7 @@ var Viewport = function ( editor ) {
 
 		return raycaster.intersectObjects( objects );
 
-	};
+	}
 
 	var onDownPosition = new THREE.Vector2();
 	var onUpPosition = new THREE.Vector2();
@@ -129,11 +153,11 @@ var Viewport = function ( editor ) {
 		var rect = dom.getBoundingClientRect();
 		return [ ( x - rect.left ) / rect.width, ( y - rect.top ) / rect.height ];
 
-	};
+	}
 
 	function handleClick() {
 
-		if ( onDownPosition.distanceTo( onUpPosition ) == 0 ) {
+		if ( onDownPosition.distanceTo( onUpPosition ) === 0 ) {
 
 			var intersects = getIntersects( onUpPosition, objects );
 
@@ -163,7 +187,7 @@ var Viewport = function ( editor ) {
 
 		}
 
-	};
+	}
 
 	function onMouseDown( event ) {
 
@@ -174,7 +198,7 @@ var Viewport = function ( editor ) {
 
 		document.addEventListener( 'mouseup', onMouseUp, false );
 
-	};
+	}
 
 	function onMouseUp( event ) {
 
@@ -185,7 +209,7 @@ var Viewport = function ( editor ) {
 
 		document.removeEventListener( 'mouseup', onMouseUp, false );
 
-	};
+	}
 
 	function onTouchStart( event ) {
 
@@ -196,7 +220,7 @@ var Viewport = function ( editor ) {
 
 		document.addEventListener( 'touchend', onTouchEnd, false );
 
-	};
+	}
 
 	function onTouchEnd( event ) {
 
@@ -209,7 +233,7 @@ var Viewport = function ( editor ) {
 
 		document.removeEventListener( 'touchend', onTouchEnd, false );
 
-	};
+	}
 
 	function onDoubleClick( event ) {
 
@@ -226,7 +250,7 @@ var Viewport = function ( editor ) {
 
 		}
 
-	};
+	}
 
 	container.dom.addEventListener( 'mousedown', onMouseDown, false );
 	container.dom.addEventListener( 'touchstart', onTouchStart, false );
@@ -358,9 +382,13 @@ var Viewport = function ( editor ) {
 
 	} );
 
-	signals.geometryChanged.add( function ( geometry ) {
+	signals.geometryChanged.add( function ( object ) {
+
+		if ( object !== null ) {
+
+			selectionBox.update( object );
 
-		selectionBox.update( editor.selected );
+		}
 
 		render();
 
@@ -368,24 +396,22 @@ var Viewport = function ( editor ) {
 
 	signals.objectAdded.add( function ( object ) {
 
-		var materialsNeedUpdate = false;
-
 		object.traverse( function ( child ) {
 
-			if ( child instanceof THREE.Light ) materialsNeedUpdate = true;
-
 			objects.push( child );
 
 		} );
 
-		if ( materialsNeedUpdate === true ) updateMaterials();
-
 	} );
 
 	signals.objectChanged.add( function ( object ) {
 
-		selectionBox.update( object );
-		transformControls.update();
+		if ( editor.selected === object ) {
+
+			selectionBox.update( object );
+			transformControls.update();
+
+		}
 
 		if ( object instanceof THREE.PerspectiveCamera ) {
 
@@ -405,18 +431,12 @@ var Viewport = function ( editor ) {
 
 	signals.objectRemoved.add( function ( object ) {
 
-		var materialsNeedUpdate = false;
-
 		object.traverse( function ( child ) {
 
-			if ( child instanceof THREE.Light ) materialsNeedUpdate = true;
-
 			objects.splice( objects.indexOf( child ), 1 );
 
 		} );
 
-		if ( materialsNeedUpdate === true ) updateMaterials();
-
 	} );
 
 	signals.helperAdded.add( function ( object ) {
@@ -455,8 +475,6 @@ var Viewport = function ( editor ) {
 
 			}
 
-			updateMaterials();
-
 			oldFogType = fogType;
 
 		}
@@ -489,6 +507,11 @@ var Viewport = function ( editor ) {
 
 	signals.windowResize.add( function () {
 
+		// TODO: Move this out?
+
+		editor.DEFAULT_CAMERA.aspect = container.dom.offsetWidth / container.dom.offsetHeight;
+		editor.DEFAULT_CAMERA.updateProjectionMatrix();
+
 		camera.aspect = container.dom.offsetWidth / container.dom.offsetHeight;
 		camera.updateProjectionMatrix();
 
@@ -513,30 +536,6 @@ var Viewport = function ( editor ) {
 
 	//
 
-	function updateMaterials() {
-
-		editor.scene.traverse( function ( node ) {
-
-			if ( node.material ) {
-
-				node.material.needsUpdate = true;
-
-				if ( node.material instanceof THREE.MeshFaceMaterial ) {
-
-					for ( var i = 0; i < node.material.materials.length; i ++ ) {
-
-						node.material.materials[ i ].needsUpdate = true;
-
-					}
-
-				}
-
-			}
-
-		} );
-
-	}
-
 	function updateFog( root ) {
 
 		if ( root.fog ) {
@@ -601,4 +600,4 @@ var Viewport = function ( editor ) {
 
 	return container;
 
-}
+};

+ 66 - 0
editor/js/commands/AddObjectCommand.js

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

+ 76 - 0
editor/js/commands/AddScriptCommand.js

@@ -0,0 +1,76 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param object THREE.Object3D
+ * @param script javascript object
+ * @constructor
+ */
+
+var AddScriptCommand = function ( object, script ) {
+
+	Command.call( this );
+
+	this.type = 'AddScriptCommand';
+	this.name = 'Add Script';
+
+	this.object = object;
+	this.script = script;
+
+};
+
+AddScriptCommand.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 = Command.prototype.toJSON.call( this );
+
+		output.objectUuid = this.object.uuid;
+		output.script = this.script;
+
+		return output;
+
+	},
+
+	fromJSON: function ( json ) {
+
+		Command.prototype.fromJSON.call( this, json );
+
+		this.script = json.script;
+		this.object = this.editor.objectByUuid( json.objectUuid );
+
+	}
+
+};

+ 107 - 0
editor/js/commands/MoveObjectCommand.js

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

+ 85 - 0
editor/js/commands/MultiCmdsCommand.js

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

+ 103 - 0
editor/js/commands/RemoveObjectCommand.js

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

+ 81 - 0
editor/js/commands/RemoveScriptCommand.js

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

+ 74 - 0
editor/js/commands/SetColorCommand.js

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

+ 86 - 0
editor/js/commands/SetGeometryCommand.js

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

+ 71 - 0
editor/js/commands/SetGeometryValueCommand.js

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

+ 74 - 0
editor/js/commands/SetMaterialColorCommand.js

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

+ 74 - 0
editor/js/commands/SetMaterialCommand.js

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

+ 125 - 0
editor/js/commands/SetMaterialMapCommand.js

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

+ 76 - 0
editor/js/commands/SetMaterialValueCommand.js

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

+ 83 - 0
editor/js/commands/SetPositionCommand.js

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

+ 84 - 0
editor/js/commands/SetRotationCommand.js

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

+ 84 - 0
editor/js/commands/SetScaleCommand.js

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

+ 100 - 0
editor/js/commands/SetSceneCommand.js

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

+ 88 - 0
editor/js/commands/SetScriptValueCommand.js

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

+ 71 - 0
editor/js/commands/SetUuidCommand.js

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

+ 76 - 0
editor/js/commands/SetValueCommand.js

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

+ 20 - 9
editor/js/libs/app.js

@@ -48,27 +48,36 @@ var APP = {
 				update: []
 			};
 
-			var scriptWrapParams = 'player,renderer,scene';
+			var scriptWrapParams = 'player,renderer,scene,camera';
 			var scriptWrapResultObj = {};
+
 			for ( var eventKey in events ) {
+
 				scriptWrapParams += ',' + eventKey;
 				scriptWrapResultObj[ eventKey ] = eventKey;
+
 			}
-			var scriptWrapResult =
-					JSON.stringify( scriptWrapResultObj ).replace( /\"/g, '' );
+
+			var scriptWrapResult = JSON.stringify( scriptWrapResultObj ).replace( /\"/g, '' );
 
 			for ( var uuid in json.scripts ) {
 
 				var object = scene.getObjectByProperty( 'uuid', uuid, true );
 
+				if ( object === undefined ) {
+
+					console.warn( 'APP.Player: Script without object.', uuid );
+					continue;
+
+				}
+
 				var scripts = json.scripts[ uuid ];
 
 				for ( var i = 0; i < scripts.length; i ++ ) {
 
 					var script = scripts[ i ];
 
-					var functions = ( new Function( scriptWrapParams,
-							script.source + '\nreturn ' + scriptWrapResult+ ';' ).bind( object ) )( this, renderer, scene );
+					var functions = ( new Function( scriptWrapParams, script.source + '\nreturn ' + scriptWrapResult + ';' ).bind( object ) )( this, renderer, scene, camera );
 
 					for ( var name in functions ) {
 
@@ -76,7 +85,7 @@ var APP = {
 
 						if ( events[ name ] === undefined ) {
 
-							console.warn( 'APP.Player: event type not supported (', name, ')' );
+							console.warn( 'APP.Player: Event type not supported (', name, ')' );
 							continue;
 
 						}
@@ -141,7 +150,7 @@ var APP = {
 
 			scene = value;
 
-		},
+		};
 
 		this.setSize = function ( width, height ) {
 
@@ -165,7 +174,7 @@ var APP = {
 
 					array[ i ]( event );
 
-				} catch (e) {
+				} catch ( e ) {
 
 					console.error( ( e.message || e ), ( e.stack || "" ) );
 
@@ -212,7 +221,8 @@ var APP = {
 			dispatch( events.start, arguments );
 
 			request = requestAnimationFrame( animate );
-			prevTime = ( window.performance || Date ).now();
+			prevTime = performance.now();
+
 		};
 
 		this.stop = function () {
@@ -229,6 +239,7 @@ var APP = {
 			dispatch( events.stop, arguments );
 
 			cancelAnimationFrame( request );
+
 		};
 
 		//

+ 38 - 0
editor/js/libs/app/index.html

@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+		<style>
+		body {
+			background-color: #000;
+			margin: 0px;
+			overflow: hidden;
+		}
+		</style>
+	</head>
+	<body ontouchstart="">
+		<script src="js/three.min.js"></script>
+		<script src="js/app.js"></script>
+		<script>
+
+			var loader = new THREE.XHRLoader();
+			loader.load( 'app.json', function ( text ) {
+
+				var player = new APP.Player();
+				player.load( JSON.parse( text ) );
+				player.setSize( window.innerWidth, window.innerHeight );
+				player.play();
+
+				document.body.appendChild( player.dom );
+
+				window.addEventListener( 'resize', function () {
+					player.setSize( window.innerWidth, window.innerHeight );
+				} );
+
+			} );
+
+		</script>
+	</body>
+</html>'

+ 100 - 50
editor/js/libs/ui.js

@@ -24,7 +24,7 @@ UI.Element.prototype = {
 
 			} else {
 
-				console.error( 'UI.Element:', argument, 'is not an instance of UI.Element.' )
+				console.error( 'UI.Element:', argument, 'is not an instance of UI.Element.' );
 
 			}
 
@@ -46,7 +46,7 @@ UI.Element.prototype = {
 
 			} else {
 
-				console.error( 'UI.Element:', argument, 'is not an instance of UI.Element.' )
+				console.error( 'UI.Element:', argument, 'is not an instance of UI.Element.' );
 
 			}
 
@@ -110,7 +110,7 @@ UI.Element.prototype = {
 
 	}
 
-}
+};
 
 // properties
 
@@ -165,6 +165,38 @@ UI.Span = function () {
 UI.Span.prototype = Object.create( UI.Element.prototype );
 UI.Span.prototype.constructor = UI.Span;
 
+// Div
+
+UI.Div = function () {
+
+	UI.Element.call( this );
+
+	this.dom = document.createElement( 'div' );
+
+	return this;
+
+};
+
+UI.Div.prototype = Object.create( UI.Element.prototype );
+UI.Div.prototype.constructor = UI.Div;
+
+// Row
+
+UI.Row = function () {
+
+	UI.Element.call( this );
+
+	var dom = document.createElement( 'div' );
+	dom.className = 'Row';
+
+	this.dom = dom;
+
+	return this;
+
+};
+
+UI.Row.prototype = Object.create( UI.Element.prototype );
+UI.Row.prototype.constructor = UI.Row;
 
 // Panel
 
@@ -198,7 +230,9 @@ UI.CollapsiblePanel = function () {
 	this.static = new UI.Panel();
 	this.static.setClass( 'Static' );
 	this.static.onClick( function () {
+
 		scope.toggle();
+
 	} );
 	this.dom.appendChild( this.static.dom );
 
@@ -263,7 +297,7 @@ UI.CollapsiblePanel.prototype.clear = function () {
 
 UI.CollapsiblePanel.prototype.toggle = function() {
 
-	this.setCollapsed( !this.isCollapsed );
+	this.setCollapsed( ! this.isCollapsed );
 
 };
 
@@ -573,9 +607,9 @@ UI.Color = function () {
 	var dom = document.createElement( 'input' );
 	dom.className = 'Color';
 	dom.style.width = '64px';
-	dom.style.height = '16px';
+	dom.style.height = '17px';
 	dom.style.border = '0px';
-	dom.style.padding = '0px';
+	dom.style.padding = '2px';
 	dom.style.backgroundColor = 'transparent';
 
 	try {
@@ -616,7 +650,7 @@ UI.Color.prototype.setValue = function ( value ) {
 
 UI.Color.prototype.setHexValue = function ( hex ) {
 
-	this.dom.value = '#' + ( '000000' + hex.toString( 16 ) ).slice( -6 );
+	this.dom.value = '#' + ( '000000' + hex.toString( 16 ) ).slice( - 6 );
 
 	return this;
 
@@ -643,6 +677,8 @@ UI.Number = function ( number ) {
 
 	}, false );
 
+	this.value = 0;
+
 	this.min = - Infinity;
 	this.max = Infinity;
 
@@ -650,6 +686,7 @@ UI.Number = function ( number ) {
 	this.step = 1;
 
 	this.dom = dom;
+
 	this.setValue( number );
 
 	var changeEvent = document.createEvent( 'HTMLEvents' );
@@ -661,40 +698,44 @@ UI.Number = function ( number ) {
 	var pointer = [ 0, 0 ];
 	var prevPointer = [ 0, 0 ];
 
-	var onMouseDown = function ( event ) {
+	function onMouseDown( event ) {
 
 		event.preventDefault();
 
 		distance = 0;
 
-		onMouseDownValue = parseFloat( dom.value );
+		onMouseDownValue = scope.value;
 
 		prevPointer = [ event.clientX, event.clientY ];
 
 		document.addEventListener( 'mousemove', onMouseMove, false );
 		document.addEventListener( 'mouseup', onMouseUp, false );
 
-	};
+	}
 
-	var onMouseMove = function ( event ) {
+	function onMouseMove( event ) {
 
-		var currentValue = dom.value;
+		var currentValue = scope.value;
 
 		pointer = [ event.clientX, event.clientY ];
 
 		distance += ( pointer[ 0 ] - prevPointer[ 0 ] ) - ( pointer[ 1 ] - prevPointer[ 1 ] );
 
-		var number = onMouseDownValue + ( distance / ( event.shiftKey ? 5 : 50 ) ) * scope.step;
+		var value = onMouseDownValue + ( distance / ( event.shiftKey ? 5 : 50 ) ) * scope.step;
+		value = Math.min( scope.max, Math.max( scope.min, value ) );
 
-		dom.value = Math.min( scope.max, Math.max( scope.min, number ) ).toFixed( scope.precision );
+		if ( currentValue !== value ) {
 
-		if ( currentValue !== dom.value ) dom.dispatchEvent( changeEvent );
+			scope.setValue( value );
+			dom.dispatchEvent( changeEvent );
+
+		}
 
 		prevPointer = [ event.clientX, event.clientY ];
 
-	};
+	}
 
-	var onMouseUp = function ( event ) {
+	function onMouseUp( event ) {
 
 		document.removeEventListener( 'mousemove', onMouseMove, false );
 		document.removeEventListener( 'mouseup', onMouseUp, false );
@@ -706,9 +747,9 @@ UI.Number = function ( number ) {
 
 		}
 
-	};
+	}
 
-	var onChange = function ( event ) {
+	function onChange( event ) {
 
 		var value = 0;
 
@@ -722,25 +763,25 @@ UI.Number = function ( number ) {
 
 		}
 
-		dom.value = parseFloat( value );
+		scope.setValue( parseFloat( value ) );
 
-	};
+	}
 
-	var onFocus = function ( event ) {
+	function onFocus( event ) {
 
 		dom.style.backgroundColor = '';
-		dom.style.borderColor = '#ccc';
 		dom.style.cursor = '';
 
-	};
+	}
 
-	var onBlur = function ( event ) {
+	function onBlur( event ) {
 
 		dom.style.backgroundColor = 'transparent';
-		dom.style.borderColor = 'transparent';
 		dom.style.cursor = 'col-resize';
 
-	};
+	}
+
+	onBlur();
 
 	dom.addEventListener( 'mousedown', onMouseDown, false );
 	dom.addEventListener( 'change', onChange, false );
@@ -756,7 +797,7 @@ UI.Number.prototype.constructor = UI.Number;
 
 UI.Number.prototype.getValue = function () {
 
-	return parseFloat( this.dom.value );
+	return this.value;
 
 };
 
@@ -764,6 +805,7 @@ UI.Number.prototype.setValue = function ( value ) {
 
 	if ( value !== undefined ) {
 
+		this.value = value;
 		this.dom.value = value.toFixed( this.precision );
 
 	}
@@ -800,7 +842,7 @@ UI.Integer = function ( number ) {
 
 	var dom = document.createElement( 'input' );
 	dom.className = 'Number';
-	dom.value = '0.00';
+	dom.value = '0';
 
 	dom.addEventListener( 'keydown', function ( event ) {
 
@@ -808,12 +850,15 @@ UI.Integer = function ( number ) {
 
 	}, false );
 
+	this.value = 0;
+
 	this.min = - Infinity;
 	this.max = Infinity;
 
 	this.step = 1;
 
 	this.dom = dom;
+
 	this.setValue( number );
 
 	var changeEvent = document.createEvent( 'HTMLEvents' );
@@ -825,40 +870,44 @@ UI.Integer = function ( number ) {
 	var pointer = [ 0, 0 ];
 	var prevPointer = [ 0, 0 ];
 
-	var onMouseDown = function ( event ) {
+	function onMouseDown( event ) {
 
 		event.preventDefault();
 
 		distance = 0;
 
-		onMouseDownValue = parseFloat( dom.value );
+		onMouseDownValue = scope.value;
 
 		prevPointer = [ event.clientX, event.clientY ];
 
 		document.addEventListener( 'mousemove', onMouseMove, false );
 		document.addEventListener( 'mouseup', onMouseUp, false );
 
-	};
+	}
 
-	var onMouseMove = function ( event ) {
+	function onMouseMove( event ) {
 
-		var currentValue = dom.value;
+		var currentValue = scope.value;
 
 		pointer = [ event.clientX, event.clientY ];
 
 		distance += ( pointer[ 0 ] - prevPointer[ 0 ] ) - ( pointer[ 1 ] - prevPointer[ 1 ] );
 
-		var number = onMouseDownValue + ( distance / ( event.shiftKey ? 5 : 50 ) ) * scope.step;
+		var value = onMouseDownValue + ( distance / ( event.shiftKey ? 5 : 50 ) ) * scope.step;
+		value = Math.min( scope.max, Math.max( scope.min, value ) ) | 0;
 
-		dom.value = Math.min( scope.max, Math.max( scope.min, number ) ) | 0;
+		if ( currentValue !== value ) {
 
-		if ( currentValue !== dom.value ) dom.dispatchEvent( changeEvent );
+			scope.setValue( value );
+			dom.dispatchEvent( changeEvent );
+
+		}
 
 		prevPointer = [ event.clientX, event.clientY ];
 
-	};
+	}
 
-	var onMouseUp = function ( event ) {
+	function onMouseUp( event ) {
 
 		document.removeEventListener( 'mousemove', onMouseMove, false );
 		document.removeEventListener( 'mouseup', onMouseUp, false );
@@ -870,9 +919,9 @@ UI.Integer = function ( number ) {
 
 		}
 
-	};
+	}
 
-	var onChange = function ( event ) {
+	function onChange( event ) {
 
 		var value = 0;
 
@@ -886,25 +935,25 @@ UI.Integer = function ( number ) {
 
 		}
 
-		dom.value = parseInt( value );
+		scope.setValue( value );
 
-	};
+	}
 
-	var onFocus = function ( event ) {
+	function onFocus( event ) {
 
 		dom.style.backgroundColor = '';
-		dom.style.borderColor = '#ccc';
 		dom.style.cursor = '';
 
-	};
+	}
 
-	var onBlur = function ( event ) {
+	function onBlur( event ) {
 
 		dom.style.backgroundColor = 'transparent';
-		dom.style.borderColor = 'transparent';
 		dom.style.cursor = 'col-resize';
 
-	};
+	}
+
+	onBlur();
 
 	dom.addEventListener( 'mousedown', onMouseDown, false );
 	dom.addEventListener( 'change', onChange, false );
@@ -920,7 +969,7 @@ UI.Integer.prototype.constructor = UI.Integer;
 
 UI.Integer.prototype.getValue = function () {
 
-	return parseInt( this.dom.value );
+	return this.value;
 
 };
 
@@ -928,6 +977,7 @@ UI.Integer.prototype.setValue = function ( value ) {
 
 	if ( value !== undefined ) {
 
+		this.value = value | 0;
 		this.dom.value = value | 0;
 
 	}

+ 59 - 22
editor/js/libs/ui.three.js

@@ -72,7 +72,7 @@ UI.Texture = function ( mapping ) {
 
 		}
 
-	}
+	};
 
 	this.dom = dom;
 	this.texture = null;
@@ -158,12 +158,12 @@ UI.Outliner = function ( editor ) {
 
 			if ( item.nextSibling === null ) {
 
-				editor.moveObject( object, editor.scene );
+				editor.execute( new MoveObjectCommand( object, editor.scene ) );
 
 			} else {
 
 				var nextObject = scene.getObjectById( item.nextSibling.value );
-				editor.moveObject( object, nextObject.parent, nextObject );
+				editor.execute( new MoveObjectCommand( object, nextObject.parent, nextObject ) );
 
 			}
 
@@ -171,48 +171,54 @@ UI.Outliner = function ( editor ) {
 	} );
 
 	// Broadcast for object selection after arrow navigation
-	var changeEvent = document.createEvent('HTMLEvents');
+	var changeEvent = document.createEvent( 'HTMLEvents' );
 	changeEvent.initEvent( 'change', true, true );
 
 	// Prevent native scroll behavior
-	dom.addEventListener( 'keydown', function (event) {
+	dom.addEventListener( 'keydown', function ( event ) {
 
 		switch ( event.keyCode ) {
 			case 38: // up
 			case 40: // down
-			event.preventDefault();
-			event.stopPropagation();
-			break;
+				event.preventDefault();
+				event.stopPropagation();
+				break;
 		}
 
-	}, false);
+	}, false );
 
 	// Keybindings to support arrow navigation
-	dom.addEventListener( 'keyup', function (event) {
+	dom.addEventListener( 'keyup', function ( event ) {
 
-		switch ( event.keyCode ) {
-			case 38: // up
-			case 40: // down
-			scope.selectedIndex += ( event.keyCode == 38 ) ? -1 : 1;
+		function select( index ) {
 
-			if ( scope.selectedIndex >= 0 && scope.selectedIndex < scope.options.length ) {
+			if ( index >= 0 && index < scope.options.length ) {
 
-				// Highlight selected dom elem and scroll parent if needed
-				scope.setValue( scope.options[ scope.selectedIndex ].value );
+				scope.selectedIndex = index;
 
+				// Highlight selected dom elem and scroll parent if needed
+				scope.setValue( scope.options[ index ].value );
 				scope.dom.dispatchEvent( changeEvent );
 
 			}
 
-			break;
 		}
 
-	}, false);
+		switch ( event.keyCode ) {
+			case 38: // up
+				select( scope.selectedIndex - 1 );
+				break;
+			case 40: // down
+				select( scope.selectedIndex + 1 );
+				break;
+		}
+
+	}, false );
 
 	this.dom = dom;
 
 	this.options = [];
-	this.selectedIndex = -1;
+	this.selectedIndex = - 1;
 	this.selectedValue = null;
 
 	return this;
@@ -242,7 +248,7 @@ UI.Outliner.prototype.setOptions = function ( options ) {
 		var option = options[ i ];
 
 		var div = document.createElement( 'div' );
-		div.className = 'option ' + ( option.static === true ? '': 'draggable' );
+		div.className = 'option ' + ( option.static === true ? '' : 'draggable' );
 		div.innerHTML = option.html;
 		div.value = option.value;
 		scope.dom.appendChild( div );
@@ -286,7 +292,7 @@ UI.Outliner.prototype.setValue = function ( value ) {
 
 			if ( this.dom.scrollTop > y ) {
 
-				this.dom.scrollTop = y
+				this.dom.scrollTop = y;
 
 			} else if ( this.dom.scrollTop < minScroll ) {
 
@@ -309,3 +315,34 @@ UI.Outliner.prototype.setValue = function ( value ) {
 	return this;
 
 };
+
+UI.THREE = {};
+
+UI.THREE.Boolean = function ( boolean, text ) {
+
+	UI.Span.call( this );
+
+	this.setMarginRight( '10px' );
+
+	this.checkbox = new UI.Checkbox( boolean );
+	this.text = new UI.Text( text ).setMarginLeft( '3px' );
+
+	this.add( this.checkbox );
+	this.add( this.text );
+
+};
+
+UI.THREE.Boolean.prototype = Object.create( UI.Span.prototype );
+UI.THREE.Boolean.prototype.constructor = UI.THREE.Boolean;
+
+UI.THREE.Boolean.prototype.getValue = function () {
+
+	return this.checkbox.getValue();
+
+};
+
+UI.THREE.Boolean.prototype.setValue = function ( value ) {
+
+	return this.checkbox.setValue( value );
+
+};

+ 1 - 1
examples/canvas_morphtargets_horse.html

@@ -78,7 +78,7 @@
 					mixer = new THREE.AnimationMixer( mesh );
 
 					var clip = THREE.AnimationClip.CreateFromMorphTargetSequence( 'gallop', geometry.morphTargets, 30 );
-					mixer.addAction( new THREE.AnimationAction( clip ).warpToDuration( 1 ) );
+					mixer.clipAction( clip ).setDuration( 1 ).play();
 
 				} );
 

File diff suppressed because it is too large
+ 7 - 57
examples/css3d_youtube.html


+ 35 - 13
examples/index.html

@@ -67,6 +67,7 @@
 					color: #2194CE;
 					text-decoration: none;
 					cursor: pointer;
+					display: block;
 				}
 
 				#panel #content .selected {
@@ -198,6 +199,7 @@
 				"webgl_decals",
 				"webgl_effects_anaglyph",
 				"webgl_effects_parallaxbarrier",
+				"webgl_effects_peppersghost",
 				"webgl_effects_stereo",
 				"webgl_exporter_obj",
 				"webgl_geometries",
@@ -225,7 +227,8 @@
 				"webgl_geometry_terrain_fog",
 				"webgl_geometry_terrain_raycast",
 				"webgl_geometry_text",
-				"webgl_geometry_text2",
+				"webgl_geometry_text_earcut",
+				"webgl_geometry_text_pnltri",
 				"webgl_gpgpu_birds",
 				"webgl_gpu_particle_system",
 				"webgl_hdr",
@@ -233,6 +236,7 @@
 				"webgl_interactive_buffergeometry",
 				"webgl_interactive_cubes",
 				"webgl_interactive_cubes_gpu",
+				"webgl_interactive_instances_gpu",
 				"webgl_interactive_cubes_ortho",
 				"webgl_interactive_draggablecubes",
 				"webgl_interactive_lines",
@@ -249,6 +253,7 @@
 				"webgl_lines_dashed",
 				"webgl_lines_sphere",
 				"webgl_lines_splines",
+				"webgl_loader_3mf",
 				"webgl_loader_amf",
 				"webgl_loader_assimp2json",
 				"webgl_loader_awd",
@@ -265,6 +270,9 @@
 				"webgl_loader_json_objconverter",
 				"webgl_loader_md2",
 				"webgl_loader_md2_control",
+				"webgl_loader_mmd",
+				"webgl_loader_mmd_pose",
+				"webgl_loader_mmd_audio",
 				"webgl_loader_msgpack",
 				"webgl_loader_obj",
 				"webgl_loader_obj_mtl",
@@ -284,7 +292,6 @@
 				"webgl_lod",
 				"webgl_marchingcubes",
 				"webgl_materials",
-				"webgl_materials2",
 				"webgl_materials_blending",
 				"webgl_materials_blending_custom",
 				"webgl_materials_bumpmap",
@@ -312,6 +319,11 @@
 				"webgl_materials_texture_manualmipmap",
 				"webgl_materials_texture_pvrtc",
 				"webgl_materials_texture_tga",
+				"webgl_materials_variations_basic",
+				"webgl_materials_variations_lambert",
+				"webgl_materials_variations_phong",
+				"webgl_materials_variations_standard",
+				"webgl_materials_variations_standard2",
 				"webgl_materials_video",
 				"webgl_materials_wireframe",
 				"webgl_mirror",
@@ -347,6 +359,7 @@
 				"webgl_postprocessing_dof2",
 				"webgl_postprocessing_glitch",
 				"webgl_postprocessing_godrays",
+				"webgl_postprocessing_masking",
 				"webgl_postprocessing_ssao",
 				"webgl_raycast_texture",
 				"webgl_read_float_buffer",
@@ -387,6 +400,7 @@
 				"webgl_buffergeometry_lines_indexed",
 				"webgl_buffergeometry_points",
 				"webgl_buffergeometry_rawshader",
+				"webgl_buffergeometry_selective_draw",
 				"webgl_buffergeometry_uint",
 				"webgl_custom_attributes",
 				"webgl_custom_attributes_lines",
@@ -411,6 +425,7 @@
 				"css3dstereo_periodictable",
 			],
 			"misc": [
+				"misc_animation_authoring",
 				"misc_animation_keys",
 				"misc_controls_deviceorientation",
 				"misc_controls_fly",
@@ -517,7 +532,7 @@
 		button.style.display = 'none';
 		document.body.appendChild( button );
 
-		var divs = {};
+		var links = {};
 		var selected = null;
 
 		for ( var key in files ) {
@@ -536,18 +551,25 @@
 					name.shift();
 					name = name.join( ' / ' );
 
-					var div = document.createElement( 'div' );
-					div.className = 'link';
-					div.textContent = name;
-					div.addEventListener( 'click', function () {
+					var link = document.createElement( 'a' );
+					link.className = 'link';
+					link.textContent = name;
+					link.href = "#" + file;
+					link.addEventListener( 'click', function ( event ) {
+
+						if ( event.button === 0 ) {
+
+							event.preventDefault();
+
+							panel.classList.toggle( 'collapsed' );
+							load( file );
 
-						panel.classList.toggle( 'collapsed' );
-						load( file );
+						}
 
 					} );
-					container.appendChild( div );
+					container.appendChild( link );
 
-					divs[ file ] = div;
+					links[ file ] = link;
 
 				} )( section[ i ] );
 
@@ -557,9 +579,9 @@
 
 		var load = function ( file ) {
 
-			if ( selected !== null ) divs[ selected ].className = 'link';
+			if ( selected !== null ) links[ selected ].className = 'link';
 
-			divs[ file ].className = 'link selected';
+			links[ file ].className = 'link selected';
 
 			window.location.hash = file;
 			viewer.src = file + '.html';

Some files were not shown because too many files changed in this diff