2
0
Эх сурвалжийг харах

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

Conflicts:
	examples/js/loaders/VTKLoader.js
ALEXANDER PLETZER 9 жил өмнө
parent
commit
5bb75b3f66
100 өөрчлөгдсөн 6028 нэмэгдсэн , 2433 устгасан
  1. 1 0
      .gitignore
  2. 8 8
      bower.json
  3. 812 278
      build/three.js
  4. 357 313
      build/three.min.js
  5. 2 0
      docs/api/core/Geometry.html
  6. 2 2
      docs/api/examples/SpriteCanvasMaterial.html
  7. 0 88
      docs/api/extras/ImageUtils.html
  8. 0 20
      docs/api/extras/geometries/CubeGeometry.html
  9. 1 1
      docs/api/loaders/MTLLoader.html
  10. 0 8
      docs/api/loaders/MaterialLoader.html
  11. 0 5
      docs/api/loaders/XHRLoader.html
  12. 62 62
      docs/api/materials/LineBasicMaterial.html
  13. 19 7
      docs/api/materials/MeshPhongMaterial.html
  14. 9 9
      docs/api/math/Box2.html
  15. 14 14
      docs/api/math/Box3.html
  16. 3 3
      docs/api/math/Matrix4.html
  17. 4 4
      docs/api/math/Plane.html
  18. 9 9
      docs/api/math/Ray.html
  19. 11 13
      docs/api/textures/CubeTexture.html
  20. 2 4
      docs/list.js
  21. 34 33
      docs/scenes/bones-browser.html
  22. 2 1
      docs/scenes/geometry-browser.html
  23. 28 27
      docs/scenes/material-browser.html
  24. 24 17
      editor/css/dark.css
  25. 24 17
      editor/css/light.css
  26. 6 6
      editor/css/main.css
  27. 132 0
      editor/docs/Implementing additional commands for undo-redo.md
  28. 94 0
      editor/docs/Writing unit tests for undo-redo commands.md
  29. 41 5
      editor/index.html
  30. 47 0
      editor/js/Command.js
  31. 2 0
      editor/js/Config.js
  32. 32 2
      editor/js/Editor.js
  33. 277 35
      editor/js/History.js
  34. 100 80
      editor/js/Loader.js
  35. 26 43
      editor/js/Menubar.Add.js
  36. 59 14
      editor/js/Menubar.Edit.js
  37. 3 0
      editor/js/Menubar.File.js
  38. 6 9
      editor/js/Menubar.Status.js
  39. 66 17
      editor/js/Script.js
  40. 5 9
      editor/js/Sidebar.Geometry.BoxGeometry.js
  41. 3 1
      editor/js/Sidebar.Geometry.BufferGeometry.js
  42. 5 7
      editor/js/Sidebar.Geometry.CircleGeometry.js
  43. 5 9
      editor/js/Sidebar.Geometry.CylinderGeometry.js
  44. 3 1
      editor/js/Sidebar.Geometry.Geometry.js
  45. 5 7
      editor/js/Sidebar.Geometry.IcosahedronGeometry.js
  46. 3 1
      editor/js/Sidebar.Geometry.Modifiers.js
  47. 5 9
      editor/js/Sidebar.Geometry.PlaneGeometry.js
  48. 5 9
      editor/js/Sidebar.Geometry.SphereGeometry.js
  49. 5 9
      editor/js/Sidebar.Geometry.TorusGeometry.js
  50. 5 9
      editor/js/Sidebar.Geometry.TorusKnotGeometry.js
  51. 19 20
      editor/js/Sidebar.Geometry.js
  52. 135 0
      editor/js/Sidebar.History.js
  53. 282 57
      editor/js/Sidebar.Material.js
  54. 73 63
      editor/js/Sidebar.Object3D.js
  55. 7 24
      editor/js/Sidebar.Project.js
  56. 1 1
      editor/js/Sidebar.Scene.js
  57. 4 5
      editor/js/Sidebar.Script.js
  58. 1 0
      editor/js/Sidebar.js
  59. 4 7
      editor/js/Toolbar.js
  60. 43 57
      editor/js/Viewport.js
  61. 66 0
      editor/js/commands/AddObjectCommand.js
  62. 76 0
      editor/js/commands/AddScriptCommand.js
  63. 107 0
      editor/js/commands/MoveObjectCommand.js
  64. 85 0
      editor/js/commands/MultiCmdsCommand.js
  65. 103 0
      editor/js/commands/RemoveObjectCommand.js
  66. 81 0
      editor/js/commands/RemoveScriptCommand.js
  67. 74 0
      editor/js/commands/SetColorCommand.js
  68. 86 0
      editor/js/commands/SetGeometryCommand.js
  69. 71 0
      editor/js/commands/SetGeometryValueCommand.js
  70. 74 0
      editor/js/commands/SetMaterialColorCommand.js
  71. 74 0
      editor/js/commands/SetMaterialCommand.js
  72. 125 0
      editor/js/commands/SetMaterialMapCommand.js
  73. 76 0
      editor/js/commands/SetMaterialValueCommand.js
  74. 83 0
      editor/js/commands/SetPositionCommand.js
  75. 84 0
      editor/js/commands/SetRotationCommand.js
  76. 84 0
      editor/js/commands/SetScaleCommand.js
  77. 100 0
      editor/js/commands/SetSceneCommand.js
  78. 88 0
      editor/js/commands/SetScriptValueCommand.js
  79. 71 0
      editor/js/commands/SetUuidCommand.js
  80. 76 0
      editor/js/commands/SetValueCommand.js
  81. 14 1
      editor/js/libs/app.js
  82. 47 10
      editor/js/libs/ui.three.js
  83. 1 1
      examples/canvas_morphtargets_horse.html
  84. 13 2
      examples/index.html
  85. 33 57
      examples/js/AnimationClipCreator.js
  86. 23 28
      examples/js/BlendCharacter.js
  87. 3 12
      examples/js/BlendCharacterGui.js
  88. 26 12
      examples/js/MD2Character.js
  89. 3 4
      examples/js/MorphAnimMesh.js
  90. 26 116
      examples/js/ShaderSkin.js
  91. 17 39
      examples/js/ShaderTerrain.js
  92. 281 0
      examples/js/TimelinerController.js
  93. 1 1
      examples/js/UCSCharacter.js
  94. 170 0
      examples/js/animation/CCDIKSolver.js
  95. 2 2
      examples/js/controls/EditorControls.js
  96. 586 648
      examples/js/controls/OrbitControls.js
  97. 10 40
      examples/js/controls/TrackballControls.js
  98. 6 0
      examples/js/controls/TransformControls.js
  99. 139 0
      examples/js/effects/PeppersGhostEffect.js
  100. 6 1
      examples/js/exporters/OBJExporter.js

+ 1 - 0
.gitignore

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

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

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 812 - 278
build/three.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 357 - 313
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>

+ 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

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

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

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

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

+ 2 - 0
editor/js/Config.js

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

+ 32 - 2
editor/js/Editor.js

@@ -62,7 +62,10 @@ var Editor = function () {
 		fogParametersChanged: new SIGNALS.Signal(),
 		windowResize: new SIGNALS.Signal(),
 
-		showGridChanged: new SIGNALS.Signal()
+		showGridChanged: new SIGNALS.Signal(),
+		refreshSidebarObject3D: new SIGNALS.Signal(),
+		historyChanged: new SIGNALS.Signal(),
+		refreshScriptEditor: new SIGNALS.Signal()
 
 	};
 
@@ -454,6 +457,7 @@ Editor.prototype = {
 
 		this.setScene( loader.parse( json.scene ) );
 		this.scripts = json.scripts;
+		this.history.fromJSON( json.history );
 
 	},
 
@@ -461,16 +465,42 @@ Editor.prototype = {
 
 		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 );
 
 	}
 

+ 100 - 80
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 );
@@ -208,8 +204,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.readAsArrayBuffer( file );
@@ -224,7 +219,7 @@ var Loader = function ( editor ) {
 						var contents = event.target.result;
 
 						var geometry = new THREE.MD2Loader().parse( contents );
-						var material = new THREE.MeshPhongMaterial( {
+						var material = new THREE.MeshStandardMaterial( {
 							morphTargets: true,
 							morphNormals: true
 						} );
@@ -233,8 +228,7 @@ var Loader = function ( editor ) {
 						mesh.mixer = new THREE.AnimationMixer( mesh )
 						mesh.name = filename;
 
-						editor.addObject( mesh );
-						editor.select( mesh );
+						editor.execute( new AddObjectCommand( mesh ) );
 
 					}, false );
 					reader.readAsArrayBuffer( file );
@@ -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 );
@@ -388,7 +395,7 @@ 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() ) {
+
+			case 'buffergeometry':
 
-			var loader = new THREE.BufferGeometryLoader();
-			var result = loader.parse( data );
+				var loader = new THREE.BufferGeometryLoader();
+				var result = loader.parse( data );
 
-			var mesh = new THREE.Mesh( result );
+				var mesh = new THREE.Mesh( result );
 
-			editor.addObject( mesh );
-			editor.select( mesh );
+				editor.execute( new AddObjectCommand( mesh ) );
+
+				break;
 
-		} else if ( data.metadata.type.toLowerCase() === 'geometry' ) {
+			case 'geometry':
 
-			var loader = new THREE.JSONLoader();
-			loader.setTexturePath( scope.texturePath );
+				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 ) {
+
+					mesh = new THREE.SkinnedMesh( geometry, material );
+
+				} else {
 
-			}
+					mesh = new THREE.Mesh( geometry, material );
 
-			geometry.sourceType = "ascii";
-			geometry.sourceFile = file.name;
+				}
 
-			var mesh;
+				mesh.name = filename;
 
-			if ( geometry.animation && geometry.animation.hierarchy ) {
+				editor.execute( new AddObjectCommand( mesh ) );
 
-				mesh = new THREE.SkinnedMesh( geometry, material );
+				break;
 
-			} else {
+			case 'object':
 
-				mesh = new THREE.Mesh( geometry, material );
+				var loader = new THREE.ObjectLoader();
+				loader.setTexturePath( scope.texturePath );
 
-			}
+				var result = loader.parse( data );
 
