浏览代码

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

kaypikun 9 年之前
父节点
当前提交
a97440e8a4
共有 100 个文件被更改,包括 5126 次插入1932 次删除
  1. 1 0
      .gitignore
  2. 2 1
      .npmignore
  3. 8 8
      bower.json
  4. 383 273
      build/three.js
  5. 399 355
      build/three.min.js
  6. 2 0
      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. 2 0
      docs/api/renderers/WebGLRenderer.html
  23. 11 13
      docs/api/textures/CubeTexture.html
  24. 2 4
      docs/list.js
  25. 34 33
      docs/scenes/bones-browser.html
  26. 2 1
      docs/scenes/geometry-browser.html
  27. 28 27
      docs/scenes/material-browser.html
  28. 24 17
      editor/css/dark.css
  29. 24 17
      editor/css/light.css
  30. 6 6
      editor/css/main.css
  31. 132 0
      editor/docs/Implementing additional commands for undo-redo.md
  32. 94 0
      editor/docs/Writing unit tests for undo-redo commands.md
  33. 43 9
      editor/index.html
  34. 47 0
      editor/js/Command.js
  35. 3 1
      editor/js/Config.js
  36. 67 11
      editor/js/Editor.js
  37. 277 35
      editor/js/History.js
  38. 126 106
      editor/js/Loader.js
  39. 43 74
      editor/js/Menubar.Add.js
  40. 59 14
      editor/js/Menubar.Edit.js
  41. 32 19
      editor/js/Menubar.File.js
  42. 6 9
      editor/js/Menubar.Status.js
  43. 66 17
      editor/js/Script.js
  44. 5 9
      editor/js/Sidebar.Geometry.BoxGeometry.js
  45. 3 1
      editor/js/Sidebar.Geometry.BufferGeometry.js
  46. 5 7
      editor/js/Sidebar.Geometry.CircleGeometry.js
  47. 5 9
      editor/js/Sidebar.Geometry.CylinderGeometry.js
  48. 3 1
      editor/js/Sidebar.Geometry.Geometry.js
  49. 5 7
      editor/js/Sidebar.Geometry.IcosahedronGeometry.js
  50. 3 1
      editor/js/Sidebar.Geometry.Modifiers.js
  51. 5 9
      editor/js/Sidebar.Geometry.PlaneGeometry.js
  52. 5 9
      editor/js/Sidebar.Geometry.SphereGeometry.js
  53. 5 9
      editor/js/Sidebar.Geometry.TorusGeometry.js
  54. 5 9
      editor/js/Sidebar.Geometry.TorusKnotGeometry.js
  55. 19 20
      editor/js/Sidebar.Geometry.js
  56. 135 0
      editor/js/Sidebar.History.js
  57. 339 63
      editor/js/Sidebar.Material.js
  58. 74 64
      editor/js/Sidebar.Object3D.js
  59. 7 24
      editor/js/Sidebar.Project.js
  60. 1 1
      editor/js/Sidebar.Scene.js
  61. 4 5
      editor/js/Sidebar.Script.js
  62. 1 0
      editor/js/Sidebar.js
  63. 4 7
      editor/js/Toolbar.js
  64. 62 68
      editor/js/Viewport.js
  65. 66 0
      editor/js/commands/AddObjectCommand.js
  66. 76 0
      editor/js/commands/AddScriptCommand.js
  67. 107 0
      editor/js/commands/MoveObjectCommand.js
  68. 85 0
      editor/js/commands/MultiCmdsCommand.js
  69. 103 0
      editor/js/commands/RemoveObjectCommand.js
  70. 81 0
      editor/js/commands/RemoveScriptCommand.js
  71. 74 0
      editor/js/commands/SetColorCommand.js
  72. 86 0
      editor/js/commands/SetGeometryCommand.js
  73. 71 0
      editor/js/commands/SetGeometryValueCommand.js
  74. 74 0
      editor/js/commands/SetMaterialColorCommand.js
  75. 74 0
      editor/js/commands/SetMaterialCommand.js
  76. 125 0
      editor/js/commands/SetMaterialMapCommand.js
  77. 76 0
      editor/js/commands/SetMaterialValueCommand.js
  78. 83 0
      editor/js/commands/SetPositionCommand.js
  79. 84 0
      editor/js/commands/SetRotationCommand.js
  80. 84 0
      editor/js/commands/SetScaleCommand.js
  81. 100 0
      editor/js/commands/SetSceneCommand.js
  82. 88 0
      editor/js/commands/SetScriptValueCommand.js
  83. 71 0
      editor/js/commands/SetUuidCommand.js
  84. 76 0
      editor/js/commands/SetValueCommand.js
  85. 20 9
      editor/js/libs/app.js
  86. 31 29
      editor/js/libs/ui.js
  87. 47 10
      editor/js/libs/ui.three.js
  88. 1 1
      examples/canvas_morphtargets_horse.html
  89. 13 2
      examples/index.html
  90. 33 57
      examples/js/AnimationClipCreator.js
  91. 23 28
      examples/js/BlendCharacter.js
  92. 3 12
      examples/js/BlendCharacterGui.js
  93. 26 12
      examples/js/MD2Character.js
  94. 3 4
      examples/js/MorphAnimMesh.js
  95. 26 116
      examples/js/ShaderSkin.js
  96. 17 39
      examples/js/ShaderTerrain.js
  97. 280 0
      examples/js/TimelinerController.js
  98. 1 1
      examples/js/UCSCharacter.js
  99. 170 0
      examples/js/animation/CCDIKSolver.js
  100. 2 2
      examples/js/controls/EditorControls.js

+ 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"
 	]
 }

文件差异内容过多而无法显示
+ 383 - 273
build/three.js


文件差异内容过多而无法显示
+ 399 - 355
build/three.min.js


+ 2 - 0
docs/api/core/Geometry.html

@@ -278,6 +278,8 @@
 		<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>

+ 2 - 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>

+ 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" ]
 
 		]
 

+ 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>

+ 24 - 17
editor/css/dark.css

@@ -39,20 +39,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 +61,7 @@ input.Number {
 #player {
 	position: absolute;
 	top: 32px;
-	left: 0px;
+	left: 0;
 	right: 300px;
 	bottom: 32px;
 }
@@ -71,10 +71,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 +86,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;
 		}
@@ -117,7 +117,7 @@ input.Number {
 				color: #888;
 				background-color: transparent;
 				padding: 5px 10px;
-				margin: 0px !important;
+				margin: 0 !important;
 			}
 
 				#menubar .menu .options .option:hover {
@@ -129,11 +129,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;
@@ -156,7 +163,7 @@ input.Number {
 	}
 
 	#sidebar .Panel.collapsed {
-		margin-bottom: 0px;
+		margin-bottom: 0;
 	}
 
 	#sidebar > .Panel {
@@ -173,9 +180,9 @@ input.Number {
 
 #toolbar {
 	position: absolute;
-	left: 0px;
+	left: 0;
 	right: 300px;
-	bottom: 0px;
+	bottom: 0;
 	height: 32px;
 	background-color: #111;
 	color: #333;

+ 24 - 17
editor/css/light.css

@@ -32,20 +32,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 +54,7 @@ input.Number {
 #player {
 	position: absolute;
 	top: 32px;
-	left: 0px;
+	left: 0;
 	right: 300px;
 	bottom: 32px;
 }
@@ -64,10 +64,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 +79,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 +110,7 @@ input.Number {
 				color: #666;
 				background-color: transparent;
 				padding: 5px 10px;
-				margin: 0px !important;
+				margin: 0 !important;
 			}
 
 				#menubar .menu .options .option:hover {
@@ -123,11 +123,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;
@@ -149,7 +156,7 @@ input.Number {
 	}
 
 	#sidebar .Panel.collapsed {
-		margin-bottom: 0px;
+		margin-bottom: 0;
 	}
 
 	#sidebar > .Panel {
@@ -160,9 +167,9 @@ input.Number {
 
 #toolbar {
 	position: absolute;
-	left: 0px;
+	left: 0;
 	right: 300px;
-	bottom: 0px;
+	bottom: 0;
 	height: 32px;
 	background: #eee;
 	color: #333;

+ 6 - 6
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,7 +88,7 @@ textarea, input { outline: none; } /* osx */
 
 		color: #f00;
 		text-align: right;
-		padding: 0px 20px;
+		padding: 0 20px;
 
 	}
 
@@ -97,7 +97,7 @@ textarea, input { outline: none; } /* osx */
 .type {
 	position:relative;
 	top:-2px;
-	padding: 0px 2px;
+	padding: 0 2px;
 	color: #ddd;
 }
 .type:after {

+ 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.

+ 43 - 9
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>
@@ -119,9 +120,31 @@
 		<script src="js/Sidebar.Geometry.TeapotBufferGeometry.js"></script>
 		<script src="js/Sidebar.Material.js"></script>
 		<script src="js/Sidebar.Script.js"></script>
+		<script src="js/Sidebar.History.js"></script>
 		<script src="js/Toolbar.js"></script>
 		<script src="js/Viewport.js"></script>
 		<script src="js/Viewport.Info.js"></script>
+		<script src="js/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 +162,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 +243,7 @@
 				signals.materialChanged.add( saveState );
 				signals.sceneGraphChanged.add( saveState );
 				signals.scriptChanged.add( saveState );
+				signals.historyChanged.add( saveState );
 
 				signals.showModal.add( function ( content ) {
 
@@ -262,9 +286,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 +319,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?' ) ) {
 

+ 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 - 1
editor/js/Config.js

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

+ 67 - 11
editor/js/Editor.js

@@ -62,7 +62,10 @@ var Editor = function () {
 		fogParametersChanged: new SIGNALS.Signal(),
 		windowResize: new SIGNALS.Signal(),
 
-		showGridChanged: new SIGNALS.Signal()
+		showGridChanged: new SIGNALS.Signal(),
+		refreshSidebarObject3D: new SIGNALS.Signal(),
+		historyChanged: new SIGNALS.Signal(),
+		refreshScriptEditor: new SIGNALS.Signal()
 
 	};
 
@@ -71,8 +74,8 @@ 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 = new THREE.PerspectiveCamera( 50, 1, 1, 10000 );
+	this.camera.position.set( 20, 10, 20 );
 	this.camera.lookAt( new THREE.Vector3() );
 	this.camera.name = 'Camera';
 
@@ -233,7 +236,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 +245,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,7 +408,7 @@ Editor.prototype = {
 		this.history.clear();
 		this.storage.clear();
 
-		this.camera.position.set( 500, 250, 500 );
+		this.camera.position.set( 20, 10, 20 );
 		this.camera.lookAt( new THREE.Vector3() );
 
 		var objects = this.scene.children;
@@ -444,6 +447,13 @@ 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 );
@@ -454,23 +464,69 @@ Editor.prototype = {
 
 		this.setScene( loader.parse( json.scene ) );
 		this.scripts = json.scripts;
+		this.history.fromJSON( json.history );
 
 	},
 
 	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( 'project/history/stored' ) ) {
+
+			cmd.json = cmd.toJSON();	// serialize the cmd immediately after execution and append the json to the cmd
+
+		}
+		this.lastCmdTime = new Date();
+
+		// clearing all the redo-commands
+
+		this.redos = [];
+		this.editor.signals.historyChanged.dispatch( cmd );
 
 	},
 
 	undo: function () {
 
-		if ( this.current < 0 ) return;
+		if ( this.historyDisabled ) {
+
+			alert( "Undo/Redo disabled while scene is playing." );
+			return;
+
+		}
+
+		var cmd = undefined;
+
+		if ( this.undos.length > 0 ) {
+
+			cmd = this.undos.pop();
+
+			if ( cmd.inMemory === false ) {
+
+				cmd.fromJSON( cmd.json );
+
+			}
+
+		}
 
-		this.isRecording = false;
+		if ( cmd !== undefined ) {
 
-		this.array[ this.current -- ].undo();
+			cmd.undo();
+			this.redos.push( cmd );
+			this.editor.signals.historyChanged.dispatch( cmd );
 
-		this.isRecording = true;
+		}
+
+		return cmd;
 
 	},
 
 	redo: function () {
 
-		if ( this.current === this.arrayLength ) return;
+		if ( this.historyDisabled ) {
+
+			alert( "Undo/Redo disabled while scene is playing." );
+			return;
+
+		}
+
+		var cmd = undefined;
+
+		if ( this.redos.length > 0 ) {
+
+			cmd = this.redos.pop();
+
+			if ( cmd.inMemory === false ) {
+
+				cmd.fromJSON( cmd.json );
+
+			}
+
+		}
+
+		if ( cmd !== undefined ) {
+
+			cmd.execute();
+			this.undos.push( cmd );
+			this.editor.signals.historyChanged.dispatch( cmd );
+
+		}
+
+		return cmd;
+
+	},
+
+	toJSON: function () {
+
+		var history = {};
+		history.undos = [];
+		history.redos = [];
+
+		if ( ! this.config.getKey( 'project/history/stored' ) ) {
+
+			return history;
+
+		}
+
+		// Append Undos to History
+
+		for ( var i = 0 ; i < this.undos.length; i ++ ) {
+
+			if ( this.undos[ i ].hasOwnProperty( "json" ) ) {
+
+				history.undos.push( this.undos[ i ].json );
 
-		this.isRecording = false;
+			}
+
+		}
+
+		// Append Redos to History
+
+		for ( var i = 0 ; i < this.redos.length; i ++ ) {
+
+			if ( this.redos[ i ].hasOwnProperty( "json" ) ) {
+
+				history.redos.push( this.redos[ i ].json );
+
+			}
+
+		}
+
+		return history;
+
+	},
+
+	fromJSON: function ( json ) {
+
+		if ( json === undefined ) return;
 
-		this.array[ ++ this.current ].redo();
+		for ( var i = 0; i < json.undos.length; i ++ ) {
 
-		this.isRecording = true;
+			var cmdJSON = json.undos[ i ];
+			var cmd = new window[ cmdJSON.type ]();	// creates a new object of type "json.type"
+			cmd.json = cmdJSON;
+			cmd.id = cmdJSON.id;
+			cmd.name = cmdJSON.name;
+			this.undos.push( cmd );
+			this.idCounter = ( cmdJSON.id > this.idCounter ) ? cmdJSON.id : this.idCounter; // set last used idCounter
+
+		}
+
+		for ( var i = 0; i < json.redos.length; i ++ ) {
+
+			var cmdJSON = json.redos[ i ];
+			var cmd = new window[ cmdJSON.type ]();	// creates a new object of type "json.type"
+			cmd.json = cmdJSON;
+			cmd.id = cmdJSON.id;
+			cmd.name = cmdJSON.name;
+			this.redos.push( cmd );
+			this.idCounter = ( cmdJSON.id > this.idCounter ) ? cmdJSON.id : this.idCounter; // set last used idCounter
+
+		}
+
+		// Select the last executed undo-command
+		this.editor.signals.historyChanged.dispatch( this.undos[ this.undos.length - 1 ] );
 
 	},
 
 	clear: function () {
 
-		this.array = [];
-		this.arrayLength = -1;
+		this.undos = [];
+		this.redos = [];
+		this.idCounter = 0;
+
+		this.editor.signals.historyChanged.dispatch();
+
+	},
+
+	goToState: function ( id ) {
+
+		if ( this.historyDisabled ) {
+
+			alert( "Undo/Redo disabled while scene is playing." );
+			return;
+
+		}
+
+		this.editor.signals.sceneGraphChanged.active = false;
+		this.editor.signals.historyChanged.active = false;
+
+		var cmd = this.undos.length > 0 ? this.undos[ this.undos.length - 1 ] : undefined;	// next cmd to pop
+
+		if ( cmd === undefined || id > cmd.id ) {
+
+			cmd = this.redo();
+			while ( cmd !== undefined && id > cmd.id ) {
+
+				cmd = this.redo();
+
+			}
+
+		} else {
+
+			while ( true ) {
+
+				cmd = this.undos[ this.undos.length - 1 ];	// next cmd to pop
+
+				if ( cmd === undefined || id === cmd.id ) break;
+
+				cmd = this.undo();
+
+			}
+
+		}
+
+		this.editor.signals.sceneGraphChanged.active = true;
+		this.editor.signals.historyChanged.active = true;
+
+		this.editor.signals.sceneGraphChanged.dispatch();
+		this.editor.signals.historyChanged.dispatch( cmd );
+
+	},
+
+	enableSerialization: function ( id ) {
+
+		/**
+		 * because there might be commands in this.undos and this.redos
+		 * which have not been serialized with .toJSON() we go back
+		 * to the oldest command and redo one command after the other
+		 * while also calling .toJSON() on them.
+		 */
+
+		this.goToState( - 1 );
+
+		this.editor.signals.sceneGraphChanged.active = false;
+		this.editor.signals.historyChanged.active = false;
+
+		var cmd = this.redo();
+		while ( cmd !== undefined ) {
+
+			if ( ! cmd.hasOwnProperty( "json" ) ) {
+
+				cmd.json = cmd.toJSON();
+
+			}
+			cmd = this.redo();
+
+		}
+
+		this.editor.signals.sceneGraphChanged.active = true;
+		this.editor.signals.historyChanged.active = true;
 
-		this.current = -1;
+		this.goToState( id );
 
 	}
 

+ 126 - 106
editor/js/Loader.js

@@ -24,8 +24,7 @@ var Loader = function ( editor ) {
 					var loader = new THREE.AMFLoader();
 					var amfobject = loader.parse( event.target.result );
 
-					editor.addObject( amfobject );
-					editor.select( amfobject );
+					editor.execute( new AddObjectCommand( amfobject ) );
 
 				}, false );
 				reader.readAsArrayBuffer( file );
@@ -40,7 +39,7 @@ var Loader = function ( editor ) {
 					var loader = new THREE.AWDLoader();
 					var scene = loader.parse( event.target.result );
 
-					editor.setScene( scene );
+					editor.execute( new SetSceneCommand( scene ) );
 
 				}, false );
 				reader.readAsArrayBuffer( file );
@@ -58,7 +57,7 @@ var Loader = function ( editor ) {
 					var loader = new THREE.BabylonLoader();
 					var scene = loader.parse( json );
 
-					editor.setScene( scene );
+					editor.execute( new SetSceneCommand( scene ) );
 
 				}, false );
 				reader.readAsText( file );
@@ -76,13 +75,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 );
@@ -105,13 +103,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 ) );
 
 					} );
 
@@ -132,8 +129,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 );
@@ -155,7 +151,7 @@ var Loader = function ( editor ) {
 
 					// 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,48 +194,46 @@ var Loader = function ( editor ) {
 				break;
 
 
-				case 'kmz':
+			case 'kmz':
 
-					var reader = new FileReader();
-					reader.addEventListener( 'load', function ( event ) {
+				var reader = new FileReader();
+				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 ) {
+				var reader = new FileReader();
+				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':
 
@@ -251,8 +245,25 @@ 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':
+
+				var reader = new FileReader();
+				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 );
@@ -270,13 +281,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 );
@@ -294,13 +304,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 );
 
@@ -329,8 +338,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 );
@@ -349,13 +357,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 );
@@ -371,7 +378,7 @@ var Loader = function ( editor ) {
 
 					var result = new THREE.VRMLLoader().parse( contents );
 
-					editor.setScene( result );
+					editor.execute( new SetSceneCommand( result ) );
 
 				}, false );
 				reader.readAsText( file );
@@ -386,9 +393,9 @@ var Loader = function ( editor ) {
 
 		}
 
-	}
+	};
 
-	var handleJSON = function ( data, file, filename ) {
+	function handleJSON( data, file, filename ) {
 
 		if ( data.metadata === undefined ) { // 2.0
 
@@ -402,101 +409,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;
 
 		}
 
-	};
+	}
 
-}
+};

+ 43 - 74
editor/js/Menubar.Add.js

@@ -40,8 +40,7 @@ Menubar.Add = function ( editor ) {
 		var mesh = new THREE.Group();
 		mesh.name = 'Group ' + ( ++ meshCount );
 
-		editor.addObject( mesh );
-		editor.select( mesh );
+		editor.execute( new AddObjectCommand( mesh ) );
 
 	} );
 	options.add( option );
@@ -57,19 +56,12 @@ Menubar.Add = function ( editor ) {
 	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 );
@@ -81,20 +73,11 @@ Menubar.Add = function ( editor ) {
 	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 );
@@ -106,15 +89,14 @@ Menubar.Add = function ( editor ) {
 	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 );
@@ -126,19 +108,18 @@ Menubar.Add = function ( editor ) {
 	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 );
@@ -150,7 +131,7 @@ Menubar.Add = function ( editor ) {
 	option.setTextContent( 'Sphere' );
 	option.onClick( function () {
 
-		var radius = 75;
+		var radius = 1;
 		var widthSegments = 32;
 		var heightSegments = 16;
 		var phiStart = 0;
@@ -159,11 +140,10 @@ 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 );
@@ -175,15 +155,14 @@ Menubar.Add = function ( editor ) {
 	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 );
@@ -195,18 +174,17 @@ Menubar.Add = function ( editor ) {
 	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 );
@@ -218,20 +196,19 @@ Menubar.Add = function ( editor ) {
 	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 );
@@ -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 );
@@ -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 );
@@ -300,8 +275,7 @@ Menubar.Add = function ( editor ) {
 		var light = new THREE.PointLight( color, intensity, distance );
 		light.name = 'PointLight ' + ( ++ lightCount );
 
-		editor.addObject( light );
-		editor.select( light );
+		editor.execute( new AddObjectCommand( light ) );
 
 	} );
 	options.add( option );
@@ -323,10 +297,9 @@ 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 );
@@ -345,10 +318,9 @@ 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 );
@@ -367,10 +339,9 @@ 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 );
@@ -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 );
@@ -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 );

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

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

+ 32 - 19
editor/js/Menubar.File.js

