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>
 		</p>
 		 
 		 
 		<h3>
 		<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>
 		</h3>
 		<p>
 		<p>
 		ينسخ بكسلات من WebGLFramebuffer الحالي إلى قوام ثنائي الأبعاد. يتيح
 		ينسخ بكسلات من WebGLFramebuffer الحالي إلى قوام ثنائي الأبعاد. يتيح
@@ -377,23 +377,16 @@
 		[link:https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/copyTexImage2D WebGLRenderingContext.copyTexImage2D].
 		[link:https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/copyTexImage2D WebGLRenderingContext.copyTexImage2D].
 		</p>
 		</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>
 		<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>
 		</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>
 		<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>
 		</p>
 		 
 		 
 		<h3>[method:undefined dispose]( )</h3>
 		<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 times] - (required) array of keyframe times.<br />
 			[page:Array values] - values for the keyframes at the times specified.<br />
 			[page:Array values] - values for the keyframes at the times specified.<br />
 		</p>
 		</p>
+		<p>
+			This keyframe track type has no interpolation parameter because the
+			interpolation is always [page:Animation InterpolateDiscrete].
+		</p>
 
 
 		<h2>Properties</h2>
 		<h2>Properties</h2>
 
 
@@ -30,7 +34,7 @@
 
 
 		<h3>[property:Constant DefaultInterpolation]</h3>
 		<h3>[property:Constant DefaultInterpolation]</h3>
 		<p>
 		<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>
 		</p>
 
 
 		<h3>[property:Array ValueBufferType]</h3>
 		<h3>[property:Array ValueBufferType]</h3>

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

@@ -20,7 +20,7 @@
 		<h2>Constructor</h2>
 		<h2>Constructor</h2>
 
 
 		<h3>
 		<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>
 		</h3>
 		<p>
 		<p>
 			[page:String name] - (required) identifier for the KeyframeTrack.<br />
 			[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>
 		<h2>Constructor</h2>
 
 
 		<h3>
 		<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>
 		</h3>
 		<p>
 		<p>
 			[page:String name] - (required) identifier for the KeyframeTrack.<br />
 			[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>
 		<h2>Constructor</h2>
 
 
 		<h3>
 		<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>
 		</h3>
 		<p>
 		<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 />
 			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 Animation Constants] for possible values. Default is
 			[page:Animation InterpolateLinear].
 			[page:Animation InterpolateLinear].
 		</p>
 		</p>

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

@@ -24,9 +24,10 @@
 			[page:String name] - (required) identifier for the KeyframeTrack.<br />
 			[page:String name] - (required) identifier for the KeyframeTrack.<br />
 			[page:Array times] - (required) array of keyframe times.<br />
 			[page:Array times] - (required) array of keyframe times.<br />
 			[page:Array values] - values for the keyframes at the times specified.<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>
 		</p>
 
 
 		<h2>Properties</h2>
 		<h2>Properties</h2>
@@ -35,7 +36,7 @@
 
 
 		<h3>[property:Constant DefaultInterpolation]</h3>
 		<h3>[property:Constant DefaultInterpolation]</h3>
 		<p>
 		<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>
 		</p>
 
 
 		<h3>[property:Array ValueBufferType]</h3>
 		<h3>[property:Array ValueBufferType]</h3>
@@ -70,4 +71,4 @@
 		</p>
 		</p>
 	</body>
 	</body>
 
 
-</html>
+</html>

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

@@ -17,7 +17,7 @@
 		<h2>Constructor</h2>
 		<h2>Constructor</h2>
 
 
 		<h3>
 		<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>
 		</h3>
 		<p>
 		<p>
 			[page:String name] - (required) identifier for the KeyframeTrack.<br />
 			[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.
 			in the list include a "z" field to perform a depth-ordered sort with.
 		</p>
 		</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>
 		<h3>
 			[method:Matrix4 getMatrixAt]( [param:Integer index], [param:Matrix4 matrix] )
 			[method:Matrix4 getMatrixAt]( [param:Integer index], [param:Matrix4 matrix] )
 		</h3>
 		</h3>
@@ -151,6 +164,18 @@
 		</p>
 		</p>
 		<p>Get whether the given instance is marked as "visible" or not.</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>
 		<h3>
 			[method:this setMatrixAt]( [param:Integer index], [param:Matrix4 matrix] )
 			[method:this setMatrixAt]( [param:Integer index], [param:Matrix4 matrix] )
 		</h3>
 		</h3>

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

@@ -14,7 +14,7 @@
 		<p class="desc">
 		<p class="desc">
 			A special version of [page:Mesh] with instanced rendering support. Use
 			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
 			[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
 			of [name] will help you to reduce the number of draw calls and thus
 			improve the overall rendering performance in your application.
 			improve the overall rendering performance in your application.
 		</p>
 		</p>
@@ -34,8 +34,8 @@
 		</h3>
 		</h3>
 		<p>
 		<p>
 			[page:BufferGeometry geometry] - an instance of [page:BufferGeometry].<br />
 			[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 />
 			[page:Integer count] - the number of instances.<br />
 		</p>
 		</p>
 
 

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

@@ -19,7 +19,7 @@
 		<h3>[name]( [param:Object parameters] )</h3>
 		<h3>[name]( [param:Object parameters] )</h3>
 		<p>
 		<p>
 			[page:Object parameters] - (optional) object with properties defining the
 			[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.
 			In all cases, it will assume sane defaults when parameters are missing.
 			The following are valid parameters:<br /><br />
 			The following are valid parameters:<br /><br />
 
 
@@ -367,7 +367,7 @@ document.body.appendChild( renderer.domElement );
 		</p>
 		</p>
 
 
 		<h3>
 		<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>
 		</h3>
 		<p>
 		<p>
 			Copies pixels from the current WebGLFramebuffer into a 2D texture. Enables
 			Copies pixels from the current WebGLFramebuffer into a 2D texture. Enables
@@ -376,19 +376,20 @@ document.body.appendChild( renderer.domElement );
 		</p>
 		</p>
 
 
 		<h3>
 		<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>
 		</h3>
 		<p>
 		<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].
 			[link:https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/texSubImage2D WebGLRenderingContext.texSubImage2D].
 		</p>
 		</p>
 
 
 		<h3>
 		<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>
 		</h3>
 		<p>
 		<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
 			the destination texture starting from the given position. Enables access
 			to
 			to
 			[link:https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext/texSubImage3D WebGL2RenderingContext.texSubImage3D].
 			[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).
 			and GPU upload overhead).
 		</p>
 		</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>
 		<h3>[method:undefined resetGLState]( )</h3>
 		<p>
 		<p>
 			Reset the GL state to default. Called internally if the WebGL context is
 			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
 			This is a wrapper around
 			[link:https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/readPixels WebGLRenderingContext.readPixels]().
 			[link:https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/readPixels WebGLRenderingContext.readPixels]().
 		</p>
 		</p>
-		<p>
-			See the [example:webgl_interactive_cubes_gpu interactive / cubes / gpu]
-			example.
-		</p>
 		<p>
 		<p>
 			For reading out a [page:WebGLCubeRenderTarget WebGLCubeRenderTarget] use
 			For reading out a [page:WebGLCubeRenderTarget WebGLCubeRenderTarget] use
 			the optional parameter activeCubeFaceIndex to determine which face should
 			the optional parameter activeCubeFaceIndex to determine which face should
 			be read.
 			be read.
 		</p>
 		</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>
 		<h3>
 			[method:undefined render]( [param:Object3D scene], [param:Camera camera] )
 			[method:undefined render]( [param:Object3D scene], [param:Camera camera] )
 		</h3>
 		</h3>

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

@@ -61,11 +61,32 @@
 		<h3>[property:Object image]</h3>
 		<h3>[property:Object image]</h3>
 		<p>Overridden with a object containing width, height, and depth.</p>
 		<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>
 		<h3>[property:Boolean isCompressedArrayTexture]</h3>
 		<p>Read-only flag to check if a given object is of type [name].</p>
 		<p>Read-only flag to check if a given object is of type [name].</p>
 
 
 		<h2>Methods</h2>
 		<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>
 		<p>
 			See the base [page:CompressedTexture CompressedTexture] class for common
 			See the base [page:CompressedTexture CompressedTexture] class for common
 			methods.
 			methods.

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

@@ -143,8 +143,30 @@
 			page for details.
 			page for details.
 		</p>
 		</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>
 		<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>
 		<p>See the base [page:Texture Texture] class for common methods.</p>
 
 
 		<h2>Source</h2>
 		<h2>Source</h2>

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

@@ -33,10 +33,7 @@
 
 
 			[page:Number height] -- height of the texture.<br />
 			[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
 			[page:Constant mapping] -- See [page:Textures mapping mode constants] for
 			details.<br />
 			details.<br />
@@ -87,10 +84,14 @@
 
 
 		<h3>[page:Texture.type type]</h3>
 		<h3>[page:Texture.type type]</h3>
 		<p>
 		<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>
 		</p>
 
 
 		<h3>[page:Texture.magFilter magFilter]</h3>
 		<h3>[page:Texture.magFilter magFilter]</h3>

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

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

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

@@ -321,22 +321,22 @@
 			Questo metodo utilizza *KHR_parallel_shader_compile*.
 			Questo metodo utilizza *KHR_parallel_shader_compile*.
 		</p>
 		</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>
 		<p>
 			Copia i pixel dal WebGLFramebuffer corrente in una texture 2D. Abilita l'accesso a 
 			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].
 			[link:https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/copyTexImage2D WebGLRenderingContext.copyTexImage2D].
 		</p>
 		</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>
 		<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>
 		</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>
 		<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>
 		</p>
 
 
 		<h3>[method:undefined dispose]( )</h3>
 		<h3>[method:undefined dispose]( )</h3>

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

@@ -281,11 +281,20 @@
 			此方法利用 *KHR_parallel_shader_compile*。
 			此方法利用 *KHR_parallel_shader_compile*。
 		</p>
 		</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>
 		<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>
 		<h3>[method:undefined dispose]( )</h3>
 		<p>处理当前的渲染环境</p>
 		<p>处理当前的渲染环境</p>

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

@@ -32,7 +32,7 @@
 		renderer.render( scene, camera );
 		renderer.render( scene, camera );
 
 
 		// copy part of the rendered frame into the framebuffer texture
 		// copy part of the rendered frame into the framebuffer texture
-		renderer.copyFramebufferToTexture( vector, frameTexture );
+		renderer.copyFramebufferToTexture( frameTexture, vector );
 		</code>
 		</code>
 
 
 		<h2>例子</h2>
 		<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).
 		and to reduce overdraw in opaque materials (front to back).
 		</p>
 		</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>
 		<h2>Source</h2>
 
 
 		<p>
 		<p>

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

@@ -69,6 +69,30 @@
 		and to reduce overdraw in opaque materials (front to back).
 		and to reduce overdraw in opaque materials (front to back).
 		</p>
 		</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>
 		<h2>源代码</h2>
 
 
 		<p>
 		<p>

+ 6 - 1
docs/list.json

@@ -378,7 +378,8 @@
 			},
 			},
 
 
 			"Objects": {
 			"Objects": {
-				"Lensflare": "examples/en/objects/Lensflare"
+				"Lensflare": "examples/en/objects/Lensflare",
+				"Sky": "examples/en/objects/Sky"
 			},
 			},
 
 
 			"Post-Processing": {
 			"Post-Processing": {
@@ -404,6 +405,10 @@
 				"Timer": "examples/en/misc/Timer"
 				"Timer": "examples/en/misc/Timer"
 			},
 			},
 
 
+			"Modifiers": {
+				"EdgeSplit": "examples/en/modifiers/EdgeSplitModifier"
+			},
+
 			"ConvexHull": {
 			"ConvexHull": {
 				"Face": "examples/en/math/convexhull/Face",
 				"Face": "examples/en/math/convexhull/Face",
 				"HalfEdge": "examples/en/math/convexhull/HalfEdge",
 				"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>
 		<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>
 		<code>
 		function animate() {
 		function animate() {
-			requestAnimationFrame( animate );
 			renderer.render( scene, camera );
 			renderer.render( scene, camera );
 		}
 		}
-		animate();
+		renderer.setAnimationLoop( animate );
 		</code>
 		</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>
 		<h2>Animating the cube</h2>
 
 
@@ -94,12 +93,12 @@
 		cube.rotation.y += 0.01;
 		cube.rotation.y += 0.01;
 		</code>
 		</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>
 		<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>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>
 		<p><i>index.html —</i></p>
 
 
@@ -129,6 +128,7 @@
 
 
 		const renderer = new THREE.WebGLRenderer();
 		const renderer = new THREE.WebGLRenderer();
 		renderer.setSize( window.innerWidth, window.innerHeight );
 		renderer.setSize( window.innerWidth, window.innerHeight );
+		renderer.setAnimationLoop( animate );
 		document.body.appendChild( renderer.domElement );
 		document.body.appendChild( renderer.domElement );
 
 
 		const geometry = new THREE.BoxGeometry( 1, 1, 1 );
 		const geometry = new THREE.BoxGeometry( 1, 1, 1 );
@@ -139,15 +139,13 @@
 		camera.position.z = 5;
 		camera.position.z = 5;
 
 
 		function animate() {
 		function animate() {
-			requestAnimationFrame( animate );
 
 
 			cube.rotation.x += 0.01;
 			cube.rotation.x += 0.01;
 			cube.rotation.y += 0.01;
 			cube.rotation.y += 0.01;
 
 
 			renderer.render( scene, camera );
 			renderer.render( scene, camera );
-		}
 
 
-		animate();
+		}
 		</code>
 		</code>
 	</body>
 	</body>
 </html>
 </html>

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

@@ -70,7 +70,7 @@ import * as THREE from 'three';
 			</li>
 			</li>
 			<li>
 			<li>
 				<p>
 				<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>
 				</p>
 				<code>
 				<code>
 # three.js
 # three.js
@@ -100,7 +100,7 @@ npm install --save-dev vite
 					<details>
 					<details>
 						<summary><i>npx</i> 是什么?</summary>
 						<summary><i>npx</i> 是什么?</summary>
 						<p>
 						<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>
 						</p>
 					</details>
 					</details>
 				</aside>
 				</aside>
@@ -123,10 +123,10 @@ npm install --save-dev vite
 				[link:https://threejs-journey.com/lessons/local-server three.js journey: Local Server]
 				[link:https://threejs-journey.com/lessons/local-server three.js journey: Local Server]
 			</li>
 			</li>
 			<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>
 			<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>
 			</li>
 		</ul>
 		</ul>
 
 
@@ -145,7 +145,7 @@ npm install --save-dev vite
 		<ol>
 		<ol>
 			<li>
 			<li>
 				<p>
 				<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>
 				</p>
 				<code>
 				<code>
 &lt;script type="importmap">
 &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;
 	color-scheme: light dark;
 }
 }
 
 
+[hidden] {
+	display: none !important;
+}
+
 body {
 body {
 	font-family: Helvetica, Arial, sans-serif;
 	font-family: Helvetica, Arial, sans-serif;
 	font-size: 14px;
 	font-size: 14px;
@@ -72,17 +76,34 @@ textarea, input { outline: none; } /* osx */
 
 
 .TabbedPanel .Tabs {
 .TabbedPanel .Tabs {
 	position: relative;
 	position: relative;
+	z-index: 1; /** Above .Panels **/
 	display: block;
 	display: block;
 	width: 100%;
 	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 {
 	.TabbedPanel .Tabs .Tab {
 		padding: 10px 9px;
 		padding: 10px 9px;
 		text-transform: uppercase;
 		text-transform: uppercase;
 	}
 	}
 
 
 	.TabbedPanel .Panels {
 	.TabbedPanel .Panels {
-		position: relative;
+		position: absolute;
+		top: 40px;
 		display: block;
 		display: block;
 		width: 100%;
 		width: 100%;
 	}
 	}
@@ -292,14 +313,26 @@ select {
 
 
 #resizer {
 #resizer {
 	position: absolute;
 	position: absolute;
+	z-index: 2; /* Above #sidebar */
 	top: 32px;
 	top: 32px;
-	right: 345px;
+	right: 350px;
 	width: 5px;
 	width: 5px;
 	bottom: 0px;
 	bottom: 0px;
-	/* background-color: rgba(255,0,0,0.5); */
+	transform: translatex(2.5px);
 	cursor: col-resize;
 	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 {
 #viewport {
 	position: absolute;
 	position: absolute;
 	top: 32px;
 	top: 32px;
@@ -308,7 +341,7 @@ select {
 	bottom: 0;
 	bottom: 0;
 }
 }
 
 
-	#viewport #info {
+	#viewport .Text {
 		text-shadow: 1px 1px 0 rgba(0,0,0,0.25);
 		text-shadow: 1px 1px 0 rgba(0,0,0,0.25);
 		pointer-events: none;
 		pointer-events: none;
 	}
 	}
@@ -362,18 +395,32 @@ select {
 			line-height: 16px;
 			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 {
 		#menubar .menu .options {
 			position: fixed;
 			position: fixed;
+			z-index: 1; /* higher than resizer */
 			display: none;
 			display: none;
 			padding: 5px 0;
 			padding: 5px 0;
 			background: #eee;
 			background: #eee;
-			width: 150px;
-			max-height: calc(100% - 80px);
+			min-width: 150px;
+			max-height: calc(100vh - 80px);
 			overflow: auto;
 			overflow: auto;
 		}
 		}
 
 
 		#menubar .menu:hover .options {
 		#menubar .menu:hover .options {
 			display: block;
 			display: block;
+			box-shadow: 0 10px 10px -5px #00000033;
 		}
 		}
 
 
 			#menubar .menu .options hr {
 			#menubar .menu .options hr {
@@ -392,18 +439,41 @@ select {
 					background-color: #08f;
 					background-color: #08f;
 				}
 				}
 
 
-				#menubar .menu .options .option:active {
+				#menubar .menu .options .option:not(.submenu-title):active {
 					color: #666;
 					color: #666;
 					background: transparent;
 					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 {
 		#menubar .menu .options .inactive {
 			color: #bbb;
 			color: #bbb;
 			background-color: transparent;
 			background-color: transparent;
 			padding: 5px 10px;
 			padding: 5px 10px;
 			margin: 0 !important;
 			margin: 0 !important;
+			cursor: not-allowed;
 		}
 		}
 
 
+		
+
 #sidebar {
 #sidebar {
 	position: absolute;
 	position: absolute;
 	right: 0;
 	right: 0;
@@ -412,6 +482,7 @@ select {
 	width: 350px;
 	width: 350px;
 	background: #eee;
 	background: #eee;
 	overflow: auto;
 	overflow: auto;
+	overflow-x: hidden;
 }
 }
 
 
 	#sidebar .Panel {
 	#sidebar .Panel {
