Mr.doob 1 year ago
parent
commit
d75d029bc7
100 changed files with 2501 additions and 1153 deletions
  1. 49 4
      build/three.cjs
  2. 49 4
      build/three.module.js
  3. 0 0
      build/three.module.min.js
  4. 8 15
      docs/api/ar/renderers/WebGLRenderer.html
  5. 5 1
      docs/api/en/animation/tracks/BooleanKeyframeTrack.html
  6. 1 1
      docs/api/en/animation/tracks/ColorKeyframeTrack.html
  7. 1 1
      docs/api/en/animation/tracks/NumberKeyframeTrack.html
  8. 5 5
      docs/api/en/animation/tracks/QuaternionKeyframeTrack.html
  9. 6 5
      docs/api/en/animation/tracks/StringKeyframeTrack.html
  10. 1 1
      docs/api/en/animation/tracks/VectorKeyframeTrack.html
  11. 25 0
      docs/api/en/objects/BatchedMesh.html
  12. 3 3
      docs/api/en/objects/InstancedMesh.html
  13. 26 11
      docs/api/en/renderers/WebGLRenderer.html
  14. 21 0
      docs/api/en/textures/CompressedArrayTexture.html
  15. 22 0
      docs/api/en/textures/DataArrayTexture.html
  16. 9 8
      docs/api/en/textures/DepthTexture.html
  17. 1 1
      docs/api/en/textures/FramebufferTexture.html
  18. 7 7
      docs/api/it/renderers/WebGLRenderer.html
  19. 12 3
      docs/api/zh/renderers/WebGLRenderer.html
  20. 1 1
      docs/api/zh/textures/FramebufferTexture.html
  21. 67 0
      docs/examples/en/modifiers/EdgeSplitModifier.html
  22. 91 0
      docs/examples/en/objects/Sky.html
  23. 24 0
      docs/examples/en/utils/SceneUtils.html
  24. 24 0
      docs/examples/zh/utils/SceneUtils.html
  25. 6 1
      docs/list.json
  26. 7 9
      docs/manual/en/introduction/Creating-a-scene.html
  27. 5 5
      docs/manual/zh/introduction/Installation.html
  28. 9 0
      editor/.eslintrc.json
  29. 92 9
      editor/css/main.css
  30. 6 12
      editor/index.html
  31. 5 1
      editor/js/Config.js
  32. 19 2
      editor/js/Editor.js
  33. 3 3
      editor/js/History.js
  34. 28 34
      editor/js/Loader.js
  35. 134 75
      editor/js/Menubar.Add.js
  36. 9 3
      editor/js/Menubar.Edit.js
  37. 0 66
      editor/js/Menubar.Examples.js
  38. 190 29
      editor/js/Menubar.File.js
  39. 76 3
      editor/js/Menubar.View.js
  40. 0 2
      editor/js/Menubar.js
  41. 1 1
      editor/js/Resizer.js
  42. 76 10
      editor/js/Script.js
  43. 3 3
      editor/js/Sidebar.Geometry.BufferGeometry.js
  44. 2 2
      editor/js/Sidebar.Geometry.CircleGeometry.js
  45. 52 33
      editor/js/Sidebar.Geometry.ExtrudeGeometry.js
  46. 2 2
      editor/js/Sidebar.Geometry.RingGeometry.js
  47. 4 4
      editor/js/Sidebar.Geometry.SphereGeometry.js
  48. 1 1
      editor/js/Sidebar.Geometry.TorusGeometry.js
  49. 51 6
      editor/js/Sidebar.Geometry.js
  50. 38 10
      editor/js/Sidebar.Material.js
  51. 1 4
      editor/js/Sidebar.Object.js
  52. 46 14
      editor/js/Sidebar.Project.Image.js
  53. 120 33
      editor/js/Sidebar.Project.Video.js
  54. 48 0
      editor/js/Sidebar.Properties.js
  55. 44 12
      editor/js/Sidebar.Scene.js
  56. 1 1
      editor/js/Sidebar.Script.js
  57. 2 2
      editor/js/Sidebar.Settings.History.js
  58. 11 1
      editor/js/Sidebar.js
  59. 1 1
      editor/js/Storage.js
  60. 360 176
      editor/js/Strings.js
  61. 20 23
      editor/js/Viewport.Controls.js
  62. 53 7
      editor/js/Viewport.Info.js
  63. 34 124
      editor/js/Viewport.Pathtracer.js
  64. 94 22
      editor/js/Viewport.js
  65. 4 3
      editor/js/commands/AddObjectCommand.js
  66. 2 2
      editor/js/commands/AddScriptCommand.js
  67. 1 0
      editor/js/commands/Commands.js
  68. 7 7
      editor/js/commands/MoveObjectCommand.js
  69. 3 3
      editor/js/commands/MultiCmdsCommand.js
  70. 11 4
      editor/js/commands/RemoveObjectCommand.js
  71. 4 3
      editor/js/commands/RemoveScriptCommand.js
  72. 3 3
      editor/js/commands/SetColorCommand.js
  73. 4 4
      editor/js/commands/SetGeometryCommand.js
  74. 3 3
      editor/js/commands/SetGeometryValueCommand.js
  75. 12 6
      editor/js/commands/SetMaterialColorCommand.js
  76. 5 3
      editor/js/commands/SetMaterialCommand.js
  77. 14 8
      editor/js/commands/SetMaterialMapCommand.js
  78. 14 8
      editor/js/commands/SetMaterialRangeCommand.js
  79. 14 8
      editor/js/commands/SetMaterialValueCommand.js
  80. 13 7
      editor/js/commands/SetMaterialVectorCommand.js
  81. 4 4
      editor/js/commands/SetPositionCommand.js
  82. 4 4
      editor/js/commands/SetRotationCommand.js
  83. 4 4
      editor/js/commands/SetScaleCommand.js
  84. 3 3
      editor/js/commands/SetSceneCommand.js
  85. 5 5
      editor/js/commands/SetScriptValueCommand.js
  86. 3 3
      editor/js/commands/SetUuidCommand.js
  87. 3 3
      editor/js/commands/SetValueCommand.js
  88. 0 0
      editor/js/libs/ffmpeg.min.js
  89. 58 17
      editor/js/libs/ui.js
  90. 21 26
      editor/js/libs/ui.three.js
  91. 0 2
      editor/sw.js
  92. 37 32
      examples/files.json
  93. 2 5
      examples/games_fps.html
  94. 1 1
      examples/jsm/controls/TransformControls.js
  95. 1 5
      examples/jsm/environments/RoomEnvironment.js
  96. 6 4
      examples/jsm/exporters/USDZExporter.js
  97. 32 67
      examples/jsm/helpers/ViewHelper.js
  98. 75 64
      examples/jsm/libs/tween.module.js
  99. 1 15
      examples/jsm/lines/LineMaterial.js
  100. 15 0
      examples/jsm/lines/LineSegments2.js

File diff suppressed because it is too large
+ 49 - 4
build/three.cjs


File diff suppressed because it is too large
+ 49 - 4
build/three.module.js


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


+ 8 - 15
docs/api/ar/renderers/WebGLRenderer.html

@@ -369,7 +369,7 @@
 		</p>
 		 
 		<h3>
-		[method:undefined copyFramebufferToTexture]( [param:Vector2 position], [param:FramebufferTexture texture], [param:Number level] )
+		[method:undefined copyFramebufferToTexture]( [param:FramebufferTexture texture], [param:Vector2 position], [param:Number level] )
 		</h3>
 		<p>
 		ينسخ بكسلات من WebGLFramebuffer الحالي إلى قوام ثنائي الأبعاد. يتيح
@@ -377,23 +377,16 @@
 		[link:https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/copyTexImage2D WebGLRenderingContext.copyTexImage2D].
 		</p>
 		 
-		<h3>
-		[method:undefined copyTextureToTexture]( [param:Vector2 position], [param:Texture srcTexture], [param:Texture dstTexture], [param:Number level] )
-		</h3>
+		<h3>[method:undefined copyTextureToTexture]( [param:Texture srcTexture], [param:Texture dstTexture], [param:Box2 srcRegion], [param:Vector2 dstPosition], [param:Number level] )</h3>
 		<p>
-		ينسخ جميع بكسلات قوام إلى قوام موجود بدءًا من
-		الموضع المعطى. يتيح الوصول إلى
-		[link:https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/texSubImage2D WebGLRenderingContext.texSubImage2D].
+			Copies the pixels of a texture in the bounds '[page:Box2 srcRegion]' in the destination texture starting from the given position. 
+			Enables access to [link:https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/texSubImage2D WebGLRenderingContext.texSubImage2D].
 		</p>
-		 
-		<h3>
-		[method:undefined copyTextureToTexture3D]( [param:Box3 sourceBox], [param:Vector3 position], [param:Texture srcTexture], [param:Texture dstTexture], [param:Number level] )
-		</h3>
+
+		<h3>[method:undefined copyTextureToTexture3D]( [param:Texture srcTexture], [param:Texture dstTexture], [param:Box3 srcRegion], [param:Vector3 dstPosition], [param:Number level] )</h3>
 		<p>
-		ينسخ بكسلات قوام في الحدود '[page:Box3 sourceBox]' في
-		قوام الوجهة بدءًا من الموضع المعطى. يتيح الوصول
-		إلى
-		[link:https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext/texSubImage3D WebGL2RenderingContext.texSubImage3D].
+			Copies the pixels of a texture in the bounds '[page:Box3 srcRegion]' in the destination texture starting from the given position. 
+			Enables access to [link:https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext/texSubImage3D WebGL2RenderingContext.texSubImage3D].
 		</p>
 		 
 		<h3>[method:undefined dispose]( )</h3>

+ 5 - 1
docs/api/en/animation/tracks/BooleanKeyframeTrack.html

@@ -23,6 +23,10 @@
 			[page:Array times] - (required) array of keyframe times.<br />
 			[page:Array values] - values for the keyframes at the times specified.<br />
 		</p>
+		<p>
+			This keyframe track type has no interpolation parameter because the
+			interpolation is always [page:Animation InterpolateDiscrete].
+		</p>
 
 		<h2>Properties</h2>
 
@@ -30,7 +34,7 @@
 
 		<h3>[property:Constant DefaultInterpolation]</h3>
 		<p>
-			The default interpolation type to use, [page:Animation InterpolateDiscrete].
+			The default interpolation type to use. Only [page:Animation InterpolateDiscrete] is valid for this track type.
 		</p>
 
 		<h3>[property:Array ValueBufferType]</h3>

+ 1 - 1
docs/api/en/animation/tracks/ColorKeyframeTrack.html

@@ -20,7 +20,7 @@
 		<h2>Constructor</h2>
 
 		<h3>
-			[name]( [param:String name], [param:Array times], [param:Array values] )
+			[name]( [param:String name], [param:Array times], [param:Array values], [param:Constant interpolation] )
 		</h3>
 		<p>
 			[page:String name] - (required) identifier for the KeyframeTrack.<br />

+ 1 - 1
docs/api/en/animation/tracks/NumberKeyframeTrack.html

@@ -16,7 +16,7 @@
 		<h2>Constructor</h2>
 
 		<h3>
-			[name]( [param:String name], [param:Array times], [param:Array values] )
+			[name]( [param:String name], [param:Array times], [param:Array values], [param:Constant interpolation] )
 		</h3>
 		<p>
 			[page:String name] - (required) identifier for the KeyframeTrack.<br />

+ 5 - 5
docs/api/en/animation/tracks/QuaternionKeyframeTrack.html

@@ -16,14 +16,14 @@
 		<h2>Constructor</h2>
 
 		<h3>
-			[name]( [param:String name], [param:Array times], [param:Array values] )
+			[name]( [param:String name], [param:Array times], [param:Array values], [param:Constant interpolation] )
 		</h3>
 		<p>
-			[page:String name] (required) identifier for the KeyframeTrack.<br />
-			[page:Array times] (required) array of keyframe times.<br />
-			[page:Array values] values for the keyframes at the times specified, a
+			[page:String name] - (required) identifier for the KeyframeTrack.<br />
+			[page:Array times] - (required) array of keyframe times.<br />
+			[page:Array values] - values for the keyframes at the times specified, a
 			flat array of quaternion components.<br />
-			[page:Constant interpolation] the type of interpolation to use. See
+			[page:Constant interpolation] - the type of interpolation to use. See
 			[page:Animation Animation Constants] for possible values. Default is
 			[page:Animation InterpolateLinear].
 		</p>

+ 6 - 5
docs/api/en/animation/tracks/StringKeyframeTrack.html

@@ -24,9 +24,10 @@
 			[page:String name] - (required) identifier for the KeyframeTrack.<br />
 			[page:Array times] - (required) array of keyframe times.<br />
 			[page:Array values] - values for the keyframes at the times specified.<br />
-			[page:Constant interpolation] - the type of interpolation to use. See
-			[page:Animation Animation Constants] for possible values. Default is
-			[page:Animation InterpolateDiscrete].
+		</p>
+		<p>
+			This keyframe track type has no interpolation parameter because the
+			interpolation is always [page:Animation InterpolateDiscrete].
 		</p>
 
 		<h2>Properties</h2>
@@ -35,7 +36,7 @@
 
 		<h3>[property:Constant DefaultInterpolation]</h3>
 		<p>
-			The default interpolation type to use, [page:Animation InterpolateDiscrete].
+			The default interpolation type to use. Only [page:Animation InterpolateDiscrete] is valid for this track type.
 		</p>
 
 		<h3>[property:Array ValueBufferType]</h3>
@@ -70,4 +71,4 @@
 		</p>
 	</body>
 
-</html>
+</html>

+ 1 - 1
docs/api/en/animation/tracks/VectorKeyframeTrack.html

@@ -17,7 +17,7 @@
 		<h2>Constructor</h2>
 
 		<h3>
-			[name]( [param:String name], [param:Array times], [param:Array values] )
+			[name]( [param:String name], [param:Array times], [param:Array values], [param:Constant interpolation] )
 		</h3>
 		<p>
 			[page:String name] - (required) identifier for the KeyframeTrack.<br />

+ 25 - 0
docs/api/en/objects/BatchedMesh.html

@@ -129,6 +129,19 @@
 			in the list include a "z" field to perform a depth-ordered sort with.
 		</p>
 
+		<h3>
+			[method:undefined getColorAt]( [param:Integer index], [param:Color color] )
+		</h3>
+		<p>
+			[page:Integer index]: The index of a geometry. Values have to be in the
+			range [0, count].
+		</p>
+		<p>
+			[page:Color color]: This color object will be set to the color of the
+			defined geometry.
+		</p>
+		<p>Get the color of the defined geometry.</p>
+
 		<h3>
 			[method:Matrix4 getMatrixAt]( [param:Integer index], [param:Matrix4 matrix] )
 		</h3>
@@ -151,6 +164,18 @@
 		</p>
 		<p>Get whether the given instance is marked as "visible" or not.</p>
 
+		<h3>
+			[method:undefined setColorAt]( [param:Integer index], [param:Color color] )
+		</h3>
+		<p>
+			[page:Integer index]: The index of a geometry. Values have to be in the
+			range [0, count].
+		</p>
+		<p>[page:Color color]: The color of a single geometry.</p>
+		<p>
+			Sets the given color to the defined geometry.
+		</p>
+
 		<h3>
 			[method:this setMatrixAt]( [param:Integer index], [param:Matrix4 matrix] )
 		</h3>

+ 3 - 3
docs/api/en/objects/InstancedMesh.html

@@ -14,7 +14,7 @@
 		<p class="desc">
 			A special version of [page:Mesh] with instanced rendering support. Use
 			[name] if you have to render a large number of objects with the same
-			geometry and material but with different world transformations. The usage
+			geometry and material(s) but with different world transformations. The usage
 			of [name] will help you to reduce the number of draw calls and thus
 			improve the overall rendering performance in your application.
 		</p>
@@ -34,8 +34,8 @@
 		</h3>
 		<p>
 			[page:BufferGeometry geometry] - an instance of [page:BufferGeometry].<br />
-			[page:Material material] - an instance of [page:Material]. Default is a
-			new [page:MeshBasicMaterial].<br />
+			[page:Material material] — a single or an array of
+			[page:Material]. Default is a new [page:MeshBasicMaterial].<br />
 			[page:Integer count] - the number of instances.<br />
 		</p>
 

+ 26 - 11
docs/api/en/renderers/WebGLRenderer.html

@@ -19,7 +19,7 @@
 		<h3>[name]( [param:Object parameters] )</h3>
 		<p>
 			[page:Object parameters] - (optional) object with properties defining the
-			renderer's behaviour. The constructor also accepts no parameters at all.
+			renderer's behavior. The constructor also accepts no parameters at all.
 			In all cases, it will assume sane defaults when parameters are missing.
 			The following are valid parameters:<br /><br />
 
@@ -367,7 +367,7 @@ document.body.appendChild( renderer.domElement );
 		</p>
 
 		<h3>
-			[method:undefined copyFramebufferToTexture]( [param:Vector2 position], [param:FramebufferTexture texture], [param:Number level] )
+			[method:undefined copyFramebufferToTexture]( [param:FramebufferTexture texture], [param:Vector2 position], [param:Number level] )
 		</h3>
 		<p>
 			Copies pixels from the current WebGLFramebuffer into a 2D texture. Enables
@@ -376,19 +376,20 @@ document.body.appendChild( renderer.domElement );
 		</p>
 
 		<h3>
-			[method:undefined copyTextureToTexture]( [param:Vector2 position], [param:Texture srcTexture], [param:Texture dstTexture], [param:Number level] )
+			[method:undefined copyTextureToTexture]( [param:Texture srcTexture], [param:Texture dstTexture], [param:Box2 srcRegion], [param:Vector2 dstPosition], [param:Number level] )
 		</h3>
 		<p>
-			Copies all pixels of a texture to an existing texture starting from the
-			given position. Enables access to
+			Copies the pixels of a texture in the bounds '[page:Box2 srcRegion]' in
+			the destination texture starting from the given position. Enables access
+			to
 			[link:https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/texSubImage2D WebGLRenderingContext.texSubImage2D].
 		</p>
 
 		<h3>
-			[method:undefined copyTextureToTexture3D]( [param:Box3 sourceBox], [param:Vector3 position], [param:Texture srcTexture], [param:Texture dstTexture], [param:Number level] )
+			[method:undefined copyTextureToTexture3D]( [param:Texture srcTexture], [param:Texture dstTexture], [param:Box3 srcRegion], [param:Vector3 dstPosition], [param:Number level] )
 		</h3>
 		<p>
-			Copies the pixels of a texture in the bounds '[page:Box3 sourceBox]' in
+			Copies the pixels of a texture in the bounds '[page:Box3 srcRegion]' in
 			the destination texture starting from the given position. Enables access
 			to
 			[link:https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext/texSubImage3D WebGL2RenderingContext.texSubImage3D].
@@ -492,6 +493,13 @@ document.body.appendChild( renderer.domElement );
 			and GPU upload overhead).
 		</p>
 
+		<h3>[method:undefined initRenderTarget]( [param:WebGLRenderTarget target] )</h3>
+		<p>
+			Initializes the given WebGLRenderTarget memory. Useful for initializing a render
+			target so data can be copied into it using [page:WebGLRenderer.copyTextureToTexture .copyTextureToTexture]
+			before it has been rendered to.
+		</p>
+
 		<h3>[method:undefined resetGLState]( )</h3>
 		<p>
 			Reset the GL state to default. Called internally if the WebGL context is
@@ -511,16 +519,23 @@ document.body.appendChild( renderer.domElement );
 			This is a wrapper around
 			[link:https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/readPixels WebGLRenderingContext.readPixels]().
 		</p>
-		<p>
-			See the [example:webgl_interactive_cubes_gpu interactive / cubes / gpu]
-			example.
-		</p>
 		<p>
 			For reading out a [page:WebGLCubeRenderTarget WebGLCubeRenderTarget] use
 			the optional parameter activeCubeFaceIndex to determine which face should
 			be read.
 		</p>
 
+		<h3>
+			[method:Promise readRenderTargetPixelsAsync]( [param:WebGLRenderTarget renderTarget], [param:Float x], [param:Float y], [param:Float width], [param:Float height], [param:TypedArray buffer], [param:Integer activeCubeFaceIndex] )
+		</h3>
+		<p>
+			Asynchronous, non-blocking version of [page:WebGLRenderer.readRenderTargetPixels .readRenderTargetPixels]. The
+			returned promise resolves once the buffer data is ready to be used.
+		</p>
+		<p>
+			See the [example:webgl_interactive_cubes_gpu interactive / cubes / gpu] example.
+		</p>
+
 		<h3>
 			[method:undefined render]( [param:Object3D scene], [param:Camera camera] )
 		</h3>

+ 21 - 0
docs/api/en/textures/CompressedArrayTexture.html

@@ -61,11 +61,32 @@
 		<h3>[property:Object image]</h3>
 		<p>Overridden with a object containing width, height, and depth.</p>
 
+		<h3>[property:Set layerUpdates]</h3>
+		<p>
+			A set of all layers which need to be updated in the texture. See
+			[Page:CompressedTextureArray.addLayerUpdate addLayerUpdate].
+		</p>
+
 		<h3>[property:Boolean isCompressedArrayTexture]</h3>
 		<p>Read-only flag to check if a given object is of type [name].</p>
 
 		<h2>Methods</h2>
 
+		<h3>[method:addLayerUpdate addLayerUpdate]( layerIndex )</h3>
+		<p>
+			Describes that a specific layer of the texture needs to be updated.
+			Normally when [page:Texture.needsUpdate needsUpdate] is set to true, the
+			entire compressed texture array is sent to the GPU. Marking specific
+			layers will only transmit subsets of all mipmaps associated with a
+			specific depth in the array which is often much more performant.
+		</p>
+
+		<h3>[method:clearLayerUpdates clearLayerUpdates]()</h3>
+		<p>
+			Resets the layer updates registry. See
+			[Page:CompressedTextureArray.addLayerUpdate addLayerUpdate].
+		</p>
+
 		<p>
 			See the base [page:CompressedTexture CompressedTexture] class for common
 			methods.

+ 22 - 0
docs/api/en/textures/DataArrayTexture.html

@@ -143,8 +143,30 @@
 			page for details.
 		</p>
 
+		<h3>[property:Set layerUpdates]</h3>
+		<p>
+			A set of all layers which need to be updated in the texture. See
+			[Page:DataArrayTexture.addLayerUpdate addLayerUpdate].
+		</p>
+
 		<h2>Methods</h2>
 
+		<h3>[method:addLayerUpdate addLayerUpdate]( layerIndex )</h3>
+		<p>
+			Describes that a specific layer of the texture needs to be updated.
+			Normally when [page:Texture.needsUpdate needsUpdate] is set to true, the
+			entire compressed texture array is sent to the GPU. Marking specific
+			layers will only transmit subsets of all mipmaps associated with a
+			specific depth in the array which is often much more performant.
+		</p>
+
+		<h3>[method:clearLayerUpdates clearLayerUpdates]()</h3>
+		<p>
+			Resets the layer updates registry. See
+			[Page:DataArrayTexture.addLayerUpdate addLayerUpdate].
+		</p>
+
+
 		<p>See the base [page:Texture Texture] class for common methods.</p>
 
 		<h2>Source</h2>

+ 9 - 8
docs/api/en/textures/DepthTexture.html

@@ -33,10 +33,7 @@
 
 			[page:Number height] -- height of the texture.<br />
 
-			[page:Constant type] -- Default is [page:Textures THREE.UnsignedIntType]
-			when using [page:Textures DepthFormat] and [page:Textures THREE.UnsignedInt248Type] 
-			when using [page:Textures DepthStencilFormat].
-			See [page:Textures type constants] for other choices.<br />
+			[page:Constant type] -- Default is [page:Textures THREE.UnsignedIntType]. See [page:DepthTexture DepthTexture.type] for other choices.<br />
 
 			[page:Constant mapping] -- See [page:Textures mapping mode constants] for
 			details.<br />
@@ -87,10 +84,14 @@
 
 		<h3>[page:Texture.type type]</h3>
 		<p>
-			Default is [page:Textures THREE.UnsignedIntType] when using [page:Textures DepthFormat] 
-			and [page:Textures THREE.UnsignedInt248Type] when using
-			[page:Textures DepthStencilFormat]. See [page:Textures format constants]
-			for details.<br />
+			Default is [page:Textures THREE.UnsignedIntType]. The following are options and how they map to internal
+			gl depth format types depending on the stencil format, as well:
+
+			[page:Textures THREE.UnsignedIntType] -- Uses DEPTH_COMPONENT24 or DEPTH24_STENCIL8 internally.<br />
+
+			[page:Textures THREE.FloatType] -- Uses DEPTH_COMPONENT32F or DEPTH32F_STENCIL8 internally.<br />
+
+			[page:Textures THREE.UnsignedShortType] -- Uses DEPTH_COMPONENT16 internally. Stencil buffer is unsupported when using this type.<br />
 		</p>
 
 		<h3>[page:Texture.magFilter magFilter]</h3>

+ 1 - 1
docs/api/en/textures/FramebufferTexture.html

@@ -33,7 +33,7 @@ renderer.clear();
 renderer.render( scene, camera );
 
 // copy part of the rendered frame into the framebuffer texture
-renderer.copyFramebufferToTexture( vector, frameTexture );
+renderer.copyFramebufferToTexture( frameTexture, vector );
 		</code>
 
 		<h2>Examples</h2>

+ 7 - 7
docs/api/it/renderers/WebGLRenderer.html

@@ -321,22 +321,22 @@
 			Questo metodo utilizza *KHR_parallel_shader_compile*.
 		</p>
 
-		<h3>[method:undefined copyFramebufferToTexture]( [param:Vector2 position], [param:FramebufferTexture texture], [param:Number level] )</h3>
+		<h3>[method:undefined copyFramebufferToTexture]( [param:FramebufferTexture texture], [param:Vector2 position], [param:Number level] )</h3>
 		<p>
 			Copia i pixel dal WebGLFramebuffer corrente in una texture 2D. Abilita l'accesso a 
 			[link:https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/copyTexImage2D WebGLRenderingContext.copyTexImage2D].
 		</p>
 
-		<h3>[method:undefined copyTextureToTexture]( [param:Vector2 position], [param:Texture srcTexture], [param:Texture dstTexture], [param:Number level] )</h3>
+		<h3>[method:undefined copyTextureToTexture]( [param:Texture srcTexture], [param:Texture dstTexture], [param:Box2 srcRegion], [param:Vector2 dstPosition], [param:Number level] )</h3>
 		<p>
-			Copia tutti i pixel della texture in una texture esistente partendo dalla posizione data. Abilita l'accesso a
-			[link:https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/texSubImage2D WebGLRenderingContext.texSubImage2D].
+			Copies the pixels of a texture in the bounds '[page:Box2 srcRegion]' in the destination texture starting from the given position. 
+			Enables access to [link:https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/texSubImage2D WebGLRenderingContext.texSubImage2D].
 		</p>
 
-		<h3>[method:undefined copyTextureToTexture3D]( [param:Box3 sourceBox], [param:Vector3 position], [param:Texture srcTexture], [param:Texture dstTexture], [param:Number level] )</h3>
+		<h3>[method:undefined copyTextureToTexture3D]( [param:Texture srcTexture], [param:Texture dstTexture], [param:Box3 srcRegion], [param:Vector3 dstPosition], [param:Number level] )</h3>
 		<p>
-			Copia i pixel della texture nei limiti '[page:Box3 sourceBox]' nella texture di destinazione partendo dalla posizione data. Abilita l'accesso a
-			[link:https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext/texSubImage3D WebGL2RenderingContext.texSubImage3D].
+			Copies the pixels of a texture in the bounds '[page:Box3 srcRegion]' in the destination texture starting from the given position. 
+			Enables access to [link:https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext/texSubImage3D WebGL2RenderingContext.texSubImage3D].
 		</p>
 
 		<h3>[method:undefined dispose]( )</h3>

+ 12 - 3
docs/api/zh/renderers/WebGLRenderer.html

@@ -281,11 +281,20 @@
 			此方法利用 *KHR_parallel_shader_compile*。
 		</p>
 
-		<h3>[method:undefined copyFramebufferToTexture]( [param:Vector2 position], [param:FramebufferTexture texture], [param:Number level] )</h3>
+		<h3>[method:undefined copyFramebufferToTexture]( [param:FramebufferTexture texture], [param:Vector2 position], [param:Number level] )</h3>
 		<p>将当前WebGLFramebuffer中的像素复制到2D纹理中。可访问[link:https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/copyTexImage2D WebGLRenderingContext.copyTexImage2D].</p>
 
-		<h3>[method:undefined copyTextureToTexture]( [param:Vector2 position], [param:Texture srcTexture], [param:Texture dstTexture], [param:Number level] )</h3>
-		<p>将纹理的所有像素复制到一个已有的从给定位置开始的纹理中。可访问[link:https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/texSubImage2D WebGLRenderingContext.texSubImage2D].</p>
+		<h3>[method:undefined copyTextureToTexture]( [param:Texture srcTexture], [param:Texture dstTexture], [param:Box2 srcRegion], [param:Vector2 dstPosition], [param:Number level] )</h3>
+		<p>
+			Copies the pixels of a texture in the bounds '[page:Box2 srcRegion]' in the destination texture starting from the given position. 
+			Enables access to [link:https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/texSubImage2D WebGLRenderingContext.texSubImage2D].
+		</p>
+
+		<h3>[method:undefined copyTextureToTexture3D]( [param:Texture srcTexture], [param:Texture dstTexture], [param:Box3 srcRegion], [param:Vector3 dstPosition], [param:Number level] )</h3>
+		<p>
+			Copies the pixels of a texture in the bounds '[page:Box3 srcRegion]' in the destination texture starting from the given position. 
+			Enables access to [link:https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext/texSubImage3D WebGL2RenderingContext.texSubImage3D].
+		</p>
 
 		<h3>[method:undefined dispose]( )</h3>
 		<p>处理当前的渲染环境</p>

+ 1 - 1
docs/api/zh/textures/FramebufferTexture.html

@@ -32,7 +32,7 @@
 		renderer.render( scene, camera );
 
 		// copy part of the rendered frame into the framebuffer texture
-		renderer.copyFramebufferToTexture( vector, frameTexture );
+		renderer.copyFramebufferToTexture( frameTexture, vector );
 		</code>
 
 		<h2>例子</h2>

+ 67 - 0
docs/examples/en/modifiers/EdgeSplitModifier.html

@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<meta charset="utf-8" />
+		<base href="../../../" />
+		<script src="page.js"></script>
+		<link type="text/css" rel="stylesheet" href="page.css" />
+	</head>
+	<body>
+
+		<h1>[name]</h1>
+
+		<p class="desc">
+			[name] is intended to modify the geometry "dissolving" the edges to give a smoother look.
+		</p>
+
+		<h2>Import</h2>
+
+		<p>
+			[name] is an add-on, and therefore must be imported explicitly.
+			See [link:#manual/introduction/Installation Installation / Addons].
+		</p>
+
+		<code>
+			import { EdgeSplitModifier } from 'three/addons/modifiers/EdgeSplitModifier.js';
+		</code>
+
+		<h2>Code Example</h2>
+
+		<code>
+			const geometry = new THREE.IcosahedronGeometry( 10, 3 );<br />
+			const modifier = new EdgeSplitModifier();<br />
+			const cutOffAngle = 0.5;<br />
+			const tryKeepNormals = false;<br />
+			<br />
+			modifier.modify( geometry, cutOffAngle, tryKeepNormals );
+		</code>
+
+		<h2>Examples</h2>
+
+		<p>[example:webgl_modifier_edgesplit misc / modifiers / EdgeSplit ]</p>
+
+		<h2>Constructor</h2>
+
+		<h3>[name]()</h3>
+		<p>
+			Create a new [name] object.
+		</p>
+
+		<h2>Methods</h2>
+
+		<h3>[method:undefined modify]( [param:geometry], [param:cutOffAngle], [param:tryKeepNormals] )</h3>
+		<p>
+			Using interpolated vertex normals, the mesh faces will blur at the edges and appear smooth.<br />
+
+			You can control the smoothness by setting the `cutOffAngle`.<br />
+
+			To try to keep the original normals, set `tryKeepNormals` to `true`.
+		</p>
+
+		<h2>Source</h2>
+
+		<p>
+			[link:https://github.com/mrdoob/three.js/blob/master/examples/jsm/modifiers/EdgeSplitModifier.js examples/jsm/modifiers/EdgeSplitModifier.js]
+		</p>
+	</body>
+</html>

+ 91 - 0
docs/examples/en/objects/Sky.html

@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<meta charset="utf-8" />
+		<base href="../../../" />
+		<script src="page.js"></script>
+		<link type="text/css" rel="stylesheet" href="page.css" />
+	</head>
+	<body>
+		[page:Mesh] &rarr;
+
+		<h1>[name]</h1>
+
+		<p class="desc">
+			[name] creates a ready to go sky environment for your scenes.
+		</p>
+
+		<h2>Import</h2>
+
+		<p>
+			[name] is an add-on, and therefore must be imported explicitly.
+			See [link:#manual/introduction/Installation Installation / Addons].
+		</p>
+
+		<code>
+			import { Sky } from 'three/addons/objects/Sky.js';
+		</code>
+
+		<h2>Code Example</h2>
+
+		<code>
+		const sky = new Sky();<br />
+		sky.scale.setScalar( 450000 );<br />
+
+		const phi = MathUtils.degToRad( 90 );<br />
+		const theta = MathUtils.degToRad( 180 );<br />
+		const sunPosition = new Vector3().setFromSphericalCoords( 1, phi, theta );<br />
+
+		sky.material.uniforms.sunPosition.value = sunPosition;<br />
+
+		scene.add( sky );
+		</code>
+
+		<h2>Examples</h2>
+
+		<p>[example:webgl_shaders_sky misc / objects / Sky ]</p>
+
+		<h2>Constructor</h2>
+
+		<h3>[name]()</h3>
+		<p>
+		Create a new [name] instance.
+		</p>
+
+		<h2>Properties</h2>
+		<p>
+		[name] instance is a [page:Mesh] with a pre-defined [page:ShaderMaterial], so every property described here should be set using [page:Uniform]s.
+		</p>
+
+		<h3>[property:Number turbidity]</h3>
+		<p>
+		Haziness of the [name].
+		</p>
+		<h3>[property:Number rayleigh]</h3>
+		<p>
+		For a more detailed explanation see: [link:https://en.wikipedia.org/wiki/Rayleigh_scattering Rayleigh scattering] .
+		</p>
+		<h3>[property:Number mieCoefficient]</h3>
+		<p>
+		[link:https://en.wikipedia.org/wiki/Mie_scattering Mie scattering] amount.
+		</p>
+		<h3>[property:Number mieDirectionalG]</h3>
+		<p>
+		[link:https://en.wikipedia.org/wiki/Mie_scattering Mie scattering] direction.
+		</p>
+		<h3>[property:Vector3 sunPosition]</h3>
+		<p>
+		The position of the sun.
+		</p>
+		<h3>[property:Vector3 up]</h3>
+		<p>
+		The sun's elevation from the horizon, in degrees.
+		</p>
+
+		<h2>Source</h2>
+
+		<p>
+		[link:https://github.com/mrdoob/three.js/blob/master/examples/jsm/objects/Sky.js examples/jsm/objects/Sky.js]
+		</p>
+	</body>
+</html>

+ 24 - 0
docs/examples/en/utils/SceneUtils.html

@@ -85,6 +85,30 @@
 		and to reduce overdraw in opaque materials (front to back).
 		</p>
 
+		<h3>[method:Generator traverseGenerator]( [param:Object3D object] )</h3>
+		<p>
+		object -- The 3D object to traverse.
+		</p>
+		<p>
+		A generator based version of [page:Object3D.traverse]().
+		</p>
+
+		<h3>[method:Generator traverseVisibleGenerator]( [param:Object3D object] )</h3>
+		<p>
+		object -- The 3D object to traverse.
+		</p>
+		<p>
+		A generator based version of [page:Object3D.traverseVisible]().
+		</p>
+
+		<h3>[method:Generator traverseAncestorsGenerator]( [param:Object3D object] )</h3>
+		<p>
+		object -- The 3D object to traverse.
+		</p>
+		<p>
+		A generator based version of [page:Object3D.traverseAncestors]().
+		</p>
+
 		<h2>Source</h2>
 
 		<p>

+ 24 - 0
docs/examples/zh/utils/SceneUtils.html

@@ -69,6 +69,30 @@
 		and to reduce overdraw in opaque materials (front to back).
 		</p>
 
+		<h3>[method:Generator traverseGenerator]( [param:Object3D object] )</h3>
+		<p>
+		object -- The 3D object to traverse.
+		</p>
+		<p>
+		A generator based version of [page:Object3D.traverse]().
+		</p>
+
+		<h3>[method:Generator traverseVisibleGenerator]( [param:Object3D object] )</h3>
+		<p>
+		object -- The 3D object to traverse.
+		</p>
+		<p>
+		A generator based version of [page:Object3D.traverseVisible]().
+		</p>
+
+		<h3>[method:Generator traverseAncestorsGenerator]( [param:Object3D object] )</h3>
+		<p>
+		object -- The 3D object to traverse.
+		</p>
+		<p>
+		A generator based version of [page:Object3D.traverseAncestors]().
+		</p>
+
 		<h2>源代码</h2>
 
 		<p>

+ 6 - 1
docs/list.json

@@ -378,7 +378,8 @@
 			},
 
 			"Objects": {
-				"Lensflare": "examples/en/objects/Lensflare"
+				"Lensflare": "examples/en/objects/Lensflare",
+				"Sky": "examples/en/objects/Sky"
 			},
 
 			"Post-Processing": {
@@ -404,6 +405,10 @@
 				"Timer": "examples/en/misc/Timer"
 			},
 
+			"Modifiers": {
+				"EdgeSplit": "examples/en/modifiers/EdgeSplitModifier"
+			},
+
 			"ConvexHull": {
 				"Face": "examples/en/math/convexhull/Face",
 				"HalfEdge": "examples/en/math/convexhull/HalfEdge",

+ 7 - 9
docs/manual/en/introduction/Creating-a-scene.html

@@ -71,17 +71,16 @@
 
 		<h2>Rendering the scene</h2>
 
-		<p>If you copied the code from above into the HTML file we created earlier, you wouldn't be able to see anything. This is because we're not actually rendering anything yet. For that, we need what's called a `render or animate loop`.</p>
+		<p>If you copied the code from above into the HTML file we created earlier, you wouldn't be able to see anything. This is because we're not actually rendering anything yet. For that, we need what's called a render or animation loop.</p>
 
 		<code>
 		function animate() {
-			requestAnimationFrame( animate );
 			renderer.render( scene, camera );
 		}
-		animate();
+		renderer.setAnimationLoop( animate );
 		</code>
 
-		<p>This will create a loop that causes the renderer to draw the scene every time the screen is refreshed (on a typical screen this means 60 times per second). If you're new to writing games in the browser, you might say <em>"why don't we just create a setInterval ?"</em> The thing is - we could, but `requestAnimationFrame` has a number of advantages. Perhaps the most important one is that it pauses when the user navigates to another browser tab, hence not wasting their precious processing power and battery life.</p>
+		<p>This will create a loop that causes the renderer to draw the scene every time the screen is refreshed (on a typical screen this means 60 times per second). If you're new to writing games in the browser, you might say <em>"why don't we just create a setInterval ?"</em> The thing is - we could, but `requestAnimationFrame` which is internally used in `WebGLRenderer` has a number of advantages. Perhaps the most important one is that it pauses when the user navigates to another browser tab, hence not wasting their precious processing power and battery life.</p>
 
 		<h2>Animating the cube</h2>
 
@@ -94,12 +93,12 @@
 		cube.rotation.y += 0.01;
 		</code>
 
-		<p>This will be run every frame (normally 60 times per second), and give the cube a nice rotation animation. Basically, anything you want to move or change while the app is running has to go through the animate loop. You can of course call other functions from there, so that you don't end up with an `animate` function that's hundreds of lines.</p>
+		<p>This will be run every frame (normally 60 times per second), and give the cube a nice rotation animation. Basically, anything you want to move or change while the app is running has to go through the animation loop. You can of course call other functions from there, so that you don't end up with an `animate` function that's hundreds of lines.</p>
 
 		<h2>The result</h2>
 		<p>Congratulations! You have now completed your first three.js application. It's simple, but you have to start somewhere.</p>
 
-		<p>The full code is available below and as an editable [link:https://jsfiddle.net/0c1oqf38/ live example]. Play around with it to get a better understanding of how it works.</p>
+		<p>The full code is available below and as an editable [link:https://jsfiddle.net/tswh48fL/ live example]. Play around with it to get a better understanding of how it works.</p>
 
 		<p><i>index.html —</i></p>
 
@@ -129,6 +128,7 @@
 
 		const renderer = new THREE.WebGLRenderer();
 		renderer.setSize( window.innerWidth, window.innerHeight );
+		renderer.setAnimationLoop( animate );
 		document.body.appendChild( renderer.domElement );
 
 		const geometry = new THREE.BoxGeometry( 1, 1, 1 );
@@ -139,15 +139,13 @@
 		camera.position.z = 5;
 
 		function animate() {
-			requestAnimationFrame( animate );
 
 			cube.rotation.x += 0.01;
 			cube.rotation.y += 0.01;
 
 			renderer.render( scene, camera );
-		}
 
-		animate();
+		}
 		</code>
 	</body>
 </html>

+ 5 - 5
docs/manual/zh/introduction/Installation.html

@@ -70,7 +70,7 @@ import * as THREE from 'three';
 			</li>
 			<li>
 				<p>
-					在项目文件夹中通过 [link:https://www.joshwcomeau.com/javascript/terminal-for-js-devs/ 终端] 安装 three.js 和构建工具 [link:https://vitejs.dev/ Vite]。Vite 将在开发过程中使用,但不会被打包成为最终网页的一部分。当然,除了 Vite 你也可以使用其他支持导入 [link:https://eloquentjavascript.net/10_modules.html#h_zWTXAU93DC ES Modules] 的现代构建工具。
+					在项目文件夹中通过 [link:https://www.joshwcomeau.com/javascript/terminal-for-js-devs/ 终端] 安装 three.js 和构建工具 [link:https://cn.vitejs.dev/ Vite]。Vite 将在开发过程中使用,但不会被打包成为最终网页的一部分。当然,除了 Vite 你也可以使用其他支持导入 [link:https://eloquentjavascript.net/10_modules.html#h_zWTXAU93DC ES Modules] 的现代构建工具。
 				</p>
 				<code>
 # three.js
@@ -100,7 +100,7 @@ npm install --save-dev vite
 					<details>
 						<summary><i>npx</i> 是什么?</summary>
 						<p>
-							npx 与 Node.js 一同安装,可运行 Vite 等命令行程序,这样你就不必自己在 <i>node_modules/</i> 中搜索正确的文件。如果你愿意,可以将 [link:https://vitejs.dev/guide/#command-line-interface Vite 的常用命令] 放入 [link:https://docs.npmjs.com/cli/v9/using-npm/scripts package.json:scripts] 列表,然后使用 <i>npm run dev</i> 代替。
+							npx 与 Node.js 一同安装,可运行 Vite 等命令行程序,这样你就不必自己在 <i>node_modules/</i> 中搜索正确的文件。如果你愿意,可以将 [link:https://cn.vitejs.dev/guide/#command-line-interface Vite 的常用命令] 放入 [link:https://docs.npmjs.com/cli/v9/using-npm/scripts package.json:scripts] 列表,然后使用 <i>npm run dev</i> 代替。
 						</p>
 					</details>
 				</aside>
@@ -123,10 +123,10 @@ npm install --save-dev vite
 				[link:https://threejs-journey.com/lessons/local-server three.js journey: Local Server]
 			</li>
 			<li>
-				[link:https://vitejs.dev/guide/cli.html Vite: Command Line Interface]
+				[link:https://cn.vitejs.dev/guide/cli.html Vite: Command Line Interface]
 			</li>
 			<li>
-				[link:https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Understanding_client-side_tools/Package_management MDN: Package management basics]
+				[link:https://developer.mozilla.org/zh-CN/docs/Learn/Tools_and_testing/Understanding_client-side_tools/Package_management MDN: Package management basics]
 			</li>
 		</ul>
 
@@ -145,7 +145,7 @@ npm install --save-dev vite
 		<ol>
 			<li>
 				<p>
-					我们在 <i>main.js</i> 中从 "three"(一个 npm 软件包)导入了代码,但网络浏览器并不知道这意味着什么。在 <i>index.html</i> 中,我们需要添加一个[link:https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap 导入映射](import map)来定义从哪里获取软件包。将下面的代码放在 <i>&lt;head>&lt/head></i> 标签内、样式(styles)之后。
+					我们在 <i>main.js</i> 中从 "three"(一个 npm 软件包)导入了代码,但网络浏览器并不知道这意味着什么。在 <i>index.html</i> 中,我们需要添加一个[link:https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/script/type/importmap 导入映射](import map)来定义从哪里获取软件包。将下面的代码放在 <i>&lt;head>&lt/head></i> 标签内、样式(styles)之后。
 				</p>
 				<code>
 &lt;script type="importmap">

+ 9 - 0
editor/.eslintrc.json

@@ -0,0 +1,9 @@
+{
+    "extends": [
+        "../.eslintrc.json"
+    ],
+    "parserOptions": {
+        "sourceType": "module",
+        "ecmaVersion": 2020
+    }
+}

+ 92 - 9
editor/css/main.css

@@ -2,6 +2,10 @@
 	color-scheme: light dark;
 }
 
+[hidden] {
+	display: none !important;
+}
+
 body {
 	font-family: Helvetica, Arial, sans-serif;
 	font-size: 14px;
@@ -72,17 +76,34 @@ textarea, input { outline: none; } /* osx */
 
 .TabbedPanel .Tabs {
 	position: relative;
+	z-index: 1; /** Above .Panels **/
 	display: block;
 	width: 100%;
+	white-space: pre;
+	overflow: hidden;
+	overflow-x: auto;
 }
 
+	.TabbedPanel .Tabs::-webkit-scrollbar {
+		height: 5px;
+		background: #eee;
+	}
+	.TabbedPanel .Tabs::-webkit-scrollbar-thumb {
+		background: #08f3;
+	}
+	.TabbedPanel .Tabs:hover::-webkit-scrollbar-thumb {
+		background: #08f;
+		cursor: ew-resize;
+	}
+
 	.TabbedPanel .Tabs .Tab {
 		padding: 10px 9px;
 		text-transform: uppercase;
 	}
 
 	.TabbedPanel .Panels {
-		position: relative;
+		position: absolute;
+		top: 40px;
 		display: block;
 		width: 100%;
 	}
@@ -292,14 +313,26 @@ select {
 
 #resizer {
 	position: absolute;
+	z-index: 2; /* Above #sidebar */
 	top: 32px;
-	right: 345px;
+	right: 350px;
 	width: 5px;
 	bottom: 0px;
-	/* background-color: rgba(255,0,0,0.5); */
+	transform: translatex(2.5px);
 	cursor: col-resize;
 }
 
+	#resizer:hover {
+		background-color: #08f8;
+		transition-property: background-color;
+		transition-delay: 0.1s;
+		transition-duration: 0.2s;
+	}
+
+	#resizer:active {
+		background-color: #08f;
+	}
+
 #viewport {
 	position: absolute;
 	top: 32px;
@@ -308,7 +341,7 @@ select {
 	bottom: 0;
 }
 
-	#viewport #info {
+	#viewport .Text {
 		text-shadow: 1px 1px 0 rgba(0,0,0,0.25);
 		pointer-events: none;
 	}
@@ -362,18 +395,32 @@ select {
 			line-height: 16px;
 		}
 
+		#menubar .menu .key {
+			position: absolute;
+			right: 10px;
+			color: #ccc;
+			border: 1px solid #ccc;
+			border-radius: 4px;
+			font-size: 9px;
+			padding: 2px 4px;
+			right: 10px;
+			pointer-events: none;
+		}
+
 		#menubar .menu .options {
 			position: fixed;
+			z-index: 1; /* higher than resizer */
 			display: none;
 			padding: 5px 0;
 			background: #eee;
-			width: 150px;
-			max-height: calc(100% - 80px);
+			min-width: 150px;
+			max-height: calc(100vh - 80px);
 			overflow: auto;
 		}
 
 		#menubar .menu:hover .options {
 			display: block;
+			box-shadow: 0 10px 10px -5px #00000033;
 		}
 
 			#menubar .menu .options hr {
@@ -392,18 +439,41 @@ select {
 					background-color: #08f;
 				}
 
-				#menubar .menu .options .option:active {
+				#menubar .menu .options .option:not(.submenu-title):active {
 					color: #666;
 					background: transparent;
 				}
 
+				#menubar .menu .options .option.toggle::before {
+
+					content: ' ';
+					display: inline-block;
+					width: 16px;
+
+				}
+
+				#menubar .menu .options .option.toggle-on::before {
+
+					content: '✔';
+					font-size: 12px;
+
+				}
+
+				#menubar .submenu-title::after {
+					content: '⏵';
+					float: right;
+				}
+
 		#menubar .menu .options .inactive {
 			color: #bbb;
 			background-color: transparent;
 			padding: 5px 10px;
 			margin: 0 !important;
+			cursor: not-allowed;
 		}
 
+		
+
 #sidebar {
 	position: absolute;
 	right: 0;
@@ -412,6 +482,7 @@ select {
 	width: 350px;
 	background: #eee;
 	overflow: auto;
+	overflow-x: hidden;
 }
 
 	#sidebar .Panel {
@@ -532,7 +603,7 @@ select {
 	}
 
 	#menubar .menu .options {
-		max-height: calc(100% - 372px);
+		max-height: calc(100% - 80px);
 	}
 
 	#menubar .menu.right {
@@ -610,6 +681,11 @@ select {
 		background: #111;
 	}
 
+			#menubar .menu .key {
+				color: #444;
+				border-color: #444;
+			}
+
 			#menubar .menu .options {
 				background: #111;
 			}
@@ -661,10 +737,13 @@ select {
 		}
 
 	.Outliner {
-		color: #888;
 		background: #222;
 	}
 
+		.Outliner .option {
+			color: #999;
+		}
+
 		.Outliner .option:hover {
 			background-color: rgba(21,60,94,0.5);
 		}
@@ -678,6 +757,10 @@ select {
 		border-top: 1px solid #222;
 	}
 
+		.TabbedPanel .Tabs::-webkit-scrollbar {
+			background: #111;
+		}
+
 		.TabbedPanel .Tab {
 			color: #555;
 			border-right: 1px solid #222;

+ 6 - 12
editor/index.html

@@ -32,7 +32,7 @@
 		<script src="js/libs/esprima.js"></script>
 		<script src="js/libs/jsonlint.js"></script>
 
-		<script src="js/libs/ffmpeg.min.js" defer></script>
+		<script src="https://cdn.jsdelivr.net/npm/@ffmpeg/[email protected]/dist/ffmpeg.min.js"></script>
 
 		<link rel="stylesheet" href="js/libs/codemirror/addon/dialog.css">
 		<link rel="stylesheet" href="js/libs/codemirror/addon/show-hint.css">
@@ -61,8 +61,8 @@
 					"three/addons/": "../examples/jsm/",
 
 					"three/examples/": "../examples/",
-					"three-gpu-pathtracer": "https://cdn.jsdelivr.net/npm/[email protected].19/build/index.module.js",
-					"three-mesh-bvh": "https://cdn.jsdelivr.net/npm/[email protected].3/build/index.module.js"
+					"three-gpu-pathtracer": "https://cdn.jsdelivr.net/npm/[email protected].22/build/index.module.js",
+					"three-mesh-bvh": "https://cdn.jsdelivr.net/npm/[email protected].4/build/index.module.js"
 				}
 			}
 		</script>
@@ -83,12 +83,6 @@
 			window.URL = window.URL || window.webkitURL;
 			window.BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder;
 
-			Number.prototype.format = function () {
-
-				return this.toString().replace( /(\d)(?=(\d{3})+(?!\d))/g, '$1,' );
-
-			};
-
 			//
 
 			const editor = new Editor();
@@ -121,13 +115,13 @@
 
 			editor.storage.init( function () {
 
-				editor.storage.get( function ( state ) {
+				editor.storage.get( async function ( state ) {
 
 					if ( isLoadingFromHash ) return;
 
 					if ( state !== undefined ) {
 
-						editor.fromJSON( state );
+						await editor.fromJSON( state );
 
 					}
 
@@ -235,7 +229,7 @@
 
 				const file = hash.slice( 6 );
 
-				if ( confirm( 'Any unsaved data will be lost. Are you sure?' ) ) {
+				if ( confirm( editor.strings.getKey( 'prompt/file/open' ) ) ) {
 
 					const loader = new THREE.FileLoader();
 					loader.crossOrigin = '';

+ 5 - 1
editor/js/Config.js

@@ -2,8 +2,12 @@ function Config() {
 
 	const name = 'threejs-editor';
 
+	const userLanguage = navigator.language.split( '-' )[ 0 ];
+
+	const suggestedLanguage = [ 'fr', 'ja', 'zh' ].includes( userLanguage ) ? userLanguage : 'en';
+
 	const storage = {
-		'language': 'en',
+		'language': suggestedLanguage,
 
 		'autosave': true,
 

+ 19 - 2
editor/js/Editor.js

@@ -82,7 +82,6 @@ function Editor() {
 
 		windowResize: new Signal(),
 
-		showGridChanged: new Signal(),
 		showHelpersChanged: new Signal(),
 		refreshSidebarObject3D: new Signal(),
 		refreshSidebarEnvironment: new Signal(),
@@ -93,6 +92,8 @@ function Editor() {
 
 		intersectionsDetected: new Signal(),
 
+		pathTracerUpdated: new Signal(),
+
 	};
 
 	this.config = new Config();
@@ -659,7 +660,16 @@ Editor.prototype = {
 		var loader = new THREE.ObjectLoader();
 		var camera = await loader.parseAsync( json.camera );
 
+		const existingUuid = this.camera.uuid;
+		const incomingUuid = camera.uuid;
+
+		// copy all properties, including uuid
 		this.camera.copy( camera );
+		this.camera.uuid = incomingUuid;
+
+		delete this.cameras[ existingUuid ]; // remove old entry [existingUuid, this.camera]
+		this.cameras[ incomingUuid ] = this.camera; // add new entry [incomingUuid, this.camera]
+
 		this.signals.cameraResetted.dispatch();
 
 		this.history.fromJSON( json.history );
@@ -754,7 +764,8 @@ Editor.prototype = {
 
 		save: save,
 		saveArrayBuffer: saveArrayBuffer,
-		saveString: saveString
+		saveString: saveString,
+		formatNumber: formatNumber
 
 	}
 
@@ -788,4 +799,10 @@ function saveString( text, filename ) {
 
 }
 
+function formatNumber( number ) {
+
+	return new Intl.NumberFormat( 'en-us', { useGrouping: true } ).format( number );
+
+}
+
 export { Editor };

+ 3 - 3
editor/js/History.js

@@ -88,7 +88,7 @@ class History {
 
 		if ( this.historyDisabled ) {
 
-			alert( 'Undo/Redo disabled while scene is playing.' );
+			alert( this.editor.strings.getKey( 'prompt/history/forbid' ) );
 			return;
 
 		}
@@ -123,7 +123,7 @@ class History {
 
 		if ( this.historyDisabled ) {
 
-			alert( 'Undo/Redo disabled while scene is playing.' );
+			alert( this.editor.strings.getKey( 'prompt/history/forbid' ) );
 			return;
 
 		}
@@ -241,7 +241,7 @@ class History {
 
 		if ( this.historyDisabled ) {
 
-			alert( 'Undo/Redo disabled while scene is playing.' );
+			alert( this.editor.strings.getKey( 'prompt/history/forbid' ) );
 			return;
 
 		}

+ 28 - 34
editor/js/Loader.js

@@ -3,7 +3,6 @@ import * as THREE from 'three';
 import { TGALoader } from 'three/addons/loaders/TGALoader.js';
 
 import { AddObjectCommand } from './commands/AddObjectCommand.js';
-import { SetSceneCommand } from './commands/SetSceneCommand.js';
 
 import { LoaderUtils } from './LoaderUtils.js';
 
@@ -70,7 +69,7 @@ function Loader( editor ) {
 		const reader = new FileReader();
 		reader.addEventListener( 'progress', function ( event ) {
 
-			const size = '(' + Math.floor( event.total / 1000 ).format() + ' KB)';
+			const size = '(' + editor.utils.formatNumber( Math.floor( event.total / 1000 ) ) + ' KB)';
 			const progress = Math.floor( ( event.loaded / event.total ) * 100 ) + '%';
 
 			console.log( 'Loading', filename, size, progress );
@@ -99,7 +98,7 @@ function Loader( editor ) {
 
 					}, function ( error ) {
 
-						console.error( error )
+						console.error( error );
 
 					} );
 
@@ -586,6 +585,7 @@ function Loader( editor ) {
 					//
 
 					const group = new THREE.Group();
+					group.name = filename;
 					group.scale.multiplyScalar( 0.1 );
 					group.scale.y *= - 1;
 
@@ -715,7 +715,7 @@ function Loader( editor ) {
 
 					const result = new VRMLLoader().parse( contents );
 
-					editor.execute( new SetSceneCommand( editor, result ) );
+					editor.execute( new AddObjectCommand( editor, result ) );
 
 				}, false );
 				reader.readAsText( file );
@@ -828,15 +828,7 @@ function Loader( editor ) {
 
 				loader.parse( data, function ( result ) {
 
-					if ( result.isScene ) {
-
-						editor.execute( new SetSceneCommand( editor, result ) );
-
-					} else {
-
-						editor.execute( new AddObjectCommand( editor, result ) );
-
-					}
+					editor.execute( new AddObjectCommand( editor, result ) );
 
 				} );
 
@@ -858,6 +850,24 @@ function Loader( editor ) {
 
 		const zip = unzipSync( new Uint8Array( contents ) );
 
+		const manager = new THREE.LoadingManager();
+		manager.setURLModifier( function ( url ) {
+
+			const file = zip[ url ];
+
+			if ( file ) {
+
+				console.log( 'Loading', url );
+
+				const blob = new Blob( [ file.buffer ], { type: 'application/octet-stream' } );
+				return URL.createObjectURL( blob );
+
+			}
+
+			return url;
+
+		} );
+
 		// Poly
 
 		if ( zip[ 'model.obj' ] && zip[ 'materials.mtl' ] ) {
@@ -865,9 +875,11 @@ function Loader( editor ) {
 			const { MTLLoader } = await import( 'three/addons/loaders/MTLLoader.js' );
 			const { OBJLoader } = await import( 'three/addons/loaders/OBJLoader.js' );
 
-			const materials = new MTLLoader().parse( strFromU8( zip[ 'materials.mtl' ] ) );
+			const materials = new MTLLoader( manager ).parse( strFromU8( zip[ 'materials.mtl' ] ) );
 			const object = new OBJLoader().setMaterials( materials ).parse( strFromU8( zip[ 'model.obj' ] ) );
+
 			editor.execute( new AddObjectCommand( editor, object ) );
+			return;
 
 		}
 
@@ -877,24 +889,6 @@ function Loader( editor ) {
 
 			const file = zip[ path ];
 
-			const manager = new THREE.LoadingManager();
-			manager.setURLModifier( function ( url ) {
-
-				const file = zip[ url ];
-
-				if ( file ) {
-
-					console.log( 'Loading', url );
-
-					const blob = new Blob( [ file.buffer ], { type: 'application/octet-stream' } );
-					return URL.createObjectURL( blob );
-
-				}
-
-				return url;
-
-			} );
-
 			const extension = path.split( '.' ).pop().toLowerCase();
 
 			switch ( extension ) {
@@ -941,7 +935,7 @@ function Loader( editor ) {
 				{
 
 					const loader = await createGLTFLoader( manager );
-					
+
 					loader.parse( strFromU8( file ), '', function ( result ) {
 
 						const scene = result.scene;
@@ -990,4 +984,4 @@ function Loader( editor ) {
 
 }
 
-export { Loader };
+export { Loader };

+ 134 - 75
editor/js/Menubar.Add.js

@@ -35,15 +35,34 @@ function MenubarAdd( editor ) {
 	} );
 	options.add( option );
 
-	//
+	// Mesh
 
-	options.add( new UIHorizontalRule() );
+	const meshSubmenuTitle = new UIRow().setTextContent( strings.getKey( 'menubar/add/mesh' ) ).addClass( 'option' ).addClass( 'submenu-title' );
+	meshSubmenuTitle.onMouseOver( function () {
 
-	// Box
+		const { top, right } = meshSubmenuTitle.dom.getBoundingClientRect();
+		const { paddingTop } = getComputedStyle( this.dom );
+		meshSubmenu.setLeft( right + 'px' );
+		meshSubmenu.setTop( top - parseFloat( paddingTop ) + 'px' );
+		meshSubmenu.setStyle( 'max-height', [ `calc( 100vh - ${top}px )` ] );
+		meshSubmenu.setDisplay( 'block' );
+
+	} );
+	meshSubmenuTitle.onMouseOut( function () {
+
+		meshSubmenu.setDisplay( 'none' );
+
+	} );
+	options.add( meshSubmenuTitle );
+
+	const meshSubmenu = new UIPanel().setPosition( 'fixed' ).addClass( 'options' ).setDisplay( 'none' );
+	meshSubmenuTitle.add( meshSubmenu );
+
+	// Mesh / Box
 
 	option = new UIRow();
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/box' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/mesh/box' ) );
 	option.onClick( function () {
 
 		const geometry = new THREE.BoxGeometry( 1, 1, 1, 1, 1, 1 );
@@ -53,13 +72,13 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 
 	} );
-	options.add( option );
+	meshSubmenu.add( option );
 
-	// Capsule
+	// Mesh / Capsule
 
 	option = new UIRow();
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/capsule' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/mesh/capsule' ) );
 	option.onClick( function () {
 
 		const geometry = new THREE.CapsuleGeometry( 1, 1, 4, 8 );
@@ -70,13 +89,13 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 
 	} );
-	options.add( option );
+	meshSubmenu.add( option );
 
-	// Circle
+	// Mesh / Circle
 
 	option = new UIRow();
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/circle' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/mesh/circle' ) );
 	option.onClick( function () {
 
 		const geometry = new THREE.CircleGeometry( 1, 32, 0, Math.PI * 2 );
@@ -86,13 +105,13 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 
 	} );
-	options.add( option );
+	meshSubmenu.add( option );
 
-	// Cylinder
+	// Mesh / Cylinder
 
 	option = new UIRow();
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/cylinder' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/mesh/cylinder' ) );
 	option.onClick( function () {
 
 		const geometry = new THREE.CylinderGeometry( 1, 1, 1, 32, 1, false, 0, Math.PI * 2 );
@@ -102,13 +121,13 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 
 	} );
-	options.add( option );
+	meshSubmenu.add( option );
 
-	// Dodecahedron
+	// Mesh / Dodecahedron
 
 	option = new UIRow();
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/dodecahedron' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/mesh/dodecahedron' ) );
 	option.onClick( function () {
 
 		const geometry = new THREE.DodecahedronGeometry( 1, 0 );
@@ -118,13 +137,13 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 
 	} );
-	options.add( option );
+	meshSubmenu.add( option );
 
-	// Icosahedron
+	// Mesh / Icosahedron
 
 	option = new UIRow();
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/icosahedron' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/mesh/icosahedron' ) );
 	option.onClick( function () {
 
 		const geometry = new THREE.IcosahedronGeometry( 1, 0 );
@@ -134,13 +153,13 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 
 	} );
-	options.add( option );
+	meshSubmenu.add( option );
 
-	// Lathe
+	// Mesh / Lathe
 
 	option = new UIRow();
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/lathe' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/mesh/lathe' ) );
 	option.onClick( function () {
 
 		const geometry = new THREE.LatheGeometry();
@@ -150,13 +169,13 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 
 	} );
-	options.add( option );
+	meshSubmenu.add( option );
 
-	// Octahedron
+	// Mesh / Octahedron
 
 	option = new UIRow();
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/octahedron' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/mesh/octahedron' ) );
 	option.onClick( function () {
 
 		const geometry = new THREE.OctahedronGeometry( 1, 0 );
@@ -166,13 +185,13 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 
 	} );
-	options.add( option );
+	meshSubmenu.add( option );
 
-	// Plane
+	// Mesh / Plane
 
 	option = new UIRow();
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/plane' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/mesh/plane' ) );
 	option.onClick( function () {
 
 		const geometry = new THREE.PlaneGeometry( 1, 1, 1, 1 );
@@ -183,13 +202,13 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 
 	} );
-	options.add( option );
+	meshSubmenu.add( option );
 
-	// Ring
+	// Mesh / Ring
 
 	option = new UIRow();
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/ring' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/mesh/ring' ) );
 	option.onClick( function () {
 
 		const geometry = new THREE.RingGeometry( 0.5, 1, 32, 1, 0, Math.PI * 2 );
@@ -199,13 +218,13 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 
 	} );
-	options.add( option );
+	meshSubmenu.add( option );
 
-	// Sphere
+	// Mesh / Sphere
 
 	option = new UIRow();
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/sphere' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/mesh/sphere' ) );
 	option.onClick( function () {
 
 		const geometry = new THREE.SphereGeometry( 1, 32, 16, 0, Math.PI * 2, 0, Math.PI );
@@ -215,13 +234,13 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 
 	} );
-	options.add( option );
+	meshSubmenu.add( option );
 
-	// Sprite
+	// Mesh / Sprite
 
 	option = new UIRow();
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/sprite' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/mesh/sprite' ) );
 	option.onClick( function () {
 
 		const sprite = new THREE.Sprite( new THREE.SpriteMaterial() );
@@ -230,13 +249,13 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, sprite ) );
 
 	} );