-			mesh.name = filename;
+				if ( result instanceof THREE.Scene ) {
 
-			editor.addObject( mesh );
-			editor.select( mesh );
+					editor.execute( new SetSceneCommand( result ) );
 
-		} else if ( data.metadata.type.toLowerCase() === 'object' ) {
+				} else {
 
-			var loader = new THREE.ObjectLoader();
-			loader.setTexturePath( scope.texturePath );
+					editor.execute( new AddObjectCommand( result ) );
 
-			var result = loader.parse( data );
+				}
 
-			if ( result instanceof THREE.Scene ) {
+				break;
 
-				editor.setScene( result );
+			case 'scene':
 
-			} else {
+				// DEPRECATED
 
-				editor.addObject( result );
-				editor.select( result );
+				var loader = new THREE.SceneLoader();
+				loader.parse( data, function ( result ) {
 
-			}
+					editor.execute( new SetSceneCommand( result.scene ) );
 
-		} else if ( data.metadata.type.toLowerCase() === 'scene' ) {
+				}, '' );
 
-			// DEPRECATED
+				break;
 
-			var loader = new THREE.SceneLoader();
-			loader.parse( data, function ( result ) {
+			case 'app':
 
-				editor.setScene( result.scene );
+				editor.fromJSON( data );
 
-			}, '' );
+				break;
 
 		}
 
-	};
+	}
 
 }

+ 26 - 43
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 );
@@ -64,12 +63,11 @@ Menubar.Add = function ( editor ) {
 		var heightSegments = 1;
 
 		var geometry = new THREE.PlaneGeometry( width, height, widthSegments, heightSegments );
-		var material = new THREE.MeshPhongMaterial();
+		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 );
@@ -90,11 +88,10 @@ Menubar.Add = function ( editor ) {
 		var depthSegments = 1;
 
 		var geometry = new THREE.BoxGeometry( width, height, depth, widthSegments, heightSegments, depthSegments );
-		var mesh = new THREE.Mesh( geometry, new THREE.MeshPhongMaterial() );
+		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 );
@@ -110,11 +107,10 @@ Menubar.Add = function ( editor ) {
 		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 );
@@ -134,11 +130,10 @@ Menubar.Add = function ( editor ) {
 		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 );
@@ -159,11 +154,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 );
@@ -179,11 +173,10 @@ Menubar.Add = function ( editor ) {
 		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 );
@@ -202,11 +195,10 @@ Menubar.Add = function ( editor ) {
 		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 );
@@ -227,11 +219,10 @@ Menubar.Add = function ( editor ) {
 		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 +243,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 +266,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 +289,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 );
@@ -325,8 +313,7 @@ Menubar.Add = function ( editor ) {
 
 		light.position.set( 0.5, 1, 0.75 ).multiplyScalar( 200 );
 
-		editor.addObject( light );
-		editor.select( light );
+		editor.execute( new AddObjectCommand( light ) );
 
 	} );
 	options.add( option );
@@ -347,8 +334,7 @@ Menubar.Add = function ( editor ) {
 
 		light.position.set( 0.5, 1, 0.75 ).multiplyScalar( 200 );
 
-		editor.addObject( light );
-		editor.select( light );
+		editor.execute( new AddObjectCommand( light ) );
 
 	} );
 	options.add( option );
@@ -367,10 +353,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, 100, 0 );
 
-		editor.addObject( light );
-		editor.select( light );
+		editor.execute( new AddObjectCommand( light ) );
 
 	} );
 	options.add( option );
@@ -387,8 +372,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 +391,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" ) );
 

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

@@ -247,6 +247,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' );
 

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

+ 282 - 57
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( 1 ).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();
@@ -343,7 +388,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 +445,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 +455,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 +464,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 ) {
+
+				editor.execute( new SetMaterialValueCommand( currentObject, 'roughness', materialRoughness.getValue() ) );
+
+			}
+
+			if ( material.metalness !== undefined && Math.abs( material.metalness - materialMetalness.getValue() ) >= 0.01 ) {
 
-				material.color.setHex( materialColor.getHexValue() );
+				editor.execute( new SetMaterialValueCommand( currentObject, 'metalness', materialMetalness.getValue() ) );
 
 			}
 