@@ -532,7 +603,7 @@ select {
 	}
 	}
 
 
 	#menubar .menu .options {
 	#menubar .menu .options {
-		max-height: calc(100% - 372px);
+		max-height: calc(100% - 80px);
 	}
 	}
 
 
 	#menubar .menu.right {
 	#menubar .menu.right {
@@ -610,6 +681,11 @@ select {
 		background: #111;
 		background: #111;
 	}
 	}
 
 
+			#menubar .menu .key {
+				color: #444;
+				border-color: #444;
+			}
+
 			#menubar .menu .options {
 			#menubar .menu .options {
 				background: #111;
 				background: #111;
 			}
 			}
@@ -661,10 +737,13 @@ select {
 		}
 		}
 
 
 	.Outliner {
 	.Outliner {
-		color: #888;
 		background: #222;
 		background: #222;
 	}
 	}
 
 
+		.Outliner .option {
+			color: #999;
+		}
+
 		.Outliner .option:hover {
 		.Outliner .option:hover {
 			background-color: rgba(21,60,94,0.5);
 			background-color: rgba(21,60,94,0.5);
 		}
 		}
@@ -678,6 +757,10 @@ select {
 		border-top: 1px solid #222;
 		border-top: 1px solid #222;
 	}
 	}
 
 
+		.TabbedPanel .Tabs::-webkit-scrollbar {
+			background: #111;
+		}
+
 		.TabbedPanel .Tab {
 		.TabbedPanel .Tab {
 			color: #555;
 			color: #555;
 			border-right: 1px solid #222;
 			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/esprima.js"></script>
 		<script src="js/libs/jsonlint.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/dialog.css">
 		<link rel="stylesheet" href="js/libs/codemirror/addon/show-hint.css">
 		<link rel="stylesheet" href="js/libs/codemirror/addon/show-hint.css">
@@ -61,8 +61,8 @@
 					"three/addons/": "../examples/jsm/",
 					"three/addons/": "../examples/jsm/",
 
 
 					"three/examples/": "../examples/",
 					"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>
 		</script>
@@ -83,12 +83,6 @@
 			window.URL = window.URL || window.webkitURL;
 			window.URL = window.URL || window.webkitURL;
 			window.BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder;
 			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();
 			const editor = new Editor();
@@ -121,13 +115,13 @@
 
 
 			editor.storage.init( function () {
 			editor.storage.init( function () {
 
 
-				editor.storage.get( function ( state ) {
+				editor.storage.get( async function ( state ) {
 
 
 					if ( isLoadingFromHash ) return;
 					if ( isLoadingFromHash ) return;
 
 
 					if ( state !== undefined ) {
 					if ( state !== undefined ) {
 
 
-						editor.fromJSON( state );
+						await editor.fromJSON( state );
 
 
 					}
 					}
 
 
@@ -235,7 +229,7 @@
 
 
 				const file = hash.slice( 6 );
 				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();
 					const loader = new THREE.FileLoader();
 					loader.crossOrigin = '';
 					loader.crossOrigin = '';

+ 5 - 1
editor/js/Config.js

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

+ 19 - 2
editor/js/Editor.js

@@ -82,7 +82,6 @@ function Editor() {
 
 
 		windowResize: new Signal(),
 		windowResize: new Signal(),
 
 
-		showGridChanged: new Signal(),
 		showHelpersChanged: new Signal(),
 		showHelpersChanged: new Signal(),
 		refreshSidebarObject3D: new Signal(),
 		refreshSidebarObject3D: new Signal(),
 		refreshSidebarEnvironment: new Signal(),
 		refreshSidebarEnvironment: new Signal(),
@@ -93,6 +92,8 @@ function Editor() {
 
 
 		intersectionsDetected: new Signal(),
 		intersectionsDetected: new Signal(),
 
 
+		pathTracerUpdated: new Signal(),
+
 	};
 	};
 
 
 	this.config = new Config();
 	this.config = new Config();
@@ -659,7 +660,16 @@ Editor.prototype = {
 		var loader = new THREE.ObjectLoader();
 		var loader = new THREE.ObjectLoader();
 		var camera = await loader.parseAsync( json.camera );
 		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.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.signals.cameraResetted.dispatch();
 
 
 		this.history.fromJSON( json.history );
 		this.history.fromJSON( json.history );
@@ -754,7 +764,8 @@ Editor.prototype = {
 
 
 		save: save,
 		save: save,
 		saveArrayBuffer: saveArrayBuffer,
 		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 };
 export { Editor };

+ 3 - 3
editor/js/History.js

@@ -88,7 +88,7 @@ class History {
 
 
 		if ( this.historyDisabled ) {
 		if ( this.historyDisabled ) {
 
 
-			alert( 'Undo/Redo disabled while scene is playing.' );
+			alert( this.editor.strings.getKey( 'prompt/history/forbid' ) );
 			return;
 			return;
 
 
 		}
 		}
@@ -123,7 +123,7 @@ class History {
 
 
 		if ( this.historyDisabled ) {
 		if ( this.historyDisabled ) {
 
 
-			alert( 'Undo/Redo disabled while scene is playing.' );
+			alert( this.editor.strings.getKey( 'prompt/history/forbid' ) );
 			return;
 			return;
 
 
 		}
 		}
@@ -241,7 +241,7 @@ class History {
 
 
 		if ( this.historyDisabled ) {
 		if ( this.historyDisabled ) {
 
 
-			alert( 'Undo/Redo disabled while scene is playing.' );
+			alert( this.editor.strings.getKey( 'prompt/history/forbid' ) );
 			return;
 			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 { TGALoader } from 'three/addons/loaders/TGALoader.js';
 
 
 import { AddObjectCommand } from './commands/AddObjectCommand.js';
 import { AddObjectCommand } from './commands/AddObjectCommand.js';
-import { SetSceneCommand } from './commands/SetSceneCommand.js';
 
 
 import { LoaderUtils } from './LoaderUtils.js';
 import { LoaderUtils } from './LoaderUtils.js';
 
 
@@ -70,7 +69,7 @@ function Loader( editor ) {
 		const reader = new FileReader();
 		const reader = new FileReader();
 		reader.addEventListener( 'progress', function ( event ) {
 		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 ) + '%';
 			const progress = Math.floor( ( event.loaded / event.total ) * 100 ) + '%';
 
 
 			console.log( 'Loading', filename, size, progress );
 			console.log( 'Loading', filename, size, progress );
@@ -99,7 +98,7 @@ function Loader( editor ) {
 
 
 					}, function ( error ) {
 					}, function ( error ) {
 
 
-						console.error( error )
+						console.error( error );
 
 
 					} );
 					} );
 
 
@@ -586,6 +585,7 @@ function Loader( editor ) {
 					//
 					//
 
 
 					const group = new THREE.Group();
 					const group = new THREE.Group();
+					group.name = filename;
 					group.scale.multiplyScalar( 0.1 );
 					group.scale.multiplyScalar( 0.1 );
 					group.scale.y *= - 1;
 					group.scale.y *= - 1;
 
 
@@ -715,7 +715,7 @@ function Loader( editor ) {
 
 
 					const result = new VRMLLoader().parse( contents );
 					const result = new VRMLLoader().parse( contents );
 
 
-					editor.execute( new SetSceneCommand( editor, result ) );
+					editor.execute( new AddObjectCommand( editor, result ) );
 
 
 				}, false );
 				}, false );
 				reader.readAsText( file );
 				reader.readAsText( file );
@@ -828,15 +828,7 @@ function Loader( editor ) {
 
 
 				loader.parse( data, function ( result ) {
 				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 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
 		// Poly
 
 
 		if ( zip[ 'model.obj' ] && zip[ 'materials.mtl' ] ) {
 		if ( zip[ 'model.obj' ] && zip[ 'materials.mtl' ] ) {
@@ -865,9 +875,11 @@ function Loader( editor ) {
 			const { MTLLoader } = await import( 'three/addons/loaders/MTLLoader.js' );
 			const { MTLLoader } = await import( 'three/addons/loaders/MTLLoader.js' );
 			const { OBJLoader } = await import( 'three/addons/loaders/OBJLoader.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' ] ) );
 			const object = new OBJLoader().setMaterials( materials ).parse( strFromU8( zip[ 'model.obj' ] ) );
+
 			editor.execute( new AddObjectCommand( editor, object ) );
 			editor.execute( new AddObjectCommand( editor, object ) );
+			return;
 
 
 		}
 		}
 
 
@@ -877,24 +889,6 @@ function Loader( editor ) {
 
 
 			const file = zip[ path ];
 			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();
 			const extension = path.split( '.' ).pop().toLowerCase();
 
 
 			switch ( extension ) {
 			switch ( extension ) {
@@ -941,7 +935,7 @@ function Loader( editor ) {
 				{
 				{
 
 
 					const loader = await createGLTFLoader( manager );
 					const loader = await createGLTFLoader( manager );
-					
+
 					loader.parse( strFromU8( file ), '', function ( result ) {
 					loader.parse( strFromU8( file ), '', function ( result ) {
 
 
 						const scene = result.scene;
 						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 );
 	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 = new UIRow();
 	option.setClass( 'option' );
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/box' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/mesh/box' ) );
 	option.onClick( function () {
 	option.onClick( function () {
 
 
 		const geometry = new THREE.BoxGeometry( 1, 1, 1, 1, 1, 1 );
 		const geometry = new THREE.BoxGeometry( 1, 1, 1, 1, 1, 1 );
@@ -53,13 +72,13 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 
 
 	} );
 	} );
-	options.add( option );
+	meshSubmenu.add( option );
 
 
-	// Capsule
+	// Mesh / Capsule
 
 
 	option = new UIRow();
 	option = new UIRow();
 	option.setClass( 'option' );
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/capsule' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/mesh/capsule' ) );
 	option.onClick( function () {
 	option.onClick( function () {
 
 
 		const geometry = new THREE.CapsuleGeometry( 1, 1, 4, 8 );
 		const geometry = new THREE.CapsuleGeometry( 1, 1, 4, 8 );
@@ -70,13 +89,13 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 
 
 	} );
 	} );
-	options.add( option );
+	meshSubmenu.add( option );
 
 
-	// Circle
+	// Mesh / Circle
 
 
 	option = new UIRow();
 	option = new UIRow();
 	option.setClass( 'option' );
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/circle' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/mesh/circle' ) );
 	option.onClick( function () {
 	option.onClick( function () {
 
 
 		const geometry = new THREE.CircleGeometry( 1, 32, 0, Math.PI * 2 );
 		const geometry = new THREE.CircleGeometry( 1, 32, 0, Math.PI * 2 );
@@ -86,13 +105,13 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 
 
 	} );
 	} );
-	options.add( option );
+	meshSubmenu.add( option );
 
 
-	// Cylinder
+	// Mesh / Cylinder
 
 
 	option = new UIRow();
 	option = new UIRow();
 	option.setClass( 'option' );
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/cylinder' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/mesh/cylinder' ) );
 	option.onClick( function () {
 	option.onClick( function () {
 
 
 		const geometry = new THREE.CylinderGeometry( 1, 1, 1, 32, 1, false, 0, Math.PI * 2 );
 		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 ) );
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 
 
 	} );
 	} );
-	options.add( option );
+	meshSubmenu.add( option );
 
 
-	// Dodecahedron
+	// Mesh / Dodecahedron
 
 
 	option = new UIRow();
 	option = new UIRow();
 	option.setClass( 'option' );
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/dodecahedron' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/mesh/dodecahedron' ) );
 	option.onClick( function () {
 	option.onClick( function () {
 
 
 		const geometry = new THREE.DodecahedronGeometry( 1, 0 );
 		const geometry = new THREE.DodecahedronGeometry( 1, 0 );
@@ -118,13 +137,13 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 
 
 	} );
 	} );
-	options.add( option );
+	meshSubmenu.add( option );
 
 
-	// Icosahedron
+	// Mesh / Icosahedron
 
 
 	option = new UIRow();
 	option = new UIRow();
 	option.setClass( 'option' );
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/icosahedron' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/mesh/icosahedron' ) );
 	option.onClick( function () {
 	option.onClick( function () {
 
 
 		const geometry = new THREE.IcosahedronGeometry( 1, 0 );
 		const geometry = new THREE.IcosahedronGeometry( 1, 0 );
@@ -134,13 +153,13 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 
 
 	} );
 	} );
-	options.add( option );
+	meshSubmenu.add( option );
 
 
-	// Lathe
+	// Mesh / Lathe
 
 
 	option = new UIRow();
 	option = new UIRow();
 	option.setClass( 'option' );
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/lathe' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/mesh/lathe' ) );
 	option.onClick( function () {
 	option.onClick( function () {
 
 
 		const geometry = new THREE.LatheGeometry();
 		const geometry = new THREE.LatheGeometry();
@@ -150,13 +169,13 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 
 
 	} );
 	} );
-	options.add( option );
+	meshSubmenu.add( option );
 
 
-	// Octahedron
+	// Mesh / Octahedron
 
 
 	option = new UIRow();
 	option = new UIRow();
 	option.setClass( 'option' );
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/octahedron' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/mesh/octahedron' ) );
 	option.onClick( function () {
 	option.onClick( function () {
 
 
 		const geometry = new THREE.OctahedronGeometry( 1, 0 );
 		const geometry = new THREE.OctahedronGeometry( 1, 0 );
@@ -166,13 +185,13 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 
 
 	} );
 	} );
-	options.add( option );
+	meshSubmenu.add( option );
 
 
-	// Plane
+	// Mesh / Plane
 
 
 	option = new UIRow();
 	option = new UIRow();
 	option.setClass( 'option' );
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/plane' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/mesh/plane' ) );
 	option.onClick( function () {
 	option.onClick( function () {
 
 
 		const geometry = new THREE.PlaneGeometry( 1, 1, 1, 1 );
 		const geometry = new THREE.PlaneGeometry( 1, 1, 1, 1 );
@@ -183,13 +202,13 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 
 
 	} );
 	} );
-	options.add( option );
+	meshSubmenu.add( option );
 
 
-	// Ring
+	// Mesh / Ring
 
 
 	option = new UIRow();
 	option = new UIRow();
 	option.setClass( 'option' );
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/ring' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/mesh/ring' ) );
 	option.onClick( function () {
 	option.onClick( function () {
 
 
 		const geometry = new THREE.RingGeometry( 0.5, 1, 32, 1, 0, Math.PI * 2 );
 		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 ) );
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 
 
 	} );
 	} );