@@ -88,13 +88,17 @@ 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 );
@@ -118,13 +122,17 @@ 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 );
@@ -139,13 +147,17 @@ 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 );
@@ -168,7 +180,7 @@ Menubar.File = function ( editor ) {
 
 		var exporter = new THREE.OBJExporter();
 
-		exportString( exporter.parse( object ), 'model.obj' );
+		saveString( exporter.parse( object ), 'model.obj' );
 
 	} );
 	options.add( option );
@@ -182,7 +194,7 @@ Menubar.File = function ( editor ) {
 
 		var exporter = new THREE.STLExporter();
 
-		exportString( exporter.parse( editor.scene ), 'model.stl' );
+		saveString( exporter.parse( editor.scene ), 'model.stl' );
 
 	} );
 	options.add( option );
@@ -247,6 +259,9 @@ Menubar.File = function ( editor ) {
 		//
 
 		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,7 +271,7 @@ 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' );
 
 		} );
 
@@ -282,23 +297,21 @@ Menubar.File = function ( editor ) {
 	link.style.display = 'none';
 	document.body.appendChild( link ); // Firefox workaround, see #6594
 
-	var exportString = function ( output, filename ) {
+	function save( blob, filename ) {
 
-		var blob = new Blob( [ output ], { type: 'text/plain' } );
-		var objectURL = URL.createObjectURL( blob );
-
-		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;
 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -2,7 +2,9 @@
  * @author mrdoob / http://mrdoob.com/
  */
 
-Sidebar.Geometry.SphereGeometry = function ( signals, object ) {
+Sidebar.Geometry.SphereGeometry = function ( editor, object ) {
+
+	var signals = editor.signals;
 
 	var container = new UI.Panel();
 
@@ -83,9 +85,7 @@ Sidebar.Geometry.SphereGeometry = function ( signals, object ) {
 
 	function update() {
 
-		object.geometry.dispose();
-
-		object.geometry = new THREE.SphereGeometry(
+		editor.execute( new 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 );
+		) ) );
 
 	}
 

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

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

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

@@ -2,7 +2,9 @@
  * @author mrdoob / http://mrdoob.com/
  */
 
-Sidebar.Geometry.TorusKnotGeometry = function ( signals, object ) {
+Sidebar.Geometry.TorusKnotGeometry = function ( editor, object ) {
+
+	var signals = editor.signals;
 
 	var container = new UI.Panel();
 
@@ -83,9 +85,7 @@ Sidebar.Geometry.TorusKnotGeometry = function ( signals, object ) {
 
 	function update() {
 
-		object.geometry.dispose();
-
-		object.geometry = new THREE.TorusKnotGeometry(
+		editor.execute( new 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 );
+		) ) );
 
 	}
 

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

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

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

@@ -0,0 +1,135 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+Sidebar.History = function ( editor ) {
+
+	var signals = editor.signals;
+
+	var config = editor.config;
+
+	var history = editor.history;
+
+	var container = new UI.CollapsiblePanel();
+	container.setCollapsed( editor.config.getKey( 'ui/sidebar/history/collapsed' ) );
+	container.onCollapsedChange( function ( boolean ) {
+
+		editor.config.setKey( 'ui/sidebar/history/collapsed', boolean );
+
+	} );
+
+	container.addStatic( new UI.Text( 'HISTORY' ) );
+
+	// Checkbox 'Save History'
+
+	var saveHistorySpan = new UI.Span().setPosition( 'absolute' ).setRight( '8px' );
+	var saveHistoryCheckbox = new UI.Checkbox( config.getKey( 'project/history/stored' ) ).onChange( function () {
+
+		config.setKey( 'project/history/stored', this.getValue() );
+		var saveHistory = this.getValue();
+
+		if ( saveHistory ) {
+
+			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();
+
+		}
+
+	} );
+
+	saveHistorySpan.add( saveHistoryCheckbox );
+
+	saveHistorySpan.onClick( function ( event ) {
+
+		event.stopPropagation(); // Avoid panel collapsing
+
+	} );
+
+	container.addStatic( saveHistorySpan );
+
+	container.add( new UI.Break() );
+
+	var ignoreObjectSelectedSignal = false;
+
+	var outliner = new UI.Outliner( editor );
+	outliner.onChange( function () {
+
+		ignoreObjectSelectedSignal = true;
+
+		editor.history.goToState( parseInt( outliner.getValue() ) );
+
+		ignoreObjectSelectedSignal = false;
+
+	} );
+	outliner.onDblClick( function () {
+
+		//editor.focusById( parseInt( outliner.getValue() ) );
+
+	} );
+	container.add( outliner );
+
+	//
+
+	var refreshUI = function () {
+
+		var options = [];
+		var enumerator = 1;
+
+		( function addObjects( objects, pad ) {
+
+			for ( var i = 0, l = objects.length; i < l; i ++ ) {
+
+				var object = objects[ i ];
+
+				var html = pad + "<span style='color: #0000cc '>" + enumerator ++ + ". Undo: " + object.name + "</span>";
+
+				options.push( { value: object.id, html: html } );
+
+			}
+
+		} )( history.undos, '&nbsp;' );
+
+
+		( function addObjects( objects, pad ) {
+
+			for ( var i = objects.length - 1; i >= 0; i -- ) {
+
+				var object = objects[ i ];
+
+				var html = pad + "<span style='color: #71544e'>" + enumerator ++ + ". Redo: " +  object.name + "</span>";
+
+				options.push( { value: object.id, html: html } );
+
+			}
+
+		} )( history.redos, '&nbsp;' );
+
+		outliner.setOptions( options );
+
+	};
+
+	refreshUI();
+
+	// events
+
+	signals.editorCleared.add( refreshUI );
+
+	signals.historyChanged.add( refreshUI );
+	signals.historyChanged.add( function ( cmd ) {
+
+		outliner.setValue( cmd !== undefined ? cmd.id : null );
+
+	} );
+
+
+	return container;
+
+};

+ 339 - 63
editor/js/Sidebar.Material.js

@@ -42,7 +42,7 @@ Sidebar.Material = function ( editor ) {
 	var materialNameRow = new UI.Panel();
 	var materialName = new UI.Input().setWidth( '150px' ).setFontSize( '12px' ).onChange( function () {
 
-		editor.setMaterialName( editor.selected.material, materialName.getValue() );
+		editor.execute( new SetMaterialValueCommand( editor.selected, 'name', materialName.getValue() ) );
 
 	} );
 
@@ -60,9 +60,10 @@ Sidebar.Material = function ( editor ) {
 		'LineDashedMaterial': 'LineDashedMaterial',
 		'MeshBasicMaterial': 'MeshBasicMaterial',
 		'MeshDepthMaterial': 'MeshDepthMaterial',
-		'MeshLambertMaterial': 'MeshLambertMaterial',
 		'MeshNormalMaterial': 'MeshNormalMaterial',
+		'MeshLambertMaterial': 'MeshLambertMaterial',
 		'MeshPhongMaterial': 'MeshPhongMaterial',
+		'MeshStandardMaterial': 'MeshStandardMaterial',
 		'ShaderMaterial': 'ShaderMaterial',
 		'SpriteMaterial': 'SpriteMaterial'
 
@@ -82,7 +83,7 @@ Sidebar.Material = function ( editor ) {
 	materialProgramInfo.setMarginLeft( '4px' );
 	materialProgramInfo.onClick( function () {
 
-		signals.editScript.dispatch( currentObject.material, 'programInfo' );
+		signals.editScript.dispatch( currentObject, 'programInfo' );
 
 	} );
 	materialProgramRow.add( materialProgramInfo );
@@ -91,7 +92,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 +101,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 );
@@ -117,6 +118,26 @@ Sidebar.Material = function ( editor ) {
 
 	container.add( materialColorRow );
 
+	// roughness
+
+	var materialRoughnessRow = new UI.Panel();
+	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.Panel();
+	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();
@@ -237,6 +258,30 @@ Sidebar.Material = function ( editor ) {
 
 	container.add( materialDisplacementMapRow );
 
+	// roughness map
+
+	var materialRoughnessMapRow = new UI.Panel();
+	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.Panel();
+	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();
@@ -289,6 +334,18 @@ Sidebar.Material = function ( editor ) {
 
 	container.add( materialAOMapRow );
 
+	// emissive map
+
+	var materialEmissiveMapRow = new UI.Panel();
+	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();
@@ -343,7 +400,7 @@ Sidebar.Material = function ( editor ) {
 	// opacity
 
 	var materialOpacityRow = new UI.Panel();
-	var materialOpacity = new UI.Number().setWidth( '60px' ).setRange( 0, 1 ).onChange( update );
+	var materialOpacity = new UI.Number( 1 ).setWidth( '60px' ).setRange( 0, 1 ).onChange( update );
 
 	materialOpacityRow.add( new UI.Text( 'Opacity' ).setWidth( '90px' ) );
 	materialOpacityRow.add( materialOpacity );
@@ -400,9 +457,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 +467,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 +476,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 +518,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 +536,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 +557,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 +578,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 +605,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 +626,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 +647,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 +726,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 +748,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 +769,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 +790,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 +887,7 @@ Sidebar.Material = function ( editor ) {
 
 		}
 
-	};
+	}
 
 	//
 
@@ -681,6 +896,8 @@ Sidebar.Material = function ( editor ) {
 		var properties = {
 			'name': materialNameRow,
 			'color': materialColorRow,
+			'roughness': materialRoughnessRow,
+			'metalness': materialMetalnessRow,
 			'emissive': materialEmissiveRow,
 			'specular': materialSpecularRow,
 			'shininess': materialShininessRow,
@@ -692,10 +909,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 +933,12 @@ Sidebar.Material = function ( editor ) {
 
 		}
 
-	};
+	}
 
 
-	function refreshUi( resetTextureSelectors ) {
+	function refreshUI( resetTextureSelectors ) {
+
+		if ( ! currentObject ) return;
 
 		var material = currentObject.material;
 
@@ -740,6 +962,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 +1068,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 +1144,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 +1217,7 @@ Sidebar.Material = function ( editor ) {
 			var objectChanged = object !== currentObject;
 
 			currentObject = object;
-			refreshUi(objectChanged);
+			refreshUI( objectChanged );
 			container.setDisplay( '' );
 
 		} else {
@@ -959,6 +1229,12 @@ Sidebar.Material = function ( editor ) {
 
 	} );
 
+	signals.materialChanged.add( function () {
+
+		refreshUI();
+
+	} );
+
 	return container;
 
-}
+};

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

@@ -20,7 +20,7 @@ Sidebar.Object3D = function ( editor ) {
 
 	// 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,23 +41,21 @@ 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 );
 
@@ -71,7 +69,7 @@ Sidebar.Object3D = function ( editor ) {
 
 		objectUUID.setValue( THREE.Math.generateUUID() );
 
-		editor.selected.uuid = objectUUID.getValue();
+		editor.execute( new SetUuidCommand( editor.selected, objectUUID.getValue() ) );
 
 	} );
 
@@ -86,7 +84,7 @@ Sidebar.Object3D = function ( editor ) {
 	var objectNameRow = new UI.Panel();
 	var objectName = new UI.Input().setWidth( '150px' ).setFontSize( '12px' ).onChange( function () {
 
-		editor.nameObject( editor.selected, objectName.getValue() );
+		editor.execute( new SetValueCommand( editor.selected, 'name', objectName.getValue() ) );
 
 	} );
 
@@ -251,21 +249,11 @@ Sidebar.Object3D = function ( editor ) {
 
 	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 objectReceiveShadowSpan = new UI.Span();
-	var objectReceiveShadow = new UI.Checkbox().onChange( update );
+	var objectCastShadow = new UI.THREE.Boolean( false, 'cast' ).onChange( update );
+	objectShadowRow.add( objectCastShadow );
 
-	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 );
 
@@ -375,110 +363,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() ) {
 
-				var value = objectReceiveShadow.getValue();
+				editor.execute( new SetValueCommand( object, 'castShadow', objectCastShadow.getValue() ) );
 
-				if ( value !== object.receiveShadow ) {
+			}
 
-					object.receiveShadow = value;
+			if ( object.receiveShadow !== undefined ) {
+
+				if ( object.receiveShadow !== objectReceiveShadow.getValue() ) {
+
+					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 +490,6 @@ Sidebar.Object3D = function ( editor ) {
 
 			}
 
-			signals.objectChanged.dispatch( object );
-
 		}
 
 	}
@@ -507,7 +509,7 @@ Sidebar.Object3D = function ( editor ) {
 			'exponent' : objectExponentRow,
 			'decay' : objectDecayRow,
 			'castShadow' : objectShadowRow,
-			'receiveShadow' : objectReceiveShadowSpan
+			'receiveShadow' : objectReceiveShadow
 		};
 
 		for ( var property in properties ) {
@@ -579,6 +581,14 @@ Sidebar.Object3D = function ( editor ) {
 
 	} );
 
+	signals.refreshSidebarObject3D.add( function ( object ) {
+
+		if ( object !== editor.selected ) return;
+
+		updateUI( object );
+
+	} );
+
 	function updateUI( object ) {
 
 		objectType.setValue( object.type );

+ 7 - 24
editor/js/Sidebar.Project.js

@@ -45,17 +45,8 @@ Sidebar.Project = function ( editor ) {
 
 		var value = this.getValue();
 
-		if ( value === 'WebGLRenderer' ) {
-
-			rendererPropertiesRow.setDisplay( '' );
-
-		} else {
-
-			rendererPropertiesRow.setDisplay( 'none' );
-
-		}
-
 		config.setKey( 'project/renderer', value );
+
 		updateRenderer();
 
 	} );
@@ -76,33 +67,23 @@ Sidebar.Project = function ( editor ) {
 	var rendererPropertiesRow = new UI.Panel();
 	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 );
 
@@ -137,6 +118,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 );

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

@@ -133,7 +133,7 @@ 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: 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 } );
 
 		( function addObjects( objects, pad ) {

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

@@ -27,7 +27,7 @@ Sidebar.Script = function ( editor ) {
 	newScript.onClick( function () {
 
 		var script = { name: '', source: 'function update( event ) {}' };
-		editor.addScript( editor.selected, script );
+		editor.execute( new 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;
 

+ 1 - 0
editor/js/Sidebar.js

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

+ 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() {
 

+ 62 - 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':
 
-			( function ( matrix1, matrix2 ) {
+					if ( ! objectRotationOnDown.equals( object.rotation ) ) {
+
+						editor.execute( new SetRotationCommand( object, object.rotation, objectRotationOnDown ) );
 
-				editor.history.add(
-					function () {
-						matrix1.decompose( object.position, object.quaternion, object.scale );
-						signals.objectChanged.dispatch( object );
-					},
-					function () {
-						matrix2.decompose( object.position, object.quaternion, object.scale );
-						signals.objectChanged.dispatch( object );
 					}
-				);
 
-			} )( matrix.clone(), object.matrix.clone() );
+					break;
+
+				case 'scale':
+
+					if ( ! objectScaleOnDown.equals( object.scale ) ) {
+
+						editor.execute( new SetScaleCommand( object, object.scale, objectScaleOnDown ) );
+
+					}
+
+					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 ) {
 
-		selectionBox.update( editor.selected );
+		if ( object !== null ) {
+
+			selectionBox.update( object );
+
+		}
 
 		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;
 
 		}
@@ -513,30 +531,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 +595,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 );
+
 		};
 
 		//

+ 31 - 29
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
 
@@ -198,7 +198,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 +265,7 @@ UI.CollapsiblePanel.prototype.clear = function () {
 
 UI.CollapsiblePanel.prototype.toggle = function() {
 
-	this.setCollapsed( !this.isCollapsed );
+	this.setCollapsed( ! this.isCollapsed );
 
 };
 
@@ -616,7 +618,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;
 
@@ -661,7 +663,7 @@ UI.Number = function ( number ) {
 	var pointer = [ 0, 0 ];
 	var prevPointer = [ 0, 0 ];
 
-	var onMouseDown = function ( event ) {
+	function onMouseDown( event ) {
 
 		event.preventDefault();
 
@@ -674,9 +676,9 @@ UI.Number = function ( number ) {
 		document.addEventListener( 'mousemove', onMouseMove, false );
 		document.addEventListener( 'mouseup', onMouseUp, false );
 
-	};
+	}
 
-	var onMouseMove = function ( event ) {
+	function onMouseMove( event ) {
 
 		var currentValue = dom.value;
 
@@ -692,9 +694,9 @@ UI.Number = function ( number ) {
 
 		prevPointer = [ event.clientX, event.clientY ];
 
-	};
+	}
 
-	var onMouseUp = function ( event ) {
+	function onMouseUp( event ) {
 
 		document.removeEventListener( 'mousemove', onMouseMove, false );
 		document.removeEventListener( 'mouseup', onMouseUp, false );
@@ -706,9 +708,9 @@ UI.Number = function ( number ) {
 
 		}
 
-	};
+	}
 
-	var onChange = function ( event ) {
+	function onChange( event ) {
 
 		var value = 0;
 
@@ -724,23 +726,23 @@ UI.Number = function ( number ) {
 
 		dom.value = 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';
 
-	};
+	}
 
 	dom.addEventListener( 'mousedown', onMouseDown, false );
 	dom.addEventListener( 'change', onChange, false );
@@ -825,7 +827,7 @@ UI.Integer = function ( number ) {
 	var pointer = [ 0, 0 ];
 	var prevPointer = [ 0, 0 ];
 
-	var onMouseDown = function ( event ) {
+	function onMouseDown( event ) {
 
 		event.preventDefault();
 
@@ -838,9 +840,9 @@ UI.Integer = function ( number ) {
 		document.addEventListener( 'mousemove', onMouseMove, false );
 		document.addEventListener( 'mouseup', onMouseUp, false );
 
-	};
+	}
 
-	var onMouseMove = function ( event ) {
+	function onMouseMove( event ) {
 
 		var currentValue = dom.value;
 
@@ -856,9 +858,9 @@ UI.Integer = function ( number ) {
 
 		prevPointer = [ event.clientX, event.clientY ];
 
-	};
+	}
 
-	var onMouseUp = function ( event ) {
+	function onMouseUp( event ) {
 
 		document.removeEventListener( 'mousemove', onMouseMove, false );
 		document.removeEventListener( 'mouseup', onMouseUp, false );
@@ -870,9 +872,9 @@ UI.Integer = function ( number ) {
 
 		}
 
-	};
+	}
 
-	var onChange = function ( event ) {
+	function onChange( event ) {
 
 		var value = 0;
 
@@ -888,23 +890,23 @@ UI.Integer = function ( number ) {
 
 		dom.value = parseInt( 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';
 
-	};
+	}
 
 	dom.addEventListener( 'mousedown', onMouseDown, false );
 	dom.addEventListener( 'change', onChange, false );

+ 47 - 10
editor/js/libs/ui.three.js

@@ -158,12 +158,12 @@ UI.Outliner = function ( editor ) {
 
 			if ( item.nextSibling === null ) {
 
-				editor.moveObject( object, editor.scene );
+				editor.execute( new 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 ) );
 
 			}
 
@@ -190,21 +190,27 @@ UI.Outliner = function ( editor ) {
 	// Keybindings to support arrow navigation
 	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;
+		}
+
+		switch ( event.keyCode ) {
+			case 38: // up
+				select( scope.selectedIndex - 1 );
+				break;
+			case 40: // down
+				select( scope.selectedIndex + 1 );
+				break;
 		}
 
 	}, false);
@@ -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();
 
 				} );
 

+ 13 - 2
examples/index.html

@@ -198,6 +198,7 @@
 				"webgl_decals",
 				"webgl_effects_anaglyph",
 				"webgl_effects_parallaxbarrier",
+				"webgl_effects_peppersghost",
 				"webgl_effects_stereo",
 				"webgl_exporter_obj",
 				"webgl_geometries",
@@ -225,7 +226,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 +235,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 +252,7 @@
 				"webgl_lines_dashed",
 				"webgl_lines_sphere",
 				"webgl_lines_splines",
+				"webgl_loader_3mf",
 				"webgl_loader_amf",
 				"webgl_loader_assimp2json",
 				"webgl_loader_awd",
@@ -265,6 +269,7 @@
 				"webgl_loader_json_objconverter",
 				"webgl_loader_md2",
 				"webgl_loader_md2_control",
+				"webgl_loader_mmd",
 				"webgl_loader_msgpack",
 				"webgl_loader_obj",
 				"webgl_loader_obj_mtl",
@@ -284,7 +289,6 @@
 				"webgl_lod",
 				"webgl_marchingcubes",
 				"webgl_materials",
-				"webgl_materials2",
 				"webgl_materials_blending",
 				"webgl_materials_blending_custom",
 				"webgl_materials_bumpmap",
@@ -312,6 +316,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",
@@ -387,6 +396,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 +421,7 @@
 				"css3dstereo_periodictable",
 			],
 			"misc": [
+				"misc_animation_authoring",
 				"misc_animation_keys",
 				"misc_controls_deviceorientation",
 				"misc_controls_fly",

+ 33 - 57
examples/js/AnimationClipCreator.js

@@ -1,7 +1,7 @@
 /**
  *
  * Creator of typical test AnimationClips / KeyframeTracks
- * 
+ *
  * @author Ben Houston / http://clara.io/
  * @author David Sarno / http://lighthaus.us/
  */
@@ -11,73 +11,64 @@ THREE.AnimationClipCreator = function() {
 
 THREE.AnimationClipCreator.CreateRotationAnimation = function( period, axis ) {
 
-	var keys = [];
-	keys.push( { time: 0, value: 0 } );
-	keys.push( { time: period, value: 360 } );
+	var times = [ 0, period ], values = [ 0, 360 ];
 
 	axis = axis || 'x';
 	var trackName = '.rotation[' + axis + ']';
 
-	var track = new THREE.NumberKeyframeTrack( trackName, keys );
+	var track = new THREE.NumberKeyframeTrack( trackName, times, values );
 
-	var clip = new THREE.AnimationClip( 'rotate.x', 10, [ track ] );
-	//console.log( 'rotateClip', clip );
+	return new THREE.AnimationClip( null, period, [ track ] );
 
-	return clip;
 };
 
 THREE.AnimationClipCreator.CreateScaleAxisAnimation = function( period, axis ) {
 
-	var keys = [];
-	keys.push( { time: 0, value: 0 } );
-	keys.push( { time: period, value: 360 } );
+	var times = [ 0, period ], values = [ 0, 1 ];
 
 	axis = axis || 'x';
 	var trackName = '.scale[' + axis + ']';
 
-	var track = new THREE.NumberKeyframeTrack( trackName, keys );
+	var track = new THREE.NumberKeyframeTrack( trackName, times, values );
 
-	var clip = new THREE.AnimationClip( 'scale.x', 10, [ track ] );
-	//console.log( 'scaleClip', clip );
+	return new THREE.AnimationClip( null, period, [ track ] );
 
-	return clip;
 };
 
 THREE.AnimationClipCreator.CreateShakeAnimation = function( duration, shakeScale ) {
 
-	var keys = [];
+	var times = [], values = [], tmp = new THREE.Vector3();
 
 	for( var i = 0; i < duration * 10; i ++ ) {
 
-		keys.push( { 
-			time: ( i / 10.0 ),
-			value: new THREE.Vector3( Math.random() * 2.0 - 1.0, Math.random() * 2.0 - 1.0, Math.random() * 2.0 - 1.0 ).multiply( shakeScale )
-		} );
+		times.push( i / 10 );
+
+		tmp.set( Math.random() * 2.0 - 1.0, Math.random() * 2.0 - 1.0, Math.random() * 2.0 - 1.0 ).
+			multiply( shakeScale ).
+			toArray( values, values.length );
 
 	}
 
 	var trackName = '.position';
 
-	var track = new THREE.VectorKeyframeTrack( trackName, keys );
+	var track = new THREE.VectorKeyframeTrack( trackName, times, values );
 
-	var clip = new THREE.AnimationClip( 'shake' + duration, duration, [ track ] );
-	//console.log( 'shakeClip', clip );
+	return new THREE.AnimationClip( null, duration, [ track ] );
 
-	return clip;
 };
 
 
 THREE.AnimationClipCreator.CreatePulsationAnimation = function( duration, pulseScale ) {
 
-	var keys = [];
+	var times = [], values = [], tmp = new THREE.Vector3();
 
 	for( var i = 0; i < duration * 10; i ++ ) {
 
+		times.push( i / 10 );
+
 		var scaleFactor = Math.random() * pulseScale;
-		keys.push( {
-			time: ( i / 10.0 ),
-			value: new THREE.Vector3( scaleFactor, scaleFactor, scaleFactor )
-		} );
+		tmp.set( scaleFactor, scaleFactor, scaleFactor ).
+			toArray( values, values.length );
 
 	}
 
@@ -85,55 +76,40 @@ THREE.AnimationClipCreator.CreatePulsationAnimation = function( duration, pulseS
 
 	var track = new THREE.VectorKeyframeTrack( trackName, keys );
 
-	var clip = new THREE.AnimationClip( 'scale' + duration, duration, [ track ] );
-	//console.log( 'scaleClip', clip );
+	return new THREE.AnimationClip( null, duration, [ track ] );
 
-	return clip;
 };
 
 
 THREE.AnimationClipCreator.CreateVisibilityAnimation = function( duration ) {
 
-	var keys = [];
-	keys.push( {
-		time: 0,
-		value: true
-	} );
-	keys.push( {
-		time: duration - 1,
-		value: false
-	} );
-	keys.push( {
-		time: duration,
-		value: true
-	} );
+	var times = [ 0, duration / 2, duration ], values = [ true, false, true ];
 
 	var trackName = '.visible';
 
-	var track = new THREE.BooleanKeyframeTrack( trackName, keys );
+	var track = new THREE.BooleanKeyframeTrack( trackName, times, values );
 
-	var clip = new THREE.AnimationClip( 'visible' + duration, duration, [ track ] );
-	//console.log( 'scaleClip', clip );
+	return new THREE.AnimationClip( null, duration, [ track ] );
 
-	return clip;
 };
 
 
 THREE.AnimationClipCreator.CreateMaterialColorAnimation = function( duration, colors, loop ) {
 
-	var timeStep = duration / colors.length;
-	var keys = [];
+	var times = [], values = [],
+		timeStep = duration / colors.length;
+
 	for( var i = 0; i <= colors.length; i ++ ) {
-		keys.push( { time: i * timeStep, value: colors[ i % colors.length ] } );
+
+		timees.push( i * timeStep );
+		values.push( colors[ i % colors.length ] );
+
 	}
 
 	var trackName = '.material[0].color';
 
-	var track = new THREE.ColorKeyframeTrack( trackName, keys );
+	var track = new THREE.ColorKeyframeTrack( trackName, times, values );
 
-	var clip = new THREE.AnimationClip( 'colorDiffuse', 10, [ track ] );
-	//console.log( 'diffuseClip', clip );
+	return new THREE.AnimationClip( null, duration, [ track ] );
 
-	return clip;
 };
-

+ 23 - 28
examples/js/BlendCharacter.js

@@ -4,7 +4,6 @@
 
 THREE.BlendCharacter = function () {
 
-	this.animations = {};
 	this.weightSchedule = [];
 	this.warpSchedule = [];
 
@@ -20,13 +19,13 @@ THREE.BlendCharacter = function () {
 
 			THREE.SkinnedMesh.call( scope, geometry, originalMaterial );
 
-			scope.mixer = new THREE.AnimationMixer( scope );
+			var mixer = new THREE.AnimationMixer( scope );
+			scope.mixer = mixer;
 
-			// Create the animations		
+			// Create the animations
 			for ( var i = 0; i < geometry.animations.length; ++ i ) {
 
-				var animName = geometry.animations[ i ].name;
-				scope.animations[ animName ] = geometry.animations[ i ];
+				mixer.clipAction( geometry.animations[ i ] );
 
 			}
 
@@ -45,49 +44,45 @@ THREE.BlendCharacter = function () {
 
 	this.play = function( animName, weight ) {
 
-		this.mixer.removeAllActions();
-		
-		this.mixer.play( new THREE.AnimationAction( this.animations[ animName ] ) );
-
+		//console.log("play('%s', %f)", animName, weight);
+		return this.mixer.clipAction( animName ).
+				setEffectiveWeight( weight ).play();
 	};
 
 	this.crossfade = function( fromAnimName, toAnimName, duration ) {
 
-		this.mixer.removeAllActions();
- 
-		var fromAction = new THREE.AnimationAction( this.animations[ fromAnimName ] );
-		var toAction = new THREE.AnimationAction( this.animations[ toAnimName ] );
+		this.mixer.stopAllAction();
 
-		this.mixer.play( fromAction );
-		this.mixer.play( toAction );
+		var fromAction = this.play( fromAnimName, 1 );
+		var toAction = this.play( toAnimName, 1 );
 
-		this.mixer.crossFade( fromAction, toAction, duration, false );
+		fromAction.crossFadeTo( toAction, duration, false );
 
 	};
 
 	this.warp = function( fromAnimName, toAnimName, duration ) {
 
-		this.mixer.removeAllActions();
-
-		var fromAction = new THREE.AnimationAction( this.animations[ fromAnimName ] );
-		var toAction = new THREE.AnimationAction( this.animations[ toAnimName ] );
+		this.mixer.stopAllAction();
 
-		this.mixer.play( fromAction );
-		this.mixer.play( toAction );
+		var fromAction = this.play( fromAnimName, 1 );
+		var toAction = this.play( toAnimName, 1 );
 
-		this.mixer.crossFade( fromAction, toAction, duration, true );
+		fromAction.crossFadeTo( toAction, duration, true );
 
 	};
 
 	this.applyWeight = function( animName, weight ) {
 
-		var action = this.mixer.findActionByName( animName );
-		if( action ) {
-			action.weight = weight;
-		}
+		this.mixer.clipAction( animName ).setEffectiveWeight( weight );
 
 	};
 
+	this.getWeight = function( animName ) {
+
+		return this.mixer.clipAction( animName ).getEffectiveWeight();
+
+	}
+
 	this.pauseAll = function() {
 
 		this.mixer.timeScale = 0;
@@ -103,7 +98,7 @@ THREE.BlendCharacter = function () {
 
 	this.stopAll = function() {
 
-		this.mixer.removeAllActions();
+		this.mixer.stopAllAction();
 
 	};
 

+ 3 - 12
examples/js/BlendCharacterGui.js

@@ -40,18 +40,9 @@ function BlendCharacterGui( blendMesh ) {
 
 	this.update = function( time ) {
 
-		var getWeight = function( actionName ) {
-			for( var i = 0; i < blendMesh.mixer.actions.length; i ++ ) {
-				var action = blendMesh.mixer.actions[i];
-				if( action.clip.name === actionName ) {
-					return action.getWeightAt( time );	
-				}
-			}
-			return 0;
-		}
-		controls[ 'idle' ] = getWeight( 'idle' );
-		controls[ 'walk' ] = getWeight( 'walk' );
-		controls[ 'run' ] = getWeight( 'run' );
+		controls[ 'idle' ] = blendMesh.getWeight( 'idle' );
+		controls[ 'walk' ] = blendMesh.getWeight( 'walk' );
+		controls[ 'run' ] = blendMesh.getWeight( 'run' );
 
 	};
 

+ 26 - 12
examples/js/MD2Character.js

@@ -33,7 +33,6 @@ THREE.MD2Character = function () {
 
 		var weaponsTextures = [];
 		for ( var i = 0; i < config.weapons.length; i ++ ) weaponsTextures[ i ] = config.weapons[ i ][ 1 ];
-
 		// SKINS
 
 		this.skinsBody = loadTextures( config.baseUrl + "skins/", config.skins );
@@ -81,6 +80,21 @@ THREE.MD2Character = function () {
 				scope.weapons[ index ] = mesh;
 				scope.meshWeapon = mesh;
 
+
+				// the animation system requires unique names, so append the
+				// uuid of the source geometry:
+
+				var geometry = mesh.geometry,
+					animations = geometry.animations;
+
+				for ( var i = 0, n = animations.length; i !== n; ++ i ) {
+
+					var animation = animations[ i ];
+					animation.name += geometry.uuid;
+
+				}
+
+
 				checkLoadingComplete();
 
 			}
@@ -154,17 +168,15 @@ THREE.MD2Character = function () {
 		if ( this.meshBody ) {
 
 			if( this.meshBody.activeAction ) {
-				scope.mixer.removeAction( this.meshBody.activeAction );
+				this.meshBody.activeAction.stop();
 				this.meshBody.activeAction = null;
 			}
 
 			var clip = THREE.AnimationClip.findByName( this.meshBody.geometry.animations, clipName );
 			if( clip ) {
 
-				var action = new THREE.AnimationAction( clip, this.mixer.time ).setLocalRoot( this.meshBody );
-				scope.mixer.addAction( action );
-
-				this.meshBody.activeAction = action;
+				this.meshBody.activeAction =
+						this.mixer.clipAction( clip, this.meshBody ).play();
 
 			}
 
@@ -183,17 +195,19 @@ THREE.MD2Character = function () {
 		if ( scope.meshWeapon ) {
 
 			if( this.meshWeapon.activeAction ) {
-				scope.mixer.removeAction( this.meshWeapon.activeAction );
+				this.meshWeapon.activeAction.stop();
 				this.meshWeapon.activeAction = null;
 			}
 
-			var clip = THREE.AnimationClip.findByName( this.meshWeapon.geometry.animations, clipName );
-			if( clip ) {
+			var geometry = this.meshWeapon.geometry,
+				animations = geometry.animations;
 
-				var action = new THREE.AnimationAction( clip ).syncWith( this.meshBody.activeAction ).setLocalRoot( this.meshWeapon );
-				scope.mixer.addAction( action );
+			var clip = THREE.AnimationClip.findByName( animations, clipName + geometry.uuid );
+			if( clip ) {
 
-				this.meshWeapon.activeAction = action;
+				this.meshWeapon.activeAction =
+						this.mixer.clipAction( clip, this.meshWeapon ).
+							syncWith( this.meshBody.activeAction ).play();
 
 			}
 

+ 3 - 4
examples/js/MorphAnimMesh.js

@@ -31,7 +31,7 @@ THREE.MorphAnimMesh.prototype.playAnimation = function ( label, fps ) {
 
 	if( this.activeAction ) {
 
-		this.mixer.removeAction( this.activeAction );
+		this.activeAction.stop();
 		this.activeAction = null;
 		
 	}
@@ -40,10 +40,9 @@ THREE.MorphAnimMesh.prototype.playAnimation = function ( label, fps ) {
 
 	if ( clip ) {
 
-		var action = new THREE.AnimationAction( clip );
+		var action = this.mixer.clipAction( clip );
 		action.timeScale = ( clip.tracks.length * fps ) / clip.duration;
-		this.mixer.addAction( action );
-		this.activeAction = action;
+		this.activeAction = action.play();
 
 	} else {
 

+ 26 - 116
examples/js/ShaderSkin.js

@@ -80,35 +80,11 @@ THREE.ShaderSkin = {
 			"varying vec3 vNormal;",
 			"varying vec2 vUv;",
 
-			"uniform vec3 ambientLightColor;",
-
-			"#if MAX_DIR_LIGHTS > 0",
-
-				"uniform vec3 directionalLightColor[ MAX_DIR_LIGHTS ];",
-				"uniform vec3 directionalLightDirection[ MAX_DIR_LIGHTS ];",
-
-			"#endif",
-
-			"#if MAX_HEMI_LIGHTS > 0",
-
-				"uniform vec3 hemisphereLightSkyColor[ MAX_HEMI_LIGHTS ];",
-				"uniform vec3 hemisphereLightGroundColor[ MAX_HEMI_LIGHTS ];",
-				"uniform vec3 hemisphereLightDirection[ MAX_HEMI_LIGHTS ];",
-
-			"#endif",
-
-			"#if MAX_POINT_LIGHTS > 0",
-
-				"uniform vec3 pointLightColor[ MAX_POINT_LIGHTS ];",
-				"uniform vec3 pointLightPosition[ MAX_POINT_LIGHTS ];",
-				"uniform float pointLightDistance[ MAX_POINT_LIGHTS ];",
-				"uniform float pointLightDecay[ MAX_POINT_LIGHTS ];",
-
-			"#endif",
-
 			"varying vec3 vViewPosition;",
 
 			THREE.ShaderChunk[ "common" ],
+			THREE.ShaderChunk[ "bsdfs" ],
+			THREE.ShaderChunk[ "lights_pars" ],
 			THREE.ShaderChunk[ "shadowmap_pars_fragment" ],
 			THREE.ShaderChunk[ "fog_pars_fragment" ],
 			THREE.ShaderChunk[ "bumpmap_pars_fragment" ],
@@ -194,13 +170,13 @@ THREE.ShaderSkin = {
 				"vec3 totalSpecularLight = vec3( 0.0 );",
 				"vec3 totalDiffuseLight = vec3( 0.0 );",
 
-				"#if MAX_POINT_LIGHTS > 0",
+				"#if NUM_POINT_LIGHTS > 0",
 
-					"for ( int i = 0; i < MAX_POINT_LIGHTS; i ++ ) {",
+					"for ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) {",
 
-						"vec3 lVector = pointLightPosition[ i ] + vViewPosition.xyz;",
+						"vec3 lVector = pointLights[ i ].position + vViewPosition.xyz;",
 
-						"float attenuation = calcLightAttenuation( length( lVector ), pointLightDistance[ i ], pointLightDecay[i] );",
+						"float attenuation = calcLightAttenuation( length( lVector ), pointLights[ i ].distance, pointLights[ i ].decay );",
 
 						"lVector = normalize( lVector );",
 
@@ -210,8 +186,8 @@ THREE.ShaderSkin = {
 
 						"float pointSpecularWeight = KS_Skin_Specular( normal, lVector, viewerDirection, uRoughness, uSpecularBrightness );",
 
-						"totalDiffuseLight += pointLightColor[ i ] * ( pointDiffuseWeight * attenuation );",
-						"totalSpecularLight += pointLightColor[ i ] * specular * ( pointSpecularWeight * specularStrength * attenuation );",
+						"totalDiffuseLight += pointLight[ i ].color * ( pointDiffuseWeight * attenuation );",
+						"totalSpecularLight += pointLight[ i ].color * specular * ( pointSpecularWeight * specularStrength * attenuation );",
 
 					"}",
 
@@ -219,11 +195,11 @@ THREE.ShaderSkin = {
 
 				// directional lights
 
-				"#if MAX_DIR_LIGHTS > 0",
+				"#if NUM_DIR_LIGHTS > 0",
 
-					"for( int i = 0; i < MAX_DIR_LIGHTS; i++ ) {",
+					"for( int i = 0; i < NUM_DIR_LIGHTS; i++ ) {",
 
-						"vec3 dirVector = directionalLightDirection[ i ];",
+						"vec3 dirVector = directionalLights[ i ].direction;",
 
 						"float dirDiffuseWeightFull = max( dot( normal, dirVector ), 0.0 );",
 						"float dirDiffuseWeightHalf = max( 0.5 * dot( normal, dirVector ) + 0.5, 0.0 );",
@@ -231,8 +207,8 @@ THREE.ShaderSkin = {
 
 						"float dirSpecularWeight = KS_Skin_Specular( normal, dirVector, viewerDirection, uRoughness, uSpecularBrightness );",
 
-						"totalDiffuseLight += directionalLightColor[ i ] * dirDiffuseWeight;",
-						"totalSpecularLight += directionalLightColor[ i ] * ( dirSpecularWeight * specularStrength );",
+						"totalDiffuseLight += directionalLights[ i ].color * dirDiffuseWeight;",
+						"totalSpecularLight += directionalLights[ i ].color * ( dirSpecularWeight * specularStrength );",
 
 					"}",
 
@@ -240,9 +216,9 @@ THREE.ShaderSkin = {
 
 				// hemisphere lights
 
-				"#if MAX_HEMI_LIGHTS > 0",
+				"#if NUM_HEMI_LIGHTS > 0",
 
-					"for ( int i = 0; i < MAX_HEMI_LIGHTS; i ++ ) {",
+					"for ( int i = 0; i < NUM_HEMI_LIGHTS; i ++ ) {",
 
 						"vec3 lVector = hemisphereLightDirection[ i ];",
 
@@ -394,21 +370,10 @@ THREE.ShaderSkin = {
 			"varying vec3 vNormal;",
 			"varying vec2 vUv;",
 
-			"uniform vec3 ambientLightColor;",
-
-			"#if MAX_DIR_LIGHTS > 0",
-				"uniform vec3 directionalLightColor[ MAX_DIR_LIGHTS ];",
-				"uniform vec3 directionalLightDirection[ MAX_DIR_LIGHTS ];",
-			"#endif",
-
-			"#if MAX_POINT_LIGHTS > 0",
-				"uniform vec3 pointLightColor[ MAX_POINT_LIGHTS ];",
-				"varying vec4 vPointLight[ MAX_POINT_LIGHTS ];",
-			"#endif",
-
 			"varying vec3 vViewPosition;",
 
 			THREE.ShaderChunk[ "common" ],
+			THREE.ShaderChunk[ "lights_pars" ],
 			THREE.ShaderChunk[ "fog_pars_fragment" ],
 
 			"float fresnelReflectance( vec3 H, vec3 V, float F0 ) {",
@@ -487,12 +452,12 @@ THREE.ShaderSkin = {
 				"vec3 totalDiffuseLight = vec3( 0.0 );",
 				"vec3 totalSpecularLight = vec3( 0.0 );",
 
-				"#if MAX_POINT_LIGHTS > 0",
+				"#if NUM_POINT_LIGHTS > 0",
 
-					"for ( int i = 0; i < MAX_POINT_LIGHTS; i ++ ) {",
+					"for ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) {",
 
-						"vec3 pointVector = normalize( vPointLight[ i ].xyz );",
-						"float attenuation = vPointLight[ i ].w;",
+						"vec3 pointVector = normalize( pointLights[ i ].direction );",
+						"float attenuation = calcLightAttenuation( length( lVector ), pointLights[ i ].distance, pointLights[ i ].decay );",
 
 						"float pointDiffuseWeight = max( dot( normal, pointVector ), 0.0 );",
 
@@ -512,21 +477,22 @@ THREE.ShaderSkin = {
 
 				// directional lights
 
-				"#if MAX_DIR_LIGHTS > 0",
+				"#if NUM_DIR_LIGHTS > 0",
 
-					"for( int i = 0; i < MAX_DIR_LIGHTS; i++ ) {",
+					"for( int i = 0; i < NUM_DIR_LIGHTS; i++ ) {",
 
-						"vec3 dirVector = directionalLightDirection[ i ];",
+						"vec3 dirVector = directionalLights[ i ].direction;",
 
 						"float dirDiffuseWeight = max( dot( normal, dirVector ), 0.0 );",
 
-						"totalDiffuseLight += directionalLightColor[ i ] * dirDiffuseWeight;",
+
+						"totalDiffuseLight += directionalLights[ i ].color * dirDiffuseWeight;",
 
 						"if ( passID == 1 ) {",
 
 							"float dirSpecularWeight = KS_Skin_Specular( normal, dirVector, viewerDirection, uRoughness, uSpecularBrightness );",
 
-							"totalSpecularLight += directionalLightColor[ i ] * mSpecular.xyz * dirSpecularWeight;",
+							"totalSpecularLight += directionalLights[ i ].color * mSpecular.xyz * dirSpecularWeight;",
 
 						"}",
 
@@ -607,16 +573,6 @@ THREE.ShaderSkin = {
 			"varying vec3 vNormal;",
 			"varying vec2 vUv;",
 
-			"#if MAX_POINT_LIGHTS > 0",
-
-				"uniform vec3 pointLightPosition[ MAX_POINT_LIGHTS ];",
-				"uniform float pointLightDistance[ MAX_POINT_LIGHTS ];",
-				"uniform float pointLightDecay[ MAX_POINT_LIGHTS ];",
-
-				"varying vec4 vPointLight[ MAX_POINT_LIGHTS ];",
-
-			"#endif",
-
 			"varying vec3 vViewPosition;",
 
 			THREE.ShaderChunk[ "common" ],
@@ -633,24 +589,6 @@ THREE.ShaderSkin = {
 
 				"vUv = uv;",
 
-				// point lights
-
-				"#if MAX_POINT_LIGHTS > 0",
-
-					"for( int i = 0; i < MAX_POINT_LIGHTS; i++ ) {",
-
-						"vec3 lVector = pointLightPosition[ i ] - vViewPosition;",
-
-						"float attenuation = calcLightAttenuation( length( lVector ), pointLightDistance[ i ], pointLightDecay[i] );",
-
-						"lVector = normalize( lVector );",
-
-						"vPointLight[ i ] = vec4( lVector, attenuation );",
-
-					"}",
-
-				"#endif",
-
 				// displacement mapping
 
 				"#ifdef VERTEX_TEXTURES",
@@ -675,16 +613,6 @@ THREE.ShaderSkin = {
 			"varying vec3 vNormal;",
 			"varying vec2 vUv;",
 
-			"#if MAX_POINT_LIGHTS > 0",
-
-				"uniform vec3 pointLightPosition[ MAX_POINT_LIGHTS ];",
-				"uniform float pointLightDistance[ MAX_POINT_LIGHTS ];",
-				"uniform float pointLightDecay[ MAX_POINT_LIGHTS ];",
-
-				"varying vec4 vPointLight[ MAX_POINT_LIGHTS ];",
-
-			"#endif",
-
 			"varying vec3 vViewPosition;",
 
 			THREE.ShaderChunk[ "common" ],
@@ -701,24 +629,6 @@ THREE.ShaderSkin = {
 
 				"vUv = uv;",
 
-				// point lights
-
-				"#if MAX_POINT_LIGHTS > 0",
-
-					"for( int i = 0; i < MAX_POINT_LIGHTS; i++ ) {",
-
-						"vec3 lVector = pointLightPosition[ i ] - vViewPosition;",
-
-						"float attenuation = calcLightAttenuation( length( lVector ), pointLightDistance[ i ], pointLightDecay[i] );",
-
-						"lVector = normalize( lVector );",
-
-						"vPointLight[ i ] = vec4( lVector, attenuation );",
-
-					"}",
-
-				"#endif",
-
 				"gl_Position = vec4( uv.x * 2.0 - 1.0, uv.y * 2.0 - 1.0, 0.0, 1.0 );",
 
 			"}"

+ 17 - 39
examples/js/ShaderTerrain.js

@@ -86,33 +86,11 @@ THREE.ShaderTerrain = {
 
 			"uniform vec3 ambientLightColor;",
 
-			"#if MAX_DIR_LIGHTS > 0",
-
-				"uniform vec3 directionalLightColor[ MAX_DIR_LIGHTS ];",
-				"uniform vec3 directionalLightDirection[ MAX_DIR_LIGHTS ];",
-
-			"#endif",
-
-			"#if MAX_HEMI_LIGHTS > 0",
-
-				"uniform vec3 hemisphereLightSkyColor[ MAX_HEMI_LIGHTS ];",
-				"uniform vec3 hemisphereLightGroundColor[ MAX_HEMI_LIGHTS ];",
-				"uniform vec3 hemisphereLightDirection[ MAX_HEMI_LIGHTS ];",
-
-			"#endif",
-
-			"#if MAX_POINT_LIGHTS > 0",
-
-				"uniform vec3 pointLightColor[ MAX_POINT_LIGHTS ];",
-				"uniform vec3 pointLightPosition[ MAX_POINT_LIGHTS ];",
-				"uniform float pointLightDistance[ MAX_POINT_LIGHTS ];",
-				"uniform float pointLightDecay[ MAX_POINT_LIGHTS ];",
-
-			"#endif",
-
 			"varying vec3 vViewPosition;",
 
 			THREE.ShaderChunk[ "common" ],
+			THREE.ShaderChunk[ "bsdfs" ],
+			THREE.ShaderChunk[ "lights_pars" ],
 			THREE.ShaderChunk[ "shadowmap_pars_fragment" ],
 			THREE.ShaderChunk[ "fog_pars_fragment" ],
 
@@ -164,13 +142,13 @@ THREE.ShaderTerrain = {
 
 				// point lights
 
-				"#if MAX_POINT_LIGHTS > 0",
+				"#if NUM_POINT_LIGHTS > 0",
 
-					"for ( int i = 0; i < MAX_POINT_LIGHTS; i ++ ) {",
+					"for ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) {",
 
-						"vec3 lVector = pointLightPosition[ i ] + vViewPosition.xyz;",
+						"vec3 lVector = pointLights[ i ].position + vViewPosition.xyz;",
 
-						"float attenuation = calcLightAttenuation( length( lVector ), pointLightDistance[ i ], pointLightDecay[i] );",
+						"float attenuation = calcLightAttenuation( length( lVector ), pointLights[ i ].distance, pointLights[ i ].decay );",
 
 						"lVector = normalize( lVector );",
 
@@ -181,8 +159,8 @@ THREE.ShaderTerrain = {
 
 						"float pointSpecularWeight = specularTex.r * max( pow( pointDotNormalHalf, shininess ), 0.0 );",
 
-						"totalDiffuseLight += attenuation * pointLightColor[ i ] * pointDiffuseWeight;",
-						"totalSpecularLight += attenuation * pointLightColor[ i ] * specular * pointSpecularWeight * pointDiffuseWeight;",
+						"totalDiffuseLight += attenuation * pointLights[ i ].color * pointDiffuseWeight;",
+						"totalSpecularLight += attenuation * pointLights[ i ].color * specular * pointSpecularWeight * pointDiffuseWeight;",
 
 					"}",
 
@@ -190,14 +168,14 @@ THREE.ShaderTerrain = {
 
 				// directional lights
 
-				"#if MAX_DIR_LIGHTS > 0",
+				"#if NUM_DIR_LIGHTS > 0",
 
 					"vec3 dirDiffuse = vec3( 0.0 );",
 					"vec3 dirSpecular = vec3( 0.0 );",
 
-					"for( int i = 0; i < MAX_DIR_LIGHTS; i++ ) {",
+					"for( int i = 0; i < NUM_DIR_LIGHTS; i++ ) {",
 
-						"vec3 dirVector = directionalLightDirection[ i ];",
+						"vec3 dirVector = directionalLights[ i ].direction;",
 						"vec3 dirHalfVector = normalize( dirVector + viewPosition );",
 
 						"float dirDotNormalHalf = max( dot( normal, dirHalfVector ), 0.0 );",
@@ -205,8 +183,8 @@ THREE.ShaderTerrain = {
 
 						"float dirSpecularWeight = specularTex.r * max( pow( dirDotNormalHalf, shininess ), 0.0 );",
 
-						"totalDiffuseLight += directionalLightColor[ i ] * dirDiffuseWeight;",
-						"totalSpecularLight += directionalLightColor[ i ] * specular * dirSpecularWeight * dirDiffuseWeight;",
+						"totalDiffuseLight += directionalLights[ i ].color * dirDiffuseWeight;",
+						"totalSpecularLight += directionalLights[ i ].color * specular * dirSpecularWeight * dirDiffuseWeight;",
 
 					"}",
 
@@ -214,12 +192,12 @@ THREE.ShaderTerrain = {
 
 				// hemisphere lights
 
-				"#if MAX_HEMI_LIGHTS > 0",
+				"#if NUM_HEMI_LIGHTS > 0",
 
 					"vec3 hemiDiffuse  = vec3( 0.0 );",
 					"vec3 hemiSpecular = vec3( 0.0 );",
 
-					"for( int i = 0; i < MAX_HEMI_LIGHTS; i ++ ) {",
+					"for( int i = 0; i < NUM_HEMI_LIGHTS; i ++ ) {",
 
 						"vec3 lVector = hemisphereLightDirection[ i ];",
 
@@ -228,7 +206,7 @@ THREE.ShaderTerrain = {
 						"float dotProduct = dot( normal, lVector );",
 						"float hemiDiffuseWeight = 0.5 * dotProduct + 0.5;",
 
-						"totalDiffuseLight += mix( hemisphereLightGroundColor[ i ], hemisphereLightSkyColor[ i ], hemiDiffuseWeight );",
+						"totalDiffuseLight += mix( hemisphereLights[ i ].groundColor, hemisphereLights[ i ].skyColor, hemiDiffuseWeight );",
 
 						// specular (sky light)
 
@@ -246,7 +224,7 @@ THREE.ShaderTerrain = {
 						"float hemiDotNormalHalfGround = 0.5 * dot( normal, hemiHalfVectorGround ) + 0.5;",
 						"hemiSpecularWeight += specularTex.r * max( pow( hemiDotNormalHalfGround, shininess ), 0.0 );",
 
-						"totalSpecularLight += specular * mix( hemisphereLightGroundColor[ i ], hemisphereLightSkyColor[ i ], hemiDiffuseWeight ) * hemiSpecularWeight * hemiDiffuseWeight;",
+						"totalSpecularLight += specular * mix( hemisphereLights[ i ].groundColor, hemisphereLights[ i ].skyColor, hemiDiffuseWeight ) * hemiSpecularWeight * hemiDiffuseWeight;",
 
 					"}",
 

+ 280 - 0
examples/js/TimelinerController.js

@@ -0,0 +1,280 @@
+/**
+ * Controller class for the Timeliner GUI.
+ *
+ * Timeliner GUI library (required to use this class):
+ *
+ * 		./libs/timeliner_gui.min.js
+ *
+ * Source code:
+ *
+ * 		https://github.com/tschw/timeliner_gui
+ * 		https://github.com/zz85/timeliner (fork's origin)
+ *
+ * @author tschw
+ *
+ */
+
+THREE.TimelinerController = function TimelinerController( scene, trackInfo, onUpdate ) {
+
+	this._scene = scene;
+	this._trackInfo = trackInfo;
+
+	this._onUpdate = onUpdate;
+
+	this._mixer = new THREE.AnimationMixer( scene );
+	this._clip = null;
+	this._action = null;
+
+	this._tracks = {};
+	this._propRefs = {};
+	this._channelNames = [];
+
+};
+
+THREE.TimelinerController.prototype = {
+
+	constructor: THREE.TimelinerController,
+
+	init: function( timeliner ) {
+
+		var tracks = [],
+			trackInfo = this._trackInfo;
+
+		for ( var i = 0, n = trackInfo.length; i !== n; ++ i ) {
+
+			var spec = trackInfo[ i ];
+
+			tracks.push( this._addTrack(
+					spec.type, spec.propertyPath,
+					spec.initialValue, spec.interpolation ) );
+		}
+
+		this._clip = new THREE.AnimationClip( 'editclip', 0, tracks );
+		this._action = this._mixer.clipAction( this._clip ).play();
+
+	},
+
+	setDisplayTime: function( time ) {
+
+		this._action.time = time;
+		this._mixer.update( 0 );
+
+		this._onUpdate();
+
+	},
+
+	setDuration: function( duration ) {
+
+		this._clip.duration = duration;
+
+	},
+
+	getChannelNames: function() {
+
+		return this._channelNames;
+
+	},
+
+	getChannelKeyTimes: function( channelName ) {
+
+		return this._tracks[ channelName ].times;
+
+	},
+
+	setKeyframe: function( channelName, time ) {
+
+		var track = this._tracks[ channelName ],
+			times = track.times,
+			index = Timeliner.binarySearch( times, time ),
+			values = track.values,
+			stride = track.getValueSize(),
+			offset = index * stride;
+
+		if ( index < 0 ) {
+
+			// insert new keyframe
+
+			index = ~ index;
+			offset = index * stride;
+
+			var nTimes = times.length + 1,
+				nValues = values.length + stride;
+
+			for ( var i = nTimes - 1; i !== index; -- i ) {
+
+				times[ i ] = times[ i - 1 ];
+
+			}
+
+			for ( var i = nValues - 1,
+					e = offset + stride - 1; i !== e; -- i ) {
+
+				values[ i ] = values[ i - stride ];
+
+			}
+
+		}
+
+		times[ index ] = time;
+		this._propRefs[ channelName ].getValue( values, offset );
+
+	},
+
+	delKeyframe: function( channelName, time ) {
+
+		var track = this._tracks[ channelName ],
+			times = track.times,
+			index = Timeliner.binarySearch( times, time );
+
+		// we disallow to remove the keyframe when it is the last one we have,
+		// since the animation system is designed to always produce a defined
+		// state
+
+		if ( times.length > 1 && index >= 0 ) {
+
+			var nTimes = times.length - 1,
+				values = track.values,
+				stride = track.getValueSize(),
+				nValues = values.length - stride;
+
+			// note: no track.getValueSize when array sizes are out of sync
+
+			for ( var i = index; i !== nTimes; ++ i ) {
+
+				times[ i ] = times[ i + 1 ];
+
+			}
+
+			times.pop();
+
+			for ( var offset = index * stride; offset !== nValues; ++ offset ) {
+
+				values[ offset ] = values[ offset + stride ];
+
+			}
+
+			values.length = nValues;
+
+		}
+
+	},
+
+	moveKeyframe: function( channelName, time, delta, moveRemaining ) {
+
+		var track = this._tracks[ channelName ],
+			times = track.times,
+			index = Timeliner.binarySearch( times, time );
+
+		if ( index >= 0 ) {
+
+			var endAt = moveRemaining ? times.length : index + 1,
+				needsSort = times[ index - 1 ] <= time ||
+					! moveRemaining && time >= times[ index + 1 ];
+
+			while ( index !== endAt ) times[ index ++ ] += delta;
+
+			if ( needsSort ) this._sort( track );
+
+		}
+
+	},
+
+	serialize: function() {
+
+		var result = {
+				duration: this._clip.duration,
+				channels: {}
+			},
+
+			names = this._channelNames,
+			tracks = this._tracks,
+
+			channels = result.channels;
+
+		for ( var i = 0, n = names.length; i !== n; ++ i ) {
+
+			var name = names[ i ],
+				track = tracks[ name ];
+
+			channels[ name ] = {
+
+				times: track.times,
+				values: track.values
+
+			};
+
+		}
+
+		return result;
+
+	},
+
+	deserialize: function( structs ) {
+
+		var names = this._channelNames,
+			tracks = this._tracks,
+
+			channels = structs.channels;
+
+		this.setDuration( structs.duration );
+
+		for ( var i = 0, n = names.length; i !== n; ++ i ) {
+
+			var name = names[ i ],
+				track = tracks[ name ];
+				data = channels[ name ];
+
+			this._setArray( track.times, data.times );
+			this._setArray( track.values, data.values );
+
+		}
+
+		// update display
+		this.setDisplayTime( this._mixer.time );
+
+	},
+
+	_sort: function( track ) {
+
+		var times = track.times,
+			order = THREE.AnimationUtils.getKeyframeOrder( times );
+
+		this._setArray( times,
+				THREE.AnimationUtils.sortedArray( times, 1, order ) );
+
+		var values = track.values,
+			stride = track.getValueSize();
+
+		this._setArray( values,
+				THREE.AnimationUtils.sortedArray( values, stride, order ) );
+
+	},
+
+	_setArray: function( dst, src ) {
+
+		dst.length = 0;
+		dst.push.apply( dst, src );
+
+	},
+
+	_addTrack: function( type, prop, initialValue, interpolation ) {
+
+		var track = new type(
+				prop, [ 0 ], initialValue, interpolation );
+
+		// data must be in JS arrays so it can be resized
+		track.times = Array.prototype.slice.call( track.times );
+		track.values = Array.prototype.slice.call( track.values );
+
+		this._channelNames.push( prop );
+		this._tracks[ prop ] = track;
+
+		// for recording the state:
+		this._propRefs[ prop ] =
+				new THREE.PropertyBinding( this._scene, prop );
+
+		return track;
+
+	}
+
+};

+ 1 - 1
examples/js/UCSCharacter.js

@@ -55,7 +55,7 @@ THREE.UCSCharacter = function() {
 			mesh.castShadow = true;
 			mesh.receiveShadow = true;
 
-			scope.mixer.addAction( new THREE.AnimationAction( geometry.animations[0] ).setLocalRoot( mesh ) );
+			scope.mixer.clipAction( geometry.animations[0], mesh ).play();
 			
 			scope.setSkin( 0 );
 			

+ 170 - 0
examples/js/animation/CCDIKSolver.js

@@ -0,0 +1,170 @@
+/**
+ * @author takahiro / https://github.com/takahirox
+ *
+ * CCD Algorithm
+ *  https://sites.google.com/site/auraliusproject/ccd-algorithm
+ *
+ * mesh.geometry needs to have iks array.
+ *
+ * ik parameter example
+ *
+ * ik = {
+ *	target: 1,
+ *	effector: 2,
+ *	links: [ { index: 5 }, { index: 4, limitation: new THREE.Vector3( 1, 0, 0 ) }, { index : 3 } ],
+ *	iteration: 10,
+ *	minAngle: 0.0,
+ *	maxAngle: 1.0,
+ * };
+ */
+
+THREE.CCDIKSolver = function ( mesh ) {
+
+	this.mesh = mesh;
+
+	var bones = mesh.skeleton.bones;
+	this.orgBones = [];
+	for ( var i = 0; i < bones.length; i++ ) {
+
+		this.orgBones.push( bones[ i ].clone() );
+
+	}
+
+};
+
+THREE.CCDIKSolver.prototype = {
+
+	constructor: THREE.CCDIKSolver,
+
+	update: function () {
+
+		var effectorVec = new THREE.Vector3();
+		var targetVec = new THREE.Vector3();
+		var axis = new THREE.Vector3();
+		var q = new THREE.Quaternion();
+
+		var bones = this.mesh.skeleton.bones;
+		var iks = this.mesh.geometry.iks;
+
+		// for reference overhead reduction in loop
+		var math = Math;
+
+		for ( var i = 0, il = iks.length; i < il; i++ ) {
+
+			var ik = iks[ i ];
+			var effector = bones[ ik.effector ];
+			var target = bones[ ik.target ];
+			var targetPos = target.getWorldPosition();
+			var links = ik.links;
+			var iteration = ik.iteration !== undefined ? ik.iteration : 1;
+
+			for ( var j = 0; j < iteration; j++ ) {
+
+				for ( var k = 0, kl = links.length; k < kl; k++ ) {
+
+					var link = bones[ links[ k ].index ];
+					var limitation = links[ k ].limitation;
+					var linkPos = link.getWorldPosition();
+					var invLinkQ = link.getWorldQuaternion().inverse();
+					var effectorPos = effector.getWorldPosition();
+
+					// work in link world
+					effectorVec.subVectors( effectorPos, linkPos );
+					effectorVec.applyQuaternion( invLinkQ );
+					effectorVec.normalize();
+
+					targetVec.subVectors( targetPos, linkPos );
+					targetVec.applyQuaternion( invLinkQ );
+					targetVec.normalize();
+
+					var angle = targetVec.dot( effectorVec );
+
+					// TODO: continue (or break) the loop for the performance
+					//       if no longer needs to rotate (angle > 1.0-1e-5 ?)
+
+					if ( angle > 1.0 ) {
+
+						angle = 1.0;
+
+					} else if ( angle < -1.0 ) {
+
+						angle = -1.0;
+
+					}
+
+					angle = math.acos( angle );
+
+					if ( ik.minAngle !== undefined && angle < ik.minAngle ) {
+
+						angle = ik.minAngle;
+
+					}
+
+					if ( ik.maxAngle !== undefined && angle > ik.maxAngle ) {
+
+						angle = ik.maxAngle;
+
+					}
+
+					axis.crossVectors( effectorVec, targetVec );
+					axis.normalize();
+
+					q.setFromAxisAngle( axis, angle );
+					link.quaternion.multiply( q );
+
+					// TODO: re-consider the limitation specification
+					if ( limitation !== undefined ) {
+
+						var c = link.quaternion.w;
+
+						if ( c > 1.0 ) {
+
+							c = 1.0;
+
+						}
+
+						var c2 = math.sqrt( 1 - c * c );
+						link.quaternion.set( limitation.x * c2,
+						                     limitation.y * c2,
+						                     limitation.z * c2,
+						                     c );
+
+					}
+
+					link.updateMatrixWorld( true );
+
+				}
+
+			}
+
+		}
+
+	},
+
+	resetLinks: function () {
+
+		var bones = this.mesh.skeleton.bones;
+		var iks = this.mesh.geometry.iks;
+
+		for ( var i = 0, il = iks.length; i < il; i++ ) {
+
+			var ik = iks[ i ];
+			var links = ik.links;
+
+			for ( var j = 0; j < links.length; j++ ) {
+
+				var link = links[ j ];
+				var index = link.index;
+				var b = bones[ index ];
+				var b2 = this.orgBones[ index ];
+				//b.position.copy( b2.position );
+				b.quaternion.copy( b2.quaternion );
+
+			}
+
+		}
+
+	}
+
+};
+

+ 2 - 2
examples/js/controls/EditorControls.js

@@ -272,7 +272,7 @@ THREE.EditorControls = function ( object, domElement ) {
 		event.preventDefault();
 		event.stopPropagation();
 
-		var getClosest = function( touch, touches ) {
+		function getClosest( touch, touches ) {
 
 			var closest = touches[ 0 ];
 
@@ -284,7 +284,7 @@ THREE.EditorControls = function ( object, domElement ) {
 
 			return closest;
 
-		};
+		}
 
 		switch ( event.touches.length ) {
 

部分文件因为文件数量过多而无法显示