-	options.add( option );
+	meshSubmenu.add( option );
 
-	// Tetrahedron
+	// Mesh / Tetrahedron
 
 	option = new UIRow();
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/tetrahedron' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/mesh/tetrahedron' ) );
 	option.onClick( function () {
 
 		const geometry = new THREE.TetrahedronGeometry( 1, 0 );
@@ -246,13 +265,13 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 
 	} );
-	options.add( option );
+	meshSubmenu.add( option );
 
-	// Torus
+	// Mesh / Torus
 
 	option = new UIRow();
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/torus' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/mesh/torus' ) );
 	option.onClick( function () {
 
 		const geometry = new THREE.TorusGeometry( 1, 0.4, 12, 48, Math.PI * 2 );
@@ -262,13 +281,13 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 
 	} );
-	options.add( option );
+	meshSubmenu.add( option );
 
-	// TorusKnot
+	// Mesh / TorusKnot
 
 	option = new UIRow();
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/torusknot' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/mesh/torusknot' ) );
 	option.onClick( function () {
 
 		const geometry = new THREE.TorusKnotGeometry( 1, 0.4, 64, 8, 2, 3 );
@@ -278,13 +297,13 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 
 	} );
-	options.add( option );
+	meshSubmenu.add( option );
 
-	// Tube
+	// Mesh / Tube
 
 	option = new UIRow();
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/tube' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/mesh/tube' ) );
 	option.onClick( function () {
 
 		const path = new THREE.CatmullRomCurve3( [
@@ -301,17 +320,37 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 
 	} );
-	options.add( option );
+	meshSubmenu.add( option );
 
-	//
+	// Light
 
-	options.add( new UIHorizontalRule() );
+	const lightSubmenuTitle = new UIRow().setTextContent( strings.getKey( 'menubar/add/light' ) ).addClass( 'option' ).addClass( 'submenu-title' );
+	lightSubmenuTitle.onMouseOver( function () {
 
-	// AmbientLight
+		const { top, right } = lightSubmenuTitle.dom.getBoundingClientRect();
+		const { paddingTop } = getComputedStyle( this.dom );
+
+		lightSubmenu.setLeft( right + 'px' );
+		lightSubmenu.setTop( top - parseFloat( paddingTop ) + 'px' );
+		lightSubmenu.setStyle( 'max-height', [ `calc( 100vh - ${top}px )` ] );
+		lightSubmenu.setDisplay( 'block' );
+
+	} );
+	lightSubmenuTitle.onMouseOut( function () {
+
+		lightSubmenu.setDisplay( 'none' );
+
+	} );
+	options.add( lightSubmenuTitle );
+
+	const lightSubmenu = new UIPanel().setPosition( 'fixed' ).addClass( 'options' ).setDisplay( 'none' );
+	lightSubmenuTitle.add( lightSubmenu );
+
+	// Light / Ambient
 
 	option = new UIRow();
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/ambientlight' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/light/ambient' ) );
 	option.onClick( function () {
 
 		const color = 0x222222;
@@ -322,13 +361,13 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, light ) );
 
 	} );
-	options.add( option );
+	lightSubmenu.add( option );
 
-	// DirectionalLight
+	// Light / Directional
 
 	option = new UIRow();
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/directionallight' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/light/directional' ) );
 	option.onClick( function () {
 
 		const color = 0xffffff;
@@ -343,13 +382,13 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, light ) );
 
 	} );
-	options.add( option );
+	lightSubmenu.add( option );
 
-	// HemisphereLight
+	// Light / Hemisphere
 
 	option = new UIRow();
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/hemispherelight' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/light/hemisphere' ) );
 	option.onClick( function () {
 
 		const skyColor = 0x00aaff;
@@ -364,13 +403,13 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, light ) );
 
 	} );
-	options.add( option );
+	lightSubmenu.add( option );
 
-	// PointLight
+	// Light / Point
 
 	option = new UIRow();
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/pointlight' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/light/point' ) );
 	option.onClick( function () {
 
 		const color = 0xffffff;
@@ -383,13 +422,13 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, light ) );
 
 	} );
-	options.add( option );
+	lightSubmenu.add( option );
 
-	// SpotLight
+	// Light / Spot
 
 	option = new UIRow();
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/spotlight' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/light/spot' ) );
 	option.onClick( function () {
 
 		const color = 0xffffff;
@@ -407,17 +446,37 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, light ) );
 
 	} );
-	options.add( option );
+	lightSubmenu.add( option );
+
+	// Camera
+
+	const cameraSubmenuTitle = new UIRow().setTextContent( strings.getKey( 'menubar/add/camera' ) ).addClass( 'option' ).addClass( 'submenu-title' );
+	cameraSubmenuTitle.onMouseOver( function () {
 
-	//
+		const { top, right } = cameraSubmenuTitle.dom.getBoundingClientRect();
+		const { paddingTop } = getComputedStyle( this.dom );
 
-	options.add( new UIHorizontalRule() );
+		cameraSubmenu.setLeft( right + 'px' );
+		cameraSubmenu.setTop( top - parseFloat( paddingTop ) + 'px' );
+		cameraSubmenu.setStyle( 'max-height', [ `calc( 100vh - ${top}px )` ] );
+		cameraSubmenu.setDisplay( 'block' );
 
-	// OrthographicCamera
+	} );
+	cameraSubmenuTitle.onMouseOut( function () {
+
+		cameraSubmenu.setDisplay( 'none' );
+
+	} );
+	options.add( cameraSubmenuTitle );
+
+	const cameraSubmenu = new UIPanel().setPosition( 'fixed' ).addClass( 'options' ).setDisplay( 'none' );
+	cameraSubmenuTitle.add( cameraSubmenu );
+
+	// Camera / Orthographic
 
 	option = new UIRow();
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/orthographiccamera' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/camera/orthographic' ) );
 	option.onClick( function () {
 
 		const aspect = editor.camera.aspect;
@@ -427,13 +486,13 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, camera ) );
 
 	} );
-	options.add( option );
+	cameraSubmenu.add( option );
 
-	// PerspectiveCamera
+	// Camera / Perspective
 
 	option = new UIRow();
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/perspectivecamera' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/camera/perspective' ) );
 	option.onClick( function () {
 
 		const camera = new THREE.PerspectiveCamera();
@@ -442,7 +501,7 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, camera ) );
 
 	} );
-	options.add( option );
+	cameraSubmenu.add( option );
 
 	return container;
 

+ 9 - 3
editor/js/Menubar.Edit.js

@@ -1,6 +1,6 @@
 import { Box3, Vector3 } from 'three';
 
-import { UIPanel, UIRow, UIHorizontalRule } from './libs/ui.js';
+import { UIPanel, UIRow, UIHorizontalRule, UIText } from './libs/ui.js';
 
 import { AddObjectCommand } from './commands/AddObjectCommand.js';
 import { RemoveObjectCommand } from './commands/RemoveObjectCommand.js';
@@ -28,6 +28,7 @@ function MenubarEdit( editor ) {
 	const undo = new UIRow();
 	undo.setClass( 'option' );
 	undo.setTextContent( strings.getKey( 'menubar/edit/undo' ) );
+	undo.add( new UIText( 'CTRL+Z' ).setClass( 'key' ) );
 	undo.onClick( function () {
 
 		editor.undo();
@@ -40,6 +41,7 @@ function MenubarEdit( editor ) {
 	const redo = new UIRow();
 	redo.setClass( 'option' );
 	redo.setTextContent( strings.getKey( 'menubar/edit/redo' ) );
+	redo.add( new UIText( 'CTRL+SHIFT+Z' ).setClass( 'key' ) );
 	redo.onClick( function () {
 
 		editor.redo();
@@ -47,7 +49,7 @@ function MenubarEdit( editor ) {
 	} );
 	options.add( redo );
 
-	editor.signals.historyChanged.add( function () {
+	function onHistoryChanged() {
 
 		const history = editor.history;
 
@@ -66,7 +68,10 @@ function MenubarEdit( editor ) {
 
 		}
 
-	} );
+	}
+
+	editor.signals.historyChanged.add( onHistoryChanged );
+	onHistoryChanged();
 
 	// ---
 
@@ -119,6 +124,7 @@ function MenubarEdit( editor ) {
 	option = new UIRow();
 	option.setClass( 'option' );
 	option.setTextContent( strings.getKey( 'menubar/edit/delete' ) );
+	option.add( new UIText( 'DEL' ).setClass( 'key' ) );
 	option.onClick( function () {
 
 		const object = editor.selected;

+ 0 - 66
editor/js/Menubar.Examples.js

@@ -1,66 +0,0 @@
-import * as THREE from 'three';
-
-import { UIPanel, UIRow } from './libs/ui.js';
-
-function MenubarExamples( editor ) {
-
-	const strings = editor.strings;
-
-	const container = new UIPanel();
-	container.setClass( 'menu' );
-
-	const title = new UIPanel();
-	title.setClass( 'title' );
-	title.setTextContent( strings.getKey( 'menubar/examples' ) );
-	container.add( title );
-
-	const options = new UIPanel();
-	options.setClass( 'options' );
-	container.add( options );
-
-	// Examples
-
-	const items = [
-		{ title: 'menubar/examples/Arkanoid', file: 'arkanoid.app.json' },
-		{ title: 'menubar/examples/Camera', file: 'camera.app.json' },
-		{ title: 'menubar/examples/Particles', file: 'particles.app.json' },
-		{ title: 'menubar/examples/Pong', file: 'pong.app.json' },
-		{ title: 'menubar/examples/Shaders', file: 'shaders.app.json' }
-	];
-
-	const loader = new THREE.FileLoader();
-
-	for ( let i = 0; i < items.length; i ++ ) {
-
-		( function ( i ) {
-
-			const item = items[ i ];
-
-			const option = new UIRow();
-			option.setClass( 'option' );
-			option.setTextContent( strings.getKey( item.title ) );
-			option.onClick( function () {
-
-				if ( confirm( 'Any unsaved data will be lost. Are you sure?' ) ) {
-
-					loader.load( 'examples/' + item.file, function ( text ) {
-
-						editor.clear();
-						editor.fromJSON( JSON.parse( text ) );
-
-					} );
-
-				}
-
-			} );
-			options.add( option );
-
-		} )( i );
-
-	}
-
-	return container;
-
-}
-
-export { MenubarExamples };

+ 190 - 29
editor/js/Menubar.File.js

@@ -1,4 +1,5 @@
 import { UIPanel, UIRow, UIHorizontalRule } from './libs/ui.js';
+import { Loader } from './Loader.js';
 
 function MenubarFile( editor ) {
 
@@ -19,20 +20,162 @@ function MenubarFile( editor ) {
 	options.setClass( 'options' );
 	container.add( options );
 
-	// New
+	// New Project
 
-	let option = new UIRow();
-	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/file/new' ) );
+	const newProjectSubmenuTitle = new UIRow().setTextContent( strings.getKey( 'menubar/file/new' ) ).addClass( 'option' ).addClass( 'submenu-title' );
+	newProjectSubmenuTitle.onMouseOver( function () {
+
+		const { top, right } = this.dom.getBoundingClientRect();
+		const { paddingTop } = getComputedStyle( this.dom );
+		newProjectSubmenu.setLeft( right + 'px' );
+		newProjectSubmenu.setTop( top - parseFloat( paddingTop ) + 'px' );
+		newProjectSubmenu.setDisplay( 'block' );
+
+	} );
+	newProjectSubmenuTitle.onMouseOut( function () {
+
+		newProjectSubmenu.setDisplay( 'none' );
+
+	} );
+	options.add( newProjectSubmenuTitle );
+
+	const newProjectSubmenu = new UIPanel().setPosition( 'fixed' ).addClass( 'options' ).setDisplay( 'none' );
+	newProjectSubmenuTitle.add( newProjectSubmenu );
+
+	// New Project / Empty
+
+	let option = new UIRow().setTextContent( strings.getKey( 'menubar/file/new/empty' ) ).setClass( 'option' );
 	option.onClick( function () {
 
-		if ( confirm( 'Any unsaved data will be lost. Are you sure?' ) ) {
+		if ( confirm( strings.getKey( 'prompt/file/open' ) ) ) {
+
+			editor.clear();
+
+		}
+
+	} );
+	newProjectSubmenu.add( option );
+
+	//
+
+	newProjectSubmenu.add( new UIHorizontalRule() );
+
+	// New Project / ...
+
+	const examples = [
+		{ title: 'menubar/file/new/Arkanoid', file: 'arkanoid.app.json' },
+		{ title: 'menubar/file/new/Camera', file: 'camera.app.json' },
+		{ title: 'menubar/file/new/Particles', file: 'particles.app.json' },
+		{ title: 'menubar/file/new/Pong', file: 'pong.app.json' },
+		{ title: 'menubar/file/new/Shaders', file: 'shaders.app.json' }
+	];
+
+	const loader = new THREE.FileLoader();
+
+	for ( let i = 0; i < examples.length; i ++ ) {
+
+		( function ( i ) {
+
+			const example = examples[ i ];
+
+			const option = new UIRow();
+			option.setClass( 'option' );
+			option.setTextContent( strings.getKey( example.title ) );
+			option.onClick( function () {
+
+				if ( confirm( strings.getKey( 'prompt/file/open' ) ) ) {
+
+					loader.load( 'examples/' + example.file, function ( text ) {
+
+						editor.clear();
+						editor.fromJSON( JSON.parse( text ) );
+
+					} );
+
+				}
+
+			} );
+			newProjectSubmenu.add( option );
+
+		} )( i );
+
+	}
+
+	// Open
+
+	const openProjectForm = document.createElement( 'form' );
+	openProjectForm.style.display = 'none';
+	document.body.appendChild( openProjectForm );
+
+	const openProjectInput = document.createElement( 'input' );
+	openProjectInput.multiple = false;
+	openProjectInput.type = 'file';
+	openProjectInput.accept = '.json';
+	openProjectInput.addEventListener( 'change', async function () {
+
+		const file = openProjectInput.files[ 0 ];
+
+		if ( file === undefined ) return;
+
+		try {
+
+			const json = JSON.parse( await file.text() );
+
+			async function onEditorCleared() {
+
+				await editor.fromJSON( json );
+
+				editor.signals.editorCleared.remove( onEditorCleared );
+
+			}
+
+			editor.signals.editorCleared.add( onEditorCleared );
 
 			editor.clear();
 
+		} catch ( e ) {
+
+			alert( strings.getKey( 'prompt/file/failedToOpenProject' ) );
+			console.error( e );
+
+		} finally {
+
+			form.reset();
+
 		}
 
 	} );
+
+	openProjectForm.appendChild( openProjectInput );
+
+	option = new UIRow()
+		.addClass( 'option' )
+		.setTextContent( strings.getKey( 'menubar/file/open' ) )
+		.onClick( function () {
+
+			if ( confirm( strings.getKey( 'prompt/file/open' ) ) ) {
+
+				openProjectInput.click();
+
+			}
+
+		} );
+
+	options.add( option );
+
+	// Save
+
+	option = new UIRow()
+		.addClass( 'option' )
+		.setTextContent( strings.getKey( 'menubar/file/save' ) )
+		.onClick( function () {
+
+			const json = editor.toJSON();
+			const blob = new Blob( [ JSON.stringify( json ) ], { type: 'application/json' } );
+			editor.utils.save( blob, 'project.json' );
+
+		} );
+
 	options.add( option );
 
 	//
@@ -66,22 +209,40 @@ function MenubarFile( editor ) {
 	} );
 	options.add( option );
 
-	//
+	// Export
 
-	options.add( new UIHorizontalRule() );
+	const fileExportSubmenuTitle = new UIRow().setTextContent( strings.getKey( 'menubar/file/export' ) ).addClass( 'option' ).addClass( 'submenu-title' );
+	fileExportSubmenuTitle.onMouseOver( function () {
+
+		const { top, right } = this.dom.getBoundingClientRect();
+		const { paddingTop } = getComputedStyle( this.dom );
+		fileExportSubmenu.setLeft( right + 'px' );
+		fileExportSubmenu.setTop( top - parseFloat( paddingTop ) + 'px' );
+		fileExportSubmenu.setDisplay( 'block' );
+
+	} );
+	fileExportSubmenuTitle.onMouseOut( function () {
+
+		fileExportSubmenu.setDisplay( 'none' );
+
+	} );
+	options.add( fileExportSubmenuTitle );
+
+	const fileExportSubmenu = new UIPanel().setPosition( 'fixed' ).addClass( 'options' ).setDisplay( 'none' );
+	fileExportSubmenuTitle.add( fileExportSubmenu );
 
 	// Export DRC
 
 	option = new UIRow();
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/file/export/drc' ) );
+	option.setTextContent( 'DRC' );
 	option.onClick( async function () {
 
 		const object = editor.selected;
 
 		if ( object === null || object.isMesh === undefined ) {
 
-			alert( 'No mesh selected' );
+			alert( strings.getKey( 'prompt/file/export/noMeshSelected' ) );
 			return;
 
 		}
@@ -105,13 +266,13 @@ function MenubarFile( editor ) {
 		saveArrayBuffer( result, 'model.drc' );
 
 	} );
-	options.add( option );
+	fileExportSubmenu.add( option );
 
 	// Export GLB
 
 	option = new UIRow();
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/file/export/glb' ) );
+	option.setTextContent( 'GLB' );
 	option.onClick( async function () {
 
 		const scene = editor.scene;
@@ -136,13 +297,13 @@ function MenubarFile( editor ) {
 		}, undefined, { binary: true, animations: optimizedAnimations } );
 
 	} );
-	options.add( option );
+	fileExportSubmenu.add( option );
 
 	// Export GLTF
 
 	option = new UIRow();
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/file/export/gltf' ) );
+	option.setTextContent( 'GLTF' );
 	option.onClick( async function () {
 
 		const scene = editor.scene;
@@ -168,20 +329,20 @@ function MenubarFile( editor ) {
 
 
 	} );
-	options.add( option );
+	fileExportSubmenu.add( option );
 
 	// Export OBJ
 
 	option = new UIRow();
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/file/export/obj' ) );
+	option.setTextContent( 'OBJ' );
 	option.onClick( async function () {
 
 		const object = editor.selected;
 
 		if ( object === null ) {
 
-			alert( 'No object selected.' );
+			alert( strings.getKey( 'prompt/file/export/noObjectSelected' ) );
 			return;
 
 		}
@@ -193,13 +354,13 @@ function MenubarFile( editor ) {
 		saveString( exporter.parse( object ), 'model.obj' );
 
 	} );
-	options.add( option );
+	fileExportSubmenu.add( option );
 
 	// Export PLY (ASCII)
 
 	option = new UIRow();
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/file/export/ply' ) );
+	option.setTextContent( 'PLY' );
 	option.onClick( async function () {
 
 		const { PLYExporter } = await import( 'three/addons/exporters/PLYExporter.js' );
@@ -213,13 +374,13 @@ function MenubarFile( editor ) {
 		} );
 
 	} );
-	options.add( option );
+	fileExportSubmenu.add( option );
 
-	// Export PLY (Binary)
+	// Export PLY (BINARY)
 
 	option = new UIRow();
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/file/export/ply_binary' ) );
+	option.setTextContent( 'PLY (BINARY)' );
 	option.onClick( async function () {
 
 		const { PLYExporter } = await import( 'three/addons/exporters/PLYExporter.js' );
@@ -233,13 +394,13 @@ function MenubarFile( editor ) {
 		}, { binary: true } );
 
 	} );
-	options.add( option );
+	fileExportSubmenu.add( option );
 
 	// Export STL (ASCII)
 
 	option = new UIRow();
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/file/export/stl' ) );
+	option.setTextContent( 'STL' );
 	option.onClick( async function () {
 
 		const { STLExporter } = await import( 'three/addons/exporters/STLExporter.js' );
@@ -249,13 +410,13 @@ function MenubarFile( editor ) {
 		saveString( exporter.parse( editor.scene ), 'model.stl' );
 
 	} );
-	options.add( option );
+	fileExportSubmenu.add( option );
 
-	// Export STL (Binary)
+	// Export STL (BINARY)
 
 	option = new UIRow();
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/file/export/stl_binary' ) );
+	option.setTextContent( 'STL (BINARY)' );
 	option.onClick( async function () {
 
 		const { STLExporter } = await import( 'three/addons/exporters/STLExporter.js' );
@@ -265,13 +426,13 @@ function MenubarFile( editor ) {
 		saveArrayBuffer( exporter.parse( editor.scene, { binary: true } ), 'model-binary.stl' );
 
 	} );
-	options.add( option );
+	fileExportSubmenu.add( option );
 
 	// Export USDZ
 
 	option = new UIRow();
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/file/export/usdz' ) );
+	option.setTextContent( 'USDZ' );
 	option.onClick( async function () {
 
 		const { USDZExporter } = await import( 'three/addons/exporters/USDZExporter.js' );
@@ -281,7 +442,7 @@ function MenubarFile( editor ) {
 		saveArrayBuffer( await exporter.parseAsync( editor.scene ), 'model.usdz' );
 
 	} );
-	options.add( option );
+	fileExportSubmenu.add( option );
 
 	//
 

+ 76 - 3
editor/js/Menubar.View.js

@@ -1,4 +1,4 @@
-import { UIPanel, UIRow } from './libs/ui.js';
+import { UIHorizontalRule, UIPanel, UIRow } from './libs/ui.js';
 
 function MenubarView( editor ) {
 
@@ -17,9 +17,80 @@ function MenubarView( editor ) {
 	options.setClass( 'options' );
 	container.add( options );
 
+	// Helpers
+
+	const states = {
+
+		gridHelper: true,
+		cameraHelpers: true,
+		lightHelpers: true,
+		skeletonHelpers: true
+
+	};
+
+	// Grid Helper
+
+	let option = new UIRow().addClass( 'option' ).addClass( 'toggle' ).setTextContent( strings.getKey( 'menubar/view/gridHelper' ) ).onClick( function () {
+
+		states.gridHelper = ! states.gridHelper;
+
+		this.toggleClass( 'toggle-on', states.gridHelper );
+
+		signals.showHelpersChanged.dispatch( states );
+
+	} ).toggleClass( 'toggle-on', states.gridHelper );
+
+	options.add( option );
+
+	// Camera Helpers
+
+	option = new UIRow().addClass( 'option' ).addClass( 'toggle' ).setTextContent( strings.getKey( 'menubar/view/cameraHelpers' ) ).onClick( function () {
+
+		states.cameraHelpers = ! states.cameraHelpers;
+
+		this.toggleClass( 'toggle-on', states.cameraHelpers );
+
+		signals.showHelpersChanged.dispatch( states );
+
+	} ).toggleClass( 'toggle-on', states.cameraHelpers );
+
+	options.add( option );
+
+	// Light Helpers
+
+	option = new UIRow().addClass( 'option' ).addClass( 'toggle' ).setTextContent( strings.getKey( 'menubar/view/lightHelpers' ) ).onClick( function () {
+
+		states.lightHelpers = ! states.lightHelpers;
+
+		this.toggleClass( 'toggle-on', states.lightHelpers );
+
+		signals.showHelpersChanged.dispatch( states );
+
+	} ).toggleClass( 'toggle-on', states.lightHelpers );
+
+	options.add( option );
+
+	// Skeleton Helpers
+
+	option = new UIRow().addClass( 'option' ).addClass( 'toggle' ).setTextContent( strings.getKey( 'menubar/view/skeletonHelpers' ) ).onClick( function () {
+
+		states.skeletonHelpers = ! states.skeletonHelpers;
+
+		this.toggleClass( 'toggle-on', states.skeletonHelpers );
+
+		signals.showHelpersChanged.dispatch( states );
+
+	} ).toggleClass( 'toggle-on', states.skeletonHelpers );
+
+	options.add( option );
+
+	//
+
+	options.add( new UIHorizontalRule() );
+
 	// Fullscreen
 
-	const option = new UIRow();
+	option = new UIRow();
 	option.setClass( 'option' );
 	option.setTextContent( strings.getKey( 'menubar/view/fullscreen' ) );
 	option.onClick( function () {
@@ -97,12 +168,14 @@ function MenubarView( editor ) {
 
 					}
 
-			} );
+				} );
 
 		}
 
 	}
 
+	//
+
 	return container;
 
 }

+ 0 - 2
editor/js/Menubar.js

@@ -3,7 +3,6 @@ import { UIPanel } from './libs/ui.js';
 import { MenubarAdd } from './Menubar.Add.js';
 import { MenubarEdit } from './Menubar.Edit.js';
 import { MenubarFile } from './Menubar.File.js';
-import { MenubarExamples } from './Menubar.Examples.js';
 import { MenubarView } from './Menubar.View.js';
 import { MenubarHelp } from './Menubar.Help.js';
 import { MenubarStatus } from './Menubar.Status.js';
@@ -16,7 +15,6 @@ function Menubar( editor ) {
 	container.add( new MenubarFile( editor ) );
 	container.add( new MenubarEdit( editor ) );
 	container.add( new MenubarAdd( editor ) );
-	container.add( new MenubarExamples( editor ) );
 	container.add( new MenubarView( editor ) );
 	container.add( new MenubarHelp( editor ) );
 

+ 1 - 1
editor/js/Resizer.js

@@ -36,7 +36,7 @@ function Resizer( editor ) {
 
 		const cX = clientX < 0 ? 0 : clientX > offsetWidth ? offsetWidth : clientX;
 
-		const x = Math.max( 260, offsetWidth - cX ); // .TabbedPanel min-width: 260px
+		const x = Math.max( 335, offsetWidth - cX ); // .TabbedPanel min-width: 335px
 
 		dom.style.right = x + 'px';
 

+ 76 - 10
editor/js/Script.js

@@ -6,6 +6,7 @@ import { SetMaterialValueCommand } from './commands/SetMaterialValueCommand.js';
 function Script( editor ) {
 
 	const signals = editor.signals;
+	const strings = editor.strings;
 
 	const container = new UIPanel();
 	container.setId( 'script' );
@@ -342,8 +343,7 @@ function Script( editor ) {
 	codemirror.on( 'keypress', function ( cm, kb ) {
 
 		if ( currentMode !== 'javascript' ) return;
-		const typed = String.fromCharCode( kb.which || kb.keyCode );
-		if ( /[\w\.]/.exec( typed ) ) {
+		if ( /[\w\.]/.exec( kb.key ) ) {
 
 			server.complete( cm );
 
@@ -360,16 +360,49 @@ function Script( editor ) {
 
 	} );
 
+	function setTitle( object, script ) {
+
+		if ( typeof script === 'object' ) {
+
+			title.setValue( object.name + ' / ' + script.name );
+
+		} else {
+
+			switch ( script ) {
+
+				case 'vertexShader':
+
+					title.setValue( object.material.name + ' / ' + strings.getKey( 'script/title/vertexShader' ) );
+					break;
+
+				case 'fragmentShader':
+
+					title.setValue( object.material.name + ' / ' + strings.getKey( 'script/title/fragmentShader' ) );
+					break;
+
+				case 'programInfo':
+
+					title.setValue( object.material.name + ' / ' + strings.getKey( 'script/title/programInfo' ) );
+					break;
+
+				default:
+
+					throw new Error( 'setTitle: Unknown script' );
+
+			}
+
+		}
+
+	}
+
 	signals.editScript.add( function ( object, script ) {
 
-		let mode, name, source;
+		let mode, source;
 
 		if ( typeof ( script ) === 'object' ) {
 
 			mode = 'javascript';
-			name = script.name;
 			source = script.source;
-			title.setValue( object.name + ' / ' + name );
 
 		} else {
 
@@ -378,7 +411,6 @@ function Script( editor ) {
 				case 'vertexShader':
 
 					mode = 'glsl';
-					name = 'Vertex Shader';
 					source = object.material.vertexShader || '';
 
 					break;
@@ -386,7 +418,6 @@ function Script( editor ) {
 				case 'fragmentShader':
 
 					mode = 'glsl';
-					name = 'Fragment Shader';
 					source = object.material.fragmentShader || '';
 
 					break;
@@ -394,7 +425,6 @@ function Script( editor ) {
 				case 'programInfo':
 
 					mode = 'json';
-					name = 'Program Properties';
 					const json = {
 						defines: object.material.defines,
 						uniforms: object.material.uniforms,
@@ -402,12 +432,18 @@ function Script( editor ) {
 					};
 					source = JSON.stringify( json, null, '\t' );
 
-			}
+					break;
 
-			title.setValue( object.material.name + ' / ' + name );
+				default:
+
+					throw new Error( 'editScript: Unknown script' );
+
+			}
 
 		}
 
+		setTitle( object, script );
+
 		currentMode = mode;
 		currentScript = script;
 		currentObject = object;
@@ -430,6 +466,36 @@ function Script( editor ) {
 
 	} );
 
+	signals.objectChanged.add( function ( object ) {
+
+		if ( object !== currentObject ) return;
+
+		if ( [ 'programInfo', 'vertexShader', 'fragmentShader' ].includes( currentScript ) ) return;
+
+		setTitle( currentObject, currentScript );
+
+	} );
+
+	signals.scriptChanged.add( function ( script ) {
+
+		if ( script === currentScript ) {
+
+			setTitle( currentObject, currentScript );
+
+		}
+
+	} );
+
+	signals.materialChanged.add( function ( object, slot ) {
+
+		if ( object !== currentObject ) return;
+
+		// TODO: Adds multi-material support
+
+		setTitle( currentObject, currentScript );
+
+	} );
+
 	return container;
 
 }

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

@@ -35,7 +35,7 @@ function SidebarGeometryBufferGeometry( editor ) {
 			if ( index !== null ) {
 
 				containerAttributes.add( new UIText( strings.getKey( 'sidebar/geometry/buffer_geometry/index' ) ).setWidth( '80px' ) );
-				containerAttributes.add( new UIText( ( index.count ).format() ).setFontSize( '12px' ) );
+				containerAttributes.add( new UIText( editor.utils.formatNumber( index.count ) ).setFontSize( '12px' ) );
 				containerAttributes.add( new UIBreak() );
 
 			}
@@ -47,7 +47,7 @@ function SidebarGeometryBufferGeometry( editor ) {
 				const attribute = attributes[ name ];
 
 				containerAttributes.add( new UIText( name ).setWidth( '80px' ) );
-				containerAttributes.add( new UIText( ( attribute.count ).format() + ' (' + attribute.itemSize + ')' ).setFontSize( '12px' ) );
+				containerAttributes.add( new UIText( editor.utils.formatNumber( attribute.count ) + ' (' + attribute.itemSize + ')' ).setFontSize( '12px' ) );
 				containerAttributes.add( new UIBreak() );
 
 			}
@@ -76,7 +76,7 @@ function SidebarGeometryBufferGeometry( editor ) {
 					const morphTargets = morphAttributes[ name ];
 
 					containerMorphAttributes.add( new UIText( name ).setWidth( '80px' ) );
-					containerMorphAttributes.add( new UIText( ( morphTargets.length ).format() ).setFontSize( '12px' ) );
+					containerMorphAttributes.add( new UIText( editor.utils.formatNumber( morphTargets.length ) ).setFontSize( '12px' ) );
 					containerMorphAttributes.add( new UIBreak() );
 
 				}

+ 2 - 2
editor/js/Sidebar.Geometry.CircleGeometry.js

@@ -36,7 +36,7 @@ function GeometryParametersPanel( editor, object ) {
 	// thetaStart
 
 	const thetaStartRow = new UIRow();
-	const thetaStart = new UINumber( parameters.thetaStart * THREE.MathUtils.RAD2DEG ).setStep( 10 ).onChange( update );
+	const thetaStart = new UINumber( parameters.thetaStart * THREE.MathUtils.RAD2DEG ).setUnit( '°' ).setStep( 10 ).onChange( update );
 
 	thetaStartRow.add( new UIText( strings.getKey( 'sidebar/geometry/circle_geometry/thetastart' ) ).setClass( 'Label' ) );
 	thetaStartRow.add( thetaStart );
@@ -46,7 +46,7 @@ function GeometryParametersPanel( editor, object ) {
 	// thetaLength
 
 	const thetaLengthRow = new UIRow();
-	const thetaLength = new UINumber( parameters.thetaLength * THREE.MathUtils.RAD2DEG ).setStep( 10 ).onChange( update );
+	const thetaLength = new UINumber( parameters.thetaLength * THREE.MathUtils.RAD2DEG ).setUnit( '°' ).setStep( 10 ).onChange( update );
 
 	thetaLengthRow.add( new UIText( strings.getKey( 'sidebar/geometry/circle_geometry/thetalength' ) ).setClass( 'Label' ) );
 	thetaLengthRow.add( thetaLength );

+ 52 - 33
editor/js/Sidebar.Geometry.ExtrudeGeometry.js

@@ -15,9 +15,10 @@ function GeometryParametersPanel( editor, object ) {
 	const options = parameters.options;
 	options.curveSegments = options.curveSegments != undefined ? options.curveSegments : 12;
 	options.steps = options.steps != undefined ? options.steps : 1;
-	options.depth = options.depth != undefined ? options.depth : 100;
-	options.bevelThickness = options.bevelThickness !== undefined ? options.bevelThickness : 6;
-	options.bevelSize = options.bevelSize !== undefined ? options.bevelSize : 4;
+	options.depth = options.depth != undefined ? options.depth : 1;
+	const bevelThickness = options.bevelThickness !== undefined ? options.bevelThickness : 0.2;
+	options.bevelThickness = bevelThickness;
+	options.bevelSize = options.bevelSize !== undefined ? options.bevelSize : bevelThickness - 0.1;
 	options.bevelOffset = options.bevelOffset !== undefined ? options.bevelOffset : 0;
 	options.bevelSegments = options.bevelSegments !== undefined ? options.bevelSegments : 3;
 
@@ -62,59 +63,77 @@ function GeometryParametersPanel( editor, object ) {
 
 	container.add( enabledRow );
 
-	let thickness, size, offset, segments;
+	// thickness
 
-	if ( options.bevelEnabled === true ) {
+	const thicknessRow = new UIRow();
+	const thickness = new UINumber( options.bevelThickness ).onChange( update );
 
-		// thickness
+	thicknessRow.add( new UIText( strings.getKey( 'sidebar/geometry/extrude_geometry/bevelThickness' ) ).setClass( 'Label' ) );
+	thicknessRow.add( thickness );
 
-		const thicknessRow = new UIRow();
-		thickness = new UINumber( options.bevelThickness ).onChange( update );
+	container.add( thicknessRow );
 
-		thicknessRow.add( new UIText( strings.getKey( 'sidebar/geometry/extrude_geometry/bevelThickness' ) ).setClass( 'Label' ) );
-		thicknessRow.add( thickness );
+	// size
 
-		container.add( thicknessRow );
+	const sizeRow = new UIRow();
+	const size = new UINumber( options.bevelSize ).onChange( update );
 
-		// size
+	sizeRow.add( new UIText( strings.getKey( 'sidebar/geometry/extrude_geometry/bevelSize' ) ).setClass( 'Label' ) );
+	sizeRow.add( size );
 
-		const sizeRow = new UIRow();
-		size = new UINumber( options.bevelSize ).onChange( update );
+	container.add( sizeRow );
 
-		sizeRow.add( new UIText( strings.getKey( 'sidebar/geometry/extrude_geometry/bevelSize' ) ).setClass( 'Label' ) );
-		sizeRow.add( size );
+	// offset
 
-		container.add( sizeRow );
+	const offsetRow = new UIRow();
+	const offset = new UINumber( options.bevelOffset ).onChange( update );
 
-		// offset
+	offsetRow.add( new UIText( strings.getKey( 'sidebar/geometry/extrude_geometry/bevelOffset' ) ).setClass( 'Label' ) );
+	offsetRow.add( offset );
 
-		const offsetRow = new UIRow();
-		offset = new UINumber( options.bevelOffset ).onChange( update );
+	container.add( offsetRow );
 
-		offsetRow.add( new UIText( strings.getKey( 'sidebar/geometry/extrude_geometry/bevelOffset' ) ).setClass( 'Label' ) );
-		offsetRow.add( offset );
+	// segments
 
-		container.add( offsetRow );
+	const segmentsRow = new UIRow();
+	const segments = new UIInteger( options.bevelSegments ).onChange( update ).setRange( 0, Infinity );
 
-		// segments
+	segmentsRow.add( new UIText( strings.getKey( 'sidebar/geometry/extrude_geometry/bevelSegments' ) ).setClass( 'Label' ) );
+	segmentsRow.add( segments );
 
-		const segmentsRow = new UIRow();
-		segments = new UIInteger( options.bevelSegments ).onChange( update ).setRange( 0, Infinity );
+	container.add( segmentsRow );
 
-		segmentsRow.add( new UIText( strings.getKey( 'sidebar/geometry/extrude_geometry/bevelSegments' ) ).setClass( 'Label' ) );
-		segmentsRow.add( segments );
-
-		container.add( segmentsRow );
-
-	}
+	updateBevelRow( options.bevelEnabled );
 
 	const button = new UIButton( strings.getKey( 'sidebar/geometry/extrude_geometry/shape' ) ).onClick( toShape ).setClass( 'Label' ).setMarginLeft( '120px' );
 	container.add( button );
 
 	//
 
+	function updateBevelRow( enabled ) {
+
+		if ( enabled === true ) {
+
+			thicknessRow.setDisplay( '' );
+			sizeRow.setDisplay( '' );
+			offsetRow.setDisplay( '' );
+			segmentsRow.setDisplay( '' );
+
+		} else {
+
+			thicknessRow.setDisplay( 'none' );
+			sizeRow.setDisplay( 'none' );
+			offsetRow.setDisplay( 'none' );
+			segmentsRow.setDisplay( 'none' );
+
+		}
+
+	}
+
 	function update() {
 
+		updateBevelRow( enabled.getValue() );
+
 		editor.execute( new SetGeometryCommand( editor, object, new THREE.ExtrudeGeometry(
 			parameters.shapes,
 			{
@@ -122,7 +141,7 @@ function GeometryParametersPanel( editor, object ) {
 				steps: steps.getValue(),
 				depth: depth.getValue(),
 				bevelEnabled: enabled.getValue(),
-				bevelThickness: options.bevelThickness,
+				bevelThickness: thickness !== undefined ? thickness.getValue() : options.bevelThickness,
 				bevelSize: size !== undefined ? size.getValue() : options.bevelSize,
 				bevelOffset: offset !== undefined ? offset.getValue() : options.bevelOffset,
 				bevelSegments: segments !== undefined ? segments.getValue() : options.bevelSegments

+ 2 - 2
editor/js/Sidebar.Geometry.RingGeometry.js

@@ -56,7 +56,7 @@ function GeometryParametersPanel( editor, object ) {
 	// thetaStart
 
 	const thetaStartRow = new UIRow();
-	const thetaStart = new UINumber( parameters.thetaStart * THREE.MathUtils.RAD2DEG ).setStep( 10 ).onChange( update );
+	const thetaStart = new UINumber( parameters.thetaStart * THREE.MathUtils.RAD2DEG ).setUnit( '°' ).setStep( 10 ).onChange( update );
 
 	thetaStartRow.add( new UIText( strings.getKey( 'sidebar/geometry/ring_geometry/thetastart' ) ).setClass( 'Label' ) );
 	thetaStartRow.add( thetaStart );
@@ -66,7 +66,7 @@ function GeometryParametersPanel( editor, object ) {
 	// thetaLength
 
 	const thetaLengthRow = new UIRow();
-	const thetaLength = new UINumber( parameters.thetaLength * THREE.MathUtils.RAD2DEG ).setStep( 10 ).onChange( update );
+	const thetaLength = new UINumber( parameters.thetaLength * THREE.MathUtils.RAD2DEG ).setUnit( '°' ).setStep( 10 ).onChange( update );
 
 	thetaLengthRow.add( new UIText( strings.getKey( 'sidebar/geometry/ring_geometry/thetalength' ) ).setClass( 'Label' ) );
 	thetaLengthRow.add( thetaLength );

+ 4 - 4
editor/js/Sidebar.Geometry.SphereGeometry.js

@@ -46,7 +46,7 @@ function GeometryParametersPanel( editor, object ) {
 	// phiStart
 
 	const phiStartRow = new UIRow();
-	const phiStart = new UINumber( parameters.phiStart * THREE.MathUtils.RAD2DEG ).setStep( 10 ).onChange( update );
+	const phiStart = new UINumber( parameters.phiStart * THREE.MathUtils.RAD2DEG ).setUnit( '°' ).setStep( 10 ).onChange( update );
 
 	phiStartRow.add( new UIText( strings.getKey( 'sidebar/geometry/sphere_geometry/phistart' ) ).setClass( 'Label' ) );
 	phiStartRow.add( phiStart );
@@ -56,7 +56,7 @@ function GeometryParametersPanel( editor, object ) {
 	// phiLength
 
 	const phiLengthRow = new UIRow();
-	const phiLength = new UINumber( parameters.phiLength * THREE.MathUtils.RAD2DEG ).setStep( 10 ).onChange( update );
+	const phiLength = new UINumber( parameters.phiLength * THREE.MathUtils.RAD2DEG ).setUnit( '°' ).setStep( 10 ).onChange( update );
 
 	phiLengthRow.add( new UIText( strings.getKey( 'sidebar/geometry/sphere_geometry/philength' ) ).setClass( 'Label' ) );
 	phiLengthRow.add( phiLength );
@@ -66,7 +66,7 @@ function GeometryParametersPanel( editor, object ) {
 	// thetaStart
 
 	const thetaStartRow = new UIRow();
-	const thetaStart = new UINumber( parameters.thetaStart * THREE.MathUtils.RAD2DEG ).setStep( 10 ).onChange( update );
+	const thetaStart = new UINumber( parameters.thetaStart * THREE.MathUtils.RAD2DEG ).setUnit( '°' ).setStep( 10 ).onChange( update );
 
 	thetaStartRow.add( new UIText( strings.getKey( 'sidebar/geometry/sphere_geometry/thetastart' ) ).setClass( 'Label' ) );
 	thetaStartRow.add( thetaStart );
@@ -76,7 +76,7 @@ function GeometryParametersPanel( editor, object ) {
 	// thetaLength
 
 	const thetaLengthRow = new UIRow();
-	const thetaLength = new UINumber( parameters.thetaLength * THREE.MathUtils.RAD2DEG ).setStep( 10 ).onChange( update );
+	const thetaLength = new UINumber( parameters.thetaLength * THREE.MathUtils.RAD2DEG ).setUnit( '°' ).setStep( 10 ).onChange( update );
 
 	thetaLengthRow.add( new UIText( strings.getKey( 'sidebar/geometry/sphere_geometry/thetalength' ) ).setClass( 'Label' ) );
 	thetaLengthRow.add( thetaLength );

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

@@ -56,7 +56,7 @@ function GeometryParametersPanel( editor, object ) {
 	// arc
 
 	const arcRow = new UIRow();
-	const arc = new UINumber( parameters.arc * THREE.MathUtils.RAD2DEG ).setStep( 10 ).onChange( update );
+	const arc = new UINumber( parameters.arc * THREE.MathUtils.RAD2DEG ).setUnit( '°' ).setStep( 10 ).onChange( update );
 
 	arcRow.add( new UIText( strings.getKey( 'sidebar/geometry/torus_geometry/arc' ) ).setClass( 'Label' ) );
 	arcRow.add( arc );

+ 51 - 6
editor/js/Sidebar.Geometry.js

@@ -1,6 +1,6 @@
 import * as THREE from 'three';
 
-import { UIPanel, UIRow, UIText, UIInput, UIButton, UISpan } from './libs/ui.js';
+import { UIPanel, UIRow, UIText, UIInput, UIButton, UISpan, UITextArea } from './libs/ui.js';
 
 import { SetGeometryValueCommand } from './commands/SetGeometryValueCommand.js';
 
@@ -145,6 +145,53 @@ function SidebarGeometry( editor ) {
 	geometryBoundingBoxRow.add( geometryBoundingBox );
 	container.add( geometryBoundingBoxRow );
 
+	// userData
+
+	const geometryUserDataRow = new UIRow();
+	const geometryUserData = new UITextArea().setValue( '{}' ).setWidth( '150px' ).setHeight( '40px' ).setFontSize( '12px' ).onChange( function () {
+
+		try {
+
+			const userData = JSON.parse( geometryUserData.getValue() );
+
+			if ( JSON.stringify( editor.selected.geometry.userData ) != JSON.stringify( userData ) ) {
+
+				editor.execute( new SetGeometryValueCommand( editor, editor.selected, 'userData', userData ) );
+
+				build();
+
+			}
+
+		} catch ( exception ) {
+
+			console.warn( exception );
+
+		}
+
+	} );
+	geometryUserData.onKeyUp( function () {
+
+		try {
+
+			JSON.parse( geometryUserData.getValue() );
+
+			geometryUserData.dom.classList.add( 'success' );
+			geometryUserData.dom.classList.remove( 'fail' );
+
+		} catch ( error ) {
+
+			geometryUserData.dom.classList.remove( 'success' );
+			geometryUserData.dom.classList.add( 'fail' );
+
+		}
+
+	} );
+
+	geometryUserDataRow.add( new UIText( strings.getKey( 'sidebar/geometry/userdata' ) ).setClass( 'Label' ) );
+	geometryUserDataRow.add( geometryUserData );
+
+	container.add( geometryUserDataRow );
+
 	// Helpers
 
 	const helpersRow = new UIRow().setMarginLeft( '120px' );
@@ -192,11 +239,7 @@ function SidebarGeometry( editor ) {
 
 		}
 
-		const left = ( screen.width - 500 ) / 2;
-		const top = ( screen.height - 500 ) / 2;
-
-		const url = URL.createObjectURL( new Blob( [ output ], { type: 'text/plain;charset=utf-8' } ) );
-		window.open( url, '_blank', `location=no,left=${left},top=${top},width=500,height=500` );
+		editor.utils.save( new Blob( [ output ] ), `${ geometryName.getValue() || 'geometry' }.json` );
 
 	} );
 	container.add( exportJson );
@@ -251,6 +294,8 @@ function SidebarGeometry( editor ) {
 
 			helpersRow.setDisplay( geometry.hasAttribute( 'normal' ) ? '' : 'none' );
 
+			geometryUserData.setValue( JSON.stringify( geometry.userData, null, '  ' ) );
+
 		} else {
 
 			container.setDisplay( 'none' );

+ 38 - 10
editor/js/Sidebar.Material.js

@@ -430,7 +430,7 @@ function SidebarMaterial( editor ) {
 	exportJson.onClick( function () {
 
 		const object = editor.selected;
-		const material = object.material;
+		const material = Array.isArray( object.material ) ? object.material[ currentMaterialSlot ] : object.material;
 
 		let output = material.toJSON();
 
@@ -445,11 +445,7 @@ function SidebarMaterial( editor ) {
 
 		}
 
-		const left = ( screen.width - 500 ) / 2;
-		const top = ( screen.height - 500 ) / 2;
-
-		const url = URL.createObjectURL( new Blob( [ output ], { type: 'text/plain;charset=utf-8' } ) );
-		window.open( url, '_blank', `location=no,left=${left},top=${top},width=500,height=500` );
+		editor.utils.save( new Blob( [ output ] ), `${ materialName.getValue() || 'material' }.json` );
 
 	} );
 	container.add( exportJson );
@@ -488,19 +484,51 @@ function SidebarMaterial( editor ) {
 
 				}
 
-				if ( Array.isArray( currentObject.material ) ) {
+				const currentMaterial = currentObject.material;
+
+				if ( material.type === 'MeshPhysicalMaterial' && currentMaterial.type === 'MeshStandardMaterial' ) {
+
+					// TODO Find a easier to maintain approach
+
+					const properties = [
+						'color', 'emissive', 'roughness', 'metalness', 'map', 'emissiveMap', 'alphaMap',
+						'bumpMap', 'normalMap', 'normalScale', 'displacementMap', 'roughnessMap', 'metalnessMap',
+						'envMap', 'lightMap', 'aoMap', 'side'
+					];
+
+					for ( const property of properties ) {
+
+						const value = currentMaterial[ property ];
+
+						if ( value === null ) continue;
+						
+						if ( value[ 'clone' ] !== undefined ) {
+
+							material[ property ] = value.clone();
+
+						} else {
+
+							material[ property ] = value;
+
+						}
+
+					}
+
+				}
+
+				if ( Array.isArray( currentMaterial ) ) {
 
 					// don't remove the entire multi-material. just the material of the selected slot
 
-					editor.removeMaterial( currentObject.material[ currentMaterialSlot ] );
+					editor.removeMaterial( currentMaterial[ currentMaterialSlot ] );
 
 				} else {
 
-					editor.removeMaterial( currentObject.material );
+					editor.removeMaterial( currentMaterial );
 
 				}
 
-				editor.execute( new SetMaterialCommand( editor, currentObject, material, currentMaterialSlot ), 'New Material: ' + materialClass.getValue() );
+				editor.execute( new SetMaterialCommand( editor, currentObject, material, currentMaterialSlot ), strings.getKey( 'command/SetMaterial' ) + ': ' + materialClass.getValue() );
 				editor.addMaterial( material );
 				// TODO Copy other references in the scene graph
 				// keeping name and UUID then.

+ 1 - 4
editor/js/Sidebar.Object.js

@@ -409,11 +409,8 @@ function SidebarObject( editor ) {
 
 		}
 
-		const left = ( screen.width - 500 ) / 2;
-		const top = ( screen.height - 500 ) / 2;
 
-		const url = URL.createObjectURL( new Blob( [ output ], { type: 'text/plain;charset=utf-8' } ) );
-		window.open( url, '_blank', `location=no,left=${left},top=${top},width=500,height=500` );
+		editor.utils.save( new Blob( [ output ] ), `${ objectName.getValue() || 'object' }.json` );
 
 	} );
 	container.add( exportJson );

+ 46 - 14
editor/js/Sidebar.Project.Image.js

@@ -2,7 +2,7 @@ import * as THREE from 'three';
 
 import { UIBreak, UIButton, UIInteger, UIPanel, UIRow, UISelect, UIText } from './libs/ui.js';
 
-// import { ViewportPathtracer } from './Viewport.Pathtracer.js';
+import { ViewportPathtracer } from './Viewport.Pathtracer.js';
 
 function SidebarProjectImage( editor ) {
 
@@ -19,17 +19,34 @@ function SidebarProjectImage( editor ) {
 	// Shading
 
 	const shadingRow = new UIRow();
-	// container.add( shadingRow );
+	container.add( shadingRow );
 
 	shadingRow.add( new UIText( strings.getKey( 'sidebar/project/shading' ) ).setClass( 'Label' ) );
 
 	const shadingTypeSelect = new UISelect().setOptions( {
-		0: 'Solid',
-		1: 'Realistic'
-	} ).setWidth( '125px' );
-	shadingTypeSelect.setValue( 0 );
+		0: 'SOLID',
+		1: 'REALISTIC'
+	} ).setWidth( '170px' ).setTextTransform( 'unset' ).onChange( refreshShadingRow ).setValue( 0 );
 	shadingRow.add( shadingTypeSelect );
 
+	const pathTracerMinSamples = 3;
+	const pathTracerMaxSamples = 65536;
+	const samplesNumber = new UIInteger( 16 ).setRange( pathTracerMinSamples, pathTracerMaxSamples );
+
+	const samplesRow = new UIRow();
+	samplesRow.add( new UIText( strings.getKey( 'sidebar/project/image/samples' ) ).setClass( 'Label' ) );
+	samplesRow.add( samplesNumber );
+
+	container.add( samplesRow );
+
+	function refreshShadingRow() {
+
+		samplesRow.setHidden( shadingTypeSelect.getValue() !== '1' );
+
+	}
+
+	refreshShadingRow();
+
 	// Resolution
 
 	const resolutionRow = new UIRow();
@@ -108,7 +125,7 @@ function SidebarProjectImage( editor ) {
 				renderer.dispose();
 
 				break;
-			/*
+
 			case 1: // REALISTIC
 
 				const status = document.createElement( 'div' );
@@ -120,26 +137,41 @@ function SidebarProjectImage( editor ) {
 				status.style.fontSize = '12px';
 				output.document.body.appendChild( status );
 
-				const pathtracer = new ViewportPathtracer( renderer );
-				pathtracer.init( scene, camera );
-				pathtracer.setSize( imageWidth.getValue(), imageHeight.getValue());
+				const pathTracer = new ViewportPathtracer( renderer );
+				pathTracer.init( scene, camera );
+				pathTracer.setSize( imageWidth.getValue(), imageHeight.getValue() );
+
+				const maxSamples = Math.max( pathTracerMinSamples, Math.min( pathTracerMaxSamples, samplesNumber.getValue() ) );
 
 				function animate() {
 
 					if ( output.closed === true ) return;
 
-					requestAnimationFrame( animate );
+					const samples = Math.floor( pathTracer.getSamples() ) + 1;
+
+					if ( samples < maxSamples ) {
+
+						requestAnimationFrame( animate );
+
+					}
+
+					pathTracer.update();
+
+					const progress = Math.floor( samples / maxSamples * 100 );
+
+					status.textContent = `${ samples } / ${ maxSamples } ( ${ progress }% )`;
+
+					if ( progress === 100 ) {
 
-					pathtracer.update();
+						status.textContent += ' ✓';
 
-					// status.textContent = Math.floor( samples );
+					}
 
 				}
 
 				animate();
 
 				break;
-			*/
 
 		}
 

+ 120 - 33
editor/js/Sidebar.Project.Video.js

@@ -16,17 +16,25 @@ function SidebarProjectVideo( editor ) {
 
 	// Resolution
 
+	function toDiv2() {
+
+		// Make sure dimensions are divisible by 2 (requirement of libx264)
+
+		this.setValue( 2 * Math.floor( this.getValue() / 2 ) );
+
+	}
+
 	const resolutionRow = new UIRow();
 	container.add( resolutionRow );
 
 	resolutionRow.add( new UIText( strings.getKey( 'sidebar/project/resolution' ) ).setClass( 'Label' ) );
 
-	const videoWidth = new UIInteger( 1024 ).setTextAlign( 'center' ).setWidth( '28px' );
+	const videoWidth = new UIInteger( 1024 ).setTextAlign( 'center' ).setWidth( '28px' ).setStep( 2 ).onChange( toDiv2 );
 	resolutionRow.add( videoWidth );
 
 	resolutionRow.add( new UIText( '×' ).setTextAlign( 'center' ).setFontSize( '12px' ).setWidth( '12px' ) );
 
-	const videoHeight = new UIInteger( 1024 ).setTextAlign( 'center' ).setWidth( '28px' );
+	const videoHeight = new UIInteger( 1024 ).setTextAlign( 'center' ).setWidth( '28px' ).setStep( 2 ).onChange( toDiv2 );
 	resolutionRow.add( videoHeight );
 
 	const videoFPS = new UIInteger( 30 ).setTextAlign( 'center' ).setWidth( '20px' );
@@ -80,13 +88,49 @@ function SidebarProjectVideo( editor ) {
 		output.document.body.style.overflow = 'hidden';
 		output.document.body.appendChild( canvas );
 
-		const progress = document.createElement( 'progress' );
-		progress.style.position = 'absolute';
-		progress.style.top = '10px';
-		progress.style.left = ( ( width - 170 ) / 2 ) + 'px';
-		progress.style.width = '170px';
-		progress.value = 0;
-		output.document.body.appendChild( progress );
+		const status = document.createElement( 'div' );
+		status.style.position = 'absolute';
+		status.style.top = '10px';
+		status.style.left = '10px';
+		status.style.color = 'white';
+		status.style.fontFamily = 'system-ui';
+		status.style.fontSize = '12px';
+		status.style.textShadow = '0 0 2px black';
+		output.document.body.appendChild( status );
+
+		const writeFileStatus = document.createElement( 'span' );
+		status.appendChild( writeFileStatus );
+
+		const encodingText = document.createElement( 'span' );
+		encodingText.textContent = ' encoding'; // TODO: l10n
+		encodingText.hidden = true;
+		status.appendChild( encodingText );
+
+		const encodingStatus = document.createElement( 'span' );
+		encodingStatus.hidden = true;
+		status.appendChild( encodingStatus );
+
+		const videoSizeText = document.createElement( 'span' );
+		videoSizeText.textContent = ' size'; // TODO: l10n
+		videoSizeText.hidden = true;
+		status.appendChild( videoSizeText );
+
+		const videoSizeStatus = document.createElement( 'span' );
+		videoSizeStatus.hidden = true;
+		status.appendChild( videoSizeStatus );
+
+		const completedStatus = document.createElement( 'span' );
+		completedStatus.textContent = ' ✓';
+		completedStatus.hidden = true;
+		status.appendChild( completedStatus );
+
+		const video = document.createElement( 'video' );
+		video.width = width;
+		video.height = height;
+		video.controls = true;
+		video.loop = true;
+		video.hidden = true;
+		output.document.body.appendChild( video );
 
 		//
 
@@ -97,7 +141,21 @@ function SidebarProjectVideo( editor ) {
 
 		ffmpeg.setProgress( ( { ratio } ) => {
 
-			progress.value = ( ratio * 0.5 ) + 0.5;
+			encodingStatus.textContent = `( ${ Math.floor( ratio * 100 ) }% )`;
+
+		} );
+
+		output.addEventListener( 'unload', function () {
+
+			if ( video.src.startsWith( 'blob:' ) ) {
+
+				URL.revokeObjectURL( video.src );
+
+			} else {
+
+				ffmpeg.exit();
+
+			}
 
 		} );
 
@@ -105,41 +163,57 @@ function SidebarProjectVideo( editor ) {
 		const duration = videoDuration.getValue();
 		const frames = duration * fps;
 
-		let currentTime = 0;
+		//
 
-		for ( let i = 0; i < frames; i ++ ) {
+		await ( async function () {
 
-			player.render( currentTime );
+			let currentTime = 0;
 
-			const num = i.toString().padStart( 5, '0' );
-			ffmpeg.FS( 'writeFile', `tmp.${num}.png`, await fetchFile( canvas.toDataURL() ) );
-			currentTime += 1 / fps;
+			for ( let i = 0; i < frames; i ++ ) {
 
-			progress.value = ( i / frames ) * 0.5;
+				player.render( currentTime );
 
-		}
+				const num = i.toString().padStart( 5, '0' );
 
-		await ffmpeg.run( '-framerate', String( fps ), '-pattern_type', 'glob', '-i', '*.png', '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-preset', 'slow', '-crf', String( 5 ), 'out.mp4' );
+				if ( output.closed ) return;
 
-		const data = ffmpeg.FS( 'readFile', 'out.mp4' );
+				ffmpeg.FS( 'writeFile', `tmp.${num}.png`, await fetchFile( canvas.toDataURL() ) );
+				currentTime += 1 / fps;
 
-		for ( let i = 0; i < frames; i ++ ) {
+				const frame = i + 1;
+				const progress = Math.floor( frame / frames * 100 );
+				writeFileStatus.textContent = `${ frame } / ${ frames } ( ${ progress }% )`;
 
-			const num = i.toString().padStart( 5, '0' );
-			ffmpeg.FS( 'unlink', `tmp.${num}.png` );
+			}
 
-		}
+			encodingText.hidden = false;
+			encodingStatus.hidden = false;
 
-		output.document.body.removeChild( canvas );
-		output.document.body.removeChild( progress );
+			await ffmpeg.run( '-framerate', String( fps ), '-pattern_type', 'glob', '-i', '*.png', '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-preset', 'slow', '-crf', String( 5 ), 'out.mp4' );
 
-		const video = document.createElement( 'video' );
-		video.width = width;
-		video.height = height;
-		video.controls = true;
-		video.loop = true;
-		video.src = URL.createObjectURL( new Blob( [ data.buffer ], { type: 'video/mp4' } ) );
-		output.document.body.appendChild( video );
+			const videoData = ffmpeg.FS( 'readFile', 'out.mp4' );
+
+			for ( let i = 0; i < frames; i ++ ) {
+
+				const num = i.toString().padStart( 5, '0' );
+				ffmpeg.FS( 'unlink', `tmp.${num}.png` );
+
+			}
+
+			ffmpeg.FS( 'unlink', 'out.mp4' );
+
+			output.document.body.removeChild( canvas );
+
+			videoSizeText.hidden = false;
+			videoSizeStatus.textContent = `( ${ formatFileSize( videoData.buffer.byteLength ) } )`;
+			videoSizeStatus.hidden = false;
+
+			completedStatus.hidden = false;
+
+			video.src = URL.createObjectURL( new Blob( [ videoData.buffer ], { type: 'video/mp4' } ) );
+			video.hidden = false;
+
+		} )();
 
 		player.dispose();
 
@@ -152,4 +226,17 @@ function SidebarProjectVideo( editor ) {
 
 }
 
+function formatFileSize( sizeB, K = 1024 ) {
+
+	if ( sizeB === 0 ) return '0B';
+
+	const sizes = [ sizeB, sizeB / K, sizeB / K / K ].reverse();
+	const units = [ 'B', 'KB', 'MB' ].reverse();
+	const index = sizes.findIndex( size => size >= 1 );
+
+	return new Intl.NumberFormat( 'en-us', { useGrouping: true, maximumFractionDigits: 1 } )
+		.format( sizes[ index ] ) + units[ index ];
+
+}
+
 export { SidebarProjectVideo };

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

@@ -18,6 +18,54 @@ function SidebarProperties( editor ) {
 	container.addTab( 'scriptTab', strings.getKey( 'sidebar/properties/script' ), new SidebarScript( editor ) );
 	container.select( 'objectTab' );
 
+	function getTabByTabId( tabs, tabId ) {
+
+		return tabs.find( function ( tab ) {
+
+			return tab.dom.id === tabId;
+
+		} );
+
+	}
+
+	const geometryTab = getTabByTabId( container.tabs, 'geometryTab' );
+	const materialTab = getTabByTabId( container.tabs, 'materialTab' );
+	const scriptTab = getTabByTabId( container.tabs, 'scriptTab' );
+
+	function toggleTabs( object ) {
+
+		container.setHidden( object === null );
+
+		if ( object === null ) return;
+
+		geometryTab.setHidden( ! object.geometry );
+
+		materialTab.setHidden( ! object.material );
+
+		scriptTab.setHidden( object === editor.camera );
+
+		// set active tab
+
+		if ( container.selected === 'geometryTab' ) {
+
+			container.select( geometryTab.isHidden() ? 'objectTab' : 'geometryTab' );
+
+		} else if ( container.selected === 'materialTab' ) {
+
+			container.select( materialTab.isHidden() ? 'objectTab' : 'materialTab' );
+
+		} else if ( container.selected === 'scriptTab' ) {
+
+			container.select( scriptTab.isHidden() ? 'objectTab' : 'scriptTab' );
+
+		}
+
+	}
+
+	editor.signals.objectSelected.add( toggleTabs );
+
+	toggleTabs( editor.selected );
+
 	return container;
 
 }

+ 44 - 12
editor/js/Sidebar.Scene.js

@@ -119,13 +119,11 @@ function SidebarScene( editor ) {
 
 	function getScript( uuid ) {
 
-		if ( editor.scripts[ uuid ] !== undefined ) {
+		if ( editor.scripts[ uuid ] === undefined ) return '';
 
-			return ' <span class="type Script"></span>';
+		if ( editor.scripts[ uuid ].length === 0 ) return '';
 
-		}
-
-		return '';
+		return ' <span class="type Script"></span>';
 
 	}
 
@@ -195,7 +193,7 @@ function SidebarScene( editor ) {
 	const backgroundIntensity = new UINumber( 1 ).setWidth( '40px' ).setRange( 0, Infinity ).onChange( onBackgroundChanged );
 	backgroundEquirectRow.add( backgroundIntensity );
 
-	const backgroundRotation = new UINumber( 0 ).setWidth( '40px' ).setRange( -180, 180 ).setStep( 10 ).setNudge( 0.1 ).setUnit( '°' ).onChange( onBackgroundChanged );
+	const backgroundRotation = new UINumber( 0 ).setWidth( '40px' ).setRange( - 180, 180 ).setStep( 10 ).setNudge( 0.1 ).setUnit( '°' ).onChange( onBackgroundChanged );
 	backgroundEquirectRow.add( backgroundRotation );
 
 	container.add( backgroundEquirectRow );
@@ -419,12 +417,18 @@ function SidebarScene( editor ) {
 		} else {
 
 			backgroundType.setValue( 'None' );
+			backgroundTexture.setValue( null );
+			backgroundEquirectangularTexture.setValue( null );
 
 		}
 
 		if ( scene.environment ) {
 
-			if ( scene.environment.mapping === THREE.EquirectangularReflectionMapping ) {
+			if ( scene.background && scene.background.isTexture && scene.background.uuid === scene.environment.uuid ) {
+
+				environmentType.setValue( 'Background' );
+
+			} else if ( scene.environment.mapping === THREE.EquirectangularReflectionMapping ) {
 
 				environmentType.setValue( 'Equirectangular' );
 				environmentEquirectangularTexture.setValue( scene.environment );
@@ -438,6 +442,7 @@ function SidebarScene( editor ) {
 		} else {
 
 			environmentType.setValue( 'None' );
+			environmentEquirectangularTexture.setValue( null );
 
 		}
 
@@ -491,18 +496,22 @@ function SidebarScene( editor ) {
 
 	signals.refreshSidebarEnvironment.add( refreshUI );
 
-	/*
 	signals.objectChanged.add( function ( object ) {
 
-		let options = outliner.options;
+		const options = outliner.options;
 
 		for ( let i = 0; i < options.length; i ++ ) {
 
-			let option = options[ i ];
+			const option = options[ i ];
 
 			if ( option.value === object.id ) {
 
-				option.innerHTML = buildHTML( object );
+				const openerElement = option.querySelector( ':scope > .opener' );
+
+				const openerHTML = openerElement ? openerElement.outerHTML : '';
+
+				option.innerHTML = openerHTML + buildHTML( object );
+
 				return;
 
 			}
@@ -510,7 +519,19 @@ function SidebarScene( editor ) {
 		}
 
 	} );
-	*/
+
+	signals.scriptAdded.add( function () {
+
+		if ( editor.selected !== null ) signals.objectChanged.dispatch( editor.selected );
+
+	} );
+
+	signals.scriptRemoved.add( function () {
+
+		if ( editor.selected !== null ) signals.objectChanged.dispatch( editor.selected );
+
+	} );
+
 
 	signals.objectSelected.add( function ( object ) {
 
@@ -546,6 +567,17 @@ function SidebarScene( editor ) {
 
 	} );
 
+	signals.sceneBackgroundChanged.add( function () {
+
+		if ( environmentType.getValue() === 'Background' ) {
+
+			onEnvironmentChanged();
+			refreshEnvironmentUI();
+
+		}
+
+	} );
+
 	return container;
 
 }

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

@@ -81,7 +81,7 @@ function SidebarScript( editor ) {
 					remove.setMarginLeft( '4px' );
 					remove.onClick( function () {
 
-						if ( confirm( 'Are you sure?' ) ) {
+						if ( confirm( strings.getKey( 'prompt/script/remove' ) ) ) {
 
 							editor.execute( new RemoveScriptCommand( editor, editor.selected, script ) );
 

+ 2 - 2
editor/js/Sidebar.Settings.History.js

@@ -25,7 +25,7 @@ function SidebarSettingsHistory( editor ) {
 
 		if ( value ) {
 
-			alert( 'The history will be preserved across sessions.\nThis can have an impact on performance when working with textures.' );
+			alert( strings.getKey( 'prompt/history/preserve' ) );
 
 			const lastUndoCmd = history.undos[ history.undos.length - 1 ];
 			const lastUndoId = ( lastUndoCmd !== undefined ) ? lastUndoCmd.id : 0;
@@ -63,7 +63,7 @@ function SidebarSettingsHistory( editor ) {
 	const option = new UIButton( strings.getKey( 'sidebar/history/clear' ) );
 	option.onClick( function () {
 
-		if ( confirm( 'The Undo/Redo History will be cleared. Are you sure?' ) ) {
+		if ( confirm( strings.getKey( 'prompt/history/clear' ) ) ) {
 
 			editor.history.clear();
 

+ 11 - 1
editor/js/Sidebar.js

@@ -12,9 +12,11 @@ function Sidebar( editor ) {
 	const container = new UITabbedPanel();
 	container.setId( 'sidebar' );
 
+	const sidebarProperties = new SidebarProperties( editor );
+
 	const scene = new UISpan().add(
 		new SidebarScene( editor ),
-		new SidebarProperties( editor )
+		sidebarProperties
 	);
 	const project = new SidebarProject( editor );
 	const settings = new SidebarSettings( editor );
@@ -24,6 +26,14 @@ function Sidebar( editor ) {
 	container.addTab( 'settings', strings.getKey( 'sidebar/settings' ), settings );
 	container.select( 'scene' );
 
+	const sidebarPropertiesResizeObserver = new ResizeObserver( function () {
+
+		sidebarProperties.tabsDiv.setWidth( getComputedStyle( container.dom ).width );
+
+	} );
+
+	sidebarPropertiesResizeObserver.observe( container.tabsDiv.dom );
+
 	return container;
 
 }

+ 1 - 1
editor/js/Storage.js

@@ -50,7 +50,7 @@ function Storage() {
 
 		get: function ( callback ) {
 
-			const transaction = database.transaction( [ 'states' ], 'readwrite' );
+			const transaction = database.transaction( [ 'states' ], 'readonly' );
 			const objectStore = transaction.objectStore( 'states' );
 			const request = objectStore.get( 0 );
 			request.onsuccess = function ( event ) {

+ 360 - 176
editor/js/Strings.js

@@ -6,63 +6,98 @@ function Strings( config ) {
 
 		en: {
 
+			'prompt/file/open': 'Any unsaved data will be lost. Are you sure?',
+			'prompt/file/failedToOpenProject': 'Failed to open project!',
+			'prompt/file/export/noMeshSelected': 'No Mesh selected!',
+			'prompt/file/export/noObjectSelected': 'No Object selected!',
+			'prompt/script/remove': 'Are you sure?',
+			'prompt/history/clear': 'The Undo/Redo History will be cleared. Are you sure?',
+			'prompt/history/preserve': 'The history will be preserved across sessions.\nThis can have an impact on performance when working with textures.',
+			'prompt/history/forbid': 'Undo/Redo disabled while scene is playing.',
+
+			'command/AddObject': 'Add Object',
+			'command/AddScript': 'Add Script',
+			'command/MoveObject': 'Move Object',
+			'command/MultiCmds': 'Multiple Changes',
+			'command/RemoveObject': 'Remove Object',
+			'command/RemoveScript': 'Remove Script',
+			'command/SetColor': 'Set Color',
+			'command/SetGeometry': 'Set Geometry',
+			'command/SetGeometryValue': 'Set Geometry Value',
+			'command/SetMaterialColor': 'Set Material Color',
+			'command/SetMaterial': 'Set Material',
+			'command/SetMaterialMap': 'Set Material Map',
+			'command/SetMaterialRange': 'Set Material Range',
+			'command/SetMaterialValue': 'Set Material Value',
+			'command/SetMaterialVector': 'Set Material Vector',
+			'command/SetPosition': 'Set Position',
+			'command/SetRotation': 'Set Rotation',
+			'command/SetScale': 'Set Scale',
+			'command/SetScene': 'Set Scene',
+			'command/SetScriptValue': 'Set Script Value',
+			'command/SetUuid': 'Set UUID',
+			'command/SetValue': 'Set Value',
+
 			'menubar/file': 'File',
 			'menubar/file/new': 'New',
+			'menubar/file/new/empty': 'Empty',
+			'menubar/file/new/Arkanoid': 'Arkanoid',
+			'menubar/file/new/Camera': 'Camera',
+			'menubar/file/new/Particles': 'Particles',
+			'menubar/file/new/Pong': 'Pong',
+			'menubar/file/new/Shaders': 'Shaders',
+			'menubar/file/open': 'Open',
+			'menubar/file/save': 'Save',
 			'menubar/file/import': 'Import',
-			'menubar/file/export/drc': 'Export DRC',
-			'menubar/file/export/glb': 'Export GLB',
-			'menubar/file/export/gltf': 'Export GLTF',
-			'menubar/file/export/obj': 'Export OBJ',
-			'menubar/file/export/ply': 'Export PLY',
-			'menubar/file/export/ply_binary': 'Export PLY (Binary)',
-			'menubar/file/export/stl': 'Export STL',
-			'menubar/file/export/stl_binary': 'Export STL (Binary)',
-			'menubar/file/export/usdz': 'Export USDZ',
+			'menubar/file/export': 'Export',
 
 			'menubar/edit': 'Edit',
-			'menubar/edit/undo': 'Undo (Ctrl+Z)',
-			'menubar/edit/redo': 'Redo (Ctrl+Shift+Z)',
+			'menubar/edit/undo': 'Undo',
+			'menubar/edit/redo': 'Redo',
 			'menubar/edit/center': 'Center',
 			'menubar/edit/clone': 'Clone',
-			'menubar/edit/delete': 'Delete (Del)',
+			'menubar/edit/delete': 'Delete',
 
 			'menubar/add': 'Add',
 			'menubar/add/group': 'Group',
-			'menubar/add/plane': 'Plane',
-			'menubar/add/box': 'Box',
-			'menubar/add/capsule': 'Capsule',
-			'menubar/add/circle': 'Circle',
-			'menubar/add/cylinder': 'Cylinder',
-			'menubar/add/ring': 'Ring',
-			'menubar/add/sphere': 'Sphere',
-			'menubar/add/dodecahedron': 'Dodecahedron',
-			'menubar/add/icosahedron': 'Icosahedron',
-			'menubar/add/octahedron': 'Octahedron',
-			'menubar/add/tetrahedron': 'Tetrahedron',
-			'menubar/add/torus': 'Torus',
-			'menubar/add/tube': 'Tube',
-			'menubar/add/torusknot': 'TorusKnot',
-			'menubar/add/lathe': 'Lathe',
-			'menubar/add/sprite': 'Sprite',
-			'menubar/add/pointlight': 'PointLight',
-			'menubar/add/spotlight': 'SpotLight',
-			'menubar/add/directionallight': 'DirectionalLight',
-			'menubar/add/hemispherelight': 'HemisphereLight',
-			'menubar/add/ambientlight': 'AmbientLight',
-			'menubar/add/perspectivecamera': 'PerspectiveCamera',
-			'menubar/add/orthographiccamera': 'OrthographicCamera',
 
-			'menubar/status/autosave': 'autosave',
+			'menubar/add/mesh': 'Mesh',
+			'menubar/add/mesh/plane': 'Plane',
+			'menubar/add/mesh/box': 'Box',
+			'menubar/add/mesh/capsule': 'Capsule',
+			'menubar/add/mesh/circle': 'Circle',
+			'menubar/add/mesh/cylinder': 'Cylinder',
+			'menubar/add/mesh/ring': 'Ring',
+			'menubar/add/mesh/sphere': 'Sphere',
+			'menubar/add/mesh/dodecahedron': 'Dodecahedron',
+			'menubar/add/mesh/icosahedron': 'Icosahedron',
+			'menubar/add/mesh/octahedron': 'Octahedron',
+			'menubar/add/mesh/tetrahedron': 'Tetrahedron',
+			'menubar/add/mesh/torus': 'Torus',
+			'menubar/add/mesh/tube': 'Tube',
+			'menubar/add/mesh/torusknot': 'TorusKnot',
+			'menubar/add/mesh/lathe': 'Lathe',
+			'menubar/add/mesh/sprite': 'Sprite',
+
+			'menubar/add/light': 'Light',
+			'menubar/add/light/ambient': 'Ambient',
+			'menubar/add/light/directional': 'Directional',
+			'menubar/add/light/hemisphere': 'Hemisphere',
+			'menubar/add/light/point': 'Point',
+			'menubar/add/light/spot': 'Spot',
+
+			'menubar/add/camera': 'Camera',
+			'menubar/add/camera/perspective': 'Perspective',
+			'menubar/add/camera/orthographic': 'Orthographic',
 
-			'menubar/examples': 'Examples',
-			'menubar/examples/Arkanoid': 'Arkanoid',
-			'menubar/examples/Camera': 'Camera',
-			'menubar/examples/Particles': 'Particles',
-			'menubar/examples/Pong': 'Pong',
-			'menubar/examples/Shaders': 'Shaders',
+			'menubar/status/autosave': 'autosave',
 
 			'menubar/view': 'View',
 			'menubar/view/fullscreen': 'Fullscreen',
+			'menubar/view/gridHelper': 'Grid Helper',
+			'menubar/view/cameraHelpers': 'Camera Helpers',
+			'menubar/view/lightHelpers': 'Light Helpers',
+			'menubar/view/skeletonHelpers': 'Skeleton Helpers',
 
 			'menubar/help': 'Help',
 			'menubar/help/source_code': 'Source Code',
@@ -123,6 +158,7 @@ function Strings( config ) {
 			'sidebar/geometry/uuid': 'UUID',
 			'sidebar/geometry/name': 'Name',
 			'sidebar/geometry/bounds': 'Bounds',
+			'sidebar/geometry/userdata': 'User Data',
 			'sidebar/geometry/show_vertex_normals': 'Show Vertex Normals',
 			'sidebar/geometry/compute_vertex_normals': 'Compute Vertex Normals',
 			'sidebar/geometry/compute_vertex_tangents': 'Compute Tangents',
@@ -161,7 +197,7 @@ function Strings( config ) {
 			'sidebar/geometry/extrude_geometry/curveSegments': 'Curve Segments',
 			'sidebar/geometry/extrude_geometry/steps': 'Steps',
 			'sidebar/geometry/extrude_geometry/depth': 'Depth',
-			'sidebar/geometry/extrude_geometry/bevelEnabled': 'Bevel?',
+			'sidebar/geometry/extrude_geometry/bevelEnabled': 'Bevel',
 			'sidebar/geometry/extrude_geometry/bevelThickness': 'Thickness',
 			'sidebar/geometry/extrude_geometry/bevelSize': 'Size',
 			'sidebar/geometry/extrude_geometry/bevelOffset': 'Offset',
@@ -321,6 +357,7 @@ function Strings( config ) {
 			'sidebar/project/app/publish': 'Publish',
 
 			'sidebar/project/image': 'Image',
+			'sidebar/project/image/samples': 'Samples',
 			'sidebar/project/video': 'Video',
 
 			'sidebar/project/shading': 'Shading',
@@ -350,72 +387,116 @@ function Strings( config ) {
 			'viewport/controls/grid': 'Grid',
 			'viewport/controls/helpers': 'Helpers',
 
+			'viewport/info/object': 'Object',
 			'viewport/info/objects': 'Objects',
+			'viewport/info/vertex': 'Vertex',
 			'viewport/info/vertices': 'Vertices',
+			'viewport/info/triangle': 'Triangle',
 			'viewport/info/triangles': 'Triangles',
-			'viewport/info/rendertime': 'Render time'
+			'viewport/info/sample': 'Sample',
+			'viewport/info/samples': 'Samples',
+			'viewport/info/rendertime': 'Render time',
+
+			'script/title/vertexShader': 'Vertex Shader',
+			'script/title/fragmentShader': 'Fragment Shader',
+			'script/title/programInfo': 'Program Properties'
 
 		},
 
 		fr: {
 
+			'prompt/file/open': 'Toutes les données non enregistrées seront perdues Êtes-vous sûr ?',
+			'prompt/file/failedToOpenProject': 'Échec de l\'ouverture du projet !',
+			'prompt/file/export/noMeshSelected': 'Aucun maillage sélectionné !',
+			'prompt/file/export/noObjectSelected': 'Aucun objet sélectionné !',
+			'prompt/script/remove': 'Es-tu sûr?',
+			'prompt/history/clear': 'L\'historique d\'annulation/rétablissement sera effacé Êtes-vous sûr ?',
+			'prompt/history/preserve': 'L\'histoire sera conservée entre les sessions.\nCela peut avoir un impact sur les performances lors de la manipulation des textures.',
+			'prompt/history/forbid': 'Les fonctions Annuler/Rétablir sont désactivées pendant la lecture de la scène.',
+
+			'command/AddObject': 'Ajouter un objet',
+			'command/AddScript': 'Ajouter un script',
+			'command/MoveObject': 'Déplacer l’objet',
+			'command/MultiCmds': 'Changements multiples',
+			'command/RemoveObject': 'Supprimer l’objet',
+			'command/RemoveScript': 'Supprimer le script',
+			'command/SetColor': 'Définir la couleur',
+			'command/SetGeometry': 'Définir la géométrie',
+			'command/SetGeometryValue': 'Définir la valeur de la géométrie',
+			'command/SetMaterialColor': 'Définir la couleur du matériau',
+			'command/SetMaterial': 'Matériel de l’ensemble',
+			'command/SetMaterialMap': 'Définir la carte des matériaux',
+			'command/SetMaterialRange': 'Définir la gamme de matériaux',
+			'command/SetMaterialValue': 'Définir la valeur du matériau',
+			'command/SetMaterialVector': 'Définir le vecteur de matériau',
+			'command/SetPosition': 'Définir la position',
+			'command/SetRotation': 'Définir la rotation',
+			'command/SetScale': 'Définir l’échelle',
+			'command/SetScene': 'Planter le décor',
+			'command/SetScriptValue': 'Définir la valeur du script',
+			'command/SetUuid': 'Définir l’UUID',
+			'command/SetValue': 'Définir la valeur',
+
 			'menubar/file': 'Fichier',
 			'menubar/file/new': 'Nouveau',
+			'menubar/file/new/empty': 'Vide',
+			'menubar/file/new/Arkanoid': 'Arkanoid',
+			'menubar/file/new/Camera': 'Camera',
+			'menubar/file/new/Particles': 'Particles',
+			'menubar/file/new/Pong': 'Pong',
+			'menubar/file/new/Shaders': 'Shaders',
+			'menubar/file/open': 'Open',
+			'menubar/file/save': 'Save',
 			'menubar/file/import': 'Importer',
-			'menubar/file/export/drc': 'Exporter DRC',
-			'menubar/file/export/glb': 'Exporter GLB',
-			'menubar/file/export/gltf': 'Exporter GLTF',
-			'menubar/file/export/obj': 'Exporter OBJ',
-			'menubar/file/export/ply': 'Exporer PLY',
-			'menubar/file/export/ply_binary': 'Exporter PLY (Binaire)',
-			'menubar/file/export/stl': 'Exporter STL',
-			'menubar/file/export/stl_binary': 'Exporter STL (Binaire)',
-			'menubar/file/export/usdz': 'Exporter USDZ',
+			'menubar/file/export': 'Exporter',
 
 			'menubar/edit': 'Edition',
-			'menubar/edit/undo': 'Annuler (Ctrl+Z)',
-			'menubar/edit/redo': 'Refaire (Ctrl+Shift+Z)',
+			'menubar/edit/undo': 'Annuler',
+			'menubar/edit/redo': 'Refaire',
 			'menubar/edit/center': 'Center',
 			'menubar/edit/clone': 'Cloner',
-			'menubar/edit/delete': 'Supprimer (Supp)',
+			'menubar/edit/delete': 'Supprimer',
 
 			'menubar/add': 'Ajouter',
 			'menubar/add/group': 'Groupe',
-			'menubar/add/plane': 'Plan',
-			'menubar/add/box': 'Cube',
-			'menubar/add/capsule': 'Capsule',
-			'menubar/add/circle': 'Cercle',
-			'menubar/add/cylinder': 'Cylindre',
-			'menubar/add/ring': 'Bague',
-			'menubar/add/sphere': 'Sphère',
-			'menubar/add/dodecahedron': 'Dodécaèdre',
-			'menubar/add/icosahedron': 'Icosaèdre',
-			'menubar/add/octahedron': 'Octaèdre',
-			'menubar/add/tetrahedron': 'Tétraèdre',
-			'menubar/add/torus': 'Torus',
-			'menubar/add/tube': 'Tube',
-			'menubar/add/torusknot': 'Noeud Torus',
-			'menubar/add/lathe': 'Tour',
-			'menubar/add/sprite': 'Sprite',
-			'menubar/add/pointlight': 'Lumière ponctuelle',
-			'menubar/add/spotlight': 'Projecteur',
-			'menubar/add/directionallight': 'Lumière directionnelle',
-			'menubar/add/hemispherelight': 'Lumière hémisphérique',
-			'menubar/add/ambientlight': 'Lumière ambiante',
-			'menubar/add/perspectivecamera': 'Caméra perspective',
-			'menubar/add/orthographiccamera': 'Caméra orthographique',
 
-			'menubar/status/autosave': 'enregistrement automatique',
+			'menubar/add/mesh': 'Maille',
+			'menubar/add/mesh/plane': 'Plan',
+			'menubar/add/mesh/box': 'Cube',
+			'menubar/add/mesh/capsule': 'Capsule',
+			'menubar/add/mesh/circle': 'Cercle',
+			'menubar/add/mesh/cylinder': 'Cylindre',
+			'menubar/add/mesh/ring': 'Bague',
+			'menubar/add/mesh/sphere': 'Sphère',
+			'menubar/add/mesh/dodecahedron': 'Dodécaèdre',
+			'menubar/add/mesh/icosahedron': 'Icosaèdre',
+			'menubar/add/mesh/octahedron': 'Octaèdre',
+			'menubar/add/mesh/tetrahedron': 'Tétraèdre',
+			'menubar/add/mesh/torus': 'Torus',
+			'menubar/add/mesh/tube': 'Tube',
+			'menubar/add/mesh/torusknot': 'Noeud Torus',
+			'menubar/add/mesh/lathe': 'Tour',
+			'menubar/add/mesh/sprite': 'Sprite',
+
+			'menubar/add/light': 'Lumière',
+			'menubar/add/light/ambient': 'Ambiante',
+			'menubar/add/light/directional': 'Directionnelle',
+			'menubar/add/light/hemisphere': 'Hémisphérique',
+			'menubar/add/light/point': 'Ponctuelle',
+			'menubar/add/light/spot': 'Projecteur',
+
+			'menubar/add/camera': 'Caméra',
+			'menubar/add/camera/perspective': 'Perspective',
+			'menubar/add/camera/orthographic': 'Orthographique',
 
-			'menubar/examples': 'Exemples',
-			'menubar/examples/Arkanoid': 'Arkanoid',
-			'menubar/examples/Camera': 'Camera',
-			'menubar/examples/Particles': 'Particles',
-			'menubar/examples/Pong': 'Pong',
-			'menubar/examples/Shaders': 'Shaders',
+			'menubar/status/autosave': 'enregistrement automatique',
 
 			'menubar/view': 'View',
 			'menubar/view/fullscreen': 'Fullscreen',
+			'menubar/view/gridHelper': 'Assistant de grille',
+			'menubar/view/cameraHelpers': 'Aides à la caméra',
+			'menubar/view/lightHelpers': 'Aides Lumière',
+			'menubar/view/skeletonHelpers': 'Aides squelettes',
 
 			'menubar/help': 'Aide',
 			'menubar/help/source_code': 'Code Source',
@@ -476,6 +557,7 @@ function Strings( config ) {
 			'sidebar/geometry/uuid': 'UUID',
 			'sidebar/geometry/name': 'Nom',
 			'sidebar/geometry/bounds': 'Limites',
+			'sidebar/geometry/userdata': 'Données utilisateur',
 			'sidebar/geometry/show_vertex_normals': 'Afficher normales',
 			'sidebar/geometry/compute_vertex_normals': 'Compute Vertex Normals',
 			'sidebar/geometry/compute_vertex_tangents': 'Compute Tangents',
@@ -674,6 +756,7 @@ function Strings( config ) {
 			'sidebar/project/app/publish': 'Publier',
 
 			'sidebar/project/image': 'Image',
+			'sidebar/project/image/samples': 'd\'échantillons',
 			'sidebar/project/video': 'Video',
 
 			'sidebar/project/shading': 'Shading',
@@ -703,72 +786,116 @@ function Strings( config ) {
 			'viewport/controls/grid': 'Grille',
 			'viewport/controls/helpers': 'Helpers',
 
+			'viewport/info/object': 'Objet',
 			'viewport/info/objects': 'Objets',
+			'viewport/info/vertex': 'Sommet',
 			'viewport/info/vertices': 'Sommets',
+			'viewport/info/triangle': 'Triangle',
 			'viewport/info/triangles': 'Triangles',
-			'viewport/info/rendertime': 'Render time'
+			'viewport/info/sample': 'Échantillon',
+			'viewport/info/samples': 'Échantillons',
+			'viewport/info/rendertime': 'Temps de rendu',
+
+			'script/title/vertexShader': 'Vertex Shader',
+			'script/title/fragmentShader': 'Fragment Shader',
+			'script/title/programInfo': 'Propriétés du programme'
 
 		},
 
 		zh: {
 
+			'prompt/file/open': '您确定吗?未保存的数据将会丢失。',
+			'prompt/file/failedToOpenProject': '无法打开项目!',
+			'prompt/file/export/noMeshSelected': '未选择网格!',
+			'prompt/file/export/noObjectSelected': '未选择对象!',
+			'prompt/script/remove': '你确定吗?',
+			'prompt/history/clear': '撤销/重做历史记录将被清除。您确定吗?',
+			'prompt/history/preserve': '历史将在会话之间保留。\n这可能会影响在处理纹理时的性能。',
+			'prompt/history/forbid': '在播放场景时,撤消/重做被禁用。',
+
+			'command/AddObject': '添加对象',
+			'command/AddScript': '添加脚本',
+			'command/MoveObject': '移动对象',
+			'command/MultiCmds': '多次更改',
+			'command/RemoveObject': '删除对象',
+			'command/RemoveScript': '删除脚本',
+			'command/SetColor': '设置颜色',
+			'command/SetGeometry': '设置几何图形',
+			'command/SetGeometryValue': '设置几何值',
+			'command/SetMaterialColor': '设置材质颜色',
+			'command/SetMaterial': '设置材质',
+			'command/SetMaterialMap': '设置材质贴图',
+			'command/SetMaterialRange': '设置材料范围',
+			'command/SetMaterialValue': '设置材料值',
+			'command/SetMaterialVector': '设置材质矢量',
+			'command/SetPosition': '设置位置',
+			'command/SetRotation': '设置旋转',
+			'command/SetScale': '设置比例',
+			'command/SetScene': '设置布景',
+			'command/SetScriptValue': '设置脚本值',
+			'command/SetUuid': '设置 UUID',
+			'command/SetValue': '设定值',
+
 			'menubar/file': '文件',
-			'menubar/file/new': '新建',
+			'menubar/file/new': '新建项目',
+			'menubar/file/new/empty': '空',
+			'menubar/file/new/Arkanoid': '打砖块',
+			'menubar/file/new/Camera': ' 摄像机',
+			'menubar/file/new/Particles': '粒子',
+			'menubar/file/new/Pong': '乒乓球',
+			'menubar/file/new/Shaders': '着色器',
+			'menubar/file/open': '打开',
+			'menubar/file/save': '保存',
 			'menubar/file/import': '导入',
-			'menubar/file/export/drc': '导出DRC',
-			'menubar/file/export/glb': '导出GLB',
-			'menubar/file/export/gltf': '导出GLTF',
-			'menubar/file/export/obj': '导出OBJ',
-			'menubar/file/export/ply': '导出PLY',
-			'menubar/file/export/ply_binary': '导出PLY(二进制)',
-			'menubar/file/export/stl': '导出STL',
-			'menubar/file/export/stl_binary': '导出STL(二进制)',
-			'menubar/file/export/usdz': '导出USDZ',
+			'menubar/file/export': '导出',
 
 			'menubar/edit': '编辑',
-			'menubar/edit/undo': '撤销 (Ctrl+Z)',
-			'menubar/edit/redo': '重做 (Ctrl+Shift+Z)',
+			'menubar/edit/undo': '撤销',
+			'menubar/edit/redo': '重做',
 			'menubar/edit/center': '居中',
 			'menubar/edit/clone': '拷贝',
-			'menubar/edit/delete': '删除 (Del)',
+			'menubar/edit/delete': '删除',
 
 			'menubar/add': '添加',
 			'menubar/add/group': '组',
-			'menubar/add/plane': '平面',
-			'menubar/add/box': '正方体',
-			'menubar/add/capsule': '胶囊',
-			'menubar/add/circle': '圆',
-			'menubar/add/cylinder': '圆柱体',
-			'menubar/add/ring': '环',
-			'menubar/add/sphere': '球体',
-			'menubar/add/dodecahedron': '十二面体',
-			'menubar/add/icosahedron': '二十面体',
-			'menubar/add/octahedron': '八面体',
-			'menubar/add/tetrahedron': '四面体',
-			'menubar/add/torus': '圆环体',
-			'menubar/add/torusknot': '环面纽结体',
-			'menubar/add/tube': '管',
-			'menubar/add/lathe': '酒杯',
-			'menubar/add/sprite': '精灵',
-			'menubar/add/pointlight': '点光源',
-			'menubar/add/spotlight': '聚光灯',
-			'menubar/add/directionallight': '平行光',
-			'menubar/add/hemispherelight': '半球光',
-			'menubar/add/ambientlight': '环境光',
-			'menubar/add/perspectivecamera': '透视相机',
-			'menubar/add/orthographiccamera': '正交相机',
 
-			'menubar/status/autosave': '自动保存',
+			'menubar/add/mesh': '网格',
+			'menubar/add/mesh/plane': '平面',
+			'menubar/add/mesh/box': '正方体',
+			'menubar/add/mesh/capsule': '胶囊',
+			'menubar/add/mesh/circle': '圆',
+			'menubar/add/mesh/cylinder': '圆柱体',
+			'menubar/add/mesh/ring': '环',
+			'menubar/add/mesh/sphere': '球体',
+			'menubar/add/mesh/dodecahedron': '十二面体',
+			'menubar/add/mesh/icosahedron': '二十面体',
+			'menubar/add/mesh/octahedron': '八面体',
+			'menubar/add/mesh/tetrahedron': '四面体',
+			'menubar/add/mesh/torus': '圆环体',
+			'menubar/add/mesh/torusknot': '环面纽结体',
+			'menubar/add/mesh/tube': '管',
+			'menubar/add/mesh/lathe': '酒杯',
+			'menubar/add/mesh/sprite': '精灵',
+
+			'menubar/add/light': '光源',
+			'menubar/add/light/ambient': '环境光',
+			'menubar/add/light/directional': '平行光',
+			'menubar/add/light/hemisphere': '半球光',
+			'menubar/add/light/point': '点光源',
+			'menubar/add/light/spot': '聚光灯',
+
+			'menubar/add/camera': '摄像机',
+			'menubar/add/camera/perspective': '透视相机',
+			'menubar/add/camera/orthographic': '正交相机',
 
-			'menubar/examples': '示例',
-			'menubar/examples/Arkanoid': '打砖块',
-			'menubar/examples/Camera': ' 摄像机',
-			'menubar/examples/Particles': '粒子',
-			'menubar/examples/Pong': '乒乓球',
-			'menubar/examples/Shaders': '着色器',
+			'menubar/status/autosave': '自动保存',
 
 			'menubar/view': '视图',
 			'menubar/view/fullscreen': '全屏',
+			'menubar/view/gridHelper': '网格助手',
+			'menubar/view/cameraHelpers': '相机助手',
+			'menubar/view/lightHelpers': '光助手',
+			'menubar/view/skeletonHelpers': '骷髅助手',
 
 			'menubar/help': '帮助',
 			'menubar/help/source_code': '源码',
@@ -829,6 +956,7 @@ function Strings( config ) {
 			'sidebar/geometry/uuid': '识别码',
 			'sidebar/geometry/name': '名称',
 			'sidebar/geometry/bounds': '界限',
+			'sidebar/geometry/userdata': '自定义数据',
 			'sidebar/geometry/show_vertex_normals': '显示顶点法线',
 			'sidebar/geometry/compute_vertex_normals': '计算顶点法线',
 			'sidebar/geometry/compute_vertex_tangents': 'Compute Tangents',
@@ -1027,6 +1155,7 @@ function Strings( config ) {
 			'sidebar/project/app/publish': '发布',
 
 			'sidebar/project/image': 'Image',
+			'sidebar/project/image/samples': '样本',
 			'sidebar/project/video': '视频',
 
 			'sidebar/project/shading': 'Shading',
@@ -1056,72 +1185,116 @@ function Strings( config ) {
 			'viewport/controls/grid': '网格',
 			'viewport/controls/helpers': '辅助',
 
+			'viewport/info/object': '物体',
 			'viewport/info/objects': '物体',
+			'viewport/info/vertex': '顶点',
 			'viewport/info/vertices': '顶点',
+			'viewport/info/triangle': '三角形',
 			'viewport/info/triangles': '三角形',
-			'viewport/info/rendertime': 'Render time'
+			'viewport/info/sample': '样本',
+			'viewport/info/samples': '样本',
+			'viewport/info/rendertime': '渲染时间',
+
+			'script/title/vertexShader': '顶点着色器',
+			'script/title/fragmentShader': '片段着色器',
+			'script/title/programInfo': '程序属性'
 
 		},
 
 		ja: {
 
+			'prompt/file/open': '保存されていないデータは失われます。 本気ですか?',
+			'prompt/file/failedToOpenProject': 'プロジェクトを開くことができませんでした!',
+			'prompt/file/export/noMeshSelected': 'メッシュが選択されていません!',
+			'prompt/file/export/noObjectSelected': 'オブジェクトが選択されていません!',
+			'prompt/script/remove': '本気ですか?',
+			'prompt/history/clear': '元に戻す/やり直しの履歴が消去されます。 本気ですか?',
+			'prompt/history/preserve': '履歴はセッションをまたいで保存されます。\nこれは、テクスチャを操作する際のパフォーマンスに影響を与える可能性があります。',
+			'prompt/history/forbid': 'シーンの再生中は元に戻す/やり直しは無効になります。',
+
+			'command/AddObject': 'オブジェクトを追加',
+			'command/AddScript': 'スクリプトを追加',
+			'command/MoveObject': 'オブジェクトの移動',
+			'command/MultiCmds': '複数の変更',
+			'command/RemoveObject': 'オブジェクトを削除',
+			'command/RemoveScript': 'スクリプトの削除',
+			'command/SetColor': 'カラーを設定',
+			'command/SetGeometry': 'ジオメトリの設定',
+			'command/SetGeometryValue': 'ジオメトリ値の設定',
+			'command/SetMaterialColor': 'マテリアル カラーの設定',
+			'command/SetMaterial': 'マテリアルの設定',
+			'command/SetMaterialMap': 'マテリアル マップの設定',
+			'command/SetMaterialRange': 'マテリアル範囲の設定',
+			'command/SetMaterialValue': 'マテリアル値の設定',
+			'command/SetMaterialVector': '素材のベクトルを設定します',
+			'command/SetPosition': '位置を設定',
+			'command/SetRotation': '回転を設定',
+			'command/SetScale': 'スケールを設定',
+			'command/SetScene': 'セットシーン',
+			'command/SetScriptValue': 'スクリプト値の設定',
+			'command/SetUuid': 'UUIDの設定',
+			'command/SetValue': '値の設定',
+
 			'menubar/file': 'ファイル',
-			'menubar/file/new': '新規',
+			'menubar/file/new': '新規プロジェクト',
+			'menubar/file/new/empty': '空',
+			'menubar/file/new/Arkanoid': 'ブロック崩し',
+			'menubar/file/new/Camera': 'カメラ',
+			'menubar/file/new/Particles': 'パーティクル',
+			'menubar/file/new/Pong': 'ピンポン',
+			'menubar/file/new/Shaders': 'シェーダー',
+			'menubar/file/open': '開く',
+			'menubar/file/save': '保存',
 			'menubar/file/import': 'インポート',
-			'menubar/file/export/drc': 'エクスポート DRC',
-			'menubar/file/export/glb': 'エクスポート GLB',
-			'menubar/file/export/gltf': 'エクスポート GLTF',
-			'menubar/file/export/obj': 'エクスポート OBJ',
-			'menubar/file/export/ply': 'エクスポート PLY',
-			'menubar/file/export/ply_binary': 'エクスポート PLY(バイナリ)',
-			'menubar/file/export/stl': 'エクスポート STL',
-			'menubar/file/export/stl_binary': 'エクスポート STL(バイナリ)',
-			'menubar/file/export/usdz': 'エクスポート USDZ',
+			'menubar/file/export': 'エクスポート',
 
 			'menubar/edit': '編集',
-			'menubar/edit/undo': '元に戻す(Ctrl+Z)',
-			'menubar/edit/redo': 'やり直す(Ctrl+Shift+Z)',
+			'menubar/edit/undo': '元に戻す',
+			'menubar/edit/redo': 'やり直す',
 			'menubar/edit/center': '中央揃え',
 			'menubar/edit/clone': '複製',
-			'menubar/edit/delete': '削除(Del)',
+			'menubar/edit/delete': '削除',
 
 			'menubar/add': '追加',
 			'menubar/add/group': 'グループ',
-			'menubar/add/plane': '平面',
-			'menubar/add/box': '直方体',
-			'menubar/add/capsule': 'カプセル',
-			'menubar/add/circle': '円',
-			'menubar/add/cylinder': '円柱',
-			'menubar/add/ring': 'リング',
-			'menubar/add/sphere': '球',
-			'menubar/add/dodecahedron': '十二面体',
-			'menubar/add/icosahedron': '二十面体',
-			'menubar/add/octahedron': '八面体',
-			'menubar/add/tetrahedron': '四面体',
-			'menubar/add/torus': 'トーラス',
-			'menubar/add/tube': 'チューブ',
-			'menubar/add/torusknot': 'ノットトーラス',
-			'menubar/add/lathe': '旋盤形',
-			'menubar/add/sprite': 'スプライト',
-			'menubar/add/pointlight': 'ポイントライト',
-			'menubar/add/spotlight': 'スポットライト',
-			'menubar/add/directionallight': 'ディレクショナルライト',
-			'menubar/add/hemispherelight': 'ヘミスフィアライト',
-			'menubar/add/ambientlight': 'アンビエントライト',
-			'menubar/add/perspectivecamera': '透視投影カメラ',
-			'menubar/add/orthographiccamera': '平行投影カメラ',
 
-			'menubar/status/autosave': '自動保存',
+			'menubar/add/mesh': 'メッシュ',
+			'menubar/add/mesh/plane': '平面',
+			'menubar/add/mesh/box': '直方体',
+			'menubar/add/mesh/capsule': 'カプセル',
+			'menubar/add/mesh/circle': '円',
+			'menubar/add/mesh/cylinder': '円柱',
+			'menubar/add/mesh/ring': 'リング',
+			'menubar/add/mesh/sphere': '球',
+			'menubar/add/mesh/dodecahedron': '十二面体',
+			'menubar/add/mesh/icosahedron': '二十面体',
+			'menubar/add/mesh/octahedron': '八面体',
+			'menubar/add/mesh/tetrahedron': '四面体',
+			'menubar/add/mesh/torus': 'トーラス',
+			'menubar/add/mesh/tube': 'チューブ',
+			'menubar/add/mesh/torusknot': 'ノットトーラス',
+			'menubar/add/mesh/lathe': '旋盤形',
+			'menubar/add/mesh/sprite': 'スプライト',
+
+			'menubar/add/light': 'ライト',
+			'menubar/add/light/ambient': 'アンビエント',
+			'menubar/add/light/directional': 'ディレクショナル',
+			'menubar/add/light/hemisphere': 'ヘミスフィア',
+			'menubar/add/light/point': 'ポイント',
+			'menubar/add/light/spot': 'スポット',
+
+			'menubar/add/camera': 'カメラ',
+			'menubar/add/camera/perspective': '透視投影',
+			'menubar/add/camera/orthographic': '平行投影',
 
-			'menubar/examples': 'サンプル',
-			'menubar/examples/Arkanoid': 'ブロック崩し',
-			'menubar/examples/Camera': 'カメラ',
-			'menubar/examples/Particles': 'パーティクル',
-			'menubar/examples/Pong': 'ピンポン',
-			'menubar/examples/Shaders': 'シェーダー',
+			'menubar/status/autosave': '自動保存',
 
 			'menubar/view': '表示',
 			'menubar/view/fullscreen': 'フルスクリーン',
+			'menubar/view/gridHelper': 'グリッドヘルパー',
+			'menubar/view/cameraHelpers': 'カメラヘルパー',
+			'menubar/view/lightHelpers': 'ライトヘルパー',
+			'menubar/view/skeletonHelpers': 'スケルトンヘルパー',
 
 			'menubar/help': 'ヘルプ',
 			'menubar/help/source_code': 'ソースコード',
@@ -1182,6 +1355,7 @@ function Strings( config ) {
 			'sidebar/geometry/uuid': 'UUID',
 			'sidebar/geometry/name': '名前',
 			'sidebar/geometry/bounds': '境界',
+			'sidebar/geometry/userdata': 'ユーザーデータ',
 			'sidebar/geometry/show_vertex_normals': '頂点法線を表示',
 			'sidebar/geometry/compute_vertex_normals': '頂点法線を計算',
 			'sidebar/geometry/compute_vertex_tangents': '接線を計算',
@@ -1220,7 +1394,7 @@ function Strings( config ) {
 			'sidebar/geometry/extrude_geometry/curveSegments': '分割数',
 			'sidebar/geometry/extrude_geometry/steps': 'ステップ',
 			'sidebar/geometry/extrude_geometry/depth': '深さ',
-			'sidebar/geometry/extrude_geometry/bevelEnabled': 'ベベルを有効にするか',
+			'sidebar/geometry/extrude_geometry/bevelEnabled': 'ベベルを有効にするか',
 			'sidebar/geometry/extrude_geometry/bevelThickness': 'ベベルの厚さ',
 			'sidebar/geometry/extrude_geometry/bevelSize': 'ベベルのサイズ',
 			'sidebar/geometry/extrude_geometry/bevelOffset': 'ベベルのオフセット',
@@ -1380,6 +1554,7 @@ function Strings( config ) {
 			'sidebar/project/app/publish': 'アプリファイルとして保存',
 
 			'sidebar/project/image': '画像',
+			'sidebar/project/image/samples': 'サンプル',
 			'sidebar/project/video': '動画',
 
 			'sidebar/project/shading': 'シェーディング',
@@ -1409,10 +1584,19 @@ function Strings( config ) {
 			'viewport/controls/grid': 'グリッド',
 			'viewport/controls/helpers': 'オーバーレイ表示',
 
+			'viewport/info/object': 'オブジェクト',
 			'viewport/info/objects': 'オブジェクト',
+			'viewport/info/vertex': '頂点',
 			'viewport/info/vertices': '頂点',
+			'viewport/info/triangle': '三角形',
 			'viewport/info/triangles': '三角形',
-			'viewport/info/rendertime': 'レンダリング時間'
+			'viewport/info/sample': 'サンプル',
+			'viewport/info/samples': 'サンプル',
+			'viewport/info/rendertime': 'レンダリング時間',
+
+			'script/title/vertexShader': '頂点シェーダー',
+			'script/title/fragmentShader': 'フラグメントシェーダ',
+			'script/title/programInfo': 'プログラムのプロパティ'
 
 		}
 

+ 20 - 23
editor/js/Viewport.Controls.js

@@ -1,10 +1,8 @@
 import { UIPanel, UISelect } from './libs/ui.js';
-import { UIBoolean } from './libs/ui.three.js';
 
 function ViewportControls( editor ) {
 
 	const signals = editor.signals;
-	const strings = editor.strings;
 
 	const container = new UIPanel();
 	container.setPosition( 'absolute' );
@@ -12,26 +10,6 @@ function ViewportControls( editor ) {
 	container.setTop( '10px' );
 	container.setColor( '#ffffff' );
 
-	// grid
-
-	const gridCheckbox = new UIBoolean( true, strings.getKey( 'viewport/controls/grid' ) );
-	gridCheckbox.onChange( function () {
-
-		signals.showGridChanged.dispatch( this.getValue() );
-
-	} );
-	container.add( gridCheckbox );
-
-	// helpers
-
-	const helpersCheckbox = new UIBoolean( true, strings.getKey( 'viewport/controls/helpers' ) );
-	helpersCheckbox.onChange( function () {
-
-		signals.showHelpersChanged.dispatch( this.getValue() );
-
-	} );
-	container.add( helpersCheckbox );
-
 	// camera
 
 	const cameraSelect = new UISelect();
@@ -46,6 +24,15 @@ function ViewportControls( editor ) {
 
 	signals.cameraAdded.add( update );
 	signals.cameraRemoved.add( update );
+	signals.objectChanged.add( function ( object ) {
+
+		if ( object.isCamera ) {
+
+			update();
+
+		}
+
+	} );
 
 	// shading
 
@@ -61,11 +48,15 @@ function ViewportControls( editor ) {
 
 	signals.editorCleared.add( function () {
 
+		editor.setViewportCamera( editor.camera.uuid );
+
 		shadingSelect.setValue( 'solid' );
 		editor.setViewportShading( shadingSelect.getValue() );
 
 	} );
 
+	signals.cameraResetted.add( update );
+
 	update();
 
 	//
@@ -84,7 +75,13 @@ function ViewportControls( editor ) {
 		}
 
 		cameraSelect.setOptions( options );
-		cameraSelect.setValue( editor.viewportCamera.uuid );
+
+		const selectedCamera = ( editor.viewportCamera.uuid in options )
+			? editor.viewportCamera
+			: editor.camera;
+
+		cameraSelect.setValue( selectedCamera.uuid );
+		editor.setViewportCamera( selectedCamera.uuid );
 
 	}
 

+ 53 - 7
editor/js/Viewport.Info.js

@@ -14,15 +14,22 @@ function ViewportInfo( editor ) {
 	container.setColor( '#fff' );
 	container.setTextTransform( 'lowercase' );
 
-	const objectsText  = new UIText( '0' ).setTextAlign( 'right' ).setWidth( '60px' ).setMarginRight( '6px' );
+	const objectsText = new UIText( '0' ).setTextAlign( 'right' ).setWidth( '60px' ).setMarginRight( '6px' );
 	const verticesText = new UIText( '0' ).setTextAlign( 'right' ).setWidth( '60px' ).setMarginRight( '6px' );
 	const trianglesText = new UIText( '0' ).setTextAlign( 'right' ).setWidth( '60px' ).setMarginRight( '6px' );
 	const frametimeText = new UIText( '0' ).setTextAlign( 'right' ).setWidth( '60px' ).setMarginRight( '6px' );
+	const samplesText = new UIText( '0' ).setTextAlign( 'right' ).setWidth( '60px' ).setMarginRight( '6px' ).setHidden( true );
 
-	container.add( objectsText, new UIText( strings.getKey( 'viewport/info/objects' ) ), new UIBreak() );
-	container.add( verticesText, new UIText( strings.getKey( 'viewport/info/vertices' ) ), new UIBreak() );
-	container.add( trianglesText, new UIText( strings.getKey( 'viewport/info/triangles' ) ), new UIBreak() );
+	const objectsUnitText = new UIText( strings.getKey( 'viewport/info/objects' ) );
+	const verticesUnitText = new UIText( strings.getKey( 'viewport/info/vertices' ) );
+	const trianglesUnitText = new UIText( strings.getKey( 'viewport/info/triangles' ) );
+	const samplesUnitText = new UIText( strings.getKey( 'viewport/info/samples' ) ).setHidden( true );
+
+	container.add( objectsText, objectsUnitText, new UIBreak() );
+	container.add( verticesText, verticesUnitText, new UIBreak() );
+	container.add( trianglesText, trianglesUnitText, new UIBreak() );
 	container.add( frametimeText, new UIText( strings.getKey( 'viewport/info/rendertime' ) ), new UIBreak() );
+	container.add( samplesText, samplesUnitText, new UIBreak() );
 
 	signals.objectAdded.add( update );
 	signals.objectRemoved.add( update );
@@ -31,6 +38,10 @@ function ViewportInfo( editor ) {
 
 	//
 
+	const pluralRules = new Intl.PluralRules( editor.config.getKey( 'language' ) );
+
+	//
+
 	function update() {
 
 		const scene = editor.scene;
@@ -71,9 +82,20 @@ function ViewportInfo( editor ) {
 
 		}
 
-		objectsText.setValue( objects.format() );
-		verticesText.setValue( vertices.format() );
-		trianglesText.setValue( triangles.format() );
+		objectsText.setValue( editor.utils.formatNumber( objects ) );
+		verticesText.setValue( editor.utils.formatNumber( vertices ) );
+		trianglesText.setValue( editor.utils.formatNumber( triangles ) );
+
+		const pluralRules = new Intl.PluralRules( editor.config.getKey( 'language' ) );
+
+		const objectsStringKey = ( pluralRules.select( objects ) === 'one' ) ? 'viewport/info/object' : 'viewport/info/objects';
+		objectsUnitText.setValue( strings.getKey( objectsStringKey ) );
+
+		const verticesStringKey = ( pluralRules.select( vertices ) === 'one' ) ? 'viewport/info/vertex' : 'viewport/info/vertices';
+		verticesUnitText.setValue( strings.getKey( verticesStringKey ) );
+
+		const trianglesStringKey = ( pluralRules.select( triangles ) === 'one' ) ? 'viewport/info/triangle' : 'viewport/info/triangles';
+		trianglesUnitText.setValue( strings.getKey( trianglesStringKey ) );
 
 	}
 
@@ -83,6 +105,30 @@ function ViewportInfo( editor ) {
 
 	}
 
+	//
+
+	editor.signals.pathTracerUpdated.add( function ( samples ) {
+
+		samples = Math.floor( samples );
+
+		samplesText.setValue( samples );
+
+		const samplesStringKey = ( pluralRules.select( samples ) === 'one' ) ? 'viewport/info/sample' : 'viewport/info/samples';
+		samplesUnitText.setValue( strings.getKey( samplesStringKey ) );
+
+	} );
+
+	editor.signals.viewportShadingChanged.add( function () {
+
+		const isRealisticShading = ( editor.viewportShading === 'realistic' );
+
+		samplesText.setHidden( ! isRealisticShading );
+		samplesUnitText.setHidden( ! isRealisticShading );
+
+		container.setBottom( isRealisticShading ? '32px' : '20px' );
+
+	} );
+
 	return container;
 
 }

+ 34 - 124
editor/js/Viewport.Pathtracer.js

@@ -1,169 +1,77 @@
-import * as THREE from 'three';
-import { FullScreenQuad } from 'three/examples/jsm/postprocessing/Pass.js';
-import {
-	PathTracingSceneGenerator,
-	PathTracingRenderer,
-	PhysicalPathTracingMaterial,
-	ProceduralEquirectTexture,
-} from 'three-gpu-pathtracer';
-
-function buildColorTexture( color ) {
-
-	const texture = new ProceduralEquirectTexture( 4, 4 );
-	texture.generationCallback = ( polar, uv, coord, target ) => {
-
-		target.copy( color );
-
-	};
-
-	texture.update();
-
-	return texture;
-
-}
+import { WebGLPathTracer } from 'three-gpu-pathtracer';
 
 function ViewportPathtracer( renderer ) {
 
-	let generator = null;
-	let pathtracer = null;
-	let quad = null;
-	let hdr = null;
+	let pathTracer = null;
 
 	function init( scene, camera ) {
 
-		if ( pathtracer === null ) {
-
-			generator = new PathTracingSceneGenerator();
+		if ( pathTracer === null ) {
 
-			pathtracer = new PathTracingRenderer( renderer );
-			pathtracer.setSize( renderer.domElement.offsetWidth, renderer.domElement.offsetHeight );
-			pathtracer.alpha = true;
-			pathtracer.camera = camera;
-			pathtracer.material = new PhysicalPathTracingMaterial();
-			pathtracer.tiles.set( 3, 4 );
-
-			quad = new FullScreenQuad( new THREE.MeshBasicMaterial( {
-				map: pathtracer.target.texture,
-				blending: THREE.CustomBlending
-			} ) );
+			pathTracer = new WebGLPathTracer( renderer );
+			pathTracer.filterGlossyFactor = 0.5;
 
 		}
 
-		pathtracer.reset();
-
-		const { bvh, textures, materials, lights } = generator.generate( scene );
-
-		const ptGeometry = bvh.geometry;
-		const ptMaterial = pathtracer.material;
-
-		ptMaterial.bvh.updateFrom( bvh );
-		ptMaterial.attributesArray.updateFrom(
-			ptGeometry.attributes.normal,
-			ptGeometry.attributes.tangent,
-			ptGeometry.attributes.uv,
-			ptGeometry.attributes.color,
-		);
-		ptMaterial.materialIndexAttribute.updateFrom( ptGeometry.attributes.materialIndex );
-		ptMaterial.textures.setTextures( renderer, 2048, 2048, textures );
-		ptMaterial.materials.updateFrom( materials, textures );
-		ptMaterial.lights.updateFrom( lights );
-		ptMaterial.filterGlossyFactor = 0.5;
-
-		//
-
-		setBackground( scene.background, scene.backgroundBlurriness );
-		setEnvironment( scene.environment );
+		pathTracer.setScene( scene, camera );
 
 	}
 
-	function setSize( width, height ) {
+	function setSize( /* width, height */ ) {
 
-		if ( pathtracer === null ) return;
+		if ( pathTracer === null ) return;
 
-		pathtracer.setSize( width, height );
-		pathtracer.reset();
+		// path tracer size automatically updates based on the canvas
+		pathTracer.updateCamera();
 
 	}
 
-	function setBackground( background, blurriness ) {
-
-		if ( pathtracer === null ) return;
-
-		const ptMaterial = pathtracer.material;
-
-		if ( background ) {
-
-			if ( background.isTexture ) {
+	function setBackground( /* background, blurriness */ ) {
 
-				ptMaterial.backgroundMap = background;
-				ptMaterial.backgroundBlur = blurriness;
+		if ( pathTracer === null ) return;
 
-			} else if ( background.isColor ) {
-
-				ptMaterial.backgroundMap = buildColorTexture( background );
-				ptMaterial.backgroundBlur = 0;
-
-			}
-
-		} else {
-
-			ptMaterial.backgroundMap = buildColorTexture( new THREE.Color( 0 ) );
-			ptMaterial.backgroundBlur = 0;
-
-		}
-
-		pathtracer.reset();
+		// update environment settings based on initialized scene fields
+		pathTracer.updateEnvironment();
 
 	}
 
-	function setEnvironment( environment ) {
-
-		if ( pathtracer === null ) return;
-
-		const ptMaterial = pathtracer.material;
-
-		if ( environment && environment.isDataTexture === true ) {
+	function updateMaterials() {
 
-			// Avoid calling envMapInfo() with the same hdr
+		if ( pathTracer === null ) return;
 
-			if ( environment !== hdr ) {
+		pathTracer.updateMaterials();
 
-				ptMaterial.envMapInfo.updateFrom( environment );
-				hdr = environment;
-
-			}
+	}
 
-		} else {
+	function setEnvironment( /* environment */ ) {
 
-			ptMaterial.envMapInfo.updateFrom( buildColorTexture( new THREE.Color( 0 ) ) );
+		if ( pathTracer === null ) return;
 
-		}
-
-		pathtracer.reset();
+		pathTracer.updateEnvironment();
 
 	}
 
 	function update() {
 
-		if ( pathtracer === null ) return;
+		if ( pathTracer === null ) return;
 
-		pathtracer.update();
+		pathTracer.renderSample();
 
-		if ( pathtracer.samples >= 1 ) {
+	}
 
-			renderer.autoClear = false;
-			quad.render( renderer );
-			renderer.autoClear = true;
+	function reset() {
 
-		}
+		if ( pathTracer === null ) return;
+
+		pathTracer.updateCamera();
 
 	}
 
-	function reset() {
+	function getSamples() {
 
-		if ( pathtracer === null ) return;
+		if ( pathTracer === null ) return;
 
-		pathtracer.reset();
+		return pathTracer.samples;
 
 	}
 
@@ -172,8 +80,10 @@ function ViewportPathtracer( renderer ) {
 		setSize: setSize,
 		setBackground: setBackground,
 		setEnvironment: setEnvironment,
+		updateMaterials: updateMaterials,
 		update: update,
-		reset: reset
+		reset: reset,
+		getSamples: getSamples
 	};
 
 }

+ 94 - 22
editor/js/Viewport.js

@@ -152,8 +152,29 @@ function Viewport( editor ) {
 
 	function updateAspectRatio() {
 
-		camera.aspect = container.dom.offsetWidth / container.dom.offsetHeight;
-		camera.updateProjectionMatrix();
+		for ( const uuid in editor.cameras ) {
+
+			const camera = editor.cameras[ uuid ];
+
+			const aspect = container.dom.offsetWidth / container.dom.offsetHeight;
+
+			if ( camera.isPerspectiveCamera ) {
+
+				camera.aspect = aspect;
+
+			} else {
+
+				camera.left = - aspect;
+				camera.right = aspect;
+
+			}
+
+			camera.updateProjectionMatrix();
+
+			const cameraHelper = editor.helpers[ camera.id ];
+			if ( cameraHelper ) cameraHelper.update();
+
+		}
 
 	}
 
@@ -292,6 +313,8 @@ function Viewport( editor ) {
 
 		transformControls.setSpace( space );
 
+		render();
+
 	} );
 
 	signals.rendererUpdated.add( function () {
@@ -462,7 +485,7 @@ function Viewport( editor ) {
 
 	signals.materialChanged.add( function () {
 
-		initPT();
+		updatePTMaterials();
 		render();
 
 	} );
@@ -538,9 +561,13 @@ function Viewport( editor ) {
 
 				useBackgroundAsEnvironment = true;
 
-				scene.environment = scene.background;
-				scene.environment.mapping = THREE.EquirectangularReflectionMapping;
-				scene.environmentRotation.y = scene.backgroundRotation.y;
+				if ( scene.background !== null && scene.background.isTexture ) {
+
+					scene.environment = scene.background;
+					scene.environment.mapping = THREE.EquirectangularReflectionMapping;
+					scene.environmentRotation.y = scene.backgroundRotation.y;
+
+				}
 
 				break;
 
@@ -614,14 +641,9 @@ function Viewport( editor ) {
 
 		const viewportCamera = editor.viewportCamera;
 
-		if ( viewportCamera.isPerspectiveCamera ) {
+		if ( viewportCamera.isPerspectiveCamera || viewportCamera.isOrthographicCamera ) {
 
-			viewportCamera.aspect = editor.camera.aspect;
-			viewportCamera.projectionMatrix.copy( editor.camera.projectionMatrix );
-
-		} else if ( viewportCamera.isOrthographicCamera ) {
-
-			// TODO
+			updateAspectRatio();
 
 		}
 
@@ -629,6 +651,7 @@ function Viewport( editor ) {
 
 		controls.enabled = ( viewportCamera === editor.camera );
 
+		initPT();
 		render();
 
 	} );
@@ -640,7 +663,7 @@ function Viewport( editor ) {
 		switch ( viewportShading ) {
 
 			case 'realistic':
-				pathtracer.init( scene, camera );
+				pathtracer.init( scene, editor.viewportCamera );
 				break;
 
 			case 'solid':
@@ -674,18 +697,56 @@ function Viewport( editor ) {
 
 	} );
 
-	signals.showGridChanged.add( function ( value ) {
+	signals.showHelpersChanged.add( function ( appearanceStates ) {
 
-		grid.visible = value;
+		grid.visible = appearanceStates.gridHelper;
 
-		render();
+		sceneHelpers.traverse( function ( object ) {
 
-	} );
+			switch ( object.type ) {
+
+				case 'CameraHelper':
+
+				{
+
+					object.visible = appearanceStates.cameraHelpers;
+					break;
+
+				}
+
+				case 'PointLightHelper':
+				case 'DirectionalLightHelper':
+				case 'SpotLightHelper':
+				case 'HemisphereLightHelper':
+
+				{
+
+					object.visible = appearanceStates.lightHelpers;
+					break;
+
+				}
 
-	signals.showHelpersChanged.add( function ( value ) {
+				case 'SkeletonHelper':
+
+				{
+
+					object.visible = appearanceStates.skeletonHelpers;
+					break;
+
+				}
+
+				default:
+
+				{
+
+					// not a helper, skip.
+
+				}
+
+			}
+
+		} );
 
-		sceneHelpers.visible = value;
-		transformControls.enabled = value;
 
 		render();
 
@@ -751,7 +812,7 @@ function Viewport( editor ) {
 
 		if ( editor.viewportShading === 'realistic' ) {
 
-			pathtracer.init( scene, camera );
+			pathtracer.init( scene, editor.viewportCamera );
 
 		}
 
@@ -777,11 +838,22 @@ function Viewport( editor ) {
 
 	}
 
+	function updatePTMaterials() {
+
+		if ( editor.viewportShading === 'realistic' ) {
+
+			pathtracer.updateMaterials();
+
+		}
+
+	}
+
 	function updatePT() {
 
 		if ( editor.viewportShading === 'realistic' ) {
 
 			pathtracer.update();
+			editor.signals.pathTracerUpdated.dispatch( pathtracer.getSamples() );
 
 		}
 

+ 4 - 3
editor/js/commands/AddObjectCommand.js

@@ -8,16 +8,17 @@ import { ObjectLoader } from 'three';
  */
 class AddObjectCommand extends Command {
 
-	constructor( editor, object ) {
+	constructor( editor, object = null ) {
 
 		super( editor );
 
 		this.type = 'AddObjectCommand';
 
 		this.object = object;
-		if ( object !== undefined ) {
 
-			this.name = `Add Object: ${object.name}`;
+		if ( object !== null ) {
+
+			this.name = editor.strings.getKey( 'command/AddObject' ) + ': ' + object.name;
 
 		}
 

+ 2 - 2
editor/js/commands/AddScriptCommand.js

@@ -8,12 +8,12 @@ import { Command } from '../Command.js';
  */
 class AddScriptCommand extends Command {
 
-	constructor( editor, object, script ) {
+	constructor( editor, object = null, script = '' ) {
 
 		super( editor );
 
 		this.type = 'AddScriptCommand';
-		this.name = 'Add Script';
+		this.name = editor.strings.getKey( 'command/AddScript' );
 
 		this.object = object;
 		this.script = script;

+ 1 - 0
editor/js/commands/Commands.js

@@ -10,6 +10,7 @@ export { SetGeometryValueCommand } from './SetGeometryValueCommand.js';
 export { SetMaterialColorCommand } from './SetMaterialColorCommand.js';
 export { SetMaterialCommand } from './SetMaterialCommand.js';
 export { SetMaterialMapCommand } from './SetMaterialMapCommand.js';
+export { SetMaterialRangeCommand } from './SetMaterialRangeCommand.js';
 export { SetMaterialValueCommand } from './SetMaterialValueCommand.js';
 export { SetMaterialVectorCommand } from './SetMaterialVectorCommand.js';
 export { SetPositionCommand } from './SetPositionCommand.js';

+ 7 - 7
editor/js/commands/MoveObjectCommand.js

@@ -9,25 +9,25 @@ import { Command } from '../Command.js';
  */
 class MoveObjectCommand extends Command {
 
-	constructor( editor, object, newParent, newBefore ) {
+	constructor( editor, object = null, newParent = null, newBefore = null ) {
 
 		super( editor );
 
 		this.type = 'MoveObjectCommand';
-		this.name = 'Move Object';
+		this.name = editor.strings.getKey( 'command/MoveObject' );
 
 		this.object = object;
-		this.oldParent = ( object !== undefined ) ? object.parent : undefined;
-		this.oldIndex = ( this.oldParent !== undefined ) ? this.oldParent.children.indexOf( this.object ) : undefined;
+		this.oldParent = ( object !== null ) ? object.parent : null;
+		this.oldIndex = ( this.oldParent !== null ) ? this.oldParent.children.indexOf( this.object ) : null;
 		this.newParent = newParent;
 
-		if ( newBefore !== undefined ) {
+		if ( newBefore !== null ) {
 
-			this.newIndex = ( newParent !== undefined ) ? newParent.children.indexOf( newBefore ) : undefined;
+			this.newIndex = ( newParent !== null ) ? newParent.children.indexOf( newBefore ) : null;
 
 		} else {
 
-			this.newIndex = ( newParent !== undefined ) ? newParent.children.length : undefined;
+			this.newIndex = ( newParent !== null ) ? newParent.children.length : null;
 
 		}
 

+ 3 - 3
editor/js/commands/MultiCmdsCommand.js

@@ -7,14 +7,14 @@ import { Command } from '../Command.js';
  */
 class MultiCmdsCommand extends Command {
 
-	constructor( editor, cmdArray ) {
+	constructor( editor, cmdArray = [] ) {
 
 		super( editor );
 
 		this.type = 'MultiCmdsCommand';
-		this.name = 'Multiple Changes';
+		this.name = editor.strings.getKey( 'command/MultiCmds' );
 
-		this.cmdArray = ( cmdArray !== undefined ) ? cmdArray : [];
+		this.cmdArray = cmdArray;
 
 	}
 

+ 11 - 4
editor/js/commands/RemoveObjectCommand.js

@@ -9,21 +9,28 @@ import { ObjectLoader } from 'three';
  */
 class RemoveObjectCommand extends Command {
 
-	constructor( editor, object ) {
+	constructor( editor, object = null ) {
 
 		super( editor );
 
 		this.type = 'RemoveObjectCommand';
-		this.name = 'Remove Object';
 
 		this.object = object;
-		this.parent = ( object !== undefined ) ? object.parent : undefined;
-		if ( this.parent !== undefined ) {
+		this.parent = ( object !== null ) ? object.parent : null;
+
+		if ( this.parent !== null ) {
 
 			this.index = this.parent.children.indexOf( this.object );
 
 		}
 
+		if ( object !== null ) {
+
+			this.name = editor.strings.getKey( 'command/RemoveObject' ) + ': ' + object.name;
+
+
+		}
+
 	}
 
 	execute() {

+ 4 - 3
editor/js/commands/RemoveScriptCommand.js

@@ -8,16 +8,17 @@ import { Command } from '../Command.js';
  */
 class RemoveScriptCommand extends Command {
 
-	constructor( editor, object, script ) {
+	constructor( editor, object = null, script = '' ) {
 
 		super( editor );
 
 		this.type = 'RemoveScriptCommand';
-		this.name = 'Remove Script';
+		this.name = editor.strings.getKey( 'command/RemoveScript' );
 
 		this.object = object;
 		this.script = script;
-		if ( this.object && this.script ) {
+
+		if ( this.object !== null && this.script !== '' ) {
 
 			this.index = this.editor.scripts[ this.object.uuid ].indexOf( this.script );
 

+ 3 - 3
editor/js/commands/SetColorCommand.js

@@ -9,17 +9,17 @@ import { Command } from '../Command.js';
  */
 class SetColorCommand extends Command {
 
-	constructor( editor, object, attributeName, newValue ) {
+	constructor( editor, object = null, attributeName = '', newValue = null ) {
 
 		super( editor );
 
 		this.type = 'SetColorCommand';
-		this.name = `Set ${attributeName}`;
+		this.name = editor.strings.getKey( 'command/SetColor' ) + ': ' + attributeName;
 		this.updatable = true;
 
 		this.object = object;
 		this.attributeName = attributeName;
-		this.oldValue = ( object !== undefined ) ? this.object[ this.attributeName ].getHex() : undefined;
+		this.oldValue = ( object !== null ) ? this.object[ this.attributeName ].getHex() : null;
 		this.newValue = newValue;
 
 	}

+ 4 - 4
editor/js/commands/SetGeometryCommand.js

@@ -10,16 +10,16 @@ import { ObjectLoader } from 'three';
 
 class SetGeometryCommand extends Command {
 
-	constructor( editor, object, newGeometry ) {
+	constructor( editor, object = null, newGeometry = null ) {
 
 		super( editor );
 
 		this.type = 'SetGeometryCommand';
-		this.name = 'Set Geometry';
+		this.name = editor.strings.getKey( 'command/SetGeometry' );
 		this.updatable = true;
 
 		this.object = object;
-		this.oldGeometry = ( object !== undefined ) ? object.geometry : undefined;
+		this.oldGeometry = ( object !== null ) ? object.geometry : null;
 		this.newGeometry = newGeometry;
 
 	}
@@ -57,7 +57,7 @@ class SetGeometryCommand extends Command {
 		const output = super.toJSON( this );
 
 		output.objectUuid = this.object.uuid;
-		output.oldGeometry = this.object.geometry.toJSON();
+		output.oldGeometry = this.oldGeometry.toJSON();
 		output.newGeometry = this.newGeometry.toJSON();
 
 		return output;

+ 3 - 3
editor/js/commands/SetGeometryValueCommand.js

@@ -9,16 +9,16 @@ import { Command } from '../Command.js';
  */
 class SetGeometryValueCommand extends Command {
 
-	constructor( editor, object, attributeName, newValue ) {
+	constructor( editor, object = null, attributeName = '', newValue = null ) {
 
 		super( editor );
 
 		this.type = 'SetGeometryValueCommand';
-		this.name = `Set Geometry.${attributeName}`;
+		this.name = editor.strings.getKey( 'command/SetGeometryValue' ) + ': ' + attributeName;
 
 		this.object = object;
 		this.attributeName = attributeName;
-		this.oldValue = ( object !== undefined ) ? object.geometry[ attributeName ] : undefined;
+		this.oldValue = ( object !== null ) ? object.geometry[ attributeName ] : null;
 		this.newValue = newValue;
 
 	}

+ 12 - 6
editor/js/commands/SetMaterialColorCommand.js

@@ -9,20 +9,20 @@ import { Command } from '../Command.js';
  */
 class SetMaterialColorCommand extends Command {
 
-	constructor( editor, object, attributeName, newValue, materialSlot ) {
+	constructor( editor, object = null, attributeName = '', newValue = null, materialSlot = - 1 ) {
 
 		super( editor );
 
 		this.type = 'SetMaterialColorCommand';
-		this.name = `Set Material.${attributeName}`;
+		this.name = editor.strings.getKey( 'command/SetMaterialColor' ) + ': ' + attributeName;
 		this.updatable = true;
 
 		this.object = object;
 		this.materialSlot = materialSlot;
 
-		this.material = ( this.object !== undefined ) ? this.editor.getObjectMaterial( object, materialSlot ) : undefined;
+		const material = ( object !== null ) ? editor.getObjectMaterial( object, materialSlot ) : null;
 
-		this.oldValue = ( this.material !== undefined ) ? this.material[ attributeName ].getHex() : undefined;
+		this.oldValue = ( material !== null ) ? material[ attributeName ].getHex() : null;
 		this.newValue = newValue;
 
 		this.attributeName = attributeName;
@@ -31,7 +31,9 @@ class SetMaterialColorCommand extends Command {
 
 	execute() {
 
-		this.material[ this.attributeName ].setHex( this.newValue );
+		const material = this.editor.getObjectMaterial( this.object, this.materialSlot );
+
+		material[ this.attributeName ].setHex( this.newValue );
 
 		this.editor.signals.materialChanged.dispatch( this.object, this.materialSlot );
 
@@ -39,7 +41,9 @@ class SetMaterialColorCommand extends Command {
 
 	undo() {
 
-		this.material[ this.attributeName ].setHex( this.oldValue );
+		const material = this.editor.getObjectMaterial( this.object, this.materialSlot );
+
+		material[ this.attributeName ].setHex( this.oldValue );
 
 		this.editor.signals.materialChanged.dispatch( this.object, this.materialSlot );
 
@@ -59,6 +63,7 @@ class SetMaterialColorCommand extends Command {
 		output.attributeName = this.attributeName;
 		output.oldValue = this.oldValue;
 		output.newValue = this.newValue;
+		output.materialSlot = this.materialSlot;
 
 		return output;
 
@@ -72,6 +77,7 @@ class SetMaterialColorCommand extends Command {
 		this.attributeName = json.attributeName;
 		this.oldValue = json.oldValue;
 		this.newValue = json.newValue;
+		this.materialSlot = json.materialSlot;
 
 	}
 

+ 5 - 3
editor/js/commands/SetMaterialCommand.js

@@ -9,17 +9,17 @@ import { ObjectLoader } from 'three';
  */
 class SetMaterialCommand extends Command {
 
-	constructor( editor, object, newMaterial, materialSlot ) {
+	constructor( editor, object = null, newMaterial = null, materialSlot = - 1 ) {
 
 		super( editor );
 
 		this.type = 'SetMaterialCommand';
-		this.name = 'New Material';
+		this.name = editor.strings.getKey( 'command/SetMaterial' );
 
 		this.object = object;
 		this.materialSlot = materialSlot;
 
-		this.oldMaterial = this.editor.getObjectMaterial( object, materialSlot );
+		this.oldMaterial = ( object !== null ) ? editor.getObjectMaterial( object, materialSlot ) : null;
 		this.newMaterial = newMaterial;
 
 	}
@@ -47,6 +47,7 @@ class SetMaterialCommand extends Command {
 		output.objectUuid = this.object.uuid;
 		output.oldMaterial = this.oldMaterial.toJSON();
 		output.newMaterial = this.newMaterial.toJSON();
+		output.materialSlot = this.materialSlot;
 
 		return output;
 
@@ -59,6 +60,7 @@ class SetMaterialCommand extends Command {
 		this.object = this.editor.objectByUuid( json.objectUuid );
 		this.oldMaterial = parseMaterial( json.oldMaterial );
 		this.newMaterial = parseMaterial( json.newMaterial );
+		this.materialSlot = json.materialSlot;
 
 		function parseMaterial( json ) {
 

+ 14 - 8
editor/js/commands/SetMaterialMapCommand.js

@@ -10,19 +10,19 @@ import { ObjectLoader } from 'three';
  */
 class SetMaterialMapCommand extends Command {
 
-	constructor( editor, object, mapName, newMap, materialSlot ) {
+	constructor( editor, object = null, mapName = '', newMap = null, materialSlot = - 1 ) {
 
 		super( editor );
 
 		this.type = 'SetMaterialMapCommand';
-		this.name = `Set Material.${mapName}`;
+		this.name = editor.strings.getKey( 'command/SetMaterialMap' ) + ': ' + mapName;
 
 		this.object = object;
 		this.materialSlot = materialSlot;
 
-		this.material = this.editor.getObjectMaterial( object, materialSlot );
+		const material = ( object !== null ) ? editor.getObjectMaterial( object, materialSlot ) : null;
 
-		this.oldMap = ( object !== undefined ) ? this.material[ mapName ] : undefined;
+		this.oldMap = ( object !== null ) ? material[ mapName ] : undefined;
 		this.newMap = newMap;
 
 		this.mapName = mapName;
@@ -33,8 +33,10 @@ class SetMaterialMapCommand extends Command {
 
 		if ( this.oldMap !== null && this.oldMap !== undefined ) this.oldMap.dispose();
 
-		this.material[ this.mapName ] = this.newMap;
-		this.material.needsUpdate = true;
+		const material = this.editor.getObjectMaterial( this.object, this.materialSlot );
+
+		material[ this.mapName ] = this.newMap;
+		material.needsUpdate = true;
 
 		this.editor.signals.materialChanged.dispatch( this.object, this.materialSlot );
 
@@ -42,8 +44,10 @@ class SetMaterialMapCommand extends Command {
 
 	undo() {
 
-		this.material[ this.mapName ] = this.oldMap;
-		this.material.needsUpdate = true;
+		const material = this.editor.getObjectMaterial( this.object, this.materialSlot );
+
+		material[ this.mapName ] = this.oldMap;
+		material.needsUpdate = true;
 
 		this.editor.signals.materialChanged.dispatch( this.object, this.materialSlot );
 
@@ -57,6 +61,7 @@ class SetMaterialMapCommand extends Command {
 		output.mapName = this.mapName;
 		output.newMap = serializeMap( this.newMap );
 		output.oldMap = serializeMap( this.oldMap );
+		output.materialSlot = this.materialSlot;
 
 		return output;
 
@@ -112,6 +117,7 @@ class SetMaterialMapCommand extends Command {
 		this.mapName = json.mapName;
 		this.oldMap = parseTexture( json.oldMap );
 		this.newMap = parseTexture( json.newMap );
+		this.materialSlot = json.materialSlot;
 
 		function parseTexture( json ) {
 

+ 14 - 8
editor/js/commands/SetMaterialRangeCommand.js

@@ -10,20 +10,20 @@ import { Command } from '../Command.js';
  */
 class SetMaterialRangeCommand extends Command {
 
-	constructor( editor, object, attributeName, newMinValue, newMaxValue, materialSlot ) {
+	constructor( editor, object = null, attributeName = '', newMinValue = - Infinity, newMaxValue = Infinity, materialSlot = - 1 ) {
 
 		super( editor );
 
 		this.type = 'SetMaterialRangeCommand';
-		this.name = `Set Material.${attributeName}`;
+		this.name = editor.strings.getKey( 'command/SetMaterialRange' ) + ': ' + attributeName;
 		this.updatable = true;
 
 		this.object = object;
 		this.materialSlot = materialSlot;
 
-		this.material = this.editor.getObjectMaterial( object, materialSlot );
+		const material = ( object !== null ) ? editor.getObjectMaterial( object, materialSlot ) : null;
 
-		this.oldRange = ( this.material !== undefined && this.material[ attributeName ] !== undefined ) ? [ ...this.material[ attributeName ] ] : undefined;
+		this.oldRange = ( material !== null && material[ attributeName ] !== undefined ) ? [ ...this.material[ attributeName ] ] : null;
 		this.newRange = [ newMinValue, newMaxValue ];
 
 		this.attributeName = attributeName;
@@ -32,8 +32,10 @@ class SetMaterialRangeCommand extends Command {
 
 	execute() {
 
-		this.material[ this.attributeName ] = [ ...this.newRange ];
-		this.material.needsUpdate = true;
+		const material = this.editor.getObjectMaterial( this.object, this.materialSlot );
+
+		material[ this.attributeName ] = [ ...this.newRange ];
+		material.needsUpdate = true;
 
 		this.editor.signals.objectChanged.dispatch( this.object );
 		this.editor.signals.materialChanged.dispatch( this.object, this.materialSlot );
@@ -42,8 +44,10 @@ class SetMaterialRangeCommand extends Command {
 
 	undo() {
 
-		this.material[ this.attributeName ] = [ ...this.oldRange ];
-		this.material.needsUpdate = true;
+		const material = this.editor.getObjectMaterial( this.object, this.materialSlot );
+
+		material[ this.attributeName ] = [ ...this.oldRange ];
+		material.needsUpdate = true;
 
 		this.editor.signals.objectChanged.dispatch( this.object );
 		this.editor.signals.materialChanged.dispatch( this.object, this.materialSlot );
@@ -64,6 +68,7 @@ class SetMaterialRangeCommand extends Command {
 		output.attributeName = this.attributeName;
 		output.oldRange = [ ...this.oldRange ];
 		output.newRange = [ ...this.newRange ];
+		output.materialSlot = this.materialSlot;
 
 		return output;
 
@@ -77,6 +82,7 @@ class SetMaterialRangeCommand extends Command {
 		this.oldRange = [ ...json.oldRange ];
 		this.newRange = [ ...json.newRange ];
 		this.object = this.editor.objectByUuid( json.objectUuid );
+		this.materialSlot = json.materialSlot;
 
 	}
 

+ 14 - 8
editor/js/commands/SetMaterialValueCommand.js

@@ -9,20 +9,20 @@ import { Command } from '../Command.js';
  */
 class SetMaterialValueCommand extends Command {
 
-	constructor( editor, object, attributeName, newValue, materialSlot ) {
+	constructor( editor, object = null, attributeName = '', newValue = null, materialSlot = - 1 ) {
 
 		super( editor );
 
 		this.type = 'SetMaterialValueCommand';
-		this.name = `Set Material.${attributeName}`;
+		this.name = editor.strings.getKey( 'command/SetMaterialValue' ) + ': ' + attributeName;
 		this.updatable = true;
 
 		this.object = object;
 		this.materialSlot = materialSlot;
 
-		this.material = this.editor.getObjectMaterial( object, materialSlot );
+		const material = ( object !== null ) ? editor.getObjectMaterial( object, materialSlot ) : null;
 
-		this.oldValue = ( this.material !== undefined ) ? this.material[ attributeName ] : undefined;
+		this.oldValue = ( material !== null ) ? material[ attributeName ] : null;
 		this.newValue = newValue;
 
 		this.attributeName = attributeName;
@@ -31,8 +31,10 @@ class SetMaterialValueCommand extends Command {
 
 	execute() {
 
-		this.material[ this.attributeName ] = this.newValue;
-		this.material.needsUpdate = true;
+		const material = this.editor.getObjectMaterial( this.object, this.materialSlot );
+
+		material[ this.attributeName ] = this.newValue;
+		material.needsUpdate = true;
 
 		this.editor.signals.objectChanged.dispatch( this.object );
 		this.editor.signals.materialChanged.dispatch( this.object, this.materialSlot );
@@ -41,8 +43,10 @@ class SetMaterialValueCommand extends Command {
 
 	undo() {
 
-		this.material[ this.attributeName ] = this.oldValue;
-		this.material.needsUpdate = true;
+		const material = this.editor.getObjectMaterial( this.object, this.materialSlot );
+
+		material[ this.attributeName ] = this.oldValue;
+		material.needsUpdate = true;
 
 		this.editor.signals.objectChanged.dispatch( this.object );
 		this.editor.signals.materialChanged.dispatch( this.object, this.materialSlot );
@@ -63,6 +67,7 @@ class SetMaterialValueCommand extends Command {
 		output.attributeName = this.attributeName;
 		output.oldValue = this.oldValue;
 		output.newValue = this.newValue;
+		output.materialSlot = this.materialSlot;
 
 		return output;
 
@@ -76,6 +81,7 @@ class SetMaterialValueCommand extends Command {
 		this.oldValue = json.oldValue;
 		this.newValue = json.newValue;
 		this.object = this.editor.objectByUuid( json.objectUuid );
+		this.materialSlot = json.materialSlot;
 
 	}
 

+ 13 - 7
editor/js/commands/SetMaterialVectorCommand.js

@@ -2,20 +2,20 @@ import { Command } from '../Command.js';
 
 class SetMaterialVectorCommand extends Command {
 
-	constructor( editor, object, attributeName, newValue, materialSlot ) {
+	constructor( editor, object = null, attributeName = '', newValue = null, materialSlot = - 1 ) {
 
 		super( editor );
 
-		this.type = 'SetMaterialColorCommand';
-		this.name = `Set Material.${attributeName}`;
+		this.type = 'SetMaterialVectorCommand';
+		this.name = editor.strings.getKey( 'command/SetMaterialVector' ) + ': ' + attributeName;
 		this.updatable = true;
 
 		this.object = object;
 		this.materialSlot = materialSlot;
 
-		this.material = this.editor.getObjectMaterial( object, materialSlot );
+		const material = ( object !== null ) ? editor.getObjectMaterial( object, materialSlot ) : null;
 
-		this.oldValue = ( this.material !== undefined ) ? this.material[ attributeName ].toArray() : undefined;
+		this.oldValue = ( material !== null ) ? material[ attributeName ].toArray() : null;
 		this.newValue = newValue;
 
 		this.attributeName = attributeName;
@@ -24,7 +24,9 @@ class SetMaterialVectorCommand extends Command {
 
 	execute() {
 
-		this.material[ this.attributeName ].fromArray( this.newValue );
+		const material = this.editor.getObjectMaterial( this.object, this.materialSlot );
+
+		material[ this.attributeName ].fromArray( this.newValue );
 
 		this.editor.signals.materialChanged.dispatch( this.object, this.materialSlot );
 
@@ -32,7 +34,9 @@ class SetMaterialVectorCommand extends Command {
 
 	undo() {
 
-		this.material[ this.attributeName ].fromArray( this.oldValue );
+		const material = this.editor.getObjectMaterial( this.object, this.materialSlot );
+
+		material[ this.attributeName ].fromArray( this.oldValue );
 
 		this.editor.signals.materialChanged.dispatch( this.object, this.materialSlot );
 
@@ -52,6 +56,7 @@ class SetMaterialVectorCommand extends Command {
 		output.attributeName = this.attributeName;
 		output.oldValue = this.oldValue;
 		output.newValue = this.newValue;
+		output.materialSlot = this.materialSlot;
 
 		return output;
 
@@ -65,6 +70,7 @@ class SetMaterialVectorCommand extends Command {
 		this.attributeName = json.attributeName;
 		this.oldValue = json.oldValue;
 		this.newValue = json.newValue;
+		this.materialSlot = json.materialSlot;
 
 	}
 

+ 4 - 4
editor/js/commands/SetPositionCommand.js

@@ -10,24 +10,24 @@ import { Vector3 } from 'three';
  */
 class SetPositionCommand extends Command {
 
-	constructor( editor, object, newPosition, optionalOldPosition ) {
+	constructor( editor, object = null, newPosition = null, optionalOldPosition = null ) {
 
 		super( editor );
 
 		this.type = 'SetPositionCommand';
-		this.name = 'Set Position';
+		this.name = editor.strings.getKey( 'command/SetPosition' );
 		this.updatable = true;
 
 		this.object = object;
 
-		if ( object !== undefined && newPosition !== undefined ) {
+		if ( object !== null && newPosition !== null ) {
 
 			this.oldPosition = object.position.clone();
 			this.newPosition = newPosition.clone();
 
 		}
 
-		if ( optionalOldPosition !== undefined ) {
+		if ( optionalOldPosition !== null ) {
 
 			this.oldPosition = optionalOldPosition.clone();
 

+ 4 - 4
editor/js/commands/SetRotationCommand.js

@@ -10,24 +10,24 @@ import { Euler } from 'three';
  */
 class SetRotationCommand extends Command {
 
-	constructor( editor, object, newRotation, optionalOldRotation ) {
+	constructor( editor, object = null, newRotation = null, optionalOldRotation = null ) {
 
 		super( editor );
 
 		this.type = 'SetRotationCommand';
-		this.name = 'Set Rotation';
+		this.name = editor.strings.getKey( 'command/SetRotation' );
 		this.updatable = true;
 
 		this.object = object;
 
-		if ( object !== undefined && newRotation !== undefined ) {
+		if ( object !== null && newRotation !== null ) {
 
 			this.oldRotation = object.rotation.clone();
 			this.newRotation = newRotation.clone();
 
 		}
 
-		if ( optionalOldRotation !== undefined ) {
+		if ( optionalOldRotation !== null ) {
 
 			this.oldRotation = optionalOldRotation.clone();
 

+ 4 - 4
editor/js/commands/SetScaleCommand.js

@@ -10,24 +10,24 @@ import { Vector3 } from 'three';
  */
 class SetScaleCommand extends Command {
 
-	constructor( editor, object, newScale, optionalOldScale ) {
+	constructor( editor, object = null, newScale = null, optionalOldScale = null ) {
 
 		super( editor );
 
 		this.type = 'SetScaleCommand';
-		this.name = 'Set Scale';
+		this.name = editor.strings.getKey( 'command/SetScale' );
 		this.updatable = true;
 
 		this.object = object;
 
-		if ( object !== undefined && newScale !== undefined ) {
+		if ( object !== null && newScale !== null ) {
 
 			this.oldScale = object.scale.clone();
 			this.newScale = newScale.clone();
 
 		}
 
-		if ( optionalOldScale !== undefined ) {
+		if ( optionalOldScale !== null ) {
 
 			this.oldScale = optionalOldScale.clone();
 

+ 3 - 3
editor/js/commands/SetSceneCommand.js

@@ -10,16 +10,16 @@ import { AddObjectCommand } from './AddObjectCommand.js';
  */
 class SetSceneCommand extends Command {
 
-	constructor( editor, scene ) {
+	constructor( editor, scene = null ) {
 
 		super( editor );
 
 		this.type = 'SetSceneCommand';
-		this.name = 'Set Scene';
+		this.name = editor.strings.getKey( 'command/SetScene' );
 
 		this.cmdArray = [];
 
-		if ( scene !== undefined ) {
+		if ( scene !== null ) {
 
 			this.cmdArray.push( new SetUuidCommand( this.editor, this.editor.scene, scene.uuid ) );
 			this.cmdArray.push( new SetValueCommand( this.editor, this.editor.scene, 'name', scene.name ) );

+ 5 - 5
editor/js/commands/SetScriptValueCommand.js

@@ -10,19 +10,19 @@ import { Command } from '../Command.js';
  */
 class SetScriptValueCommand extends Command {
 
-	constructor( editor, object, script, attributeName, newValue ) {
+	constructor( editor, object = null, script = '', attributeName = '', newValue = null ) {
 
 		super( editor );
 
 		this.type = 'SetScriptValueCommand';
-		this.name = `Set Script.${attributeName}`;
+		this.name = editor.strings.getKey( 'command/SetScriptValue' ) + ': ' + attributeName;
 		this.updatable = true;
 
 		this.object = object;
 		this.script = script;
 
 		this.attributeName = attributeName;
-		this.oldValue = ( script !== undefined ) ? script[ this.attributeName ] : undefined;
+		this.oldValue = ( script !== '' ) ? script[ this.attributeName ] : null;
 		this.newValue = newValue;
 
 	}
@@ -31,7 +31,7 @@ class SetScriptValueCommand extends Command {
 
 		this.script[ this.attributeName ] = this.newValue;
 
-		this.editor.signals.scriptChanged.dispatch();
+		this.editor.signals.scriptChanged.dispatch( this.script );
 
 	}
 
@@ -39,7 +39,7 @@ class SetScriptValueCommand extends Command {
 
 		this.script[ this.attributeName ] = this.oldValue;
 
-		this.editor.signals.scriptChanged.dispatch();
+		this.editor.signals.scriptChanged.dispatch( this.script );
 
 	}
 

+ 3 - 3
editor/js/commands/SetUuidCommand.js

@@ -8,16 +8,16 @@ import { Command } from '../Command.js';
  */
 class SetUuidCommand extends Command {
 
-	constructor( editor, object, newUuid ) {
+	constructor( editor, object = null, newUuid = null ) {
 
 		super( editor );
 
 		this.type = 'SetUuidCommand';
-		this.name = 'Update UUID';
+		this.name = editor.strings.getKey( 'command/SetUuid' );
 
 		this.object = object;
 
-		this.oldUuid = ( object !== undefined ) ? object.uuid : undefined;
+		this.oldUuid = ( object !== null ) ? object.uuid : null;
 		this.newUuid = newUuid;
 
 	}

+ 3 - 3
editor/js/commands/SetValueCommand.js

@@ -9,17 +9,17 @@ import { Command } from '../Command.js';
  */
 class SetValueCommand extends Command {
 
-	constructor( editor, object, attributeName, newValue ) {
+	constructor( editor, object = null, attributeName = '', newValue = null ) {
 
 		super( editor );
 
 		this.type = 'SetValueCommand';
-		this.name = `Set ${attributeName}`;
+		this.name = editor.strings.getKey( 'command/SetValue' ) + ': ' + attributeName;
 		this.updatable = true;
 
 		this.object = object;
 		this.attributeName = attributeName;
-		this.oldValue = ( object !== undefined ) ? object[ attributeName ] : undefined;
+		this.oldValue = ( object !== null ) ? object[ attributeName ] : null;
 		this.newValue = newValue;
 
 	}

File diff suppressed because it is too large
+ 0 - 0
editor/js/libs/ffmpeg.min.js


+ 58 - 17
editor/js/libs/ui.js

@@ -98,6 +98,14 @@ class UIElement {
 
 	}
 
+	toggleClass( name, toggle ) {
+
+		this.dom.classList.toggle( name, toggle );
+
+		return this;
+
+	}
+
 	setStyle( style, array ) {
 
 		for ( let i = 0; i < array.length; i ++ ) {
@@ -110,6 +118,20 @@ class UIElement {
 
 	}
 
+	setHidden( isHidden ) {
+
+		this.dom.hidden = isHidden;
+
+		return this;
+
+	}
+
+	isHidden() {
+
+		return this.dom.hidden;
+
+	}
+
 	setDisabled( value ) {
 
 		this.dom.disabled = value;
@@ -151,7 +173,7 @@ const properties = [ 'position', 'left', 'top', 'right', 'bottom', 'width', 'hei
 
 properties.forEach( function ( property ) {
 
-	const method = 'set' + property.substr( 0, 1 ).toUpperCase() + property.substr( 1, property.length );
+	const method = 'set' + property.substring( 0, 1 ).toUpperCase() + property.substring( 1 );
 
 	UIElement.prototype[ method ] = function () {
 
@@ -314,7 +336,7 @@ class UITextArea extends UIElement {
 
 			event.stopPropagation();
 
-			if ( event.keyCode === 9 ) {
+			if ( event.code === 'Tab' ) {
 
 				event.preventDefault();
 
@@ -494,7 +516,7 @@ class UIColor extends UIElement {
 
 	getHexValue() {
 
-		return parseInt( this.dom.value.substr( 1 ), 16 );
+		return parseInt( this.dom.value.substring( 1 ), 16 );
 
 	}
 
@@ -542,8 +564,7 @@ class UINumber extends UIElement {
 
 		const scope = this;
 
-		const changeEvent = document.createEvent( 'HTMLEvents' );
-		changeEvent.initEvent( 'change', true, true );
+		const changeEvent = new Event( 'change', { bubbles: true, cancelable: true } );
 
 		let distance = 0;
 		let onMouseDownValue = 0;
@@ -686,19 +707,19 @@ class UINumber extends UIElement {
 
 			event.stopPropagation();
 
-			switch ( event.keyCode ) {
+			switch ( event.code ) {
 
-				case 13: // enter
+				case 'Enter':
 					scope.dom.blur();
 					break;
 
-				case 38: // up
+				case 'ArrowUp':
 					event.preventDefault();
 					scope.setValue( scope.getValue() + scope.nudge );
 					scope.dom.dispatchEvent( changeEvent );
 					break;
 
-				case 40: // down
+				case 'ArrowDown':
 					event.preventDefault();
 					scope.setValue( scope.getValue() - scope.nudge );
 					scope.dom.dispatchEvent( changeEvent );
@@ -782,6 +803,8 @@ class UINumber extends UIElement {
 
 		this.unit = unit;
 
+		this.setValue( this.value );
+
 		return this;
 
 	}
@@ -812,8 +835,7 @@ class UIInteger extends UIElement {
 
 		const scope = this;
 
-		const changeEvent = document.createEvent( 'HTMLEvents' );
-		changeEvent.initEvent( 'change', true, true );
+		const changeEvent = new Event( 'change', { bubbles: true, cancelable: true } );
 
 		let distance = 0;
 		let onMouseDownValue = 0;
@@ -901,19 +923,19 @@ class UIInteger extends UIElement {
 
 			event.stopPropagation();
 
-			switch ( event.keyCode ) {
+			switch ( event.code ) {
 
-				case 13: // enter
+				case 'Enter':
 					scope.dom.blur();
 					break;
 
-				case 38: // up
+				case 'ArrowUp':
 					event.preventDefault();
 					scope.setValue( scope.getValue() + scope.nudge );
 					scope.dom.dispatchEvent( changeEvent );
 					break;
 
-				case 40: // down
+				case 'ArrowDown':
 					event.preventDefault();
 					scope.setValue( scope.getValue() - scope.nudge );
 					scope.dom.dispatchEvent( changeEvent );
@@ -1119,6 +1141,26 @@ class UITabbedPanel extends UIDiv {
 
 		this.selected = id;
 
+		// Scrolls to tab
+		if ( tab ) {
+
+			const tabOffsetRight = tab.dom.offsetLeft + tab.dom.offsetWidth;
+			const containerWidth = this.tabsDiv.dom.getBoundingClientRect().width;
+
+			if ( tabOffsetRight > containerWidth ) {
+
+				this.tabsDiv.dom.scrollTo( { left: tabOffsetRight - containerWidth, behavior: 'smooth' } );
+
+			}
+
+			if ( tab.dom.offsetLeft < this.tabsDiv.dom.scrollLeft ) {
+
+				this.tabsDiv.dom.scrollTo( { left: 0, behavior: 'smooth' } );
+
+			}
+
+		}
+
 		return this;
 
 	}
@@ -1266,8 +1308,7 @@ class UIListbox extends UIDiv {
 
 		this.selectedValue = value;
 
-		const changeEvent = document.createEvent( 'HTMLEvents' );
-		changeEvent.initEvent( 'change', true, true );
+		const changeEvent = new Event( 'change', { bubbles: true, cancelable: true } );
 		this.dom.dispatchEvent( changeEvent );
 
 	}

+ 21 - 26
editor/js/libs/ui.three.js

@@ -3,6 +3,7 @@ import * as THREE from 'three';
 import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
 import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
 import { TGALoader } from 'three/addons/loaders/TGALoader.js';
+import { FullScreenQuad } from 'three/addons/postprocessing/Pass.js';
 
 import { UISpan, UIDiv, UIRow, UIButton, UICheckbox, UIText, UINumber } from './ui.js';
 import { MoveObjectCommand } from '../commands/MoveObjectCommand.js';
@@ -270,10 +271,10 @@ class UIOutliner extends UIDiv {
 		// Prevent native scroll behavior
 		this.dom.addEventListener( 'keydown', function ( event ) {
 
-			switch ( event.keyCode ) {
+			switch ( event.code ) {
 
-				case 38: // up
-				case 40: // down
+				case 'ArrowUp':
+				case 'ArrowDown':
 					event.preventDefault();
 					event.stopPropagation();
 					break;
@@ -285,12 +286,12 @@ class UIOutliner extends UIDiv {
 		// Keybindings to support arrow navigation
 		this.dom.addEventListener( 'keyup', function ( event ) {
 
-			switch ( event.keyCode ) {
+			switch ( event.code ) {
 
-				case 38: // up
+				case 'ArrowUp':
 					scope.selectIndex( scope.selectedIndex - 1 );
 					break;
-				case 40: // down
+				case 'ArrowDown':
 					scope.selectIndex( scope.selectedIndex + 1 );
 					break;
 
@@ -312,8 +313,7 @@ class UIOutliner extends UIDiv {
 
 			this.setValue( this.options[ index ].value );
 
-			const changeEvent = document.createEvent( 'HTMLEvents' );
-			changeEvent.initEvent( 'change', true, true );
+			const changeEvent = new Event( 'change', { bubbles: true, cancelable: true } );
 			this.dom.dispatchEvent( changeEvent );
 
 		}
@@ -334,8 +334,7 @@ class UIOutliner extends UIDiv {
 
 			scope.setValue( this.value );
 
-			const changeEvent = document.createEvent( 'HTMLEvents' );
-			changeEvent.initEvent( 'change', true, true );
+			const changeEvent = new Event( 'change', { bubbles: true, cancelable: true } );
 			scope.dom.dispatchEvent( changeEvent );
 
 		}
@@ -448,8 +447,7 @@ class UIOutliner extends UIDiv {
 			const editor = scope.editor;
 			editor.execute( new MoveObjectCommand( editor, object, newParent, nextObject ) );
 
-			const changeEvent = document.createEvent( 'HTMLEvents' );
-			changeEvent.initEvent( 'change', true, true );
+			const changeEvent = new Event( 'change', { bubbles: true, cancelable: true } );
 			scope.dom.dispatchEvent( changeEvent );
 
 		}
@@ -551,9 +549,7 @@ class UIPoints extends UISpan {
 		this.lastPointIdx = 0;
 		this.onChangeCallback = null;
 
-		// TODO Remove this bind() stuff
-
-		this.update = function () {
+		this.update = () => { // bind lexical this
 
 			if ( this.onChangeCallback !== null ) {
 
@@ -561,7 +557,7 @@ class UIPoints extends UISpan {
 
 			}
 
-		}.bind( this );
+		};
 
 	}
 
@@ -831,7 +827,7 @@ class UIBoolean extends UISpan {
 
 }
 
-let renderer;
+let renderer, fsQuad;
 
 function renderToCanvas( texture ) {
 
@@ -841,19 +837,18 @@ function renderToCanvas( texture ) {
 
 	}
 
-	const image = texture.image;
+	if ( fsQuad === undefined ) {
 
-	renderer.setSize( image.width, image.height, false );
+		fsQuad = new FullScreenQuad( new THREE.MeshBasicMaterial() );
 
-	const scene = new THREE.Scene();
-	const camera = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
+	}
 
-	const material = new THREE.MeshBasicMaterial( { map: texture } );
-	const quad = new THREE.PlaneGeometry( 2, 2 );
-	const mesh = new THREE.Mesh( quad, material );
-	scene.add( mesh );
+	const image = texture.image;
+
+	renderer.setSize( image.width, image.height, false );
 
-	renderer.render( scene, camera );
+	fsQuad.material.map = texture;
+	fsQuad.render( renderer );
 
 	return renderer.domElement;
 

+ 0 - 2
editor/sw.js

@@ -91,7 +91,6 @@ const assets = [
 	'./js/libs/codemirror/mode/glsl.js',
 
 	'./js/libs/esprima.js',
-	'./js/libs/ffmpeg.min.js',
 	'./js/libs/jsonlint.js',
 
 	'./js/libs/codemirror/addon/dialog.css',
@@ -137,7 +136,6 @@ const assets = [
 	'./js/Menubar.File.js',
 	'./js/Menubar.Edit.js',
 	'./js/Menubar.Add.js',
-	'./js/Menubar.Examples.js',
 	'./js/Menubar.Help.js',
 	'./js/Menubar.View.js',
 	'./js/Menubar.Status.js',

+ 37 - 32
examples/files.json

@@ -255,6 +255,8 @@
 	],
 	"webgl / advanced": [
 		"webgl_buffergeometry",
+		"webgl_buffergeometry_attributes_integer",
+		"webgl_buffergeometry_attributes_none",
 		"webgl_buffergeometry_compression",
 		"webgl_buffergeometry_custom_attributes_particles",
 		"webgl_buffergeometry_drawrange",
@@ -270,6 +272,7 @@
 		"webgl_buffergeometry_rawshader",
 		"webgl_buffergeometry_selective_draw",
 		"webgl_buffergeometry_uint",
+		"webgl_clipculldistance",
 		"webgl_custom_attributes",
 		"webgl_custom_attributes_lines",
 		"webgl_custom_attributes_points",
@@ -280,30 +283,25 @@
 		"webgl_gpgpu_water",
 		"webgl_gpgpu_protoplanet",
 		"webgl_materials_modified",
+		"webgl_multiple_rendertargets",
+		"webgl_multisampled_renderbuffers",
 		"webgl_raymarching_reflect",
+		"webgl_rendertarget_texture2darray",
 		"webgl_shadowmap_csm",
 		"webgl_shadowmap_pcss",
 		"webgl_shadowmap_progressive",
 		"webgl_simple_gi",
+		"webgl_texture2darray",
+		"webgl_texture2darray_compressed",
+		"webgl_texture3d",
+		"webgl_texture3d_partialupdate",
+		"webgl_ubo",
+		"webgl_ubo_arrays",
+		"webgl_volume_cloud",
+		"webgl_volume_instancing",
+		"webgl_volume_perlin",
 		"webgl_worker_offscreencanvas"
 	],
-	"webgl2": [
-		"webgl2_buffergeometry_attributes_integer",
-		"webgl2_buffergeometry_attributes_none",
-		"webgl2_clipculldistance",
-		"webgl2_materials_texture2darray",
-		"webgl2_materials_texture3d",
-		"webgl2_materials_texture3d_partialupdate",
-		"webgl2_multiple_rendertargets",
-		"webgl2_multisampled_renderbuffers",
-		"webgl2_rendertarget_texture2darray",
-		"webgl2_texture2darray_compressed",
-		"webgl2_ubo",
-		"webgl2_ubo_arrays",
-		"webgl2_volume_cloud",
-		"webgl2_volume_instancing",
-		"webgl2_volume_perlin"
-	],
 	"webgpu (wip)": [
 		"webgpu_backdrop",
 		"webgpu_backdrop_area",
@@ -329,6 +327,7 @@
 		"webgpu_instance_mesh",
 		"webgpu_instance_points",
 		"webgpu_instance_uniform",
+		"webgpu_instancing_morph",
 		"webgpu_lights_custom",
 		"webgpu_lights_ies_spotlight",
 		"webgpu_lights_phong",
@@ -337,27 +336,39 @@
 		"webgpu_loader_gltf",
 		"webgpu_loader_gltf_anisotropy",
 		"webgpu_loader_gltf_compressed",
+		"webgpu_loader_gltf_dispersion",
 		"webgpu_loader_gltf_iridescence",
 		"webgpu_loader_gltf_sheen",
 		"webgpu_loader_gltf_transmission",
 		"webgpu_loader_materialx",
 		"webgpu_materials",
+		"webgpu_materials_displacementmap",
 		"webgpu_materials_lightmap",
+		"webgpu_materials_matcap",
 		"webgpu_materials_sss",
 		"webgpu_materials_transmission",
+		"webgpu_materials_toon",
 		"webgpu_materials_video",
 		"webgpu_materialx_noise",
-		"webgpu_multiple_rendertargets",
-		"webgpu_multiple_rendertargets_readback",
+		"webgpu_mesh_batch",
+		"webgpu_mirror",
 		"webgpu_morphtargets",
 		"webgpu_morphtargets_face",
+		"webgpu_multiple_rendertargets",
+		"webgpu_multiple_rendertargets_readback",
+		"webgpu_multisampled_renderbuffers",
 		"webgpu_occlusion",
 		"webgpu_parallax_uv",
 		"webgpu_particles",
+		"webgpu_performance_renderbundle",
+		"webgpu_pmrem_cubemap",
+		"webgpu_pmrem_equirectangular",
+		"webgpu_pmrem_scene",
 		"webgpu_portal",
+		"webgpu_postprocessing_afterimage",
+		"webgpu_postprocessing_anamorphic",
 		"webgpu_reflection",
 		"webgpu_rtt",
-		"webgpu_materials_texture_partialupdate",
 		"webgpu_sandbox",
 		"webgpu_shadertoy",
 		"webgpu_shadowmap",
@@ -365,22 +376,16 @@
 		"webgpu_skinning_instancing",
 		"webgpu_skinning_points",
 		"webgpu_sprites",
+		"webgpu_storage_buffer",
+		"webgpu_texturegrad",
 		"webgpu_textures_2d-array",
+		"webgpu_textures_anisotropy",
+		"webgpu_textures_partialupdate",
 		"webgpu_tsl_editor",
 		"webgpu_tsl_transpiler",
 		"webgpu_video_panorama",
-		"webgpu_pmrem_cubemap",
-		"webgpu_pmrem_equirectangular",
-		"webgpu_pmrem_scene",
-		"webgpu_postprocessing_afterimage",
-		"webgpu_postprocessing_anamorphic",
-		"webgpu_mirror",
-		"webgpu_multisampled_renderbuffers",
-		"webgpu_materials_texture_anisotropy",
-		"webgpu_storage_buffer",
-		"webgpu_mesh_batch",
-		"webgpu_instancing_morph",
-		"webgpu_texturegrad"
+		"webgpu_volume_cloud",
+		"webgpu_volume_perlin"
 	],
 	"webaudio": [
 		"webaudio_orientation",

+ 2 - 5
examples/games_fps.html

@@ -71,6 +71,7 @@
 			const renderer = new THREE.WebGLRenderer( { antialias: true } );
 			renderer.setPixelRatio( window.devicePixelRatio );
 			renderer.setSize( window.innerWidth, window.innerHeight );
+			renderer.setAnimationLoop( animate );
 			renderer.shadowMap.enabled = true;
 			renderer.shadowMap.type = THREE.VSMShadowMap;
 			renderer.toneMapping = THREE.ACESFilmicToneMapping;
@@ -442,9 +443,7 @@
 						helper.visible = value;
 
 					} );
-
-				animate();
-
+			
 			} );
 
 			function teleportPlayerIfOob() {
@@ -485,8 +484,6 @@
 
 				stats.update();
 
-				requestAnimationFrame( animate );
-
 			}
 
 		</script>

+ 1 - 1
examples/jsm/controls/TransformControls.js

@@ -32,7 +32,7 @@ const _unit = {
 };
 
 const _changeEvent = { type: 'change' };
-const _mouseDownEvent = { type: 'mouseDown' };
+const _mouseDownEvent = { type: 'mouseDown', mode: null };
 const _mouseUpEvent = { type: 'mouseUp', mode: null };
 const _objectChangeEvent = { type: 'objectChange' };
 

+ 1 - 5
examples/jsm/environments/RoomEnvironment.js

@@ -24,11 +24,7 @@ class RoomEnvironment extends Scene {
 		const roomMaterial = new MeshStandardMaterial( { side: BackSide } );
 		const boxMaterial = new MeshStandardMaterial();
 
-		let intensity = 5;
-
-		if ( renderer !== null && renderer._useLegacyLights === false ) intensity = 900;
-
-		const mainLight = new PointLight( 0xffffff, intensity, 28, 2 );
+		const mainLight = new PointLight( 0xffffff, 900, 28, 2 );
 		mainLight.position.set( 0.418, 16.199, 0.300 );
 		this.add( mainLight );
 

+ 6 - 4
examples/jsm/exporters/USDZExporter.js

@@ -25,6 +25,7 @@ class USDZExporter {
 				anchoring: { type: 'plane' },
 				planeAnchoring: { alignment: 'horizontal' }
 			},
+			includeAnchoringProperties: true,
 			quickLookCompatible: false,
 			maxTextureSize: 1024,
 		}, options );
@@ -198,6 +199,10 @@ function buildHeader() {
 
 function buildSceneStart( options ) {
 
+	const alignment = options.includeAnchoringProperties === true ? `
+		token preliminary:anchoring:type = "${options.ar.anchoring.type}"
+		token preliminary:planeAnchoring:alignment = "${options.ar.planeAnchoring.alignment}"
+	` : '';
 	return `def Xform "Root"
 {
 	def Scope "Scenes" (
@@ -211,10 +216,7 @@ function buildSceneStart( options ) {
 			}
 			sceneName = "Scene"
 		)
-		{
-		token preliminary:anchoring:type = "${options.ar.anchoring.type}"
-		token preliminary:planeAnchoring:alignment = "${options.ar.planeAnchoring.alignment}"
-
+		{${alignment}
 `;
 
 }

+ 32 - 67
examples/jsm/helpers/ViewHelper.js

@@ -1,5 +1,5 @@
 import {
-	BoxGeometry,
+	CylinderGeometry,
 	CanvasTexture,
 	Color,
 	Euler,
@@ -11,6 +11,7 @@ import {
 	Raycaster,
 	Sprite,
 	SpriteMaterial,
+	SRGBColorSpace,
 	Vector2,
 	Vector3,
 	Vector4
@@ -27,9 +28,10 @@ class ViewHelper extends Object3D {
 		this.animating = false;
 		this.center = new Vector3();
 
-		const color1 = new Color( '#ff3653' );
-		const color2 = new Color( '#8adb00' );
-		const color3 = new Color( '#2c8fff' );
+		const color1 = new Color( '#ff4466' );
+		const color2 = new Color( '#88ff44' );
+		const color3 = new Color( '#4488ff' );
+		const color4 = new Color( '#000000' );
 
 		const interactiveObjects = [];
 		const raycaster = new Raycaster();
@@ -39,7 +41,7 @@ class ViewHelper extends Object3D {
 		const orthoCamera = new OrthographicCamera( - 2, 2, 2, - 2, 0, 4 );
 		orthoCamera.position.set( 0, 0, 2 );
 
-		const geometry = new BoxGeometry( 0.8, 0.05, 0.05 ).translate( 0.4, 0, 0 );
+		const geometry = new CylinderGeometry( 0.04, 0.04, 0.8, 5 ).rotateZ( - Math.PI / 2 ).translate( 0.4, 0, 0 );
 
 		const xAxis = new Mesh( geometry, getAxisMaterial( color1 ) );
 		const yAxis = new Mesh( geometry, getAxisMaterial( color2 ) );
@@ -52,28 +54,35 @@ class ViewHelper extends Object3D {
 		this.add( zAxis );
 		this.add( yAxis );
 
-		const posXAxisHelper = new Sprite( getSpriteMaterial( color1, 'X' ) );
-		posXAxisHelper.userData.type = 'posX';
-		const posYAxisHelper = new Sprite( getSpriteMaterial( color2, 'Y' ) );
-		posYAxisHelper.userData.type = 'posY';
-		const posZAxisHelper = new Sprite( getSpriteMaterial( color3, 'Z' ) );
-		posZAxisHelper.userData.type = 'posZ';
-		const negXAxisHelper = new Sprite( getSpriteMaterial( color1 ) );
-		negXAxisHelper.userData.type = 'negX';
-		const negYAxisHelper = new Sprite( getSpriteMaterial( color2 ) );
-		negYAxisHelper.userData.type = 'negY';
-		const negZAxisHelper = new Sprite( getSpriteMaterial( color3 ) );
-		negZAxisHelper.userData.type = 'negZ';
+		const spriteMaterial1 = getSpriteMaterial( color1 );
+		const spriteMaterial2 = getSpriteMaterial( color2 );
+		const spriteMaterial3 = getSpriteMaterial( color3 );
+		const spriteMaterial4 = getSpriteMaterial( color4 );
+
+		const posXAxisHelper = new Sprite( spriteMaterial1 );
+		const posYAxisHelper = new Sprite( spriteMaterial2 );
+		const posZAxisHelper = new Sprite( spriteMaterial3 );
+		const negXAxisHelper = new Sprite( spriteMaterial4 );
+		const negYAxisHelper = new Sprite( spriteMaterial4 );
+		const negZAxisHelper = new Sprite( spriteMaterial4 );
 
 		posXAxisHelper.position.x = 1;
 		posYAxisHelper.position.y = 1;
 		posZAxisHelper.position.z = 1;
 		negXAxisHelper.position.x = - 1;
-		negXAxisHelper.scale.setScalar( 0.8 );
 		negYAxisHelper.position.y = - 1;
-		negYAxisHelper.scale.setScalar( 0.8 );
 		negZAxisHelper.position.z = - 1;
-		negZAxisHelper.scale.setScalar( 0.8 );
+
+		negXAxisHelper.material.opacity = 0.2;
+		negYAxisHelper.material.opacity = 0.2;
+		negZAxisHelper.material.opacity = 0.2;
+
+		posXAxisHelper.userData.type = 'posX';
+		posYAxisHelper.userData.type = 'posY';
+		posZAxisHelper.userData.type = 'posZ';
+		negXAxisHelper.userData.type = 'negX';
+		negYAxisHelper.userData.type = 'negY';
+		negZAxisHelper.userData.type = 'negZ';
 
 		this.add( posXAxisHelper );
 		this.add( posYAxisHelper );
@@ -101,42 +110,6 @@ class ViewHelper extends Object3D {
 			point.set( 0, 0, 1 );
 			point.applyQuaternion( camera.quaternion );
 
-			if ( point.x >= 0 ) {
-
-				posXAxisHelper.material.opacity = 1;
-				negXAxisHelper.material.opacity = 0.5;
-
-			} else {
-
-				posXAxisHelper.material.opacity = 0.5;
-				negXAxisHelper.material.opacity = 1;
-
-			}
-
-			if ( point.y >= 0 ) {
-
-				posYAxisHelper.material.opacity = 1;
-				negYAxisHelper.material.opacity = 0.5;
-
-			} else {
-
-				posYAxisHelper.material.opacity = 0.5;
-				negYAxisHelper.material.opacity = 1;
-
-			}
-
-			if ( point.z >= 0 ) {
-
-				posZAxisHelper.material.opacity = 1;
-				negZAxisHelper.material.opacity = 0.5;
-
-			} else {
-
-				posZAxisHelper.material.opacity = 0.5;
-				negZAxisHelper.material.opacity = 1;
-
-			}
-
 			//
 
 			const x = domElement.offsetWidth - dim;
@@ -298,7 +271,7 @@ class ViewHelper extends Object3D {
 
 		}
 
-		function getSpriteMaterial( color, text = null ) {
+		function getSpriteMaterial( color ) {
 
 			const canvas = document.createElement( 'canvas' );
 			canvas.width = 64;
@@ -306,21 +279,13 @@ class ViewHelper extends Object3D {
 
 			const context = canvas.getContext( '2d' );
 			context.beginPath();
-			context.arc( 32, 32, 16, 0, 2 * Math.PI );
+			context.arc( 32, 32, 14, 0, 2 * Math.PI );
 			context.closePath();
 			context.fillStyle = color.getStyle();
 			context.fill();
 
-			if ( text !== null ) {
-
-				context.font = '24px Arial';
-				context.textAlign = 'center';
-				context.fillStyle = '#000000';
-				context.fillText( text, 32, 41 );
-
-			}
-
 			const texture = new CanvasTexture( canvas );
+			texture.colorSpace = SRGBColorSpace;
 
 			return new SpriteMaterial( { map: texture, toneMapped: false } );
 

+ 75 - 64
examples/jsm/libs/tween.module.js

@@ -7,13 +7,13 @@ var Easing = Object.freeze({
             return amount;
         },
         In: function (amount) {
-            return this.None(amount);
+            return amount;
         },
         Out: function (amount) {
-            return this.None(amount);
+            return amount;
         },
         InOut: function (amount) {
-            return this.None(amount);
+            return amount;
         },
     }),
     Quadratic: Object.freeze({
@@ -676,13 +676,11 @@ var Tween = /** @class */ (function () {
      * it is still playing, just paused).
      */
     Tween.prototype.update = function (time, autoStart) {
-        var _this = this;
         var _a;
         if (time === void 0) { time = now(); }
         if (autoStart === void 0) { autoStart = true; }
         if (this._isPaused)
             return true;
-        var property;
         var endTime = this._startTime + this._duration;
         if (!this._goToEnd && !this._isPlaying) {
             if (time > endTime)
@@ -709,72 +707,85 @@ var Tween = /** @class */ (function () {
         var elapsedTime = time - this._startTime;
         var durationAndDelay = this._duration + ((_a = this._repeatDelayTime) !== null && _a !== void 0 ? _a : this._delayTime);
         var totalTime = this._duration + this._repeat * durationAndDelay;
-        var calculateElapsedPortion = function () {
-            if (_this._duration === 0)
-                return 1;
-            if (elapsedTime > totalTime) {
-                return 1;
-            }
-            var timesRepeated = Math.trunc(elapsedTime / durationAndDelay);
-            var timeIntoCurrentRepeat = elapsedTime - timesRepeated * durationAndDelay;
-            // TODO use %?
-            // const timeIntoCurrentRepeat = elapsedTime % durationAndDelay
-            var portion = Math.min(timeIntoCurrentRepeat / _this._duration, 1);
-            if (portion === 0 && elapsedTime === _this._duration) {
-                return 1;
-            }
-            return portion;
-        };
-        var elapsed = calculateElapsedPortion();
+        var elapsed = this._calculateElapsedPortion(elapsedTime, durationAndDelay, totalTime);
         var value = this._easingFunction(elapsed);
-        // properties transformations
+        var status = this._calculateCompletionStatus(elapsedTime, durationAndDelay);
+        if (status === 'repeat') {
+            // the current update is happening after the instant the tween repeated
+            this._processRepetition(elapsedTime, durationAndDelay);
+        }
         this._updateProperties(this._object, this._valuesStart, this._valuesEnd, value);
+        if (status === 'about-to-repeat') {
+            // the current update is happening at the exact instant the tween is going to repeat
+            // the values should match the end of the tween, not the beginning,
+            // that's why _processRepetition happens after _updateProperties
+            this._processRepetition(elapsedTime, durationAndDelay);
+        }
         if (this._onUpdateCallback) {
             this._onUpdateCallback(this._object, elapsed);
         }
-        if (this._duration === 0 || elapsedTime >= this._duration) {
-            if (this._repeat > 0) {
-                var completeCount = Math.min(Math.trunc((elapsedTime - this._duration) / durationAndDelay) + 1, this._repeat);
-                if (isFinite(this._repeat)) {
-                    this._repeat -= completeCount;
-                }
-                // Reassign starting values, restart by making startTime = now
-                for (property in this._valuesStartRepeat) {
-                    if (!this._yoyo && typeof this._valuesEnd[property] === 'string') {
-                        this._valuesStartRepeat[property] =
-                            // eslint-disable-next-line
-                            // @ts-ignore FIXME?
-                            this._valuesStartRepeat[property] + parseFloat(this._valuesEnd[property]);
-                    }
-                    if (this._yoyo) {
-                        this._swapEndStartRepeatValues(property);
-                    }
-                    this._valuesStart[property] = this._valuesStartRepeat[property];
-                }
-                if (this._yoyo) {
-                    this._reversed = !this._reversed;
-                }
-                this._startTime += durationAndDelay * completeCount;
-                if (this._onRepeatCallback) {
-                    this._onRepeatCallback(this._object);
-                }
-                this._onEveryStartCallbackFired = false;
-                return true;
+        if (status === 'repeat' || status === 'about-to-repeat') {
+            if (this._onRepeatCallback) {
+                this._onRepeatCallback(this._object);
             }
-            else {
-                if (this._onCompleteCallback) {
-                    this._onCompleteCallback(this._object);
-                }
-                for (var i = 0, numChainedTweens = this._chainedTweens.length; i < numChainedTweens; i++) {
-                    // Make the chained tweens start exactly at the time they should,
-                    // even if the `update()` method was called way past the duration of the tween
-                    this._chainedTweens[i].start(this._startTime + this._duration, false);
-                }
-                this._isPlaying = false;
-                return false;
+            this._onEveryStartCallbackFired = false;
+        }
+        else if (status === 'completed') {
+            this._isPlaying = false;
+            if (this._onCompleteCallback) {
+                this._onCompleteCallback(this._object);
+            }
+            for (var i = 0, numChainedTweens = this._chainedTweens.length; i < numChainedTweens; i++) {
+                // Make the chained tweens start exactly at the time they should,
+                // even if the `update()` method was called way past the duration of the tween
+                this._chainedTweens[i].start(this._startTime + this._duration, false);
             }
         }
-        return true;
+        return status !== 'completed';
+    };
+    Tween.prototype._calculateElapsedPortion = function (elapsedTime, durationAndDelay, totalTime) {
+        if (this._duration === 0 || elapsedTime > totalTime) {
+            return 1;
+        }
+        var timeIntoCurrentRepeat = elapsedTime % durationAndDelay;
+        var portion = Math.min(timeIntoCurrentRepeat / this._duration, 1);
+        if (portion === 0 && elapsedTime !== 0 && elapsedTime % this._duration === 0) {
+            return 1;
+        }
+        return portion;
+    };
+    Tween.prototype._calculateCompletionStatus = function (elapsedTime, durationAndDelay) {
+        if (this._duration !== 0 && elapsedTime < this._duration) {
+            return 'playing';
+        }
+        if (this._repeat <= 0) {
+            return 'completed';
+        }
+        if (elapsedTime === this._duration) {
+            return 'about-to-repeat';
+        }
+        return 'repeat';
+    };
+    Tween.prototype._processRepetition = function (elapsedTime, durationAndDelay) {
+        var completeCount = Math.min(Math.trunc((elapsedTime - this._duration) / durationAndDelay) + 1, this._repeat);
+        if (isFinite(this._repeat)) {
+            this._repeat -= completeCount;
+        }
+        // Reassign starting values, restart by making startTime = now
+        for (var property in this._valuesStartRepeat) {
+            var valueEnd = this._valuesEnd[property];
+            if (!this._yoyo && typeof valueEnd === 'string') {
+                this._valuesStartRepeat[property] = this._valuesStartRepeat[property] + parseFloat(valueEnd);
+            }
+            if (this._yoyo) {
+                this._swapEndStartRepeatValues(property);
+            }
+            this._valuesStart[property] = this._valuesStartRepeat[property];
+        }
+        if (this._yoyo) {
+            this._reversed = !this._reversed;
+        }
+        this._startTime += durationAndDelay * completeCount;
     };
     Tween.prototype._updateProperties = function (_object, _valuesStart, _valuesEnd, value) {
         for (var property in _valuesEnd) {
@@ -830,7 +841,7 @@ var Tween = /** @class */ (function () {
     return Tween;
 }());
 
-var VERSION = '23.1.1';
+var VERSION = '23.1.2';
 
 /**
  * Tween.js - Licensed under the MIT license

+ 1 - 15
examples/jsm/lines/LineMaterial.js

@@ -1,25 +1,11 @@
-/**
- * parameters = {
- *  color: <hex>,
- *  linewidth: <float>,
- *  dashed: <boolean>,
- *  dashScale: <float>,
- *  dashSize: <float>,
- *  dashOffset: <float>,
- *  gapSize: <float>,
- *  resolution: <Vector2>, // to be set by renderer
- * }
- */
-
 import {
 	ShaderLib,
 	ShaderMaterial,
 	UniformsLib,
 	UniformsUtils,
-	Vector2
+	Vector2,
 } from 'three';
 
-
 UniformsLib.line = {
 
 	worldUnits: { value: 1 },

+ 15 - 0
examples/jsm/lines/LineSegments2.js

@@ -13,6 +13,8 @@ import {
 import { LineSegmentsGeometry } from '../lines/LineSegmentsGeometry.js';
 import { LineMaterial } from '../lines/LineMaterial.js';
 
+const _viewport = new Vector4();
+
 const _start = new Vector3();
 const _end = new Vector3();
 
@@ -356,6 +358,19 @@ class LineSegments2 extends Mesh {
 
 	}
 
+	onBeforeRender( renderer ) {
+
+		const uniforms = this.material.uniforms;
+
+		if ( uniforms && uniforms.resolution ) {
+
+			renderer.getViewport( _viewport );
+			this.material.uniforms.resolution.value.set( _viewport.z, _viewport.w );
+
+		}
+
+	}
+
 }
 
 export { LineSegments2 };

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