-	options.add( option );
+	meshSubmenu.add( option );
 
 
-	// Sphere
+	// Mesh / Sphere
 
 
 	option = new UIRow();
 	option = new UIRow();
 	option.setClass( 'option' );
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/sphere' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/mesh/sphere' ) );
 	option.onClick( function () {
 	option.onClick( function () {
 
 
 		const geometry = new THREE.SphereGeometry( 1, 32, 16, 0, Math.PI * 2, 0, Math.PI );
 		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 ) );
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 
 
 	} );
 	} );
-	options.add( option );
+	meshSubmenu.add( option );
 
 
-	// Sprite
+	// Mesh / Sprite
 
 
 	option = new UIRow();
 	option = new UIRow();
 	option.setClass( 'option' );
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/sprite' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/mesh/sprite' ) );
 	option.onClick( function () {
 	option.onClick( function () {
 
 
 		const sprite = new THREE.Sprite( new THREE.SpriteMaterial() );
 		const sprite = new THREE.Sprite( new THREE.SpriteMaterial() );
@@ -230,13 +249,13 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, sprite ) );
 		editor.execute( new AddObjectCommand( editor, sprite ) );
 
 
 	} );
 	} );
-	options.add( option );
+	meshSubmenu.add( option );
 
 
-	// Tetrahedron
+	// Mesh / Tetrahedron
 
 
 	option = new UIRow();
 	option = new UIRow();
 	option.setClass( 'option' );
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/tetrahedron' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/mesh/tetrahedron' ) );
 	option.onClick( function () {
 	option.onClick( function () {
 
 
 		const geometry = new THREE.TetrahedronGeometry( 1, 0 );
 		const geometry = new THREE.TetrahedronGeometry( 1, 0 );
@@ -246,13 +265,13 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 
 
 	} );
 	} );
-	options.add( option );
+	meshSubmenu.add( option );
 
 
-	// Torus
+	// Mesh / Torus
 
 
 	option = new UIRow();
 	option = new UIRow();
 	option.setClass( 'option' );
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/torus' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/mesh/torus' ) );
 	option.onClick( function () {
 	option.onClick( function () {
 
 
 		const geometry = new THREE.TorusGeometry( 1, 0.4, 12, 48, Math.PI * 2 );
 		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 ) );
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 
 
 	} );
 	} );
-	options.add( option );
+	meshSubmenu.add( option );
 
 
-	// TorusKnot
+	// Mesh / TorusKnot
 
 
 	option = new UIRow();
 	option = new UIRow();
 	option.setClass( 'option' );
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/torusknot' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/mesh/torusknot' ) );
 	option.onClick( function () {
 	option.onClick( function () {
 
 
 		const geometry = new THREE.TorusKnotGeometry( 1, 0.4, 64, 8, 2, 3 );
 		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 ) );
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 
 
 	} );
 	} );
-	options.add( option );
+	meshSubmenu.add( option );
 
 
-	// Tube
+	// Mesh / Tube
 
 
 	option = new UIRow();
 	option = new UIRow();
 	option.setClass( 'option' );
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/tube' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/mesh/tube' ) );
 	option.onClick( function () {
 	option.onClick( function () {
 
 
 		const path = new THREE.CatmullRomCurve3( [
 		const path = new THREE.CatmullRomCurve3( [
@@ -301,17 +320,37 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, mesh ) );
 		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 = new UIRow();
 	option.setClass( 'option' );
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/ambientlight' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/light/ambient' ) );
 	option.onClick( function () {
 	option.onClick( function () {
 
 
 		const color = 0x222222;
 		const color = 0x222222;
@@ -322,13 +361,13 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, light ) );
 		editor.execute( new AddObjectCommand( editor, light ) );
 
 
 	} );
 	} );
-	options.add( option );
+	lightSubmenu.add( option );
 
 
-	// DirectionalLight
+	// Light / Directional
 
 
 	option = new UIRow();
 	option = new UIRow();
 	option.setClass( 'option' );
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/directionallight' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/light/directional' ) );
 	option.onClick( function () {
 	option.onClick( function () {
 
 
 		const color = 0xffffff;
 		const color = 0xffffff;
@@ -343,13 +382,13 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, light ) );
 		editor.execute( new AddObjectCommand( editor, light ) );
 
 
 	} );
 	} );
-	options.add( option );
+	lightSubmenu.add( option );
 
 
-	// HemisphereLight
+	// Light / Hemisphere
 
 
 	option = new UIRow();
 	option = new UIRow();
 	option.setClass( 'option' );
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/hemispherelight' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/light/hemisphere' ) );
 	option.onClick( function () {
 	option.onClick( function () {
 
 
 		const skyColor = 0x00aaff;
 		const skyColor = 0x00aaff;
@@ -364,13 +403,13 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, light ) );
 		editor.execute( new AddObjectCommand( editor, light ) );
 
 
 	} );
 	} );
-	options.add( option );
+	lightSubmenu.add( option );
 
 
-	// PointLight
+	// Light / Point
 
 
 	option = new UIRow();
 	option = new UIRow();
 	option.setClass( 'option' );
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/pointlight' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/light/point' ) );
 	option.onClick( function () {
 	option.onClick( function () {
 
 
 		const color = 0xffffff;
 		const color = 0xffffff;
@@ -383,13 +422,13 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, light ) );
 		editor.execute( new AddObjectCommand( editor, light ) );
 
 
 	} );
 	} );
-	options.add( option );
+	lightSubmenu.add( option );
 
 
-	// SpotLight
+	// Light / Spot
 
 
 	option = new UIRow();
 	option = new UIRow();
 	option.setClass( 'option' );
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/spotlight' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/light/spot' ) );
 	option.onClick( function () {
 	option.onClick( function () {
 
 
 		const color = 0xffffff;
 		const color = 0xffffff;
@@ -407,17 +446,37 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, light ) );
 		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 = new UIRow();
 	option.setClass( 'option' );
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/orthographiccamera' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/camera/orthographic' ) );
 	option.onClick( function () {
 	option.onClick( function () {
 
 
 		const aspect = editor.camera.aspect;
 		const aspect = editor.camera.aspect;
@@ -427,13 +486,13 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, camera ) );
 		editor.execute( new AddObjectCommand( editor, camera ) );
 
 
 	} );
 	} );
-	options.add( option );
+	cameraSubmenu.add( option );
 
 
-	// PerspectiveCamera
+	// Camera / Perspective
 
 
 	option = new UIRow();
 	option = new UIRow();
 	option.setClass( 'option' );
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/add/perspectivecamera' ) );
+	option.setTextContent( strings.getKey( 'menubar/add/camera/perspective' ) );
 	option.onClick( function () {
 	option.onClick( function () {
 
 
 		const camera = new THREE.PerspectiveCamera();
 		const camera = new THREE.PerspectiveCamera();
@@ -442,7 +501,7 @@ function MenubarAdd( editor ) {
 		editor.execute( new AddObjectCommand( editor, camera ) );
 		editor.execute( new AddObjectCommand( editor, camera ) );
 
 
 	} );
 	} );
-	options.add( option );
+	cameraSubmenu.add( option );
 
 
 	return container;
 	return container;
 
 

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

@@ -1,6 +1,6 @@
 import { Box3, Vector3 } from 'three';
 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 { AddObjectCommand } from './commands/AddObjectCommand.js';
 import { RemoveObjectCommand } from './commands/RemoveObjectCommand.js';
 import { RemoveObjectCommand } from './commands/RemoveObjectCommand.js';
@@ -28,6 +28,7 @@ function MenubarEdit( editor ) {
 	const undo = new UIRow();
 	const undo = new UIRow();
 	undo.setClass( 'option' );
 	undo.setClass( 'option' );
 	undo.setTextContent( strings.getKey( 'menubar/edit/undo' ) );
 	undo.setTextContent( strings.getKey( 'menubar/edit/undo' ) );
+	undo.add( new UIText( 'CTRL+Z' ).setClass( 'key' ) );
 	undo.onClick( function () {
 	undo.onClick( function () {
 
 
 		editor.undo();
 		editor.undo();
@@ -40,6 +41,7 @@ function MenubarEdit( editor ) {
 	const redo = new UIRow();
 	const redo = new UIRow();
 	redo.setClass( 'option' );
 	redo.setClass( 'option' );
 	redo.setTextContent( strings.getKey( 'menubar/edit/redo' ) );
 	redo.setTextContent( strings.getKey( 'menubar/edit/redo' ) );
+	redo.add( new UIText( 'CTRL+SHIFT+Z' ).setClass( 'key' ) );
 	redo.onClick( function () {
 	redo.onClick( function () {
 
 
 		editor.redo();
 		editor.redo();
@@ -47,7 +49,7 @@ function MenubarEdit( editor ) {
 	} );
 	} );
 	options.add( redo );
 	options.add( redo );
 
 
-	editor.signals.historyChanged.add( function () {
+	function onHistoryChanged() {
 
 
 		const history = editor.history;
 		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 = new UIRow();
 	option.setClass( 'option' );
 	option.setClass( 'option' );
 	option.setTextContent( strings.getKey( 'menubar/edit/delete' ) );
 	option.setTextContent( strings.getKey( 'menubar/edit/delete' ) );
+	option.add( new UIText( 'DEL' ).setClass( 'key' ) );
 	option.onClick( function () {
 	option.onClick( function () {
 
 
 		const object = editor.selected;
 		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 { UIPanel, UIRow, UIHorizontalRule } from './libs/ui.js';
+import { Loader } from './Loader.js';
 
 
 function MenubarFile( editor ) {
 function MenubarFile( editor ) {
 
 
@@ -19,20 +20,162 @@ function MenubarFile( editor ) {
 	options.setClass( 'options' );
 	options.setClass( 'options' );
 	container.add( 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 () {
 	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();
 			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 );
 	options.add( option );
 
 
 	//
 	//
@@ -66,22 +209,40 @@ function MenubarFile( editor ) {
 	} );
 	} );
 	options.add( option );
 	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
 	// Export DRC
 
 
 	option = new UIRow();
 	option = new UIRow();
 	option.setClass( 'option' );
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/file/export/drc' ) );
+	option.setTextContent( 'DRC' );
 	option.onClick( async function () {
 	option.onClick( async function () {
 
 
 		const object = editor.selected;
 		const object = editor.selected;
 
 
 		if ( object === null || object.isMesh === undefined ) {
 		if ( object === null || object.isMesh === undefined ) {
 
 
-			alert( 'No mesh selected' );
+			alert( strings.getKey( 'prompt/file/export/noMeshSelected' ) );
 			return;
 			return;
 
 
 		}
 		}
@@ -105,13 +266,13 @@ function MenubarFile( editor ) {
 		saveArrayBuffer( result, 'model.drc' );
 		saveArrayBuffer( result, 'model.drc' );
 
 
 	} );
 	} );
-	options.add( option );
+	fileExportSubmenu.add( option );
 
 
 	// Export GLB
 	// Export GLB
 
 
 	option = new UIRow();
 	option = new UIRow();
 	option.setClass( 'option' );
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/file/export/glb' ) );
+	option.setTextContent( 'GLB' );
 	option.onClick( async function () {
 	option.onClick( async function () {
 
 
 		const scene = editor.scene;
 		const scene = editor.scene;
@@ -136,13 +297,13 @@ function MenubarFile( editor ) {
 		}, undefined, { binary: true, animations: optimizedAnimations } );
 		}, undefined, { binary: true, animations: optimizedAnimations } );
 
 
 	} );
 	} );
-	options.add( option );
+	fileExportSubmenu.add( option );
 
 
 	// Export GLTF
 	// Export GLTF
 
 
 	option = new UIRow();
 	option = new UIRow();
 	option.setClass( 'option' );
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/file/export/gltf' ) );
+	option.setTextContent( 'GLTF' );
 	option.onClick( async function () {
 	option.onClick( async function () {
 
 
 		const scene = editor.scene;
 		const scene = editor.scene;
@@ -168,20 +329,20 @@ function MenubarFile( editor ) {
 
 
 
 
 	} );
 	} );
-	options.add( option );
+	fileExportSubmenu.add( option );
 
 
 	// Export OBJ
 	// Export OBJ
 
 
 	option = new UIRow();
 	option = new UIRow();
 	option.setClass( 'option' );
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/file/export/obj' ) );
+	option.setTextContent( 'OBJ' );
 	option.onClick( async function () {
 	option.onClick( async function () {
 
 
 		const object = editor.selected;
 		const object = editor.selected;
 
 
 		if ( object === null ) {
 		if ( object === null ) {
 
 
-			alert( 'No object selected.' );
+			alert( strings.getKey( 'prompt/file/export/noObjectSelected' ) );
 			return;
 			return;
 
 
 		}
 		}
@@ -193,13 +354,13 @@ function MenubarFile( editor ) {
 		saveString( exporter.parse( object ), 'model.obj' );
 		saveString( exporter.parse( object ), 'model.obj' );
 
 
 	} );
 	} );
-	options.add( option );
+	fileExportSubmenu.add( option );
 
 
 	// Export PLY (ASCII)
 	// Export PLY (ASCII)
 
 
 	option = new UIRow();
 	option = new UIRow();
 	option.setClass( 'option' );
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/file/export/ply' ) );
+	option.setTextContent( 'PLY' );
 	option.onClick( async function () {
 	option.onClick( async function () {
 
 
 		const { PLYExporter } = await import( 'three/addons/exporters/PLYExporter.js' );
 		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 = new UIRow();
 	option.setClass( 'option' );
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/file/export/ply_binary' ) );
+	option.setTextContent( 'PLY (BINARY)' );
 	option.onClick( async function () {
 	option.onClick( async function () {
 
 
 		const { PLYExporter } = await import( 'three/addons/exporters/PLYExporter.js' );
 		const { PLYExporter } = await import( 'three/addons/exporters/PLYExporter.js' );
@@ -233,13 +394,13 @@ function MenubarFile( editor ) {
 		}, { binary: true } );
 		}, { binary: true } );
 
 
 	} );
 	} );
-	options.add( option );
+	fileExportSubmenu.add( option );
 
 
 	// Export STL (ASCII)
 	// Export STL (ASCII)
 
 
 	option = new UIRow();
 	option = new UIRow();
 	option.setClass( 'option' );
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/file/export/stl' ) );
+	option.setTextContent( 'STL' );
 	option.onClick( async function () {
 	option.onClick( async function () {
 
 
 		const { STLExporter } = await import( 'three/addons/exporters/STLExporter.js' );
 		const { STLExporter } = await import( 'three/addons/exporters/STLExporter.js' );
@@ -249,13 +410,13 @@ function MenubarFile( editor ) {
 		saveString( exporter.parse( editor.scene ), 'model.stl' );
 		saveString( exporter.parse( editor.scene ), 'model.stl' );
 
 
 	} );
 	} );
-	options.add( option );
+	fileExportSubmenu.add( option );
 
 
-	// Export STL (Binary)
+	// Export STL (BINARY)
 
 
 	option = new UIRow();
 	option = new UIRow();
 	option.setClass( 'option' );
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/file/export/stl_binary' ) );
+	option.setTextContent( 'STL (BINARY)' );
 	option.onClick( async function () {
 	option.onClick( async function () {
 
 
 		const { STLExporter } = await import( 'three/addons/exporters/STLExporter.js' );
 		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' );
 		saveArrayBuffer( exporter.parse( editor.scene, { binary: true } ), 'model-binary.stl' );
 
 
 	} );
 	} );
-	options.add( option );
+	fileExportSubmenu.add( option );
 
 
 	// Export USDZ
 	// Export USDZ
 
 
 	option = new UIRow();
 	option = new UIRow();
 	option.setClass( 'option' );
 	option.setClass( 'option' );
-	option.setTextContent( strings.getKey( 'menubar/file/export/usdz' ) );
+	option.setTextContent( 'USDZ' );
 	option.onClick( async function () {
 	option.onClick( async function () {
 
 
 		const { USDZExporter } = await import( 'three/addons/exporters/USDZExporter.js' );
 		const { USDZExporter } = await import( 'three/addons/exporters/USDZExporter.js' );
@@ -281,7 +442,7 @@ function MenubarFile( editor ) {
 		saveArrayBuffer( await exporter.parseAsync( editor.scene ), 'model.usdz' );
 		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 ) {
 function MenubarView( editor ) {
 
 
@@ -17,9 +17,80 @@ function MenubarView( editor ) {
 	options.setClass( 'options' );
 	options.setClass( 'options' );
 	container.add( 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
 	// Fullscreen
 
 
-	const option = new UIRow();
+	option = new UIRow();
 	option.setClass( 'option' );
 	option.setClass( 'option' );
 	option.setTextContent( strings.getKey( 'menubar/view/fullscreen' ) );
 	option.setTextContent( strings.getKey( 'menubar/view/fullscreen' ) );
 	option.onClick( function () {
 	option.onClick( function () {
@@ -97,12 +168,14 @@ function MenubarView( editor ) {
 
 
 					}
 					}
 
 
-			} );
+				} );
 
 
 		}
 		}
 
 
 	}
 	}
 
 
+	//
+
 	return container;
 	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 { MenubarAdd } from './Menubar.Add.js';
 import { MenubarEdit } from './Menubar.Edit.js';
 import { MenubarEdit } from './Menubar.Edit.js';
 import { MenubarFile } from './Menubar.File.js';
 import { MenubarFile } from './Menubar.File.js';
-import { MenubarExamples } from './Menubar.Examples.js';
 import { MenubarView } from './Menubar.View.js';
 import { MenubarView } from './Menubar.View.js';
 import { MenubarHelp } from './Menubar.Help.js';
 import { MenubarHelp } from './Menubar.Help.js';
 import { MenubarStatus } from './Menubar.Status.js';
 import { MenubarStatus } from './Menubar.Status.js';
@@ -16,7 +15,6 @@ function Menubar( editor ) {
 	container.add( new MenubarFile( editor ) );
 	container.add( new MenubarFile( editor ) );
 	container.add( new MenubarEdit( editor ) );
 	container.add( new MenubarEdit( editor ) );
 	container.add( new MenubarAdd( editor ) );
 	container.add( new MenubarAdd( editor ) );
-	container.add( new MenubarExamples( editor ) );
 	container.add( new MenubarView( editor ) );
 	container.add( new MenubarView( editor ) );
 	container.add( new MenubarHelp( 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 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';
 		dom.style.right = x + 'px';
 
 

+ 76 - 10
editor/js/Script.js

@@ -6,6 +6,7 @@ import { SetMaterialValueCommand } from './commands/SetMaterialValueCommand.js';
 function Script( editor ) {
 function Script( editor ) {
 
 
 	const signals = editor.signals;
 	const signals = editor.signals;
+	const strings = editor.strings;
 
 
 	const container = new UIPanel();
 	const container = new UIPanel();
 	container.setId( 'script' );
 	container.setId( 'script' );
@@ -342,8 +343,7 @@ function Script( editor ) {
 	codemirror.on( 'keypress', function ( cm, kb ) {
 	codemirror.on( 'keypress', function ( cm, kb ) {
 
 
 		if ( currentMode !== 'javascript' ) return;
 		if ( currentMode !== 'javascript' ) return;
-		const typed = String.fromCharCode( kb.which || kb.keyCode );
-		if ( /[\w\.]/.exec( typed ) ) {
+		if ( /[\w\.]/.exec( kb.key ) ) {
 
 
 			server.complete( cm );
 			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 ) {
 	signals.editScript.add( function ( object, script ) {
 
 
-		let mode, name, source;
+		let mode, source;
 
 
 		if ( typeof ( script ) === 'object' ) {
 		if ( typeof ( script ) === 'object' ) {
 
 
 			mode = 'javascript';
 			mode = 'javascript';
-			name = script.name;
 			source = script.source;
 			source = script.source;
-			title.setValue( object.name + ' / ' + name );
 
 
 		} else {
 		} else {
 
 
@@ -378,7 +411,6 @@ function Script( editor ) {
 				case 'vertexShader':
 				case 'vertexShader':
 
 
 					mode = 'glsl';
 					mode = 'glsl';
-					name = 'Vertex Shader';
 					source = object.material.vertexShader || '';
 					source = object.material.vertexShader || '';
 
 
 					break;
 					break;
@@ -386,7 +418,6 @@ function Script( editor ) {
 				case 'fragmentShader':
 				case 'fragmentShader':
 
 
 					mode = 'glsl';
 					mode = 'glsl';
-					name = 'Fragment Shader';
 					source = object.material.fragmentShader || '';
 					source = object.material.fragmentShader || '';
 
 
 					break;
 					break;
@@ -394,7 +425,6 @@ function Script( editor ) {
 				case 'programInfo':
 				case 'programInfo':
 
 
 					mode = 'json';
 					mode = 'json';
-					name = 'Program Properties';
 					const json = {
 					const json = {
 						defines: object.material.defines,
 						defines: object.material.defines,
 						uniforms: object.material.uniforms,
 						uniforms: object.material.uniforms,
@@ -402,12 +432,18 @@ function Script( editor ) {
 					};
 					};
 					source = JSON.stringify( json, null, '\t' );
 					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;
 		currentMode = mode;
 		currentScript = script;
 		currentScript = script;
 		currentObject = object;
 		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;
 	return container;
 
 
 }
 }

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

@@ -35,7 +35,7 @@ function SidebarGeometryBufferGeometry( editor ) {
 			if ( index !== null ) {
 			if ( index !== null ) {
 
 
 				containerAttributes.add( new UIText( strings.getKey( 'sidebar/geometry/buffer_geometry/index' ) ).setWidth( '80px' ) );
 				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() );
 				containerAttributes.add( new UIBreak() );
 
 
 			}
 			}
@@ -47,7 +47,7 @@ function SidebarGeometryBufferGeometry( editor ) {
 				const attribute = attributes[ name ];
 				const attribute = attributes[ name ];
 
 
 				containerAttributes.add( new UIText( name ).setWidth( '80px' ) );
 				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() );
 				containerAttributes.add( new UIBreak() );
 
 
 			}
 			}
@@ -76,7 +76,7 @@ function SidebarGeometryBufferGeometry( editor ) {
 					const morphTargets = morphAttributes[ name ];
 					const morphTargets = morphAttributes[ name ];
 
 
 					containerMorphAttributes.add( new UIText( name ).setWidth( '80px' ) );
 					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() );
 					containerMorphAttributes.add( new UIBreak() );
 
 
 				}
 				}

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

@@ -36,7 +36,7 @@ function GeometryParametersPanel( editor, object ) {
 	// thetaStart
 	// thetaStart
 
 
 	const thetaStartRow = new UIRow();
 	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( new UIText( strings.getKey( 'sidebar/geometry/circle_geometry/thetastart' ) ).setClass( 'Label' ) );
 	thetaStartRow.add( thetaStart );
 	thetaStartRow.add( thetaStart );
@@ -46,7 +46,7 @@ function GeometryParametersPanel( editor, object ) {
 	// thetaLength
 	// thetaLength
 
 
 	const thetaLengthRow = new UIRow();
 	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( new UIText( strings.getKey( 'sidebar/geometry/circle_geometry/thetalength' ) ).setClass( 'Label' ) );
 	thetaLengthRow.add( thetaLength );
 	thetaLengthRow.add( thetaLength );

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

@@ -15,9 +15,10 @@ function GeometryParametersPanel( editor, object ) {
 	const options = parameters.options;
 	const options = parameters.options;
 	options.curveSegments = options.curveSegments != undefined ? options.curveSegments : 12;
 	options.curveSegments = options.curveSegments != undefined ? options.curveSegments : 12;
 	options.steps = options.steps != undefined ? options.steps : 1;
 	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.bevelOffset = options.bevelOffset !== undefined ? options.bevelOffset : 0;
 	options.bevelSegments = options.bevelSegments !== undefined ? options.bevelSegments : 3;
 	options.bevelSegments = options.bevelSegments !== undefined ? options.bevelSegments : 3;
 
 
@@ -62,59 +63,77 @@ function GeometryParametersPanel( editor, object ) {
 
 
 	container.add( enabledRow );
 	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' );
 	const button = new UIButton( strings.getKey( 'sidebar/geometry/extrude_geometry/shape' ) ).onClick( toShape ).setClass( 'Label' ).setMarginLeft( '120px' );
 	container.add( button );
 	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() {
 	function update() {
 
 
+		updateBevelRow( enabled.getValue() );
+
 		editor.execute( new SetGeometryCommand( editor, object, new THREE.ExtrudeGeometry(
 		editor.execute( new SetGeometryCommand( editor, object, new THREE.ExtrudeGeometry(
 			parameters.shapes,
 			parameters.shapes,
 			{
 			{
@@ -122,7 +141,7 @@ function GeometryParametersPanel( editor, object ) {
 				steps: steps.getValue(),
 				steps: steps.getValue(),
 				depth: depth.getValue(),
 				depth: depth.getValue(),
 				bevelEnabled: enabled.getValue(),
 				bevelEnabled: enabled.getValue(),
-				bevelThickness: options.bevelThickness,
+				bevelThickness: thickness !== undefined ? thickness.getValue() : options.bevelThickness,
 				bevelSize: size !== undefined ? size.getValue() : options.bevelSize,
 				bevelSize: size !== undefined ? size.getValue() : options.bevelSize,
 				bevelOffset: offset !== undefined ? offset.getValue() : options.bevelOffset,
 				bevelOffset: offset !== undefined ? offset.getValue() : options.bevelOffset,
 				bevelSegments: segments !== undefined ? segments.getValue() : options.bevelSegments
 				bevelSegments: segments !== undefined ? segments.getValue() : options.bevelSegments

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

@@ -56,7 +56,7 @@ function GeometryParametersPanel( editor, object ) {
 	// thetaStart
 	// thetaStart
 
 
 	const thetaStartRow = new UIRow();
 	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( new UIText( strings.getKey( 'sidebar/geometry/ring_geometry/thetastart' ) ).setClass( 'Label' ) );
 	thetaStartRow.add( thetaStart );
 	thetaStartRow.add( thetaStart );
@@ -66,7 +66,7 @@ function GeometryParametersPanel( editor, object ) {
 	// thetaLength
 	// thetaLength
 
 
 	const thetaLengthRow = new UIRow();
 	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( new UIText( strings.getKey( 'sidebar/geometry/ring_geometry/thetalength' ) ).setClass( 'Label' ) );
 	thetaLengthRow.add( thetaLength );
 	thetaLengthRow.add( thetaLength );

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

@@ -46,7 +46,7 @@ function GeometryParametersPanel( editor, object ) {
 	// phiStart
 	// phiStart
 
 
 	const phiStartRow = new UIRow();
 	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( new UIText( strings.getKey( 'sidebar/geometry/sphere_geometry/phistart' ) ).setClass( 'Label' ) );
 	phiStartRow.add( phiStart );
 	phiStartRow.add( phiStart );
@@ -56,7 +56,7 @@ function GeometryParametersPanel( editor, object ) {
 	// phiLength
 	// phiLength
 
 
 	const phiLengthRow = new UIRow();
 	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( new UIText( strings.getKey( 'sidebar/geometry/sphere_geometry/philength' ) ).setClass( 'Label' ) );
 	phiLengthRow.add( phiLength );
 	phiLengthRow.add( phiLength );
@@ -66,7 +66,7 @@ function GeometryParametersPanel( editor, object ) {
 	// thetaStart
 	// thetaStart
 
 
 	const thetaStartRow = new UIRow();
 	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( new UIText( strings.getKey( 'sidebar/geometry/sphere_geometry/thetastart' ) ).setClass( 'Label' ) );
 	thetaStartRow.add( thetaStart );
 	thetaStartRow.add( thetaStart );
@@ -76,7 +76,7 @@ function GeometryParametersPanel( editor, object ) {
 	// thetaLength
 	// thetaLength
 
 
 	const thetaLengthRow = new UIRow();
 	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( new UIText( strings.getKey( 'sidebar/geometry/sphere_geometry/thetalength' ) ).setClass( 'Label' ) );
 	thetaLengthRow.add( thetaLength );
 	thetaLengthRow.add( thetaLength );

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

@@ -56,7 +56,7 @@ function GeometryParametersPanel( editor, object ) {
 	// arc
 	// arc
 
 
 	const arcRow = new UIRow();
 	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( new UIText( strings.getKey( 'sidebar/geometry/torus_geometry/arc' ) ).setClass( 'Label' ) );
 	arcRow.add( arc );
 	arcRow.add( arc );

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

@@ -1,6 +1,6 @@
 import * as THREE from 'three';
 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';
 import { SetGeometryValueCommand } from './commands/SetGeometryValueCommand.js';
 
 
@@ -145,6 +145,53 @@ function SidebarGeometry( editor ) {
 	geometryBoundingBoxRow.add( geometryBoundingBox );
 	geometryBoundingBoxRow.add( geometryBoundingBox );
 	container.add( geometryBoundingBoxRow );
 	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
 	// Helpers
 
 
 	const helpersRow = new UIRow().setMarginLeft( '120px' );
 	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 );
 	container.add( exportJson );
@@ -251,6 +294,8 @@ function SidebarGeometry( editor ) {
 
 
 			helpersRow.setDisplay( geometry.hasAttribute( 'normal' ) ? '' : 'none' );
 			helpersRow.setDisplay( geometry.hasAttribute( 'normal' ) ? '' : 'none' );
 
 
+			geometryUserData.setValue( JSON.stringify( geometry.userData, null, '  ' ) );
+
 		} else {
 		} else {
 
 
 			container.setDisplay( 'none' );
 			container.setDisplay( 'none' );

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

@@ -430,7 +430,7 @@ function SidebarMaterial( editor ) {
 	exportJson.onClick( function () {
 	exportJson.onClick( function () {
 
 
 		const object = editor.selected;
 		const object = editor.selected;
-		const material = object.material;
+		const material = Array.isArray( object.material ) ? object.material[ currentMaterialSlot ] : object.material;
 
 
 		let output = material.toJSON();
 		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 );
 	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
 					// don't remove the entire multi-material. just the material of the selected slot
 
 
-					editor.removeMaterial( currentObject.material[ currentMaterialSlot ] );
+					editor.removeMaterial( currentMaterial[ currentMaterialSlot ] );
 
 
 				} else {
 				} 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 );
 				editor.addMaterial( material );
 				// TODO Copy other references in the scene graph
 				// TODO Copy other references in the scene graph
 				// keeping name and UUID then.
 				// 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 );
 	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 { 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 ) {
 function SidebarProjectImage( editor ) {
 
 
@@ -19,17 +19,34 @@ function SidebarProjectImage( editor ) {
 	// Shading
 	// Shading
 
 
 	const shadingRow = new UIRow();
 	const shadingRow = new UIRow();
-	// container.add( shadingRow );
+	container.add( shadingRow );
 
 
 	shadingRow.add( new UIText( strings.getKey( 'sidebar/project/shading' ) ).setClass( 'Label' ) );
 	shadingRow.add( new UIText( strings.getKey( 'sidebar/project/shading' ) ).setClass( 'Label' ) );
 
 
 	const shadingTypeSelect = new UISelect().setOptions( {
 	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 );
 	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
 	// Resolution
 
 
 	const resolutionRow = new UIRow();
 	const resolutionRow = new UIRow();
@@ -108,7 +125,7 @@ function SidebarProjectImage( editor ) {
 				renderer.dispose();
 				renderer.dispose();
 
 
 				break;
 				break;
-			/*
+
 			case 1: // REALISTIC
 			case 1: // REALISTIC
 
 
 				const status = document.createElement( 'div' );
 				const status = document.createElement( 'div' );
@@ -120,26 +137,41 @@ function SidebarProjectImage( editor ) {
 				status.style.fontSize = '12px';
 				status.style.fontSize = '12px';
 				output.document.body.appendChild( status );
 				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() {
 				function animate() {
 
 
 					if ( output.closed === true ) return;
 					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();
 				animate();
 
 
 				break;
 				break;
-			*/
 
 
 		}
 		}
 
 

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

@@ -16,17 +16,25 @@ function SidebarProjectVideo( editor ) {
 
 
 	// Resolution
 	// 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();
 	const resolutionRow = new UIRow();
 	container.add( resolutionRow );
 	container.add( resolutionRow );
 
 
 	resolutionRow.add( new UIText( strings.getKey( 'sidebar/project/resolution' ) ).setClass( 'Label' ) );
 	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( videoWidth );
 
 
 	resolutionRow.add( new UIText( '×' ).setTextAlign( 'center' ).setFontSize( '12px' ).setWidth( '12px' ) );
 	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 );
 	resolutionRow.add( videoHeight );
 
 
 	const videoFPS = new UIInteger( 30 ).setTextAlign( 'center' ).setWidth( '20px' );
 	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.style.overflow = 'hidden';
 		output.document.body.appendChild( canvas );
 		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 } ) => {
 		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 duration = videoDuration.getValue();
 		const frames = duration * fps;
 		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();
 		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 };
 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.addTab( 'scriptTab', strings.getKey( 'sidebar/properties/script' ), new SidebarScript( editor ) );
 	container.select( 'objectTab' );
 	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;
 	return container;
 
 
 }
 }

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

@@ -119,13 +119,11 @@ function SidebarScene( editor ) {
 
 
 	function getScript( uuid ) {
 	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 );
 	const backgroundIntensity = new UINumber( 1 ).setWidth( '40px' ).setRange( 0, Infinity ).onChange( onBackgroundChanged );
 	backgroundEquirectRow.add( backgroundIntensity );
 	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 );
 	backgroundEquirectRow.add( backgroundRotation );
 
 
 	container.add( backgroundEquirectRow );
 	container.add( backgroundEquirectRow );
@@ -419,12 +417,18 @@ function SidebarScene( editor ) {
 		} else {
 		} else {
 
 
 			backgroundType.setValue( 'None' );
 			backgroundType.setValue( 'None' );
+			backgroundTexture.setValue( null );
+			backgroundEquirectangularTexture.setValue( null );
 
 
 		}
 		}
 
 
 		if ( scene.environment ) {
 		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' );
 				environmentType.setValue( 'Equirectangular' );
 				environmentEquirectangularTexture.setValue( scene.environment );
 				environmentEquirectangularTexture.setValue( scene.environment );
@@ -438,6 +442,7 @@ function SidebarScene( editor ) {
 		} else {
 		} else {
 
 
 			environmentType.setValue( 'None' );
 			environmentType.setValue( 'None' );
+			environmentEquirectangularTexture.setValue( null );
 
 
 		}
 		}
 
 
@@ -491,18 +496,22 @@ function SidebarScene( editor ) {
 
 
 	signals.refreshSidebarEnvironment.add( refreshUI );
 	signals.refreshSidebarEnvironment.add( refreshUI );
 
 
-	/*
 	signals.objectChanged.add( function ( object ) {
 	signals.objectChanged.add( function ( object ) {
 
 
-		let options = outliner.options;
+		const options = outliner.options;
 
 
 		for ( let i = 0; i < options.length; i ++ ) {
 		for ( let i = 0; i < options.length; i ++ ) {
 
 
-			let option = options[ i ];
+			const option = options[ i ];
 
 
 			if ( option.value === object.id ) {
 			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;
 				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 ) {
 	signals.objectSelected.add( function ( object ) {
 
 
@@ -546,6 +567,17 @@ function SidebarScene( editor ) {
 
 
 	} );
 	} );
 
 
+	signals.sceneBackgroundChanged.add( function () {
+
+		if ( environmentType.getValue() === 'Background' ) {
+
+			onEnvironmentChanged();
+			refreshEnvironmentUI();
+
+		}
+
+	} );
+
 	return container;
 	return container;
 
 
 }
 }

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

@@ -81,7 +81,7 @@ function SidebarScript( editor ) {
 					remove.setMarginLeft( '4px' );
 					remove.setMarginLeft( '4px' );
 					remove.onClick( function () {
 					remove.onClick( function () {
 
 
-						if ( confirm( 'Are you sure?' ) ) {
+						if ( confirm( strings.getKey( 'prompt/script/remove' ) ) ) {
 
 
 							editor.execute( new RemoveScriptCommand( editor, editor.selected, script ) );
 							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 ) {
 		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 lastUndoCmd = history.undos[ history.undos.length - 1 ];
 			const lastUndoId = ( lastUndoCmd !== undefined ) ? lastUndoCmd.id : 0;
 			const lastUndoId = ( lastUndoCmd !== undefined ) ? lastUndoCmd.id : 0;
@@ -63,7 +63,7 @@ function SidebarSettingsHistory( editor ) {
 	const option = new UIButton( strings.getKey( 'sidebar/history/clear' ) );
 	const option = new UIButton( strings.getKey( 'sidebar/history/clear' ) );
 	option.onClick( function () {
 	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();
 			editor.history.clear();
 
 

+ 11 - 1
editor/js/Sidebar.js

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

+ 1 - 1
editor/js/Storage.js

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

+ 360 - 176
editor/js/Strings.js

@@ -6,63 +6,98 @@ function Strings( config ) {
 
 
 		en: {
 		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': 'File',
 			'menubar/file/new': 'New',
 			'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/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': '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/center': 'Center',
 			'menubar/edit/clone': 'Clone',
 			'menubar/edit/clone': 'Clone',
-			'menubar/edit/delete': 'Delete (Del)',
+			'menubar/edit/delete': 'Delete',
 
 
 			'menubar/add': 'Add',
 			'menubar/add': 'Add',
 			'menubar/add/group': 'Group',
 			'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': 'View',
 			'menubar/view/fullscreen': 'Fullscreen',
 			'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': 'Help',
 			'menubar/help/source_code': 'Source Code',
 			'menubar/help/source_code': 'Source Code',
@@ -123,6 +158,7 @@ function Strings( config ) {
 			'sidebar/geometry/uuid': 'UUID',
 			'sidebar/geometry/uuid': 'UUID',
 			'sidebar/geometry/name': 'Name',
 			'sidebar/geometry/name': 'Name',
 			'sidebar/geometry/bounds': 'Bounds',
 			'sidebar/geometry/bounds': 'Bounds',
+			'sidebar/geometry/userdata': 'User Data',
 			'sidebar/geometry/show_vertex_normals': 'Show Vertex Normals',
 			'sidebar/geometry/show_vertex_normals': 'Show Vertex Normals',
 			'sidebar/geometry/compute_vertex_normals': 'Compute Vertex Normals',
 			'sidebar/geometry/compute_vertex_normals': 'Compute Vertex Normals',
 			'sidebar/geometry/compute_vertex_tangents': 'Compute Tangents',
 			'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/curveSegments': 'Curve Segments',
 			'sidebar/geometry/extrude_geometry/steps': 'Steps',
 			'sidebar/geometry/extrude_geometry/steps': 'Steps',
 			'sidebar/geometry/extrude_geometry/depth': 'Depth',
 			'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/bevelThickness': 'Thickness',
 			'sidebar/geometry/extrude_geometry/bevelSize': 'Size',
 			'sidebar/geometry/extrude_geometry/bevelSize': 'Size',
 			'sidebar/geometry/extrude_geometry/bevelOffset': 'Offset',
 			'sidebar/geometry/extrude_geometry/bevelOffset': 'Offset',
@@ -321,6 +357,7 @@ function Strings( config ) {
 			'sidebar/project/app/publish': 'Publish',
 			'sidebar/project/app/publish': 'Publish',
 
 
 			'sidebar/project/image': 'Image',
 			'sidebar/project/image': 'Image',
+			'sidebar/project/image/samples': 'Samples',
 			'sidebar/project/video': 'Video',
 			'sidebar/project/video': 'Video',
 
 
 			'sidebar/project/shading': 'Shading',
 			'sidebar/project/shading': 'Shading',
@@ -350,72 +387,116 @@ function Strings( config ) {
 			'viewport/controls/grid': 'Grid',
 			'viewport/controls/grid': 'Grid',
 			'viewport/controls/helpers': 'Helpers',
 			'viewport/controls/helpers': 'Helpers',
 
 
+			'viewport/info/object': 'Object',
 			'viewport/info/objects': 'Objects',
 			'viewport/info/objects': 'Objects',
+			'viewport/info/vertex': 'Vertex',
 			'viewport/info/vertices': 'Vertices',
 			'viewport/info/vertices': 'Vertices',
+			'viewport/info/triangle': 'Triangle',
 			'viewport/info/triangles': 'Triangles',
 			'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: {
 		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': 'Fichier',
 			'menubar/file/new': 'Nouveau',
 			'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/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': '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/center': 'Center',
 			'menubar/edit/clone': 'Cloner',
 			'menubar/edit/clone': 'Cloner',
-			'menubar/edit/delete': 'Supprimer (Supp)',
+			'menubar/edit/delete': 'Supprimer',
 
 
 			'menubar/add': 'Ajouter',
 			'menubar/add': 'Ajouter',
 			'menubar/add/group': 'Groupe',
 			'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': 'View',
 			'menubar/view/fullscreen': 'Fullscreen',
 			'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': 'Aide',
 			'menubar/help/source_code': 'Code Source',
 			'menubar/help/source_code': 'Code Source',
@@ -476,6 +557,7 @@ function Strings( config ) {
 			'sidebar/geometry/uuid': 'UUID',
 			'sidebar/geometry/uuid': 'UUID',
 			'sidebar/geometry/name': 'Nom',
 			'sidebar/geometry/name': 'Nom',
 			'sidebar/geometry/bounds': 'Limites',
 			'sidebar/geometry/bounds': 'Limites',
+			'sidebar/geometry/userdata': 'Données utilisateur',
 			'sidebar/geometry/show_vertex_normals': 'Afficher normales',
 			'sidebar/geometry/show_vertex_normals': 'Afficher normales',
 			'sidebar/geometry/compute_vertex_normals': 'Compute Vertex Normals',
 			'sidebar/geometry/compute_vertex_normals': 'Compute Vertex Normals',
 			'sidebar/geometry/compute_vertex_tangents': 'Compute Tangents',
 			'sidebar/geometry/compute_vertex_tangents': 'Compute Tangents',
@@ -674,6 +756,7 @@ function Strings( config ) {
 			'sidebar/project/app/publish': 'Publier',
 			'sidebar/project/app/publish': 'Publier',
 
 
 			'sidebar/project/image': 'Image',
 			'sidebar/project/image': 'Image',
+			'sidebar/project/image/samples': 'd\'échantillons',
 			'sidebar/project/video': 'Video',
 			'sidebar/project/video': 'Video',
 
 
 			'sidebar/project/shading': 'Shading',
 			'sidebar/project/shading': 'Shading',
@@ -703,72 +786,116 @@ function Strings( config ) {
 			'viewport/controls/grid': 'Grille',
 			'viewport/controls/grid': 'Grille',
 			'viewport/controls/helpers': 'Helpers',
 			'viewport/controls/helpers': 'Helpers',
 
 
+			'viewport/info/object': 'Objet',
 			'viewport/info/objects': 'Objets',
 			'viewport/info/objects': 'Objets',
+			'viewport/info/vertex': 'Sommet',
 			'viewport/info/vertices': 'Sommets',
 			'viewport/info/vertices': 'Sommets',
+			'viewport/info/triangle': 'Triangle',
 			'viewport/info/triangles': 'Triangles',
 			'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: {
 		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': '文件',
-			'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/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': '编辑',
-			'menubar/edit/undo': '撤销 (Ctrl+Z)',
-			'menubar/edit/redo': '重做 (Ctrl+Shift+Z)',
+			'menubar/edit/undo': '撤销',
+			'menubar/edit/redo': '重做',
 			'menubar/edit/center': '居中',
 			'menubar/edit/center': '居中',
 			'menubar/edit/clone': '拷贝',
 			'menubar/edit/clone': '拷贝',
-			'menubar/edit/delete': '删除 (Del)',
+			'menubar/edit/delete': '删除',
 
 
 			'menubar/add': '添加',
 			'menubar/add': '添加',
 			'menubar/add/group': '组',
 			'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': '视图',
 			'menubar/view/fullscreen': '全屏',
 			'menubar/view/fullscreen': '全屏',
+			'menubar/view/gridHelper': '网格助手',
+			'menubar/view/cameraHelpers': '相机助手',
+			'menubar/view/lightHelpers': '光助手',
+			'menubar/view/skeletonHelpers': '骷髅助手',
 
 
 			'menubar/help': '帮助',
 			'menubar/help': '帮助',
 			'menubar/help/source_code': '源码',
 			'menubar/help/source_code': '源码',
@@ -829,6 +956,7 @@ function Strings( config ) {
 			'sidebar/geometry/uuid': '识别码',
 			'sidebar/geometry/uuid': '识别码',
 			'sidebar/geometry/name': '名称',
 			'sidebar/geometry/name': '名称',
 			'sidebar/geometry/bounds': '界限',
 			'sidebar/geometry/bounds': '界限',
+			'sidebar/geometry/userdata': '自定义数据',
 			'sidebar/geometry/show_vertex_normals': '显示顶点法线',
 			'sidebar/geometry/show_vertex_normals': '显示顶点法线',
 			'sidebar/geometry/compute_vertex_normals': '计算顶点法线',
 			'sidebar/geometry/compute_vertex_normals': '计算顶点法线',
 			'sidebar/geometry/compute_vertex_tangents': 'Compute Tangents',
 			'sidebar/geometry/compute_vertex_tangents': 'Compute Tangents',
@@ -1027,6 +1155,7 @@ function Strings( config ) {
 			'sidebar/project/app/publish': '发布',
 			'sidebar/project/app/publish': '发布',
 
 
 			'sidebar/project/image': 'Image',
 			'sidebar/project/image': 'Image',
+			'sidebar/project/image/samples': '样本',
 			'sidebar/project/video': '视频',
 			'sidebar/project/video': '视频',
 
 
 			'sidebar/project/shading': 'Shading',
 			'sidebar/project/shading': 'Shading',
@@ -1056,72 +1185,116 @@ function Strings( config ) {
 			'viewport/controls/grid': '网格',
 			'viewport/controls/grid': '网格',
 			'viewport/controls/helpers': '辅助',
 			'viewport/controls/helpers': '辅助',
 
 
+			'viewport/info/object': '物体',
 			'viewport/info/objects': '物体',
 			'viewport/info/objects': '物体',
+			'viewport/info/vertex': '顶点',
 			'viewport/info/vertices': '顶点',
 			'viewport/info/vertices': '顶点',
+			'viewport/info/triangle': '三角形',
 			'viewport/info/triangles': '三角形',
 			'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: {
 		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': 'ファイル',
-			'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/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': '編集',
-			'menubar/edit/undo': '元に戻す(Ctrl+Z)',
-			'menubar/edit/redo': 'やり直す(Ctrl+Shift+Z)',
+			'menubar/edit/undo': '元に戻す',
+			'menubar/edit/redo': 'やり直す',
 			'menubar/edit/center': '中央揃え',
 			'menubar/edit/center': '中央揃え',
 			'menubar/edit/clone': '複製',
 			'menubar/edit/clone': '複製',
-			'menubar/edit/delete': '削除(Del)',
+			'menubar/edit/delete': '削除',
 
 
 			'menubar/add': '追加',
 			'menubar/add': '追加',
 			'menubar/add/group': 'グループ',
 			'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': '表示',
 			'menubar/view/fullscreen': 'フルスクリーン',
 			'menubar/view/fullscreen': 'フルスクリーン',
+			'menubar/view/gridHelper': 'グリッドヘルパー',
+			'menubar/view/cameraHelpers': 'カメラヘルパー',
+			'menubar/view/lightHelpers': 'ライトヘルパー',
+			'menubar/view/skeletonHelpers': 'スケルトンヘルパー',
 
 
 			'menubar/help': 'ヘルプ',
 			'menubar/help': 'ヘルプ',
 			'menubar/help/source_code': 'ソースコード',
 			'menubar/help/source_code': 'ソースコード',
@@ -1182,6 +1355,7 @@ function Strings( config ) {
 			'sidebar/geometry/uuid': 'UUID',
 			'sidebar/geometry/uuid': 'UUID',
 			'sidebar/geometry/name': '名前',
 			'sidebar/geometry/name': '名前',
 			'sidebar/geometry/bounds': '境界',
 			'sidebar/geometry/bounds': '境界',
+			'sidebar/geometry/userdata': 'ユーザーデータ',
 			'sidebar/geometry/show_vertex_normals': '頂点法線を表示',
 			'sidebar/geometry/show_vertex_normals': '頂点法線を表示',
 			'sidebar/geometry/compute_vertex_normals': '頂点法線を計算',
 			'sidebar/geometry/compute_vertex_normals': '頂点法線を計算',
 			'sidebar/geometry/compute_vertex_tangents': '接線を計算',
 			'sidebar/geometry/compute_vertex_tangents': '接線を計算',
@@ -1220,7 +1394,7 @@ function Strings( config ) {
 			'sidebar/geometry/extrude_geometry/curveSegments': '分割数',
 			'sidebar/geometry/extrude_geometry/curveSegments': '分割数',
 			'sidebar/geometry/extrude_geometry/steps': 'ステップ',
 			'sidebar/geometry/extrude_geometry/steps': 'ステップ',
 			'sidebar/geometry/extrude_geometry/depth': '深さ',
 			'sidebar/geometry/extrude_geometry/depth': '深さ',
-			'sidebar/geometry/extrude_geometry/bevelEnabled': 'ベベルを有効にするか',
+			'sidebar/geometry/extrude_geometry/bevelEnabled': 'ベベルを有効にするか',
 			'sidebar/geometry/extrude_geometry/bevelThickness': 'ベベルの厚さ',
 			'sidebar/geometry/extrude_geometry/bevelThickness': 'ベベルの厚さ',
 			'sidebar/geometry/extrude_geometry/bevelSize': 'ベベルのサイズ',
 			'sidebar/geometry/extrude_geometry/bevelSize': 'ベベルのサイズ',
 			'sidebar/geometry/extrude_geometry/bevelOffset': 'ベベルのオフセット',
 			'sidebar/geometry/extrude_geometry/bevelOffset': 'ベベルのオフセット',
@@ -1380,6 +1554,7 @@ function Strings( config ) {
 			'sidebar/project/app/publish': 'アプリファイルとして保存',
 			'sidebar/project/app/publish': 'アプリファイルとして保存',
 
 
 			'sidebar/project/image': '画像',
 			'sidebar/project/image': '画像',
+			'sidebar/project/image/samples': 'サンプル',
 			'sidebar/project/video': '動画',
 			'sidebar/project/video': '動画',
 
 
 			'sidebar/project/shading': 'シェーディング',
 			'sidebar/project/shading': 'シェーディング',
@@ -1409,10 +1584,19 @@ function Strings( config ) {
 			'viewport/controls/grid': 'グリッド',
 			'viewport/controls/grid': 'グリッド',
 			'viewport/controls/helpers': 'オーバーレイ表示',
 			'viewport/controls/helpers': 'オーバーレイ表示',
 
 
+			'viewport/info/object': 'オブジェクト',
 			'viewport/info/objects': 'オブジェクト',
 			'viewport/info/objects': 'オブジェクト',
+			'viewport/info/vertex': '頂点',
 			'viewport/info/vertices': '頂点',
 			'viewport/info/vertices': '頂点',
+			'viewport/info/triangle': '三角形',
 			'viewport/info/triangles': '三角形',
 			'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 { UIPanel, UISelect } from './libs/ui.js';
-import { UIBoolean } from './libs/ui.three.js';
 
 
 function ViewportControls( editor ) {
 function ViewportControls( editor ) {
 
 
 	const signals = editor.signals;
 	const signals = editor.signals;
-	const strings = editor.strings;
 
 
 	const container = new UIPanel();
 	const container = new UIPanel();
 	container.setPosition( 'absolute' );
 	container.setPosition( 'absolute' );
@@ -12,26 +10,6 @@ function ViewportControls( editor ) {
 	container.setTop( '10px' );
 	container.setTop( '10px' );
 	container.setColor( '#ffffff' );
 	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
 	// camera
 
 
 	const cameraSelect = new UISelect();
 	const cameraSelect = new UISelect();
@@ -46,6 +24,15 @@ function ViewportControls( editor ) {
 
 
 	signals.cameraAdded.add( update );
 	signals.cameraAdded.add( update );
 	signals.cameraRemoved.add( update );
 	signals.cameraRemoved.add( update );
+	signals.objectChanged.add( function ( object ) {
+
+		if ( object.isCamera ) {
+
+			update();
+
+		}
+
+	} );
 
 
 	// shading
 	// shading
 
 
@@ -61,11 +48,15 @@ function ViewportControls( editor ) {
 
 
 	signals.editorCleared.add( function () {
 	signals.editorCleared.add( function () {
 
 
+		editor.setViewportCamera( editor.camera.uuid );
+
 		shadingSelect.setValue( 'solid' );
 		shadingSelect.setValue( 'solid' );
 		editor.setViewportShading( shadingSelect.getValue() );
 		editor.setViewportShading( shadingSelect.getValue() );
 
 
 	} );
 	} );
 
 
+	signals.cameraResetted.add( update );
+
 	update();
 	update();
 
 
 	//
 	//
@@ -84,7 +75,13 @@ function ViewportControls( editor ) {
 		}
 		}
 
 
 		cameraSelect.setOptions( options );
 		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.setColor( '#fff' );
 	container.setTextTransform( 'lowercase' );
 	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 verticesText = new UIText( '0' ).setTextAlign( 'right' ).setWidth( '60px' ).setMarginRight( '6px' );
 	const trianglesText = 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 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( frametimeText, new UIText( strings.getKey( 'viewport/info/rendertime' ) ), new UIBreak() );
+	container.add( samplesText, samplesUnitText, new UIBreak() );
 
 
 	signals.objectAdded.add( update );
 	signals.objectAdded.add( update );
 	signals.objectRemoved.add( update );
 	signals.objectRemoved.add( update );
@@ -31,6 +38,10 @@ function ViewportInfo( editor ) {
 
 
 	//
 	//
 
 
+	const pluralRules = new Intl.PluralRules( editor.config.getKey( 'language' ) );
+
+	//
+
 	function update() {
 	function update() {
 
 
 		const scene = editor.scene;
 		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;
 	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 ) {
 function ViewportPathtracer( renderer ) {
 
 
-	let generator = null;
-	let pathtracer = null;
-	let quad = null;
-	let hdr = null;
+	let pathTracer = null;
 
 
 	function init( scene, camera ) {
 	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() {
 	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,
 		setSize: setSize,
 		setBackground: setBackground,
 		setBackground: setBackground,
 		setEnvironment: setEnvironment,
 		setEnvironment: setEnvironment,
+		updateMaterials: updateMaterials,
 		update: update,
 		update: update,
-		reset: reset
+		reset: reset,
+		getSamples: getSamples
 	};
 	};
 
 
 }
 }

+ 94 - 22
editor/js/Viewport.js

@@ -152,8 +152,29 @@ function Viewport( editor ) {
 
 
 	function updateAspectRatio() {
 	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 );
 		transformControls.setSpace( space );
 
 
+		render();
+
 	} );
 	} );
 
 
 	signals.rendererUpdated.add( function () {
 	signals.rendererUpdated.add( function () {
@@ -462,7 +485,7 @@ function Viewport( editor ) {
 
 
 	signals.materialChanged.add( function () {
 	signals.materialChanged.add( function () {
 
 
-		initPT();
+		updatePTMaterials();
 		render();
 		render();
 
 
 	} );
 	} );
@@ -538,9 +561,13 @@ function Viewport( editor ) {
 
 
 				useBackgroundAsEnvironment = true;
 				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;
 				break;
 
 
@@ -614,14 +641,9 @@ function Viewport( editor ) {
 
 
 		const viewportCamera = editor.viewportCamera;
 		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 );
 		controls.enabled = ( viewportCamera === editor.camera );
 
 
+		initPT();
 		render();
 		render();
 
 
 	} );
 	} );
@@ -640,7 +663,7 @@ function Viewport( editor ) {
 		switch ( viewportShading ) {
 		switch ( viewportShading ) {
 
 
 			case 'realistic':
 			case 'realistic':
-				pathtracer.init( scene, camera );
+				pathtracer.init( scene, editor.viewportCamera );
 				break;
 				break;
 
 
 			case 'solid':
 			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();
 		render();
 
 
@@ -751,7 +812,7 @@ function Viewport( editor ) {
 
 
 		if ( editor.viewportShading === 'realistic' ) {
 		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() {
 	function updatePT() {
 
 
 		if ( editor.viewportShading === 'realistic' ) {
 		if ( editor.viewportShading === 'realistic' ) {
 
 
 			pathtracer.update();
 			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 {
 class AddObjectCommand extends Command {
 
 
-	constructor( editor, object ) {
+	constructor( editor, object = null ) {
 
 
 		super( editor );
 		super( editor );
 
 
 		this.type = 'AddObjectCommand';
 		this.type = 'AddObjectCommand';
 
 
 		this.object = object;
 		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 {
 class AddScriptCommand extends Command {
 
 
-	constructor( editor, object, script ) {
+	constructor( editor, object = null, script = '' ) {
 
 
 		super( editor );
 		super( editor );
 
 
 		this.type = 'AddScriptCommand';
 		this.type = 'AddScriptCommand';
-		this.name = 'Add Script';
+		this.name = editor.strings.getKey( 'command/AddScript' );
 
 
 		this.object = object;
 		this.object = object;
 		this.script = script;
 		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 { SetMaterialColorCommand } from './SetMaterialColorCommand.js';
 export { SetMaterialCommand } from './SetMaterialCommand.js';
 export { SetMaterialCommand } from './SetMaterialCommand.js';
 export { SetMaterialMapCommand } from './SetMaterialMapCommand.js';
 export { SetMaterialMapCommand } from './SetMaterialMapCommand.js';
+export { SetMaterialRangeCommand } from './SetMaterialRangeCommand.js';
 export { SetMaterialValueCommand } from './SetMaterialValueCommand.js';
 export { SetMaterialValueCommand } from './SetMaterialValueCommand.js';
 export { SetMaterialVectorCommand } from './SetMaterialVectorCommand.js';
 export { SetMaterialVectorCommand } from './SetMaterialVectorCommand.js';
 export { SetPositionCommand } from './SetPositionCommand.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 {
 class MoveObjectCommand extends Command {
 
 
-	constructor( editor, object, newParent, newBefore ) {
+	constructor( editor, object = null, newParent = null, newBefore = null ) {
 
 
 		super( editor );
 		super( editor );
 
 
 		this.type = 'MoveObjectCommand';
 		this.type = 'MoveObjectCommand';
-		this.name = 'Move Object';
+		this.name = editor.strings.getKey( 'command/MoveObject' );
 
 
 		this.object = object;
 		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;
 		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 {
 		} 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 {
 class MultiCmdsCommand extends Command {
 
 
-	constructor( editor, cmdArray ) {
+	constructor( editor, cmdArray = [] ) {
 
 
 		super( editor );
 		super( editor );
 
 
 		this.type = 'MultiCmdsCommand';
 		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 {
 class RemoveObjectCommand extends Command {
 
 
-	constructor( editor, object ) {
+	constructor( editor, object = null ) {
 
 
 		super( editor );
 		super( editor );
 
 
 		this.type = 'RemoveObjectCommand';
 		this.type = 'RemoveObjectCommand';
-		this.name = 'Remove Object';
 
 
 		this.object = 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 );
 			this.index = this.parent.children.indexOf( this.object );
 
 
 		}
 		}
 
 
+		if ( object !== null ) {
+
+			this.name = editor.strings.getKey( 'command/RemoveObject' ) + ': ' + object.name;
+
+
+		}
+
 	}
 	}
 
 
 	execute() {
 	execute() {

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

@@ -8,16 +8,17 @@ import { Command } from '../Command.js';
  */
  */
 class RemoveScriptCommand extends Command {
 class RemoveScriptCommand extends Command {
 
 
-	constructor( editor, object, script ) {
+	constructor( editor, object = null, script = '' ) {
 
 
 		super( editor );
 		super( editor );
 
 
 		this.type = 'RemoveScriptCommand';
 		this.type = 'RemoveScriptCommand';
-		this.name = 'Remove Script';
+		this.name = editor.strings.getKey( 'command/RemoveScript' );
 
 
 		this.object = object;
 		this.object = object;
 		this.script = script;
 		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 );
 			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 {
 class SetColorCommand extends Command {
 
 
-	constructor( editor, object, attributeName, newValue ) {
+	constructor( editor, object = null, attributeName = '', newValue = null ) {
 
 
 		super( editor );
 		super( editor );
 
 
 		this.type = 'SetColorCommand';
 		this.type = 'SetColorCommand';
-		this.name = `Set ${attributeName}`;
+		this.name = editor.strings.getKey( 'command/SetColor' ) + ': ' + attributeName;
 		this.updatable = true;
 		this.updatable = true;
 
 
 		this.object = object;
 		this.object = object;
 		this.attributeName = attributeName;
 		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;
 		this.newValue = newValue;
 
 
 	}
 	}

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

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

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

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

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

@@ -9,20 +9,20 @@ import { Command } from '../Command.js';
  */
  */
 class SetMaterialColorCommand extends Command {
 class SetMaterialColorCommand extends Command {
 
 
-	constructor( editor, object, attributeName, newValue, materialSlot ) {
+	constructor( editor, object = null, attributeName = '', newValue = null, materialSlot = - 1 ) {
 
 
 		super( editor );
 		super( editor );
 
 
 		this.type = 'SetMaterialColorCommand';
 		this.type = 'SetMaterialColorCommand';
-		this.name = `Set Material.${attributeName}`;
+		this.name = editor.strings.getKey( 'command/SetMaterialColor' ) + ': ' + attributeName;
 		this.updatable = true;
 		this.updatable = true;
 
 
 		this.object = object;
 		this.object = object;
 		this.materialSlot = materialSlot;
 		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.newValue = newValue;
 
 
 		this.attributeName = attributeName;
 		this.attributeName = attributeName;
@@ -31,7 +31,9 @@ class SetMaterialColorCommand extends Command {
 
 
 	execute() {
 	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 );
 		this.editor.signals.materialChanged.dispatch( this.object, this.materialSlot );
 
 
@@ -39,7 +41,9 @@ class SetMaterialColorCommand extends Command {
 
 
 	undo() {
 	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 );
 		this.editor.signals.materialChanged.dispatch( this.object, this.materialSlot );
 
 
@@ -59,6 +63,7 @@ class SetMaterialColorCommand extends Command {
 		output.attributeName = this.attributeName;
 		output.attributeName = this.attributeName;
 		output.oldValue = this.oldValue;
 		output.oldValue = this.oldValue;
 		output.newValue = this.newValue;
 		output.newValue = this.newValue;
+		output.materialSlot = this.materialSlot;
 
 
 		return output;
 		return output;
 
 
@@ -72,6 +77,7 @@ class SetMaterialColorCommand extends Command {
 		this.attributeName = json.attributeName;
 		this.attributeName = json.attributeName;
 		this.oldValue = json.oldValue;
 		this.oldValue = json.oldValue;
 		this.newValue = json.newValue;
 		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 {
 class SetMaterialCommand extends Command {
 
 
-	constructor( editor, object, newMaterial, materialSlot ) {
+	constructor( editor, object = null, newMaterial = null, materialSlot = - 1 ) {
 
 
 		super( editor );
 		super( editor );
 
 
 		this.type = 'SetMaterialCommand';
 		this.type = 'SetMaterialCommand';
-		this.name = 'New Material';
+		this.name = editor.strings.getKey( 'command/SetMaterial' );
 
 
 		this.object = object;
 		this.object = object;
 		this.materialSlot = materialSlot;
 		this.materialSlot = materialSlot;
 
 
-		this.oldMaterial = this.editor.getObjectMaterial( object, materialSlot );
+		this.oldMaterial = ( object !== null ) ? editor.getObjectMaterial( object, materialSlot ) : null;
 		this.newMaterial = newMaterial;
 		this.newMaterial = newMaterial;
 
 
 	}
 	}
@@ -47,6 +47,7 @@ class SetMaterialCommand extends Command {
 		output.objectUuid = this.object.uuid;
 		output.objectUuid = this.object.uuid;
 		output.oldMaterial = this.oldMaterial.toJSON();
 		output.oldMaterial = this.oldMaterial.toJSON();
 		output.newMaterial = this.newMaterial.toJSON();
 		output.newMaterial = this.newMaterial.toJSON();
+		output.materialSlot = this.materialSlot;
 
 
 		return output;
 		return output;
 
 
@@ -59,6 +60,7 @@ class SetMaterialCommand extends Command {
 		this.object = this.editor.objectByUuid( json.objectUuid );
 		this.object = this.editor.objectByUuid( json.objectUuid );
 		this.oldMaterial = parseMaterial( json.oldMaterial );
 		this.oldMaterial = parseMaterial( json.oldMaterial );
 		this.newMaterial = parseMaterial( json.newMaterial );
 		this.newMaterial = parseMaterial( json.newMaterial );
+		this.materialSlot = json.materialSlot;
 
 
 		function parseMaterial( json ) {
 		function parseMaterial( json ) {
 
 

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

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

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

@@ -10,20 +10,20 @@ import { Command } from '../Command.js';
  */
  */
 class SetMaterialRangeCommand extends Command {
 class SetMaterialRangeCommand extends Command {
 
 
-	constructor( editor, object, attributeName, newMinValue, newMaxValue, materialSlot ) {
+	constructor( editor, object = null, attributeName = '', newMinValue = - Infinity, newMaxValue = Infinity, materialSlot = - 1 ) {
 
 
 		super( editor );
 		super( editor );
 
 
 		this.type = 'SetMaterialRangeCommand';
 		this.type = 'SetMaterialRangeCommand';
-		this.name = `Set Material.${attributeName}`;
+		this.name = editor.strings.getKey( 'command/SetMaterialRange' ) + ': ' + attributeName;
 		this.updatable = true;
 		this.updatable = true;
 
 
 		this.object = object;
 		this.object = object;
 		this.materialSlot = materialSlot;
 		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.newRange = [ newMinValue, newMaxValue ];
 
 
 		this.attributeName = attributeName;
 		this.attributeName = attributeName;
@@ -32,8 +32,10 @@ class SetMaterialRangeCommand extends Command {
 
 
 	execute() {
 	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.objectChanged.dispatch( this.object );
 		this.editor.signals.materialChanged.dispatch( this.object, this.materialSlot );
 		this.editor.signals.materialChanged.dispatch( this.object, this.materialSlot );
@@ -42,8 +44,10 @@ class SetMaterialRangeCommand extends Command {
 
 
 	undo() {
 	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.objectChanged.dispatch( this.object );
 		this.editor.signals.materialChanged.dispatch( this.object, this.materialSlot );
 		this.editor.signals.materialChanged.dispatch( this.object, this.materialSlot );
@@ -64,6 +68,7 @@ class SetMaterialRangeCommand extends Command {
 		output.attributeName = this.attributeName;
 		output.attributeName = this.attributeName;
 		output.oldRange = [ ...this.oldRange ];
 		output.oldRange = [ ...this.oldRange ];
 		output.newRange = [ ...this.newRange ];
 		output.newRange = [ ...this.newRange ];
+		output.materialSlot = this.materialSlot;
 
 
 		return output;
 		return output;
 
 
@@ -77,6 +82,7 @@ class SetMaterialRangeCommand extends Command {
 		this.oldRange = [ ...json.oldRange ];
 		this.oldRange = [ ...json.oldRange ];
 		this.newRange = [ ...json.newRange ];
 		this.newRange = [ ...json.newRange ];
 		this.object = this.editor.objectByUuid( json.objectUuid );
 		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 {
 class SetMaterialValueCommand extends Command {
 
 
-	constructor( editor, object, attributeName, newValue, materialSlot ) {
+	constructor( editor, object = null, attributeName = '', newValue = null, materialSlot = - 1 ) {
 
 
 		super( editor );
 		super( editor );
 
 
 		this.type = 'SetMaterialValueCommand';
 		this.type = 'SetMaterialValueCommand';
-		this.name = `Set Material.${attributeName}`;
+		this.name = editor.strings.getKey( 'command/SetMaterialValue' ) + ': ' + attributeName;
 		this.updatable = true;
 		this.updatable = true;
 
 
 		this.object = object;
 		this.object = object;
 		this.materialSlot = materialSlot;
 		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.newValue = newValue;
 
 
 		this.attributeName = attributeName;
 		this.attributeName = attributeName;
@@ -31,8 +31,10 @@ class SetMaterialValueCommand extends Command {
 
 
 	execute() {
 	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.objectChanged.dispatch( this.object );
 		this.editor.signals.materialChanged.dispatch( this.object, this.materialSlot );
 		this.editor.signals.materialChanged.dispatch( this.object, this.materialSlot );
@@ -41,8 +43,10 @@ class SetMaterialValueCommand extends Command {
 
 
 	undo() {
 	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.objectChanged.dispatch( this.object );
 		this.editor.signals.materialChanged.dispatch( this.object, this.materialSlot );
 		this.editor.signals.materialChanged.dispatch( this.object, this.materialSlot );
@@ -63,6 +67,7 @@ class SetMaterialValueCommand extends Command {
 		output.attributeName = this.attributeName;
 		output.attributeName = this.attributeName;
 		output.oldValue = this.oldValue;
 		output.oldValue = this.oldValue;
 		output.newValue = this.newValue;
 		output.newValue = this.newValue;
+		output.materialSlot = this.materialSlot;
 
 
 		return output;
 		return output;
 
 
@@ -76,6 +81,7 @@ class SetMaterialValueCommand extends Command {
 		this.oldValue = json.oldValue;
 		this.oldValue = json.oldValue;
 		this.newValue = json.newValue;
 		this.newValue = json.newValue;
 		this.object = this.editor.objectByUuid( json.objectUuid );
 		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 {
 class SetMaterialVectorCommand extends Command {
 
 
-	constructor( editor, object, attributeName, newValue, materialSlot ) {
+	constructor( editor, object = null, attributeName = '', newValue = null, materialSlot = - 1 ) {
 
 
 		super( editor );
 		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.updatable = true;
 
 
 		this.object = object;
 		this.object = object;
 		this.materialSlot = materialSlot;
 		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.newValue = newValue;
 
 
 		this.attributeName = attributeName;
 		this.attributeName = attributeName;
@@ -24,7 +24,9 @@ class SetMaterialVectorCommand extends Command {
 
 
 	execute() {
 	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 );
 		this.editor.signals.materialChanged.dispatch( this.object, this.materialSlot );
 
 
@@ -32,7 +34,9 @@ class SetMaterialVectorCommand extends Command {
 
 
 	undo() {
 	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 );
 		this.editor.signals.materialChanged.dispatch( this.object, this.materialSlot );
 
 
@@ -52,6 +56,7 @@ class SetMaterialVectorCommand extends Command {
 		output.attributeName = this.attributeName;
 		output.attributeName = this.attributeName;
 		output.oldValue = this.oldValue;
 		output.oldValue = this.oldValue;
 		output.newValue = this.newValue;
 		output.newValue = this.newValue;
+		output.materialSlot = this.materialSlot;
 
 
 		return output;
 		return output;
 
 
@@ -65,6 +70,7 @@ class SetMaterialVectorCommand extends Command {
 		this.attributeName = json.attributeName;
 		this.attributeName = json.attributeName;
 		this.oldValue = json.oldValue;
 		this.oldValue = json.oldValue;
 		this.newValue = json.newValue;
 		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 {
 class SetPositionCommand extends Command {
 
 
-	constructor( editor, object, newPosition, optionalOldPosition ) {
+	constructor( editor, object = null, newPosition = null, optionalOldPosition = null ) {
 
 
 		super( editor );
 		super( editor );
 
 
 		this.type = 'SetPositionCommand';
 		this.type = 'SetPositionCommand';
-		this.name = 'Set Position';
+		this.name = editor.strings.getKey( 'command/SetPosition' );
 		this.updatable = true;
 		this.updatable = true;
 
 
 		this.object = object;
 		this.object = object;
 
 
-		if ( object !== undefined && newPosition !== undefined ) {
+		if ( object !== null && newPosition !== null ) {
 
 
 			this.oldPosition = object.position.clone();
 			this.oldPosition = object.position.clone();
 			this.newPosition = newPosition.clone();
 			this.newPosition = newPosition.clone();
 
 
 		}
 		}
 
 
-		if ( optionalOldPosition !== undefined ) {
+		if ( optionalOldPosition !== null ) {
 
 
 			this.oldPosition = optionalOldPosition.clone();
 			this.oldPosition = optionalOldPosition.clone();
 
 

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

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

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

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

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

@@ -10,16 +10,16 @@ import { AddObjectCommand } from './AddObjectCommand.js';
  */
  */
 class SetSceneCommand extends Command {
 class SetSceneCommand extends Command {
 
 
-	constructor( editor, scene ) {
+	constructor( editor, scene = null ) {
 
 
 		super( editor );
 		super( editor );
 
 
 		this.type = 'SetSceneCommand';
 		this.type = 'SetSceneCommand';
-		this.name = 'Set Scene';
+		this.name = editor.strings.getKey( 'command/SetScene' );
 
 
 		this.cmdArray = [];
 		this.cmdArray = [];
 
 
-		if ( scene !== undefined ) {
+		if ( scene !== null ) {
 
 
 			this.cmdArray.push( new SetUuidCommand( this.editor, this.editor.scene, scene.uuid ) );
 			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 ) );
 			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 {
 class SetScriptValueCommand extends Command {
 
 
-	constructor( editor, object, script, attributeName, newValue ) {
+	constructor( editor, object = null, script = '', attributeName = '', newValue = null ) {
 
 
 		super( editor );
 		super( editor );
 
 
 		this.type = 'SetScriptValueCommand';
 		this.type = 'SetScriptValueCommand';
-		this.name = `Set Script.${attributeName}`;
+		this.name = editor.strings.getKey( 'command/SetScriptValue' ) + ': ' + attributeName;
 		this.updatable = true;
 		this.updatable = true;
 
 
 		this.object = object;
 		this.object = object;
 		this.script = script;
 		this.script = script;
 
 
 		this.attributeName = attributeName;
 		this.attributeName = attributeName;
-		this.oldValue = ( script !== undefined ) ? script[ this.attributeName ] : undefined;
+		this.oldValue = ( script !== '' ) ? script[ this.attributeName ] : null;
 		this.newValue = newValue;
 		this.newValue = newValue;
 
 
 	}
 	}
@@ -31,7 +31,7 @@ class SetScriptValueCommand extends Command {
 
 
 		this.script[ this.attributeName ] = this.newValue;
 		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.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 {
 class SetUuidCommand extends Command {
 
 
-	constructor( editor, object, newUuid ) {
+	constructor( editor, object = null, newUuid = null ) {
 
 
 		super( editor );
 		super( editor );
 
 
 		this.type = 'SetUuidCommand';
 		this.type = 'SetUuidCommand';
-		this.name = 'Update UUID';
+		this.name = editor.strings.getKey( 'command/SetUuid' );
 
 
 		this.object = object;
 		this.object = object;
 
 
-		this.oldUuid = ( object !== undefined ) ? object.uuid : undefined;
+		this.oldUuid = ( object !== null ) ? object.uuid : null;
 		this.newUuid = newUuid;
 		this.newUuid = newUuid;
 
 
 	}
 	}

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

@@ -9,17 +9,17 @@ import { Command } from '../Command.js';
  */
  */
 class SetValueCommand extends Command {
 class SetValueCommand extends Command {
 
 
-	constructor( editor, object, attributeName, newValue ) {
+	constructor( editor, object = null, attributeName = '', newValue = null ) {
 
 
 		super( editor );
 		super( editor );
 
 
 		this.type = 'SetValueCommand';
 		this.type = 'SetValueCommand';
-		this.name = `Set ${attributeName}`;
+		this.name = editor.strings.getKey( 'command/SetValue' ) + ': ' + attributeName;
 		this.updatable = true;
 		this.updatable = true;
 
 
 		this.object = object;
 		this.object = object;
 		this.attributeName = attributeName;
 		this.attributeName = attributeName;
-		this.oldValue = ( object !== undefined ) ? object[ attributeName ] : undefined;
+		this.oldValue = ( object !== null ) ? object[ attributeName ] : null;
 		this.newValue = newValue;
 		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 ) {
 	setStyle( style, array ) {
 
 
 		for ( let i = 0; i < array.length; i ++ ) {
 		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 ) {
 	setDisabled( value ) {
 
 
 		this.dom.disabled = value;
 		this.dom.disabled = value;
@@ -151,7 +173,7 @@ const properties = [ 'position', 'left', 'top', 'right', 'bottom', 'width', 'hei
 
 
 properties.forEach( function ( property ) {
 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 () {
 	UIElement.prototype[ method ] = function () {
 
 
@@ -314,7 +336,7 @@ class UITextArea extends UIElement {
 
 
 			event.stopPropagation();
 			event.stopPropagation();
 
 
-			if ( event.keyCode === 9 ) {
+			if ( event.code === 'Tab' ) {
 
 
 				event.preventDefault();
 				event.preventDefault();
 
 
@@ -494,7 +516,7 @@ class UIColor extends UIElement {
 
 
 	getHexValue() {
 	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 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 distance = 0;
 		let onMouseDownValue = 0;
 		let onMouseDownValue = 0;
@@ -686,19 +707,19 @@ class UINumber extends UIElement {
 
 
 			event.stopPropagation();
 			event.stopPropagation();
 
 
-			switch ( event.keyCode ) {
+			switch ( event.code ) {
 
 
-				case 13: // enter
+				case 'Enter':
 					scope.dom.blur();
 					scope.dom.blur();
 					break;
 					break;
 
 
-				case 38: // up
+				case 'ArrowUp':
 					event.preventDefault();
 					event.preventDefault();
 					scope.setValue( scope.getValue() + scope.nudge );
 					scope.setValue( scope.getValue() + scope.nudge );
 					scope.dom.dispatchEvent( changeEvent );
 					scope.dom.dispatchEvent( changeEvent );
 					break;
 					break;
 
 
-				case 40: // down
+				case 'ArrowDown':
 					event.preventDefault();
 					event.preventDefault();
 					scope.setValue( scope.getValue() - scope.nudge );
 					scope.setValue( scope.getValue() - scope.nudge );
 					scope.dom.dispatchEvent( changeEvent );
 					scope.dom.dispatchEvent( changeEvent );
@@ -782,6 +803,8 @@ class UINumber extends UIElement {
 
 
 		this.unit = unit;
 		this.unit = unit;
 
 
+		this.setValue( this.value );
+
 		return this;
 		return this;
 
 
 	}
 	}
@@ -812,8 +835,7 @@ class UIInteger extends UIElement {
 
 
 		const scope = this;
 		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 distance = 0;
 		let onMouseDownValue = 0;
 		let onMouseDownValue = 0;
@@ -901,19 +923,19 @@ class UIInteger extends UIElement {
 
 
 			event.stopPropagation();
 			event.stopPropagation();
 
 
-			switch ( event.keyCode ) {
+			switch ( event.code ) {
 
 
-				case 13: // enter
+				case 'Enter':
 					scope.dom.blur();
 					scope.dom.blur();
 					break;
 					break;
 
 
-				case 38: // up
+				case 'ArrowUp':
 					event.preventDefault();
 					event.preventDefault();
 					scope.setValue( scope.getValue() + scope.nudge );
 					scope.setValue( scope.getValue() + scope.nudge );
 					scope.dom.dispatchEvent( changeEvent );
 					scope.dom.dispatchEvent( changeEvent );
 					break;
 					break;
 
 
-				case 40: // down
+				case 'ArrowDown':
 					event.preventDefault();
 					event.preventDefault();
 					scope.setValue( scope.getValue() - scope.nudge );
 					scope.setValue( scope.getValue() - scope.nudge );
 					scope.dom.dispatchEvent( changeEvent );
 					scope.dom.dispatchEvent( changeEvent );
@@ -1119,6 +1141,26 @@ class UITabbedPanel extends UIDiv {
 
 
 		this.selected = id;
 		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;
 		return this;
 
 
 	}
 	}
@@ -1266,8 +1308,7 @@ class UIListbox extends UIDiv {
 
 
 		this.selectedValue = value;
 		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 );
 		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 { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
 import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
 import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
 import { TGALoader } from 'three/addons/loaders/TGALoader.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 { UISpan, UIDiv, UIRow, UIButton, UICheckbox, UIText, UINumber } from './ui.js';
 import { MoveObjectCommand } from '../commands/MoveObjectCommand.js';
 import { MoveObjectCommand } from '../commands/MoveObjectCommand.js';
@@ -270,10 +271,10 @@ class UIOutliner extends UIDiv {
 		// Prevent native scroll behavior
 		// Prevent native scroll behavior
 		this.dom.addEventListener( 'keydown', function ( event ) {
 		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.preventDefault();
 					event.stopPropagation();
 					event.stopPropagation();
 					break;
 					break;
@@ -285,12 +286,12 @@ class UIOutliner extends UIDiv {
 		// Keybindings to support arrow navigation
 		// Keybindings to support arrow navigation
 		this.dom.addEventListener( 'keyup', function ( event ) {
 		this.dom.addEventListener( 'keyup', function ( event ) {
 
 
-			switch ( event.keyCode ) {
+			switch ( event.code ) {
 
 
-				case 38: // up
+				case 'ArrowUp':
 					scope.selectIndex( scope.selectedIndex - 1 );
 					scope.selectIndex( scope.selectedIndex - 1 );
 					break;
 					break;
-				case 40: // down
+				case 'ArrowDown':
 					scope.selectIndex( scope.selectedIndex + 1 );
 					scope.selectIndex( scope.selectedIndex + 1 );
 					break;
 					break;
 
 
@@ -312,8 +313,7 @@ class UIOutliner extends UIDiv {
 
 
 			this.setValue( this.options[ index ].value );
 			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 );
 			this.dom.dispatchEvent( changeEvent );
 
 
 		}
 		}
@@ -334,8 +334,7 @@ class UIOutliner extends UIDiv {
 
 
 			scope.setValue( this.value );
 			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 );
 			scope.dom.dispatchEvent( changeEvent );
 
 
 		}
 		}
@@ -448,8 +447,7 @@ class UIOutliner extends UIDiv {
 			const editor = scope.editor;
 			const editor = scope.editor;
 			editor.execute( new MoveObjectCommand( editor, object, newParent, nextObject ) );
 			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 );
 			scope.dom.dispatchEvent( changeEvent );
 
 
 		}
 		}
@@ -551,9 +549,7 @@ class UIPoints extends UISpan {
 		this.lastPointIdx = 0;
 		this.lastPointIdx = 0;
 		this.onChangeCallback = null;
 		this.onChangeCallback = null;
 
 
-		// TODO Remove this bind() stuff
-
-		this.update = function () {
+		this.update = () => { // bind lexical this
 
 
 			if ( this.onChangeCallback !== null ) {
 			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 ) {
 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;
 	return renderer.domElement;
 
 

+ 0 - 2
editor/sw.js

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

+ 37 - 32
examples/files.json

@@ -255,6 +255,8 @@
 	],
 	],
 	"webgl / advanced": [
 	"webgl / advanced": [
 		"webgl_buffergeometry",
 		"webgl_buffergeometry",
+		"webgl_buffergeometry_attributes_integer",
+		"webgl_buffergeometry_attributes_none",
 		"webgl_buffergeometry_compression",
 		"webgl_buffergeometry_compression",
 		"webgl_buffergeometry_custom_attributes_particles",
 		"webgl_buffergeometry_custom_attributes_particles",
 		"webgl_buffergeometry_drawrange",
 		"webgl_buffergeometry_drawrange",
@@ -270,6 +272,7 @@
 		"webgl_buffergeometry_rawshader",
 		"webgl_buffergeometry_rawshader",
 		"webgl_buffergeometry_selective_draw",
 		"webgl_buffergeometry_selective_draw",
 		"webgl_buffergeometry_uint",
 		"webgl_buffergeometry_uint",
+		"webgl_clipculldistance",
 		"webgl_custom_attributes",
 		"webgl_custom_attributes",
 		"webgl_custom_attributes_lines",
 		"webgl_custom_attributes_lines",
 		"webgl_custom_attributes_points",
 		"webgl_custom_attributes_points",
@@ -280,30 +283,25 @@
 		"webgl_gpgpu_water",
 		"webgl_gpgpu_water",
 		"webgl_gpgpu_protoplanet",
 		"webgl_gpgpu_protoplanet",
 		"webgl_materials_modified",
 		"webgl_materials_modified",
+		"webgl_multiple_rendertargets",
+		"webgl_multisampled_renderbuffers",
 		"webgl_raymarching_reflect",
 		"webgl_raymarching_reflect",
+		"webgl_rendertarget_texture2darray",
 		"webgl_shadowmap_csm",
 		"webgl_shadowmap_csm",
 		"webgl_shadowmap_pcss",
 		"webgl_shadowmap_pcss",
 		"webgl_shadowmap_progressive",
 		"webgl_shadowmap_progressive",
 		"webgl_simple_gi",
 		"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"
 		"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 (wip)": [
 		"webgpu_backdrop",
 		"webgpu_backdrop",
 		"webgpu_backdrop_area",
 		"webgpu_backdrop_area",
@@ -329,6 +327,7 @@
 		"webgpu_instance_mesh",
 		"webgpu_instance_mesh",
 		"webgpu_instance_points",
 		"webgpu_instance_points",
 		"webgpu_instance_uniform",
 		"webgpu_instance_uniform",
+		"webgpu_instancing_morph",
 		"webgpu_lights_custom",
 		"webgpu_lights_custom",
 		"webgpu_lights_ies_spotlight",
 		"webgpu_lights_ies_spotlight",
 		"webgpu_lights_phong",
 		"webgpu_lights_phong",
@@ -337,27 +336,39 @@
 		"webgpu_loader_gltf",
 		"webgpu_loader_gltf",
 		"webgpu_loader_gltf_anisotropy",
 		"webgpu_loader_gltf_anisotropy",
 		"webgpu_loader_gltf_compressed",
 		"webgpu_loader_gltf_compressed",
+		"webgpu_loader_gltf_dispersion",
 		"webgpu_loader_gltf_iridescence",
 		"webgpu_loader_gltf_iridescence",
 		"webgpu_loader_gltf_sheen",
 		"webgpu_loader_gltf_sheen",
 		"webgpu_loader_gltf_transmission",
 		"webgpu_loader_gltf_transmission",
 		"webgpu_loader_materialx",
 		"webgpu_loader_materialx",
 		"webgpu_materials",
 		"webgpu_materials",
+		"webgpu_materials_displacementmap",
 		"webgpu_materials_lightmap",
 		"webgpu_materials_lightmap",
+		"webgpu_materials_matcap",
 		"webgpu_materials_sss",
 		"webgpu_materials_sss",
 		"webgpu_materials_transmission",
 		"webgpu_materials_transmission",
+		"webgpu_materials_toon",
 		"webgpu_materials_video",
 		"webgpu_materials_video",
 		"webgpu_materialx_noise",
 		"webgpu_materialx_noise",
-		"webgpu_multiple_rendertargets",
-		"webgpu_multiple_rendertargets_readback",
+		"webgpu_mesh_batch",
+		"webgpu_mirror",
 		"webgpu_morphtargets",
 		"webgpu_morphtargets",
 		"webgpu_morphtargets_face",
 		"webgpu_morphtargets_face",
+		"webgpu_multiple_rendertargets",
+		"webgpu_multiple_rendertargets_readback",
+		"webgpu_multisampled_renderbuffers",
 		"webgpu_occlusion",
 		"webgpu_occlusion",
 		"webgpu_parallax_uv",
 		"webgpu_parallax_uv",
 		"webgpu_particles",
 		"webgpu_particles",
+		"webgpu_performance_renderbundle",
+		"webgpu_pmrem_cubemap",
+		"webgpu_pmrem_equirectangular",
+		"webgpu_pmrem_scene",
 		"webgpu_portal",
 		"webgpu_portal",
+		"webgpu_postprocessing_afterimage",
+		"webgpu_postprocessing_anamorphic",
 		"webgpu_reflection",
 		"webgpu_reflection",
 		"webgpu_rtt",
 		"webgpu_rtt",
-		"webgpu_materials_texture_partialupdate",
 		"webgpu_sandbox",
 		"webgpu_sandbox",
 		"webgpu_shadertoy",
 		"webgpu_shadertoy",
 		"webgpu_shadowmap",
 		"webgpu_shadowmap",
@@ -365,22 +376,16 @@
 		"webgpu_skinning_instancing",
 		"webgpu_skinning_instancing",
 		"webgpu_skinning_points",
 		"webgpu_skinning_points",
 		"webgpu_sprites",
 		"webgpu_sprites",
+		"webgpu_storage_buffer",
+		"webgpu_texturegrad",
 		"webgpu_textures_2d-array",
 		"webgpu_textures_2d-array",
+		"webgpu_textures_anisotropy",
+		"webgpu_textures_partialupdate",
 		"webgpu_tsl_editor",
 		"webgpu_tsl_editor",
 		"webgpu_tsl_transpiler",
 		"webgpu_tsl_transpiler",
 		"webgpu_video_panorama",
 		"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": [
 		"webaudio_orientation",
 		"webaudio_orientation",

+ 2 - 5
examples/games_fps.html

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

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

@@ -32,7 +32,7 @@ const _unit = {
 };
 };
 
 
 const _changeEvent = { type: 'change' };
 const _changeEvent = { type: 'change' };
-const _mouseDownEvent = { type: 'mouseDown' };
+const _mouseDownEvent = { type: 'mouseDown', mode: null };
 const _mouseUpEvent = { type: 'mouseUp', mode: null };
 const _mouseUpEvent = { type: 'mouseUp', mode: null };
 const _objectChangeEvent = { type: 'objectChange' };
 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 roomMaterial = new MeshStandardMaterial( { side: BackSide } );
 		const boxMaterial = new MeshStandardMaterial();
 		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 );
 		mainLight.position.set( 0.418, 16.199, 0.300 );
 		this.add( mainLight );
 		this.add( mainLight );
 
 

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

@@ -25,6 +25,7 @@ class USDZExporter {
 				anchoring: { type: 'plane' },
 				anchoring: { type: 'plane' },
 				planeAnchoring: { alignment: 'horizontal' }
 				planeAnchoring: { alignment: 'horizontal' }
 			},
 			},
+			includeAnchoringProperties: true,
 			quickLookCompatible: false,
 			quickLookCompatible: false,
 			maxTextureSize: 1024,
 			maxTextureSize: 1024,
 		}, options );
 		}, options );
@@ -198,6 +199,10 @@ function buildHeader() {
 
 
 function buildSceneStart( options ) {
 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"
 	return `def Xform "Root"
 {
 {
 	def Scope "Scenes" (
 	def Scope "Scenes" (
@@ -211,10 +216,7 @@ function buildSceneStart( options ) {
 			}
 			}
 			sceneName = "Scene"
 			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 {
 import {
-	BoxGeometry,
+	CylinderGeometry,
 	CanvasTexture,
 	CanvasTexture,
 	Color,
 	Color,
 	Euler,
 	Euler,
@@ -11,6 +11,7 @@ import {
 	Raycaster,
 	Raycaster,
 	Sprite,
 	Sprite,
 	SpriteMaterial,
 	SpriteMaterial,
+	SRGBColorSpace,
 	Vector2,
 	Vector2,
 	Vector3,
 	Vector3,
 	Vector4
 	Vector4
@@ -27,9 +28,10 @@ class ViewHelper extends Object3D {
 		this.animating = false;
 		this.animating = false;
 		this.center = new Vector3();
 		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 interactiveObjects = [];
 		const raycaster = new Raycaster();
 		const raycaster = new Raycaster();
@@ -39,7 +41,7 @@ class ViewHelper extends Object3D {
 		const orthoCamera = new OrthographicCamera( - 2, 2, 2, - 2, 0, 4 );
 		const orthoCamera = new OrthographicCamera( - 2, 2, 2, - 2, 0, 4 );
 		orthoCamera.position.set( 0, 0, 2 );
 		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 xAxis = new Mesh( geometry, getAxisMaterial( color1 ) );
 		const yAxis = new Mesh( geometry, getAxisMaterial( color2 ) );
 		const yAxis = new Mesh( geometry, getAxisMaterial( color2 ) );
@@ -52,28 +54,35 @@ class ViewHelper extends Object3D {
 		this.add( zAxis );
 		this.add( zAxis );
 		this.add( yAxis );
 		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;
 		posXAxisHelper.position.x = 1;
 		posYAxisHelper.position.y = 1;
 		posYAxisHelper.position.y = 1;
 		posZAxisHelper.position.z = 1;
 		posZAxisHelper.position.z = 1;
 		negXAxisHelper.position.x = - 1;
 		negXAxisHelper.position.x = - 1;
-		negXAxisHelper.scale.setScalar( 0.8 );
 		negYAxisHelper.position.y = - 1;
 		negYAxisHelper.position.y = - 1;
-		negYAxisHelper.scale.setScalar( 0.8 );
 		negZAxisHelper.position.z = - 1;
 		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( posXAxisHelper );
 		this.add( posYAxisHelper );
 		this.add( posYAxisHelper );
@@ -101,42 +110,6 @@ class ViewHelper extends Object3D {
 			point.set( 0, 0, 1 );
 			point.set( 0, 0, 1 );
 			point.applyQuaternion( camera.quaternion );
 			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;
 			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' );
 			const canvas = document.createElement( 'canvas' );
 			canvas.width = 64;
 			canvas.width = 64;
@@ -306,21 +279,13 @@ class ViewHelper extends Object3D {
 
 
 			const context = canvas.getContext( '2d' );
 			const context = canvas.getContext( '2d' );
 			context.beginPath();
 			context.beginPath();
-			context.arc( 32, 32, 16, 0, 2 * Math.PI );
+			context.arc( 32, 32, 14, 0, 2 * Math.PI );
 			context.closePath();
 			context.closePath();
 			context.fillStyle = color.getStyle();
 			context.fillStyle = color.getStyle();
 			context.fill();
 			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 );
 			const texture = new CanvasTexture( canvas );
+			texture.colorSpace = SRGBColorSpace;
 
 
 			return new SpriteMaterial( { map: texture, toneMapped: false } );
 			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;
             return amount;
         },
         },
         In: function (amount) {
         In: function (amount) {
-            return this.None(amount);
+            return amount;
         },
         },
         Out: function (amount) {
         Out: function (amount) {
-            return this.None(amount);
+            return amount;
         },
         },
         InOut: function (amount) {
         InOut: function (amount) {
-            return this.None(amount);
+            return amount;
         },
         },
     }),
     }),
     Quadratic: Object.freeze({
     Quadratic: Object.freeze({
@@ -676,13 +676,11 @@ var Tween = /** @class */ (function () {
      * it is still playing, just paused).
      * it is still playing, just paused).
      */
      */
     Tween.prototype.update = function (time, autoStart) {
     Tween.prototype.update = function (time, autoStart) {
-        var _this = this;
         var _a;
         var _a;
         if (time === void 0) { time = now(); }
         if (time === void 0) { time = now(); }
         if (autoStart === void 0) { autoStart = true; }
         if (autoStart === void 0) { autoStart = true; }
         if (this._isPaused)
         if (this._isPaused)
             return true;
             return true;
-        var property;
         var endTime = this._startTime + this._duration;
         var endTime = this._startTime + this._duration;
         if (!this._goToEnd && !this._isPlaying) {
         if (!this._goToEnd && !this._isPlaying) {
             if (time > endTime)
             if (time > endTime)
@@ -709,72 +707,85 @@ var Tween = /** @class */ (function () {
         var elapsedTime = time - this._startTime;
         var elapsedTime = time - this._startTime;
         var durationAndDelay = this._duration + ((_a = this._repeatDelayTime) !== null && _a !== void 0 ? _a : this._delayTime);
         var durationAndDelay = this._duration + ((_a = this._repeatDelayTime) !== null && _a !== void 0 ? _a : this._delayTime);
         var totalTime = this._duration + this._repeat * durationAndDelay;
         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);
         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);
         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) {
         if (this._onUpdateCallback) {
             this._onUpdateCallback(this._object, elapsed);
             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) {
     Tween.prototype._updateProperties = function (_object, _valuesStart, _valuesEnd, value) {
         for (var property in _valuesEnd) {
         for (var property in _valuesEnd) {
@@ -830,7 +841,7 @@ var Tween = /** @class */ (function () {
     return Tween;
     return Tween;
 }());
 }());
 
 
-var VERSION = '23.1.1';
+var VERSION = '23.1.2';
 
 
 /**
 /**
  * Tween.js - Licensed under the MIT license
  * 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 {
 import {
 	ShaderLib,
 	ShaderLib,
 	ShaderMaterial,
 	ShaderMaterial,
 	UniformsLib,
 	UniformsLib,
 	UniformsUtils,
 	UniformsUtils,
-	Vector2
+	Vector2,
 } from 'three';
 } from 'three';
 
 
-
 UniformsLib.line = {
 UniformsLib.line = {
 
 
 	worldUnits: { value: 1 },
 	worldUnits: { value: 1 },

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

@@ -13,6 +13,8 @@ import {
 import { LineSegmentsGeometry } from '../lines/LineSegmentsGeometry.js';
 import { LineSegmentsGeometry } from '../lines/LineSegmentsGeometry.js';
 import { LineMaterial } from '../lines/LineMaterial.js';
 import { LineMaterial } from '../lines/LineMaterial.js';
 
 
+const _viewport = new Vector4();
+
 const _start = new Vector3();
 const _start = new Vector3();
 const _end = 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 };
 export { LineSegments2 };

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