-			if ( material.emissive !== undefined ) {
+			if ( material.emissive !== undefined && material.emissive.getHex() !== materialEmissive.getHexValue() ) {
 
-				material.emissive.setHex( materialEmissive.getHexValue() );
+				editor.execute( new SetMaterialColorCommand( currentObject, 'emissive', materialEmissive.getHexValue() ) );
 
 			}
 
-			if ( material.specular !== undefined ) {
+			if ( material.specular !== undefined && material.specular.getHex() !== materialSpecular.getHexValue() ) {
 
-				material.specular.setHex( materialSpecular.getHexValue() );
+				editor.execute( new SetMaterialColorCommand( currentObject, 'specular', materialSpecular.getHexValue() ) );
 
 			}
 
-			if ( material.shininess !== undefined ) {
+			if ( material.shininess !== undefined && Math.abs( material.shininess - materialShininess.getValue() ) >= 0.01 ) {
 
-				material.shininess = materialShininess.getValue();
+				editor.execute( new SetMaterialValueCommand( currentObject, 'shininess', materialShininess.getValue() ) );
 
 			}
 
@@ -449,16 +506,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 +524,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 +545,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 +566,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 +593,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 +614,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 +635,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 +714,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 +736,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 +757,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 {
 
@@ -614,49 +780,65 @@ Sidebar.Material = function ( editor ) {
 
 			if ( material.side !== undefined ) {
 
-				material.side = parseInt( materialSide.getValue() );
+				var side = parseInt( materialSide.getValue() );
+				if ( material.side !== side ) {
+
+					editor.execute( new 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() ) );
 
 			}
 
@@ -681,6 +863,8 @@ Sidebar.Material = function ( editor ) {
 		var properties = {
 			'name': materialNameRow,
 			'color': materialColorRow,
+			'roughness': materialRoughnessRow,
+			'metalness': materialMetalnessRow,
 			'emissive': materialEmissiveRow,
 			'specular': materialSpecularRow,
 			'shininess': materialShininessRow,
@@ -692,6 +876,8 @@ Sidebar.Material = function ( editor ) {
 			'bumpMap': materialBumpMapRow,
 			'normalMap': materialNormalMapRow,
 			'displacementMap': materialDisplacementMapRow,
+			'roughnessMap': materialRoughnessMapRow,
+			'metalnessMap': materialMetalnessMapRow,
 			'specularMap': materialSpecularMapRow,
 			'envMap': materialEnvMapRow,
 			'lightMap': materialLightMapRow,
@@ -718,6 +904,8 @@ Sidebar.Material = function ( editor ) {
 
 	function refreshUi( resetTextureSelectors ) {
 
+		if ( !currentObject ) return;
+
 		var material = currentObject.material;
 
 		if ( material.uuid !== undefined ) {
@@ -740,6 +928,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 +1034,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 );
@@ -959,6 +1183,7 @@ Sidebar.Material = function ( editor ) {
 
 	} );
 
+	signals.materialChanged.add( function () { refreshUi() } );
 	return container;
 
 }

+ 73 - 63
editor/js/Sidebar.Object3D.js

@@ -41,23 +41,21 @@ Sidebar.Object3D = function ( editor ) {
 		switch ( this.getValue() ) {
 
 			case 'Reset Position':
-				object.position.set( 0, 0, 0 );
+				editor.execute( new 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() {
 

+ 43 - 57
editor/js/Viewport.js

@@ -34,7 +34,9 @@ var Viewport = function ( editor ) {
 	selectionBox.visible = false;
 	sceneHelpers.add( selectionBox );
 
-	var matrix = new THREE.Matrix4();
+	var objectPositionOnDown = null;
+	var objectRotationOnDown = null;
+	var objectScaleOnDown = null;
 
 	var transformControls = new THREE.TransformControls( camera, container.dom );
 	transformControls.addEventListener( 'change', function () {
@@ -51,6 +53,8 @@ var Viewport = function ( editor ) {
 
 			}
 
+			signals.refreshSidebarObject3D.dispatch( object );
+
 		}
 
 		render();
@@ -60,7 +64,9 @@ var Viewport = function ( editor ) {
 
 		var object = transformControls.object;
 
-		matrix.copy( object.matrix );
+		objectPositionOnDown = object.position.clone();
+		objectRotationOnDown = object.rotation.clone();
+		objectScaleOnDown = object.scale.clone();
 
 		controls.enabled = false;
 
@@ -69,26 +75,36 @@ var Viewport = function ( editor ) {
 
 		var object = transformControls.object;
 
-		if ( matrix.equals( object.matrix ) === false ) {
+		if ( object != null ) {
+
+			switch ( transformControls.getMode() ) {
+
+				case 'translate':
+					if (!objectPositionOnDown.equals(object.position)) {
+
+						editor.execute(new SetPositionCommand( object, object.position, objectPositionOnDown ));
+
+					}
+					break;
+				case 'rotate':
+					if (!objectRotationOnDown.equals(object.rotation)) {
 
-			( function ( matrix1, matrix2 ) {
+						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 );
 					}
-				);
+					break;
+				case 'scale':
+					if (!objectScaleOnDown.equals(object.scale)) {
 
-			} )( matrix.clone(), object.matrix.clone() );
+						editor.execute(new SetScaleCommand( object, object.scale, objectScaleOnDown ));
+
+					}
+					break;
+
+			}
 
 		}
 
-		signals.objectChanged.dispatch( object );
 		controls.enabled = true;
 
 	} );
@@ -358,9 +374,13 @@ var Viewport = function ( editor ) {
 
 	} );
 
-	signals.geometryChanged.add( function ( geometry ) {
+	signals.geometryChanged.add( function ( object ) {
+
+		if ( object !== null ) {
+
+			selectionBox.update( object );
 
-		selectionBox.update( editor.selected );
+		}
 
 		render();
 
@@ -368,24 +388,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 +423,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 +467,6 @@ var Viewport = function ( editor ) {
 
 			}
 
-			updateMaterials();
-
 			oldFogType = fogType;
 
 		}
@@ -513,30 +523,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 ) {

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

+ 14 - 1
editor/js/libs/app.js

@@ -61,6 +61,13 @@ var APP = {
 
 				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 ++ ) {
@@ -76,7 +83,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;
 
 						}
@@ -93,6 +100,12 @@ var APP = {
 
 		};
 
+		this.getCamera = function () {
+
+			return camera;
+
+		};
+
 		this.setCamera = function ( value ) {
 
 			camera = value;

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

+ 281 - 0
examples/js/TimelinerController.js

@@ -0,0 +1,281 @@
+/**
+ * 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;
+
+			times[ index ] = time;
+
+			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 ];
+
+			}
+
+		}
+
+		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 ) {
 

+ 586 - 648
examples/js/controls/OrbitControls.js

@@ -5,1111 +5,1049 @@
  * @author WestLangley / http://github.com/WestLangley
  * @author erich666 / http://erichaines.com
  */
-/*global THREE, console */
 
-( function () {
+// This set of controls performs orbiting, dollying (zooming), and panning.
+// Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default).
+//
+//    Orbit - left mouse / touch: one finger move
+//    Zoom - middle mouse, or mousewheel / touch: two finger spread or squish
+//    Pan - right mouse, or arrow keys / touch: three finter swipe
 
-	function OrbitConstraint ( object ) {
+THREE.OrbitControls = function ( object, domElement ) {
 
-		this.object = object;
+	this.object = object;
 
-		// "target" sets the location of focus, where the object orbits around
-		// and where it pans with respect to.
-		this.target = new THREE.Vector3();
+	this.domElement = ( domElement !== undefined ) ? domElement : document;
 
-		// Limits to how far you can dolly in and out ( PerspectiveCamera only )
-		this.minDistance = 0;
-		this.maxDistance = Infinity;
+	// Set to false to disable this control
+	this.enabled = true;
 
-		// Limits to how far you can zoom in and out ( OrthographicCamera only )
-		this.minZoom = 0;
-		this.maxZoom = Infinity;
+	// "target" sets the location of focus, where the object orbits around
+	this.target = new THREE.Vector3();
 
-		// How far you can orbit vertically, upper and lower limits.
-		// Range is 0 to Math.PI radians.
-		this.minPolarAngle = 0; // radians
-		this.maxPolarAngle = Math.PI; // radians
+	// How far you can dolly in and out ( PerspectiveCamera only )
+	this.minDistance = 0;
+	this.maxDistance = Infinity;
 
-		// How far you can orbit horizontally, upper and lower limits.
-		// If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ].
-		this.minAzimuthAngle = - Infinity; // radians
-		this.maxAzimuthAngle = Infinity; // radians
+	// How far you can zoom in and out ( OrthographicCamera only )
+	this.minZoom = 0;
+	this.maxZoom = Infinity;
 
-		// Set to true to enable damping (inertia)
-		// If damping is enabled, you must call controls.update() in your animation loop
-		this.enableDamping = false;
-		this.dampingFactor = 0.25;
+	// How far you can orbit vertically, upper and lower limits.
+	// Range is 0 to Math.PI radians.
+	this.minPolarAngle = 0; // radians
+	this.maxPolarAngle = Math.PI; // radians
 
-		////////////
-		// internals
+	// How far you can orbit horizontally, upper and lower limits.
+	// If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ].
+	this.minAzimuthAngle = - Infinity; // radians
+	this.maxAzimuthAngle = Infinity; // radians
 
-		var scope = this;
+	// Set to true to enable damping (inertia)
+	// If damping is enabled, you must call controls.update() in your animation loop
+	this.enableDamping = false;
+	this.dampingFactor = 0.25;
 
-		var EPS = 0.000001;
+	// This option actually enables dollying in and out; left as "zoom" for backwards compatibility.
+	// Set to false to disable zooming
+	this.enableZoom = true;
+	this.zoomSpeed = 1.0;
 
-		// Current position in spherical coordinate system.
-		var theta;
-		var phi;
+	// Set to false to disable rotating
+	this.enableRotate = true;
+	this.rotateSpeed = 1.0;
 
-		// Pending changes
-		var phiDelta = 0;
-		var thetaDelta = 0;
-		var scale = 1;
-		var panOffset = new THREE.Vector3();
-		var zoomChanged = false;
+	// Set to false to disable panning
+	this.enablePan = true;
+	this.keyPanSpeed = 7.0;	// pixels moved per arrow key push
 
-		// API
+	// Set to true to automatically rotate around the target
+	// If auto-rotate is enabled, you must call controls.update() in your animation loop
+	this.autoRotate = false;
+	this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60
 
-		this.getPolarAngle = function () {
+	// Set to false to disable use of the keys
+	this.enableKeys = true;
 
-			return phi;
+	// The four arrow keys
+	this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 };
 
-		};
-
-		this.getAzimuthalAngle = function () {
-
-			return theta;
-
-		};
-
-		this.rotateLeft = function ( angle ) {
+	// Mouse buttons
+	this.mouseButtons = { ORBIT: THREE.MOUSE.LEFT, ZOOM: THREE.MOUSE.MIDDLE, PAN: THREE.MOUSE.RIGHT };
 
-			thetaDelta -= angle;
+	// for reset
+	this.target0 = this.target.clone();
+	this.position0 = this.object.position.clone();
+	this.zoom0 = this.object.zoom;
 
-		};
-
-		this.rotateUp = function ( angle ) {
+	//
+	// public methods
+	//
 
-			phiDelta -= angle;
+	this.getPolarAngle = function () {
 
-		};
+		return phi;
 
-		// pass in distance in world space to move left
-		this.panLeft = function() {
+	};
 
-			var v = new THREE.Vector3();
+	this.getAzimuthalAngle = function () {
 
-			return function panLeft ( distance ) {
+		return theta;
 
-				var te = this.object.matrix.elements;
+	};
 
-				// get X column of matrix
-				v.set( te[ 0 ], te[ 1 ], te[ 2 ] );
-				v.multiplyScalar( - distance );
+	this.reset = function () {
 
-				panOffset.add( v );
+		scope.target.copy( scope.target0 );
+		scope.object.position.copy( scope.position0 );
+		scope.object.zoom = scope.zoom0;
 
-			};
+		scope.object.updateProjectionMatrix();
+		scope.dispatchEvent( changeEvent );
 
-		}();
+		scope.update();
 
-		// pass in distance in world space to move up
-		this.panUp = function() {
+		state = STATE.NONE;
 
-			var v = new THREE.Vector3();
+	};
 
-			return function panUp ( distance ) {
+	// this method is exposed, but perhaps it would be better if we can make it private...
+	this.update = function() {
 
-				var te = this.object.matrix.elements;
+		var offset = new THREE.Vector3();
 
-				// get Y column of matrix
-				v.set( te[ 4 ], te[ 5 ], te[ 6 ] );
-				v.multiplyScalar( distance );
+		// so camera.up is the orbit axis
+		var quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) );
+		var quatInverse = quat.clone().inverse();
 
-				panOffset.add( v );
+		var lastPosition = new THREE.Vector3();
+		var lastQuaternion = new THREE.Quaternion();
 
-			};
+		return function () {
 
-		}();
+			var position = scope.object.position;
 
-		// pass in x,y of change desired in pixel space,
-		// right and down are positive
-		this.pan = function ( deltaX, deltaY, screenWidth, screenHeight ) {
+			offset.copy( position ).sub( scope.target );
 
-			if ( scope.object instanceof THREE.PerspectiveCamera ) {
+			// rotate offset to "y-axis-is-up" space
+			offset.applyQuaternion( quat );
 
-				// perspective
-				var position = scope.object.position;
-				var offset = position.clone().sub( scope.target );
-				var targetDistance = offset.length();
+			// angle from z-axis around y-axis
 
-				// half of the fov is center to top of screen
-				targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 );
+			theta = Math.atan2( offset.x, offset.z );
 
-				// we actually don't use screenWidth, since perspective camera is fixed to screen height
-				scope.panLeft( 2 * deltaX * targetDistance / screenHeight );
-				scope.panUp( 2 * deltaY * targetDistance / screenHeight );
+			// angle from y-axis
 
-			} else if ( scope.object instanceof THREE.OrthographicCamera ) {
+			phi = Math.atan2( Math.sqrt( offset.x * offset.x + offset.z * offset.z ), offset.y );
 
-				// orthographic
-				scope.panLeft( deltaX * ( scope.object.right - scope.object.left ) / screenWidth );
-				scope.panUp( deltaY * ( scope.object.top - scope.object.bottom ) / screenHeight );
+			if ( scope.autoRotate && state === STATE.NONE ) {
 
-			} else {
-
-				// camera neither orthographic or perspective
-				console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' );
+				rotateLeft( getAutoRotationAngle() );
 
 			}
 
-		};
-
-		this.dollyIn = function ( dollyScale ) {
-
-			if ( scope.object instanceof THREE.PerspectiveCamera ) {
+			theta += thetaDelta;
+			phi += phiDelta;
 
-				scale /= dollyScale;
+			// restrict theta to be between desired limits
+			theta = Math.max( scope.minAzimuthAngle, Math.min( scope.maxAzimuthAngle, theta ) );
 
-			} else if ( scope.object instanceof THREE.OrthographicCamera ) {
+			// restrict phi to be between desired limits
+			phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, phi ) );
 
-				scope.object.zoom = Math.max( this.minZoom, Math.min( this.maxZoom, this.object.zoom * dollyScale ) );
-				scope.object.updateProjectionMatrix();
-				zoomChanged = true;
+			// restrict phi to be betwee EPS and PI-EPS
+			phi = Math.max( EPS, Math.min( Math.PI - EPS, phi ) );
 
-			} else {
+			var radius = offset.length() * scale;
 
-				console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
+			// restrict radius to be between desired limits
+			radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, radius ) );
 
-			}
+			// move target to panned location
+			scope.target.add( panOffset );
 
-		};
+			offset.x = radius * Math.sin( phi ) * Math.sin( theta );
+			offset.y = radius * Math.cos( phi );
+			offset.z = radius * Math.sin( phi ) * Math.cos( theta );
 
-		this.dollyOut = function ( dollyScale ) {
+			// rotate offset back to "camera-up-vector-is-up" space
+			offset.applyQuaternion( quatInverse );
 
-			if ( scope.object instanceof THREE.PerspectiveCamera ) {
+			position.copy( scope.target ).add( offset );
 
-				scale *= dollyScale;
+			scope.object.lookAt( scope.target );
 
-			} else if ( scope.object instanceof THREE.OrthographicCamera ) {
+			if ( scope.enableDamping === true ) {
 
-				scope.object.zoom = Math.max( this.minZoom, Math.min( this.maxZoom, this.object.zoom / dollyScale ) );
-				scope.object.updateProjectionMatrix();
-				zoomChanged = true;
+				thetaDelta *= ( 1 - scope.dampingFactor );
+				phiDelta *= ( 1 - scope.dampingFactor );
 
 			} else {
 
-				console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
+				thetaDelta = 0;
+				phiDelta = 0;
 
 			}
 
-		};
+			scale = 1;
+			panOffset.set( 0, 0, 0 );
 
-		this.update = function() {
+			// update condition is:
+			// min(camera displacement, camera rotation in radians)^2 > EPS
+			// using small-angle approximation cos(x/2) = 1 - x^2 / 8
 
-			var offset = new THREE.Vector3();
+			if ( zoomChanged ||
+				lastPosition.distanceToSquared( scope.object.position ) > EPS ||
+				8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) {
 
-			// so camera.up is the orbit axis
-			var quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) );
-			var quatInverse = quat.clone().inverse();
+				scope.dispatchEvent( changeEvent );
 
-			var lastPosition = new THREE.Vector3();
-			var lastQuaternion = new THREE.Quaternion();
+				lastPosition.copy( scope.object.position );
+				lastQuaternion.copy( scope.object.quaternion );
+				zoomChanged = false;
 
-			return function () {
+				return true;
 
-				var position = this.object.position;
+			}
 
-				offset.copy( position ).sub( this.target );
+			return false;
 
-				// rotate offset to "y-axis-is-up" space
-				offset.applyQuaternion( quat );
+		};
 
-				// angle from z-axis around y-axis
+	}();
 
-				theta = Math.atan2( offset.x, offset.z );
+	this.dispose = function() {
 
-				// angle from y-axis
+		scope.domElement.removeEventListener( 'contextmenu', contextmenu, false );
+		scope.domElement.removeEventListener( 'mousedown', onMouseDown, false );
+		scope.domElement.removeEventListener( 'mousewheel', onMouseWheel, false );
+		scope.domElement.removeEventListener( 'MozMousePixelScroll', onMouseWheel, false ); // firefox
 
-				phi = Math.atan2( Math.sqrt( offset.x * offset.x + offset.z * offset.z ), offset.y );
+		scope.domElement.removeEventListener( 'touchstart', onTouchStart, false );
+		scope.domElement.removeEventListener( 'touchend', onTouchEnd, false );
+		scope.domElement.removeEventListener( 'touchmove', onTouchMove, false );
 
-				theta += thetaDelta;
-				phi += phiDelta;
+		document.removeEventListener( 'mousemove', onMouseMove, false );
+		document.removeEventListener( 'mouseup', onMouseUp, false );
+		document.removeEventListener( 'mouseout', onMouseUp, false );
 
-				// restrict theta to be between desired limits
-				theta = Math.max( this.minAzimuthAngle, Math.min( this.maxAzimuthAngle, theta ) );
+		window.removeEventListener( 'keydown', onKeyDown, false );
 
-				// restrict phi to be between desired limits
-				phi = Math.max( this.minPolarAngle, Math.min( this.maxPolarAngle, phi ) );
+		//scope.dispatchEvent( { type: 'dispose' } ); // should this be added here?
 
-				// restrict phi to be betwee EPS and PI-EPS
-				phi = Math.max( EPS, Math.min( Math.PI - EPS, phi ) );
+	}
 
-				var radius = offset.length() * scale;
+	//
+	// internals
+	//
 
-				// restrict radius to be between desired limits
-				radius = Math.max( this.minDistance, Math.min( this.maxDistance, radius ) );
+	var scope = this;
 
-				// move target to panned location
-				this.target.add( panOffset );
+	var changeEvent = { type: 'change' };
+	var startEvent = { type: 'start' };
+	var endEvent = { type: 'end' };
 
-				offset.x = radius * Math.sin( phi ) * Math.sin( theta );
-				offset.y = radius * Math.cos( phi );
-				offset.z = radius * Math.sin( phi ) * Math.cos( theta );
+	var STATE = { NONE : - 1, ROTATE : 0, DOLLY : 1, PAN : 2, TOUCH_ROTATE : 3, TOUCH_DOLLY : 4, TOUCH_PAN : 5 };
 
-				// rotate offset back to "camera-up-vector-is-up" space
-				offset.applyQuaternion( quatInverse );
+	var state = STATE.NONE;
 
-				position.copy( this.target ).add( offset );
+	var EPS = 0.000001;
 
-				this.object.lookAt( this.target );
+	// current position in spherical coordinates
+	var theta;
+	var phi;
 
-				if ( this.enableDamping === true ) {
+	var phiDelta = 0;
+	var thetaDelta = 0;
+	var scale = 1;
+	var panOffset = new THREE.Vector3();
+	var zoomChanged = false;
 
-					thetaDelta *= ( 1 - this.dampingFactor );
-					phiDelta *= ( 1 - this.dampingFactor );
+	var rotateStart = new THREE.Vector2();
+	var rotateEnd = new THREE.Vector2();
+	var rotateDelta = new THREE.Vector2();
 
-				} else {
+	var panStart = new THREE.Vector2();
+	var panEnd = new THREE.Vector2();
+	var panDelta = new THREE.Vector2();
 
-					thetaDelta = 0;
-					phiDelta = 0;
+	var dollyStart = new THREE.Vector2();
+	var dollyEnd = new THREE.Vector2();
+	var dollyDelta = new THREE.Vector2();
 
-				}
+	function getAutoRotationAngle() {
 
-				scale = 1;
-				panOffset.set( 0, 0, 0 );
+		return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed;
 
-				// update condition is:
-				// min(camera displacement, camera rotation in radians)^2 > EPS
-				// using small-angle approximation cos(x/2) = 1 - x^2 / 8
+	}
 
-				if ( zoomChanged ||
-					 lastPosition.distanceToSquared( this.object.position ) > EPS ||
-				    8 * ( 1 - lastQuaternion.dot( this.object.quaternion ) ) > EPS ) {
+	function getZoomScale() {
 
-					lastPosition.copy( this.object.position );
-					lastQuaternion.copy( this.object.quaternion );
-					zoomChanged = false;
+		return Math.pow( 0.95, scope.zoomSpeed );
 
-					return true;
+	}
 
-				}
+	function rotateLeft( angle ) {
 
-				return false;
+		thetaDelta -= angle;
 
-			};
+	}
 
-		}();
+	function rotateUp( angle ) {
 
-	};
+		phiDelta -= angle;
 
+	}
 
-	// This set of controls performs orbiting, dollying (zooming), and panning. It maintains
-	// the "up" direction as +Y, unlike the TrackballControls. Touch on tablet and phones is
-	// supported.
-	//
-	//    Orbit - left mouse / touch: one finger move
-	//    Zoom - middle mouse, or mousewheel / touch: two finger spread or squish
-	//    Pan - right mouse, or arrow keys / touch: three finter swipe
+	var panLeft = function() {
 
-	THREE.OrbitControls = function ( object, domElement ) {
+		var v = new THREE.Vector3();
 
-		var constraint = new OrbitConstraint( object );
+		return function panLeft( distance, objectMatrix ) {
 
-		this.domElement = ( domElement !== undefined ) ? domElement : document;
+			var te = objectMatrix.elements;
 
-		// API
+			// get X column of objectMatrix
+			v.set( te[ 0 ], te[ 1 ], te[ 2 ] );
 
-		Object.defineProperty( this, 'constraint', {
+			v.multiplyScalar( - distance );
 
-			get: function() {
+			panOffset.add( v );
 
-				return constraint;
+		};
 
-			}
+	}();
 
-		} );
+	var panUp = function() {
 
-		this.getPolarAngle = function () {
+		var v = new THREE.Vector3();
 
-			return constraint.getPolarAngle();
+		return function panUp( distance, objectMatrix ) {
 
-		};
+			var te = objectMatrix.elements;
+
+			// get Y column of objectMatrix
+			v.set( te[ 4 ], te[ 5 ], te[ 6 ] );
 
-		this.getAzimuthalAngle = function () {
+			v.multiplyScalar( distance );
 
-			return constraint.getAzimuthalAngle();
+			panOffset.add( v );
 
 		};
 
-		// Set to false to disable this control
-		this.enabled = true;
+	}();
 
-		// center is old, deprecated; use "target" instead
-		this.center = this.target;
+	// deltaX and deltaY are in pixels; right and down are positive
+	var pan = function() {
 
-		// This option actually enables dollying in and out; left as "zoom" for
-		// backwards compatibility.
-		// Set to false to disable zooming
-		this.enableZoom = true;
-		this.zoomSpeed = 1.0;
+		var offset = new THREE.Vector3();
 
-		// Set to false to disable rotating
-		this.enableRotate = true;
-		this.rotateSpeed = 1.0;
+		return function( deltaX, deltaY ) {
 
-		// Set to false to disable panning
-		this.enablePan = true;
-		this.keyPanSpeed = 7.0;	// pixels moved per arrow key push
+			var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
 
-		// Set to true to automatically rotate around the target
-		// If auto-rotate is enabled, you must call controls.update() in your animation loop
-		this.autoRotate = false;
-		this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60
+			if ( scope.object instanceof THREE.PerspectiveCamera ) {
 
-		// Set to false to disable use of the keys
-		this.enableKeys = true;
+				// perspective
+				var position = scope.object.position;
+				offset.copy( position ).sub( scope.target );
+				var targetDistance = offset.length();
 
-		// The four arrow keys
-		this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 };
+				// half of the fov is center to top of screen
+				targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 );
 
-		// Mouse buttons
-		this.mouseButtons = { ORBIT: THREE.MOUSE.LEFT, ZOOM: THREE.MOUSE.MIDDLE, PAN: THREE.MOUSE.RIGHT };
+				// we actually don't use screenWidth, since perspective camera is fixed to screen height
+				panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix );
+				panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix );
 
-		////////////
-		// internals
+			} else if ( scope.object instanceof THREE.OrthographicCamera ) {
 
-		var scope = this;
+				// orthographic
+				panLeft( deltaX * ( scope.object.right - scope.object.left ) / element.clientWidth, scope.object.matrix );
+				panUp( deltaY * ( scope.object.top - scope.object.bottom ) / element.clientHeight, scope.object.matrix );
 
-		var rotateStart = new THREE.Vector2();
-		var rotateEnd = new THREE.Vector2();
-		var rotateDelta = new THREE.Vector2();
+			} else {
 
-		var panStart = new THREE.Vector2();
-		var panEnd = new THREE.Vector2();
-		var panDelta = new THREE.Vector2();
+				// camera neither orthographic nor perspective
+				console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' );
+				scope.enablePan = false;
 
-		var dollyStart = new THREE.Vector2();
-		var dollyEnd = new THREE.Vector2();
-		var dollyDelta = new THREE.Vector2();
+			}
 
-		var STATE = { NONE : - 1, ROTATE : 0, DOLLY : 1, PAN : 2, TOUCH_ROTATE : 3, TOUCH_DOLLY : 4, TOUCH_PAN : 5 };
+		};
 
-		var state = STATE.NONE;
+	}();
 
-		// for reset
+	function dollyIn( dollyScale ) {
 
-		this.target0 = this.target.clone();
-		this.position0 = this.object.position.clone();
-		this.zoom0 = this.object.zoom;
+		if ( scope.object instanceof THREE.PerspectiveCamera ) {
 
-		// events
+			scale /= dollyScale;
 
-		var changeEvent = { type: 'change' };
-		var startEvent = { type: 'start' };
-		var endEvent = { type: 'end' };
+		} else if ( scope.object instanceof THREE.OrthographicCamera ) {
 
-		// pass in x,y of change desired in pixel space,
-		// right and down are positive
-		function pan( deltaX, deltaY ) {
+			scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) );
+			scope.object.updateProjectionMatrix();
+			zoomChanged = true;
 
-			var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
+		} else {
 
-			constraint.pan( deltaX, deltaY, element.clientWidth, element.clientHeight );
+			console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
+			scope.enableZoom = false;
 
 		}
 
-		this.update = function () {
+	}
 
-			if ( this.autoRotate && state === STATE.NONE ) {
-
-				constraint.rotateLeft( getAutoRotationAngle() );
-
-			}
+	function dollyOut( dollyScale ) {
 
-			if ( constraint.update() === true ) {
+		if ( scope.object instanceof THREE.PerspectiveCamera ) {
 
-				this.dispatchEvent( changeEvent );
+			scale *= dollyScale;
 
-			}
+		} else if ( scope.object instanceof THREE.OrthographicCamera ) {
 
-		};
+			scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) );
+			scope.object.updateProjectionMatrix();
+			zoomChanged = true;
 
-		this.reset = function () {
+		} else {
 
-			state = STATE.NONE;
+			console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
+			scope.enableZoom = false;
 
-			this.target.copy( this.target0 );
-			this.object.position.copy( this.position0 );
-			this.object.zoom = this.zoom0;
+		}
 
-			this.object.updateProjectionMatrix();
-			this.dispatchEvent( changeEvent );
+	}
 
-			this.update();
+	//
+	// event callbacks - update the object state
+	//
 
-		};
+	handleMouseDownRotate = function( event ) {
 
-		function getAutoRotationAngle() {
+		//console.log( 'handleMouseDownRotate' );
 
-			return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed;
+		rotateStart.set( event.clientX, event.clientY );
 
-		}
+	};
 
-		function getZoomScale() {
+	handleMouseDownDolly = function( event ) {
 
-			return Math.pow( 0.95, scope.zoomSpeed );
+		//console.log( 'handleMouseDownDolly' );
 
-		}
+		dollyStart.set( event.clientX, event.clientY );
 
-		function onMouseDown( event ) {
+	};
 
-			if ( scope.enabled === false ) return;
+	handleMouseDownPan = function( event ) {
 
-			event.preventDefault();
+		//console.log( 'handleMouseDownPan' );
 
-			if ( event.button === scope.mouseButtons.ORBIT ) {
+		panStart.set( event.clientX, event.clientY );
 
-				if ( scope.enableRotate === false ) return;
+	};
 
-				state = STATE.ROTATE;
+	handleMouseMoveRotate = function( event ) {
 
-				rotateStart.set( event.clientX, event.clientY );
+		//console.log( 'handleMouseMoveRotate' );
 
-			} else if ( event.button === scope.mouseButtons.ZOOM ) {
+		rotateEnd.set( event.clientX, event.clientY );
+		rotateDelta.subVectors( rotateEnd, rotateStart );
 
-				if ( scope.enableZoom === false ) return;
+		var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
 
-				state = STATE.DOLLY;
+		// rotating across whole screen goes 360 degrees around
+		rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed );
 
-				dollyStart.set( event.clientX, event.clientY );
+		// rotating up and down along whole screen attempts to go 360, but limited to 180
+		rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed );
 
-			} else if ( event.button === scope.mouseButtons.PAN ) {
+		rotateStart.copy( rotateEnd );
 
-				if ( scope.enablePan === false ) return;
+		scope.update();
 
-				state = STATE.PAN;
+	};
 
-				panStart.set( event.clientX, event.clientY );
+	handleMouseMoveDolly = function( event ) {
 
-			}
+		//console.log( 'handleMouseMoveDolly' );
 
-			if ( state !== STATE.NONE ) {
+		dollyEnd.set( event.clientX, event.clientY );
 
-				document.addEventListener( 'mousemove', onMouseMove, false );
-				document.addEventListener( 'mouseup', onMouseUp, false );
-				scope.dispatchEvent( startEvent );
+		dollyDelta.subVectors( dollyEnd, dollyStart );
 
-			}
+		if ( dollyDelta.y > 0 ) {
 
-		}
+			dollyIn( scope.getZoomScale() );
 
-		function onMouseMove( event ) {
+		} else if ( dollyDelta.y < 0 ) {
 
-			if ( scope.enabled === false ) return;
+			dollyOut( scope.getZoomScale() );
 
-			event.preventDefault();
+		}
 
-			var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
+		dollyStart.copy( dollyEnd );
 
-			if ( state === STATE.ROTATE ) {
+		scope.update();
 
-				if ( scope.enableRotate === false ) return;
+	};
 
-				rotateEnd.set( event.clientX, event.clientY );
-				rotateDelta.subVectors( rotateEnd, rotateStart );
+	handleMouseMovePan = function( event ) {
 
-				// rotating across whole screen goes 360 degrees around
-				constraint.rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed );
+		//console.log( 'handleMouseMovePan' );
 
-				// rotating up and down along whole screen attempts to go 360, but limited to 180
-				constraint.rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed );
+		panEnd.set( event.clientX, event.clientY );
 
-				rotateStart.copy( rotateEnd );
+		panDelta.subVectors( panEnd, panStart );
 
-			} else if ( state === STATE.DOLLY ) {
+		pan( panDelta.x, panDelta.y );
 
-				if ( scope.enableZoom === false ) return;
+		panStart.copy( panEnd );
 
-				dollyEnd.set( event.clientX, event.clientY );
-				dollyDelta.subVectors( dollyEnd, dollyStart );
+		scope.update();
 
-				if ( dollyDelta.y > 0 ) {
+	};
 
-					constraint.dollyIn( getZoomScale() );
+	handleMouseUp = function( event ) {
 
-				} else if ( dollyDelta.y < 0 ) {
+		//console.log( 'handleMouseUp' );
 
-					constraint.dollyOut( getZoomScale() );
+	};
 
-				}
+	handleMouseWheel = function( event ) {
 
-				dollyStart.copy( dollyEnd );
+		//console.log( 'handleMouseWheel' );
 
-			} else if ( state === STATE.PAN ) {
+		var delta = 0;
 
-				if ( scope.enablePan === false ) return;
+		if ( event.wheelDelta !== undefined ) {
 
-				panEnd.set( event.clientX, event.clientY );
-				panDelta.subVectors( panEnd, panStart );
+			// WebKit / Opera / Explorer 9
 
-				pan( panDelta.x, panDelta.y );
+			delta = event.wheelDelta;
 
-				panStart.copy( panEnd );
+		} else if ( event.detail !== undefined ) {
 
-			}
+			// Firefox
 
-			if ( state !== STATE.NONE ) scope.update();
+			delta = - event.detail;
 
 		}
 
-		function onMouseUp( /* event */ ) {
+		if ( delta > 0 ) {
 
-			if ( scope.enabled === false ) return;
+			dollyOut( getZoomScale() );
 
-			document.removeEventListener( 'mousemove', onMouseMove, false );
-			document.removeEventListener( 'mouseup', onMouseUp, false );
-			scope.dispatchEvent( endEvent );
-			state = STATE.NONE;
-
-		}
+		} else if ( delta < 0 ) {
 
-		function onMouseWheel( event ) {
+			dollyIn( getZoomScale() );
 
-			if ( scope.enabled === false || scope.enableZoom === false || state !== STATE.NONE ) return;
+		}
 
-			event.preventDefault();
-			event.stopPropagation();
+		scope.update();
 
-			var delta = 0;
+	};
 
-			if ( event.wheelDelta !== undefined ) {
+	handleKeyDown = function( event ) {
 
-				// WebKit / Opera / Explorer 9
+		//console.log( 'handleKeyDown' );
 
-				delta = event.wheelDelta;
+		switch ( event.keyCode ) {
 
-			} else if ( event.detail !== undefined ) {
+			case scope.keys.UP:
+				pan( 0, scope.keyPanSpeed );
+				scope.update();
+				break;
 
-				// Firefox
+			case scope.keys.BOTTOM:
+				pan( 0, - scope.keyPanSpeed );
+				scope.update();
+				break;
 
-				delta = - event.detail;
+			case scope.keys.LEFT:
+				pan( scope.keyPanSpeed, 0 );
+				scope.update();
+				break;
 
-			}
+			case scope.keys.RIGHT:
+				pan( - scope.keyPanSpeed, 0 );
+				scope.update();
+				break;
 
-			if ( delta > 0 ) {
+		}
 
-				constraint.dollyOut( getZoomScale() );
+	};
 
-			} else if ( delta < 0 ) {
+	handleTouchStartRotate = function( event ) {
 
-				constraint.dollyIn( getZoomScale() );
+		//console.log( 'handleTouchStartRotate' );
 
-			}
+		rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
 
-			scope.update();
-			scope.dispatchEvent( startEvent );
-			scope.dispatchEvent( endEvent );
+	};
 
-		}
+	handleTouchStartDolly = function( event ) {
 
-		function onKeyDown( event ) {
+		//console.log( 'handleTouchStartDolly' );
 
-			if ( scope.enabled === false || scope.enableKeys === false || scope.enablePan === false ) return;
+		var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
+		var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
 
-			switch ( event.keyCode ) {
+		var distance = Math.sqrt( dx * dx + dy * dy );
 
-				case scope.keys.UP:
-					pan( 0, scope.keyPanSpeed );
-					scope.update();
-					break;
+		dollyStart.set( 0, distance );
 
-				case scope.keys.BOTTOM:
-					pan( 0, - scope.keyPanSpeed );
-					scope.update();
-					break;
+	};
 
-				case scope.keys.LEFT:
-					pan( scope.keyPanSpeed, 0 );
-					scope.update();
-					break;
+	handleTouchStartPan = function( event ) {
 
-				case scope.keys.RIGHT:
-					pan( - scope.keyPanSpeed, 0 );
-					scope.update();
-					break;
+		//console.log( 'handleTouchStartPan' );
 
-			}
+		panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
 
-		}
+	};
 
-		function touchstart( event ) {
+	handleTouchMoveRotate = function( event ) {
 
-			if ( scope.enabled === false ) return;
+		//console.log( 'handleTouchMoveRotate' );
 
-			switch ( event.touches.length ) {
+		rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
+		rotateDelta.subVectors( rotateEnd, rotateStart );
 
-				case 1:	// one-fingered touch: rotate
+		var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
 
-					if ( scope.enableRotate === false ) return;
+		// rotating across whole screen goes 360 degrees around
+		rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed );
 
-					state = STATE.TOUCH_ROTATE;
+		// rotating up and down along whole screen attempts to go 360, but limited to 180
+		rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed );
 
-					rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
-					break;
+		rotateStart.copy( rotateEnd );
 
-				case 2:	// two-fingered touch: dolly
+		scope.update();
 
-					if ( scope.enableZoom === false ) return;
+	};
 
-					state = STATE.TOUCH_DOLLY;
+	handleTouchMoveDolly = function( event ) {
 
-					var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
-					var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
-					var distance = Math.sqrt( dx * dx + dy * dy );
-					dollyStart.set( 0, distance );
-					break;
+		//console.log( 'handleTouchMoveDolly' );
 
-				case 3: // three-fingered touch: pan
+		var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
+		var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
 
-					if ( scope.enablePan === false ) return;
+		var distance = Math.sqrt( dx * dx + dy * dy );
 
-					state = STATE.TOUCH_PAN;
+		dollyEnd.set( 0, distance );
 
-					panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
-					break;
+		dollyDelta.subVectors( dollyEnd, dollyStart );
 
-				default:
+		if ( dollyDelta.y > 0 ) {
 
-					state = STATE.NONE;
+			dollyOut( getZoomScale() );
 
-			}
+		} else if ( dollyDelta.y < 0 ) {
 
-			if ( state !== STATE.NONE ) scope.dispatchEvent( startEvent );
+			dollyIn( getZoomScale() );
 
 		}
 
-		function touchmove( event ) {
+		dollyStart.copy( dollyEnd );
 
-			if ( scope.enabled === false ) return;
+		scope.update();
 
-			event.preventDefault();
-			event.stopPropagation();
-
-			var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
+	};
 
-			switch ( event.touches.length ) {
+	handleTouchMovePan = function( event ) {
 
-				case 1: // one-fingered touch: rotate
+		//console.log( 'handleTouchMovePan' );
 
-					if ( scope.enableRotate === false ) return;
-					if ( state !== STATE.TOUCH_ROTATE ) return;
+		panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
 
-					rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
-					rotateDelta.subVectors( rotateEnd, rotateStart );
+		panDelta.subVectors( panEnd, panStart );
 
-					// rotating across whole screen goes 360 degrees around
-					constraint.rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed );
-					// rotating up and down along whole screen attempts to go 360, but limited to 180
-					constraint.rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed );
+		pan( panDelta.x, panDelta.y );
 
-					rotateStart.copy( rotateEnd );
+		panStart.copy( panEnd );
 
-					scope.update();
-					break;
+		scope.update();
 
-				case 2: // two-fingered touch: dolly
+	};
 
-					if ( scope.enableZoom === false ) return;
-					if ( state !== STATE.TOUCH_DOLLY ) return;
+	handleTouchEnd = function( event ) {
 
-					var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
-					var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
-					var distance = Math.sqrt( dx * dx + dy * dy );
+		//console.log( 'handleTouchEnd' );
 
-					dollyEnd.set( 0, distance );
-					dollyDelta.subVectors( dollyEnd, dollyStart );
+	};
 
-					if ( dollyDelta.y > 0 ) {
+	//
+	// event handlers - FSM: listen for events and reset state
+	//
 
-						constraint.dollyOut( getZoomScale() );
+	function onMouseDown( event ) {
 
-					} else if ( dollyDelta.y < 0 ) {
+		if ( scope.enabled === false ) return;
 
-						constraint.dollyIn( getZoomScale() );
+		event.preventDefault();
 
-					}
+		if ( event.button === scope.mouseButtons.ORBIT ) {
 
-					dollyStart.copy( dollyEnd );
+			if ( scope.enableRotate === false ) return;
 
-					scope.update();
-					break;
+			handleMouseDownRotate( event );
 
-				case 3: // three-fingered touch: pan
+			state = STATE.ROTATE;
 
-					if ( scope.enablePan === false ) return;
-					if ( state !== STATE.TOUCH_PAN ) return;
+		} else if ( event.button === scope.mouseButtons.ZOOM ) {
 
-					panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
-					panDelta.subVectors( panEnd, panStart );
+			if ( scope.enableZoom === false ) return;
 
-					pan( panDelta.x, panDelta.y );
+			handleMouseDownDolly( event );
 
-					panStart.copy( panEnd );
+			state = STATE.DOLLY;
 
-					scope.update();
-					break;
+		} else if ( event.button === scope.mouseButtons.PAN ) {
 
-				default:
+			if ( scope.enablePan === false ) return;
 
-					state = STATE.NONE;
+			handleMouseDownPan( event );
 
-			}
+			state = STATE.PAN;
 
 		}
 
-		function touchend( /* event */ ) {
+		if ( state !== STATE.NONE ) {
 
-			if ( scope.enabled === false ) return;
+			document.addEventListener( 'mousemove', onMouseMove, false );
+			document.addEventListener( 'mouseup', onMouseUp, false );
+			document.addEventListener( 'mouseout', onMouseUp, false );
 
-			scope.dispatchEvent( endEvent );
-			state = STATE.NONE;
+			scope.dispatchEvent( startEvent );
 
 		}
 
-		function contextmenu( event ) {
+	};
 
-			event.preventDefault();
+	function onMouseMove( event ) {
 
-		}
+		if ( scope.enabled === false ) return;
 
-		this.dispose = function() {
+		event.preventDefault();
 
-			this.domElement.removeEventListener( 'contextmenu', contextmenu, false );
-			this.domElement.removeEventListener( 'mousedown', onMouseDown, false );
-			this.domElement.removeEventListener( 'mousewheel', onMouseWheel, false );
-			this.domElement.removeEventListener( 'MozMousePixelScroll', onMouseWheel, false ); // firefox
+		if ( state === STATE.ROTATE ) {
 
-			this.domElement.removeEventListener( 'touchstart', touchstart, false );
-			this.domElement.removeEventListener( 'touchend', touchend, false );
-			this.domElement.removeEventListener( 'touchmove', touchmove, false );
+			if ( scope.enableRotate === false ) return;
 
-			document.removeEventListener( 'mousemove', onMouseMove, false );
-			document.removeEventListener( 'mouseup', onMouseUp, false );
+			handleMouseMoveRotate( event );
 
-			window.removeEventListener( 'keydown', onKeyDown, false );
+		} else if ( state === STATE.DOLLY ) {
 
-		}
+			if ( scope.enableZoom === false ) return;
 
-		this.domElement.addEventListener( 'contextmenu', contextmenu, false );
+			handleMouseMoveDolly( event );
 
-		this.domElement.addEventListener( 'mousedown', onMouseDown, false );
-		this.domElement.addEventListener( 'mousewheel', onMouseWheel, false );
-		this.domElement.addEventListener( 'MozMousePixelScroll', onMouseWheel, false ); // firefox
+		} else if ( state === STATE.PAN ) {
 
-		this.domElement.addEventListener( 'touchstart', touchstart, false );
-		this.domElement.addEventListener( 'touchend', touchend, false );
-		this.domElement.addEventListener( 'touchmove', touchmove, false );
+			if ( scope.enablePan === false ) return;
 
-		window.addEventListener( 'keydown', onKeyDown, false );
+			handleMouseMovePan( event );
 
-		// force an update at start
-		this.update();
+		}
 
 	};
 
-	THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype );
-	THREE.OrbitControls.prototype.constructor = THREE.OrbitControls;
-
-	Object.defineProperties( THREE.OrbitControls.prototype, {
+	function onMouseUp( event ) {
 
-		object: {
-
-			get: function () {
-
-				return this.constraint.object;
-
-			}
-
-		},
+		if ( scope.enabled === false ) return;
 
-		target: {
+		handleMouseUp( event );
 
-			get: function () {
+		document.removeEventListener( 'mousemove', onMouseMove, false );
+		document.removeEventListener( 'mouseup', onMouseUp, false );
+		document.removeEventListener( 'mouseout', onMouseUp, false );
 
-				return this.constraint.target;
+		scope.dispatchEvent( endEvent );
 
-			},
+		state = STATE.NONE;
 
-			set: function ( value ) {
+	};
 
-				console.warn( 'THREE.OrbitControls: target is now immutable. Use target.set() instead.' );
-				this.constraint.target.copy( value );
+	function onMouseWheel( event ) {
 
-			}
+		if ( scope.enabled === false || scope.enableZoom === false || state !== STATE.NONE ) return;
 
-		},
+		event.preventDefault();
+		event.stopPropagation();
 
-		minDistance : {
+		handleMouseWheel( event );
 
-			get: function () {
+		scope.dispatchEvent( startEvent ); // not sure why these are here...
+		scope.dispatchEvent( endEvent );
 
-				return this.constraint.minDistance;
+	};
 
-			},
+	function onKeyDown( event ) {
 
-			set: function ( value ) {
+		if ( scope.enabled === false || scope.enableKeys === false || scope.enablePan === false ) return;
 
-				this.constraint.minDistance = value;
+		handleKeyDown( event );
 
-			}
+	};
 
-		},
+	function onTouchStart( event ) {
 
-		maxDistance : {
+		if ( scope.enabled === false ) return;
 
-			get: function () {
+		switch ( event.touches.length ) {
 
-				return this.constraint.maxDistance;
+			case 1:	// one-fingered touch: rotate
 
-			},
+				if ( scope.enableRotate === false ) return;
 
-			set: function ( value ) {
+				handleTouchStartRotate( event );
 
-				this.constraint.maxDistance = value;
+				state = STATE.TOUCH_ROTATE;
 
-			}
+				break;
 
-		},
+			case 2:	// two-fingered touch: dolly
 
-		minZoom : {
+				if ( scope.enableZoom === false ) return;
 
-			get: function () {
+				handleTouchStartDolly( event );
 
-				return this.constraint.minZoom;
+				state = STATE.TOUCH_DOLLY;
 
-			},
+				break;
 
-			set: function ( value ) {
+			case 3: // three-fingered touch: pan
 
-				this.constraint.minZoom = value;
+				if ( scope.enablePan === false ) return;
 
-			}
+				handleTouchStartPan( event );
 
-		},
+				state = STATE.TOUCH_PAN;
 
-		maxZoom : {
+				break;
 
-			get: function () {
+			default:
 
-				return this.constraint.maxZoom;
+				state = STATE.NONE;
 
-			},
+		}
 
-			set: function ( value ) {
+		if ( state !== STATE.NONE ) {
 
-				this.constraint.maxZoom = value;
+			scope.dispatchEvent( startEvent );
 
-			}
+		}
 
-		},
+	};
 
-		minPolarAngle : {
+	function onTouchMove( event ) {
 
-			get: function () {
+		if ( scope.enabled === false ) return;
 
-				return this.constraint.minPolarAngle;
+		event.preventDefault();
+		event.stopPropagation();
 
-			},
+		switch ( event.touches.length ) {
 
-			set: function ( value ) {
+			case 1: // one-fingered touch: rotate
 
-				this.constraint.minPolarAngle = value;
+				if ( scope.enableRotate === false ) return;
+				if ( state !== STATE.TOUCH_ROTATE ) return; // is this needed?...
 
-			}
+				handleTouchMoveRotate( event );
 
-		},
+				break;
 
-		maxPolarAngle : {
+			case 2: // two-fingered touch: dolly
 
-			get: function () {
+				if ( scope.enableZoom === false ) return;
+				if ( state !== STATE.TOUCH_DOLLY ) return; // is this needed?...
 
-				return this.constraint.maxPolarAngle;
+				handleTouchMoveDolly( event );
 
-			},
+				break;
 
-			set: function ( value ) {
+			case 3: // three-fingered touch: pan
 
-				this.constraint.maxPolarAngle = value;
+				if ( scope.enablePan === false ) return;
+				if ( state !== STATE.TOUCH_PAN ) return; // is this needed?...
 
-			}
+				handleTouchMovePan( event );
 
-		},
+				break;
 
-		minAzimuthAngle : {
+			default:
 
-			get: function () {
+				state = STATE.NONE;
 
-				return this.constraint.minAzimuthAngle;
+		}
 
-			},
+	};
 
-			set: function ( value ) {
+	function onTouchEnd( event ) {
 
-				this.constraint.minAzimuthAngle = value;
+		if ( scope.enabled === false ) return;
 
-			}
+		handleTouchEnd( event );
 
-		},
+		scope.dispatchEvent( endEvent );
 
-		maxAzimuthAngle : {
+		state = STATE.NONE;
 
-			get: function () {
+	};
 
-				return this.constraint.maxAzimuthAngle;
+	onContextMenu = function( event ) {
 
-			},
+		//console.log( 'onContextMenu' );
 
-			set: function ( value ) {
+		event.preventDefault();
 
-				this.constraint.maxAzimuthAngle = value;
+	}
 
-			}
+	//
 
-		},
+	function init() {
 
-		enableDamping : {
+		scope.domElement.addEventListener( 'contextmenu', onContextMenu, false );
 
-			get: function () {
+		scope.domElement.addEventListener( 'mousedown', onMouseDown, false );
+		scope.domElement.addEventListener( 'mousewheel', onMouseWheel, false );
+		scope.domElement.addEventListener( 'MozMousePixelScroll', onMouseWheel, false ); // firefox
 
-				return this.constraint.enableDamping;
+		scope.domElement.addEventListener( 'touchstart', onTouchStart, false );
+		scope.domElement.addEventListener( 'touchend', onTouchEnd, false );
+		scope.domElement.addEventListener( 'touchmove', onTouchMove, false );
 
-			},
+		window.addEventListener( 'keydown', onKeyDown, false );
 
-			set: function ( value ) {
+	};
 
-				this.constraint.enableDamping = value;
+	init();
 
-			}
+	// force an update at start
 
-		},
+	this.update();
 
-		dampingFactor : {
+};
 
-			get: function () {
+THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype );
+THREE.OrbitControls.prototype.constructor = THREE.OrbitControls;
 
-				return this.constraint.dampingFactor;
+Object.defineProperties( THREE.OrbitControls.prototype, {
 
-			},
+	// backward compatibility
 
-			set: function ( value ) {
+	noZoom: {
 
-				this.constraint.dampingFactor = value;
+		get: function () {
 
-			}
+			console.warn( 'THREE.OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' );
+			return ! this.enableZoom;
 
 		},
 
-		// backward compatibility
-
-		noZoom: {
+		set: function ( value ) {
 
-			get: function () {
+			console.warn( 'THREE.OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' );
+			this.enableZoom = ! value;
 
-				console.warn( 'THREE.OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' );
-				return ! this.enableZoom;
+		}
 
-			},
+	},
 
-			set: function ( value ) {
+	noRotate: {
 
-				console.warn( 'THREE.OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' );
-				this.enableZoom = ! value;
+		get: function () {
 
-			}
+			console.warn( 'THREE.OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.' );
+			return ! this.enableRotate;
 
 		},
 
-		noRotate: {
+		set: function ( value ) {
 
-			get: function () {
+			console.warn( 'THREE.OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.' );
+			this.enableRotate = ! value;
 
-				console.warn( 'THREE.OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.' );
-				return ! this.enableRotate;
+		}
 
-			},
+	},
 
-			set: function ( value ) {
+	noPan: {
 
-				console.warn( 'THREE.OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.' );
-				this.enableRotate = ! value;
+		get: function () {
 
-			}
+			console.warn( 'THREE.OrbitControls: .noPan has been deprecated. Use .enablePan instead.' );
+			return ! this.enablePan;
 
 		},
 
-		noPan: {
+		set: function ( value ) {
 
-			get: function () {
+			console.warn( 'THREE.OrbitControls: .noPan has been deprecated. Use .enablePan instead.' );
+			this.enablePan = ! value;
 
-				console.warn( 'THREE.OrbitControls: .noPan has been deprecated. Use .enablePan instead.' );
-				return ! this.enablePan;
+		}
 
-			},
+	},
 
-			set: function ( value ) {
+	noKeys: {
 
-				console.warn( 'THREE.OrbitControls: .noPan has been deprecated. Use .enablePan instead.' );
-				this.enablePan = ! value;
+		get: function () {
 
-			}
+			console.warn( 'THREE.OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.' );
+			return ! this.enableKeys;
 
 		},
 
-		noKeys: {
+		set: function ( value ) {
 
-			get: function () {
+			console.warn( 'THREE.OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.' );
+			this.enableKeys = ! value;
 
-				console.warn( 'THREE.OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.' );
-				return ! this.enableKeys;
+		}
 
-			},
+	},
 
-			set: function ( value ) {
+	staticMoving : {
 
-				console.warn( 'THREE.OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.' );
-				this.enableKeys = ! value;
+		get: function () {
 
-			}
+			console.warn( 'THREE.OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.' );
+			return ! this.constraint.enableDamping;
 
 		},
 
-		staticMoving : {
+		set: function ( value ) {
 
-			get: function () {
+			console.warn( 'THREE.OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.' );
+			this.constraint.enableDamping = ! value;
 
-				console.warn( 'THREE.OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.' );
-				return ! this.constraint.enableDamping;
+		}
 
-			},
+	},
 
-			set: function ( value ) {
+	dynamicDampingFactor : {
 
-				console.warn( 'THREE.OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.' );
-				this.constraint.enableDamping = ! value;
+		get: function () {
 
-			}
+			console.warn( 'THREE.OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.' );
+			return this.constraint.dampingFactor;
 
 		},
 
-		dynamicDampingFactor : {
-
-			get: function () {
-
-				console.warn( 'THREE.OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.' );
-				return this.constraint.dampingFactor;
+		set: function ( value ) {
 
-			},
-
-			set: function ( value ) {
-
-				console.warn( 'THREE.OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.' );
-				this.constraint.dampingFactor = value;
-
-			}
+			console.warn( 'THREE.OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.' );
+			this.constraint.dampingFactor = value;
 
 		}
 
-	} );
+	}
+
+} );
 
-}() );

+ 10 - 40
examples/js/controls/TrackballControls.js

@@ -356,8 +356,6 @@ THREE.TrackballControls = function ( object, domElement ) {
 
 		if ( _this.enabled === false ) return;
 
-		window.removeEventListener( 'keydown', keydown );
-
 		_prevState = _state;
 
 		if ( _state !== STATE.NONE ) {
@@ -386,17 +384,12 @@ THREE.TrackballControls = function ( object, domElement ) {
 
 		_state = _prevState;
 
-		window.addEventListener( 'keydown', keydown, false );
-
 	}
 
 	function mousedown( event ) {
 
 		if ( _this.enabled === false ) return;
 
-		event.preventDefault();
-		event.stopPropagation();
-
 		if ( _state === STATE.NONE ) {
 
 			_state = event.button;
@@ -431,9 +424,6 @@ THREE.TrackballControls = function ( object, domElement ) {
 
 		if ( _this.enabled === false ) return;
 
-		event.preventDefault();
-		event.stopPropagation();
-
 		if ( _state === STATE.ROTATE && ! _this.noRotate ) {
 
 			_movePrev.copy( _moveCurr );
@@ -455,9 +445,6 @@ THREE.TrackballControls = function ( object, domElement ) {
 
 		if ( _this.enabled === false ) return;
 
-		event.preventDefault();
-		event.stopPropagation();
-
 		_state = STATE.NONE;
 
 		document.removeEventListener( 'mousemove', mousemove );
@@ -470,9 +457,6 @@ THREE.TrackballControls = function ( object, domElement ) {
 
 		if ( _this.enabled === false ) return;
 
-		event.preventDefault();
-		event.stopPropagation();
-
 		var delta = 0;
 
 		if ( event.wheelDelta ) {
@@ -507,7 +491,7 @@ THREE.TrackballControls = function ( object, domElement ) {
 				_movePrev.copy( _moveCurr );
 				break;
 
-			case 2:
+			default: // 2 or more
 				_state = STATE.TOUCH_ZOOM_PAN;
 				var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
 				var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
@@ -519,12 +503,9 @@ THREE.TrackballControls = function ( object, domElement ) {
 				_panEnd.copy( _panStart );
 				break;
 
-			default:
-				_state = STATE.NONE;
-
 		}
-		_this.dispatchEvent( startEvent );
 
+		_this.dispatchEvent( startEvent );
 
 	}
 
@@ -532,17 +513,14 @@ THREE.TrackballControls = function ( object, domElement ) {
 
 		if ( _this.enabled === false ) return;
 
-		event.preventDefault();
-		event.stopPropagation();
-
 		switch ( event.touches.length ) {
 
 			case 1:
 				_movePrev.copy( _moveCurr );
-				_moveCurr.copy( getMouseOnCircle(  event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) );
+				_moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) );
 				break;
 
-			case 2:
+			default: // 2 or more
 				var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
 				var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
 				_touchZoomDistanceEnd = Math.sqrt( dx * dx + dy * dy );
@@ -552,9 +530,6 @@ THREE.TrackballControls = function ( object, domElement ) {
 				_panEnd.copy( getMouseOnScreen( x, y ) );
 				break;
 
-			default:
-				_state = STATE.NONE;
-
 		}
 
 	}
@@ -565,23 +540,18 @@ THREE.TrackballControls = function ( object, domElement ) {
 
 		switch ( event.touches.length ) {
 
-			case 1:
-				_movePrev.copy( _moveCurr );
-				_moveCurr.copy( getMouseOnCircle(  event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) );
+			case 0:
+				_state = STATE.NONE;
 				break;
 
-			case 2:
-				_touchZoomDistanceStart = _touchZoomDistanceEnd = 0;
-
-				var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2;
-				var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2;
-				_panEnd.copy( getMouseOnScreen( x, y ) );
-				_panStart.copy( _panEnd );
+			case 1:
+				_state = STATE.TOUCH_ROTATE;
+				_moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) );
+				_movePrev.copy( _moveCurr );
 				break;
 
 		}
 
-		_state = STATE.NONE;
 		_this.dispatchEvent( endEvent );
 
 	}

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

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

+ 139 - 0
examples/js/effects/PeppersGhostEffect.js

@@ -0,0 +1,139 @@
+/**
+ * Created by tpowellmeto on 29/10/2015.
+ *
+ * peppers ghost effect based on http://www.instructables.com/id/Reflective-Prism/?ALLSTEPS
+ */
+
+THREE.PeppersGhostEffect = function ( renderer ) {
+
+    var scope = this;
+
+    scope.cameraDistance = 15;
+    scope.reflectFromAbove = false;
+
+    // Internals
+    var _halfWidth, _width, _height;
+
+    var _cameraF = new THREE.PerspectiveCamera(); //front
+    var _cameraB = new THREE.PerspectiveCamera(); //back
+    var _cameraL = new THREE.PerspectiveCamera(); //left
+    var _cameraR = new THREE.PerspectiveCamera(); //right
+
+    var _position = new THREE.Vector3();
+    var _quaternion = new THREE.Quaternion();
+    var _scale = new THREE.Vector3();
+
+    // Initialization
+    renderer.autoClear = false;
+
+    this.setSize = function ( width, height ) {
+
+        _halfWidth = width / 2;
+        if ( width < height ) {
+
+            _width = width / 3;
+            _height = width / 3;
+
+        } else {
+
+            _width = height / 3;
+            _height = height / 3;
+
+        }
+        renderer.setSize( width, height );
+
+    };
+
+    this.render = function ( scene, camera ) {
+
+        scene.updateMatrixWorld();
+
+        if ( camera.parent === null ) camera.updateMatrixWorld();
+
+        camera.matrixWorld.decompose( _position, _quaternion, _scale );
+
+        // front
+        _cameraF.position.copy( _position );
+        _cameraF.quaternion.copy( _quaternion );
+        _cameraF.translateZ( scope.cameraDistance );
+        _cameraF.lookAt( scene.position );
+
+        // back
+        _cameraB.position.copy( _position );
+        _cameraB.quaternion.copy( _quaternion );
+        _cameraB.translateZ( - ( scope.cameraDistance ) );
+        _cameraB.lookAt( scene.position );
+        _cameraB.rotation.z += 180 * ( Math.PI / 180 );
+
+        // left
+        _cameraL.position.copy( _position );
+        _cameraL.quaternion.copy( _quaternion );
+        _cameraL.translateX( - ( scope.cameraDistance ) );
+        _cameraL.lookAt( scene.position );
+        _cameraL.rotation.x += 90 * ( Math.PI / 180 );
+
+        // right
+        _cameraR.position.copy( _position );
+        _cameraR.quaternion.copy( _quaternion );
+        _cameraR.translateX( scope.cameraDistance );
+        _cameraR.lookAt( scene.position );
+        _cameraR.rotation.x += 90 * ( Math.PI / 180 );
+
+
+        renderer.clear();
+        renderer.enableScissorTest( true );
+
+        renderer.setScissor( _halfWidth - ( _width / 2 ), ( _height * 2 ), _width, _height );
+        renderer.setViewport( _halfWidth - ( _width / 2 ), ( _height * 2 ), _width, _height );
+        if ( scope.reflectFromAbove ) {
+
+            renderer.render( scene, _cameraB );
+
+        } else {
+
+            renderer.render( scene, _cameraF );
+
+        }
+
+        renderer.setScissor( _halfWidth - ( _width / 2 ), 0, _width, _height );
+        renderer.setViewport( _halfWidth - ( _width / 2 ), 0, _width, _height );
+        if ( scope.reflectFromAbove ) {
+
+            renderer.render( scene, _cameraF );
+
+        } else {
+
+            renderer.render( scene, _cameraB );
+
+        }
+
+        renderer.setScissor( _halfWidth - ( _width / 2 ) - _width, _height, _width, _height );
+        renderer.setViewport( _halfWidth - ( _width / 2 ) - _width, _height, _width, _height );
+        if ( scope.reflectFromAbove ) {
+
+            renderer.render( scene, _cameraR );
+
+        } else {
+
+            renderer.render( scene, _cameraL );
+
+        }
+
+        renderer.setScissor( _halfWidth + ( _width / 2 ), _height, _width, _height );
+        renderer.setViewport( _halfWidth + ( _width / 2 ), _height, _width, _height );
+        if ( scope.reflectFromAbove ) {
+
+            renderer.render( scene, _cameraL );
+
+        } else {
+
+            renderer.render( scene, _cameraR );
+
+        }
+
+        renderer.enableScissorTest( false );
+
+    };
+
+
+};

+ 6 - 1
examples/js/exporters/OBJExporter.js

@@ -24,6 +24,12 @@ THREE.OBJExporter.prototype = {
 
 			var geometry = mesh.geometry;
 
+			if ( geometry instanceof THREE.BufferGeometry ) {
+
+				geometry = new THREE.Geometry().fromBufferGeometry(geometry);
+
+			}
+
 			if ( geometry instanceof THREE.Geometry ) {
 
 				output += 'o ' + mesh.name + '\n';
@@ -124,7 +130,6 @@ THREE.OBJExporter.prototype = {
 			} else {
 
 				console.warn( 'THREE.OBJExporter.parseMesh(): geometry type unsupported', mesh );
-				// TODO: Support only BufferGeometry and use use setFromObject()
 
 			}
 

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно