Mr.doob 2 years ago
parent
commit
9f23aae7e1
100 changed files with 5247 additions and 3301 deletions
  1. 862 395
      build/three.cjs
  2. 862 393
      build/three.js
  3. 1 1
      build/three.min.js
  4. 100 133
      build/three.module.js
  5. 0 4
      docs/api/en/constants/Materials.html
  6. 10 5
      docs/api/en/core/GLBufferAttribute.html
  7. 11 6
      docs/api/en/core/Object3D.html
  8. 2 2
      docs/api/en/helpers/CameraHelper.html
  9. 1 1
      docs/api/en/lights/DirectionalLight.html
  10. 1 1
      docs/api/en/lights/HemisphereLight.html
  11. 1 1
      docs/api/en/lights/SpotLight.html
  12. 2 1
      docs/api/en/materials/Material.html
  13. 2 2
      docs/api/en/objects/Mesh.html
  14. 1 1
      docs/api/en/objects/SkinnedMesh.html
  15. 0 4
      docs/api/fr/constants/Materials.html
  16. 0 4
      docs/api/it/constants/Materials.html
  17. 10 5
      docs/api/it/core/GLBufferAttribute.html
  18. 11 7
      docs/api/it/core/Object3D.html
  19. 2 2
      docs/api/it/helpers/CameraHelper.html
  20. 1 1
      docs/api/it/lights/DirectionalLight.html
  21. 1 1
      docs/api/it/lights/HemisphereLight.html
  22. 1 1
      docs/api/it/lights/SpotLight.html
  23. 2 1
      docs/api/it/materials/Material.html
  24. 0 1
      docs/api/ko/constants/Materials.html
  25. 10 5
      docs/api/ko/core/GLBufferAttribute.html
  26. 11 6
      docs/api/ko/core/Object3D.html
  27. 0 4
      docs/api/pt-br/constants/Materials.html
  28. 0 4
      docs/api/zh/constants/Materials.html
  29. 10 5
      docs/api/zh/core/GLBufferAttribute.html
  30. 12 7
      docs/api/zh/core/Object3D.html
  31. 2 2
      docs/api/zh/helpers/CameraHelper.html
  32. 1 1
      docs/api/zh/lights/DirectionalLight.html
  33. 1 1
      docs/api/zh/lights/HemisphereLight.html
  34. 1 1
      docs/api/zh/lights/SpotLight.html
  35. 2 1
      docs/api/zh/materials/Material.html
  36. 1 6
      docs/examples/en/loaders/DRACOLoader.html
  37. 0 7
      docs/examples/en/loaders/GLTFLoader.html
  38. 1 2
      docs/examples/en/loaders/KTX2Loader.html
  39. 1 1
      docs/examples/en/utils/CameraUtils.html
  40. 1 6
      docs/examples/zh/loaders/DRACOLoader.html
  41. 0 6
      docs/examples/zh/loaders/GLTFLoader.html
  42. 4 4
      docs/index.html
  43. 1 0
      docs/manual/en/introduction/Libraries-and-Plugins.html
  44. 1 0
      docs/manual/fr/introduction/Libraries-and-Plugins.html
  45. 1 0
      docs/manual/it/introduction/Libraries-and-Plugins.html
  46. 1 0
      docs/manual/ja/introduction/Libraries-and-Plugins.html
  47. 1 0
      docs/manual/pt-br/introduction/Libraries-and-Plugins.html
  48. 1 0
      docs/manual/ru/introduction/Libraries-and-Plugins.html
  49. 3 0
      docs/manual/zh/introduction/Useful-links.html
  50. 1 1
      docs/page.js
  51. 4 0
      editor/js/Editor.js
  52. 14 51
      editor/js/Loader.js
  53. 2 2
      editor/js/Sidebar.Animation.js
  54. 1 1
      editor/js/Sidebar.Material.ColorProperty.js
  55. 2 2
      editor/js/Sidebar.Material.MapProperty.js
  56. 6 2
      editor/js/Sidebar.Material.js
  57. 7 1
      editor/js/Sidebar.Scene.js
  58. 3 0
      editor/js/Strings.js
  59. 3 2
      editor/js/Viewport.js
  60. 2 0
      editor/js/libs/app/index.html
  61. 2 2
      editor/sw.js
  62. 3 0
      examples/files.json
  63. 28 0
      examples/ies/007cfb11e343e2f42e3b476be4ab684e.ies
  64. 202 0
      examples/ies/02a7562c650498ebb301153dbbf59207.ies
  65. 87 0
      examples/ies/06b4cfdc8805709e767b5e2e904be8ad.ies
  66. 30 0
      examples/ies/1a936937a49c63374e6d4fbed9252b29.ies
  67. 3 0
      examples/ies/README.md
  68. 4 4
      examples/index.html
  69. 1828 1821
      examples/jsm/controls/ArcballControls.js
  70. 11 3
      examples/jsm/csm/CSM.js
  71. 108 6
      examples/jsm/exporters/GLTFExporter.js
  72. 7 0
      examples/jsm/exporters/PLYExporter.js
  73. 1 0
      examples/jsm/helpers/OctreeHelper.js
  74. 70 56
      examples/jsm/helpers/ViewHelper.js
  75. 1 1
      examples/jsm/interactive/HTMLMesh.js
  76. 1 1
      examples/jsm/interactive/InteractiveGroup.js
  77. 0 0
      examples/jsm/libs/flow.module.js
  78. 25 0
      examples/jsm/lights/IESSpotLight.js
  79. 5 4
      examples/jsm/loaders/3MFLoader.js
  80. 1 2
      examples/jsm/loaders/AMFLoader.js
  81. 6 0
      examples/jsm/loaders/DRACOLoader.js
  82. 10 13
      examples/jsm/loaders/FBXLoader.js
  83. 9 100
      examples/jsm/loaders/GLTFLoader.js
  84. 337 0
      examples/jsm/loaders/IESLoader.js
  85. 5 6
      examples/jsm/loaders/MMDLoader.js
  86. 9 3
      examples/jsm/loaders/MaterialXLoader.js
  87. 1 2
      examples/jsm/loaders/PCDLoader.js
  88. 197 77
      examples/jsm/loaders/PLYLoader.js
  89. 1 2
      examples/jsm/loaders/STLLoader.js
  90. 24 24
      examples/jsm/loaders/SVGLoader.js
  91. 1 1
      examples/jsm/loaders/USDZLoader.js
  92. 6 5
      examples/jsm/loaders/VTKLoader.js
  93. 19 19
      examples/jsm/loaders/lwo/IFFParser.js
  94. 1 1
      examples/jsm/misc/GPUComputationRenderer.js
  95. 9 0
      examples/jsm/nodes/Nodes.js
  96. 5 5
      examples/jsm/nodes/accessors/BitangentNode.js
  97. 2 2
      examples/jsm/nodes/accessors/CameraNode.js
  98. 4 6
      examples/jsm/nodes/accessors/CubeTextureNode.js
  99. 52 0
      examples/jsm/nodes/accessors/ExtendedMaterialNode.js
  100. 136 31
      examples/jsm/nodes/accessors/MaterialNode.js

File diff suppressed because it is too large
+ 862 - 395
build/three.cjs


File diff suppressed because it is too large
+ 862 - 393
build/three.js


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


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


+ 0 - 4
docs/api/en/constants/Materials.html

@@ -20,15 +20,11 @@
 		THREE.FrontSide
 		THREE.BackSide
 		THREE.DoubleSide
-		THREE.TwoPassDoubleSide
 		</code>
 		<p>
 		Defines which side of faces will be rendered - front, back or both.
 		Default is [page:Constant FrontSide].
 		</p>
-		<p>
-		[page:Materials TwoPassDoubleSide] will renderer double-sided transparent materials in two passes in back-front order to mitigate transparency artifacts.
-		</p>
 
 		<h2>Blending Mode</h2>
 		<code>

+ 10 - 5
docs/api/en/core/GLBufferAttribute.html

@@ -56,6 +56,11 @@
 			The expected number of vertices in VBO.
 		</p>
 
+		<h3>[property:Boolean isGLBufferAttribute]</h3>
+		<p>
+			Read-only. Always `true`.
+		</p>
+
 		<h3>[property:Integer itemSize]</h3>
 		<p>
 			How many values make up each item (vertex).
@@ -69,6 +74,11 @@
 			See above (constructor) for a list of known type sizes.
 		</p>
 
+		<h3>[property:String name]</h3>
+		<p>
+			Optional name for this attribute instance. Default is an empty string.
+		</p>
+
 		<h3>[property:GLenum type]</h3>
 		<p>
 			A [link:https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Constants#Data_types WebGL Data Type]
@@ -79,11 +89,6 @@
 			using the `setType` method.
 		</p>
 
-		<h3>[property:Boolean isGLBufferAttribute]</h3>
-		<p>
-			Read-only. Always `true`.
-		</p>
-
 		<h2>Methods</h2>
 
 		<h3>[method:this setBuffer]( buffer ) </h3>

+ 11 - 6
docs/api/en/core/Object3D.html

@@ -75,7 +75,7 @@
 		<h3>[property:Boolean matrixAutoUpdate]</h3>
 		<p>
 		When this is set, it calculates the matrix of position, (rotation or quaternion) and
-		scale every frame and also recalculates the matrixWorld property. Default is [page:Object3D.DefaultMatrixAutoUpdate] (true).
+		scale every frame and also recalculates the matrixWorld property. Default is [page:Object3D.DEFAULT_MATRIX_AUTO_UPDATE] (true).
 		</p>
 
 		<h3>[property:Matrix4 matrixWorld]</h3>
@@ -86,8 +86,9 @@
 
 		<h3>[property:Boolean matrixWorldAutoUpdate]</h3>
 		<p>
-		Default is true. If set, then the renderer checks every frame if the object and its children need matrix updates.
+		If set, then the renderer checks every frame if the object and its children need matrix updates.
 		When it isn't, then you have to maintain all matrices in the object and its children yourself.
+		Default is [page:Object3D.DEFAULT_MATRIX_WORLD_AUTO_UPDATE] (true).
 		</p>
 
 		<h3>[property:Boolean matrixWorldNeedsUpdate]</h3>
@@ -169,7 +170,7 @@
 		<h3>[property:Vector3 up]</h3>
 		<p>
 		This is used by the [page:.lookAt lookAt] method, for example, to determine the orientation of the result.<br />
-		Default is [page:Object3D.DefaultUp] - that is, `( 0, 1, 0 )`.
+		Default is [page:Object3D.DEFAULT_UP] - that is, `( 0, 1, 0 )`.
 		</p>
 
 		<h3>[property:Object userData]</h3>
@@ -193,25 +194,29 @@
 		<h2>Static Properties</h2>
 		<p>
 			Static properties and methods are defined per class rather than per instance of that class.
-			This means that changing [page:Object3D.DefaultUp] or [page:Object3D.DefaultMatrixAutoUpdate]
+			This means that changing [page:Object3D.DEFAULT_UP] or [page:Object3D.DEFAULT_MATRIX_AUTO_UPDATE]
 			will change the values of [page:.up up] and [page:.matrixAutoUpdate matrixAutoUpdate] for
 			`every`	instance of Object3D (or derived classes)	created after the change has
 			been made (already created Object3Ds will not be affected).
 		</p>
 
-		<h3>[property:Vector3 DefaultUp]</h3>
+		<h3>[property:Vector3 DEFAULT_UP]</h3>
 		<p>
 			The default [page:.up up] direction for objects, also used as the default position for [page:DirectionalLight],
 			[page:HemisphereLight] and [page:Spotlight] (which creates lights shining from the top down).<br />
 			Set to ( 0, 1, 0 ) by default.
 		</p>
 
-		<h3>[property:Boolean DefaultMatrixAutoUpdate]</h3>
+		<h3>[property:Boolean DEFAULT_MATRIX_AUTO_UPDATE]</h3>
 		<p>
 			The default setting for [page:.matrixAutoUpdate matrixAutoUpdate] for newly created Object3Ds.<br />
 
 		</p>
 
+		<h3>[property:Boolean DEFAULT_MATRIX_WORLD_AUTO_UPDATE]</h3>
+		<p>
+			The default setting for [page:.matrixWorldAutoUpdate matrixWorldAutoUpdate] for newly created Object3Ds.<br />
+		</p>
 
 		<h2>Methods</h2>
 

+ 2 - 2
docs/api/en/helpers/CameraHelper.html

@@ -12,8 +12,8 @@
 		<h1>[name]</h1>
 
 		<p class="desc">
-		This helps with visualizing what a camera contains in its frustum.<br />
-		It visualizes the frustum of a camera using a [page:LineSegments].
+		This helps with visualizing what a camera contains in its frustum. It visualizes the frustum of a camera using a [page:LineSegments].<br /><br />
+		[name] must be a child of the scene.
 		</p>
 
 		<h2>Code Example</h2>

+ 1 - 1
docs/api/en/lights/DirectionalLight.html

@@ -81,7 +81,7 @@
 
 		<h3>[property:Vector3 position]</h3>
 		<p>
-			This is set equal to [page:Object3D.DefaultUp] (0, 1, 0), so that the light shines from the top down.
+			This is set equal to [page:Object3D.DEFAULT_UP] (0, 1, 0), so that the light shines from the top down.
 		</p>
 
 		<h3>[property:DirectionalLightShadow shadow]</h3>

+ 1 - 1
docs/api/en/lights/HemisphereLight.html

@@ -69,7 +69,7 @@
 
 		<h3>[property:Vector3 position]</h3>
 		<p>
-			This is set equal to [page:Object3D.DefaultUp] (0, 1, 0), so that the light shines from the top down.
+			This is set equal to [page:Object3D.DEFAULT_UP] (0, 1, 0), so that the light shines from the top down.
 		</p>
 
 

+ 1 - 1
docs/api/en/lights/SpotLight.html

@@ -126,7 +126,7 @@
 
 		<h3>[property:Vector3 position]</h3>
 		<p>
-			This is set equal to [page:Object3D.DefaultUp] (0, 1, 0), so that the light shines from the top down.
+			This is set equal to [page:Object3D.DEFAULT_UP] (0, 1, 0), so that the light shines from the top down.
 		</p>
 
 		<h3>[property:Float power]</h3>

+ 2 - 1
docs/api/en/materials/Material.html

@@ -260,7 +260,7 @@
 		<p>
 		Defines which side of faces will be rendered - front, back or both.
 		Default is [page:Materials THREE.FrontSide].
-		Other options are [page:Materials THREE.BackSide], [page:Materials THREE.DoubleSide] or [page:Materials THREE.TwoPassDoubleSide].
+		Other options are [page:Materials THREE.BackSide] or [page:Materials THREE.DoubleSide].
 		</p>
 
 		<h3>[property:Boolean toneMapped]</h3>
@@ -298,6 +298,7 @@
 		<h3>[property:Boolean vertexColors]</h3>
 		<p>
 		Defines whether vertex coloring is used. Default is `false`.
+		The engine supports RGB and RGBA vertex colors depending on whether a three (RGB) or four (RGBA) component color buffer attribute is used.
 		</p>
 
 		<h3>[property:Boolean visible]</h3>

+ 2 - 2
docs/api/en/objects/Mesh.html

@@ -72,9 +72,9 @@
 		<h3>[method:Mesh clone]()</h3>
 		<p>Returns a clone of this [name] object and its descendants.</p>
 
-    <h3>[method:Vector3 getVertexPosition]( [param:Integer vert], [param:Vector3 target] )</h3>
+		<h3>[method:Vector3 getVertexPosition]( [param:Integer index], [param:Vector3 target] )</h3>
 		<p>
-		Get the current position of the indicated vertex in local space, taking into account the
+		Get the local-space position of the vertex at the given index, taking into account the
 		current animation state of both morph targets and skinning.
 		</p>
 

+ 1 - 1
docs/api/en/objects/SkinnedMesh.html

@@ -158,7 +158,7 @@
 		<h3>[method:Vector3 boneTransform]( [param:Integer index], [param:Vector3 target] )</h3>
 		<p>
 		Calculates the position of the vertex at the given index relative to the current bone transformations.
-		Target vector must be initialized with the vetrex coordinates prior to the transformation:
+		Target vector must be initialized with the vertex coordinates prior to the transformation:
 		<code>
 const target = new THREE.Vector3();
 target.fromBufferAttribute( mesh.geometry.attributes.position, index );

+ 0 - 4
docs/api/fr/constants/Materials.html

@@ -20,15 +20,11 @@
 		THREE.FrontSide
 		THREE.BackSide
 		THREE.DoubleSide
-		THREE.TwoPassDoubleSide
 		</code>
 		<p>
 		Définit quel côté des faces sera rendu - avant, arrière ou les deux.
 		La valeur par défaut est [page:Constant FrontSide].
 		</p>
-		<p>
-		[page:Materials TwoPassDoubleSide] will renderer double-sided transparent materials in two passes in back-front order to mitigate transparency artifacts.
-		</p>
 
 		<h2>Mode de fusion</h2>
 		<code>

+ 0 - 4
docs/api/it/constants/Materials.html

@@ -22,15 +22,11 @@
 		THREE.FrontSide
 		THREE.BackSide
 		THREE.DoubleSide
-		THREE.TwoPassDoubleSide
 		</code>
 		<p>
       Definisce quale lato delle facce sarà visualizzato - frontale, retro o entrambi.
       Il valore predefinito è [page:Constant FrontSide].
 		</p>
-		<p>
-		[page:Materials TwoPassDoubleSide] will renderer double-sided transparent materials in two passes in back-front order to mitigate transparency artifacts.
-		</p>
 
 		<h2>Modalità Blending</h2>
 		<code>

+ 10 - 5
docs/api/it/core/GLBufferAttribute.html

@@ -55,6 +55,11 @@
       Il numero previsto di vertici in VBO.
 		</p>
 
+		<h3>[property:Boolean isGLBufferAttribute]</h3>
+		<p>
+      Solo lettura. Sempre `true`.
+		</p>
+
 		<h3>[property:Integer itemSize]</h3>
 		<p>
       Quanti valori compongono ogni elemento (vertice).
@@ -68,6 +73,11 @@
       Vedi sopra (costruttore) per un elenco di dimensioni di type conosciute.
 		</p>
 
+		<h3>[property:String name]</h3>
+		<p>
+      Un nome opzionale per questa istanza dell'attributo. Il valore predefinito è una stringa vuota.
+		</p>
+
 		<h3>[property:GLenum type]</h3>
 		<p>
       Un [link:https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Constants#Data_types WebGL Data Type]
@@ -78,11 +88,6 @@
       di usare il metodo `setType`.
 		</p>
 
-		<h3>[property:Boolean isGLBufferAttribute]</h3>
-		<p>
-      Solo lettura. Sempre `true`.
-		</p>
-
 		<h2>Metodi</h2>
 
 		<h3>[method:this setBuffer]( buffer ) </h3>

+ 11 - 7
docs/api/it/core/Object3D.html

@@ -79,7 +79,7 @@
 		<h3>[property:Boolean matrixAutoUpdate]</h3>
 		<p>
       Quando viene settato calcola la matrice di posizione, (rotazione o quaternione) e 
-      ridimensiona ogni fotogramma ed inoltre ricalcola la  proprietà matrixWorld. L'impostazione predefinita è [page:Object3D.DefaultMatrixAutoUpdate] (true).
+      ridimensiona ogni fotogramma ed inoltre ricalcola la  proprietà matrixWorld. L'impostazione predefinita è [page:Object3D.DEFAULT_MATRIX_AUTO_UPDATE] (true).
 		</p>
 
 		<h3>[property:Matrix4 matrixWorld]</h3>
@@ -89,9 +89,9 @@
 
 		<h3>[property:Boolean matrixWorldAutoUpdate]</h3>
 		<p>
-      Il valore predefinito è true. Se impostato, il renderer controlla ogni frame se l'oggetto e i suo figli 
-      necessitano di aggiornare la matrice.
+      Se impostato, il renderer controlla ogni frame se l'oggetto e i suo figli necessitano di aggiornare la matrice.
       Quando non lo è, devi mantenere tu stesso tutte le matrici nell'oggetto e i suoi figli.
+	  Default is [page:Object3D.DEFAULT_MATRIX_WORLD_AUTO_UPDATE] (true).
 		</p>
 
 		<h3>[property:Boolean matrixWorldNeedsUpdate]</h3>
@@ -173,7 +173,7 @@
 		<h3>[property:Vector3 up]</h3>
 		<p>
       Questa proprietà viene utilizzata dal metodo [page:.lookAt lookAt], per esempio, per determinare l'orientamento del risultato.<br />
-      L'impostazione predefinita è [page:Object3D.DefaultUp] - che è, `( 0, 1, 0 )`.
+      L'impostazione predefinita è [page:Object3D.DEFAULT_UP] - che è, `( 0, 1, 0 )`.
 		</p>
 
 		<h3>[property:Object userData]</h3>
@@ -197,24 +197,28 @@
 		<h2>Proprietà Statiche</h2>
 		<p>
       Le proprietà statiche e i metodi sono definiti per classe piuttosto che per istanza della classe.
-      Questo significa che modificando [page:Object3D.DefaultUp] o [page:Object3D.DefaultMatrixAutoUpdate]
+      Questo significa che modificando [page:Object3D.DEFAULT_UP] o [page:Object3D.DEFAULT_MATRIX_AUTO_UPDATE]
       verranno modificati i valori di [page:.up up] e [page:.matrixAutoUpdate matrixAutoUpdate] per `ogni`
       istanza di Object3D (o classi derivate) creata dopo che la modifica è stata fatta 
       (gli Object3D già creati non saranno interessati).
 		</p>
 
-		<h3>[property:Vector3 DefaultUp]</h3>
+		<h3>[property:Vector3 DEFAULT_UP]</h3>
 		<p>
       La direzione predefinita di [page:.up up] per gli oggetti, utilizzata anche come posizione predefinita per [page:DirectionalLight],
 			[page:HemisphereLight] e [page:Spotlight] (che crea luci che brillano dall'alto verso il basso).<br />
       Impostare su ( 0, 1, 0 ) per impostazione predefinita.
 		</p>
 
-		<h3>[property:Boolean DefaultMatrixAutoUpdate]</h3>
+		<h3>[property:Boolean DEFAULT_MATRIX_AUTO_UPDATE]</h3>
 		<p>
       L'impostazione predefinita per [page:.matrixAutoUpdate matrixAutoUpdate] per Object3D appena creati.<br />
 		</p>
 
+		<h3>[property:Boolean DEFAULT_MATRIX_WORLD_AUTO_UPDATE]</h3>
+		<p>
+			L'impostazione predefinita per[page:.matrixWorldAutoUpdate matrixWorldAutoUpdate] per Object3D appena creati.<br />
+		</p>
 
 		<h2>Metodi</h2>
 

+ 2 - 2
docs/api/it/helpers/CameraHelper.html

@@ -12,8 +12,8 @@
 		<h1>[name]</h1>
 
 		<p class="desc">
-      Questa classe aiuta a visualizzare ciò che una telecamera contiene nel suo frustum.<br />
-      Visualizza il frustum di una telecamera utilizzando un [page:LineSegments].
+		Questa classe aiuta a visualizzare ciò che una telecamera contiene nel suo frustum. Visualizza il frustum di una telecamera utilizzando un [page:LineSegments].<br /><br />
+		[name] must be a child of the scene.
 		</p>
 
 		<h2>Codice di Esempio</h2>

+ 1 - 1
docs/api/it/lights/DirectionalLight.html

@@ -83,7 +83,7 @@
 
 		<h3>[property:Vector3 position]</h3>
 		<p>
-      Questo è impostato uguale a [page:Object3D.DefaultUp] (0, 1, 0), in modo che la luce brilli dall'alto verso il basso.
+      Questo è impostato uguale a [page:Object3D.DEFAULT_UP] (0, 1, 0), in modo che la luce brilli dall'alto verso il basso.
 		</p>
 
 		<h3>[property:DirectionalLightShadow shadow]</h3>

+ 1 - 1
docs/api/it/lights/HemisphereLight.html

@@ -69,7 +69,7 @@
 
 		<h3>[property:Vector3 position]</h3>
 		<p>
-      Questo è impostato uguale a [page:Object3D.DefaultUp] (0, 1, 0), in modo che la luce risplenda
+      Questo è impostato uguale a [page:Object3D.DEFAULT_UP] (0, 1, 0), in modo che la luce risplenda
       dall'alto verso il basso.
 		</p>
 

+ 1 - 1
docs/api/it/lights/SpotLight.html

@@ -125,7 +125,7 @@
 
 		<h3>[property:Vector3 position]</h3>
 		<p>
-      Questo è impostato uguale a [page:Object3D.DefaultUp] (0, 1, 0), così che la luce brilli dall'alto veso il basso.
+      Questo è impostato uguale a [page:Object3D.DEFAULT_UP] (0, 1, 0), così che la luce brilli dall'alto veso il basso.
 		</p>
 
 		<h3>[property:Float power]</h3>

+ 2 - 1
docs/api/it/materials/Material.html

@@ -276,7 +276,7 @@
 		<p>
 			Definisce quale lato delle facce sarà visualizzato - frontale, posteriore o entrambi.
 			Il valore predefinito è [page:Materials THREE.FrontSide].
-			Altre opzioni sono [page:Materials THREE.BackSide], [page:Materials THREE.DoubleSide] e [page:Materials THREE.TwoPassDoubleSide].
+			Altre opzioni sono [page:Materials THREE.BackSide] e [page:Materials THREE.DoubleSide].
 		</p>
 
 		<h3>[property:Boolean toneMapped]</h3>
@@ -315,6 +315,7 @@
 		<h3>[property:Boolean vertexColors]</h3>
 		<p>
 			Definisce se viene utilizzata la colorazione dei vertici. Il valore predefinito è `false`.
+			The engine supports RGB and RGBA vertex colors depending on whether a three (RGB) or four (RGBA) component color buffer attribute is used.
 		</p>
 
 		<h3>[property:Boolean visible]</h3>

+ 0 - 1
docs/api/ko/constants/Materials.html

@@ -23,7 +23,6 @@
 		THREE.FrontSide
 		THREE.BackSide
 		THREE.DoubleSide
-		THREE.TwoPassDoubleSide
 		</code>
 		<p>
 		어느 측면(앞,뒤 혹은 둘 다)이 렌더링 될 지 정의합니다.

+ 10 - 5
docs/api/ko/core/GLBufferAttribute.html

@@ -52,6 +52,11 @@
 			VBO의 꼭짓점 수.
 		</p>
 
+		<h3>[property:Boolean isGLBufferAttribute]</h3>
+		<p>
+			읽기 전용. 언제나 *true*입니다.
+		</p>
+
 		<h3>[property:Integer itemSize]</h3>
 		<p>
 			각 항목을 구성하는 값의 크기 (꼭짓점).
@@ -65,6 +70,11 @@
 			알려진 타입 크기 리스트는 위의 (생성자)를 참고.
 		</p>
 
+		<h3>[property:String name]</h3>
+		<p>
+		이 속성 인스턴스의 임시 이름. 기본값은 빈 문자열입니다.
+		</p>
+
 		<h3>[property:GLenum type]</h3>
 		<p>
 			기저의 VBO 컨텐츠를 묘사하는 [link:https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Constants#Data_types WebGL Data Type]
@@ -74,11 +84,6 @@
 			*elementSize*와 함께 이 속성을 설정합니다. 추천하는 방법은 *setType* 메서드를 사용하는 것입니다.
 		</p>
 
-		<h3>[property:Boolean isGLBufferAttribute]</h3>
-		<p>
-			읽기 전용. 언제나 *true*입니다.
-		</p>
-
 		<h2>메서드</h2>
 
 		<h3>[method:this setBuffer]( buffer ) </h3>

+ 11 - 6
docs/api/ko/core/Object3D.html

@@ -75,7 +75,7 @@
 		<h3>[property:Boolean matrixAutoUpdate]</h3>
 		<p>
 		이 값을 설정하면 위치의 매트릭스를 계산하고 (회전 및 쿼터니언), 매 프레임마다 확대/축소하고 matrixWorld 프로퍼티를 재계산합니다.
-		기본값은 [page:Object3D.DefaultMatrixAutoUpdate] (true)입니다.
+		기본값은 [page:Object3D.DEFAULT_MATRIX_AUTO_UPDATE] (true)입니다.
 		</p>
 
 		<h3>[property:Matrix4 matrixWorld]</h3>
@@ -85,8 +85,9 @@
 
 		<h3>[property:Boolean matrixWorldAutoUpdate]</h3>
 		<p>
-		Default is true. If set, then the renderer checks every frame if the object and its children need matrix updates.
+		If set, then the renderer checks every frame if the object and its children need matrix updates.
 		When it isn't, then you have to maintain all matrices in the object and its children yourself.
+		Default is [page:Object3D.DEFAULT_MATRIX_WORLD_AUTO_UPDATE] (true).
 		</p>
 
 		<h3>[property:Boolean matrixWorldNeedsUpdate]</h3>
@@ -163,7 +164,7 @@
 		<h3>[property:Vector3 up]</h3>
 		<p>
 		[page:.lookAt lookAt] 메서드에서 사용되며, 결과의 방향을 결정합니다.<br />
-		기본값은 [page:Object3D.DefaultUp]값, ( 0, 1, 0 )입니다.
+		기본값은 [page:Object3D.DEFAULT_UP]값, ( 0, 1, 0 )입니다.
 		</p>
 
 		<h3>[property:Object userData]</h3>
@@ -186,24 +187,28 @@
 		<h2>정적 프로퍼티</h2>
 		<p>
 			정적 프로퍼티와 메서드는 해당 클래스의 인스턴스가 아니라 클래스 별로 정의됩니다.
-			이는 [page:Object3D.DefaultUp] 혹은 [page:Object3D.DefaultMatrixAutoUpdate]를 변경하면
+			이는 [page:Object3D.DEFAULT_UP] 혹은 [page:Object3D.DEFAULT_MATRIX_AUTO_UPDATE]를 변경하면
 			변경이 이루어진 시점 이후의(이미 만들어진 Object3Ds는 영향을 받지 않습니다) 모든 Object3D(및 파생 클래스)의
 			[page:.up up]과 [page:.matrixAutoUpdate matrixAutoUpdate] 값을 변경시킬 것입니다.
 		</p>
 
-		<h3>[property:Vector3 DefaultUp]</h3>
+		<h3>[property:Vector3 DEFAULT_UP]</h3>
 		<p>
 			The default 오브젝트의 기본값 [page:.up up] 방향이며
 			[page:DirectionalLight], [page:HemisphereLight] 및 [page:Spotlight]의 기본 위치값으로도 사용됩니다(위에서 아래로 내려오는 빛을 만듭니다).<br />
 			기본값으로 ( 0, 1, 0 ) 을 설정합니다.
 		</p>
 
-		<h3>[property:Boolean DefaultMatrixAutoUpdate]</h3>
+		<h3>[property:Boolean DEFAULT_MATRIX_AUTO_UPDATE]</h3>
 		<p>
 			새로 만들어진 Object3D의 [page:.matrixAutoUpdate matrixAutoUpdate] 기본 세팅입니다.<br />
 
 		</p>
 
+		<h3>[property:Boolean DEFAULT_MATRIX_WORLD_AUTO_UPDATE]</h3>
+		<p>
+			새로 만들어진 Object3D의 [page:.matrixWorldAutoUpdate matrixWorldAutoUpdate] 기본 세팅입니다.<br />
+		</p>
 
 		<h2>메서드</h2>
 

+ 0 - 4
docs/api/pt-br/constants/Materials.html

@@ -19,15 +19,11 @@
 		THREE.FrontSide
 		THREE.BackSide
 		THREE.DoubleSide
-		THREE.TwoPassDoubleSide
 		</code>
 		<p>
 		Define qual lado das faces será renderizado - frente, verso ou ambos.
 		O padrão é [page:Constant FrontSide].
 		</p>
-		<p>
-		[page:Materials TwoPassDoubleSide] will renderer double-sided transparent materials in two passes in back-front order to mitigate transparency artifacts.
-		</p>
 
 		<h2>Modo de Mesclagem (Blending Mode)</h2>
 		<code>

+ 0 - 4
docs/api/zh/constants/Materials.html

@@ -20,15 +20,11 @@
 		THREE.FrontSide
 		THREE.BackSide
 		THREE.DoubleSide
-		THREE.TwoPassDoubleSide
 		</code>
 		<p>
 			定义了哪一边的面将会被渲染 —— 正面,或是反面,还是两个面都渲染。
 			默认值是[page:Constant FrontSide](只渲染正面)。
 		</p>
-		<p>
-		[page:Materials TwoPassDoubleSide] will renderer double-sided transparent materials in two passes in back-front order to mitigate transparency artifacts.
-		</p>
 
 		<h2>混合模式</h2>
 		<code>

+ 10 - 5
docs/api/zh/core/GLBufferAttribute.html

@@ -56,6 +56,11 @@
 			The expected number of vertices in VBO.
 		</p>
 
+		<h3>[property:Boolean isGLBufferAttribute]</h3>
+		<p>
+			Read-only. Always *true*.
+		</p>
+
 		<h3>[property:Integer itemSize]</h3>
 		<p>
 			How many values make up each item (vertex).
@@ -69,6 +74,11 @@
 			See above (constructor) for a list of known type sizes.
 		</p>
 
+		<h3>[property:String name]</h3>
+		<p>
+			该 attribute 实例的别名,默认值为空字符串。
+		</p>
+
 		<h3>[property:GLenum type]</h3>
 		<p>
 			A [link:https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Constants#Data_types WebGL Data Type]
@@ -79,11 +89,6 @@
 			using the *setType* method.
 		</p>
 
-		<h3>[property:Boolean isGLBufferAttribute]</h3>
-		<p>
-			Read-only. Always *true*.
-		</p>
-
 		<h2>Methods</h2>
 
 		<h3>[method:this setBuffer]( buffer ) </h3>

+ 12 - 7
docs/api/zh/core/Object3D.html

@@ -72,7 +72,7 @@
 
 	<h3>[property:Boolean matrixAutoUpdate]</h3>
 	<p>
-		当这个属性设置了之后,它将计算每一帧的位移、旋转(四元变换)和缩放矩阵,并重新计算matrixWorld属性。默认值是[page:Object3D.DefaultMatrixAutoUpdate] (true)。
+		当这个属性设置了之后,它将计算每一帧的位移、旋转(四元变换)和缩放矩阵,并重新计算matrixWorld属性。默认值是[page:Object3D.DEFAULT_MATRIX_AUTO_UPDATE] (true)。
 	</p>
 
 	<h3>[property:Matrix4 matrixWorld]</h3>
@@ -82,8 +82,9 @@
 
 	<h3>[property:Boolean matrixWorldAutoUpdate]</h3>
 	<p>
-	Default is true. If set, then the renderer checks every frame if the object and its children need matrix updates.
+	If set, then the renderer checks every frame if the object and its children need matrix updates.
 	When it isn't, then you have to maintain all matrices in the object and its children yourself.
+	Default is [page:Object3D.DEFAULT_MATRIX_WORLD_AUTO_UPDATE] (true).
 	</p>
 
 	<h3>[property:Boolean matrixWorldNeedsUpdate]</h3>
@@ -162,7 +163,7 @@
 	<h3>[property:Vector3 up]</h3>
 	<p>
 		这个属性由[page:.lookAt lookAt]方法所使用,例如,来决定结果的朝向。
-		默认值是[page:Object3D.DefaultUp],即( 0, 1, 0 )。
+		默认值是[page:Object3D.DEFAULT_UP],即( 0, 1, 0 )。
 	</p>
 
 	<h3>[property:Object userData]</h3>
@@ -186,22 +187,26 @@
 	<h2>静态属性</h2>
 	<p>
 		静态属性和方法由每个类所定义,并非由每个类的实例所定义。
-		也就是说,改变[page:Object3D.DefaultUp]或[page:Object3D.DefaultMatrixAutoUpdate]的值,
+		也就是说,改变[page:Object3D.DEFAULT_UP]或[page:Object3D.DEFAULT_MATRIX_AUTO_UPDATE]的值,
 		将改变<b>每个在此之后</b>由Object3D类(或派生类)创建的实例中的[page:.up up]和[page:.matrixAutoUpdate matrixAutoUpdate]的值。(已经创建好的Object3D不会受到影响)。
 	</p>
 
-	<h3>[property:Vector3 DefaultUp]</h3>
+	<h3>[property:Vector3 DEFAULT_UP]</h3>
 	<p>
 		默认的物体的[page:.up up]方向,同时也作为[page:DirectionalLight]、[page:HemisphereLight]和[page:Spotlight](自顶向下创建的灯光)的默认方向。
 		默认设为( 0, 1, 0 )。
 	</p>
 
-	<h3>[property:Boolean DefaultMatrixAutoUpdate]</h3>
+	<h3>[property:Boolean DEFAULT_MATRIX_AUTO_UPDATE]</h3>
 	<p>
-			[page:.matrixAutoUpdate matrixAutoUpdate]的默认设置,用于新创建的Object3D。<br />
+		[page:.matrixAutoUpdate matrixAutoUpdate]的默认设置,用于新创建的Object3D。<br />
 
 	</p>
 
+	<h3>[property:Boolean DEFAULT_MATRIX_WORLD_AUTO_UPDATE]</h3>
+	<p>
+		[page:.matrixWorldAutoUpdate matrixWorldAutoUpdate]的默认设置,用于新创建的Object3D。<br />
+	</p>
 
 	<h2>方法</h2>
 

+ 2 - 2
docs/api/zh/helpers/CameraHelper.html

@@ -12,8 +12,8 @@
 		<h1>[name]</h1>
 
 		<p class="desc">
-		用于模拟相机视锥体的辅助对象.<br />
-		它使用 [page:LineSegments] 来模拟相机视锥体.
+		用于模拟相机视锥体的辅助对象.它使用 [page:LineSegments] 来模拟相机视锥体.<br /><br />
+		[name] must be a child of the scene.
 		</p>
 
 		<h2>代码示例</h2>

+ 1 - 1
docs/api/zh/lights/DirectionalLight.html

@@ -76,7 +76,7 @@
 
 		<h3>[property:Vector3 position]</h3>
 		<p>
-			假如这个值设置等于 [page:Object3D.DefaultUp] (0, 1, 0),那么光线将会从上往下照射。
+			假如这个值设置等于 [page:Object3D.DEFAULT_UP] (0, 1, 0),那么光线将会从上往下照射。
 		</p>
 
 		<h3>[property:DirectionalLightShadow shadow]</h3>

+ 1 - 1
docs/api/zh/lights/HemisphereLight.html

@@ -75,7 +75,7 @@
 
 		<h3>[property:Vector3 position]</h3>
 		<p>
-			假如这个值设置等于 [page:Object3D.DefaultUp] (0, 1, 0),那么光线将会从上往下照射。
+			假如这个值设置等于 [page:Object3D.DEFAULT_UP] (0, 1, 0),那么光线将会从上往下照射。
 		</p>
 
 

+ 1 - 1
docs/api/zh/lights/SpotLight.html

@@ -101,7 +101,7 @@
 
 		<h3>[property:Vector3 position]</h3>
 		<p>
-			假如这个值设置等于 [page:Object3D.DefaultUp] (0, 1, 0),那么光线将会从上往下照射。
+			假如这个值设置等于 [page:Object3D.DEFAULT_UP] (0, 1, 0),那么光线将会从上往下照射。
 		</p>
 
 		<h3>[property:Float power]</h3>

+ 2 - 1
docs/api/zh/materials/Material.html

@@ -226,7 +226,7 @@
 
 <h3>[property:Integer side]</h3>
 <p> 定义将要渲染哪一面 - 正面,背面或两者。
-	默认为[page:Materials THREE.FrontSide]。其他选项有[page:Materials THREE.BackSide], [page:Materials THREE.DoubleSide] 和 [page:Materials THREE.TwoPassDoubleSide]。
+	默认为[page:Materials THREE.FrontSide]。其他选项有[page:Materials THREE.BackSide] 和 [page:Materials THREE.DoubleSide]。
 </p>
 
 <h3>[property:Boolean toneMapped]</h3>
@@ -258,6 +258,7 @@
 <h3>[property:Boolean vertexColors]</h3>
 <p>
 是否使用顶点着色。默认值为false。
+The engine supports RGB and RGBA vertex colors depending on whether a three (RGB) or four (RGBA) component color buffer attribute is used.
 </p>
 
 <h3>[property:Boolean visible]</h3>

+ 1 - 6
docs/examples/en/loaders/DRACOLoader.html

@@ -78,12 +78,7 @@
 
 		<h2>Browser compatibility</h2>
 
-		<p>DRACOLoader relies on ES6 [link:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise Promises],
-		which are not supported in IE11. To use the loader in IE11, you must
-		[link:https://github.com/stefanpenner/es6-promise include a polyfill]
-		providing a Promise replacement. DRACOLoader will automatically use
-		either the JS or the WASM decoding library, based on browser
-		capabilities.</p>
+		<p>DRACOLoader will automatically use either the JS or the WASM decoding library, based on browser capabilities.</p>
 
 		<br>
 		<hr>

+ 0 - 7
docs/examples/en/loaders/GLTFLoader.html

@@ -123,13 +123,6 @@
 			[example:webgl_loader_gltf]
 		</p>
 
-		<h2>Browser compatibility</h2>
-
-		<p>GLTFLoader relies on ES6 [link:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise Promises],
-		which are not supported in IE11. To use the loader in IE11, you must
-		[link:https://github.com/stefanpenner/es6-promise include a polyfill]
-		providing a Promise replacement.</p>
-
 		<h2>Textures</h2>
 
 		<p>Textures containing color information (.map, .emissiveMap, and .specularMap) always use sRGB colorspace in

+ 1 - 2
docs/examples/en/loaders/KTX2Loader.html

@@ -57,8 +57,7 @@
 		<h2>Browser compatibility</h2>
 
 		<p>
-			See notes for [page:BasisTextureLoader]. This loader relies on ES6 Promises and Web Assembly, which are not
-			supported in IE11.
+			This loader relies on Web Assembly which is not supported in older browsers.
 		</p>
 
 		<br>

+ 1 - 1
docs/examples/en/utils/CameraUtils.html

@@ -14,7 +14,7 @@
 
 		<h2>Methods</h2>
 
-		<h3>[method:undefined frameCorners]( [param:PerspectiveCamera camera] [param:Vector3 bottomLeftCorner], [param:Vector3 bottomRightCorner], [param:Vector3 topLeftCorner], [param:boolean estimateViewFrustum] )</h3>
+		<h3>[method:undefined frameCorners]( [param:PerspectiveCamera camera], [param:Vector3 bottomLeftCorner], [param:Vector3 bottomRightCorner], [param:Vector3 topLeftCorner], [param:boolean estimateViewFrustum] )</h3>
 		<p>
 		Set a PerspectiveCamera's projectionMatrix and quaternion to exactly frame the corners of an arbitrary rectangle using [link:https://web.archive.org/web/20191110002841/http://csc.lsu.edu/~kooima/articles/genperspective/index.html Kooima's Generalized Perspective Projection formulation].
 		NOTE: This function ignores the standard parameters; do not call updateProjectionMatrix() after this! toJSON will also not capture the off-axis matrix generated by this function.

+ 1 - 6
docs/examples/zh/loaders/DRACOLoader.html

@@ -73,12 +73,7 @@
 
 		<h2>Browser compatibility</h2>
 
-		<p>DRACOLoader relies on ES6 [link:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise Promises],
-		which are not supported in IE11. To use the loader in IE11, you must
-		[link:https://github.com/stefanpenner/es6-promise include a polyfill]
-		providing a Promise replacement. DRACOLoader will automatically use
-		either the JS or the WASM decoding library, based on browser
-		capabilities.</p>
+		<p>DRACOLoader will automatically use either the JS or the WASM decoding library, based on browser capabilities.</p>
 
 		<br>
 		<hr>

+ 0 - 6
docs/examples/zh/loaders/GLTFLoader.html

@@ -122,12 +122,6 @@
 			[example:webgl_loader_gltf]
 		</p>
 
-		<h2>浏览器兼容性</h2>
-
-		<p>GLTFLoader 依赖 ES6 [link:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise Promises],
-		这一特性不支持IE11。若要在IE11中使用该加载器,你必须引入polyfill([link:https://github.com/stefanpenner/es6-promise include a polyfill])
-		来提供一个Promise的替代方案。</p>
-
 		<h2>纹理</h2>
 
 		<p>纹理中包含的颜色信息(.map, .emissiveMap, 和 .specularMap)在glTF中总是使用sRGB颜色空间,而顶点颜色和材质属性(.color, .emissive, .specular)

+ 4 - 4
docs/index.html

@@ -43,7 +43,7 @@
 			<div id="contentWrapper">
 				<div id="inputWrapper">
 					<input placeholder="" type="text" id="filterInput" autocorrect="off" autocapitalize="off" spellcheck="false" />
-					<div id="exitSearchButton"></div>
+					<div id="clearSearchButton"></div>
 					<select id="language">
 						<option value="en">en</option>
 						<option value="ar">ar</option>
@@ -68,7 +68,7 @@
 		const panel = document.getElementById( 'panel' );
 		const content = document.getElementById( 'content' );
 		const expandButton = document.getElementById( 'expandButton' );
-		const exitSearchButton = document.getElementById( 'exitSearchButton' );
+		const clearSearchButton = document.getElementById( 'clearSearchButton' );
 		const panelScrim = document.getElementById( 'panelScrim' );
 		const filterInput = document.getElementById( 'filterInput' );
 		let iframe = document.querySelector( 'iframe' );
@@ -179,11 +179,11 @@
 
 			};
 
-			exitSearchButton.onclick = function () {
+			clearSearchButton.onclick = function () {
 
 				filterInput.value = '';
 				updateFilter();
-				panel.classList.remove( 'searchFocused' );
+				filterInput.focus();
 
 			};
 

+ 1 - 0
docs/manual/en/introduction/Libraries-and-Plugins.html

@@ -77,6 +77,7 @@
 		<h3>Particle Systems</h3>
 
 		<ul>
+			<li>[link:https://github.com/Alchemist0823/three.quarks three.quarks]</li>
 			<li>[link:https://github.com/creativelifeform/three-nebula three-nebula]</li>
 		</ul>
 

+ 1 - 0
docs/manual/fr/introduction/Libraries-and-Plugins.html

@@ -77,6 +77,7 @@
 		<h3>Systèmes de particules</h3>
 
 		<ul>
+			<li>[link:https://github.com/Alchemist0823/three.quarks three.quarks]</li>
 			<li>[link:https://github.com/creativelifeform/three-nebula three-nebula]</li>
 		</ul>
 

+ 1 - 0
docs/manual/it/introduction/Libraries-and-Plugins.html

@@ -77,6 +77,7 @@
 		<h3>Sistemi di particelle</h3>
 
 		<ul>
+			<li>[link:https://github.com/Alchemist0823/three.quarks three.quarks]</li>
 			<li>[link:https://github.com/creativelifeform/three-nebula three-nebula]</li>
 		</ul>
 

+ 1 - 0
docs/manual/ja/introduction/Libraries-and-Plugins.html

@@ -71,6 +71,7 @@
     <h3>Particle Systems</h3>
 
     <ul>
+        <li>[link:https://github.com/Alchemist0823/three.quarks three.quarks]</li>
         <li>[link:https://github.com/creativelifeform/three-nebula three-nebula]</li>
     </ul>
 

+ 1 - 0
docs/manual/pt-br/introduction/Libraries-and-Plugins.html

@@ -77,6 +77,7 @@
 		<h3>Sistema de partículas</h3>
 
 		<ul>
+			<li>[link:https://github.com/Alchemist0823/three.quarks three.quarks]</li>
 			<li>[link:https://github.com/creativelifeform/three-nebula three-nebula]</li>
 		</ul>
 

+ 1 - 0
docs/manual/ru/introduction/Libraries-and-Plugins.html

@@ -77,6 +77,7 @@
 		<h3>Системы частиц</h3>
 
 		<ul>
+			<li>[link:https://github.com/Alchemist0823/three.quarks three.quarks]</li>
 			<li>[link:https://github.com/creativelifeform/three-nebula three-nebula]</li>
 		</ul>
 

+ 3 - 0
docs/manual/zh/introduction/Useful-links.html

@@ -114,6 +114,9 @@
 		<li>
 			[link:http://idflood.github.io/ThreeNodes.js/ ThreeNodes.js].
 		</li>
+		<li>
+			[link:https://github.com/Alchemist0823/three.quarks three.quarks] - 针对 three.js 高速粒子特效系统
+		</li>
 		<li>
 			[link:https://marketplace.visualstudio.com/items?itemName=slevesque.shader vscode shader] - Syntax highlighter for shader language.
 			<br />

+ 1 - 1
docs/page.js

@@ -165,7 +165,7 @@ function onDocumentLoad() {
 
 		}
 
-		prettyPrint();
+		prettyPrint(); // eslint-disable-line no-undef
 
 	};
 

+ 4 - 0
editor/js/Editor.js

@@ -7,6 +7,8 @@ import { Strings } from './Strings.js';
 import { Storage as _Storage } from './Storage.js';
 import { Selector } from './Viewport.Selector.js';
 
+THREE.ColorManagement.legacyMode = false;
+
 var _DEFAULT_CAMERA = new THREE.PerspectiveCamera( 50, 1, 0.01, 1000 );
 _DEFAULT_CAMERA.name = 'Camera';
 _DEFAULT_CAMERA.position.set( 0, 5, 10 );
@@ -136,6 +138,8 @@ Editor.prototype = {
 		this.scene.background = scene.background;
 		this.scene.environment = scene.environment;
 		this.scene.fog = scene.fog;
+		this.scene.backgroundBlurriness = scene.backgroundBlurriness;
+		this.scene.backgroundIntensity = scene.backgroundIntensity;
 
 		this.scene.userData = JSON.parse( JSON.stringify( scene.userData ) );
 

+ 14 - 51
editor/js/Loader.js

@@ -279,6 +279,8 @@ function Loader( editor ) {
 						scene.animations.push( ...result.animations );
 						editor.execute( new AddObjectCommand( editor, scene ) );
 
+						dracoLoader.dispose();
+
 					} );
 
 				}, false );
@@ -296,24 +298,14 @@ function Loader( editor ) {
 
 					const contents = event.target.result;
 
-					let loader;
-
-					if ( isGLTF1( contents ) ) {
-
-						alert( 'Import of glTF asset not possible. Only versions >= 2.0 are supported. Please try to upgrade the file to glTF 2.0 using glTF-Pipeline.' );
-
-					} else {
-
-						const { DRACOLoader } = await import( 'three/addons/loaders/DRACOLoader.js' );
-						const { GLTFLoader } = await import( 'three/addons/loaders/GLTFLoader.js' );
-
-						const dracoLoader = new DRACOLoader();
-						dracoLoader.setDecoderPath( '../examples/jsm/libs/draco/gltf/' );
+					const { DRACOLoader } = await import( 'three/addons/loaders/DRACOLoader.js' );
+					const { GLTFLoader } = await import( 'three/addons/loaders/GLTFLoader.js' );
 
-						loader = new GLTFLoader( manager );
-						loader.setDRACOLoader( dracoLoader );
+					const dracoLoader = new DRACOLoader();
+					dracoLoader.setDecoderPath( '../examples/jsm/libs/draco/gltf/' );
 
-					}
+					const loader = new GLTFLoader( manager );
+					loader.setDRACOLoader( dracoLoader );
 
 					loader.parse( contents, '', function ( result ) {
 
@@ -323,6 +315,8 @@ function Loader( editor ) {
 						scene.animations.push( ...result.animations );
 						editor.execute( new AddObjectCommand( editor, scene ) );
 
+						dracoLoader.dispose();
+
 					} );
 
 				}, false );
@@ -968,6 +962,8 @@ function Loader( editor ) {
 						scene.animations.push( ...result.animations );
 						editor.execute( new AddObjectCommand( editor, scene ) );
 
+						dracoLoader.dispose();
+
 					} );
 
 					break;
@@ -993,6 +989,8 @@ function Loader( editor ) {
 						scene.animations.push( ...result.animations );
 						editor.execute( new AddObjectCommand( editor, scene ) );
 
+						dracoLoader.dispose();
+
 					} );
 
 					break;
@@ -1005,41 +1003,6 @@ function Loader( editor ) {
 
 	}
 
-	function isGLTF1( contents ) {
-
-		let resultContent;
-
-		if ( typeof contents === 'string' ) {
-
-			// contents is a JSON string
-			resultContent = contents;
-
-		} else {
-
-			const magic = THREE.LoaderUtils.decodeText( new Uint8Array( contents, 0, 4 ) );
-
-			if ( magic === 'glTF' ) {
-
-				// contents is a .glb file; extract the version
-				const version = new DataView( contents ).getUint32( 4, true );
-
-				return version < 2;
-
-			} else {
-
-				// contents is a .gltf file
-				resultContent = THREE.LoaderUtils.decodeText( new Uint8Array( contents ) );
-
-			}
-
-		}
-
-		const json = JSON.parse( resultContent );
-
-		return ( json.asset != undefined && json.asset.version[ 0 ] < 2 );
-
-	}
-
 }
 
 export { Loader };

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

@@ -23,11 +23,11 @@ function SidebarAnimation( editor ) {
 		const name = new UIText( animation.name ).setWidth( '200px' );
 		container.add( name );
 
-		const button = new UIButton( getButtonText( action  ) );
+		const button = new UIButton( getButtonText( action ) );
 		button.onClick( function () {
 
 			action.isRunning() ? action.stop() : action.play();
-			button.setTextContent( getButtonText( action  ) );
+			button.setTextContent( getButtonText( action ) );
 
 		} );
 

+ 1 - 1
editor/js/Sidebar.Material.ColorProperty.js

@@ -16,7 +16,7 @@ function SidebarMaterialColorProperty( editor, property, name ) {
 
 	if ( property === 'emissive' ) {
 
-		intensity = new UINumber().setWidth( '30px' ).onChange( onChange );
+		intensity = new UINumber( 1 ).setWidth( '30px' ).setRange( 0, Infinity ).onChange( onChange );
 		container.add( intensity );
 
 	}

+ 2 - 2
editor/js/Sidebar.Material.MapProperty.js

@@ -26,7 +26,7 @@ function SidebarMaterialMapProperty( editor, property, name ) {
 
 	if ( property === 'aoMap' ) {
 
-		intensity = new UINumber().setWidth( '30px' ).onChange( onIntensityChange );
+		intensity = new UINumber( 1 ).setWidth( '30px' ).setRange( 0, 1 ).onChange( onIntensityChange );
 		container.add( intensity );
 
 	}
@@ -55,7 +55,7 @@ function SidebarMaterialMapProperty( editor, property, name ) {
 	let rangeMin, rangeMax;
 
 	if ( property === 'iridescenceThicknessMap' ) {
-		
+
 		const range = new UIDiv().setMarginLeft( '3px' );
 		container.add( range );
 

+ 6 - 2
editor/js/Sidebar.Material.js

@@ -306,8 +306,7 @@ function SidebarMaterial( editor ) {
 	const materialSideOptions = {
 		0: 'Front',
 		1: 'Back',
-		2: 'Double',
-		3: 'TwoPassDouble'
+		2: 'Double'
 	};
 
 	const materialSide = new SidebarMaterialConstantProperty( editor, 'side', strings.getKey( 'sidebar/material/side' ), materialSideOptions );
@@ -352,6 +351,11 @@ function SidebarMaterial( editor ) {
 	const materialTransparent = new SidebarMaterialBooleanProperty( editor, 'transparent', strings.getKey( 'sidebar/material/transparent' ) );
 	container.add( materialTransparent );
 
+	// forceSinglePass
+
+	const materialForceSinglePass = new SidebarMaterialBooleanProperty( editor, 'forceSinglePass', strings.getKey( 'sidebar/material/forcesinglepass' ) );
+	container.add( materialForceSinglePass );
+
 	// alpha test
 
 	const materialAlphaTest = new SidebarMaterialNumberProperty( editor, 'alphaTest', strings.getKey( 'sidebar/material/alphatest' ), [ 0, 1 ] );

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

@@ -192,6 +192,9 @@ function SidebarScene( editor ) {
 	const backgroundBlurriness = new UINumber( 0 ).setWidth( '40px' ).setRange( 0, 1 ).onChange( onBackgroundChanged );
 	backgroundEquirectRow.add( backgroundBlurriness );
 
+	const backgroundIntensity = new UINumber( 1 ).setWidth( '40px' ).setRange( 0, Infinity ).onChange( onBackgroundChanged );
+	backgroundEquirectRow.add( backgroundIntensity );
+
 	container.add( backgroundEquirectRow );
 
 	function onBackgroundChanged() {
@@ -201,7 +204,8 @@ function SidebarScene( editor ) {
 			backgroundColor.getHexValue(),
 			backgroundTexture.getValue(),
 			backgroundEquirectangularTexture.getValue(),
-			backgroundBlurriness.getValue()
+			backgroundBlurriness.getValue(),
+			backgroundIntensity.getValue()
 		);
 
 	}
@@ -395,6 +399,8 @@ function SidebarScene( editor ) {
 
 					backgroundType.setValue( 'Equirectangular' );
 					backgroundEquirectangularTexture.setValue( scene.background );
+					backgroundBlurriness.setValue( scene.backgroundBlurriness );
+					backgroundIntensity.setValue( scene.backgroundIntensity );
 
 				} else {
 

+ 3 - 0
editor/js/Strings.js

@@ -298,6 +298,7 @@ function Strings( config ) {
 			'sidebar/material/blending': 'Blending',
 			'sidebar/material/opacity': 'Opacity',
 			'sidebar/material/transparent': 'Transparent',
+			'sidebar/material/forcesinglepass': 'Force Single Pass',
 			'sidebar/material/alphatest': 'Alpha Test',
 			'sidebar/material/depthtest': 'Depth Test',
 			'sidebar/material/depthwrite': 'Depth Write',
@@ -649,6 +650,7 @@ function Strings( config ) {
 			'sidebar/material/blending': 'Mélange',
 			'sidebar/material/opacity': 'Opacité',
 			'sidebar/material/transparent': 'Transparence',
+			'sidebar/material/forcesinglepass': 'Force Single Pass',
 			'sidebar/material/alphatest': 'Test de transparence',
 			'sidebar/material/depthtest': 'Depth Test',
 			'sidebar/material/depthwrite': 'Depth Write',
@@ -1000,6 +1002,7 @@ function Strings( config ) {
 			'sidebar/material/blending': '混合',
 			'sidebar/material/opacity': '透明度',
 			'sidebar/material/transparent': '透明性',
+			'sidebar/material/forcesinglepass': 'Force Single Pass',
 			'sidebar/material/alphatest': 'α测试',
 			'sidebar/material/depthtest': '深度测试',
 			'sidebar/material/depthwrite': '深度缓冲',

+ 3 - 2
editor/js/Viewport.js

@@ -292,7 +292,7 @@ function Viewport( editor ) {
 		signals.refreshSidebarObject3D.dispatch( camera );
 
 	} );
-	viewHelper.controls = controls;
+	viewHelper.center = controls.center;
 
 	// signals
 
@@ -481,7 +481,7 @@ function Viewport( editor ) {
 
 	// background
 
-	signals.sceneBackgroundChanged.add( function ( backgroundType, backgroundColor, backgroundTexture, backgroundEquirectangularTexture, backgroundBlurriness ) {
+	signals.sceneBackgroundChanged.add( function ( backgroundType, backgroundColor, backgroundTexture, backgroundEquirectangularTexture, backgroundBlurriness, backgroundIntensity ) {
 
 		switch ( backgroundType ) {
 
@@ -514,6 +514,7 @@ function Viewport( editor ) {
 					backgroundEquirectangularTexture.mapping = THREE.EquirectangularReflectionMapping;
 					scene.background = backgroundEquirectangularTexture;
 					scene.backgroundBlurriness = backgroundBlurriness;
+					scene.backgroundIntensity = backgroundIntensity;
 
 				}
 

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

@@ -27,6 +27,8 @@
 			window.THREE = THREE; // Used by APP Scripts.
 			window.VRButton = VRButton; // Used by APP Scripts.
 
+			THREE.ColorManagement.legacyMode = false;
+
 			var loader = new THREE.FileLoader();
 			loader.load( 'app.json', function ( text ) {
 

+ 2 - 2
editor/sw.js

@@ -268,8 +268,8 @@ async function networkFirst( request ) {
 		if ( request.url.endsWith( 'editor/' ) || request.url.endsWith( 'editor/index.html' ) ) { // copied from coi-serviceworker
 
 			const newHeaders = new Headers( response.headers );
-			newHeaders.set( "Cross-Origin-Embedder-Policy", "require-corp" );
-			newHeaders.set( "Cross-Origin-Opener-Policy", "same-origin" );
+			newHeaders.set( 'Cross-Origin-Embedder-Policy', 'require-corp' );
+			newHeaders.set( 'Cross-Origin-Opener-Policy', 'same-origin' );
 
 			response = new Response( response.body, { status: response.status, statusText: response.statusText, headers: newHeaders } );
 

+ 3 - 0
examples/files.json

@@ -27,6 +27,7 @@
 		"webgl_geometry_colors",
 		"webgl_geometry_colors_lookuptable",
 		"webgl_geometry_convex",
+		"webgl_geometry_csg",
 		"webgl_geometry_cube",
 		"webgl_geometry_dynamic",
 		"webgl_geometry_extrude_shapes",
@@ -333,6 +334,8 @@
 		"webgpu_instance_mesh",
 		"webgpu_instance_uniform",
 		"webgpu_lights_custom",
+		"webgpu_lights_ies_spotlight",
+		"webgpu_lights_phong",
 		"webgpu_lights_selective",
 		"webgpu_loader_gltf",
 		"webgpu_materials",

+ 28 - 0
examples/ies/007cfb11e343e2f42e3b476be4ab684e.ies

@@ -0,0 +1,28 @@
+IESNA:LM-63-1995
+[TEST] 
+[MANUFAC] BEGA
+[MORE] Copyright LUMCat V 
+[LUMCAT] 
+[LUMINAIRE] 50975.6K3 (Preliminary)
+[LAMPCAT] LED  7,9W
+[LAMP]     321 lm,9 W
+TILT=NONE
+1 -1 1.0 73 1 1 2 -0.080 0.000 0.000
+1.0 1.0 9
+   0.0   2.5   5.0   7.5  10.0  12.5  15.0  17.5  20.0  22.5  25.0  27.5  30.0
+  32.5  35.0  37.5  40.0  42.5  45.0  47.5  50.0  52.5  55.0  57.5  60.0  62.5
+  65.0  67.5  70.0  72.5  75.0  77.5  80.0  82.5  85.0  87.5  90.0  92.5  95.0
+  97.5 100.0 102.5 105.0 107.5 110.0 112.5 115.0 117.5 120.0 122.5 125.0 127.5
+ 130.0 132.5 135.0 137.5 140.0 142.5 145.0 147.5 150.0 152.5 155.0 157.5 160.0
+ 162.5 165.0 167.5 170.0 172.5 175.0 177.5 180.0
+   0.0
+     330.8     333.2     335.2     336.2     336.8     337.1     337.2     336.7
+     331.4     302.1     238.1     159.9      90.7      51.3      38.9      36.0
+      34.8      34.2      33.5      32.7      31.6      29.3      25.2      19.8
+      15.4      12.3       9.9       8.1       6.4       5.1       3.9       3.0
+       2.1       1.4       0.9       0.4       0.1       0.1       0.1       0.0
+       0.0       0.0       0.0       0.0       0.0       0.0       0.0       0.0
+       0.0       0.0       0.0       0.0       0.0       0.0       0.0       0.0
+       0.0       0.0       0.0       0.0       0.0       0.0       0.0       0.0
+       0.0       0.0       0.0       0.0       0.0       0.0       0.0       0.0
+       0.0

+ 202 - 0
examples/ies/02a7562c650498ebb301153dbbf59207.ies

@@ -0,0 +1,202 @@
+IESNA:LM-63-1995
+[TEST] 
+[MANUFAC] BEGA
+[MORE] Copyright LUMCat V 
+[LUMCAT] 
+[LUMINAIRE] 84659K4 (Preliminary)
+[LAMPCAT] LED  62W
+[LAMP]    9600 lm,68 W
+TILT=NONE
+1 -1 1.0 37 37 1 2 0.240 0.270 0.000
+1.0 1.0 68
+   0.0   2.5   5.0   7.5  10.0  12.5  15.0  17.5  20.0  22.5  25.0  27.5  30.0
+  32.5  35.0  37.5  40.0  42.5  45.0  47.5  50.0  52.5  55.0  57.5  60.0  62.5
+  65.0  67.5  70.0  72.5  75.0  77.5  80.0  82.5  85.0  87.5  90.0
+  90.0  95.0 100.0 105.0 110.0 115.0 120.0 125.0 130.0 135.0 140.0 145.0 150.0
+ 155.0 160.0 165.0 170.0 175.0 180.0 185.0 190.0 195.0 200.0 205.0 210.0 215.0
+ 220.0 225.0 230.0 235.0 240.0 245.0 250.0 255.0 260.0 265.0 270.0
+    1739.8    1748.5    1754.2    1758.7    1775.5    1813.8    1888.7    2057.8
+    2268.7    2530.0    2653.0    2727.6    2847.9    2943.6    3012.0    3010.7
+    2925.3    2695.3    2336.5    1929.2    1501.3    1136.6     808.5     532.9
+     280.7     122.6      64.3      37.0      26.4      18.3      12.9       8.2
+       5.1       2.4       0.3       0.0       0.0
+    1739.8    1748.6    1755.1    1761.4    1779.4    1810.3    1897.9    2049.0
+    2262.6    2522.0    2673.8    2761.2    2883.7    2983.2    3028.5    3050.3
+    2988.3    2746.1    2362.9    1967.1    1541.8    1172.7     839.4     558.2
+     309.8     141.1      80.4      50.8      35.6      24.5      16.9      10.8
+       7.1       4.0       1.2       0.0       0.0
+    1739.8    1749.1    1757.4    1767.4    1785.7    1823.3    1899.7    2048.4
+    2256.2    2498.9    2694.0    2773.9    2844.4    2922.8    2988.7    3031.0
+    3027.4    2881.6    2567.2    2188.7    1761.0    1375.4    1018.7     699.3
+     400.3     196.2     113.3      71.3      47.6      32.5      22.1      14.8
+       9.7       5.2       2.1       0.1       0.0
+    1739.8    1749.8    1760.2    1773.9    1795.1    1832.1    1900.1    2045.2
+    2239.3    2482.5    2660.9    2744.7    2861.5    3034.0    3184.8    3305.3
+    3327.2    3210.1    2878.7    2442.9    1979.1    1517.9    1151.7     808.6
+     498.3     261.5     152.9      99.1      66.3      42.9      26.9      18.1
+      11.5       6.5       2.5       0.0       0.0
+    1739.8    1750.6    1763.0    1780.3    1803.0    1833.0    1894.3    2008.2
+    2199.0    2444.7    2696.9    2888.1    3087.5    3267.2    3389.1    3432.1
+    3418.0    3298.7    3024.6    2609.8    2146.7    1683.6    1305.7     944.7
+     626.2     363.9     214.5     137.1      87.4      54.7      34.9      23.2
+      15.2       8.4       2.5       0.0       0.0
+    1739.8    1751.6    1766.6    1786.2    1806.4    1826.5    1860.2    1973.2
+    2177.0    2478.9    2808.4    3035.9    3161.6    3281.4    3384.3    3448.8
+    3438.0    3361.0    3159.2    2800.0    2365.0    1909.0    1518.8    1156.2
+     839.7     537.7     333.0     229.1     153.9      98.5      60.9      38.2
+      22.2      10.8       3.6       0.0       0.0
+    1739.8    1752.7    1771.7    1792.3    1802.7    1808.7    1832.9    1970.3
+    2231.9    2557.8    2848.8    3046.8    3145.9    3246.5    3374.8    3453.3
+    3465.7    3410.5    3291.8    3011.3    2624.9    2203.5    1792.9    1450.0
+    1136.5     840.7     606.6     481.8     364.3     246.9     150.7      88.9
+      48.1      22.0       6.9       0.3       0.0
+    1739.8    1753.8    1777.0    1796.5    1794.8    1791.9    1841.4    2021.7
+    2280.7    2542.2    2771.2    2975.5    3121.1    3248.2    3391.1    3511.0
+    3531.5    3484.4    3392.6    3203.7    2913.7    2548.4    2163.0    1821.9
+    1540.7    1327.8    1160.0    1022.5     800.9     563.3     370.4     228.6
+     125.4      51.8      15.1       1.2       0.1
+    1739.8    1754.8    1781.0    1795.7    1781.8    1792.6    1886.5    2075.7
+    2261.2    2450.6    2649.5    2876.7    3093.7    3285.2    3444.9    3600.0
+    3677.9    3661.2    3584.7    3460.9    3265.7    2991.9    2661.8    2352.5
+    2166.2    2101.0    2042.7    1810.4    1435.5    1092.7     774.0     501.6
+     276.8     103.1      26.9       2.6       0.0
+    1739.8    1755.4    1782.8    1790.5    1772.5    1813.9    1953.0    2096.5
+    2208.0    2334.6    2514.0    2750.9    3033.0    3305.6    3497.4    3638.5
+    3780.3    3871.8    3869.9    3803.3    3693.5    3534.0    3313.6    3128.5
+    3083.1    3162.1    3100.2    2758.2    2262.6    1809.0    1341.3     899.8
+     500.0     187.6      46.7       4.3       0.0
+    1739.8    1755.7    1782.8    1783.3    1768.3    1848.9    2006.2    2097.5
+    2154.6    2236.7    2392.4    2608.1    2899.4    3231.2    3514.3    3713.1
+    3856.4    3983.7    4070.7    4087.8    4080.7    4098.0    4077.7    4091.0
+    4192.6    4272.4    4144.6    3754.2    3197.0    2607.4    2005.6    1386.9
+     778.1     293.9      79.5       7.5       0.0
+    1739.8    1755.4    1782.2    1774.3    1772.9    1890.9    2037.3    2090.6
+    2116.9    2172.1    2286.5    2481.7    2751.7    3076.9    3413.4    3703.8
+    3904.5    4053.3    4183.4    4293.1    4376.2    4499.1    4733.8    5032.2
+    5271.9    5261.5    5017.0    4611.3    4064.9    3373.1    2651.0    1878.4
+    1053.3     386.3     107.3      13.3       0.0
+    1739.8    1754.8    1781.2    1763.5    1782.8    1925.4    2052.3    2085.7
+    2109.9    2148.2    2230.5    2373.6    2592.0    2872.4    3193.1    3524.8
+    3813.4    4054.3    4243.0    4393.5    4537.7    4758.7    5191.2    5738.1
+    6099.8    6004.9    5742.4    5359.5    4807.0    4117.3    3298.1    2376.1
+    1374.9     499.6     137.2      19.0       0.1
+    1739.8    1754.2    1779.0    1753.3    1795.5    1954.6    2057.7    2081.8
+    2106.0    2140.8    2198.8    2293.9    2431.6    2638.7    2872.8    3146.3
+    3442.6    3729.7    4009.0    4277.9    4534.4    4841.2    5334.3    5971.2
+    6458.4    6427.8    6214.9    5945.5    5405.8    4726.0    3924.8    2910.2
+    1731.6     647.9     172.7      24.4       0.2
+    1739.8    1753.7    1775.5    1747.2    1811.0    1975.6    2058.3    2078.1
+    2100.7    2143.1    2192.8    2252.8    2332.3    2443.4    2578.3    2735.2
+    2900.4    3095.8    3322.8    3611.9    3979.1    4473.8    5057.7    5592.9
+    6039.2    6254.0    6274.6    6086.8    5637.3    5050.5    4315.5    3337.6
+    2052.6     777.2     188.6      27.0       0.1
+    1739.8    1753.5    1772.1    1743.4    1825.3    1988.7    2055.7    2075.2
+    2104.0    2147.1    2182.2    2230.3    2278.6    2338.0    2407.8    2488.4
+    2561.1    2649.0    2758.2    2933.2    3221.1    3689.9    4315.9    4887.8
+    5217.3    5403.3    5578.3    5593.9    5285.6    4859.4    4234.2    3286.5
+    2092.1     833.6     192.9      25.7       0.1
+    1739.8    1753.4    1770.3    1741.1    1834.5    1998.4    2060.2    2074.5
+    2102.5    2137.9    2177.5    2212.0    2239.4    2273.8    2324.5    2374.0
+    2422.9    2478.7    2555.6    2672.0    2890.8    3264.9    3773.6    4189.1
+    4395.8    4538.9    4636.8    4609.0    4417.1    4064.6    3531.1    2726.5
+    1743.9     712.5     163.7      22.3       0.0
+    1739.8    1753.2    1769.9    1740.7    1844.3    2003.6    2059.3    2074.3
+    2101.9    2140.5    2174.2    2200.8    2225.4    2242.9    2274.6    2316.0
+    2375.6    2439.1    2522.0    2651.4    2890.5    3269.0    3713.2    4051.6
+    4168.6    4152.3    4098.0    3932.4    3645.8    3237.7    2704.4    2024.6
+    1266.2     505.7     118.2      17.2       0.2
+    1739.8    1752.7    1768.9    1739.4    1848.0    2005.0    2057.8    2076.1
+    2105.3    2138.8    2172.1    2193.3    2208.4    2232.2    2264.7    2306.5
+    2370.7    2456.0    2566.0    2716.8    2996.1    3411.2    3879.6    4247.8
+    4401.7    4315.5    4107.0    3814.8    3470.1    3017.1    2489.7    1881.1
+    1180.9     457.8     104.1      14.4       0.2
+    1739.8    1751.9    1766.5    1736.6    1841.4    1999.8    2051.3    2064.5
+    2091.3    2125.1    2159.2    2184.1    2191.0    2207.5    2233.7    2281.7
+    2347.3    2428.6    2529.8    2693.0    2968.7    3391.4    3882.6    4271.4
+    4468.1    4373.3    4162.7    3880.4    3514.7    2965.6    2326.5    1745.4
+    1081.3     418.7      96.3      13.4       1.0
+    1739.8    1750.6    1763.4    1733.9    1826.0    1984.8    2039.0    2046.3
+    2062.0    2088.3    2113.7    2133.3    2141.8    2161.8    2185.5    2224.5
+    2273.4    2341.5    2413.9    2555.7    2779.9    3119.7    3534.7    3883.8
+    4018.8    3886.8    3657.9    3358.8    2911.8    2312.5    1679.0    1171.2
+     650.0     219.8      51.6       7.1       0.1
+    1739.8    1748.8    1761.1    1728.9    1802.9    1961.8    2019.1    2024.3
+    2035.5    2051.2    2063.9    2066.3    2069.3    2082.4    2104.1    2127.4
+    2161.4    2207.3    2257.9    2335.9    2458.5    2688.2    2954.9    3149.0
+    3185.9    2991.3    2712.6    2412.9    1981.8    1449.2     948.1     615.6
+     320.2      97.1      23.3       3.3       0.2
+    1739.8    1746.5    1759.4    1723.6    1777.0    1932.0    1998.8    1995.7
+    1994.2    2001.1    2002.5    1998.9    1987.0    1981.7    1985.9    1995.0
+    1999.0    1996.4    1977.9    1933.3    1936.8    2021.7    2181.6    2300.9
+    2256.1    2053.8    1778.0    1471.1    1135.3     799.3     514.5     323.3
+     164.0      54.3      15.1       1.6       0.1
+    1739.8    1744.0    1757.0    1721.8    1748.6    1894.2    1976.7    1971.4
+    1956.9    1944.0    1936.1    1924.3    1898.2    1872.1    1842.7    1794.3
+    1718.2    1646.3    1576.3    1483.8    1417.8    1401.1    1440.9    1460.8
+    1416.0    1272.8    1065.0     836.9     632.9     445.5     286.8     178.6
+      94.5      37.6      12.6       1.2       0.3
+    1739.8    1741.7    1753.4    1722.7    1722.0    1842.1    1944.5    1941.7
+    1918.0    1891.7    1868.5    1851.0    1816.6    1752.6    1640.2    1519.0
+    1434.8    1324.8    1200.9    1070.9     944.0     869.6     846.3     831.3
+     790.4     737.8     618.3     463.7     343.7     247.9     165.4     107.8
+      60.6      28.2      10.6       1.1       0.1
+    1739.8    1739.8    1748.9    1724.2    1698.3    1788.1    1901.0    1907.5
+    1882.0    1856.4    1822.8    1785.5    1710.2    1563.1    1442.7    1331.9
+    1185.6    1054.8     883.0     738.7     614.5     520.0     465.7     457.4
+     447.6     406.0     346.2     275.1     205.1     152.5     114.7      80.8
+      48.6      23.4       9.0       1.0       0.0
+    1739.8    1738.1    1743.9    1725.3    1681.1    1728.5    1845.0    1873.7
+    1849.8    1813.1    1773.9    1679.5    1531.6    1417.5    1288.7    1126.7
+     980.5     804.6     659.8     523.7     411.4     329.1     333.5     349.1
+     341.6     296.6     255.7     218.4     169.0     130.9     111.9      85.7
+      54.1      24.0       7.9       0.6       0.4
+    1739.8    1736.4    1738.6    1726.7    1675.0    1674.9    1765.5    1832.5
+    1821.7    1779.7    1679.0    1525.9    1406.7    1277.8    1119.9     973.6
+     776.8     629.6     489.6     373.0     292.8     297.0     338.4     373.3
+     360.6     319.8     289.8     241.5     177.2     139.2     116.7      85.4
+      50.4      21.6       5.9       0.6       0.2
+    1739.8    1734.9    1733.0    1726.6    1678.5    1639.2    1676.6    1759.3
+    1781.8    1728.1    1560.6    1415.1    1296.8    1138.6     994.1     791.0
+     639.3     488.9     362.5     271.5     278.4     342.3     430.8     470.2
+     433.8     400.4     375.7     296.9     194.9     131.9      99.3      72.7
+      46.8      21.9       6.2       0.3       0.1
+    1739.8    1733.4    1726.2    1721.4    1683.0    1628.9    1608.0    1656.3
+    1704.4    1642.1    1461.3    1338.6    1178.5    1036.7     842.3     665.5
+     509.1     370.1     248.1     237.4     299.4     424.0     538.7     550.9
+     505.8     511.8     478.4     333.3     185.0     107.6      77.9      62.2
+      39.9      17.1       4.6       0.4       0.7
+    1739.8    1731.9    1718.6    1711.6    1685.5    1634.5    1578.3    1566.8
+    1578.6    1503.9    1391.7    1255.9    1088.7     918.0     716.7     559.7
+     401.4     255.8     187.5     211.9     297.5     428.9     500.2     478.8
+     461.1     488.9     417.1     244.3     117.7      65.4      46.5      33.1
+      19.9       8.9       2.5       0.5       0.7
+    1739.8    1730.4    1712.0    1698.4    1680.5    1643.5    1584.6    1522.5
+    1448.6    1328.1    1281.5    1153.1    1024.0     806.2     630.3     450.3
+     309.5     184.1     158.9     180.9     253.0     347.8     373.9     338.1
+     321.6     318.9     245.2     135.1      69.1      41.8      29.2      21.0
+      13.5       6.3       1.6       0.2       0.5
+    1739.8    1728.7    1707.5    1686.4    1669.4    1640.1    1597.6    1534.1
+    1390.3    1215.9    1131.7    1016.4     911.0     711.6     558.9     394.2
+     239.7     152.3     141.0     158.0     216.4     290.9     300.0     255.6
+     228.7     224.1     172.4      88.1      36.6      18.1      12.7       8.7
+       5.8       3.1       0.9       0.3       0.3
+    1739.8    1727.3    1704.7    1677.5    1654.2    1625.8    1594.5    1538.4
+    1391.4    1223.5    1086.3     937.0     788.1     619.2     476.3     340.5
+     195.2     136.2     124.3     136.7     186.6     250.5     253.2     209.8
+     198.9     210.2     154.6      57.6      19.1       7.5       2.6       0.4
+       0.3       0.1       0.3       0.1       0.1
+    1739.8    1726.1    1702.3    1671.4    1641.1    1613.2    1577.6    1520.4
+    1376.1    1245.4    1102.8     950.4     762.0     587.5     432.1     303.3
+     173.0     126.7     111.5     113.1     144.8     183.9     180.1     150.1
+     148.3     152.5     110.7      58.0      29.4      14.6       6.5       3.5
+       2.2       1.2       0.3       0.2       0.3
+    1739.8    1725.3    1699.4    1669.8    1633.6    1599.7    1563.7    1498.5
+    1345.3    1223.2    1074.8     935.8     757.2     592.9     431.9     290.9
+     166.9     125.1     107.4     101.7     108.6     116.4     100.5      77.0
+      69.3      66.1      49.9      31.9      17.8       9.2       6.1       5.1
+       3.8       2.4       0.4       0.2       0.1
+    1739.8    1725.1    1697.8    1670.7    1629.3    1595.2    1555.4    1496.7
+    1324.7    1209.7    1067.7     938.0     736.8     576.8     423.8     288.7
+     163.9     124.3     105.5      94.1      90.2      83.9      64.6      44.4
+      32.4      26.8      19.6      12.9       7.9       4.1       2.8       2.2
+       1.1       0.2       0.2       0.1       0.2

+ 87 - 0
examples/ies/06b4cfdc8805709e767b5e2e904be8ad.ies

@@ -0,0 +1,87 @@
+IESNA:LM-63-1995
+[TEST] 
+[MANUFAC] BEGA
+[MORE] Copyright LUMCat V 
+[LUMCAT] 
+[LUMINAIRE] 50899.2K3
+[LAMPCAT] LED  11,5W
+[LAMP]    1221 lm,14 W
+TILT=NONE
+1 -1 1.0 19 24 1 2 -0.120 0.000 0.000
+1.0 1.0 14
+   0.0   5.0  10.0  15.0  20.0  25.0  30.0  35.0  40.0  45.0  50.0  55.0  60.0
+  65.0  70.0  75.0  80.0  85.0  90.0
+   0.0  15.0  30.0  45.0  60.0  75.0  90.0 105.0 120.0 135.0 150.0 165.0 180.0
+ 195.0 210.0 225.0 240.0 255.0 270.0 285.0 300.0 315.0 330.0 345.0
+    2160.3    1896.0    1447.3    1122.5     882.7     695.9     546.9     529.2
+     284.6      41.6      27.5      23.8      21.4      20.2      18.5      10.0
+       4.9       1.7       0.0
+    2160.3    1896.0    1447.3    1122.5     882.7     695.9     546.9     529.2
+     284.6      41.6      27.5      23.8      21.4      20.2      18.5      10.0
+       4.9       1.7       0.0
+    2160.3    1896.0    1447.3    1122.5     882.7     695.9     546.9     529.2
+     284.6      41.6      27.5      23.8      21.4      20.2      18.5      10.0
+       4.9       1.7       0.0
+    2160.3    1896.0    1447.3    1122.5     882.7     695.9     546.9     529.2
+     284.6      41.6      27.5      23.8      21.4      20.2      18.5      10.0
+       4.9       1.7       0.0
+    2160.3    1896.0    1447.3    1122.5     882.7     695.9     546.9     529.2
+     284.6      41.6      27.5      23.8      21.4      20.2      18.5      10.0
+       4.9       1.7       0.0
+    2160.3    1896.0    1447.3    1122.5     882.7     695.9     546.9     529.2
+     284.6      41.6      27.5      23.8      21.4      20.2      18.5      10.0
+       4.9       1.7       0.0
+    2160.3    1896.0    1447.3    1122.5     882.7     695.9     546.9     529.2
+     284.6      41.6      27.5      23.8      21.4      20.2      18.5      10.0
+       4.9       1.7       0.0
+    2160.3    1896.0    1447.3    1122.5     882.7     695.9     546.9     529.2
+     284.6      41.6      27.5      23.8      21.4      20.2      18.5      10.0
+       4.9       1.7       0.0
+    2160.3    1896.0    1447.3    1122.5     882.7     695.9     546.9     529.2
+     284.6      41.6      27.5      23.8      21.4      20.2      18.5      10.0
+       4.9       1.7       0.0
+    2160.3    1896.0    1447.3    1122.5     882.7     695.9     546.9     529.2
+     284.6      41.6      27.5      23.8      21.4      20.2      18.5      10.0
+       4.9       1.7       0.0
+    2160.3    1896.0    1447.3    1122.5     882.7     695.9     546.9     529.2
+     284.6      41.6      27.5      23.8      21.4      20.2      18.5      10.0
+       4.9       1.7       0.0
+    2160.3    1896.0    1447.3    1122.5     882.7     695.9     546.9     529.2
+     284.6      41.6      27.5      23.8      21.4      20.2      18.5      10.0
+       4.9       1.7       0.0
+    2160.3    1896.0    1447.3    1122.5     882.7     695.9     546.9     529.2
+     284.6      41.6      27.5      23.8      21.4      20.2      18.5      10.0
+       4.9       1.7       0.0
+    2160.3    1896.0    1447.3    1122.5     882.7     695.9     546.9     529.2
+     284.6      41.6      27.5      23.8      21.4      20.2      18.5      10.0
+       4.9       1.7       0.0
+    2160.3    1896.0    1447.3    1122.5     882.7     695.9     546.9     529.2
+     284.6      41.6      27.5      23.8      21.4      20.2      18.5      10.0
+       4.9       1.7       0.0
+    2160.3    1896.0    1447.3    1122.5     882.7     695.9     546.9     529.2
+     284.6      41.6      27.5      23.8      21.4      20.2      18.5      10.0
+       4.9       1.7       0.0
+    2160.3    1896.0    1447.3    1122.5     882.7     695.9     546.9     529.2
+     284.6      41.6      27.5      23.8      21.4      20.2      18.5      10.0
+       4.9       1.7       0.0
+    2160.3    1896.0    1447.3    1122.5     882.7     695.9     546.9     529.2
+     284.6      41.6      27.5      23.8      21.4      20.2      18.5      10.0
+       4.9       1.7       0.0
+    2160.3    1896.0    1447.3    1122.5     882.7     695.9     546.9     529.2
+     284.6      41.6      27.5      23.8      21.4      20.2      18.5      10.0
+       4.9       1.7       0.0
+    2160.3    1896.0    1447.3    1122.5     882.7     695.9     546.9     529.2
+     284.6      41.6      27.5      23.8      21.4      20.2      18.5      10.0
+       4.9       1.7       0.0
+    2160.3    1896.0    1447.3    1122.5     882.7     695.9     546.9     529.2
+     284.6      41.6      27.5      23.8      21.4      20.2      18.5      10.0
+       4.9       1.7       0.0
+    2160.3    1896.0    1447.3    1122.5     882.7     695.9     546.9     529.2
+     284.6      41.6      27.5      23.8      21.4      20.2      18.5      10.0
+       4.9       1.7       0.0
+    2160.3    1896.0    1447.3    1122.5     882.7     695.9     546.9     529.2
+     284.6      41.6      27.5      23.8      21.4      20.2      18.5      10.0
+       4.9       1.7       0.0
+    2160.3    1896.0    1447.3    1122.5     882.7     695.9     546.9     529.2
+     284.6      41.6      27.5      23.8      21.4      20.2      18.5      10.0
+       4.9       1.7       0.0

+ 30 - 0
examples/ies/1a936937a49c63374e6d4fbed9252b29.ies

@@ -0,0 +1,30 @@
+IESNA:LM-63-2002
+[TEST] LightLab International Test Report No. LL20433-R01-S1
+[MANUFAC] Efficient Lighting Systems,
+[MORE] Brunswick. VIC. 3056.
+[LUMINAIRE] Efficient Lighting Systems LED Display Track Light. Product ID: DT106.XTM10.N.94.61.
+[MORE] Cylindrical cast aluminium body and rectangular gear housing, 150 x 165 x 140 mm deep.
+[MORE] Recessed glass lens. Luminous opening of 92 mm diameter. Specular multifaceted “15 degree” reflector
+[MORE] about LED. One "Xicato XTM19954030CCA 0D" LED centred 60 mm above L/O.
+[MORE] One Harvard Technology CL40-1050S2D 220-240V 50/60Hz electronic driver set to “1050mA”
+[OTHER] Absolute test - lamp lumens value set to -1
+[MORE] NA conventions used for C0 plane alignment and C-plane rotation direction.
+[MORE] The sample was tested at a distance of 8m.
+[MORE] This IES file created by LightLab/LSA Report program version 3.803a.
+[DATE] This file created: Tuesday, 26 September 2017 4:14:01 PM
+[LUMCAT] DT106.XTM10.N.94.61
+[TESTLAB] LightLab International
+[ISSUEDATE] 26/09/2017
+TILT=NONE
+1 -1 1.498 181 1 1 2 -0.092 -0.092 0
+1 1 39
+0 0.5 1 1.5 2 2.5 3 3.5 4 4.5 5 5.5 6 6.5 7 7.5 8 8.5 9 9.5 10 10.5 11 11.5 12 12.5 13 13.5 14 14.5 15 15.5 16 16.5 17 17.5 18 18.5 19 19.5 20 20.5 21 21.5 22 22.5 23 23.5 24 24.5 25 25.5 26 26.5 27 27.5 28 28.5 29 29.5 30 30.5 31 31.5 32 32.5 33 33.5 34 
+34.5 35 35.5 36 36.5 37 37.5 38 38.5 39 39.5 40 40.5 41 41.5 42 42.5 43 43.5 44 44.5 45 45.5 46 46.5 47 47.5 48 48.5 49 49.5 50 50.5 51 51.5 52 52.5 53 53.5 54 54.5 55 55.5 56 56.5 57 57.5 58 58.5 59 59.5 60 60.5 61 61.5 62 62.5 63 63.5 64 64.5 65 65.5 66
+ 66.5 67 67.5 68 68.5 69 69.5 70 70.5 71 71.5 72 72.5 73 73.5 74 74.5 75 75.5 76 76.5 77 77.5 78 78.5 79 79.5 80 80.5 81 81.5 82 82.5 83 83.5 84 84.5 85 85.5 86 86.5 87 87.5 88 88.5 89 89.5 90
+0
+9769.798 9766.099 9733.675 9662.947 9575.834 9442.665 9280.229 9118.304 8886.575 8613.292 8341.374 7954.059 7478.424 7037.077 6451.128 5813.887 5183.818 4670.251 4124.621 3645.885 3310.188 2945.517 2631.518 2403.866 2164.416 1958.844 1816.224 1663.451 
+1524.167 1419.652 1306.115 1202.535 1115.449 1047.913 981.195 921.649 876.95 831.168 790.026 760.98 730.924 706.114 687.086 667.788 650.355 637.35 624.922 614.813 604.786 598.047 591.914 586.282 582.085 577.621 574.641 572.333 569.62 567.557 565.539 
+563.204 559.745 553.524 544.477 534.858 527.80 521.81 511.364 498.373 483.43 463.707 439.765 418.717 389.445 360.335 336.512 304.879 270.151 238.945 213.729 181.877 150.681 128.811 105.391 82.645 66.833 52.248 42.075 37.054 34.538 33.947 33.94 34.272 
+34.798 35.093 35.396 35.972 36.688 37.212 38.144 38.89 40.057 40.797 41.463 40.917 39.831 37.592 34.888 32.145 28.847 25.431 22.88 20.927 19.271 18.25 17.761 17.155 16.807 16.327 16.267 15.817 15.558 14.90 14.597 14.109 13.879 13.68 13.192 13.221 12.815 
+12.386 12.512 12.193 12.364 11.876 11.735 11.684 11.387 11.248 11.307 11.121 10.967 10.93 10.766 10.818 10.663 10.766 10.522 10.745 10.486 10.56 10.308 10.079 10.064 9.99 9.813 9.724 9.354 9.303 9.111 8.586 8.408 8.061 7.779 7.129 6.693 6.123 5.287 4.541 
+3.646 3.513 2.833 2.678 2.33 1.916 1.709 1.178 0.814 0.502 0.396 0.059 0.00

+ 3 - 0
examples/ies/README.md

@@ -0,0 +1,3 @@
+Profiles from the [IES Library](https://ieslibrary.com/en/home) website.
+
+New profiles can be created via [CNDL](https://cndl.io/).

+ 4 - 4
examples/index.html

@@ -42,7 +42,7 @@
 
 				<div id="inputWrapper">
 					<input placeholder="" type="text" id="filterInput" autocorrect="off" autocapitalize="off" spellcheck="false" />
-					<div id="exitSearchButton"></div>
+					<div id="clearSearchButton"></div>
 				</div>
 
 				<div id="content">
@@ -62,7 +62,7 @@
 		const content = document.getElementById( 'content' );
 		const viewer = document.getElementById( 'viewer' );
 		const filterInput = document.getElementById( 'filterInput' );
-		const exitSearchButton = document.getElementById( 'exitSearchButton' );
+		const clearSearchButton = document.getElementById( 'clearSearchButton' );
 		const expandButton = document.getElementById( 'expandButton' );
 		const viewSrcButton = document.getElementById( 'button' );
 		const panelScrim = document.getElementById( 'panelScrim' );
@@ -166,11 +166,11 @@
 
 			};
 
-			exitSearchButton.onclick = function ( ) {
+			clearSearchButton.onclick = function ( ) {
 
 				filterInput.value = '';
 				updateFilter( files, tags );
-				panel.classList.remove( 'searchFocused' );
+				filterInput.focus();
 
 			};
 

+ 1828 - 1821
examples/jsm/controls/ArcballControls.js

@@ -230,2986 +230,2993 @@ class ArcballControls extends EventDispatcher {
 
 		this.initializeMouseActions();
 
-		this.domElement.addEventListener( 'contextmenu', this.onContextMenu );
-		this.domElement.addEventListener( 'wheel', this.onWheel );
-		this.domElement.addEventListener( 'pointerdown', this.onPointerDown );
-		this.domElement.addEventListener( 'pointercancel', this.onPointerCancel );
+		this._onContextMenu = onContextMenu.bind( this );
+		this._onWheel = onWheel.bind( this );
+		this._onPointerUp = onPointerUp.bind( this );
+		this._onPointerMove = onPointerMove.bind( this );
+		this._onPointerDown = onPointerDown.bind( this );
+		this._onPointerCancel = onPointerCancel.bind( this );
+		this._onWindowResize = onWindowResize.bind( this );
 
-		window.addEventListener( 'resize', this.onWindowResize );
+		this.domElement.addEventListener( 'contextmenu', this._onContextMenu );
+		this.domElement.addEventListener( 'wheel', this._onWheel );
+		this.domElement.addEventListener( 'pointerdown', this._onPointerDown );
+		this.domElement.addEventListener( 'pointercancel', this._onPointerCancel );
 
-	}
-
-	//listeners
+		window.addEventListener( 'resize', this._onWindowResize );
 
-	onWindowResize = () => {
-
-		const scale = ( this._gizmos.scale.x + this._gizmos.scale.y + this._gizmos.scale.z ) / 3;
-		this._tbRadius = this.calculateTbRadius( this.camera );
+	}
 
-		const newRadius = this._tbRadius / scale;
-		const curve = new EllipseCurve( 0, 0, newRadius, newRadius );
-		const points = curve.getPoints( this._curvePts );
-		const curveGeometry = new BufferGeometry().setFromPoints( points );
+	onSinglePanStart( event, operation ) {
 
+		if ( this.enabled ) {
 
-		for ( const gizmo in this._gizmos.children ) {
+			this.dispatchEvent( _startEvent );
 
-			this._gizmos.children[ gizmo ].geometry = curveGeometry;
+			this.setCenter( event.clientX, event.clientY );
 
-		}
+			switch ( operation ) {
 
-		this.dispatchEvent( _changeEvent );
+				case 'PAN':
 
-	};
+					if ( ! this.enablePan ) {
 
-	onContextMenu = ( event ) => {
+						return;
 
-		if ( ! this.enabled ) {
+					}
 
-			return;
+					if ( this._animationId != - 1 ) {
 
-		}
+						cancelAnimationFrame( this._animationId );
+						this._animationId = - 1;
+						this._timeStart = - 1;
 
-		for ( let i = 0; i < this.mouseActions.length; i ++ ) {
+						this.activateGizmos( false );
+						this.dispatchEvent( _changeEvent );
 
-			if ( this.mouseActions[ i ].mouse == 2 ) {
+					}
 
-				//prevent only if button 2 is actually used
-				event.preventDefault();
-				break;
+					this.updateTbState( STATE.PAN, true );
+					this._startCursorPosition.copy( this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement ) );
+					if ( this.enableGrid ) {
 
-			}
+						this.drawGrid();
+						this.dispatchEvent( _changeEvent );
 
-		}
+					}
 
-	};
+					break;
 
-	onPointerCancel = () => {
+				case 'ROTATE':
 
-		this._touchStart.splice( 0, this._touchStart.length );
-		this._touchCurrent.splice( 0, this._touchCurrent.length );
-		this._input = INPUT.NONE;
+					if ( ! this.enableRotate ) {
 
-	};
+						return;
 
-	onPointerDown = ( event ) => {
+					}
 
-		if ( event.button == 0 && event.isPrimary ) {
+					if ( this._animationId != - 1 ) {
 
-			this._downValid = true;
-			this._downEvents.push( event );
-			this._downStart = performance.now();
+						cancelAnimationFrame( this._animationId );
+						this._animationId = - 1;
+						this._timeStart = - 1;
 
-		} else {
+					}
 
-			this._downValid = false;
+					this.updateTbState( STATE.ROTATE, true );
+					this._startCursorPosition.copy( this.unprojectOnTbSurface( this.camera, _center.x, _center.y, this.domElement, this._tbRadius ) );
+					this.activateGizmos( true );
+					if ( this.enableAnimations ) {
 
-		}
+						this._timePrev = this._timeCurrent = performance.now();
+						this._angleCurrent = this._anglePrev = 0;
+						this._cursorPosPrev.copy( this._startCursorPosition );
+						this._cursorPosCurr.copy( this._cursorPosPrev );
+						this._wCurr = 0;
+						this._wPrev = this._wCurr;
 
-		if ( event.pointerType == 'touch' && this._input != INPUT.CURSOR ) {
+					}
 
-			this._touchStart.push( event );
-			this._touchCurrent.push( event );
+					this.dispatchEvent( _changeEvent );
+					break;
 
-			switch ( this._input ) {
+				case 'FOV':
 
-				case INPUT.NONE:
+					if ( ! this.camera.isPerspectiveCamera || ! this.enableZoom ) {
 
-					//singleStart
-					this._input = INPUT.ONE_FINGER;
-					this.onSinglePanStart( event, 'ROTATE' );
+						return;
 
-					window.addEventListener( 'pointermove', this.onPointerMove );
-					window.addEventListener( 'pointerup', this.onPointerUp );
+					}
 
-					break;
+					if ( this._animationId != - 1 ) {
 
-				case INPUT.ONE_FINGER:
-				case INPUT.ONE_FINGER_SWITCHED:
+						cancelAnimationFrame( this._animationId );
+						this._animationId = - 1;
+						this._timeStart = - 1;
 
-					//doubleStart
-					this._input = INPUT.TWO_FINGER;
+						this.activateGizmos( false );
+						this.dispatchEvent( _changeEvent );
 
-					this.onRotateStart();
-					this.onPinchStart();
-					this.onDoublePanStart();
+					}
 
+					this.updateTbState( STATE.FOV, true );
+					this._startCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 );
+					this._currentCursorPosition.copy( this._startCursorPosition );
 					break;
 
-				case INPUT.TWO_FINGER:
+				case 'ZOOM':
 
-					//multipleStart
-					this._input = INPUT.MULT_FINGER;
-					this.onTriplePanStart( event );
-					break;
+					if ( ! this.enableZoom ) {
 
-			}
+						return;
 
-		} else if ( event.pointerType != 'touch' && this._input == INPUT.NONE ) {
+					}
 
-			let modifier = null;
+					if ( this._animationId != - 1 ) {
 
-			if ( event.ctrlKey || event.metaKey ) {
+						cancelAnimationFrame( this._animationId );
+						this._animationId = - 1;
+						this._timeStart = - 1;
 
-				modifier = 'CTRL';
+						this.activateGizmos( false );
+						this.dispatchEvent( _changeEvent );
 
-			} else if ( event.shiftKey ) {
+					}
 
-				modifier = 'SHIFT';
+					this.updateTbState( STATE.SCALE, true );
+					this._startCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 );
+					this._currentCursorPosition.copy( this._startCursorPosition );
+					break;
 
 			}
 
-			this._mouseOp = this.getOpFromAction( event.button, modifier );
-			if ( this._mouseOp != null ) {
+		}
 
-				window.addEventListener( 'pointermove', this.onPointerMove );
-				window.addEventListener( 'pointerup', this.onPointerUp );
+	}
 
-				//singleStart
-				this._input = INPUT.CURSOR;
-				this._button = event.button;
-				this.onSinglePanStart( event, this._mouseOp );
+	onSinglePanMove( event, opState ) {
 
-			}
+		if ( this.enabled ) {
 
-		}
+			const restart = opState != this._state;
+			this.setCenter( event.clientX, event.clientY );
 
-	};
+			switch ( opState ) {
 
-	onPointerMove = ( event ) => {
+				case STATE.PAN:
 
-		if ( event.pointerType == 'touch' && this._input != INPUT.CURSOR ) {
+					if ( this.enablePan ) {
 
-			switch ( this._input ) {
+						if ( restart ) {
 
-				case INPUT.ONE_FINGER:
+							//switch to pan operation
 
-					//singleMove
-					this.updateTouchEvent( event );
+							this.dispatchEvent( _endEvent );
+							this.dispatchEvent( _startEvent );
 
-					this.onSinglePanMove( event, STATE.ROTATE );
-					break;
+							this.updateTbState( opState, true );
+							this._startCursorPosition.copy( this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement ) );
+							if ( this.enableGrid ) {
+
+								this.drawGrid();
 
-				case INPUT.ONE_FINGER_SWITCHED:
+							}
 
-					const movement = this.calculatePointersDistance( this._touchCurrent[ 0 ], event ) * this._devPxRatio;
+							this.activateGizmos( false );
 
-					if ( movement >= this._switchSensibility ) {
+						} else {
 
-						//singleMove
-						this._input = INPUT.ONE_FINGER;
-						this.updateTouchEvent( event );
+							//continue with pan operation
+							this._currentCursorPosition.copy( this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement ) );
+							this.applyTransformMatrix( this.pan( this._startCursorPosition, this._currentCursorPosition ) );
 
-						this.onSinglePanStart( event, 'ROTATE' );
-						break;
+						}
 
 					}
 
 					break;
 
-				case INPUT.TWO_FINGER:
+				case STATE.ROTATE:
 
-					//rotate/pan/pinchMove
-					this.updateTouchEvent( event );
+					if ( this.enableRotate ) {
 
-					this.onRotateMove();
-					this.onPinchMove();
-					this.onDoublePanMove();
+						if ( restart ) {
 
-					break;
+							//switch to rotate operation
 
-				case INPUT.MULT_FINGER:
+							this.dispatchEvent( _endEvent );
+							this.dispatchEvent( _startEvent );
 
-					//multMove
-					this.updateTouchEvent( event );
+							this.updateTbState( opState, true );
+							this._startCursorPosition.copy( this.unprojectOnTbSurface( this.camera, _center.x, _center.y, this.domElement, this._tbRadius ) );
 
-					this.onTriplePanMove( event );
-					break;
+							if ( this.enableGrid ) {
 
-			}
+								this.disposeGrid();
 
-		} else if ( event.pointerType != 'touch' && this._input == INPUT.CURSOR ) {
+							}
 
-			let modifier = null;
+							this.activateGizmos( true );
 
-			if ( event.ctrlKey || event.metaKey ) {
+						} else {
 
-				modifier = 'CTRL';
+							//continue with rotate operation
+							this._currentCursorPosition.copy( this.unprojectOnTbSurface( this.camera, _center.x, _center.y, this.domElement, this._tbRadius ) );
 
-			} else if ( event.shiftKey ) {
+							const distance = this._startCursorPosition.distanceTo( this._currentCursorPosition );
+							const angle = this._startCursorPosition.angleTo( this._currentCursorPosition );
+							const amount = Math.max( distance / this._tbRadius, angle ); //effective rotation angle
 
-				modifier = 'SHIFT';
+							this.applyTransformMatrix( this.rotate( this.calculateRotationAxis( this._startCursorPosition, this._currentCursorPosition ), amount ) );
 
-			}
+							if ( this.enableAnimations ) {
 
-			const mouseOpState = this.getOpStateFromAction( this._button, modifier );
+								this._timePrev = this._timeCurrent;
+								this._timeCurrent = performance.now();
+								this._anglePrev = this._angleCurrent;
+								this._angleCurrent = amount;
+								this._cursorPosPrev.copy( this._cursorPosCurr );
+								this._cursorPosCurr.copy( this._currentCursorPosition );
+								this._wPrev = this._wCurr;
+								this._wCurr = this.calculateAngularSpeed( this._anglePrev, this._angleCurrent, this._timePrev, this._timeCurrent );
 
-			if ( mouseOpState != null ) {
+							}
 
-				this.onSinglePanMove( event, mouseOpState );
+						}
 
-			}
+					}
 
-		}
+					break;
 
-		//checkDistance
-		if ( this._downValid ) {
+				case STATE.SCALE:
 
-			const movement = this.calculatePointersDistance( this._downEvents[ this._downEvents.length - 1 ], event ) * this._devPxRatio;
-			if ( movement > this._movementThreshold ) {
+					if ( this.enableZoom ) {
 
-				this._downValid = false;
+						if ( restart ) {
 
-			}
+							//switch to zoom operation
 
-		}
+							this.dispatchEvent( _endEvent );
+							this.dispatchEvent( _startEvent );
 
-	};
+							this.updateTbState( opState, true );
+							this._startCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 );
+							this._currentCursorPosition.copy( this._startCursorPosition );
 
-	onPointerUp = ( event ) => {
+							if ( this.enableGrid ) {
 
-		if ( event.pointerType == 'touch' && this._input != INPUT.CURSOR ) {
+								this.disposeGrid();
 
-			const nTouch = this._touchCurrent.length;
+							}
 
-			for ( let i = 0; i < nTouch; i ++ ) {
+							this.activateGizmos( false );
 
-				if ( this._touchCurrent[ i ].pointerId == event.pointerId ) {
+						} else {
 
-					this._touchCurrent.splice( i, 1 );
-					this._touchStart.splice( i, 1 );
-					break;
+							//continue with zoom operation
+							const screenNotches = 8;	//how many wheel notches corresponds to a full screen pan
+							this._currentCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 );
 
-				}
+							const movement = this._currentCursorPosition.y - this._startCursorPosition.y;
 
-			}
+							let size = 1;
 
-			switch ( this._input ) {
+							if ( movement < 0 ) {
 
-				case INPUT.ONE_FINGER:
-				case INPUT.ONE_FINGER_SWITCHED:
+								size = 1 / ( Math.pow( this.scaleFactor, - movement * screenNotches ) );
 
-					//singleEnd
-					window.removeEventListener( 'pointermove', this.onPointerMove );
-					window.removeEventListener( 'pointerup', this.onPointerUp );
+							} else if ( movement > 0 ) {
 
-					this._input = INPUT.NONE;
-					this.onSinglePanEnd();
+								size = Math.pow( this.scaleFactor, movement * screenNotches );
 
-					break;
+							}
+
+							this._v3_1.setFromMatrixPosition( this._gizmoMatrixState );
 
-				case INPUT.TWO_FINGER:
+							this.applyTransformMatrix( this.scale( size, this._v3_1 ) );
 
-					//doubleEnd
-					this.onDoublePanEnd( event );
-					this.onPinchEnd( event );
-					this.onRotateEnd( event );
+						}
 
-					//switching to singleStart
-					this._input = INPUT.ONE_FINGER_SWITCHED;
+					}
 
 					break;
 
-				case INPUT.MULT_FINGER:
+				case STATE.FOV:
 
-					if ( this._touchCurrent.length == 0 ) {
+					if ( this.enableZoom && this.camera.isPerspectiveCamera ) {
 
-						window.removeEventListener( 'pointermove', this.onPointerMove );
-						window.removeEventListener( 'pointerup', this.onPointerUp );
+						if ( restart ) {
 
-						//multCancel
-						this._input = INPUT.NONE;
-						this.onTriplePanEnd();
+							//switch to fov operation
 
-					}
+							this.dispatchEvent( _endEvent );
+							this.dispatchEvent( _startEvent );
 
-					break;
+							this.updateTbState( opState, true );
+							this._startCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 );
+							this._currentCursorPosition.copy( this._startCursorPosition );
 
-			}
+							if ( this.enableGrid ) {
 
-		} else if ( event.pointerType != 'touch' && this._input == INPUT.CURSOR ) {
+								this.disposeGrid();
 
-			window.removeEventListener( 'pointermove', this.onPointerMove );
-			window.removeEventListener( 'pointerup', this.onPointerUp );
+							}
 
-			this._input = INPUT.NONE;
-			this.onSinglePanEnd();
-			this._button = - 1;
+							this.activateGizmos( false );
 
-		}
+						} else {
 
-		if ( event.isPrimary ) {
+							//continue with fov operation
+							const screenNotches = 8;	//how many wheel notches corresponds to a full screen pan
+							this._currentCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 );
 
-			if ( this._downValid ) {
+							const movement = this._currentCursorPosition.y - this._startCursorPosition.y;
 
-				const downTime = event.timeStamp - this._downEvents[ this._downEvents.length - 1 ].timeStamp;
+							let size = 1;
 
-				if ( downTime <= this._maxDownTime ) {
+							if ( movement < 0 ) {
 
-					if ( this._nclicks == 0 ) {
+								size = 1 / ( Math.pow( this.scaleFactor, - movement * screenNotches ) );
 
-						//first valid click detected
-						this._nclicks = 1;
-						this._clickStart = performance.now();
+							} else if ( movement > 0 ) {
 
-					} else {
+								size = Math.pow( this.scaleFactor, movement * screenNotches );
 
-						const clickInterval = event.timeStamp - this._clickStart;
-						const movement = this.calculatePointersDistance( this._downEvents[ 1 ], this._downEvents[ 0 ] ) * this._devPxRatio;
+							}
 
-						if ( clickInterval <= this._maxInterval && movement <= this._posThreshold ) {
+							this._v3_1.setFromMatrixPosition( this._cameraMatrixState );
+							const x = this._v3_1.distanceTo( this._gizmos.position );
+							let xNew = x / size; //distance between camera and gizmos if scale(size, scalepoint) would be performed
 
-							//second valid click detected
-							//fire double tap and reset values
-							this._nclicks = 0;
-							this._downEvents.splice( 0, this._downEvents.length );
-							this.onDoubleTap( event );
+							//check min and max distance
+							xNew = MathUtils.clamp( xNew, this.minDistance, this.maxDistance );
 
-						} else {
+							const y = x * Math.tan( MathUtils.DEG2RAD * this._fovState * 0.5 );
 
-							//new 'first click'
-							this._nclicks = 1;
-							this._downEvents.shift();
-							this._clickStart = performance.now();
+							//calculate new fov
+							let newFov = MathUtils.RAD2DEG * ( Math.atan( y / xNew ) * 2 );
 
-						}
+							//check min and max fov
+							newFov = MathUtils.clamp( newFov, this.minFov, this.maxFov );
 
-					}
+							const newDistance = y / Math.tan( MathUtils.DEG2RAD * ( newFov / 2 ) );
+							size = x / newDistance;
+							this._v3_2.setFromMatrixPosition( this._gizmoMatrixState );
 
-				} else {
+							this.setFov( newFov );
+							this.applyTransformMatrix( this.scale( size, this._v3_2, false ) );
 
-					this._downValid = false;
-					this._nclicks = 0;
-					this._downEvents.splice( 0, this._downEvents.length );
+							//adjusting distance
+							_offset.copy( this._gizmos.position ).sub( this.camera.position ).normalize().multiplyScalar( newDistance / x );
+							this._m4_1.makeTranslation( _offset.x, _offset.y, _offset.z );
 
-				}
+						}
 
-			} else {
+					}
 
-				this._nclicks = 0;
-				this._downEvents.splice( 0, this._downEvents.length );
+					break;
 
 			}
 
-		}
-
-	};
+			this.dispatchEvent( _changeEvent );
 
-	onWheel = ( event ) => {
+		}
 
-		if ( this.enabled && this.enableZoom ) {
+	}
 
-			let modifier = null;
+	onSinglePanEnd() {
 
-			if ( event.ctrlKey || event.metaKey ) {
+		if ( this._state == STATE.ROTATE ) {
 
-				modifier = 'CTRL';
 
-			} else if ( event.shiftKey ) {
+			if ( ! this.enableRotate ) {
 
-				modifier = 'SHIFT';
+				return;
 
 			}
 
-			const mouseOp = this.getOpFromAction( 'WHEEL', modifier );
+			if ( this.enableAnimations ) {
 
-			if ( mouseOp != null ) {
+				//perform rotation animation
+				const deltaTime = ( performance.now() - this._timeCurrent );
+				if ( deltaTime < 120 ) {
 
-				event.preventDefault();
-				this.dispatchEvent( _startEvent );
+					const w = Math.abs( ( this._wPrev + this._wCurr ) / 2 );
 
-				const notchDeltaY = 125; //distance of one notch of mouse wheel
-				let sgn = event.deltaY / notchDeltaY;
+					const self = this;
+					this._animationId = window.requestAnimationFrame( function ( t ) {
 
-				let size = 1;
+						self.updateTbState( STATE.ANIMATION_ROTATE, true );
+						const rotationAxis = self.calculateRotationAxis( self._cursorPosPrev, self._cursorPosCurr );
 
-				if ( sgn > 0 ) {
+						self.onRotationAnim( t, rotationAxis, Math.min( w, self.wMax ) );
 
-					size = 1 / this.scaleFactor;
+					} );
 
-				} else if ( sgn < 0 ) {
+				} else {
 
-					size = this.scaleFactor;
+					//cursor has been standing still for over 120 ms since last movement
+					this.updateTbState( STATE.IDLE, false );
+					this.activateGizmos( false );
+					this.dispatchEvent( _changeEvent );
 
 				}
 
-				switch ( mouseOp ) {
+			} else {
 
-					case 'ZOOM':
+				this.updateTbState( STATE.IDLE, false );
+				this.activateGizmos( false );
+				this.dispatchEvent( _changeEvent );
 
-						this.updateTbState( STATE.SCALE, true );
+			}
 
-						if ( sgn > 0 ) {
+		} else if ( this._state == STATE.PAN || this._state == STATE.IDLE ) {
 
-							size = 1 / ( Math.pow( this.scaleFactor, sgn ) );
+			this.updateTbState( STATE.IDLE, false );
 
-						} else if ( sgn < 0 ) {
+			if ( this.enableGrid ) {
 
-							size = Math.pow( this.scaleFactor, - sgn );
+				this.disposeGrid();
 
-						}
+			}
 
-						if ( this.cursorZoom && this.enablePan ) {
+			this.activateGizmos( false );
+			this.dispatchEvent( _changeEvent );
 
-							let scalePoint;
 
-							if ( this.camera.isOrthographicCamera ) {
+		}
 
-								scalePoint = this.unprojectOnTbPlane( this.camera, event.clientX, event.clientY, this.domElement ).applyQuaternion( this.camera.quaternion ).multiplyScalar( 1 / this.camera.zoom ).add( this._gizmos.position );
+		this.dispatchEvent( _endEvent );
 
-							} else if ( this.camera.isPerspectiveCamera ) {
+	}
 
-								scalePoint = this.unprojectOnTbPlane( this.camera, event.clientX, event.clientY, this.domElement ).applyQuaternion( this.camera.quaternion ).add( this._gizmos.position );
+	onDoubleTap( event ) {
 
-							}
+		if ( this.enabled && this.enablePan && this.scene != null ) {
 
-							this.applyTransformMatrix( this.scale( size, scalePoint ) );
+			this.dispatchEvent( _startEvent );
 
-						} else {
+			this.setCenter( event.clientX, event.clientY );
+			const hitP = this.unprojectOnObj( this.getCursorNDC( _center.x, _center.y, this.domElement ), this.camera );
 
-							this.applyTransformMatrix( this.scale( size, this._gizmos.position ) );
+			if ( hitP != null && this.enableAnimations ) {
 
-						}
+				const self = this;
+				if ( this._animationId != - 1 ) {
 
-						if ( this._grid != null ) {
+					window.cancelAnimationFrame( this._animationId );
 
-							this.disposeGrid();
-							this.drawGrid();
+				}
 
-						}
+				this._timeStart = - 1;
+				this._animationId = window.requestAnimationFrame( function ( t ) {
 
-						this.updateTbState( STATE.IDLE, false );
+					self.updateTbState( STATE.ANIMATION_FOCUS, true );
+					self.onFocusAnim( t, hitP, self._cameraMatrixState, self._gizmoMatrixState );
 
-						this.dispatchEvent( _changeEvent );
-						this.dispatchEvent( _endEvent );
+				} );
 
-						break;
+			} else if ( hitP != null && ! this.enableAnimations ) {
 
-					case 'FOV':
+				this.updateTbState( STATE.FOCUS, true );
+				this.focus( hitP, this.scaleFactor );
+				this.updateTbState( STATE.IDLE, false );
+				this.dispatchEvent( _changeEvent );
 
-						if ( this.camera.isPerspectiveCamera ) {
+			}
 
-							this.updateTbState( STATE.FOV, true );
+		}
 
+		this.dispatchEvent( _endEvent );
 
-							//Vertigo effect
+	}
 
-							//	  fov / 2
-							//		|\
-							//		| \
-							//		|  \
-							//	x	|	\
-							//		| 	 \
-							//		| 	  \
-							//		| _ _ _\
-							//			y
+	onDoublePanStart() {
 
-							//check for iOs shift shortcut
-							if ( event.deltaX != 0 ) {
+		if ( this.enabled && this.enablePan ) {
 
-								sgn = event.deltaX / notchDeltaY;
+			this.dispatchEvent( _startEvent );
 
-								size = 1;
+			this.updateTbState( STATE.PAN, true );
 
-								if ( sgn > 0 ) {
+			this.setCenter( ( this._touchCurrent[ 0 ].clientX + this._touchCurrent[ 1 ].clientX ) / 2, ( this._touchCurrent[ 0 ].clientY + this._touchCurrent[ 1 ].clientY ) / 2 );
+			this._startCursorPosition.copy( this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement, true ) );
+			this._currentCursorPosition.copy( this._startCursorPosition );
 
-									size = 1 / ( Math.pow( this.scaleFactor, sgn ) );
+			this.activateGizmos( false );
 
-								} else if ( sgn < 0 ) {
+		}
 
-									size = Math.pow( this.scaleFactor, - sgn );
+	}
 
-								}
+	onDoublePanMove() {
 
-							}
+		if ( this.enabled && this.enablePan ) {
 
-							this._v3_1.setFromMatrixPosition( this._cameraMatrixState );
-							const x = this._v3_1.distanceTo( this._gizmos.position );
-							let xNew = x / size;	//distance between camera and gizmos if scale(size, scalepoint) would be performed
+			this.setCenter( ( this._touchCurrent[ 0 ].clientX + this._touchCurrent[ 1 ].clientX ) / 2, ( this._touchCurrent[ 0 ].clientY + this._touchCurrent[ 1 ].clientY ) / 2 );
 
-							//check min and max distance
-							xNew = MathUtils.clamp( xNew, this.minDistance, this.maxDistance );
+			if ( this._state != STATE.PAN ) {
 
-							const y = x * Math.tan( MathUtils.DEG2RAD * this.camera.fov * 0.5 );
+				this.updateTbState( STATE.PAN, true );
+				this._startCursorPosition.copy( this._currentCursorPosition );
 
-							//calculate new fov
-							let newFov = MathUtils.RAD2DEG * ( Math.atan( y / xNew ) * 2 );
+			}
 
-							//check min and max fov
-							if ( newFov > this.maxFov ) {
+			this._currentCursorPosition.copy( this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement, true ) );
+			this.applyTransformMatrix( this.pan( this._startCursorPosition, this._currentCursorPosition, true ) );
+			this.dispatchEvent( _changeEvent );
 
-								newFov = this.maxFov;
+		}
 
-							} else if ( newFov < this.minFov ) {
+	}
 
-								newFov = this.minFov;
+	onDoublePanEnd() {
 
-							}
+		this.updateTbState( STATE.IDLE, false );
+		this.dispatchEvent( _endEvent );
 
-							const newDistance = y / Math.tan( MathUtils.DEG2RAD * ( newFov / 2 ) );
-							size = x / newDistance;
+	}
 
-							this.setFov( newFov );
-							this.applyTransformMatrix( this.scale( size, this._gizmos.position, false ) );
+	onRotateStart() {
 
-						}
+		if ( this.enabled && this.enableRotate ) {
 
-						if ( this._grid != null ) {
+			this.dispatchEvent( _startEvent );
 
-							this.disposeGrid();
-							this.drawGrid();
+			this.updateTbState( STATE.ZROTATE, true );
 
-						}
+			//this._startFingerRotation = event.rotation;
 
-						this.updateTbState( STATE.IDLE, false );
+			this._startFingerRotation = this.getAngle( this._touchCurrent[ 1 ], this._touchCurrent[ 0 ] ) + this.getAngle( this._touchStart[ 1 ], this._touchStart[ 0 ] );
+			this._currentFingerRotation = this._startFingerRotation;
 
-						this.dispatchEvent( _changeEvent );
-						this.dispatchEvent( _endEvent );
+			this.camera.getWorldDirection( this._rotationAxis ); //rotation axis
 
-						break;
+			if ( ! this.enablePan && ! this.enableZoom ) {
 
-				}
+				this.activateGizmos( true );
 
 			}
 
 		}
 
-	};
+	}
 
-	onSinglePanStart = ( event, operation ) => {
+	onRotateMove() {
 
-		if ( this.enabled ) {
+		if ( this.enabled && this.enableRotate ) {
 
-			this.dispatchEvent( _startEvent );
+			this.setCenter( ( this._touchCurrent[ 0 ].clientX + this._touchCurrent[ 1 ].clientX ) / 2, ( this._touchCurrent[ 0 ].clientY + this._touchCurrent[ 1 ].clientY ) / 2 );
+			let rotationPoint;
 
-			this.setCenter( event.clientX, event.clientY );
+			if ( this._state != STATE.ZROTATE ) {
 
-			switch ( operation ) {
+				this.updateTbState( STATE.ZROTATE, true );
+				this._startFingerRotation = this._currentFingerRotation;
 
-				case 'PAN':
+			}
 
-					if ( ! this.enablePan ) {
+			//this._currentFingerRotation = event.rotation;
+			this._currentFingerRotation = this.getAngle( this._touchCurrent[ 1 ], this._touchCurrent[ 0 ] ) + this.getAngle( this._touchStart[ 1 ], this._touchStart[ 0 ] );
 
-						return;
+			if ( ! this.enablePan ) {
 
-					}
+				rotationPoint = new Vector3().setFromMatrixPosition( this._gizmoMatrixState );
 
-					if ( this._animationId != - 1 ) {
+			} else {
 
-						cancelAnimationFrame( this._animationId );
-						this._animationId = - 1;
-						this._timeStart = - 1;
+				this._v3_2.setFromMatrixPosition( this._gizmoMatrixState );
+				rotationPoint = this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement ).applyQuaternion( this.camera.quaternion ).multiplyScalar( 1 / this.camera.zoom ).add( this._v3_2 );
 
-						this.activateGizmos( false );
-						this.dispatchEvent( _changeEvent );
+			}
 
-					}
+			const amount = MathUtils.DEG2RAD * ( this._startFingerRotation - this._currentFingerRotation );
 
-					this.updateTbState( STATE.PAN, true );
-					this._startCursorPosition.copy( this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement ) );
-					if ( this.enableGrid ) {
+			this.applyTransformMatrix( this.zRotate( rotationPoint, amount ) );
+			this.dispatchEvent( _changeEvent );
 
-						this.drawGrid();
-						this.dispatchEvent( _changeEvent );
+		}
 
-					}
+	}
 
-					break;
+	onRotateEnd() {
 
-				case 'ROTATE':
+		this.updateTbState( STATE.IDLE, false );
+		this.activateGizmos( false );
+		this.dispatchEvent( _endEvent );
 
-					if ( ! this.enableRotate ) {
+	}
 
-						return;
+	onPinchStart() {
 
-					}
+		if ( this.enabled && this.enableZoom ) {
 
-					if ( this._animationId != - 1 ) {
+			this.dispatchEvent( _startEvent );
+			this.updateTbState( STATE.SCALE, true );
 
-						cancelAnimationFrame( this._animationId );
-						this._animationId = - 1;
-						this._timeStart = - 1;
+			this._startFingerDistance = this.calculatePointersDistance( this._touchCurrent[ 0 ], this._touchCurrent[ 1 ] );
+			this._currentFingerDistance = this._startFingerDistance;
 
-					}
+			this.activateGizmos( false );
 
-					this.updateTbState( STATE.ROTATE, true );
-					this._startCursorPosition.copy( this.unprojectOnTbSurface( this.camera, _center.x, _center.y, this.domElement, this._tbRadius ) );
-					this.activateGizmos( true );
-					if ( this.enableAnimations ) {
+		}
 
-						this._timePrev = this._timeCurrent = performance.now();
-						this._angleCurrent = this._anglePrev = 0;
-						this._cursorPosPrev.copy( this._startCursorPosition );
-						this._cursorPosCurr.copy( this._cursorPosPrev );
-						this._wCurr = 0;
-						this._wPrev = this._wCurr;
+	}
 
-					}
+	onPinchMove() {
 
-					this.dispatchEvent( _changeEvent );
-					break;
+		if ( this.enabled && this.enableZoom ) {
 
-				case 'FOV':
+			this.setCenter( ( this._touchCurrent[ 0 ].clientX + this._touchCurrent[ 1 ].clientX ) / 2, ( this._touchCurrent[ 0 ].clientY + this._touchCurrent[ 1 ].clientY ) / 2 );
+			const minDistance = 12; //minimum distance between fingers (in css pixels)
 
-					if ( ! this.camera.isPerspectiveCamera || ! this.enableZoom ) {
+			if ( this._state != STATE.SCALE ) {
 
-						return;
+				this._startFingerDistance = this._currentFingerDistance;
+				this.updateTbState( STATE.SCALE, true );
 
-					}
+			}
 
-					if ( this._animationId != - 1 ) {
+			this._currentFingerDistance = Math.max( this.calculatePointersDistance( this._touchCurrent[ 0 ], this._touchCurrent[ 1 ] ), minDistance * this._devPxRatio );
+			const amount = this._currentFingerDistance / this._startFingerDistance;
 
-						cancelAnimationFrame( this._animationId );
-						this._animationId = - 1;
-						this._timeStart = - 1;
+			let scalePoint;
 
-						this.activateGizmos( false );
-						this.dispatchEvent( _changeEvent );
+			if ( ! this.enablePan ) {
 
-					}
+				scalePoint = this._gizmos.position;
 
-					this.updateTbState( STATE.FOV, true );
-					this._startCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 );
-					this._currentCursorPosition.copy( this._startCursorPosition );
-					break;
+			} else {
 
-				case 'ZOOM':
+				if ( this.camera.isOrthographicCamera ) {
 
-					if ( ! this.enableZoom ) {
+					scalePoint = this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement )
+						.applyQuaternion( this.camera.quaternion )
+						.multiplyScalar( 1 / this.camera.zoom )
+						.add( this._gizmos.position );
 
-						return;
+				} else if ( this.camera.isPerspectiveCamera ) {
 
-					}
+					scalePoint = this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement )
+						.applyQuaternion( this.camera.quaternion )
+						.add( this._gizmos.position );
 
-					if ( this._animationId != - 1 ) {
+				}
 
-						cancelAnimationFrame( this._animationId );
-						this._animationId = - 1;
-						this._timeStart = - 1;
+			}
 
-						this.activateGizmos( false );
-						this.dispatchEvent( _changeEvent );
+			this.applyTransformMatrix( this.scale( amount, scalePoint ) );
+			this.dispatchEvent( _changeEvent );
 
-					}
+		}
 
-					this.updateTbState( STATE.SCALE, true );
-					this._startCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 );
-					this._currentCursorPosition.copy( this._startCursorPosition );
-					break;
+	}
 
-			}
+	onPinchEnd() {
 
-		}
+		this.updateTbState( STATE.IDLE, false );
+		this.dispatchEvent( _endEvent );
 
-	};
+	}
 
-	onSinglePanMove = ( event, opState ) => {
+	onTriplePanStart() {
 
-		if ( this.enabled ) {
+		if ( this.enabled && this.enableZoom ) {
 
-			const restart = opState != this._state;
-			this.setCenter( event.clientX, event.clientY );
+			this.dispatchEvent( _startEvent );
 
-			switch ( opState ) {
+			this.updateTbState( STATE.SCALE, true );
 
-				case STATE.PAN:
+			//const center = event.center;
+			let clientX = 0;
+			let clientY = 0;
+			const nFingers = this._touchCurrent.length;
 
-					if ( this.enablePan ) {
+			for ( let i = 0; i < nFingers; i ++ ) {
 
-						if ( restart ) {
+				clientX += this._touchCurrent[ i ].clientX;
+				clientY += this._touchCurrent[ i ].clientY;
 
-							//switch to pan operation
+			}
 
-							this.dispatchEvent( _endEvent );
-							this.dispatchEvent( _startEvent );
+			this.setCenter( clientX / nFingers, clientY / nFingers );
 
-							this.updateTbState( opState, true );
-							this._startCursorPosition.copy( this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement ) );
-							if ( this.enableGrid ) {
+			this._startCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 );
+			this._currentCursorPosition.copy( this._startCursorPosition );
 
-								this.drawGrid();
+		}
 
-							}
+	}
 
-							this.activateGizmos( false );
+	onTriplePanMove() {
 
-						} else {
+		if ( this.enabled && this.enableZoom ) {
 
-							//continue with pan operation
-							this._currentCursorPosition.copy( this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement ) );
-							this.applyTransformMatrix( this.pan( this._startCursorPosition, this._currentCursorPosition ) );
+			//	  fov / 2
+			//		|\
+			//		| \
+			//		|  \
+			//	x	|	\
+			//		| 	 \
+			//		| 	  \
+			//		| _ _ _\
+			//			y
 
-						}
+			//const center = event.center;
+			let clientX = 0;
+			let clientY = 0;
+			const nFingers = this._touchCurrent.length;
 
-					}
+			for ( let i = 0; i < nFingers; i ++ ) {
 
-					break;
+				clientX += this._touchCurrent[ i ].clientX;
+				clientY += this._touchCurrent[ i ].clientY;
 
-				case STATE.ROTATE:
+			}
 
-					if ( this.enableRotate ) {
+			this.setCenter( clientX / nFingers, clientY / nFingers );
 
-						if ( restart ) {
+			const screenNotches = 8;	//how many wheel notches corresponds to a full screen pan
+			this._currentCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 );
 
-							//switch to rotate operation
+			const movement = this._currentCursorPosition.y - this._startCursorPosition.y;
 
-							this.dispatchEvent( _endEvent );
-							this.dispatchEvent( _startEvent );
+			let size = 1;
 
-							this.updateTbState( opState, true );
-							this._startCursorPosition.copy( this.unprojectOnTbSurface( this.camera, _center.x, _center.y, this.domElement, this._tbRadius ) );
+			if ( movement < 0 ) {
 
-							if ( this.enableGrid ) {
+				size = 1 / ( Math.pow( this.scaleFactor, - movement * screenNotches ) );
 
-								this.disposeGrid();
+			} else if ( movement > 0 ) {
 
-							}
+				size = Math.pow( this.scaleFactor, movement * screenNotches );
 
-							this.activateGizmos( true );
+			}
 
-						} else {
+			this._v3_1.setFromMatrixPosition( this._cameraMatrixState );
+			const x = this._v3_1.distanceTo( this._gizmos.position );
+			let xNew = x / size; //distance between camera and gizmos if scale(size, scalepoint) would be performed
 
-							//continue with rotate operation
-							this._currentCursorPosition.copy( this.unprojectOnTbSurface( this.camera, _center.x, _center.y, this.domElement, this._tbRadius ) );
+			//check min and max distance
+			xNew = MathUtils.clamp( xNew, this.minDistance, this.maxDistance );
 
-							const distance = this._startCursorPosition.distanceTo( this._currentCursorPosition );
-							const angle = this._startCursorPosition.angleTo( this._currentCursorPosition );
-							const amount = Math.max( distance / this._tbRadius, angle ); //effective rotation angle
+			const y = x * Math.tan( MathUtils.DEG2RAD * this._fovState * 0.5 );
 
-							this.applyTransformMatrix( this.rotate( this.calculateRotationAxis( this._startCursorPosition, this._currentCursorPosition ), amount ) );
+			//calculate new fov
+			let newFov = MathUtils.RAD2DEG * ( Math.atan( y / xNew ) * 2 );
 
-							if ( this.enableAnimations ) {
+			//check min and max fov
+			newFov = MathUtils.clamp( newFov, this.minFov, this.maxFov );
 
-								this._timePrev = this._timeCurrent;
-								this._timeCurrent = performance.now();
-								this._anglePrev = this._angleCurrent;
-								this._angleCurrent = amount;
-								this._cursorPosPrev.copy( this._cursorPosCurr );
-								this._cursorPosCurr.copy( this._currentCursorPosition );
-								this._wPrev = this._wCurr;
-								this._wCurr = this.calculateAngularSpeed( this._anglePrev, this._angleCurrent, this._timePrev, this._timeCurrent );
+			const newDistance = y / Math.tan( MathUtils.DEG2RAD * ( newFov / 2 ) );
+			size = x / newDistance;
+			this._v3_2.setFromMatrixPosition( this._gizmoMatrixState );
 
-							}
+			this.setFov( newFov );
+			this.applyTransformMatrix( this.scale( size, this._v3_2, false ) );
 
-						}
+			//adjusting distance
+			_offset.copy( this._gizmos.position ).sub( this.camera.position ).normalize().multiplyScalar( newDistance / x );
+			this._m4_1.makeTranslation( _offset.x, _offset.y, _offset.z );
 
-					}
+			this.dispatchEvent( _changeEvent );
 
-					break;
+		}
 
-				case STATE.SCALE:
+	}
 
-					if ( this.enableZoom ) {
+	onTriplePanEnd() {
 
-						if ( restart ) {
+		this.updateTbState( STATE.IDLE, false );
+		this.dispatchEvent( _endEvent );
+		//this.dispatchEvent( _changeEvent );
 
-							//switch to zoom operation
+	}
 
-							this.dispatchEvent( _endEvent );
-							this.dispatchEvent( _startEvent );
+	/**
+	 * Set _center's x/y coordinates
+	 * @param {Number} clientX
+	 * @param {Number} clientY
+	 */
+	setCenter( clientX, clientY ) {
 
-							this.updateTbState( opState, true );
-							this._startCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 );
-							this._currentCursorPosition.copy( this._startCursorPosition );
+		_center.x = clientX;
+		_center.y = clientY;
 
-							if ( this.enableGrid ) {
+	}
 
-								this.disposeGrid();
+	/**
+	 * Set default mouse actions
+	 */
+	initializeMouseActions() {
 
-							}
+		this.setMouseAction( 'PAN', 0, 'CTRL' );
+		this.setMouseAction( 'PAN', 2 );
 
-							this.activateGizmos( false );
+		this.setMouseAction( 'ROTATE', 0 );
 
-						} else {
+		this.setMouseAction( 'ZOOM', 'WHEEL' );
+		this.setMouseAction( 'ZOOM', 1 );
 
-							//continue with zoom operation
-							const screenNotches = 8;	//how many wheel notches corresponds to a full screen pan
-							this._currentCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 );
+		this.setMouseAction( 'FOV', 'WHEEL', 'SHIFT' );
+		this.setMouseAction( 'FOV', 1, 'SHIFT' );
 
-							const movement = this._currentCursorPosition.y - this._startCursorPosition.y;
 
-							let size = 1;
+	}
 
-							if ( movement < 0 ) {
+	/**
+	 * Compare two mouse actions
+	 * @param {Object} action1
+	 * @param {Object} action2
+	 * @returns {Boolean} True if action1 and action 2 are the same mouse action, false otherwise
+	 */
+	compareMouseAction( action1, action2 ) {
 
-								size = 1 / ( Math.pow( this.scaleFactor, - movement * screenNotches ) );
+		if ( action1.operation == action2.operation ) {
 
-							} else if ( movement > 0 ) {
+			if ( action1.mouse == action2.mouse && action1.key == action2.key ) {
 
-								size = Math.pow( this.scaleFactor, movement * screenNotches );
+				return true;
 
-							}
+			} else {
 
-							this._v3_1.setFromMatrixPosition( this._gizmoMatrixState );
+				return false;
 
-							this.applyTransformMatrix( this.scale( size, this._v3_1 ) );
+			}
 
-						}
+		} else {
 
-					}
+			return false;
 
-					break;
+		}
 
-				case STATE.FOV:
+	}
 
-					if ( this.enableZoom && this.camera.isPerspectiveCamera ) {
+	/**
+	 * Set a new mouse action by specifying the operation to be performed and a mouse/key combination. In case of conflict, replaces the existing one
+	 * @param {String} operation The operation to be performed ('PAN', 'ROTATE', 'ZOOM', 'FOV)
+	 * @param {*} mouse A mouse button (0, 1, 2) or 'WHEEL' for wheel notches
+	 * @param {*} key The keyboard modifier ('CTRL', 'SHIFT') or null if key is not needed
+	 * @returns {Boolean} True if the mouse action has been successfully added, false otherwise
+	 */
+	setMouseAction( operation, mouse, key = null ) {
 
-						if ( restart ) {
+		const operationInput = [ 'PAN', 'ROTATE', 'ZOOM', 'FOV' ];
+		const mouseInput = [ 0, 1, 2, 'WHEEL' ];
+		const keyInput = [ 'CTRL', 'SHIFT', null ];
+		let state;
 
-							//switch to fov operation
+		if ( ! operationInput.includes( operation ) || ! mouseInput.includes( mouse ) || ! keyInput.includes( key ) ) {
 
-							this.dispatchEvent( _endEvent );
-							this.dispatchEvent( _startEvent );
+			//invalid parameters
+			return false;
 
-							this.updateTbState( opState, true );
-							this._startCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 );
-							this._currentCursorPosition.copy( this._startCursorPosition );
+		}
 
-							if ( this.enableGrid ) {
+		if ( mouse == 'WHEEL' ) {
 
-								this.disposeGrid();
+			if ( operation != 'ZOOM' && operation != 'FOV' ) {
 
-							}
+				//cannot associate 2D operation to 1D input
+				return false;
 
-							this.activateGizmos( false );
+			}
 
-						} else {
+		}
 
-							//continue with fov operation
-							const screenNotches = 8;	//how many wheel notches corresponds to a full screen pan
-							this._currentCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 );
+		switch ( operation ) {
 
-							const movement = this._currentCursorPosition.y - this._startCursorPosition.y;
+			case 'PAN':
 
-							let size = 1;
+				state = STATE.PAN;
+				break;
 
-							if ( movement < 0 ) {
+			case 'ROTATE':
 
-								size = 1 / ( Math.pow( this.scaleFactor, - movement * screenNotches ) );
+				state = STATE.ROTATE;
+				break;
 
-							} else if ( movement > 0 ) {
+			case 'ZOOM':
 
-								size = Math.pow( this.scaleFactor, movement * screenNotches );
+				state = STATE.SCALE;
+				break;
 
-							}
+			case 'FOV':
 
-							this._v3_1.setFromMatrixPosition( this._cameraMatrixState );
-							const x = this._v3_1.distanceTo( this._gizmos.position );
-							let xNew = x / size; //distance between camera and gizmos if scale(size, scalepoint) would be performed
-
-							//check min and max distance
-							xNew = MathUtils.clamp( xNew, this.minDistance, this.maxDistance );
-
-							const y = x * Math.tan( MathUtils.DEG2RAD * this._fovState * 0.5 );
-
-							//calculate new fov
-							let newFov = MathUtils.RAD2DEG * ( Math.atan( y / xNew ) * 2 );
+				state = STATE.FOV;
+				break;
 
-							//check min and max fov
-							newFov = MathUtils.clamp( newFov, this.minFov, this.maxFov );
+		}
 
-							const newDistance = y / Math.tan( MathUtils.DEG2RAD * ( newFov / 2 ) );
-							size = x / newDistance;
-							this._v3_2.setFromMatrixPosition( this._gizmoMatrixState );
+		const action = {
 
-							this.setFov( newFov );
-							this.applyTransformMatrix( this.scale( size, this._v3_2, false ) );
+			operation: operation,
+			mouse: mouse,
+			key: key,
+			state: state
 
-							//adjusting distance
-							_offset.copy( this._gizmos.position ).sub( this.camera.position ).normalize().multiplyScalar( newDistance / x );
-							this._m4_1.makeTranslation( _offset.x, _offset.y, _offset.z );
+		};
 
-						}
+		for ( let i = 0; i < this.mouseActions.length; i ++ ) {
 
-					}
+			if ( this.mouseActions[ i ].mouse == action.mouse && this.mouseActions[ i ].key == action.key ) {
 
-					break;
+				this.mouseActions.splice( i, 1, action );
+				return true;
 
 			}
 
-			this.dispatchEvent( _changeEvent );
-
 		}
 
-	};
+		this.mouseActions.push( action );
+		return true;
 
-	onSinglePanEnd = () => {
+	}
 
-		if ( this._state == STATE.ROTATE ) {
+	/**
+	 * Remove a mouse action by specifying its mouse/key combination
+	 * @param {*} mouse A mouse button (0, 1, 2) or 'WHEEL' for wheel notches
+	 * @param {*} key The keyboard modifier ('CTRL', 'SHIFT') or null if key is not needed
+	 * @returns {Boolean} True if the operation has been succesfully removed, false otherwise
+	 */
+	unsetMouseAction( mouse, key = null ) {
 
+		for ( let i = 0; i < this.mouseActions.length; i ++ ) {
 
-			if ( ! this.enableRotate ) {
+			if ( this.mouseActions[ i ].mouse == mouse && this.mouseActions[ i ].key == key ) {
 
-				return;
+				this.mouseActions.splice( i, 1 );
+				return true;
 
 			}
 
-			if ( this.enableAnimations ) {
-
-				//perform rotation animation
-				const deltaTime = ( performance.now() - this._timeCurrent );
-				if ( deltaTime < 120 ) {
-
-					const w = Math.abs( ( this._wPrev + this._wCurr ) / 2 );
-
-					const self = this;
-					this._animationId = window.requestAnimationFrame( function ( t ) {
-
-						self.updateTbState( STATE.ANIMATION_ROTATE, true );
-						const rotationAxis = self.calculateRotationAxis( self._cursorPosPrev, self._cursorPosCurr );
+		}
 
-						self.onRotationAnim( t, rotationAxis, Math.min( w, self.wMax ) );
+		return false;
 
-					} );
+	}
 
-				} else {
+	/**
+	 * Return the operation associated to a mouse/keyboard combination
+	 * @param {*} mouse A mouse button (0, 1, 2) or 'WHEEL' for wheel notches
+	 * @param {*} key The keyboard modifier ('CTRL', 'SHIFT') or null if key is not needed
+	 * @returns The operation if it has been found, null otherwise
+	 */
+	getOpFromAction( mouse, key ) {
 
-					//cursor has been standing still for over 120 ms since last movement
-					this.updateTbState( STATE.IDLE, false );
-					this.activateGizmos( false );
-					this.dispatchEvent( _changeEvent );
+		let action;
 
-				}
+		for ( let i = 0; i < this.mouseActions.length; i ++ ) {
 
-			} else {
+			action = this.mouseActions[ i ];
+			if ( action.mouse == mouse && action.key == key ) {
 
-				this.updateTbState( STATE.IDLE, false );
-				this.activateGizmos( false );
-				this.dispatchEvent( _changeEvent );
+				return action.operation;
 
 			}
 
-		} else if ( this._state == STATE.PAN || this._state == STATE.IDLE ) {
+		}
 
-			this.updateTbState( STATE.IDLE, false );
+		if ( key != null ) {
 
-			if ( this.enableGrid ) {
+			for ( let i = 0; i < this.mouseActions.length; i ++ ) {
 
-				this.disposeGrid();
+				action = this.mouseActions[ i ];
+				if ( action.mouse == mouse && action.key == null ) {
 
-			}
+					return action.operation;
 
-			this.activateGizmos( false );
-			this.dispatchEvent( _changeEvent );
+				}
 
+			}
 
 		}
 
-		this.dispatchEvent( _endEvent );
-
-	};
+		return null;
 
-	onDoubleTap = ( event ) => {
+	}
 
-		if ( this.enabled && this.enablePan && this.scene != null ) {
+	/**
+	 * Get the operation associated to mouse and key combination and returns the corresponding FSA state
+	 * @param {Number} mouse Mouse button
+	 * @param {String} key Keyboard modifier
+	 * @returns The FSA state obtained from the operation associated to mouse/keyboard combination
+	 */
+	getOpStateFromAction( mouse, key ) {
 
-			this.dispatchEvent( _startEvent );
+		let action;
 
-			this.setCenter( event.clientX, event.clientY );
-			const hitP = this.unprojectOnObj( this.getCursorNDC( _center.x, _center.y, this.domElement ), this.camera );
+		for ( let i = 0; i < this.mouseActions.length; i ++ ) {
 
-			if ( hitP != null && this.enableAnimations ) {
+			action = this.mouseActions[ i ];
+			if ( action.mouse == mouse && action.key == key ) {
 
-				const self = this;
-				if ( this._animationId != - 1 ) {
+				return action.state;
 
-					window.cancelAnimationFrame( this._animationId );
+			}
 
-				}
+		}
 
-				this._timeStart = - 1;
-				this._animationId = window.requestAnimationFrame( function ( t ) {
+		if ( key != null ) {
 
-					self.updateTbState( STATE.ANIMATION_FOCUS, true );
-					self.onFocusAnim( t, hitP, self._cameraMatrixState, self._gizmoMatrixState );
+			for ( let i = 0; i < this.mouseActions.length; i ++ ) {
 
-				} );
+				action = this.mouseActions[ i ];
+				if ( action.mouse == mouse && action.key == null ) {
 
-			} else if ( hitP != null && ! this.enableAnimations ) {
+					return action.state;
 
-				this.updateTbState( STATE.FOCUS, true );
-				this.focus( hitP, this.scaleFactor );
-				this.updateTbState( STATE.IDLE, false );
-				this.dispatchEvent( _changeEvent );
+				}
 
 			}
 
 		}
 
-		this.dispatchEvent( _endEvent );
+		return null;
 
-	};
+	}
 
-	onDoublePanStart = () => {
+	/**
+	 * Calculate the angle between two pointers
+	 * @param {PointerEvent} p1
+	 * @param {PointerEvent} p2
+	 * @returns {Number} The angle between two pointers in degrees
+	 */
+	getAngle( p1, p2 ) {
 
-		if ( this.enabled && this.enablePan ) {
+		return Math.atan2( p2.clientY - p1.clientY, p2.clientX - p1.clientX ) * 180 / Math.PI;
 
-			this.dispatchEvent( _startEvent );
+	}
 
-			this.updateTbState( STATE.PAN, true );
+	/**
+	 * Update a PointerEvent inside current pointerevents array
+	 * @param {PointerEvent} event
+	 */
+	updateTouchEvent( event ) {
 
-			this.setCenter( ( this._touchCurrent[ 0 ].clientX + this._touchCurrent[ 1 ].clientX ) / 2, ( this._touchCurrent[ 0 ].clientY + this._touchCurrent[ 1 ].clientY ) / 2 );
-			this._startCursorPosition.copy( this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement, true ) );
-			this._currentCursorPosition.copy( this._startCursorPosition );
+		for ( let i = 0; i < this._touchCurrent.length; i ++ ) {
 
-			this.activateGizmos( false );
+			if ( this._touchCurrent[ i ].pointerId == event.pointerId ) {
+
+				this._touchCurrent.splice( i, 1, event );
+				break;
+
+			}
 
 		}
 
-	};
+	}
 
-	onDoublePanMove = () => {
+	/**
+	 * Apply a transformation matrix, to the camera and gizmos
+	 * @param {Object} transformation Object containing matrices to apply to camera and gizmos
+	 */
+	applyTransformMatrix( transformation ) {
 
-		if ( this.enabled && this.enablePan ) {
+		if ( transformation.camera != null ) {
 
-			this.setCenter( ( this._touchCurrent[ 0 ].clientX + this._touchCurrent[ 1 ].clientX ) / 2, ( this._touchCurrent[ 0 ].clientY + this._touchCurrent[ 1 ].clientY ) / 2 );
+			this._m4_1.copy( this._cameraMatrixState ).premultiply( transformation.camera );
+			this._m4_1.decompose( this.camera.position, this.camera.quaternion, this.camera.scale );
+			this.camera.updateMatrix();
 
-			if ( this._state != STATE.PAN ) {
+			//update camera up vector
+			if ( this._state == STATE.ROTATE || this._state == STATE.ZROTATE || this._state == STATE.ANIMATION_ROTATE ) {
 
-				this.updateTbState( STATE.PAN, true );
-				this._startCursorPosition.copy( this._currentCursorPosition );
+				this.camera.up.copy( this._upState ).applyQuaternion( this.camera.quaternion );
 
 			}
 
-			this._currentCursorPosition.copy( this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement, true ) );
-			this.applyTransformMatrix( this.pan( this._startCursorPosition, this._currentCursorPosition, true ) );
-			this.dispatchEvent( _changeEvent );
+		}
+
+		if ( transformation.gizmos != null ) {
+
+			this._m4_1.copy( this._gizmoMatrixState ).premultiply( transformation.gizmos );
+			this._m4_1.decompose( this._gizmos.position, this._gizmos.quaternion, this._gizmos.scale );
+			this._gizmos.updateMatrix();
 
 		}
 
-	};
+		if ( this._state == STATE.SCALE || this._state == STATE.FOCUS || this._state == STATE.ANIMATION_FOCUS ) {
 
-	onDoublePanEnd = () => {
+			this._tbRadius = this.calculateTbRadius( this.camera );
 
-		this.updateTbState( STATE.IDLE, false );
-		this.dispatchEvent( _endEvent );
+			if ( this.adjustNearFar ) {
 
-	};
+				const cameraDistance = this.camera.position.distanceTo( this._gizmos.position );
 
+				const bb = new Box3();
+				bb.setFromObject( this._gizmos );
+				const sphere = new Sphere();
+				bb.getBoundingSphere( sphere );
 
-	onRotateStart = () => {
+				const adjustedNearPosition = Math.max( this._nearPos0, sphere.radius + sphere.center.length() );
+				const regularNearPosition = cameraDistance - this._initialNear;
 
-		if ( this.enabled && this.enableRotate ) {
+				const minNearPos = Math.min( adjustedNearPosition, regularNearPosition );
+				this.camera.near = cameraDistance - minNearPos;
 
-			this.dispatchEvent( _startEvent );
 
-			this.updateTbState( STATE.ZROTATE, true );
+				const adjustedFarPosition = Math.min( this._farPos0, - sphere.radius + sphere.center.length() );
+				const regularFarPosition = cameraDistance - this._initialFar;
 
-			//this._startFingerRotation = event.rotation;
+				const minFarPos = Math.min( adjustedFarPosition, regularFarPosition );
+				this.camera.far = cameraDistance - minFarPos;
 
-			this._startFingerRotation = this.getAngle( this._touchCurrent[ 1 ], this._touchCurrent[ 0 ] ) + this.getAngle( this._touchStart[ 1 ], this._touchStart[ 0 ] );
-			this._currentFingerRotation = this._startFingerRotation;
+				this.camera.updateProjectionMatrix();
 
-			this.camera.getWorldDirection( this._rotationAxis ); //rotation axis
+			} else {
 
-			if ( ! this.enablePan && ! this.enableZoom ) {
+				let update = false;
 
-				this.activateGizmos( true );
+				if ( this.camera.near != this._initialNear ) {
 
-			}
+					this.camera.near = this._initialNear;
+					update = true;
 
-		}
+				}
 
-	};
+				if ( this.camera.far != this._initialFar ) {
 
-	onRotateMove = () => {
+					this.camera.far = this._initialFar;
+					update = true;
 
-		if ( this.enabled && this.enableRotate ) {
+				}
 
-			this.setCenter( ( this._touchCurrent[ 0 ].clientX + this._touchCurrent[ 1 ].clientX ) / 2, ( this._touchCurrent[ 0 ].clientY + this._touchCurrent[ 1 ].clientY ) / 2 );
-			let rotationPoint;
+				if ( update ) {
 
-			if ( this._state != STATE.ZROTATE ) {
+					this.camera.updateProjectionMatrix();
 
-				this.updateTbState( STATE.ZROTATE, true );
-				this._startFingerRotation = this._currentFingerRotation;
+				}
 
 			}
 
-			//this._currentFingerRotation = event.rotation;
-			this._currentFingerRotation = this.getAngle( this._touchCurrent[ 1 ], this._touchCurrent[ 0 ] ) + this.getAngle( this._touchStart[ 1 ], this._touchStart[ 0 ] );
-
-			if ( ! this.enablePan ) {
-
-				rotationPoint = new Vector3().setFromMatrixPosition( this._gizmoMatrixState );
-
-			} else {
+		}
 
-				this._v3_2.setFromMatrixPosition( this._gizmoMatrixState );
-				rotationPoint = this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement ).applyQuaternion( this.camera.quaternion ).multiplyScalar( 1 / this.camera.zoom ).add( this._v3_2 );
+	}
 
-			}
+	/**
+	 * Calculate the angular speed
+	 * @param {Number} p0 Position at t0
+	 * @param {Number} p1 Position at t1
+	 * @param {Number} t0 Initial time in milliseconds
+	 * @param {Number} t1 Ending time in milliseconds
+	 */
+	calculateAngularSpeed( p0, p1, t0, t1 ) {
 
-			const amount = MathUtils.DEG2RAD * ( this._startFingerRotation - this._currentFingerRotation );
+		const s = p1 - p0;
+		const t = ( t1 - t0 ) / 1000;
+		if ( t == 0 ) {
 
-			this.applyTransformMatrix( this.zRotate( rotationPoint, amount ) );
-			this.dispatchEvent( _changeEvent );
+			return 0;
 
 		}
 
-	};
+		return s / t;
 
-	onRotateEnd = () => {
+	}
 
-		this.updateTbState( STATE.IDLE, false );
-		this.activateGizmos( false );
-		this.dispatchEvent( _endEvent );
+	/**
+	 * Calculate the distance between two pointers
+	 * @param {PointerEvent} p0 The first pointer
+	 * @param {PointerEvent} p1 The second pointer
+	 * @returns {number} The distance between the two pointers
+	 */
+	calculatePointersDistance( p0, p1 ) {
 
-	};
+		return Math.sqrt( Math.pow( p1.clientX - p0.clientX, 2 ) + Math.pow( p1.clientY - p0.clientY, 2 ) );
 
-	onPinchStart = () => {
+	}
 
-		if ( this.enabled && this.enableZoom ) {
+	/**
+	 * Calculate the rotation axis as the vector perpendicular between two vectors
+	 * @param {Vector3} vec1 The first vector
+	 * @param {Vector3} vec2 The second vector
+	 * @returns {Vector3} The normalized rotation axis
+	 */
+	calculateRotationAxis( vec1, vec2 ) {
 
-			this.dispatchEvent( _startEvent );
-			this.updateTbState( STATE.SCALE, true );
+		this._rotationMatrix.extractRotation( this._cameraMatrixState );
+		this._quat.setFromRotationMatrix( this._rotationMatrix );
 
-			this._startFingerDistance = this.calculatePointersDistance( this._touchCurrent[ 0 ], this._touchCurrent[ 1 ] );
-			this._currentFingerDistance = this._startFingerDistance;
+		this._rotationAxis.crossVectors( vec1, vec2 ).applyQuaternion( this._quat );
+		return this._rotationAxis.normalize().clone();
 
-			this.activateGizmos( false );
+	}
 
-		}
+	/**
+	 * Calculate the trackball radius so that gizmo's diamater will be 2/3 of the minimum side of the camera frustum
+	 * @param {Camera} camera
+	 * @returns {Number} The trackball radius
+	 */
+	calculateTbRadius( camera ) {
 
-	};
+		const distance = camera.position.distanceTo( this._gizmos.position );
 
-	onPinchMove = () => {
+		if ( camera.type == 'PerspectiveCamera' ) {
 
-		if ( this.enabled && this.enableZoom ) {
+			const halfFovV = MathUtils.DEG2RAD * camera.fov * 0.5; //vertical fov/2 in radians
+			const halfFovH = Math.atan( ( camera.aspect ) * Math.tan( halfFovV ) ); //horizontal fov/2 in radians
+			return Math.tan( Math.min( halfFovV, halfFovH ) ) * distance * this.radiusFactor;
 
-			this.setCenter( ( this._touchCurrent[ 0 ].clientX + this._touchCurrent[ 1 ].clientX ) / 2, ( this._touchCurrent[ 0 ].clientY + this._touchCurrent[ 1 ].clientY ) / 2 );
-			const minDistance = 12; //minimum distance between fingers (in css pixels)
+		} else if ( camera.type == 'OrthographicCamera' ) {
 
-			if ( this._state != STATE.SCALE ) {
+			return Math.min( camera.top, camera.right ) * this.radiusFactor;
 
-				this._startFingerDistance = this._currentFingerDistance;
-				this.updateTbState( STATE.SCALE, true );
+		}
 
-			}
+	}
 
-			this._currentFingerDistance = Math.max( this.calculatePointersDistance( this._touchCurrent[ 0 ], this._touchCurrent[ 1 ] ), minDistance * this._devPxRatio );
-			const amount = this._currentFingerDistance / this._startFingerDistance;
+	/**
+	 * Focus operation consist of positioning the point of interest in front of the camera and a slightly zoom in
+	 * @param {Vector3} point The point of interest
+	 * @param {Number} size Scale factor
+	 * @param {Number} amount Amount of operation to be completed (used for focus animations, default is complete full operation)
+	 */
+	focus( point, size, amount = 1 ) {
 
-			let scalePoint;
+		//move center of camera (along with gizmos) towards point of interest
+		_offset.copy( point ).sub( this._gizmos.position ).multiplyScalar( amount );
+		this._translationMatrix.makeTranslation( _offset.x, _offset.y, _offset.z );
 
-			if ( ! this.enablePan ) {
+		_gizmoMatrixStateTemp.copy( this._gizmoMatrixState );
+		this._gizmoMatrixState.premultiply( this._translationMatrix );
+		this._gizmoMatrixState.decompose( this._gizmos.position, this._gizmos.quaternion, this._gizmos.scale );
 
-				scalePoint = this._gizmos.position;
+		_cameraMatrixStateTemp.copy( this._cameraMatrixState );
+		this._cameraMatrixState.premultiply( this._translationMatrix );
+		this._cameraMatrixState.decompose( this.camera.position, this.camera.quaternion, this.camera.scale );
 
-			} else {
+		//apply zoom
+		if ( this.enableZoom ) {
 
-				if ( this.camera.isOrthographicCamera ) {
+			this.applyTransformMatrix( this.scale( size, this._gizmos.position ) );
 
-					scalePoint = this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement )
-						.applyQuaternion( this.camera.quaternion )
-						.multiplyScalar( 1 / this.camera.zoom )
-						.add( this._gizmos.position );
+		}
 
-				} else if ( this.camera.isPerspectiveCamera ) {
+		this._gizmoMatrixState.copy( _gizmoMatrixStateTemp );
+		this._cameraMatrixState.copy( _cameraMatrixStateTemp );
 
-					scalePoint = this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement )
-						.applyQuaternion( this.camera.quaternion )
-						.add( this._gizmos.position );
+	}
 
-				}
+	/**
+	 * Draw a grid and add it to the scene
+	 */
+	drawGrid() {
 
-			}
+		if ( this.scene != null ) {
 
-			this.applyTransformMatrix( this.scale( amount, scalePoint ) );
-			this.dispatchEvent( _changeEvent );
+			const color = 0x888888;
+			const multiplier = 3;
+			let size, divisions, maxLength, tick;
 
-		}
+			if ( this.camera.isOrthographicCamera ) {
 
-	};
+				const width = this.camera.right - this.camera.left;
+				const height = this.camera.bottom - this.camera.top;
 
-	onPinchEnd = () => {
+				maxLength = Math.max( width, height );
+				tick = maxLength / 20;
 
-		this.updateTbState( STATE.IDLE, false );
-		this.dispatchEvent( _endEvent );
+				size = maxLength / this.camera.zoom * multiplier;
+				divisions = size / tick * this.camera.zoom;
 
-	};
+			} else if ( this.camera.isPerspectiveCamera ) {
 
-	onTriplePanStart = () => {
+				const distance = this.camera.position.distanceTo( this._gizmos.position );
+				const halfFovV = MathUtils.DEG2RAD * this.camera.fov * 0.5;
+				const halfFovH = Math.atan( ( this.camera.aspect ) * Math.tan( halfFovV ) );
 
-		if ( this.enabled && this.enableZoom ) {
+				maxLength = Math.tan( Math.max( halfFovV, halfFovH ) ) * distance * 2;
+				tick = maxLength / 20;
 
-			this.dispatchEvent( _startEvent );
+				size = maxLength * multiplier;
+				divisions = size / tick;
 
-			this.updateTbState( STATE.SCALE, true );
+			}
 
-			//const center = event.center;
-			let clientX = 0;
-			let clientY = 0;
-			const nFingers = this._touchCurrent.length;
+			if ( this._grid == null ) {
 
-			for ( let i = 0; i < nFingers; i ++ ) {
+				this._grid = new GridHelper( size, divisions, color, color );
+				this._grid.position.copy( this._gizmos.position );
+				this._gridPosition.copy( this._grid.position );
+				this._grid.quaternion.copy( this.camera.quaternion );
+				this._grid.rotateX( Math.PI * 0.5 );
 
-				clientX += this._touchCurrent[ i ].clientX;
-				clientY += this._touchCurrent[ i ].clientY;
+				this.scene.add( this._grid );
 
 			}
 
-			this.setCenter( clientX / nFingers, clientY / nFingers );
-
-			this._startCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 );
-			this._currentCursorPosition.copy( this._startCursorPosition );
-
 		}
 
-	};
+	}
 
-	onTriplePanMove = () => {
+	/**
+	 * Remove all listeners, stop animations and clean scene
+	 */
+	dispose() {
 
-		if ( this.enabled && this.enableZoom ) {
+		if ( this._animationId != - 1 ) {
 
-			//	  fov / 2
-			//		|\
-			//		| \
-			//		|  \
-			//	x	|	\
-			//		| 	 \
-			//		| 	  \
-			//		| _ _ _\
-			//			y
+			window.cancelAnimationFrame( this._animationId );
 
-			//const center = event.center;
-			let clientX = 0;
-			let clientY = 0;
-			const nFingers = this._touchCurrent.length;
+		}
 
-			for ( let i = 0; i < nFingers; i ++ ) {
+		this.domElement.removeEventListener( 'pointerdown', this._onPointerDown );
+		this.domElement.removeEventListener( 'pointercancel', this._onPointerCancel );
+		this.domElement.removeEventListener( 'wheel', this._onWheel );
+		this.domElement.removeEventListener( 'contextmenu', this._onContextMenu );
 
-				clientX += this._touchCurrent[ i ].clientX;
-				clientY += this._touchCurrent[ i ].clientY;
+		window.removeEventListener( 'pointermove', this._onPointerMove );
+		window.removeEventListener( 'pointerup', this._onPointerUp );
 
-			}
+		window.removeEventListener( 'resize', this._onWindowResize );
 
-			this.setCenter( clientX / nFingers, clientY / nFingers );
+		if ( this.scene !== null ) this.scene.remove( this._gizmos );
+		this.disposeGrid();
 
-			const screenNotches = 8;	//how many wheel notches corresponds to a full screen pan
-			this._currentCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 );
+	}
 
-			const movement = this._currentCursorPosition.y - this._startCursorPosition.y;
+	/**
+	 * remove the grid from the scene
+	 */
+	disposeGrid() {
 
-			let size = 1;
+		if ( this._grid != null && this.scene != null ) {
 
-			if ( movement < 0 ) {
+			this.scene.remove( this._grid );
+			this._grid = null;
 
-				size = 1 / ( Math.pow( this.scaleFactor, - movement * screenNotches ) );
+		}
 
-			} else if ( movement > 0 ) {
+	}
 
-				size = Math.pow( this.scaleFactor, movement * screenNotches );
+	/**
+	 * Compute the easing out cubic function for ease out effect in animation
+	 * @param {Number} t The absolute progress of the animation in the bound of 0 (beginning of the) and 1 (ending of animation)
+	 * @returns {Number} Result of easing out cubic at time t
+	 */
+	easeOutCubic( t ) {
 
-			}
+		return 1 - Math.pow( 1 - t, 3 );
 
-			this._v3_1.setFromMatrixPosition( this._cameraMatrixState );
-			const x = this._v3_1.distanceTo( this._gizmos.position );
-			let xNew = x / size; //distance between camera and gizmos if scale(size, scalepoint) would be performed
+	}
 
-			//check min and max distance
-			xNew = MathUtils.clamp( xNew, this.minDistance, this.maxDistance );
+	/**
+	 * Make rotation gizmos more or less visible
+	 * @param {Boolean} isActive If true, make gizmos more visible
+	 */
+	activateGizmos( isActive ) {
 
-			const y = x * Math.tan( MathUtils.DEG2RAD * this._fovState * 0.5 );
+		const gizmoX = this._gizmos.children[ 0 ];
+		const gizmoY = this._gizmos.children[ 1 ];
+		const gizmoZ = this._gizmos.children[ 2 ];
 
-			//calculate new fov
-			let newFov = MathUtils.RAD2DEG * ( Math.atan( y / xNew ) * 2 );
+		if ( isActive ) {
 
-			//check min and max fov
-			newFov = MathUtils.clamp( newFov, this.minFov, this.maxFov );
+			gizmoX.material.setValues( { opacity: 1 } );
+			gizmoY.material.setValues( { opacity: 1 } );
+			gizmoZ.material.setValues( { opacity: 1 } );
 
-			const newDistance = y / Math.tan( MathUtils.DEG2RAD * ( newFov / 2 ) );
-			size = x / newDistance;
-			this._v3_2.setFromMatrixPosition( this._gizmoMatrixState );
+		} else {
 
-			this.setFov( newFov );
-			this.applyTransformMatrix( this.scale( size, this._v3_2, false ) );
+			gizmoX.material.setValues( { opacity: 0.6 } );
+			gizmoY.material.setValues( { opacity: 0.6 } );
+			gizmoZ.material.setValues( { opacity: 0.6 } );
 
-			//adjusting distance
-			_offset.copy( this._gizmos.position ).sub( this.camera.position ).normalize().multiplyScalar( newDistance / x );
-			this._m4_1.makeTranslation( _offset.x, _offset.y, _offset.z );
+		}
 
-			this.dispatchEvent( _changeEvent );
-
-		}
-
-	};
+	}
 
-	onTriplePanEnd = () => {
+	/**
+	 * Calculate the cursor position in NDC
+	 * @param {number} x Cursor horizontal coordinate within the canvas
+	 * @param {number} y Cursor vertical coordinate within the canvas
+	 * @param {HTMLElement} canvas The canvas where the renderer draws its output
+	 * @returns {Vector2} Cursor normalized position inside the canvas
+	 */
+	getCursorNDC( cursorX, cursorY, canvas ) {
 
-		this.updateTbState( STATE.IDLE, false );
-		this.dispatchEvent( _endEvent );
-		//this.dispatchEvent( _changeEvent );
+		const canvasRect = canvas.getBoundingClientRect();
+		this._v2_1.setX( ( ( cursorX - canvasRect.left ) / canvasRect.width ) * 2 - 1 );
+		this._v2_1.setY( ( ( canvasRect.bottom - cursorY ) / canvasRect.height ) * 2 - 1 );
+		return this._v2_1.clone();
 
-	};
+	}
 
 	/**
-	 * Set _center's x/y coordinates
-	 * @param {Number} clientX
-	 * @param {Number} clientY
+	 * Calculate the cursor position inside the canvas x/y coordinates with the origin being in the center of the canvas
+	 * @param {Number} x Cursor horizontal coordinate within the canvas
+	 * @param {Number} y Cursor vertical coordinate within the canvas
+	 * @param {HTMLElement} canvas The canvas where the renderer draws its output
+	 * @returns {Vector2} Cursor position inside the canvas
 	 */
-	setCenter = ( clientX, clientY ) => {
+	getCursorPosition( cursorX, cursorY, canvas ) {
 
-		_center.x = clientX;
-		_center.y = clientY;
+		this._v2_1.copy( this.getCursorNDC( cursorX, cursorY, canvas ) );
+		this._v2_1.x *= ( this.camera.right - this.camera.left ) * 0.5;
+		this._v2_1.y *= ( this.camera.top - this.camera.bottom ) * 0.5;
+		return this._v2_1.clone();
 
-	};
+	}
 
 	/**
-	 * Set default mouse actions
+	 * Set the camera to be controlled
+	 * @param {Camera} camera The virtual camera to be controlled
 	 */
-	initializeMouseActions = () => {
+	setCamera( camera ) {
 
-		this.setMouseAction( 'PAN', 0, 'CTRL' );
-		this.setMouseAction( 'PAN', 2 );
+		camera.lookAt( this.target );
+		camera.updateMatrix();
 
-		this.setMouseAction( 'ROTATE', 0 );
+		//setting state
+		if ( camera.type == 'PerspectiveCamera' ) {
 
-		this.setMouseAction( 'ZOOM', 'WHEEL' );
-		this.setMouseAction( 'ZOOM', 1 );
+			this._fov0 = camera.fov;
+			this._fovState = camera.fov;
 
-		this.setMouseAction( 'FOV', 'WHEEL', 'SHIFT' );
-		this.setMouseAction( 'FOV', 1, 'SHIFT' );
+		}
+
+		this._cameraMatrixState0.copy( camera.matrix );
+		this._cameraMatrixState.copy( this._cameraMatrixState0 );
+		this._cameraProjectionState.copy( camera.projectionMatrix );
+		this._zoom0 = camera.zoom;
+		this._zoomState = this._zoom0;
+
+		this._initialNear = camera.near;
+		this._nearPos0 = camera.position.distanceTo( this.target ) - camera.near;
+		this._nearPos = this._initialNear;
+
+		this._initialFar = camera.far;
+		this._farPos0 = camera.position.distanceTo( this.target ) - camera.far;
+		this._farPos = this._initialFar;
+
+		this._up0.copy( camera.up );
+		this._upState.copy( camera.up );
 
+		this.camera = camera;
+		this.camera.updateProjectionMatrix();
+
+		//making gizmos
+		this._tbRadius = this.calculateTbRadius( camera );
+		this.makeGizmos( this.target, this._tbRadius );
 
-	};
+	}
 
 	/**
-	 * Compare two mouse actions
-	 * @param {Object} action1
-	 * @param {Object} action2
-	 * @returns {Boolean} True if action1 and action 2 are the same mouse action, false otherwise
+	 * Set gizmos visibility
+	 * @param {Boolean} value Value of gizmos visibility
 	 */
-	compareMouseAction = ( action1, action2 ) => {
+	setGizmosVisible( value ) {
 
-		if ( action1.operation == action2.operation ) {
+		this._gizmos.visible = value;
+		this.dispatchEvent( _changeEvent );
 
-			if ( action1.mouse == action2.mouse && action1.key == action2.key ) {
+	}
 
-				return true;
+	/**
+	 * Set gizmos radius factor and redraws gizmos
+	 * @param {Float} value Value of radius factor
+	 */
+	setTbRadius( value ) {
 
-			} else {
+		this.radiusFactor = value;
+		this._tbRadius = this.calculateTbRadius( this.camera );
 
-				return false;
+		const curve = new EllipseCurve( 0, 0, this._tbRadius, this._tbRadius );
+		const points = curve.getPoints( this._curvePts );
+		const curveGeometry = new BufferGeometry().setFromPoints( points );
 
-			}
 
-		} else {
+		for ( const gizmo in this._gizmos.children ) {
 
-			return false;
+			this._gizmos.children[ gizmo ].geometry = curveGeometry;
 
 		}
 
-	};
+		this.dispatchEvent( _changeEvent );
+
+	}
 
 	/**
-	 * Set a new mouse action by specifying the operation to be performed and a mouse/key combination. In case of conflict, replaces the existing one
-	 * @param {String} operation The operation to be performed ('PAN', 'ROTATE', 'ZOOM', 'FOV)
-	 * @param {*} mouse A mouse button (0, 1, 2) or 'WHEEL' for wheel notches
-	 * @param {*} key The keyboard modifier ('CTRL', 'SHIFT') or null if key is not needed
-	 * @returns {Boolean} True if the mouse action has been successfully added, false otherwise
+	 * Creates the rotation gizmos matching trackball center and radius
+	 * @param {Vector3} tbCenter The trackball center
+	 * @param {number} tbRadius The trackball radius
 	 */
-	setMouseAction = ( operation, mouse, key = null ) => {
-
-		const operationInput = [ 'PAN', 'ROTATE', 'ZOOM', 'FOV' ];
-		const mouseInput = [ 0, 1, 2, 'WHEEL' ];
-		const keyInput = [ 'CTRL', 'SHIFT', null ];
-		let state;
+	makeGizmos( tbCenter, tbRadius ) {
 
-		if ( ! operationInput.includes( operation ) || ! mouseInput.includes( mouse ) || ! keyInput.includes( key ) ) {
+		const curve = new EllipseCurve( 0, 0, tbRadius, tbRadius );
+		const points = curve.getPoints( this._curvePts );
 
-			//invalid parameters
-			return false;
+		//geometry
+		const curveGeometry = new BufferGeometry().setFromPoints( points );
 
-		}
+		//material
+		const curveMaterialX = new LineBasicMaterial( { color: 0xff8080, fog: false, transparent: true, opacity: 0.6 } );
+		const curveMaterialY = new LineBasicMaterial( { color: 0x80ff80, fog: false, transparent: true, opacity: 0.6 } );
+		const curveMaterialZ = new LineBasicMaterial( { color: 0x8080ff, fog: false, transparent: true, opacity: 0.6 } );
 
-		if ( mouse == 'WHEEL' ) {
+		//line
+		const gizmoX = new Line( curveGeometry, curveMaterialX );
+		const gizmoY = new Line( curveGeometry, curveMaterialY );
+		const gizmoZ = new Line( curveGeometry, curveMaterialZ );
 
-			if ( operation != 'ZOOM' && operation != 'FOV' ) {
+		const rotation = Math.PI * 0.5;
+		gizmoX.rotation.x = rotation;
+		gizmoY.rotation.y = rotation;
 
-				//cannot associate 2D operation to 1D input
-				return false;
 
-			}
+		//setting state
+		this._gizmoMatrixState0.identity().setPosition( tbCenter );
+		this._gizmoMatrixState.copy( this._gizmoMatrixState0 );
 
-		}
+		if ( this.camera.zoom !== 1 ) {
 
-		switch ( operation ) {
+			//adapt gizmos size to camera zoom
+			const size = 1 / this.camera.zoom;
+			this._scaleMatrix.makeScale( size, size, size );
+			this._translationMatrix.makeTranslation( - tbCenter.x, - tbCenter.y, - tbCenter.z );
 
-			case 'PAN':
+			this._gizmoMatrixState.premultiply( this._translationMatrix ).premultiply( this._scaleMatrix );
+			this._translationMatrix.makeTranslation( tbCenter.x, tbCenter.y, tbCenter.z );
+			this._gizmoMatrixState.premultiply( this._translationMatrix );
 
-				state = STATE.PAN;
-				break;
+		}
 
-			case 'ROTATE':
+		this._gizmoMatrixState.decompose( this._gizmos.position, this._gizmos.quaternion, this._gizmos.scale );
 
-				state = STATE.ROTATE;
-				break;
+		//
 
-			case 'ZOOM':
+		this._gizmos.traverse( function ( object ) {
 
-				state = STATE.SCALE;
-				break;
+			if ( object.isLine ) {
 
-			case 'FOV':
+				object.geometry.dispose();
+				object.material.dispose();
 
-				state = STATE.FOV;
-				break;
+			}
 
-		}
+		} );
 
-		const action = {
+		this._gizmos.clear();
 
-			operation: operation,
-			mouse: mouse,
-			key: key,
-			state: state
+		//
 
-		};
+		this._gizmos.add( gizmoX );
+		this._gizmos.add( gizmoY );
+		this._gizmos.add( gizmoZ );
 
-		for ( let i = 0; i < this.mouseActions.length; i ++ ) {
+	}
 
-			if ( this.mouseActions[ i ].mouse == action.mouse && this.mouseActions[ i ].key == action.key ) {
+	/**
+	 * Perform animation for focus operation
+	 * @param {Number} time Instant in which this function is called as performance.now()
+	 * @param {Vector3} point Point of interest for focus operation
+	 * @param {Matrix4} cameraMatrix Camera matrix
+	 * @param {Matrix4} gizmoMatrix Gizmos matrix
+	 */
+	onFocusAnim( time, point, cameraMatrix, gizmoMatrix ) {
 
-				this.mouseActions.splice( i, 1, action );
-				return true;
+		if ( this._timeStart == - 1 ) {
 
-			}
+			//animation start
+			this._timeStart = time;
 
 		}
 
-		this.mouseActions.push( action );
-		return true;
+		if ( this._state == STATE.ANIMATION_FOCUS ) {
 
-	};
+			const deltaTime = time - this._timeStart;
+			const animTime = deltaTime / this.focusAnimationTime;
 
-	/**
-	 * Remove a mouse action by specifying its mouse/key combination
-	 * @param {*} mouse A mouse button (0, 1, 2) or 'WHEEL' for wheel notches
-	 * @param {*} key The keyboard modifier ('CTRL', 'SHIFT') or null if key is not needed
-	 * @returns {Boolean} True if the operation has been succesfully removed, false otherwise
-	 */
-	unsetMouseAction = ( mouse, key = null ) => {
+			this._gizmoMatrixState.copy( gizmoMatrix );
 
-		for ( let i = 0; i < this.mouseActions.length; i ++ ) {
+			if ( animTime >= 1 ) {
 
-			if ( this.mouseActions[ i ].mouse == mouse && this.mouseActions[ i ].key == key ) {
+				//animation end
 
-				this.mouseActions.splice( i, 1 );
-				return true;
+				this._gizmoMatrixState.decompose( this._gizmos.position, this._gizmos.quaternion, this._gizmos.scale );
 
-			}
+				this.focus( point, this.scaleFactor );
 
-		}
+				this._timeStart = - 1;
+				this.updateTbState( STATE.IDLE, false );
+				this.activateGizmos( false );
 
-		return false;
+				this.dispatchEvent( _changeEvent );
 
-	};
+			} else {
 
-	/**
-	 * Return the operation associated to a mouse/keyboard combination
-	 * @param {*} mouse A mouse button (0, 1, 2) or 'WHEEL' for wheel notches
-	 * @param {*} key The keyboard modifier ('CTRL', 'SHIFT') or null if key is not needed
-	 * @returns The operation if it has been found, null otherwise
-	 */
-	getOpFromAction = ( mouse, key ) => {
+				const amount = this.easeOutCubic( animTime );
+				const size = ( ( 1 - amount ) + ( this.scaleFactor * amount ) );
 
-		let action;
+				this._gizmoMatrixState.decompose( this._gizmos.position, this._gizmos.quaternion, this._gizmos.scale );
+				this.focus( point, size, amount );
 
-		for ( let i = 0; i < this.mouseActions.length; i ++ ) {
+				this.dispatchEvent( _changeEvent );
+				const self = this;
+				this._animationId = window.requestAnimationFrame( function ( t ) {
 
-			action = this.mouseActions[ i ];
-			if ( action.mouse == mouse && action.key == key ) {
+					self.onFocusAnim( t, point, cameraMatrix, gizmoMatrix.clone() );
 
-				return action.operation;
+				} );
 
 			}
 
-		}
+		} else {
 
-		if ( key != null ) {
+			//interrupt animation
 
-			for ( let i = 0; i < this.mouseActions.length; i ++ ) {
+			this._animationId = - 1;
+			this._timeStart = - 1;
 
-				action = this.mouseActions[ i ];
-				if ( action.mouse == mouse && action.key == null ) {
+		}
 
-					return action.operation;
+	}
 
-				}
+	/**
+	 * Perform animation for rotation operation
+	 * @param {Number} time Instant in which this function is called as performance.now()
+	 * @param {Vector3} rotationAxis Rotation axis
+	 * @param {number} w0 Initial angular velocity
+	 */
+	onRotationAnim( time, rotationAxis, w0 ) {
 
-			}
+		if ( this._timeStart == - 1 ) {
+
+			//animation start
+			this._anglePrev = 0;
+			this._angleCurrent = 0;
+			this._timeStart = time;
 
 		}
 
-		return null;
+		if ( this._state == STATE.ANIMATION_ROTATE ) {
 
-	};
+			//w = w0 + alpha * t
+			const deltaTime = ( time - this._timeStart ) / 1000;
+			const w = w0 + ( ( - this.dampingFactor ) * deltaTime );
 
-	/**
-	 * Get the operation associated to mouse and key combination and returns the corresponding FSA state
-	 * @param {Number} mouse Mouse button
-	 * @param {String} key Keyboard modifier
-	 * @returns The FSA state obtained from the operation associated to mouse/keyboard combination
-	 */
-	getOpStateFromAction = ( mouse, key ) => {
+			if ( w > 0 ) {
 
-		let action;
+				//tetha = 0.5 * alpha * t^2 + w0 * t + tetha0
+				this._angleCurrent = 0.5 * ( - this.dampingFactor ) * Math.pow( deltaTime, 2 ) + w0 * deltaTime + 0;
+				this.applyTransformMatrix( this.rotate( rotationAxis, this._angleCurrent ) );
+				this.dispatchEvent( _changeEvent );
+				const self = this;
+				this._animationId = window.requestAnimationFrame( function ( t ) {
 
-		for ( let i = 0; i < this.mouseActions.length; i ++ ) {
+					self.onRotationAnim( t, rotationAxis, w0 );
 
-			action = this.mouseActions[ i ];
-			if ( action.mouse == mouse && action.key == key ) {
+				} );
 
-				return action.state;
+			} else {
 
-			}
+				this._animationId = - 1;
+				this._timeStart = - 1;
 
-		}
+				this.updateTbState( STATE.IDLE, false );
+				this.activateGizmos( false );
 
-		if ( key != null ) {
+				this.dispatchEvent( _changeEvent );
 
-			for ( let i = 0; i < this.mouseActions.length; i ++ ) {
+			}
 
-				action = this.mouseActions[ i ];
-				if ( action.mouse == mouse && action.key == null ) {
+		} else {
 
-					return action.state;
+			//interrupt animation
 
-				}
+			this._animationId = - 1;
+			this._timeStart = - 1;
+
+			if ( this._state != STATE.ROTATE ) {
+
+				this.activateGizmos( false );
+				this.dispatchEvent( _changeEvent );
 
 			}
 
 		}
 
-		return null;
+	}
 
-	};
 
 	/**
-	 * Calculate the angle between two pointers
-	 * @param {PointerEvent} p1
-	 * @param {PointerEvent} p2
-	 * @returns {Number} The angle between two pointers in degrees
+	 * Perform pan operation moving camera between two points
+	 * @param {Vector3} p0 Initial point
+	 * @param {Vector3} p1 Ending point
+	 * @param {Boolean} adjust If movement should be adjusted considering camera distance (Perspective only)
 	 */
-	getAngle = ( p1, p2 ) => {
+	pan( p0, p1, adjust = false ) {
 
-		return Math.atan2( p2.clientY - p1.clientY, p2.clientX - p1.clientX ) * 180 / Math.PI;
+		const movement = p0.clone().sub( p1 );
 
-	};
+		if ( this.camera.isOrthographicCamera ) {
 
-	/**
-	 * Update a PointerEvent inside current pointerevents array
-	 * @param {PointerEvent} event
-	 */
-	updateTouchEvent = ( event ) => {
+			//adjust movement amount
+			movement.multiplyScalar( 1 / this.camera.zoom );
 
-		for ( let i = 0; i < this._touchCurrent.length; i ++ ) {
+		} else if ( this.camera.isPerspectiveCamera && adjust ) {
 
-			if ( this._touchCurrent[ i ].pointerId == event.pointerId ) {
+			//adjust movement amount
+			this._v3_1.setFromMatrixPosition( this._cameraMatrixState0 );	//camera's initial position
+			this._v3_2.setFromMatrixPosition( this._gizmoMatrixState0 );	//gizmo's initial position
+			const distanceFactor = this._v3_1.distanceTo( this._v3_2 ) / this.camera.position.distanceTo( this._gizmos.position );
+			movement.multiplyScalar( 1 / distanceFactor );
 
-				this._touchCurrent.splice( i, 1, event );
-				break;
+		}
 
-			}
+		this._v3_1.set( movement.x, movement.y, 0 ).applyQuaternion( this.camera.quaternion );
 
-		}
+		this._m4_1.makeTranslation( this._v3_1.x, this._v3_1.y, this._v3_1.z );
 
-	};
+		this.setTransformationMatrices( this._m4_1, this._m4_1 );
+		return _transformation;
+
+	}
 
 	/**
-	 * Apply a transformation matrix, to the camera and gizmos
-	 * @param {Object} transformation Object containing matrices to apply to camera and gizmos
+	 * Reset trackball
 	 */
-	applyTransformMatrix( transformation ) {
+	reset() {
 
-		if ( transformation.camera != null ) {
+		this.camera.zoom = this._zoom0;
 
-			this._m4_1.copy( this._cameraMatrixState ).premultiply( transformation.camera );
-			this._m4_1.decompose( this.camera.position, this.camera.quaternion, this.camera.scale );
-			this.camera.updateMatrix();
+		if ( this.camera.isPerspectiveCamera ) {
 
-			//update camera up vector
-			if ( this._state == STATE.ROTATE || this._state == STATE.ZROTATE || this._state == STATE.ANIMATION_ROTATE ) {
+			this.camera.fov = this._fov0;
 
-				this.camera.up.copy( this._upState ).applyQuaternion( this.camera.quaternion );
+		}
 
-			}
+		this.camera.near = this._nearPos;
+		this.camera.far = this._farPos;
+		this._cameraMatrixState.copy( this._cameraMatrixState0 );
+		this._cameraMatrixState.decompose( this.camera.position, this.camera.quaternion, this.camera.scale );
+		this.camera.up.copy( this._up0 );
 
-		}
+		this.camera.updateMatrix();
+		this.camera.updateProjectionMatrix();
 
-		if ( transformation.gizmos != null ) {
+		this._gizmoMatrixState.copy( this._gizmoMatrixState0 );
+		this._gizmoMatrixState0.decompose( this._gizmos.position, this._gizmos.quaternion, this._gizmos.scale );
+		this._gizmos.updateMatrix();
 
-			this._m4_1.copy( this._gizmoMatrixState ).premultiply( transformation.gizmos );
-			this._m4_1.decompose( this._gizmos.position, this._gizmos.quaternion, this._gizmos.scale );
-			this._gizmos.updateMatrix();
+		this._tbRadius = this.calculateTbRadius( this.camera );
+		this.makeGizmos( this._gizmos.position, this._tbRadius );
 
-		}
+		this.camera.lookAt( this._gizmos.position );
 
-		if ( this._state == STATE.SCALE || this._state == STATE.FOCUS || this._state == STATE.ANIMATION_FOCUS ) {
+		this.updateTbState( STATE.IDLE, false );
 
-			this._tbRadius = this.calculateTbRadius( this.camera );
+		this.dispatchEvent( _changeEvent );
 
-			if ( this.adjustNearFar ) {
+	}
 
-				const cameraDistance = this.camera.position.distanceTo( this._gizmos.position );
+	/**
+	 * Rotate the camera around an axis passing by trackball's center
+	 * @param {Vector3} axis Rotation axis
+	 * @param {number} angle Angle in radians
+	 * @returns {Object} Object with 'camera' field containing transformation matrix resulting from the operation to be applied to the camera
+	 */
+	rotate( axis, angle ) {
 
-				const bb = new Box3();
-				bb.setFromObject( this._gizmos );
-				const sphere = new Sphere();
-				bb.getBoundingSphere( sphere );
+		const point = this._gizmos.position; //rotation center
+		this._translationMatrix.makeTranslation( - point.x, - point.y, - point.z );
+		this._rotationMatrix.makeRotationAxis( axis, - angle );
 
-				const adjustedNearPosition = Math.max( this._nearPos0, sphere.radius + sphere.center.length() );
-				const regularNearPosition = cameraDistance - this._initialNear;
+		//rotate camera
+		this._m4_1.makeTranslation( point.x, point.y, point.z );
+		this._m4_1.multiply( this._rotationMatrix );
+		this._m4_1.multiply( this._translationMatrix );
 
-				const minNearPos = Math.min( adjustedNearPosition, regularNearPosition );
-				this.camera.near = cameraDistance - minNearPos;
+		this.setTransformationMatrices( this._m4_1 );
 
+		return _transformation;
 
-				const adjustedFarPosition = Math.min( this._farPos0, - sphere.radius + sphere.center.length() );
-				const regularFarPosition = cameraDistance - this._initialFar;
+	}
 
-				const minFarPos = Math.min( adjustedFarPosition, regularFarPosition );
-				this.camera.far = cameraDistance - minFarPos;
+	copyState() {
 
-				this.camera.updateProjectionMatrix();
+		let state;
+		if ( this.camera.isOrthographicCamera ) {
 
-			} else {
+			state = JSON.stringify( { arcballState: {
 
-				let update = false;
+				cameraFar: this.camera.far,
+				cameraMatrix: this.camera.matrix,
+				cameraNear: this.camera.near,
+				cameraUp: this.camera.up,
+				cameraZoom: this.camera.zoom,
+				gizmoMatrix: this._gizmos.matrix
 
-				if ( this.camera.near != this._initialNear ) {
+			} } );
 
-					this.camera.near = this._initialNear;
-					update = true;
+		} else if ( this.camera.isPerspectiveCamera ) {
 
-				}
+			state = JSON.stringify( { arcballState: {
+				cameraFar: this.camera.far,
+				cameraFov: this.camera.fov,
+				cameraMatrix: this.camera.matrix,
+				cameraNear: this.camera.near,
+				cameraUp: this.camera.up,
+				cameraZoom: this.camera.zoom,
+				gizmoMatrix: this._gizmos.matrix
 
-				if ( this.camera.far != this._initialFar ) {
+			} } );
 
-					this.camera.far = this._initialFar;
-					update = true;
+		}
 
-				}
+		navigator.clipboard.writeText( state );
 
-				if ( update ) {
+	}
 
-					this.camera.updateProjectionMatrix();
+	pasteState() {
 
-				}
+		const self = this;
+		navigator.clipboard.readText().then( function resolved( value ) {
 
-			}
+			self.setStateFromJSON( value );
 
-		}
+		} );
 
 	}
 
 	/**
-	 * Calculate the angular speed
-	 * @param {Number} p0 Position at t0
-	 * @param {Number} p1 Position at t1
-	 * @param {Number} t0 Initial time in milliseconds
-	 * @param {Number} t1 Ending time in milliseconds
+	 * Save the current state of the control. This can later be recover with .reset
 	 */
-	calculateAngularSpeed = ( p0, p1, t0, t1 ) => {
+	saveState() {
 
-		const s = p1 - p0;
-		const t = ( t1 - t0 ) / 1000;
-		if ( t == 0 ) {
+		this._cameraMatrixState0.copy( this.camera.matrix );
+		this._gizmoMatrixState0.copy( this._gizmos.matrix );
+		this._nearPos = this.camera.near;
+		this._farPos = this.camera.far;
+		this._zoom0 = this.camera.zoom;
+		this._up0.copy( this.camera.up );
 
-			return 0;
+		if ( this.camera.isPerspectiveCamera ) {
 
-		}
+			this._fov0 = this.camera.fov;
 
-		return s / t;
+		}
 
-	};
+	}
 
 	/**
-	 * Calculate the distance between two pointers
-	 * @param {PointerEvent} p0 The first pointer
-	 * @param {PointerEvent} p1 The second pointer
-	 * @returns {number} The distance between the two pointers
+	 * Perform uniform scale operation around a given point
+	 * @param {Number} size Scale factor
+	 * @param {Vector3} point Point around which scale
+	 * @param {Boolean} scaleGizmos If gizmos should be scaled (Perspective only)
+	 * @returns {Object} Object with 'camera' and 'gizmo' fields containing transformation matrices resulting from the operation to be applied to the camera and gizmos
 	 */
-	calculatePointersDistance = ( p0, p1 ) => {
+	scale( size, point, scaleGizmos = true ) {
 
-		return Math.sqrt( Math.pow( p1.clientX - p0.clientX, 2 ) + Math.pow( p1.clientY - p0.clientY, 2 ) );
+		_scalePointTemp.copy( point );
+		let sizeInverse = 1 / size;
 
-	};
+		if ( this.camera.isOrthographicCamera ) {
 
-	/**
-	 * Calculate the rotation axis as the vector perpendicular between two vectors
-	 * @param {Vector3} vec1 The first vector
-	 * @param {Vector3} vec2 The second vector
-	 * @returns {Vector3} The normalized rotation axis
-	 */
-	calculateRotationAxis = ( vec1, vec2 ) => {
+			//camera zoom
+			this.camera.zoom = this._zoomState;
+			this.camera.zoom *= size;
 
-		this._rotationMatrix.extractRotation( this._cameraMatrixState );
-		this._quat.setFromRotationMatrix( this._rotationMatrix );
+			//check min and max zoom
+			if ( this.camera.zoom > this.maxZoom ) {
 
-		this._rotationAxis.crossVectors( vec1, vec2 ).applyQuaternion( this._quat );
-		return this._rotationAxis.normalize().clone();
+				this.camera.zoom = this.maxZoom;
+				sizeInverse = this._zoomState / this.maxZoom;
 
-	};
+			} else if ( this.camera.zoom < this.minZoom ) {
 
-	/**
-	 * Calculate the trackball radius so that gizmo's diamater will be 2/3 of the minimum side of the camera frustum
-	 * @param {Camera} camera
-	 * @returns {Number} The trackball radius
-	 */
-	calculateTbRadius = ( camera ) => {
+				this.camera.zoom = this.minZoom;
+				sizeInverse = this._zoomState / this.minZoom;
 
-		const distance = camera.position.distanceTo( this._gizmos.position );
+			}
 
-		if ( camera.type == 'PerspectiveCamera' ) {
+			this.camera.updateProjectionMatrix();
 
-			const halfFovV = MathUtils.DEG2RAD * camera.fov * 0.5; //vertical fov/2 in radians
-			const halfFovH = Math.atan( ( camera.aspect ) * Math.tan( halfFovV ) ); //horizontal fov/2 in radians
-			return Math.tan( Math.min( halfFovV, halfFovH ) ) * distance * this.radiusFactor;
+			this._v3_1.setFromMatrixPosition( this._gizmoMatrixState );	//gizmos position
 
-		} else if ( camera.type == 'OrthographicCamera' ) {
+			//scale gizmos so they appear in the same spot having the same dimension
+			this._scaleMatrix.makeScale( sizeInverse, sizeInverse, sizeInverse );
+			this._translationMatrix.makeTranslation( - this._v3_1.x, - this._v3_1.y, - this._v3_1.z );
 
-			return Math.min( camera.top, camera.right ) * this.radiusFactor;
+			this._m4_2.makeTranslation( this._v3_1.x, this._v3_1.y, this._v3_1.z ).multiply( this._scaleMatrix );
+			this._m4_2.multiply( this._translationMatrix );
 
-		}
 
-	};
+			//move camera and gizmos to obtain pinch effect
+			_scalePointTemp.sub( this._v3_1 );
 
-	/**
-	 * Focus operation consist of positioning the point of interest in front of the camera and a slightly zoom in
-	 * @param {Vector3} point The point of interest
-	 * @param {Number} size Scale factor
-	 * @param {Number} amount Amount of operation to be completed (used for focus animations, default is complete full operation)
-	 */
-	focus = ( point, size, amount = 1 ) => {
+			const amount = _scalePointTemp.clone().multiplyScalar( sizeInverse );
+			_scalePointTemp.sub( amount );
 
-		//move center of camera (along with gizmos) towards point of interest
-		_offset.copy( point ).sub( this._gizmos.position ).multiplyScalar( amount );
-		this._translationMatrix.makeTranslation( _offset.x, _offset.y, _offset.z );
+			this._m4_1.makeTranslation( _scalePointTemp.x, _scalePointTemp.y, _scalePointTemp.z );
+			this._m4_2.premultiply( this._m4_1 );
 
-		_gizmoMatrixStateTemp.copy( this._gizmoMatrixState );
-		this._gizmoMatrixState.premultiply( this._translationMatrix );
-		this._gizmoMatrixState.decompose( this._gizmos.position, this._gizmos.quaternion, this._gizmos.scale );
+			this.setTransformationMatrices( this._m4_1, this._m4_2 );
+			return _transformation;
 
-		_cameraMatrixStateTemp.copy( this._cameraMatrixState );
-		this._cameraMatrixState.premultiply( this._translationMatrix );
-		this._cameraMatrixState.decompose( this.camera.position, this.camera.quaternion, this.camera.scale );
+		} else if ( this.camera.isPerspectiveCamera ) {
 
-		//apply zoom
-		if ( this.enableZoom ) {
+			this._v3_1.setFromMatrixPosition( this._cameraMatrixState );
+			this._v3_2.setFromMatrixPosition( this._gizmoMatrixState );
 
-			this.applyTransformMatrix( this.scale( size, this._gizmos.position ) );
+			//move camera
+			let distance = this._v3_1.distanceTo( _scalePointTemp );
+			let amount = distance - ( distance * sizeInverse );
 
-		}
+			//check min and max distance
+			const newDistance = distance - amount;
+			if ( newDistance < this.minDistance ) {
 
-		this._gizmoMatrixState.copy( _gizmoMatrixStateTemp );
-		this._cameraMatrixState.copy( _cameraMatrixStateTemp );
+				sizeInverse = this.minDistance / distance;
+				amount = distance - ( distance * sizeInverse );
 
-	};
+			} else if ( newDistance > this.maxDistance ) {
 
-	/**
-	 * Draw a grid and add it to the scene
-	 */
-	drawGrid = () => {
+				sizeInverse = this.maxDistance / distance;
+				amount = distance - ( distance * sizeInverse );
 
-		if ( this.scene != null ) {
+			}
 
-			const color = 0x888888;
-			const multiplier = 3;
-			let size, divisions, maxLength, tick;
+			_offset.copy( _scalePointTemp ).sub( this._v3_1 ).normalize().multiplyScalar( amount );
 
-			if ( this.camera.isOrthographicCamera ) {
+			this._m4_1.makeTranslation( _offset.x, _offset.y, _offset.z );
 
-				const width = this.camera.right - this.camera.left;
-				const height = this.camera.bottom - this.camera.top;
 
-				maxLength = Math.max( width, height );
-				tick = maxLength / 20;
+			if ( scaleGizmos ) {
 
-				size = maxLength / this.camera.zoom * multiplier;
-				divisions = size / tick * this.camera.zoom;
+				//scale gizmos so they appear in the same spot having the same dimension
+				const pos = this._v3_2;
 
-			} else if ( this.camera.isPerspectiveCamera ) {
+				distance = pos.distanceTo( _scalePointTemp );
+				amount = distance - ( distance * sizeInverse );
+				_offset.copy( _scalePointTemp ).sub( this._v3_2 ).normalize().multiplyScalar( amount );
 
-				const distance = this.camera.position.distanceTo( this._gizmos.position );
-				const halfFovV = MathUtils.DEG2RAD * this.camera.fov * 0.5;
-				const halfFovH = Math.atan( ( this.camera.aspect ) * Math.tan( halfFovV ) );
+				this._translationMatrix.makeTranslation( pos.x, pos.y, pos.z );
+				this._scaleMatrix.makeScale( sizeInverse, sizeInverse, sizeInverse );
 
-				maxLength = Math.tan( Math.max( halfFovV, halfFovH ) ) * distance * 2;
-				tick = maxLength / 20;
+				this._m4_2.makeTranslation( _offset.x, _offset.y, _offset.z ).multiply( this._translationMatrix );
+				this._m4_2.multiply( this._scaleMatrix );
 
-				size = maxLength * multiplier;
-				divisions = size / tick;
+				this._translationMatrix.makeTranslation( - pos.x, - pos.y, - pos.z );
 
-			}
+				this._m4_2.multiply( this._translationMatrix );
+				this.setTransformationMatrices( this._m4_1, this._m4_2 );
 
-			if ( this._grid == null ) {
 
-				this._grid = new GridHelper( size, divisions, color, color );
-				this._grid.position.copy( this._gizmos.position );
-				this._gridPosition.copy( this._grid.position );
-				this._grid.quaternion.copy( this.camera.quaternion );
-				this._grid.rotateX( Math.PI * 0.5 );
+			} else {
 
-				this.scene.add( this._grid );
+				this.setTransformationMatrices( this._m4_1 );
 
 			}
 
+			return _transformation;
+
 		}
 
-	};
+	}
 
 	/**
-	 * Remove all listeners, stop animations and clean scene
+	 * Set camera fov
+	 * @param {Number} value fov to be setted
 	 */
-	dispose = () => {
+	setFov( value ) {
 
-		if ( this._animationId != - 1 ) {
+		if ( this.camera.isPerspectiveCamera ) {
 
-			window.cancelAnimationFrame( this._animationId );
+			this.camera.fov = MathUtils.clamp( value, this.minFov, this.maxFov );
+			this.camera.updateProjectionMatrix();
 
 		}
 
-		this.domElement.removeEventListener( 'pointerdown', this.onPointerDown );
-		this.domElement.removeEventListener( 'pointercancel', this.onPointerCancel );
-		this.domElement.removeEventListener( 'wheel', this.onWheel );
-		this.domElement.removeEventListener( 'contextmenu', this.onContextMenu );
+	}
 
-		window.removeEventListener( 'pointermove', this.onPointerMove );
-		window.removeEventListener( 'pointerup', this.onPointerUp );
+	/**
+	 * Set values in transformation object
+	 * @param {Matrix4} camera Transformation to be applied to the camera
+	 * @param {Matrix4} gizmos Transformation to be applied to gizmos
+	 */
+	 setTransformationMatrices( camera = null, gizmos = null ) {
 
-		window.removeEventListener( 'resize', this.onWindowResize );
+		if ( camera != null ) {
 
-		if ( this.scene !== null ) this.scene.remove( this._gizmos );
-		this.disposeGrid();
+			if ( _transformation.camera != null ) {
 
-	};
+				_transformation.camera.copy( camera );
 
-	/**
-	 * remove the grid from the scene
-	 */
-	disposeGrid = () => {
+			} else {
 
-		if ( this._grid != null && this.scene != null ) {
+				_transformation.camera = camera.clone();
 
-			this.scene.remove( this._grid );
-			this._grid = null;
+			}
 
-		}
+		} else {
 
-	};
+			_transformation.camera = null;
 
-	/**
-	 * Compute the easing out cubic function for ease out effect in animation
-	 * @param {Number} t The absolute progress of the animation in the bound of 0 (beginning of the) and 1 (ending of animation)
-	 * @returns {Number} Result of easing out cubic at time t
-	 */
-	easeOutCubic = ( t ) => {
+		}
 
-		return 1 - Math.pow( 1 - t, 3 );
+		if ( gizmos != null ) {
 
-	};
+			if ( _transformation.gizmos != null ) {
 
-	/**
-	 * Make rotation gizmos more or less visible
-	 * @param {Boolean} isActive If true, make gizmos more visible
-	 */
-	activateGizmos = ( isActive ) => {
+				_transformation.gizmos.copy( gizmos );
 
-		const gizmoX = this._gizmos.children[ 0 ];
-		const gizmoY = this._gizmos.children[ 1 ];
-		const gizmoZ = this._gizmos.children[ 2 ];
+			} else {
 
-		if ( isActive ) {
+				_transformation.gizmos = gizmos.clone();
 
-			gizmoX.material.setValues( { opacity: 1 } );
-			gizmoY.material.setValues( { opacity: 1 } );
-			gizmoZ.material.setValues( { opacity: 1 } );
+			}
 
 		} else {
 
-			gizmoX.material.setValues( { opacity: 0.6 } );
-			gizmoY.material.setValues( { opacity: 0.6 } );
-			gizmoZ.material.setValues( { opacity: 0.6 } );
+			_transformation.gizmos = null;
 
 		}
 
-	};
+	}
 
 	/**
-	 * Calculate the cursor position in NDC
-	 * @param {number} x Cursor horizontal coordinate within the canvas
-	 * @param {number} y Cursor vertical coordinate within the canvas
-	 * @param {HTMLElement} canvas The canvas where the renderer draws its output
-	 * @returns {Vector2} Cursor normalized position inside the canvas
+	 * Rotate camera around its direction axis passing by a given point by a given angle
+	 * @param {Vector3} point The point where the rotation axis is passing trough
+	 * @param {Number} angle Angle in radians
+	 * @returns The computed transormation matix
 	 */
-	getCursorNDC = ( cursorX, cursorY, canvas ) => {
+	zRotate( point, angle ) {
 
-		const canvasRect = canvas.getBoundingClientRect();
-		this._v2_1.setX( ( ( cursorX - canvasRect.left ) / canvasRect.width ) * 2 - 1 );
-		this._v2_1.setY( ( ( canvasRect.bottom - cursorY ) / canvasRect.height ) * 2 - 1 );
-		return this._v2_1.clone();
+		this._rotationMatrix.makeRotationAxis( this._rotationAxis, angle );
+		this._translationMatrix.makeTranslation( - point.x, - point.y, - point.z );
 
-	};
+		this._m4_1.makeTranslation( point.x, point.y, point.z );
+		this._m4_1.multiply( this._rotationMatrix );
+		this._m4_1.multiply( this._translationMatrix );
 
-	/**
-	 * Calculate the cursor position inside the canvas x/y coordinates with the origin being in the center of the canvas
-	 * @param {Number} x Cursor horizontal coordinate within the canvas
-	 * @param {Number} y Cursor vertical coordinate within the canvas
-	 * @param {HTMLElement} canvas The canvas where the renderer draws its output
-	 * @returns {Vector2} Cursor position inside the canvas
-	 */
-	getCursorPosition = ( cursorX, cursorY, canvas ) => {
+		this._v3_1.setFromMatrixPosition( this._gizmoMatrixState ).sub( point );	//vector from rotation center to gizmos position
+		this._v3_2.copy( this._v3_1 ).applyAxisAngle( this._rotationAxis, angle );	//apply rotation
+		this._v3_2.sub( this._v3_1 );
 
-		this._v2_1.copy( this.getCursorNDC( cursorX, cursorY, canvas ) );
-		this._v2_1.x *= ( this.camera.right - this.camera.left ) * 0.5;
-		this._v2_1.y *= ( this.camera.top - this.camera.bottom ) * 0.5;
-		return this._v2_1.clone();
+		this._m4_2.makeTranslation( this._v3_2.x, this._v3_2.y, this._v3_2.z );
 
-	};
+		this.setTransformationMatrices( this._m4_1, this._m4_2 );
+		return _transformation;
 
-	/**
-	 * Set the camera to be controlled
-	 * @param {Camera} camera The virtual camera to be controlled
-	 */
-	setCamera = ( camera ) => {
+	}
 
-		camera.lookAt( this.target );
-		camera.updateMatrix();
 
-		//setting state
-		if ( camera.type == 'PerspectiveCamera' ) {
+	getRaycaster() {
 
-			this._fov0 = camera.fov;
-			this._fovState = camera.fov;
+		return _raycaster;
 
-		}
+	}
 
-		this._cameraMatrixState0.copy( camera.matrix );
-		this._cameraMatrixState.copy( this._cameraMatrixState0 );
-		this._cameraProjectionState.copy( camera.projectionMatrix );
-		this._zoom0 = camera.zoom;
-		this._zoomState = this._zoom0;
 
-		this._initialNear = camera.near;
-		this._nearPos0 = camera.position.distanceTo( this.target ) - camera.near;
-		this._nearPos = this._initialNear;
+	/**
+	 * Unproject the cursor on the 3D object surface
+	 * @param {Vector2} cursor Cursor coordinates in NDC
+	 * @param {Camera} camera Virtual camera
+	 * @returns {Vector3} The point of intersection with the model, if exist, null otherwise
+	 */
+	unprojectOnObj( cursor, camera ) {
 
-		this._initialFar = camera.far;
-		this._farPos0 = camera.position.distanceTo( this.target ) - camera.far;
-		this._farPos = this._initialFar;
+		const raycaster = this.getRaycaster();
+		raycaster.near = camera.near;
+		raycaster.far = camera.far;
+		raycaster.setFromCamera( cursor, camera );
 
-		this._up0.copy( camera.up );
-		this._upState.copy( camera.up );
+		const intersect = raycaster.intersectObjects( this.scene.children, true );
 
-		this.camera = camera;
-		this.camera.updateProjectionMatrix();
+		for ( let i = 0; i < intersect.length; i ++ ) {
 
-		//making gizmos
-		this._tbRadius = this.calculateTbRadius( camera );
-		this.makeGizmos( this.target, this._tbRadius );
+			if ( intersect[ i ].object.uuid != this._gizmos.uuid && intersect[ i ].face != null ) {
 
-	};
+				return intersect[ i ].point.clone();
 
-	/**
-	 * Set gizmos visibility
-	 * @param {Boolean} value Value of gizmos visibility
-	 */
-	setGizmosVisible( value ) {
+			}
 
-		this._gizmos.visible = value;
-		this.dispatchEvent( _changeEvent );
+		}
+
+		return null;
 
 	}
 
 	/**
-	 * Set gizmos radius factor and redraws gizmos
-	 * @param {Float} value Value of radius factor
+	 * Unproject the cursor on the trackball surface
+	 * @param {Camera} camera The virtual camera
+	 * @param {Number} cursorX Cursor horizontal coordinate on screen
+	 * @param {Number} cursorY Cursor vertical coordinate on screen
+	 * @param {HTMLElement} canvas The canvas where the renderer draws its output
+	 * @param {number} tbRadius The trackball radius
+	 * @returns {Vector3} The unprojected point on the trackball surface
 	 */
-	setTbRadius( value ) {
-
-		this.radiusFactor = value;
-		this._tbRadius = this.calculateTbRadius( this.camera );
-
-		const curve = new EllipseCurve( 0, 0, this._tbRadius, this._tbRadius );
-		const points = curve.getPoints( this._curvePts );
-		const curveGeometry = new BufferGeometry().setFromPoints( points );
+	unprojectOnTbSurface( camera, cursorX, cursorY, canvas, tbRadius ) {
 
+		if ( camera.type == 'OrthographicCamera' ) {
 
-		for ( const gizmo in this._gizmos.children ) {
+			this._v2_1.copy( this.getCursorPosition( cursorX, cursorY, canvas ) );
+			this._v3_1.set( this._v2_1.x, this._v2_1.y, 0 );
 
-			this._gizmos.children[ gizmo ].geometry = curveGeometry;
+			const x2 = Math.pow( this._v2_1.x, 2 );
+			const y2 = Math.pow( this._v2_1.y, 2 );
+			const r2 = Math.pow( this._tbRadius, 2 );
 
-		}
+			if ( x2 + y2 <= r2 * 0.5 ) {
 
-		this.dispatchEvent( _changeEvent );
+				//intersection with sphere
+				this._v3_1.setZ( Math.sqrt( r2 - ( x2 + y2 ) ) );
 
-	}
+			} else {
 
-	/**
-	 * Creates the rotation gizmos matching trackball center and radius
-	 * @param {Vector3} tbCenter The trackball center
-	 * @param {number} tbRadius The trackball radius
-	 */
-	makeGizmos = ( tbCenter, tbRadius ) => {
+				//intersection with hyperboloid
+				this._v3_1.setZ( ( r2 * 0.5 ) / ( Math.sqrt( x2 + y2 ) ) );
 
-		const curve = new EllipseCurve( 0, 0, tbRadius, tbRadius );
-		const points = curve.getPoints( this._curvePts );
+			}
 
-		//geometry
-		const curveGeometry = new BufferGeometry().setFromPoints( points );
+			return this._v3_1;
 
-		//material
-		const curveMaterialX = new LineBasicMaterial( { color: 0xff8080, fog: false, transparent: true, opacity: 0.6 } );
-		const curveMaterialY = new LineBasicMaterial( { color: 0x80ff80, fog: false, transparent: true, opacity: 0.6 } );
-		const curveMaterialZ = new LineBasicMaterial( { color: 0x8080ff, fog: false, transparent: true, opacity: 0.6 } );
+		} else if ( camera.type == 'PerspectiveCamera' ) {
 
-		//line
-		const gizmoX = new Line( curveGeometry, curveMaterialX );
-		const gizmoY = new Line( curveGeometry, curveMaterialY );
-		const gizmoZ = new Line( curveGeometry, curveMaterialZ );
+			//unproject cursor on the near plane
+			this._v2_1.copy( this.getCursorNDC( cursorX, cursorY, canvas ) );
 
-		const rotation = Math.PI * 0.5;
-		gizmoX.rotation.x = rotation;
-		gizmoY.rotation.y = rotation;
+			this._v3_1.set( this._v2_1.x, this._v2_1.y, - 1 );
+			this._v3_1.applyMatrix4( camera.projectionMatrixInverse );
 
+			const rayDir = this._v3_1.clone().normalize(); //unprojected ray direction
+			const cameraGizmoDistance = camera.position.distanceTo( this._gizmos.position );
+			const radius2 = Math.pow( tbRadius, 2 );
 
-		//setting state
-		this._gizmoMatrixState0.identity().setPosition( tbCenter );
-		this._gizmoMatrixState.copy( this._gizmoMatrixState0 );
+			//	  camera
+			//		|\
+			//		| \
+			//		|  \
+			//	h	|	\
+			//		| 	 \
+			//		| 	  \
+			//	_ _ | _ _ _\ _ _  near plane
+			//			l
 
-		if ( this.camera.zoom !== 1 ) {
+			const h = this._v3_1.z;
+			const l = Math.sqrt( Math.pow( this._v3_1.x, 2 ) + Math.pow( this._v3_1.y, 2 ) );
 
-			//adapt gizmos size to camera zoom
-			const size = 1 / this.camera.zoom;
-			this._scaleMatrix.makeScale( size, size, size );
-			this._translationMatrix.makeTranslation( - tbCenter.x, - tbCenter.y, - tbCenter.z );
+			if ( l == 0 ) {
 
-			this._gizmoMatrixState.premultiply( this._translationMatrix ).premultiply( this._scaleMatrix );
-			this._translationMatrix.makeTranslation( tbCenter.x, tbCenter.y, tbCenter.z );
-			this._gizmoMatrixState.premultiply( this._translationMatrix );
+				//ray aligned with camera
+				rayDir.set( this._v3_1.x, this._v3_1.y, tbRadius );
+				return rayDir;
 
-		}
+			}
 
-		this._gizmoMatrixState.decompose( this._gizmos.position, this._gizmos.quaternion, this._gizmos.scale );
+			const m = h / l;
+			const q = cameraGizmoDistance;
 
-		//
+			/*
+			 * calculate intersection point between unprojected ray and trackball surface
+			 *|y = m * x + q
+			 *|x^2 + y^2 = r^2
+			 *
+			 * (m^2 + 1) * x^2 + (2 * m * q) * x + q^2 - r^2 = 0
+			 */
+			let a = Math.pow( m, 2 ) + 1;
+			let b = 2 * m * q;
+			let c = Math.pow( q, 2 ) - radius2;
+			let delta = Math.pow( b, 2 ) - ( 4 * a * c );
 
-		this._gizmos.traverse( function ( object ) {
+			if ( delta >= 0 ) {
 
-			if ( object.isLine ) {
+				//intersection with sphere
+				this._v2_1.setX( ( - b - Math.sqrt( delta ) ) / ( 2 * a ) );
+				this._v2_1.setY( m * this._v2_1.x + q );
 
-				object.geometry.dispose();
-				object.material.dispose();
+				const angle = MathUtils.RAD2DEG * this._v2_1.angle();
 
-			}
+				if ( angle >= 45 ) {
 
-		} );
+					//if angle between intersection point and X' axis is >= 45°, return that point
+					//otherwise, calculate intersection point with hyperboloid
 
-		this._gizmos.clear();
+					const rayLength = Math.sqrt( Math.pow( this._v2_1.x, 2 ) + Math.pow( ( cameraGizmoDistance - this._v2_1.y ), 2 ) );
+					rayDir.multiplyScalar( rayLength );
+					rayDir.z += cameraGizmoDistance;
+					return rayDir;
 
-		//
+				}
 
-		this._gizmos.add( gizmoX );
-		this._gizmos.add( gizmoY );
-		this._gizmos.add( gizmoZ );
+			}
 
-	};
+			//intersection with hyperboloid
+			/*
+			 *|y = m * x + q
+			 *|y = (1 / x) * (r^2 / 2)
+			 *
+			 * m * x^2 + q * x - r^2 / 2 = 0
+			 */
 
-	/**
-	 * Perform animation for focus operation
-	 * @param {Number} time Instant in which this function is called as performance.now()
-	 * @param {Vector3} point Point of interest for focus operation
-	 * @param {Matrix4} cameraMatrix Camera matrix
-	 * @param {Matrix4} gizmoMatrix Gizmos matrix
-	 */
-	onFocusAnim = ( time, point, cameraMatrix, gizmoMatrix ) => {
+			a = m;
+			b = q;
+			c = - radius2 * 0.5;
+			delta = Math.pow( b, 2 ) - ( 4 * a * c );
+			this._v2_1.setX( ( - b - Math.sqrt( delta ) ) / ( 2 * a ) );
+			this._v2_1.setY( m * this._v2_1.x + q );
 
-		if ( this._timeStart == - 1 ) {
+			const rayLength = Math.sqrt( Math.pow( this._v2_1.x, 2 ) + Math.pow( ( cameraGizmoDistance - this._v2_1.y ), 2 ) );
 
-			//animation start
-			this._timeStart = time;
+			rayDir.multiplyScalar( rayLength );
+			rayDir.z += cameraGizmoDistance;
+			return rayDir;
 
 		}
 
-		if ( this._state == STATE.ANIMATION_FOCUS ) {
+	}
 
-			const deltaTime = time - this._timeStart;
-			const animTime = deltaTime / this.focusAnimationTime;
 
-			this._gizmoMatrixState.copy( gizmoMatrix );
+	/**
+	 * Unproject the cursor on the plane passing through the center of the trackball orthogonal to the camera
+	 * @param {Camera} camera The virtual camera
+	 * @param {Number} cursorX Cursor horizontal coordinate on screen
+	 * @param {Number} cursorY Cursor vertical coordinate on screen
+	 * @param {HTMLElement} canvas The canvas where the renderer draws its output
+	 * @param {Boolean} initialDistance If initial distance between camera and gizmos should be used for calculations instead of current (Perspective only)
+	 * @returns {Vector3} The unprojected point on the trackball plane
+	 */
+	unprojectOnTbPlane( camera, cursorX, cursorY, canvas, initialDistance = false ) {
 
-			if ( animTime >= 1 ) {
+		if ( camera.type == 'OrthographicCamera' ) {
 
-				//animation end
+			this._v2_1.copy( this.getCursorPosition( cursorX, cursorY, canvas ) );
+			this._v3_1.set( this._v2_1.x, this._v2_1.y, 0 );
 
-				this._gizmoMatrixState.decompose( this._gizmos.position, this._gizmos.quaternion, this._gizmos.scale );
+			return this._v3_1.clone();
 
-				this.focus( point, this.scaleFactor );
+		} else if ( camera.type == 'PerspectiveCamera' ) {
 
-				this._timeStart = - 1;
-				this.updateTbState( STATE.IDLE, false );
-				this.activateGizmos( false );
+			this._v2_1.copy( this.getCursorNDC( cursorX, cursorY, canvas ) );
 
-				this.dispatchEvent( _changeEvent );
+			//unproject cursor on the near plane
+			this._v3_1.set( this._v2_1.x, this._v2_1.y, - 1 );
+			this._v3_1.applyMatrix4( camera.projectionMatrixInverse );
 
-			} else {
+			const rayDir = this._v3_1.clone().normalize(); //unprojected ray direction
 
-				const amount = this.easeOutCubic( animTime );
-				const size = ( ( 1 - amount ) + ( this.scaleFactor * amount ) );
+			//	  camera
+			//		|\
+			//		| \
+			//		|  \
+			//	h	|	\
+			//		| 	 \
+			//		| 	  \
+			//	_ _ | _ _ _\ _ _  near plane
+			//			l
 
-				this._gizmoMatrixState.decompose( this._gizmos.position, this._gizmos.quaternion, this._gizmos.scale );
-				this.focus( point, size, amount );
+			const h = this._v3_1.z;
+			const l = Math.sqrt( Math.pow( this._v3_1.x, 2 ) + Math.pow( this._v3_1.y, 2 ) );
+			let cameraGizmoDistance;
 
-				this.dispatchEvent( _changeEvent );
-				const self = this;
-				this._animationId = window.requestAnimationFrame( function ( t ) {
+			if ( initialDistance ) {
 
-					self.onFocusAnim( t, point, cameraMatrix, gizmoMatrix.clone() );
+				cameraGizmoDistance = this._v3_1.setFromMatrixPosition( this._cameraMatrixState0 ).distanceTo( this._v3_2.setFromMatrixPosition( this._gizmoMatrixState0 ) );
 
-				} );
+			} else {
+
+				cameraGizmoDistance = camera.position.distanceTo( this._gizmos.position );
 
 			}
 
-		} else {
+			/*
+			 * calculate intersection point between unprojected ray and the plane
+			 *|y = mx + q
+			 *|y = 0
+			 *
+			 * x = -q/m
+			*/
+			if ( l == 0 ) {
 
-			//interrupt animation
+				//ray aligned with camera
+				rayDir.set( 0, 0, 0 );
+				return rayDir;
 
-			this._animationId = - 1;
-			this._timeStart = - 1;
+			}
+
+			const m = h / l;
+			const q = cameraGizmoDistance;
+			const x = - q / m;
+
+			const rayLength = Math.sqrt( Math.pow( q, 2 ) + Math.pow( x, 2 ) );
+			rayDir.multiplyScalar( rayLength );
+			rayDir.z = 0;
+			return rayDir;
 
 		}
 
-	};
+	}
 
 	/**
-	 * Perform animation for rotation operation
-	 * @param {Number} time Instant in which this function is called as performance.now()
-	 * @param {Vector3} rotationAxis Rotation axis
-	 * @param {number} w0 Initial angular velocity
+	 * Update camera and gizmos state
 	 */
-	onRotationAnim = ( time, rotationAxis, w0 ) => {
+	updateMatrixState() {
 
-		if ( this._timeStart == - 1 ) {
+		//update camera and gizmos state
+		this._cameraMatrixState.copy( this.camera.matrix );
+		this._gizmoMatrixState.copy( this._gizmos.matrix );
 
-			//animation start
-			this._anglePrev = 0;
-			this._angleCurrent = 0;
-			this._timeStart = time;
+		if ( this.camera.isOrthographicCamera ) {
 
-		}
+			this._cameraProjectionState.copy( this.camera.projectionMatrix );
+			this.camera.updateProjectionMatrix();
+			this._zoomState = this.camera.zoom;
 
-		if ( this._state == STATE.ANIMATION_ROTATE ) {
+		} else if ( this.camera.isPerspectiveCamera ) {
 
-			//w = w0 + alpha * t
-			const deltaTime = ( time - this._timeStart ) / 1000;
-			const w = w0 + ( ( - this.dampingFactor ) * deltaTime );
+			this._fovState = this.camera.fov;
 
-			if ( w > 0 ) {
+		}
 
-				//tetha = 0.5 * alpha * t^2 + w0 * t + tetha0
-				this._angleCurrent = 0.5 * ( - this.dampingFactor ) * Math.pow( deltaTime, 2 ) + w0 * deltaTime + 0;
-				this.applyTransformMatrix( this.rotate( rotationAxis, this._angleCurrent ) );
-				this.dispatchEvent( _changeEvent );
-				const self = this;
-				this._animationId = window.requestAnimationFrame( function ( t ) {
+	}
 
-					self.onRotationAnim( t, rotationAxis, w0 );
+	/**
+	 * Update the trackball FSA
+	 * @param {STATE} newState New state of the FSA
+	 * @param {Boolean} updateMatrices If matriices state should be updated
+	 */
+	updateTbState( newState, updateMatrices ) {
 
-				} );
+		this._state = newState;
+		if ( updateMatrices ) {
 
-			} else {
+			this.updateMatrixState();
 
-				this._animationId = - 1;
-				this._timeStart = - 1;
+		}
 
-				this.updateTbState( STATE.IDLE, false );
-				this.activateGizmos( false );
+	}
 
-				this.dispatchEvent( _changeEvent );
+	update() {
 
-			}
+		const EPS = 0.000001;
 
-		} else {
+		if ( this.target.equals( this._currentTarget ) === false ) {
 
-			//interrupt animation
+			this._gizmos.position.copy( this.target );	//for correct radius calculation
+			this._tbRadius = this.calculateTbRadius( this.camera );
+			this.makeGizmos( this.target, this._tbRadius );
+			this._currentTarget.copy( this.target );
 
-			this._animationId = - 1;
-			this._timeStart = - 1;
+		}
 
-			if ( this._state != STATE.ROTATE ) {
+		//check min/max parameters
+		if ( this.camera.isOrthographicCamera ) {
 
-				this.activateGizmos( false );
-				this.dispatchEvent( _changeEvent );
+			//check zoom
+			if ( this.camera.zoom > this.maxZoom || this.camera.zoom < this.minZoom ) {
+
+				const newZoom = MathUtils.clamp( this.camera.zoom, this.minZoom, this.maxZoom );
+				this.applyTransformMatrix( this.scale( newZoom / this.camera.zoom, this._gizmos.position, true ) );
 
 			}
 
-		}
+		} else if ( this.camera.isPerspectiveCamera ) {
 
-	};
+			//check distance
+			const distance = this.camera.position.distanceTo( this._gizmos.position );
 
+			if ( distance > this.maxDistance + EPS || distance < this.minDistance - EPS ) {
 
-	/**
-	 * Perform pan operation moving camera between two points
-	 * @param {Vector3} p0 Initial point
-	 * @param {Vector3} p1 Ending point
-	 * @param {Boolean} adjust If movement should be adjusted considering camera distance (Perspective only)
-	 */
-	pan = ( p0, p1, adjust = false ) => {
+				const newDistance = MathUtils.clamp( distance, this.minDistance, this.maxDistance );
+				this.applyTransformMatrix( this.scale( newDistance / distance, this._gizmos.position ) );
+				this.updateMatrixState();
 
-		const movement = p0.clone().sub( p1 );
+			 }
 
-		if ( this.camera.isOrthographicCamera ) {
+			//check fov
+			if ( this.camera.fov < this.minFov || this.camera.fov > this.maxFov ) {
 
-			//adjust movement amount
-			movement.multiplyScalar( 1 / this.camera.zoom );
+				this.camera.fov = MathUtils.clamp( this.camera.fov, this.minFov, this.maxFov );
+				this.camera.updateProjectionMatrix();
 
-		} else if ( this.camera.isPerspectiveCamera && adjust ) {
+			}
 
-			//adjust movement amount
-			this._v3_1.setFromMatrixPosition( this._cameraMatrixState0 );	//camera's initial position
-			this._v3_2.setFromMatrixPosition( this._gizmoMatrixState0 );	//gizmo's initial position
-			const distanceFactor = this._v3_1.distanceTo( this._v3_2 ) / this.camera.position.distanceTo( this._gizmos.position );
-			movement.multiplyScalar( 1 / distanceFactor );
+			const oldRadius = this._tbRadius;
+			this._tbRadius = this.calculateTbRadius( this.camera );
 
-		}
+			if ( oldRadius < this._tbRadius - EPS || oldRadius > this._tbRadius + EPS ) {
 
-		this._v3_1.set( movement.x, movement.y, 0 ).applyQuaternion( this.camera.quaternion );
+				const scale = ( this._gizmos.scale.x + this._gizmos.scale.y + this._gizmos.scale.z ) / 3;
+				const newRadius = this._tbRadius / scale;
+				const curve = new EllipseCurve( 0, 0, newRadius, newRadius );
+				const points = curve.getPoints( this._curvePts );
+				const curveGeometry = new BufferGeometry().setFromPoints( points );
 
-		this._m4_1.makeTranslation( this._v3_1.x, this._v3_1.y, this._v3_1.z );
+				for ( const gizmo in this._gizmos.children ) {
 
-		this.setTransformationMatrices( this._m4_1, this._m4_1 );
-		return _transformation;
+					this._gizmos.children[ gizmo ].geometry = curveGeometry;
 
-	};
+				}
 
-	/**
-	 * Reset trackball
-	 */
-	reset = () => {
+			}
 
-		this.camera.zoom = this._zoom0;
+		}
 
-		if ( this.camera.isPerspectiveCamera ) {
+		this.camera.lookAt( this._gizmos.position );
 
-			this.camera.fov = this._fov0;
+	}
 
-		}
+	setStateFromJSON( json ) {
 
-		this.camera.near = this._nearPos;
-		this.camera.far = this._farPos;
-		this._cameraMatrixState.copy( this._cameraMatrixState0 );
-		this._cameraMatrixState.decompose( this.camera.position, this.camera.quaternion, this.camera.scale );
-		this.camera.up.copy( this._up0 );
+		const state = JSON.parse( json );
 
-		this.camera.updateMatrix();
-		this.camera.updateProjectionMatrix();
+		if ( state.arcballState != undefined ) {
 
-		this._gizmoMatrixState.copy( this._gizmoMatrixState0 );
-		this._gizmoMatrixState0.decompose( this._gizmos.position, this._gizmos.quaternion, this._gizmos.scale );
-		this._gizmos.updateMatrix();
+			this._cameraMatrixState.fromArray( state.arcballState.cameraMatrix.elements );
+			this._cameraMatrixState.decompose( this.camera.position, this.camera.quaternion, this.camera.scale );
 
-		this._tbRadius = this.calculateTbRadius( this.camera );
-		this.makeGizmos( this._gizmos.position, this._tbRadius );
+			this.camera.up.copy( state.arcballState.cameraUp );
+			this.camera.near = state.arcballState.cameraNear;
+			this.camera.far = state.arcballState.cameraFar;
 
-		this.camera.lookAt( this._gizmos.position );
+			this.camera.zoom = state.arcballState.cameraZoom;
 
-		this.updateTbState( STATE.IDLE, false );
+			if ( this.camera.isPerspectiveCamera ) {
 
-		this.dispatchEvent( _changeEvent );
+				this.camera.fov = state.arcballState.cameraFov;
 
-	};
+			}
 
-	/**
-	 * Rotate the camera around an axis passing by trackball's center
-	 * @param {Vector3} axis Rotation axis
-	 * @param {number} angle Angle in radians
-	 * @returns {Object} Object with 'camera' field containing transformation matrix resulting from the operation to be applied to the camera
-	 */
-	rotate = ( axis, angle ) => {
+			this._gizmoMatrixState.fromArray( state.arcballState.gizmoMatrix.elements );
+			this._gizmoMatrixState.decompose( this._gizmos.position, this._gizmos.quaternion, this._gizmos.scale );
 
-		const point = this._gizmos.position; //rotation center
-		this._translationMatrix.makeTranslation( - point.x, - point.y, - point.z );
-		this._rotationMatrix.makeRotationAxis( axis, - angle );
+			this.camera.updateMatrix();
+			this.camera.updateProjectionMatrix();
 
-		//rotate camera
-		this._m4_1.makeTranslation( point.x, point.y, point.z );
-		this._m4_1.multiply( this._rotationMatrix );
-		this._m4_1.multiply( this._translationMatrix );
+			this._gizmos.updateMatrix();
 
-		this.setTransformationMatrices( this._m4_1 );
+			this._tbRadius = this.calculateTbRadius( this.camera );
+			const gizmoTmp = new Matrix4().copy( this._gizmoMatrixState0 );
+			this.makeGizmos( this._gizmos.position, this._tbRadius );
+			this._gizmoMatrixState0.copy( gizmoTmp );
 
-		return _transformation;
+			this.camera.lookAt( this._gizmos.position );
+			this.updateTbState( STATE.IDLE, false );
 
-	};
+			this.dispatchEvent( _changeEvent );
 
-	copyState = () => {
+		}
 
-		let state;
-		if ( this.camera.isOrthographicCamera ) {
+	}
 
-			state = JSON.stringify( { arcballState: {
+}
 
-				cameraFar: this.camera.far,
-				cameraMatrix: this.camera.matrix,
-				cameraNear: this.camera.near,
-				cameraUp: this.camera.up,
-				cameraZoom: this.camera.zoom,
-				gizmoMatrix: this._gizmos.matrix
+//listeners
 
-			} } );
+function onWindowResize() {
 
-		} else if ( this.camera.isPerspectiveCamera ) {
+	const scale = ( this._gizmos.scale.x + this._gizmos.scale.y + this._gizmos.scale.z ) / 3;
+	this._tbRadius = this.calculateTbRadius( this.camera );
 
-			state = JSON.stringify( { arcballState: {
-				cameraFar: this.camera.far,
-				cameraFov: this.camera.fov,
-				cameraMatrix: this.camera.matrix,
-				cameraNear: this.camera.near,
-				cameraUp: this.camera.up,
-				cameraZoom: this.camera.zoom,
-				gizmoMatrix: this._gizmos.matrix
+	const newRadius = this._tbRadius / scale;
+	const curve = new EllipseCurve( 0, 0, newRadius, newRadius );
+	const points = curve.getPoints( this._curvePts );
+	const curveGeometry = new BufferGeometry().setFromPoints( points );
 
-			} } );
 
-		}
+	for ( const gizmo in this._gizmos.children ) {
 
-		navigator.clipboard.writeText( state );
+		this._gizmos.children[ gizmo ].geometry = curveGeometry;
 
-	};
+	}
 
-	pasteState = () => {
+	this.dispatchEvent( _changeEvent );
 
-		const self = this;
-		navigator.clipboard.readText().then( function resolved( value ) {
+}
 
-			self.setStateFromJSON( value );
+function onContextMenu( event ) {
 
-		} );
+	if ( ! this.enabled ) {
 
-	};
+		return;
 
-	/**
-	 * Save the current state of the control. This can later be recover with .reset
-	 */
-	saveState = () => {
+	}
 
-		this._cameraMatrixState0.copy( this.camera.matrix );
-		this._gizmoMatrixState0.copy( this._gizmos.matrix );
-		this._nearPos = this.camera.near;
-		this._farPos = this.camera.far;
-		this._zoom0 = this.camera.zoom;
-		this._up0.copy( this.camera.up );
+	for ( let i = 0; i < this.mouseActions.length; i ++ ) {
 
-		if ( this.camera.isPerspectiveCamera ) {
+		if ( this.mouseActions[ i ].mouse == 2 ) {
 
-			this._fov0 = this.camera.fov;
+			//prevent only if button 2 is actually used
+			event.preventDefault();
+			break;
 
 		}
 
-	};
+	}
 
-	/**
-	 * Perform uniform scale operation around a given point
-	 * @param {Number} size Scale factor
-	 * @param {Vector3} point Point around which scale
-	 * @param {Boolean} scaleGizmos If gizmos should be scaled (Perspective only)
-	 * @returns {Object} Object with 'camera' and 'gizmo' fields containing transformation matrices resulting from the operation to be applied to the camera and gizmos
-	 */
-	scale = ( size, point, scaleGizmos = true ) => {
+}
 
-		_scalePointTemp.copy( point );
-		let sizeInverse = 1 / size;
+function onPointerCancel() {
 
-		if ( this.camera.isOrthographicCamera ) {
+	this._touchStart.splice( 0, this._touchStart.length );
+	this._touchCurrent.splice( 0, this._touchCurrent.length );
+	this._input = INPUT.NONE;
 
-			//camera zoom
-			this.camera.zoom = this._zoomState;
-			this.camera.zoom *= size;
+}
 
-			//check min and max zoom
-			if ( this.camera.zoom > this.maxZoom ) {
+function onPointerDown( event ) {
 
-				this.camera.zoom = this.maxZoom;
-				sizeInverse = this._zoomState / this.maxZoom;
+	if ( event.button == 0 && event.isPrimary ) {
 
-			} else if ( this.camera.zoom < this.minZoom ) {
+		this._downValid = true;
+		this._downEvents.push( event );
+		this._downStart = performance.now();
 
-				this.camera.zoom = this.minZoom;
-				sizeInverse = this._zoomState / this.minZoom;
+	} else {
 
-			}
+		this._downValid = false;
 
-			this.camera.updateProjectionMatrix();
+	}
 
-			this._v3_1.setFromMatrixPosition( this._gizmoMatrixState );	//gizmos position
+	if ( event.pointerType == 'touch' && this._input != INPUT.CURSOR ) {
 
-			//scale gizmos so they appear in the same spot having the same dimension
-			this._scaleMatrix.makeScale( sizeInverse, sizeInverse, sizeInverse );
-			this._translationMatrix.makeTranslation( - this._v3_1.x, - this._v3_1.y, - this._v3_1.z );
+		this._touchStart.push( event );
+		this._touchCurrent.push( event );
 
-			this._m4_2.makeTranslation( this._v3_1.x, this._v3_1.y, this._v3_1.z ).multiply( this._scaleMatrix );
-			this._m4_2.multiply( this._translationMatrix );
+		switch ( this._input ) {
 
+			case INPUT.NONE:
 
-			//move camera and gizmos to obtain pinch effect
-			_scalePointTemp.sub( this._v3_1 );
+				//singleStart
+				this._input = INPUT.ONE_FINGER;
+				this.onSinglePanStart( event, 'ROTATE' );
 
-			const amount = _scalePointTemp.clone().multiplyScalar( sizeInverse );
-			_scalePointTemp.sub( amount );
+				window.addEventListener( 'pointermove', this._onPointerMove );
+				window.addEventListener( 'pointerup', this._onPointerUp );
 
-			this._m4_1.makeTranslation( _scalePointTemp.x, _scalePointTemp.y, _scalePointTemp.z );
-			this._m4_2.premultiply( this._m4_1 );
+				break;
 
-			this.setTransformationMatrices( this._m4_1, this._m4_2 );
-			return _transformation;
+			case INPUT.ONE_FINGER:
+			case INPUT.ONE_FINGER_SWITCHED:
 
-		} else if ( this.camera.isPerspectiveCamera ) {
+				//doubleStart
+				this._input = INPUT.TWO_FINGER;
 
-			this._v3_1.setFromMatrixPosition( this._cameraMatrixState );
-			this._v3_2.setFromMatrixPosition( this._gizmoMatrixState );
+				this.onRotateStart();
+				this.onPinchStart();
+				this.onDoublePanStart();
 
-			//move camera
-			let distance = this._v3_1.distanceTo( _scalePointTemp );
-			let amount = distance - ( distance * sizeInverse );
+				break;
 
-			//check min and max distance
-			const newDistance = distance - amount;
-			if ( newDistance < this.minDistance ) {
+			case INPUT.TWO_FINGER:
 
-				sizeInverse = this.minDistance / distance;
-				amount = distance - ( distance * sizeInverse );
+				//multipleStart
+				this._input = INPUT.MULT_FINGER;
+				this.onTriplePanStart( event );
+				break;
 
-			} else if ( newDistance > this.maxDistance ) {
+		}
 
-				sizeInverse = this.maxDistance / distance;
-				amount = distance - ( distance * sizeInverse );
+	} else if ( event.pointerType != 'touch' && this._input == INPUT.NONE ) {
 
-			}
+		let modifier = null;
 
-			_offset.copy( _scalePointTemp ).sub( this._v3_1 ).normalize().multiplyScalar( amount );
+		if ( event.ctrlKey || event.metaKey ) {
 
-			this._m4_1.makeTranslation( _offset.x, _offset.y, _offset.z );
+			modifier = 'CTRL';
 
+		} else if ( event.shiftKey ) {
 
-			if ( scaleGizmos ) {
+			modifier = 'SHIFT';
 
-				//scale gizmos so they appear in the same spot having the same dimension
-				const pos = this._v3_2;
+		}
 
-				distance = pos.distanceTo( _scalePointTemp );
-				amount = distance - ( distance * sizeInverse );
-				_offset.copy( _scalePointTemp ).sub( this._v3_2 ).normalize().multiplyScalar( amount );
+		this._mouseOp = this.getOpFromAction( event.button, modifier );
+		if ( this._mouseOp != null ) {
 
-				this._translationMatrix.makeTranslation( pos.x, pos.y, pos.z );
-				this._scaleMatrix.makeScale( sizeInverse, sizeInverse, sizeInverse );
+			window.addEventListener( 'pointermove', this._onPointerMove );
+			window.addEventListener( 'pointerup', this._onPointerUp );
 
-				this._m4_2.makeTranslation( _offset.x, _offset.y, _offset.z ).multiply( this._translationMatrix );
-				this._m4_2.multiply( this._scaleMatrix );
+			//singleStart
+			this._input = INPUT.CURSOR;
+			this._button = event.button;
+			this.onSinglePanStart( event, this._mouseOp );
 
-				this._translationMatrix.makeTranslation( - pos.x, - pos.y, - pos.z );
+		}
 
-				this._m4_2.multiply( this._translationMatrix );
-				this.setTransformationMatrices( this._m4_1, this._m4_2 );
+	}
 
+}
 
-			} else {
+function onPointerMove( event ) {
 
-				this.setTransformationMatrices( this._m4_1 );
+	if ( event.pointerType == 'touch' && this._input != INPUT.CURSOR ) {
 
-			}
+		switch ( this._input ) {
 
-			return _transformation;
+			case INPUT.ONE_FINGER:
 
-		}
+				//singleMove
+				this.updateTouchEvent( event );
 
-	};
+				this.onSinglePanMove( event, STATE.ROTATE );
+				break;
 
-	/**
-	 * Set camera fov
-	 * @param {Number} value fov to be setted
-	 */
-	setFov = ( value ) => {
+			case INPUT.ONE_FINGER_SWITCHED:
 
-		if ( this.camera.isPerspectiveCamera ) {
+				const movement = this.calculatePointersDistance( this._touchCurrent[ 0 ], event ) * this._devPxRatio;
 
-			this.camera.fov = MathUtils.clamp( value, this.minFov, this.maxFov );
-			this.camera.updateProjectionMatrix();
+				if ( movement >= this._switchSensibility ) {
 
-		}
+					//singleMove
+					this._input = INPUT.ONE_FINGER;
+					this.updateTouchEvent( event );
 
-	};
+					this.onSinglePanStart( event, 'ROTATE' );
+					break;
 
-	/**
-	 * Set values in transformation object
-	 * @param {Matrix4} camera Transformation to be applied to the camera
-	 * @param {Matrix4} gizmos Transformation to be applied to gizmos
-	 */
-	 setTransformationMatrices( camera = null, gizmos = null ) {
+				}
 
-		if ( camera != null ) {
+				break;
 
-			if ( _transformation.camera != null ) {
+			case INPUT.TWO_FINGER:
 
-				_transformation.camera.copy( camera );
+				//rotate/pan/pinchMove
+				this.updateTouchEvent( event );
 
-			} else {
+				this.onRotateMove();
+				this.onPinchMove();
+				this.onDoublePanMove();
 
-				_transformation.camera = camera.clone();
+				break;
 
-			}
+			case INPUT.MULT_FINGER:
 
-		} else {
+				//multMove
+				this.updateTouchEvent( event );
 
-			_transformation.camera = null;
+				this.onTriplePanMove( event );
+				break;
 
 		}
 
-		if ( gizmos != null ) {
-
-			if ( _transformation.gizmos != null ) {
-
-				_transformation.gizmos.copy( gizmos );
+	} else if ( event.pointerType != 'touch' && this._input == INPUT.CURSOR ) {
 
-			} else {
+		let modifier = null;
 
-				_transformation.gizmos = gizmos.clone();
+		if ( event.ctrlKey || event.metaKey ) {
 
-			}
+			modifier = 'CTRL';
 
-		} else {
+		} else if ( event.shiftKey ) {
 
-			_transformation.gizmos = null;
+			modifier = 'SHIFT';
 
 		}
 
-	}
-
-	/**
-	 * Rotate camera around its direction axis passing by a given point by a given angle
-	 * @param {Vector3} point The point where the rotation axis is passing trough
-	 * @param {Number} angle Angle in radians
-	 * @returns The computed transormation matix
-	 */
-	zRotate = ( point, angle ) => {
-
-		this._rotationMatrix.makeRotationAxis( this._rotationAxis, angle );
-		this._translationMatrix.makeTranslation( - point.x, - point.y, - point.z );
+		const mouseOpState = this.getOpStateFromAction( this._button, modifier );
 
-		this._m4_1.makeTranslation( point.x, point.y, point.z );
-		this._m4_1.multiply( this._rotationMatrix );
-		this._m4_1.multiply( this._translationMatrix );
+		if ( mouseOpState != null ) {
 
-		this._v3_1.setFromMatrixPosition( this._gizmoMatrixState ).sub( point );	//vector from rotation center to gizmos position
-		this._v3_2.copy( this._v3_1 ).applyAxisAngle( this._rotationAxis, angle );	//apply rotation
-		this._v3_2.sub( this._v3_1 );
+			this.onSinglePanMove( event, mouseOpState );
 
-		this._m4_2.makeTranslation( this._v3_2.x, this._v3_2.y, this._v3_2.z );
+		}
 
-		this.setTransformationMatrices( this._m4_1, this._m4_2 );
-		return _transformation;
+	}
 
-	};
+	//checkDistance
+	if ( this._downValid ) {
 
+		const movement = this.calculatePointersDistance( this._downEvents[ this._downEvents.length - 1 ], event ) * this._devPxRatio;
+		if ( movement > this._movementThreshold ) {
 
-	getRaycaster() {
+			this._downValid = false;
 
-		return _raycaster;
+		}
 
 	}
 
+}
 
-	/**
-	 * Unproject the cursor on the 3D object surface
-	 * @param {Vector2} cursor Cursor coordinates in NDC
-	 * @param {Camera} camera Virtual camera
-	 * @returns {Vector3} The point of intersection with the model, if exist, null otherwise
-	 */
-	unprojectOnObj = ( cursor, camera ) => {
+function onPointerUp( event ) {
 
-		const raycaster = this.getRaycaster();
-		raycaster.near = camera.near;
-		raycaster.far = camera.far;
-		raycaster.setFromCamera( cursor, camera );
+	if ( event.pointerType == 'touch' && this._input != INPUT.CURSOR ) {
 
-		const intersect = raycaster.intersectObjects( this.scene.children, true );
+		const nTouch = this._touchCurrent.length;
 
-		for ( let i = 0; i < intersect.length; i ++ ) {
+		for ( let i = 0; i < nTouch; i ++ ) {
 
-			if ( intersect[ i ].object.uuid != this._gizmos.uuid && intersect[ i ].face != null ) {
+			if ( this._touchCurrent[ i ].pointerId == event.pointerId ) {
 
-				return intersect[ i ].point.clone();
+				this._touchCurrent.splice( i, 1 );
+				this._touchStart.splice( i, 1 );
+				break;
 
 			}
 
 		}
 
-		return null;
+		switch ( this._input ) {
 
-	};
+			case INPUT.ONE_FINGER:
+			case INPUT.ONE_FINGER_SWITCHED:
 
-	/**
-	 * Unproject the cursor on the trackball surface
-	 * @param {Camera} camera The virtual camera
-	 * @param {Number} cursorX Cursor horizontal coordinate on screen
-	 * @param {Number} cursorY Cursor vertical coordinate on screen
-	 * @param {HTMLElement} canvas The canvas where the renderer draws its output
-	 * @param {number} tbRadius The trackball radius
-	 * @returns {Vector3} The unprojected point on the trackball surface
-	 */
-	unprojectOnTbSurface = ( camera, cursorX, cursorY, canvas, tbRadius ) => {
+				//singleEnd
+				window.removeEventListener( 'pointermove', this._onPointerMove );
+				window.removeEventListener( 'pointerup', this._onPointerUp );
 
-		if ( camera.type == 'OrthographicCamera' ) {
+				this._input = INPUT.NONE;
+				this.onSinglePanEnd();
 
-			this._v2_1.copy( this.getCursorPosition( cursorX, cursorY, canvas ) );
-			this._v3_1.set( this._v2_1.x, this._v2_1.y, 0 );
+				break;
 
-			const x2 = Math.pow( this._v2_1.x, 2 );
-			const y2 = Math.pow( this._v2_1.y, 2 );
-			const r2 = Math.pow( this._tbRadius, 2 );
+			case INPUT.TWO_FINGER:
 
-			if ( x2 + y2 <= r2 * 0.5 ) {
+				//doubleEnd
+				this.onDoublePanEnd( event );
+				this.onPinchEnd( event );
+				this.onRotateEnd( event );
 
-				//intersection with sphere
-				this._v3_1.setZ( Math.sqrt( r2 - ( x2 + y2 ) ) );
+				//switching to singleStart
+				this._input = INPUT.ONE_FINGER_SWITCHED;
 
-			} else {
+				break;
 
-				//intersection with hyperboloid
-				this._v3_1.setZ( ( r2 * 0.5 ) / ( Math.sqrt( x2 + y2 ) ) );
+			case INPUT.MULT_FINGER:
+
+				if ( this._touchCurrent.length == 0 ) {
+
+					window.removeEventListener( 'pointermove', this._onPointerMove );
+					window.removeEventListener( 'pointerup', this._onPointerUp );
+
+					//multCancel
+					this._input = INPUT.NONE;
+					this.onTriplePanEnd();
+
+				}
 
-			}
+				break;
 
-			return this._v3_1;
+		}
 
-		} else if ( camera.type == 'PerspectiveCamera' ) {
+	} else if ( event.pointerType != 'touch' && this._input == INPUT.CURSOR ) {
 
-			//unproject cursor on the near plane
-			this._v2_1.copy( this.getCursorNDC( cursorX, cursorY, canvas ) );
+		window.removeEventListener( 'pointermove', this._onPointerMove );
+		window.removeEventListener( 'pointerup', this._onPointerUp );
 
-			this._v3_1.set( this._v2_1.x, this._v2_1.y, - 1 );
-			this._v3_1.applyMatrix4( camera.projectionMatrixInverse );
+		this._input = INPUT.NONE;
+		this.onSinglePanEnd();
+		this._button = - 1;
 
-			const rayDir = this._v3_1.clone().normalize(); //unprojected ray direction
-			const cameraGizmoDistance = camera.position.distanceTo( this._gizmos.position );
-			const radius2 = Math.pow( tbRadius, 2 );
+	}
 
-			//	  camera
-			//		|\
-			//		| \
-			//		|  \
-			//	h	|	\
-			//		| 	 \
-			//		| 	  \
-			//	_ _ | _ _ _\ _ _  near plane
-			//			l
+	if ( event.isPrimary ) {
 
-			const h = this._v3_1.z;
-			const l = Math.sqrt( Math.pow( this._v3_1.x, 2 ) + Math.pow( this._v3_1.y, 2 ) );
+		if ( this._downValid ) {
 
-			if ( l == 0 ) {
+			const downTime = event.timeStamp - this._downEvents[ this._downEvents.length - 1 ].timeStamp;
 
-				//ray aligned with camera
-				rayDir.set( this._v3_1.x, this._v3_1.y, tbRadius );
-				return rayDir;
+			if ( downTime <= this._maxDownTime ) {
 
-			}
+				if ( this._nclicks == 0 ) {
 
-			const m = h / l;
-			const q = cameraGizmoDistance;
+					//first valid click detected
+					this._nclicks = 1;
+					this._clickStart = performance.now();
 
-			/*
-			 * calculate intersection point between unprojected ray and trackball surface
-			 *|y = m * x + q
-			 *|x^2 + y^2 = r^2
-			 *
-			 * (m^2 + 1) * x^2 + (2 * m * q) * x + q^2 - r^2 = 0
-			 */
-			let a = Math.pow( m, 2 ) + 1;
-			let b = 2 * m * q;
-			let c = Math.pow( q, 2 ) - radius2;
-			let delta = Math.pow( b, 2 ) - ( 4 * a * c );
+				} else {
 
-			if ( delta >= 0 ) {
+					const clickInterval = event.timeStamp - this._clickStart;
+					const movement = this.calculatePointersDistance( this._downEvents[ 1 ], this._downEvents[ 0 ] ) * this._devPxRatio;
 
-				//intersection with sphere
-				this._v2_1.setX( ( - b - Math.sqrt( delta ) ) / ( 2 * a ) );
-				this._v2_1.setY( m * this._v2_1.x + q );
+					if ( clickInterval <= this._maxInterval && movement <= this._posThreshold ) {
 
-				const angle = MathUtils.RAD2DEG * this._v2_1.angle();
+						//second valid click detected
+						//fire double tap and reset values
+						this._nclicks = 0;
+						this._downEvents.splice( 0, this._downEvents.length );
+						this.onDoubleTap( event );
 
-				if ( angle >= 45 ) {
+					} else {
 
-					//if angle between intersection point and X' axis is >= 45°, return that point
-					//otherwise, calculate intersection point with hyperboloid
+						//new 'first click'
+						this._nclicks = 1;
+						this._downEvents.shift();
+						this._clickStart = performance.now();
 
-					const rayLength = Math.sqrt( Math.pow( this._v2_1.x, 2 ) + Math.pow( ( cameraGizmoDistance - this._v2_1.y ), 2 ) );
-					rayDir.multiplyScalar( rayLength );
-					rayDir.z += cameraGizmoDistance;
-					return rayDir;
+					}
 
 				}
 
-			}
+			} else {
 
-			//intersection with hyperboloid
-			/*
-			 *|y = m * x + q
-			 *|y = (1 / x) * (r^2 / 2)
-			 *
-			 * m * x^2 + q * x - r^2 / 2 = 0
-			 */
+				this._downValid = false;
+				this._nclicks = 0;
+				this._downEvents.splice( 0, this._downEvents.length );
 
-			a = m;
-			b = q;
-			c = - radius2 * 0.5;
-			delta = Math.pow( b, 2 ) - ( 4 * a * c );
-			this._v2_1.setX( ( - b - Math.sqrt( delta ) ) / ( 2 * a ) );
-			this._v2_1.setY( m * this._v2_1.x + q );
+			}
 
-			const rayLength = Math.sqrt( Math.pow( this._v2_1.x, 2 ) + Math.pow( ( cameraGizmoDistance - this._v2_1.y ), 2 ) );
+		} else {
 
-			rayDir.multiplyScalar( rayLength );
-			rayDir.z += cameraGizmoDistance;
-			return rayDir;
+			this._nclicks = 0;
+			this._downEvents.splice( 0, this._downEvents.length );
 
 		}
 
-	};
+	}
 
+}
 
-	/**
-	 * Unproject the cursor on the plane passing through the center of the trackball orthogonal to the camera
-	 * @param {Camera} camera The virtual camera
-	 * @param {Number} cursorX Cursor horizontal coordinate on screen
-	 * @param {Number} cursorY Cursor vertical coordinate on screen
-	 * @param {HTMLElement} canvas The canvas where the renderer draws its output
-	 * @param {Boolean} initialDistance If initial distance between camera and gizmos should be used for calculations instead of current (Perspective only)
-	 * @returns {Vector3} The unprojected point on the trackball plane
-	 */
-	unprojectOnTbPlane = ( camera, cursorX, cursorY, canvas, initialDistance = false ) => {
+function onWheel( event ) {
 
-		if ( camera.type == 'OrthographicCamera' ) {
+	if ( this.enabled && this.enableZoom ) {
 
-			this._v2_1.copy( this.getCursorPosition( cursorX, cursorY, canvas ) );
-			this._v3_1.set( this._v2_1.x, this._v2_1.y, 0 );
+		let modifier = null;
 
-			return this._v3_1.clone();
+		if ( event.ctrlKey || event.metaKey ) {
 
-		} else if ( camera.type == 'PerspectiveCamera' ) {
+			modifier = 'CTRL';
 
-			this._v2_1.copy( this.getCursorNDC( cursorX, cursorY, canvas ) );
+		} else if ( event.shiftKey ) {
 
-			//unproject cursor on the near plane
-			this._v3_1.set( this._v2_1.x, this._v2_1.y, - 1 );
-			this._v3_1.applyMatrix4( camera.projectionMatrixInverse );
+			modifier = 'SHIFT';
 
-			const rayDir = this._v3_1.clone().normalize(); //unprojected ray direction
+		}
 
-			//	  camera
-			//		|\
-			//		| \
-			//		|  \
-			//	h	|	\
-			//		| 	 \
-			//		| 	  \
-			//	_ _ | _ _ _\ _ _  near plane
-			//			l
+		const mouseOp = this.getOpFromAction( 'WHEEL', modifier );
 
-			const h = this._v3_1.z;
-			const l = Math.sqrt( Math.pow( this._v3_1.x, 2 ) + Math.pow( this._v3_1.y, 2 ) );
-			let cameraGizmoDistance;
+		if ( mouseOp != null ) {
 
-			if ( initialDistance ) {
+			event.preventDefault();
+			this.dispatchEvent( _startEvent );
 
-				cameraGizmoDistance = this._v3_1.setFromMatrixPosition( this._cameraMatrixState0 ).distanceTo( this._v3_2.setFromMatrixPosition( this._gizmoMatrixState0 ) );
+			const notchDeltaY = 125; //distance of one notch of mouse wheel
+			let sgn = event.deltaY / notchDeltaY;
 
-			} else {
+			let size = 1;
 
-				cameraGizmoDistance = camera.position.distanceTo( this._gizmos.position );
+			if ( sgn > 0 ) {
 
-			}
+				size = 1 / this.scaleFactor;
 
-			/*
-			 * calculate intersection point between unprojected ray and the plane
-			 *|y = mx + q
-			 *|y = 0
-			 *
-			 * x = -q/m
-			*/
-			if ( l == 0 ) {
+			} else if ( sgn < 0 ) {
 
-				//ray aligned with camera
-				rayDir.set( 0, 0, 0 );
-				return rayDir;
+				size = this.scaleFactor;
 
 			}
 
-			const m = h / l;
-			const q = cameraGizmoDistance;
-			const x = - q / m;
+			switch ( mouseOp ) {
 
-			const rayLength = Math.sqrt( Math.pow( q, 2 ) + Math.pow( x, 2 ) );
-			rayDir.multiplyScalar( rayLength );
-			rayDir.z = 0;
-			return rayDir;
+				case 'ZOOM':
 
-		}
+					this.updateTbState( STATE.SCALE, true );
 
-	};
+					if ( sgn > 0 ) {
 
-	/**
-	 * Update camera and gizmos state
-	 */
-	updateMatrixState = () => {
+						size = 1 / ( Math.pow( this.scaleFactor, sgn ) );
 
-		//update camera and gizmos state
-		this._cameraMatrixState.copy( this.camera.matrix );
-		this._gizmoMatrixState.copy( this._gizmos.matrix );
+					} else if ( sgn < 0 ) {
 
-		if ( this.camera.isOrthographicCamera ) {
+						size = Math.pow( this.scaleFactor, - sgn );
 
-			this._cameraProjectionState.copy( this.camera.projectionMatrix );
-			this.camera.updateProjectionMatrix();
-			this._zoomState = this.camera.zoom;
+					}
 
-		} else if ( this.camera.isPerspectiveCamera ) {
+					if ( this.cursorZoom && this.enablePan ) {
 
-			this._fovState = this.camera.fov;
+						let scalePoint;
 
-		}
+						if ( this.camera.isOrthographicCamera ) {
 
-	};
+							scalePoint = this.unprojectOnTbPlane( this.camera, event.clientX, event.clientY, this.domElement ).applyQuaternion( this.camera.quaternion ).multiplyScalar( 1 / this.camera.zoom ).add( this._gizmos.position );
 
-	/**
-	 * Update the trackball FSA
-	 * @param {STATE} newState New state of the FSA
-	 * @param {Boolean} updateMatrices If matriices state should be updated
-	 */
-	updateTbState = ( newState, updateMatrices ) => {
+						} else if ( this.camera.isPerspectiveCamera ) {
 
-		this._state = newState;
-		if ( updateMatrices ) {
+							scalePoint = this.unprojectOnTbPlane( this.camera, event.clientX, event.clientY, this.domElement ).applyQuaternion( this.camera.quaternion ).add( this._gizmos.position );
 
-			this.updateMatrixState();
+						}
 
-		}
+						this.applyTransformMatrix( this.scale( size, scalePoint ) );
 
-	};
+					} else {
 
-	update = () => {
+						this.applyTransformMatrix( this.scale( size, this._gizmos.position ) );
 
-		const EPS = 0.000001;
+					}
 
-		if ( this.target.equals( this._currentTarget ) === false ) {
+					if ( this._grid != null ) {
 
-			this._gizmos.position.copy( this.target );	//for correct radius calculation
-			this._tbRadius = this.calculateTbRadius( this.camera );
-			this.makeGizmos( this.target, this._tbRadius );
-			this._currentTarget.copy( this.target );
+						this.disposeGrid();
+						this.drawGrid();
 
-		}
+					}
 
-		//check min/max parameters
-		if ( this.camera.isOrthographicCamera ) {
+					this.updateTbState( STATE.IDLE, false );
 
-			//check zoom
-			if ( this.camera.zoom > this.maxZoom || this.camera.zoom < this.minZoom ) {
+					this.dispatchEvent( _changeEvent );
+					this.dispatchEvent( _endEvent );
 
-				const newZoom = MathUtils.clamp( this.camera.zoom, this.minZoom, this.maxZoom );
-				this.applyTransformMatrix( this.scale( newZoom / this.camera.zoom, this._gizmos.position, true ) );
+					break;
 
-			}
+				case 'FOV':
 
-		} else if ( this.camera.isPerspectiveCamera ) {
+					if ( this.camera.isPerspectiveCamera ) {
 
-			//check distance
-			const distance = this.camera.position.distanceTo( this._gizmos.position );
+						this.updateTbState( STATE.FOV, true );
 
-			if ( distance > this.maxDistance + EPS || distance < this.minDistance - EPS ) {
 
-				const newDistance = MathUtils.clamp( distance, this.minDistance, this.maxDistance );
-				this.applyTransformMatrix( this.scale( newDistance / distance, this._gizmos.position ) );
-				this.updateMatrixState();
+						//Vertigo effect
 
-			 }
+						//	  fov / 2
+						//		|\
+						//		| \
+						//		|  \
+						//	x	|	\
+						//		| 	 \
+						//		| 	  \
+						//		| _ _ _\
+						//			y
 
-			//check fov
-			if ( this.camera.fov < this.minFov || this.camera.fov > this.maxFov ) {
+						//check for iOs shift shortcut
+						if ( event.deltaX != 0 ) {
 
-				this.camera.fov = MathUtils.clamp( this.camera.fov, this.minFov, this.maxFov );
-				this.camera.updateProjectionMatrix();
+							sgn = event.deltaX / notchDeltaY;
 
-			}
+							size = 1;
 
-			const oldRadius = this._tbRadius;
-			this._tbRadius = this.calculateTbRadius( this.camera );
+							if ( sgn > 0 ) {
 
-			if ( oldRadius < this._tbRadius - EPS || oldRadius > this._tbRadius + EPS ) {
+								size = 1 / ( Math.pow( this.scaleFactor, sgn ) );
 
-				const scale = ( this._gizmos.scale.x + this._gizmos.scale.y + this._gizmos.scale.z ) / 3;
-				const newRadius = this._tbRadius / scale;
-				const curve = new EllipseCurve( 0, 0, newRadius, newRadius );
-				const points = curve.getPoints( this._curvePts );
-				const curveGeometry = new BufferGeometry().setFromPoints( points );
+							} else if ( sgn < 0 ) {
 
-				for ( const gizmo in this._gizmos.children ) {
+								size = Math.pow( this.scaleFactor, - sgn );
 
-					this._gizmos.children[ gizmo ].geometry = curveGeometry;
+							}
 
-				}
+						}
 
-			}
+						this._v3_1.setFromMatrixPosition( this._cameraMatrixState );
+						const x = this._v3_1.distanceTo( this._gizmos.position );
+						let xNew = x / size;	//distance between camera and gizmos if scale(size, scalepoint) would be performed
 
-		}
+						//check min and max distance
+						xNew = MathUtils.clamp( xNew, this.minDistance, this.maxDistance );
 
-		this.camera.lookAt( this._gizmos.position );
+						const y = x * Math.tan( MathUtils.DEG2RAD * this.camera.fov * 0.5 );
 
-	};
+						//calculate new fov
+						let newFov = MathUtils.RAD2DEG * ( Math.atan( y / xNew ) * 2 );
 
-	setStateFromJSON = ( json ) => {
+						//check min and max fov
+						if ( newFov > this.maxFov ) {
 
-		const state = JSON.parse( json );
+							newFov = this.maxFov;
 
-		if ( state.arcballState != undefined ) {
+						} else if ( newFov < this.minFov ) {
 
-			this._cameraMatrixState.fromArray( state.arcballState.cameraMatrix.elements );
-			this._cameraMatrixState.decompose( this.camera.position, this.camera.quaternion, this.camera.scale );
+							newFov = this.minFov;
 
-			this.camera.up.copy( state.arcballState.cameraUp );
-			this.camera.near = state.arcballState.cameraNear;
-			this.camera.far = state.arcballState.cameraFar;
+						}
 
-			this.camera.zoom = state.arcballState.cameraZoom;
+						const newDistance = y / Math.tan( MathUtils.DEG2RAD * ( newFov / 2 ) );
+						size = x / newDistance;
 
-			if ( this.camera.isPerspectiveCamera ) {
+						this.setFov( newFov );
+						this.applyTransformMatrix( this.scale( size, this._gizmos.position, false ) );
 
-				this.camera.fov = state.arcballState.cameraFov;
+					}
 
-			}
+					if ( this._grid != null ) {
 
-			this._gizmoMatrixState.fromArray( state.arcballState.gizmoMatrix.elements );
-			this._gizmoMatrixState.decompose( this._gizmos.position, this._gizmos.quaternion, this._gizmos.scale );
+						this.disposeGrid();
+						this.drawGrid();
 
-			this.camera.updateMatrix();
-			this.camera.updateProjectionMatrix();
+					}
 
-			this._gizmos.updateMatrix();
+					this.updateTbState( STATE.IDLE, false );
 
-			this._tbRadius = this.calculateTbRadius( this.camera );
-			const gizmoTmp = new Matrix4().copy( this._gizmoMatrixState0 );
-			this.makeGizmos( this._gizmos.position, this._tbRadius );
-			this._gizmoMatrixState0.copy( gizmoTmp );
+					this.dispatchEvent( _changeEvent );
+					this.dispatchEvent( _endEvent );
 
-			this.camera.lookAt( this._gizmos.position );
-			this.updateTbState( STATE.IDLE, false );
+					break;
 
-			this.dispatchEvent( _changeEvent );
+			}
 
 		}
 
-	};
+	}
 
 }
 

+ 11 - 3
examples/jsm/csm/CSM.js

@@ -16,6 +16,9 @@ const _center = new Vector3();
 const _bbox = new Box3();
 const _uniformArray = [];
 const _logArray = [];
+const _lightOrientationMatrix = new Matrix4();
+const _lightOrientationMatrixInverse = new Matrix4();
+const _up = new Vector3( 0, 1, 0 );
 
 export class CSM {
 
@@ -200,14 +203,19 @@ export class CSM {
 
 		const camera = this.camera;
 		const frustums = this.frustums;
+
+		// for each frustum we need to find its min-max box aligned with the light orientation
+		// the position in _lightOrientationMatrix does not matter, as we transform there and back
+		_lightOrientationMatrix.lookAt( new Vector3(), this.lightDirection, _up );
+		_lightOrientationMatrixInverse.copy( _lightOrientationMatrix ).invert();
+
 		for ( let i = 0; i < frustums.length; i ++ ) {
 
 			const light = this.lights[ i ];
 			const shadowCam = light.shadow.camera;
 			const texelWidth = ( shadowCam.right - shadowCam.left ) / this.shadowMapSize;
 			const texelHeight = ( shadowCam.top - shadowCam.bottom ) / this.shadowMapSize;
-			light.shadow.camera.updateMatrixWorld( true );
-			_cameraToLightMatrix.multiplyMatrices( light.shadow.camera.matrixWorldInverse, camera.matrixWorld );
+			_cameraToLightMatrix.multiplyMatrices( _lightOrientationMatrixInverse, camera.matrixWorld );
 			frustums[ i ].toSpace( _cameraToLightMatrix, _lightSpaceFrustum );
 
 			const nearVerts = _lightSpaceFrustum.vertices.near;
@@ -224,7 +232,7 @@ export class CSM {
 			_center.z = _bbox.max.z + this.lightMargin;
 			_center.x = Math.floor( _center.x / texelWidth ) * texelWidth;
 			_center.y = Math.floor( _center.y / texelHeight ) * texelHeight;
-			_center.applyMatrix4( light.shadow.camera.matrixWorld );
+			_center.applyMatrix4( _lightOrientationMatrix );
 
 			light.position.copy( _center );
 			light.target.position.copy( _center );

+ 108 - 6
examples/jsm/exporters/GLTFExporter.js

@@ -1,6 +1,7 @@
 import {
 	BufferAttribute,
 	ClampToEdgeWrapping,
+	Color,
 	DoubleSide,
 	InterpolateDiscrete,
 	InterpolateLinear,
@@ -53,6 +54,18 @@ class GLTFExporter {
 
 		} );
 
+		this.register( function ( writer ) {
+
+			return new GLTFMaterialsIorExtension( writer );
+
+		} );
+
+		this.register( function ( writer ) {
+
+			return new GLTFMaterialsSpecularExtension( writer );
+
+		} );
+
 		this.register( function ( writer ) {
 
 			return new GLTFMaterialsClearcoatExtension( writer );
@@ -180,6 +193,8 @@ const PATH_PROPERTIES = {
 	morphTargetInfluences: 'weights'
 };
 
+const DEFAULT_SPECULAR_COLOR = new Color();
+
 // GLB constants
 // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#glb-file-format-specification
 
@@ -746,11 +761,11 @@ class GLTFWriter {
 
 		console.warn( 'THREE.GLTFExporter: Merged metalnessMap and roughnessMap textures.' );
 
-		const metalness = metalnessMap?.image;
-		const roughness = roughnessMap?.image;
+		const metalness = metalnessMap ? metalnessMap.image : null;
+		const roughness = roughnessMap ? roughnessMap.image : null;
 
-		const width = Math.max( metalness?.width || 0, roughness?.width || 0 );
-		const height = Math.max( metalness?.height || 0, roughness?.height || 0 );
+		const width = Math.max( metalness ? metalness.width : 0, roughness ? roughness.width : 0 );
+		const height = Math.max( metalness ? metalness.height : 0, roughness ? roughness.height : 0 );
 
 		const canvas = getCanvas();
 		canvas.width = width;
@@ -2313,7 +2328,7 @@ class GLTFMaterialsClearcoatExtension {
 
 	writeMaterial( material, materialDef ) {
 
-		if ( ! material.isMeshPhysicalMaterial ) return;
+		if ( ! material.isMeshPhysicalMaterial || material.clearcoat === 0 ) return;
 
 		const writer = this.writer;
 		const extensionsUsed = writer.extensionsUsed;
@@ -2374,7 +2389,7 @@ class GLTFMaterialsIridescenceExtension {
 
 	writeMaterial( material, materialDef ) {
 
-		if ( ! material.isMeshPhysicalMaterial ) return;
+		if ( ! material.isMeshPhysicalMaterial || material.iridescence === 0 ) return;
 
 		const writer = this.writer;
 		const extensionsUsed = writer.extensionsUsed;
@@ -2499,6 +2514,93 @@ class GLTFMaterialsVolumeExtension {
 
 }
 
+/**
+ * Materials ior Extension
+ *
+ * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_ior
+ */
+class GLTFMaterialsIorExtension {
+
+	constructor( writer ) {
+
+		this.writer = writer;
+		this.name = 'KHR_materials_ior';
+
+	}
+
+	writeMaterial( material, materialDef ) {
+
+		if ( ! material.isMeshPhysicalMaterial || material.ior === 1.5 ) return;
+
+		const writer = this.writer;
+		const extensionsUsed = writer.extensionsUsed;
+
+		const extensionDef = {};
+
+		extensionDef.ior = material.ior;
+
+		materialDef.extensions = materialDef.extensions || {};
+		materialDef.extensions[ this.name ] = extensionDef;
+
+		extensionsUsed[ this.name ] = true;
+
+	}
+
+}
+
+/**
+ * Materials specular Extension
+ *
+ * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_specular
+ */
+class GLTFMaterialsSpecularExtension {
+
+	constructor( writer ) {
+
+		this.writer = writer;
+		this.name = 'KHR_materials_specular';
+
+	}
+
+	writeMaterial( material, materialDef ) {
+
+		if ( ! material.isMeshPhysicalMaterial || ( material.specularIntensity === 1.0 && 
+		       material.specularColor.equals( DEFAULT_SPECULAR_COLOR ) && 
+		     ! material.specularIntensityMap && ! material.specularColorTexture ) ) return;
+
+		const writer = this.writer;
+		const extensionsUsed = writer.extensionsUsed;
+
+		const extensionDef = {};
+
+		if ( material.specularIntensityMap ) {
+
+			const specularIntensityMapDef = { index: writer.processTexture( material.specularIntensityMap ) };
+			writer.applyTextureTransform( specularIntensityMapDef, material.specularIntensityMap );
+			extensionDef.specularTexture = specularIntensityMapDef;
+
+		}
+
+		if ( material.specularColorMap ) {
+
+			const specularColorMapDef = { index: writer.processTexture( material.specularColorMap ) };
+			writer.applyTextureTransform( specularColorMapDef, material.specularColorMap );
+			extensionDef.specularColorTexture = specularColorMapDef;
+
+		}
+
+		extensionDef.specularFactor = material.specularIntensity;
+		extensionDef.specularColorFactor = material.specularColor.toArray();
+
+		materialDef.extensions = materialDef.extensions || {};
+		materialDef.extensions[ this.name ] = extensionDef;
+
+		extensionsUsed[ this.name ] = true;
+
+	}
+
+}
+
 /**
  * Static utility functions
  */

+ 7 - 0
examples/jsm/exporters/PLYExporter.js

@@ -97,8 +97,15 @@ class PLYExporter {
 				const geometry = mesh.geometry;
 
 				const vertices = geometry.getAttribute( 'position' );
+				const normals = geometry.getAttribute( 'normal' );
+				const colors = geometry.getAttribute( 'color' );
+
 				vertexCount += vertices.count;
 
+				if ( normals !== undefined ) includeNormals = true;
+
+				if ( colors !== undefined ) includeColors = true;
+
 				includeIndices = false;
 
 			}

+ 1 - 0
examples/jsm/helpers/OctreeHelper.js

@@ -67,6 +67,7 @@ class OctreeHelper extends LineSegments {
 		this.material.dispose();
 
 	}
+
 }
 
 export { OctreeHelper };

+ 70 - 56
examples/jsm/helpers/ViewHelper.js

@@ -1,35 +1,49 @@
-import * as THREE from 'three';
-
-const vpTemp = new THREE.Vector4();
-
-class ViewHelper extends THREE.Object3D {
-
-	constructor( editorCamera, dom ) {
+import {
+	BoxGeometry,
+	CanvasTexture,
+	Color,
+	Euler,
+	Mesh,
+	MeshBasicMaterial,
+	Object3D,
+	OrthographicCamera,
+	Quaternion,
+	Raycaster,
+	Sprite,
+	SpriteMaterial,
+	Vector2,
+	Vector3,
+	Vector4
+} from 'three';
+
+class ViewHelper extends Object3D {
+
+	constructor( camera, domElement ) {
 
 		super();
 
 		this.isViewHelper = true;
 
 		this.animating = false;
-		this.controls = null;
+		this.center = new Vector3();
 
-		const color1 = new THREE.Color( '#ff3653' );
-		const color2 = new THREE.Color( '#8adb00' );
-		const color3 = new THREE.Color( '#2c8fff' );
+		const color1 = new Color( '#ff3653' );
+		const color2 = new Color( '#8adb00' );
+		const color3 = new Color( '#2c8fff' );
 
 		const interactiveObjects = [];
-		const raycaster = new THREE.Raycaster();
-		const mouse = new THREE.Vector2();
-		const dummy = new THREE.Object3D();
+		const raycaster = new Raycaster();
+		const mouse = new Vector2();
+		const dummy = new Object3D();
 
-		const camera = new THREE.OrthographicCamera( - 2, 2, 2, - 2, 0, 4 );
-		camera.position.set( 0, 0, 2 );
+		const orthoCamera = new OrthographicCamera( - 2, 2, 2, - 2, 0, 4 );
+		orthoCamera.position.set( 0, 0, 2 );
 
-		const geometry = new THREE.BoxGeometry( 0.8, 0.05, 0.05 ).translate( 0.4, 0, 0 );
+		const geometry = new BoxGeometry( 0.8, 0.05, 0.05 ).translate( 0.4, 0, 0 );
 
-		const xAxis = new THREE.Mesh( geometry, getAxisMaterial( color1 ) );
-		const yAxis = new THREE.Mesh( geometry, getAxisMaterial( color2 ) );
-		const zAxis = new THREE.Mesh( geometry, getAxisMaterial( color3 ) );
+		const xAxis = new Mesh( geometry, getAxisMaterial( color1 ) );
+		const yAxis = new Mesh( geometry, getAxisMaterial( color2 ) );
+		const zAxis = new Mesh( geometry, getAxisMaterial( color3 ) );
 
 		yAxis.rotation.z = Math.PI / 2;
 		zAxis.rotation.y = - Math.PI / 2;
@@ -38,17 +52,17 @@ class ViewHelper extends THREE.Object3D {
 		this.add( zAxis );
 		this.add( yAxis );
 
-		const posXAxisHelper = new THREE.Sprite( getSpriteMaterial( color1, 'X' ) );
+		const posXAxisHelper = new Sprite( getSpriteMaterial( color1, 'X' ) );
 		posXAxisHelper.userData.type = 'posX';
-		const posYAxisHelper = new THREE.Sprite( getSpriteMaterial( color2, 'Y' ) );
+		const posYAxisHelper = new Sprite( getSpriteMaterial( color2, 'Y' ) );
 		posYAxisHelper.userData.type = 'posY';
-		const posZAxisHelper = new THREE.Sprite( getSpriteMaterial( color3, 'Z' ) );
+		const posZAxisHelper = new Sprite( getSpriteMaterial( color3, 'Z' ) );
 		posZAxisHelper.userData.type = 'posZ';
-		const negXAxisHelper = new THREE.Sprite( getSpriteMaterial( color1 ) );
+		const negXAxisHelper = new Sprite( getSpriteMaterial( color1 ) );
 		negXAxisHelper.userData.type = 'negX';
-		const negYAxisHelper = new THREE.Sprite( getSpriteMaterial( color2 ) );
+		const negYAxisHelper = new Sprite( getSpriteMaterial( color2 ) );
 		negYAxisHelper.userData.type = 'negY';
-		const negZAxisHelper = new THREE.Sprite( getSpriteMaterial( color3 ) );
+		const negZAxisHelper = new Sprite( getSpriteMaterial( color3 ) );
 		negZAxisHelper.userData.type = 'negZ';
 
 		posXAxisHelper.position.x = 1;
@@ -75,17 +89,17 @@ class ViewHelper extends THREE.Object3D {
 		interactiveObjects.push( negYAxisHelper );
 		interactiveObjects.push( negZAxisHelper );
 
-		const point = new THREE.Vector3();
+		const point = new Vector3();
 		const dim = 128;
 		const turnRate = 2 * Math.PI; // turn rate in angles per second
 
 		this.render = function ( renderer ) {
 
-			this.quaternion.copy( editorCamera.quaternion ).invert();
+			this.quaternion.copy( camera.quaternion ).invert();
 			this.updateMatrixWorld();
 
 			point.set( 0, 0, 1 );
-			point.applyQuaternion( editorCamera.quaternion );
+			point.applyQuaternion( camera.quaternion );
 
 			if ( point.x >= 0 ) {
 
@@ -125,37 +139,38 @@ class ViewHelper extends THREE.Object3D {
 
 			//
 
-			const x = dom.offsetWidth - dim;
+			const x = domElement.offsetWidth - dim;
 
 			renderer.clearDepth();
 
-			renderer.getViewport( vpTemp );
+			renderer.getViewport( viewport );
 			renderer.setViewport( x, 0, dim, dim );
 
-			renderer.render( this, camera );
+			renderer.render( this, orthoCamera );
 
-			renderer.setViewport( vpTemp.x, vpTemp.y, vpTemp.z, vpTemp.w );
+			renderer.setViewport( viewport.x, viewport.y, viewport.z, viewport.w );
 
 		};
 
-		const targetPosition = new THREE.Vector3();
-		const targetQuaternion = new THREE.Quaternion();
+		const targetPosition = new Vector3();
+		const targetQuaternion = new Quaternion();
 
-		const q1 = new THREE.Quaternion();
-		const q2 = new THREE.Quaternion();
+		const q1 = new Quaternion();
+		const q2 = new Quaternion();
+		const viewport = new Vector4();
 		let radius = 0;
 
 		this.handleClick = function ( event ) {
 
 			if ( this.animating === true ) return false;
 
-			const rect = dom.getBoundingClientRect();
-			const offsetX = rect.left + ( dom.offsetWidth - dim );
-			const offsetY = rect.top + ( dom.offsetHeight - dim );
+			const rect = domElement.getBoundingClientRect();
+			const offsetX = rect.left + ( domElement.offsetWidth - dim );
+			const offsetY = rect.top + ( domElement.offsetHeight - dim );
 			mouse.x = ( ( event.clientX - offsetX ) / ( rect.width - offsetX ) ) * 2 - 1;
 			mouse.y = - ( ( event.clientY - offsetY ) / ( rect.bottom - offsetY ) ) * 2 + 1;
 
-			raycaster.setFromCamera( mouse, camera );
+			raycaster.setFromCamera( mouse, orthoCamera );
 
 			const intersects = raycaster.intersectObjects( interactiveObjects );
 
@@ -164,7 +179,7 @@ class ViewHelper extends THREE.Object3D {
 				const intersection = intersects[ 0 ];
 				const object = intersection.object;
 
-				prepareAnimationData( object, this.controls.center );
+				prepareAnimationData( object, this.center );
 
 				this.animating = true;
 
@@ -181,16 +196,15 @@ class ViewHelper extends THREE.Object3D {
 		this.update = function ( delta ) {
 
 			const step = delta * turnRate;
-			const focusPoint = this.controls.center;
 
 			// animate position by doing a slerp and then scaling the position on the unit sphere
 
 			q1.rotateTowards( q2, step );
-			editorCamera.position.set( 0, 0, 1 ).applyQuaternion( q1 ).multiplyScalar( radius ).add( focusPoint );
+			camera.position.set( 0, 0, 1 ).applyQuaternion( q1 ).multiplyScalar( radius ).add( this.center );
 
 			// animate orientation
 
-			editorCamera.quaternion.rotateTowards( targetQuaternion, step );
+			camera.quaternion.rotateTowards( targetQuaternion, step );
 
 			if ( q1.angleTo( q2 ) === 0 ) {
 
@@ -230,32 +244,32 @@ class ViewHelper extends THREE.Object3D {
 
 				case 'posX':
 					targetPosition.set( 1, 0, 0 );
-					targetQuaternion.setFromEuler( new THREE.Euler( 0, Math.PI * 0.5, 0 ) );
+					targetQuaternion.setFromEuler( new Euler( 0, Math.PI * 0.5, 0 ) );
 					break;
 
 				case 'posY':
 					targetPosition.set( 0, 1, 0 );
-					targetQuaternion.setFromEuler( new THREE.Euler( - Math.PI * 0.5, 0, 0 ) );
+					targetQuaternion.setFromEuler( new Euler( - Math.PI * 0.5, 0, 0 ) );
 					break;
 
 				case 'posZ':
 					targetPosition.set( 0, 0, 1 );
-					targetQuaternion.setFromEuler( new THREE.Euler() );
+					targetQuaternion.setFromEuler( new Euler() );
 					break;
 
 				case 'negX':
 					targetPosition.set( - 1, 0, 0 );
-					targetQuaternion.setFromEuler( new THREE.Euler( 0, - Math.PI * 0.5, 0 ) );
+					targetQuaternion.setFromEuler( new Euler( 0, - Math.PI * 0.5, 0 ) );
 					break;
 
 				case 'negY':
 					targetPosition.set( 0, - 1, 0 );
-					targetQuaternion.setFromEuler( new THREE.Euler( Math.PI * 0.5, 0, 0 ) );
+					targetQuaternion.setFromEuler( new Euler( Math.PI * 0.5, 0, 0 ) );
 					break;
 
 				case 'negZ':
 					targetPosition.set( 0, 0, - 1 );
-					targetQuaternion.setFromEuler( new THREE.Euler( 0, Math.PI, 0 ) );
+					targetQuaternion.setFromEuler( new Euler( 0, Math.PI, 0 ) );
 					break;
 
 				default:
@@ -265,12 +279,12 @@ class ViewHelper extends THREE.Object3D {
 
 			//
 
-			radius = editorCamera.position.distanceTo( focusPoint );
+			radius = camera.position.distanceTo( focusPoint );
 			targetPosition.multiplyScalar( radius ).add( focusPoint );
 
 			dummy.position.copy( focusPoint );
 
-			dummy.lookAt( editorCamera.position );
+			dummy.lookAt( camera.position );
 			q1.copy( dummy.quaternion );
 
 			dummy.lookAt( targetPosition );
@@ -280,7 +294,7 @@ class ViewHelper extends THREE.Object3D {
 
 		function getAxisMaterial( color ) {
 
-			return new THREE.MeshBasicMaterial( { color: color, toneMapped: false } );
+			return new MeshBasicMaterial( { color: color, toneMapped: false } );
 
 		}
 
@@ -306,9 +320,9 @@ class ViewHelper extends THREE.Object3D {
 
 			}
 
-			const texture = new THREE.CanvasTexture( canvas );
+			const texture = new CanvasTexture( canvas );
 
-			return new THREE.SpriteMaterial( { map: texture, toneMapped: false } );
+			return new SpriteMaterial( { map: texture, toneMapped: false } );
 
 		}
 

+ 1 - 1
examples/jsm/interactive/HTMLMesh.js

@@ -36,7 +36,7 @@ class HTMLMesh extends Mesh {
 			material.dispose();
 
 			material.map.dispose();
-			
+
 			canvases.delete( dom );
 
 			this.removeEventListener( 'mousedown', onEvent );

+ 1 - 1
examples/jsm/interactive/InteractiveGroup.js

@@ -28,7 +28,7 @@ class InteractiveGroup extends Group {
 			event.stopPropagation();
 
 			const rect = renderer.domElement.getBoundingClientRect();
-			
+
 			_pointer.x = ( event.clientX - rect.left ) / rect.width * 2 - 1;
 			_pointer.y = - ( event.clientY - rect.top ) / rect.height * 2 + 1;
 

File diff suppressed because it is too large
+ 0 - 0
examples/jsm/libs/flow.module.js


+ 25 - 0
examples/jsm/lights/IESSpotLight.js

@@ -0,0 +1,25 @@
+import { SpotLight } from 'three';
+
+class IESSpotLight extends SpotLight {
+
+	constructor( color, intensity, distance, angle, penumbra, decay ) {
+
+		super( color, intensity, distance, angle, penumbra, decay );
+
+		this.iesMap = null;
+
+	}
+
+	copy( source, recursive ) {
+
+		super.copy( source, recursive );
+
+		this.iesMap = source.iesMap;
+
+		return this;
+
+	}
+
+}
+
+export default IESSpotLight;

+ 5 - 4
examples/jsm/loaders/3MFLoader.js

@@ -9,7 +9,6 @@ import {
 	LinearFilter,
 	LinearMipmapLinearFilter,
 	Loader,
-	LoaderUtils,
 	Matrix4,
 	Mesh,
 	MeshPhongMaterial,
@@ -104,6 +103,8 @@ class ThreeMFLoader extends Loader {
 			const printTicketParts = {};
 			const texturesParts = {};
 
+			const textDecoder = new TextDecoder();
+
 			try {
 
 				zip = fflate.unzipSync( new Uint8Array( data ) );
@@ -144,7 +145,7 @@ class ThreeMFLoader extends Loader {
 			//
 
 			const relsView = zip[ relsName ];
-			const relsFileText = LoaderUtils.decodeText( relsView );
+			const relsFileText = textDecoder.decode( relsView );
 			const rels = parseRelsXml( relsFileText );
 
 			//
@@ -152,7 +153,7 @@ class ThreeMFLoader extends Loader {
 			if ( modelRelsName ) {
 
 				const relsView = zip[ modelRelsName ];
-				const relsFileText = LoaderUtils.decodeText( relsView );
+				const relsFileText = textDecoder.decode( relsView );
 				modelRels = parseRelsXml( relsFileText );
 
 			}
@@ -164,7 +165,7 @@ class ThreeMFLoader extends Loader {
 				const modelPart = modelPartNames[ i ];
 				const view = zip[ modelPart ];
 
-				const fileText = LoaderUtils.decodeText( view );
+				const fileText = textDecoder.decode( view );
 				const xmlData = new DOMParser().parseFromString( fileText, 'application/xml' );
 
 				if ( xmlData.documentElement.nodeName.toLowerCase() !== 'model' ) {

+ 1 - 2
examples/jsm/loaders/AMFLoader.js

@@ -5,7 +5,6 @@ import {
 	Float32BufferAttribute,
 	Group,
 	Loader,
-	LoaderUtils,
 	Mesh,
 	MeshPhongMaterial
 } from 'three';
@@ -114,7 +113,7 @@ class AMFLoader extends Loader {
 
 			}
 
-			const fileText = LoaderUtils.decodeText( view );
+			const fileText = new TextDecoder().decode( view );
 			const xmlData = new DOMParser().parseFromString( fileText, 'application/xml' );
 
 			if ( xmlData.documentElement.nodeName.toLowerCase() !== 'amf' ) {

+ 6 - 0
examples/jsm/loaders/DRACOLoader.js

@@ -351,6 +351,12 @@ class DRACOLoader extends Loader {
 
 		this.workerPool.length = 0;
 
+		if ( this.workerSourceURL !== '' ) {
+
+			URL.revokeObjectURL( this.workerSourceURL );
+
+		}
+
 		return this;
 
 	}

+ 10 - 13
examples/jsm/loaders/FBXLoader.js

@@ -3592,6 +3592,7 @@ class BinaryReader {
 		this.dv = new DataView( buffer );
 		this.offset = 0;
 		this.littleEndian = ( littleEndian !== undefined ) ? littleEndian : true;
+		this._textDecoder = new TextDecoder();
 
 	}
 
@@ -3810,19 +3811,15 @@ class BinaryReader {
 
 	getString( size ) {
 
-		// note: safari 9 doesn't support Uint8Array.indexOf; create intermediate array instead
-		let a = [];
+		const start = this.offset;
+		let a = new Uint8Array( this.dv.buffer, start, size );
 
-		for ( let i = 0; i < size; i ++ ) {
-
-			a[ i ] = this.getUint8();
-
-		}
+		this.skip( size );
 
 		const nullByte = a.indexOf( 0 );
-		if ( nullByte >= 0 ) a = a.slice( 0, nullByte );
+		if ( nullByte >= 0 ) a = new Uint8Array( this.dv.buffer, start, nullByte );
 
-		return LoaderUtils.decodeText( new Uint8Array( a ) );
+		return this._textDecoder.decode( a );
 
 	}
 
@@ -3968,7 +3965,7 @@ function generateTransform( transformData ) {
 	if ( transformData.preRotation ) {
 
 		const array = transformData.preRotation.map( MathUtils.degToRad );
-		array.push( transformData.eulerOrder || Euler.DefaultOrder );
+		array.push( transformData.eulerOrder || Euler.DEFAULT_ORDER );
 		lPreRotationM.makeRotationFromEuler( tempEuler.fromArray( array ) );
 
 	}
@@ -3976,7 +3973,7 @@ function generateTransform( transformData ) {
 	if ( transformData.rotation ) {
 
 		const array = transformData.rotation.map( MathUtils.degToRad );
-		array.push( transformData.eulerOrder || Euler.DefaultOrder );
+		array.push( transformData.eulerOrder || Euler.DEFAULT_ORDER );
 		lRotationM.makeRotationFromEuler( tempEuler.fromArray( array ) );
 
 	}
@@ -3984,7 +3981,7 @@ function generateTransform( transformData ) {
 	if ( transformData.postRotation ) {
 
 		const array = transformData.postRotation.map( MathUtils.degToRad );
-		array.push( transformData.eulerOrder || Euler.DefaultOrder );
+		array.push( transformData.eulerOrder || Euler.DEFAULT_ORDER );
 		lPostRotationM.makeRotationFromEuler( tempEuler.fromArray( array ) );
 		lPostRotationM.invert();
 
@@ -4104,7 +4101,7 @@ function convertArrayBufferToString( buffer, from, to ) {
 	if ( from === undefined ) from = 0;
 	if ( to === undefined ) to = buffer.byteLength;
 
-	return LoaderUtils.decodeText( new Uint8Array( buffer, from, to ) );
+	return new TextDecoder().decode( new Uint8Array( buffer, from, to ) );
 
 }
 

+ 9 - 100
examples/jsm/loaders/GLTFLoader.js

@@ -62,6 +62,7 @@ import {
 	VectorKeyframeTrack,
 	sRGBEncoding
 } from 'three';
+import { toTrianglesDrawMode } from '../utils/BufferGeometryUtils.js';
 
 class GLTFLoader extends Loader {
 
@@ -286,6 +287,7 @@ class GLTFLoader extends Loader {
 		let json;
 		const extensions = {};
 		const plugins = {};
+		const textDecoder = new TextDecoder();
 
 		if ( typeof data === 'string' ) {
 
@@ -293,7 +295,7 @@ class GLTFLoader extends Loader {
 
 		} else if ( data instanceof ArrayBuffer ) {
 
-			const magic = LoaderUtils.decodeText( new Uint8Array( data, 0, 4 ) );
+			const magic = textDecoder.decode( new Uint8Array( data, 0, 4 ) );
 
 			if ( magic === BINARY_EXTENSION_HEADER_MAGIC ) {
 
@@ -312,7 +314,7 @@ class GLTFLoader extends Loader {
 
 			} else {
 
-				json = JSON.parse( LoaderUtils.decodeText( new Uint8Array( data ) ) );
+				json = JSON.parse( textDecoder.decode( data ) );
 
 			}
 
@@ -584,7 +586,7 @@ class GLTFLightsExtension {
 
 	}
 
-	getDependency( type, index ) {	
+	getDependency( type, index ) {
 
 		if ( type !== 'light' ) return;
 
@@ -1566,9 +1568,10 @@ class GLTFBinaryExtension {
 		this.body = null;
 
 		const headerView = new DataView( data, 0, BINARY_EXTENSION_HEADER_LENGTH );
+		const textDecoder = new TextDecoder();
 
 		this.header = {
-			magic: LoaderUtils.decodeText( new Uint8Array( data.slice( 0, 4 ) ) ),
+			magic: textDecoder.decode( new Uint8Array( data.slice( 0, 4 ) ) ),
 			version: headerView.getUint32( 4, true ),
 			length: headerView.getUint32( 8, true )
 		};
@@ -1598,7 +1601,7 @@ class GLTFBinaryExtension {
 			if ( chunkType === BINARY_EXTENSION_CHUNK_TYPES.JSON ) {
 
 				const contentArray = new Uint8Array( data, BINARY_EXTENSION_HEADER_LENGTH + chunkIndex, chunkLength );
-				this.content = LoaderUtils.decodeText( contentArray );
+				this.content = textDecoder.decode( contentArray );
 
 			} else if ( chunkType === BINARY_EXTENSION_CHUNK_TYPES.BIN ) {
 
@@ -2264,7 +2267,7 @@ class GLTFParser {
 
 		// Use an ImageBitmapLoader if imageBitmaps are supported. Moves much of the
 		// expensive work of uploading a texture to the GPU off the main thread.
-		
+
 		let isSafari = false;
 		let isFirefox = false;
 		let firefoxVersion = - 1;
@@ -4305,98 +4308,4 @@ function addPrimitiveAttributes( geometry, primitiveDef, parser ) {
 
 }
 
-/**
- * @param {BufferGeometry} geometry
- * @param {Number} drawMode
- * @return {BufferGeometry}
- */
-function toTrianglesDrawMode( geometry, drawMode ) {
-
-	let index = geometry.getIndex();
-
-	// generate index if not present
-
-	if ( index === null ) {
-
-		const indices = [];
-
-		const position = geometry.getAttribute( 'position' );
-
-		if ( position !== undefined ) {
-
-			for ( let i = 0; i < position.count; i ++ ) {
-
-				indices.push( i );
-
-			}
-
-			geometry.setIndex( indices );
-			index = geometry.getIndex();
-
-		} else {
-
-			console.error( 'THREE.GLTFLoader.toTrianglesDrawMode(): Undefined position attribute. Processing not possible.' );
-			return geometry;
-
-		}
-
-	}
-
-	//
-
-	const numberOfTriangles = index.count - 2;
-	const newIndices = [];
-
-	if ( drawMode === TriangleFanDrawMode ) {
-
-		// gl.TRIANGLE_FAN
-
-		for ( let i = 1; i <= numberOfTriangles; i ++ ) {
-
-			newIndices.push( index.getX( 0 ) );
-			newIndices.push( index.getX( i ) );
-			newIndices.push( index.getX( i + 1 ) );
-
-		}
-
-	} else {
-
-		// gl.TRIANGLE_STRIP
-
-		for ( let i = 0; i < numberOfTriangles; i ++ ) {
-
-			if ( i % 2 === 0 ) {
-
-				newIndices.push( index.getX( i ) );
-				newIndices.push( index.getX( i + 1 ) );
-				newIndices.push( index.getX( i + 2 ) );
-
-
-			} else {
-
-				newIndices.push( index.getX( i + 2 ) );
-				newIndices.push( index.getX( i + 1 ) );
-				newIndices.push( index.getX( i ) );
-
-			}
-
-		}
-
-	}
-
-	if ( ( newIndices.length / 3 ) !== numberOfTriangles ) {
-
-		console.error( 'THREE.GLTFLoader.toTrianglesDrawMode(): Unable to generate correct amount of triangles.' );
-
-	}
-
-	// build final geometry
-
-	const newGeometry = geometry.clone();
-	newGeometry.setIndex( newIndices );
-
-	return newGeometry;
-
-}
-
 export { GLTFLoader };

+ 337 - 0
examples/jsm/loaders/IESLoader.js

@@ -0,0 +1,337 @@
+import {
+	DataTexture,
+	FileLoader,
+	FloatType,
+	RedFormat,
+	MathUtils,
+	Loader,
+	UnsignedByteType,
+	LinearFilter,
+	HalfFloatType,
+	DataUtils
+} from 'three';
+
+class IESLoader extends Loader {
+
+	constructor( manager ) {
+
+		super( manager );
+
+		this.type = HalfFloatType;
+
+	}
+
+	_getIESValues( iesLamp, type ) {
+
+		const width = 360;
+		const height = 180;
+		const size = width * height;
+
+		const data = new Array( size );
+
+		function interpolateCandelaValues( phi, theta ) {
+
+			let phiIndex = 0, thetaIndex = 0;
+			let startTheta = 0, endTheta = 0, startPhi = 0, endPhi = 0;
+
+			for ( let i = 0; i < iesLamp.numHorAngles - 1; ++ i ) { // numHorAngles = horAngles.length-1 because of extra padding, so this wont cause an out of bounds error
+
+				if ( theta < iesLamp.horAngles[ i + 1 ] || i == iesLamp.numHorAngles - 2 ) {
+
+					thetaIndex = i;
+					startTheta = iesLamp.horAngles[ i ];
+					endTheta = iesLamp.horAngles[ i + 1 ];
+
+					break;
+
+				}
+
+			}
+
+			for ( let i = 0; i < iesLamp.numVerAngles - 1; ++ i ) {
+
+				if ( phi < iesLamp.verAngles[ i + 1 ] || i == iesLamp.numVerAngles - 2 ) {
+
+					phiIndex = i;
+					startPhi = iesLamp.verAngles[ i ];
+					endPhi = iesLamp.verAngles[ i + 1 ];
+
+					break;
+
+				}
+
+			}
+
+			const deltaTheta = endTheta - startTheta;
+			const deltaPhi = endPhi - startPhi;
+
+			if ( deltaPhi === 0 ) // Outside range
+				return 0;
+
+			const t1 = deltaTheta === 0 ? 0 : ( theta - startTheta ) / deltaTheta;
+			const t2 = ( phi - startPhi ) / deltaPhi;
+
+			const nextThetaIndex = deltaTheta === 0 ? thetaIndex : thetaIndex + 1;
+
+			const v1 = MathUtils.lerp( iesLamp.candelaValues[ thetaIndex ][ phiIndex ], iesLamp.candelaValues[ nextThetaIndex ][ phiIndex ], t1 );
+			const v2 = MathUtils.lerp( iesLamp.candelaValues[ thetaIndex ][ phiIndex + 1 ], iesLamp.candelaValues[ nextThetaIndex ][ phiIndex + 1 ], t1 );
+			const v = MathUtils.lerp( v1, v2, t2 );
+
+			return v;
+
+		}
+
+		const startTheta = iesLamp.horAngles[ 0 ], endTheta = iesLamp.horAngles[ iesLamp.numHorAngles - 1 ];
+
+		for ( let i = 0; i < size; ++ i ) {
+
+			let theta = i % width;
+			const phi = Math.floor( i / width );
+
+			if ( endTheta - startTheta !== 0 && ( theta < startTheta || theta >= endTheta ) ) { // Handle symmetry for hor angles
+
+				theta %= endTheta * 2;
+
+				if ( theta > endTheta )
+					theta = endTheta * 2 - theta;
+
+			}
+
+			data[ phi + theta * height ] = interpolateCandelaValues( phi, theta );
+
+		}
+
+		let result = null;
+
+		if ( type === UnsignedByteType ) result = Uint8Array.from( data.map( v => Math.min( v * 0xFF, 0xFF ) ) );
+		else if ( type === HalfFloatType ) result = Uint16Array.from( data.map( v => DataUtils.toHalfFloat( v ) ) );
+		else if ( type === FloatType ) result = Float32Array.from( data );
+		else console.error( 'IESLoader: Unsupported type:', type );
+
+		return result;
+
+	}
+
+	load( url, onLoad, onProgress, onError ) {
+
+		const loader = new FileLoader( this.manager );
+		loader.setResponseType( 'text' );
+		loader.setCrossOrigin( this.crossOrigin );
+		loader.setWithCredentials( this.withCredentials );
+		loader.setPath( this.path );
+		loader.setRequestHeader( this.requestHeader );
+
+		loader.load( url, text => {
+
+			onLoad( this.parse( text ) );
+
+		}, onProgress, onError );
+
+	}
+
+	parse( text ) {
+
+		const type = this.type;
+
+		const iesLamp = new IESLamp( text );
+		const data = this._getIESValues( iesLamp, type );
+
+		const texture = new DataTexture( data, 180, 1, RedFormat, type );
+		texture.minFilter = LinearFilter;
+		texture.magFilter = LinearFilter;
+		texture.needsUpdate = true;
+
+		return texture;
+
+	}
+
+}
+
+
+function IESLamp( text ) {
+
+	const _self = this;
+
+	const textArray = text.split( '\n' );
+
+	let lineNumber = 0;
+	let line;
+
+	_self.verAngles = [ ];
+	_self.horAngles = [ ];
+
+	_self.candelaValues = [ ];
+
+	_self.tiltData = { };
+	_self.tiltData.angles = [ ];
+	_self.tiltData.mulFactors = [ ];
+
+	function textToArray( text ) {
+
+		text = text.replace( /^\s+|\s+$/g, '' ); // remove leading or trailing spaces
+		text = text.replace( /,/g, ' ' ); // replace commas with spaces
+		text = text.replace( /\s\s+/g, ' ' ); // replace white space/tabs etc by single whitespace
+
+		const array = text.split( ' ' );
+
+		return array;
+
+	}
+
+	function readArray( count, array ) {
+
+		while ( true ) {
+
+			const line = textArray[ lineNumber ++ ];
+			const lineData = textToArray( line );
+
+			for ( let i = 0; i < lineData.length; ++ i ) {
+
+				array.push( Number( lineData[ i ] ) );
+
+			}
+
+			if ( array.length === count )
+				break;
+
+		}
+
+	}
+
+	function readTilt() {
+
+		let line = textArray[ lineNumber ++ ];
+		let lineData = textToArray( line );
+
+		_self.tiltData.lampToLumGeometry = Number( lineData[ 0 ] );
+
+		line = textArray[ lineNumber ++ ];
+		lineData = textToArray( line );
+
+		_self.tiltData.numAngles = Number( lineData[ 0 ] );
+
+		readArray( _self.tiltData.numAngles, _self.tiltData.angles );
+		readArray( _self.tiltData.numAngles, _self.tiltData.mulFactors );
+
+	}
+
+	function readLampValues() {
+
+		const values = [ ];
+		readArray( 10, values );
+
+		_self.count = Number( values[ 0 ] );
+		_self.lumens = Number( values[ 1 ] );
+		_self.multiplier = Number( values[ 2 ] );
+		_self.numVerAngles = Number( values[ 3 ] );
+		_self.numHorAngles = Number( values[ 4 ] );
+		_self.gonioType = Number( values[ 5 ] );
+		_self.units = Number( values[ 6 ] );
+		_self.width = Number( values[ 7 ] );
+		_self.length = Number( values[ 8 ] );
+		_self.height = Number( values[ 9 ] );
+
+	}
+
+	function readLampFactors() {
+
+		const values = [ ];
+		readArray( 3, values );
+
+		_self.ballFactor = Number( values[ 0 ] );
+		_self.blpFactor = Number( values[ 1 ] );
+		_self.inputWatts = Number( values[ 2 ] );
+
+	}
+
+	while ( true ) {
+
+		line = textArray[ lineNumber ++ ];
+
+		if ( line.includes( 'TILT' ) ) {
+
+			break;
+
+		}
+
+	}
+
+	if ( ! line.includes( 'NONE' ) ) {
+
+		if ( line.includes( 'INCLUDE' ) ) {
+
+			readTilt();
+
+		} else {
+
+			// TODO:: Read tilt data from a file
+
+		}
+
+	}
+
+	readLampValues();
+
+	readLampFactors();
+
+	// Initialize candela value array
+	for ( let i = 0; i < _self.numHorAngles; ++ i ) {
+
+		_self.candelaValues.push( [ ] );
+
+	}
+
+	// Parse Angles
+	readArray( _self.numVerAngles, _self.verAngles );
+	readArray( _self.numHorAngles, _self.horAngles );
+
+	// Parse Candela values
+	for ( let i = 0; i < _self.numHorAngles; ++ i ) {
+
+		readArray( _self.numVerAngles, _self.candelaValues[ i ] );
+
+	}
+
+	// Calculate actual candela values, and normalize.
+	for ( let i = 0; i < _self.numHorAngles; ++ i ) {
+
+		for ( let j = 0; j < _self.numVerAngles; ++ j ) {
+
+			_self.candelaValues[ i ][ j ] *= _self.candelaValues[ i ][ j ] * _self.multiplier
+				* _self.ballFactor * _self.blpFactor;
+
+		}
+
+	}
+
+	let maxVal = - 1;
+	for ( let i = 0; i < _self.numHorAngles; ++ i ) {
+
+		for ( let j = 0; j < _self.numVerAngles; ++ j ) {
+
+			const value = _self.candelaValues[ i ][ j ];
+			maxVal = maxVal < value ? value : maxVal;
+
+		}
+
+	}
+
+	const bNormalize = true;
+	if ( bNormalize && maxVal > 0 ) {
+
+		for ( let i = 0; i < _self.numHorAngles; ++ i ) {
+
+			for ( let j = 0; j < _self.numVerAngles; ++ j ) {
+
+				_self.candelaValues[ i ][ j ] /= maxVal;
+
+			}
+
+		}
+
+	}
+
+}
+
+
+export { IESLoader };

+ 5 - 6
examples/jsm/loaders/MMDLoader.js

@@ -1134,7 +1134,7 @@ class MaterialBuilder {
 
 			if ( data.metadata.format === 'pmd' ) {
 
-				// map, envMap
+				// map, matcap
 
 				if ( material.fileName ) {
 
@@ -1142,7 +1142,7 @@ class MaterialBuilder {
 					const fileNames = fileName.split( '*' );
 
 					// fileNames[ 0 ]: mapFileName
-					// fileNames[ 1 ]: envMapFileName( optional )
+					// fileNames[ 1 ]: matcapFileName( optional )
 
 					params.map = this._loadTexture( fileNames[ 0 ], textures );
 
@@ -1150,12 +1150,12 @@ class MaterialBuilder {
 
 						const extension = fileNames[ 1 ].slice( - 4 ).toLowerCase();
 
-						params.envMap = this._loadTexture(
+						params.matcap = this._loadTexture(
 							fileNames[ 1 ],
 							textures
 						);
 
-						params.combine = extension === '.sph'
+						params.matcapCombine = extension === '.sph'
 							? MultiplyOperation
 							: AddOperation;
 
@@ -1202,7 +1202,7 @@ class MaterialBuilder {
 
 				}
 
-				// envMap TODO: support m.envFlag === 3
+				// matcap TODO: support m.envFlag === 3
 
 				if ( material.envTextureIndex !== - 1 && ( material.envFlag === 1 || material.envFlag == 2 ) ) {
 
@@ -2161,7 +2161,6 @@ class MMDToonMaterial extends ShaderMaterial {
 
 			'alphaMap',
 
-			'envMap',
 			'reflectivity',
 			'refractionRatio',
 		];

+ 9 - 3
examples/jsm/loaders/MaterialXLoader.js

@@ -340,7 +340,11 @@ class MaterialXNode {
 
 		let node = this.node;
 
-		if ( node !== null ) { return node; }
+		if ( node !== null ) {
+
+			return node;
+
+		}
 
 		//
 
@@ -478,7 +482,9 @@ class MaterialXNode {
 
 	getNodeByName( name ) {
 
-		return this.getChildByName( name )?.getNode();
+		const child = this.getChildByName( name );
+
+		return child ? child.getNode() : undefined;
 
 	}
 
@@ -681,7 +687,7 @@ class MaterialX {
 
 	}
 
-    /*getMaterialXNodeFromXML( xmlNode ) {
+	/*getMaterialXNodeFromXML( xmlNode ) {
 
         return this.nodesXRefLib.get( xmlNode );
 

+ 1 - 2
examples/jsm/loaders/PCDLoader.js

@@ -4,7 +4,6 @@ import {
 	Float32BufferAttribute,
 	Int32BufferAttribute,
 	Loader,
-	LoaderUtils,
 	Points,
 	PointsMaterial
 } from 'three';
@@ -219,7 +218,7 @@ class PCDLoader extends Loader {
 
 		}
 
-		const textData = LoaderUtils.decodeText( new Uint8Array( data ) );
+		const textData = new TextDecoder().decode( data );
 
 		// parse header (always ascii format)
 

+ 197 - 77
examples/jsm/loaders/PLYLoader.js

@@ -3,7 +3,6 @@ import {
 	FileLoader,
 	Float32BufferAttribute,
 	Loader,
-	LoaderUtils,
 	Color
 } from 'three';
 
@@ -298,6 +297,44 @@ class PLYLoader extends Loader {
 
 		}
 
+		function mapElementAttributes( properties ) {
+
+			const elementNames = properties.map( property => {
+
+				return property.name;
+
+			} );
+
+			function findAttrName( names ) {
+
+				for ( let i = 0, l = names.length; i < l; i ++ ) {
+
+					const name = names[ i ];
+
+					if ( elementNames.includes( name ) ) return name;
+
+				}
+
+				return null;
+
+			}
+
+			return {
+				attrX: findAttrName( [ 'x', 'px', 'posx' ] ) || 'x',
+				attrY: findAttrName( [ 'y', 'py', 'posy' ] ) || 'y',
+				attrZ: findAttrName( [ 'z', 'pz', 'posz' ] ) || 'z',
+				attrNX: findAttrName( [ 'nx', 'normalx' ] ),
+				attrNY: findAttrName( [ 'ny', 'normaly' ] ),
+				attrNZ: findAttrName( [ 'nz', 'normalz' ] ),
+				attrS: findAttrName( [ 's', 'u', 'texture_u', 'tx' ] ),
+				attrT: findAttrName( [ 't', 'v', 'texture_v', 'ty' ] ),
+				attrR: findAttrName( [ 'red', 'diffuse_red', 'r', 'diffuse_r' ] ),
+				attrG: findAttrName( [ 'green', 'diffuse_green', 'g', 'diffuse_g' ] ),
+				attrB: findAttrName( [ 'blue', 'diffuse_blue', 'b', 'diffuse_b' ] ),
+			};
+
+		}
+
 		function parseASCII( data, header ) {
 
 			// PLY ascii format specification, as per http://en.wikipedia.org/wiki/PLY_(file_format)
@@ -317,6 +354,8 @@ class PLYLoader extends Loader {
 			const lines = body.split( /\r\n|\r|\n/ );
 			let currentElement = 0;
 			let currentElementCount = 0;
+			let elementDesc = header.elements[ currentElement ];
+			let attributeMap = mapElementAttributes( elementDesc.properties );
 
 			for ( let i = 0; i < lines.length; i ++ ) {
 
@@ -328,16 +367,19 @@ class PLYLoader extends Loader {
 
 				}
 
-				if ( currentElementCount >= header.elements[ currentElement ].count ) {
+				if ( currentElementCount >= elementDesc.count ) {
 
 					currentElement ++;
 					currentElementCount = 0;
+					elementDesc = header.elements[ currentElement ];
+
+					attributeMap = mapElementAttributes( elementDesc.properties );
 
 				}
 
-				const element = parseASCIIElement( header.elements[ currentElement ].properties, line );
+				const element = parseASCIIElement( elementDesc.properties, line );
 
-				handleElement( buffer, header.elements[ currentElement ].name, element );
+				handleElement( buffer, elementDesc.name, element, attributeMap );
 
 				currentElementCount ++;
 
@@ -412,56 +454,30 @@ class PLYLoader extends Loader {
 
 		}
 
-		function handleElement( buffer, elementName, element ) {
-
-			function findAttrName( names ) {
-
-				for ( let i = 0, l = names.length; i < l; i ++ ) {
-
-					const name = names[ i ];
-
-					if ( name in element ) return name;
-
-				}
-
-				return null;
-
-			}
-
-			const attrX = findAttrName( [ 'x', 'px', 'posx' ] ) || 'x';
-			const attrY = findAttrName( [ 'y', 'py', 'posy' ] ) || 'y';
-			const attrZ = findAttrName( [ 'z', 'pz', 'posz' ] ) || 'z';
-			const attrNX = findAttrName( [ 'nx', 'normalx' ] );
-			const attrNY = findAttrName( [ 'ny', 'normaly' ] );
-			const attrNZ = findAttrName( [ 'nz', 'normalz' ] );
-			const attrS = findAttrName( [ 's', 'u', 'texture_u', 'tx' ] );
-			const attrT = findAttrName( [ 't', 'v', 'texture_v', 'ty' ] );
-			const attrR = findAttrName( [ 'red', 'diffuse_red', 'r', 'diffuse_r' ] );
-			const attrG = findAttrName( [ 'green', 'diffuse_green', 'g', 'diffuse_g' ] );
-			const attrB = findAttrName( [ 'blue', 'diffuse_blue', 'b', 'diffuse_b' ] );
+		function handleElement( buffer, elementName, element, cacheEntry ) {
 
 			if ( elementName === 'vertex' ) {
 
-				buffer.vertices.push( element[ attrX ], element[ attrY ], element[ attrZ ] );
+				buffer.vertices.push( element[ cacheEntry.attrX ], element[ cacheEntry.attrY ], element[ cacheEntry.attrZ ] );
 
-				if ( attrNX !== null && attrNY !== null && attrNZ !== null ) {
+				if ( cacheEntry.attrNX !== null && cacheEntry.attrNY !== null && cacheEntry.attrNZ !== null ) {
 
-					buffer.normals.push( element[ attrNX ], element[ attrNY ], element[ attrNZ ] );
+					buffer.normals.push( element[ cacheEntry.attrNX ], element[ cacheEntry.attrNY ], element[ cacheEntry.attrNZ ] );
 
 				}
 
-				if ( attrS !== null && attrT !== null ) {
+				if ( cacheEntry.attrS !== null && cacheEntry.attrT !== null ) {
 
-					buffer.uvs.push( element[ attrS ], element[ attrT ] );
+					buffer.uvs.push( element[ cacheEntry.attrS ], element[ cacheEntry.attrT ] );
 
 				}
 
-				if ( attrR !== null && attrG !== null && attrB !== null ) {
+				if ( cacheEntry.attrR !== null && cacheEntry.attrG !== null && cacheEntry.attrB !== null ) {
 
 					_color.setRGB(
-						element[ attrR ] / 255.0,
-						element[ attrG ] / 255.0,
-						element[ attrB ] / 255.0
+						element[ cacheEntry.attrR ] / 255.0,
+						element[ cacheEntry.attrG ] / 255.0,
+						element[ cacheEntry.attrB ] / 255.0
 					).convertSRGBToLinear();
 
 					buffer.colors.push( _color.r, _color.g, _color.b );
@@ -506,54 +522,36 @@ class PLYLoader extends Loader {
 
 		}
 
-		function binaryRead( dataview, at, type, little_endian ) {
-
-			switch ( type ) {
-
-				// corespondences for non-specific length types here match rply:
-				case 'int8':		case 'char':	 return [ dataview.getInt8( at ), 1 ];
-				case 'uint8':		case 'uchar':	 return [ dataview.getUint8( at ), 1 ];
-				case 'int16':		case 'short':	 return [ dataview.getInt16( at, little_endian ), 2 ];
-				case 'uint16':	case 'ushort': return [ dataview.getUint16( at, little_endian ), 2 ];
-				case 'int32':		case 'int':		 return [ dataview.getInt32( at, little_endian ), 4 ];
-				case 'uint32':	case 'uint':	 return [ dataview.getUint32( at, little_endian ), 4 ];
-				case 'float32': case 'float':	 return [ dataview.getFloat32( at, little_endian ), 4 ];
-				case 'float64': case 'double': return [ dataview.getFloat64( at, little_endian ), 8 ];
-
-			}
-
-		}
-
-		function binaryReadElement( dataview, at, properties, little_endian ) {
+		function binaryReadElement( at, properties ) {
 
 			const element = {};
-			let result, read = 0;
+			let read = 0;
 
 			for ( let i = 0; i < properties.length; i ++ ) {
 
-				if ( properties[ i ].type === 'list' ) {
+				const property = properties[ i ];
+				const valueReader = property.valueReader;
+
+				if ( property.type === 'list' ) {
 
 					const list = [];
 
-					result = binaryRead( dataview, at + read, properties[ i ].countType, little_endian );
-					const n = result[ 0 ];
-					read += result[ 1 ];
+					const n = property.countReader.read( at + read );
+					read += property.countReader.size;
 
 					for ( let j = 0; j < n; j ++ ) {
 
-						result = binaryRead( dataview, at + read, properties[ i ].itemType, little_endian );
-						list.push( result[ 0 ] );
-						read += result[ 1 ];
+						list.push( valueReader.read( at + read ) );
+						read += valueReader.size;
 
 					}
 
-					element[ properties[ i ].name ] = list;
+					element[ property.name ] = list;
 
 				} else {
 
-					result = binaryRead( dataview, at + read, properties[ i ].type, little_endian );
-					element[ properties[ i ].name ] = result[ 0 ];
-					read += result[ 1 ];
+					element[ property.name ] = valueReader.read( at + read );
+					read += valueReader.size;
 
 				}
 
@@ -563,6 +561,77 @@ class PLYLoader extends Loader {
 
 		}
 
+		function setPropertyBinaryReaders( properties, body, little_endian ) {
+
+			function getBinaryReader( dataview, type, little_endian ) {
+
+				switch ( type ) {
+
+					// corespondences for non-specific length types here match rply:
+					case 'int8':	case 'char':	return { read: ( at ) => {
+
+						return dataview.getInt8( at );
+
+					}, size: 1 };
+					case 'uint8':	case 'uchar':	return { read: ( at ) => {
+
+						return dataview.getUint8( at );
+
+					}, size: 1 };
+					case 'int16':	case 'short':	return { read: ( at ) => {
+
+						return dataview.getInt16( at, little_endian );
+
+					}, size: 2 };
+					case 'uint16':	case 'ushort':	return { read: ( at ) => {
+
+						return dataview.getUint16( at, little_endian );
+
+					}, size: 2 };
+					case 'int32':	case 'int':		return { read: ( at ) => {
+
+						return dataview.getInt32( at, little_endian );
+
+					}, size: 4 };
+					case 'uint32':	case 'uint':	return { read: ( at ) => {
+
+						return dataview.getUint32( at, little_endian );
+
+					}, size: 4 };
+					case 'float32': case 'float':	return { read: ( at ) => {
+
+						return dataview.getFloat32( at, little_endian );
+
+					}, size: 4 };
+					case 'float64': case 'double':	return { read: ( at ) => {
+
+						return dataview.getFloat64( at, little_endian );
+
+					}, size: 8 };
+
+				}
+
+			}
+
+			for ( let i = 0, l = properties.length; i < l; i ++ ) {
+
+				const property = properties[ i ];
+
+				if ( property.type === 'list' ) {
+
+					property.countReader = getBinaryReader( body, property.countType, little_endian );
+					property.valueReader = getBinaryReader( body, property.itemType, little_endian );
+
+				} else {
+
+					property.valueReader = getBinaryReader( body, property.type, little_endian );
+
+				}
+
+			}
+
+		}
+
 		function parseBinary( data, header ) {
 
 			const buffer = createBuffer();
@@ -573,13 +642,19 @@ class PLYLoader extends Loader {
 
 			for ( let currentElement = 0; currentElement < header.elements.length; currentElement ++ ) {
 
-				for ( let currentElementCount = 0; currentElementCount < header.elements[ currentElement ].count; currentElementCount ++ ) {
+				const elementDesc = header.elements[ currentElement ];
+				const properties = elementDesc.properties;
+				const attributeMap = mapElementAttributes( properties );
+
+				setPropertyBinaryReaders( properties, body, little_endian );
 
-					result = binaryReadElement( body, loc, header.elements[ currentElement ].properties, little_endian );
+				for ( let currentElementCount = 0; currentElementCount < elementDesc.count; currentElementCount ++ ) {
+
+					result = binaryReadElement( loc, properties );
 					loc += result[ 1 ];
 					const element = result[ 0 ];
 
-					handleElement( buffer, header.elements[ currentElement ].name, element );
+					handleElement( buffer, elementDesc.name, element, attributeMap );
 
 				}
 
@@ -589,6 +664,40 @@ class PLYLoader extends Loader {
 
 		}
 
+		function extractHeaderText( bytes ) {
+
+			let i = 0;
+			let cont = true;
+
+			let line = '';
+			const lines = [];
+
+			do {
+
+				const c = String.fromCharCode( bytes[ i ++ ] );
+
+				if ( c !== '\n' && c !== '\r' ) {
+
+					line += c;
+
+				} else {
+
+					if ( line === 'end_header' ) cont = false;
+					if ( line !== '' ) {
+
+						lines.push( line );
+						line = '';
+
+					}
+
+				}
+
+			} while ( cont && i < bytes.length );
+
+			return lines.join( '\r' ) + '\r';
+
+		}
+
 		//
 
 		let geometry;
@@ -596,10 +705,21 @@ class PLYLoader extends Loader {
 
 		if ( data instanceof ArrayBuffer ) {
 
-			const text = LoaderUtils.decodeText( new Uint8Array( data ) );
-			const header = parseHeader( text );
+			const bytes = new Uint8Array( data );
+			const headerText = extractHeaderText( bytes );
+			const header = parseHeader( headerText );
+
+			if ( header.format === 'ascii' ) {
+
+				const text = new TextDecoder().decode( bytes );
+
+				geometry = parseASCII( text, header );
 
-			geometry = header.format === 'ascii' ? parseASCII( text, header ) : parseBinary( data, header );
+			} else {
+
+				geometry = parseBinary( data, header );
+
+			}
 
 		} else {
 

+ 1 - 2
examples/jsm/loaders/STLLoader.js

@@ -4,7 +4,6 @@ import {
 	FileLoader,
 	Float32BufferAttribute,
 	Loader,
-	LoaderUtils,
 	Vector3
 } from 'three';
 
@@ -357,7 +356,7 @@ class STLLoader extends Loader {
 
 			if ( typeof buffer !== 'string' ) {
 
-				return LoaderUtils.decodeText( new Uint8Array( buffer ) );
+				return new TextDecoder().decode( buffer );
 
 			}
 

+ 24 - 24
examples/jsm/loaders/SVGLoader.js

@@ -866,7 +866,7 @@ class SVGLoader extends Loader {
 
 			}
 
-			const regex = /(-?[\d\.?]+)[,|\s](-?[\d\.?]+)/g;
+			const regex = /([+-]?\d*\.?\d+(?:e[+-]?\d+)?)(?:,|\s)([+-]?\d*\.?\d+(?:e[+-]?\d+)?)/g;
 
 			const path = new ShapePath();
 
@@ -901,7 +901,7 @@ class SVGLoader extends Loader {
 
 			}
 
-			const regex = /(-?[\d\.?]+)[,|\s](-?[\d\.?]+)/g;
+			const regex = /([+-]?\d*\.?\d+(?:e[+-]?\d+)?)(?:,|\s)([+-]?\d*\.?\d+(?:e[+-]?\d+)?)/g;
 
 			const path = new ShapePath();
 
@@ -1600,7 +1600,7 @@ class SVGLoader extends Loader {
 				const sinTheta = Math.sin( curve.aRotation );
 
 				const v1 = new Vector3( a * cosTheta, a * sinTheta, 0 );
-				const v2 = new Vector3( -b * sinTheta, b * cosTheta, 0 );
+				const v2 = new Vector3( - b * sinTheta, b * cosTheta, 0 );
 
 				const f1 = v1.applyMatrix3( m );
 				const f2 = v2.applyMatrix3( m );
@@ -1608,7 +1608,7 @@ class SVGLoader extends Loader {
 				const mF = tempTransform0.set(
 					f1.x, f2.x, 0,
 					f1.y, f2.y, 0,
-					0,    0,    1,
+					0, 0, 1,
 				);
 
 				const mFInv = tempTransform1.copy( mF ).invert();
@@ -1616,7 +1616,7 @@ class SVGLoader extends Loader {
 				const mQ = mFInvT.multiply( mFInv );
 				const mQe = mQ.elements;
 
-				const ed = eigenDecomposition( mQe[0], mQe[1], mQe[4] );
+				const ed = eigenDecomposition( mQe[ 0 ], mQe[ 1 ], mQe[ 4 ] );
 				const rt1sqrt = Math.sqrt( ed.rt1 );
 				const rt2sqrt = Math.sqrt( ed.rt2 );
 
@@ -1630,18 +1630,18 @@ class SVGLoader extends Loader {
 				// Do not touch angles of a full ellipse because after transformation they
 				// would converge to a sinle value effectively removing the whole curve
 
-				if ( !isFullEllipse ) {
+				if ( ! isFullEllipse ) {
 
 					const mDsqrt = tempTransform1.set(
 						rt1sqrt, 0, 0,
 						0, rt2sqrt, 0,
-						0, 0,       1,
+						0, 0, 1,
 					);
 
 					const mRT = tempTransform2.set(
-						ed.cs,  ed.sn, 0,
-						-ed.sn, ed.cs, 0,
-						0,      0,     1,
+						ed.cs, ed.sn, 0,
+						- ed.sn, ed.cs, 0,
+						0, 0, 1,
 					);
 
 					const mDRF = mDsqrt.multiply( mRT ).multiply( mF );
@@ -1660,7 +1660,7 @@ class SVGLoader extends Loader {
 
 					if ( isTransformFlipped( m ) ) {
 
-						curve.aClockwise = !curve.aClockwise;
+						curve.aClockwise = ! curve.aClockwise;
 
 					}
 
@@ -1688,16 +1688,16 @@ class SVGLoader extends Loader {
 				// `sx`, `sy`, or both might be zero.
 				const theta =
 					sx > Number.EPSILON
-					? Math.atan2( m.elements[ 1 ], m.elements[ 0 ] )
-					: Math.atan2( - m.elements[ 3 ], m.elements[ 4 ] );
+						? Math.atan2( m.elements[ 1 ], m.elements[ 0 ] )
+						: Math.atan2( - m.elements[ 3 ], m.elements[ 4 ] );
 
 				curve.aRotation += theta;
 
 				if ( isTransformFlipped( m ) ) {
 
-					curve.aStartAngle *= -1;
-					curve.aEndAngle *= -1;
-					curve.aClockwise = !curve.aClockwise;
+					curve.aStartAngle *= - 1;
+					curve.aEndAngle *= - 1;
+					curve.aClockwise = ! curve.aClockwise;
 
 				}
 
@@ -1829,7 +1829,7 @@ class SVGLoader extends Loader {
 				// This case needs to be treated separately to avoid div by 0
 
 				rt1 = 0.5 * rt;
-				rt2 = -0.5 * rt;
+				rt2 = - 0.5 * rt;
 
 			}
 
@@ -1847,7 +1847,7 @@ class SVGLoader extends Loader {
 
 			if ( Math.abs( cs ) > 2 * Math.abs( B ) ) {
 
-				t = -2 * B / cs;
+				t = - 2 * B / cs;
 				sn = 1 / Math.sqrt( 1 + t * t );
 				cs = t * sn;
 
@@ -1858,7 +1858,7 @@ class SVGLoader extends Loader {
 
 			} else {
 
-				t = -0.5 * cs / B;
+				t = - 0.5 * cs / B;
 				cs = 1 / Math.sqrt( 1 + t * t );
 				sn = t * cs;
 
@@ -1867,7 +1867,7 @@ class SVGLoader extends Loader {
 			if ( df > 0 ) {
 
 				t = cs;
-				cs = -sn;
+				cs = - sn;
 				sn = t;
 
 			}
@@ -2311,20 +2311,20 @@ class SVGLoader extends Loader {
 
 			}
 
-			return { curves: p.curves, points: points, isCW: ShapeUtils.isClockWise( points ), identifier: -1, boundingBox: new Box2( new Vector2( minX, minY ), new Vector2( maxX, maxY ) ) };
+			return { curves: p.curves, points: points, isCW: ShapeUtils.isClockWise( points ), identifier: - 1, boundingBox: new Box2( new Vector2( minX, minY ), new Vector2( maxX, maxY ) ) };
 
 		} );
 
 		simplePaths = simplePaths.filter( sp => sp.points.length > 1 );
 
-		for ( let identifier = 0; identifier < simplePaths.length; identifier++ ) {
+		for ( let identifier = 0; identifier < simplePaths.length; identifier ++ ) {
 
-			simplePaths[identifier].identifier = identifier;
+			simplePaths[ identifier ].identifier = identifier;
 
 		}
 
 		// check if path is solid or a hole
-		const isAHole = simplePaths.map( p => isHoleTo( p, simplePaths, scanlineMinX, scanlineMaxX, shapePath.userData?.style.fillRule ) );
+		const isAHole = simplePaths.map( p => isHoleTo( p, simplePaths, scanlineMinX, scanlineMaxX, ( shapePath.userData ? shapePath.userData.style.fillRule : undefined ) ) );
 
 
 		const shapesToReturn = [];

+ 1 - 1
examples/jsm/loaders/USDZLoader.js

@@ -254,7 +254,7 @@ class USDZLoader extends Loader {
 
 			if ( id !== undefined ) {
 
-				const def = `def "%{id}"`;
+				const def = `def "${id}"`;
 
 				if ( def in data ) {
 

+ 6 - 5
examples/jsm/loaders/VTKLoader.js

@@ -3,8 +3,7 @@ import {
 	BufferGeometry,
 	FileLoader,
 	Float32BufferAttribute,
-	Loader,
-	LoaderUtils
+	Loader
 } from 'three';
 import * as fflate from '../libs/fflate.module.js';
 
@@ -1130,16 +1129,18 @@ class VTKLoader extends Loader {
 
 		}
 
+		const textDecoder = new TextDecoder();
+
 		// get the 5 first lines of the files to check if there is the key word binary
-		const meta = LoaderUtils.decodeText( new Uint8Array( data, 0, 250 ) ).split( '\n' );
+		const meta = textDecoder.decode( new Uint8Array( data, 0, 250 ) ).split( '\n' );
 
 		if ( meta[ 0 ].indexOf( 'xml' ) !== - 1 ) {
 
-			return parseXML( LoaderUtils.decodeText( data ) );
+			return parseXML( textDecoder.decode( data ) );
 
 		} else if ( meta[ 2 ].includes( 'ASCII' ) ) {
 
-			return parseASCII( LoaderUtils.decodeText( data ) );
+			return parseASCII( textDecoder.decode( data ) );
 
 		} else {
 

+ 19 - 19
examples/jsm/loaders/lwo/IFFParser.js

@@ -32,7 +32,6 @@
  *
  **/
 
-import { LoaderUtils } from 'three';
 import { LWO2Parser } from './LWO2Parser.js';
 import { LWO3Parser } from './LWO3Parser.js';
 
@@ -908,6 +907,8 @@ function DataViewReader( buffer ) {
 
 	this.dv = new DataView( buffer );
 	this.offset = 0;
+	this._textDecoder = new TextDecoder();
+	this._bytes = new Uint8Array( buffer );
 
 }
 
@@ -1065,35 +1066,34 @@ DataViewReader.prototype = {
 
 		if ( size === 0 ) return;
 
-		// note: safari 9 doesn't support Uint8Array.indexOf; create intermediate array instead
-		var a = [];
-
-		if ( size ) {
+		const start = this.offset;
 
-			for ( var i = 0; i < size; i ++ ) {
+		let result;
+		let length;
 
-				a[ i ] = this.getUint8();
+		if ( size ) {
 
-			}
+			length = size;
+			result = this._textDecoder.decode( new Uint8Array( this.dv.buffer, start, size ) );
 
 		} else {
 
-			var currentChar;
-			var len = 0;
-
-			while ( currentChar !== 0 ) {
+			// use 1:1 mapping of buffer to avoid redundant new array creation.
+			length = this._bytes.indexOf( 0, start ) - start;
 
-				currentChar = this.getUint8();
-				if ( currentChar !== 0 ) a.push( currentChar );
-				len ++;
+			result = this._textDecoder.decode( new Uint8Array( this.dv.buffer, start, length ) );
 
-			}
+			// account for null byte in length
+			length ++;
 
-			if ( ! isEven( len + 1 ) ) this.getUint8(); // if string with terminating nullbyte is uneven, extra nullbyte is added
+			// if string with terminating nullbyte is uneven, extra nullbyte is added, skip that too
+			length += length % 2;
 
 		}
 
-		return LoaderUtils.decodeText( new Uint8Array( a ) );
+		this.skip( length );
+
+		return result;
 
 	},
 
@@ -1211,7 +1211,7 @@ function stringOffset( string ) {
 // printBuffer( this.reader.dv.buffer, this.reader.offset, length );
 function printBuffer( buffer, from, to ) {
 
-	console.log( LoaderUtils.decodeText( new Uint8Array( buffer, from, to ) ) );
+	console.log( new TextDecoder().decode( new Uint8Array( buffer, from, to ) ) );
 
 }
 

+ 1 - 1
examples/jsm/misc/GPUComputationRenderer.js

@@ -301,7 +301,7 @@ class GPUComputationRenderer {
 
 				const variable = variables[ i ];
 
-				variable.initialValueTexture?.dispose();
+				if ( variable.initialValueTexture ) variable.initialValueTexture.dispose();
 
 				const renderTargets = variable.renderTargets;
 

+ 9 - 0
examples/jsm/nodes/Nodes.js

@@ -10,6 +10,7 @@ import ExpressionNode from './core/ExpressionNode.js';
 import FunctionCallNode from './core/FunctionCallNode.js';
 import FunctionNode from './core/FunctionNode.js';
 import InstanceIndexNode from './core/InstanceIndexNode.js';
+import LightingModel from './core/LightingModel.js';
 import Node from './core/Node.js';
 import NodeAttribute from './core/NodeAttribute.js';
 import NodeBuilder from './core/NodeBuilder.js';
@@ -75,6 +76,7 @@ import CondNode from './math/CondNode.js';
 import PointLightNode from './lighting/PointLightNode.js';
 import DirectionalLightNode from './lighting/DirectionalLightNode.js';
 import SpotLightNode from './lighting/SpotLightNode.js';
+import IESSpotLightNode from './lighting/IESSpotLightNode.js';
 import AmbientLightNode from './lighting/AmbientLightNode.js';
 import LightsNode from './lighting/LightsNode.js';
 import LightingNode from './lighting/LightingNode.js';
@@ -87,6 +89,7 @@ import AnalyticLightNode from './lighting/AnalyticLightNode.js';
 // utils
 import ArrayElementNode from './utils/ArrayElementNode.js';
 import ConvertNode from './utils/ConvertNode.js';
+import DiscardNode from './utils/DiscardNode.js';
 import EquirectUVNode from './utils/EquirectUVNode.js';
 import JoinNode from './utils/JoinNode.js';
 import MatcapUVNode from './utils/MatcapUVNode.js';
@@ -145,6 +148,7 @@ const nodeLib = {
 	FunctionCallNode,
 	FunctionNode,
 	InstanceIndexNode,
+	LightingModel,
 	Node,
 	NodeAttribute,
 	NodeBuilder,
@@ -210,6 +214,7 @@ const nodeLib = {
 	PointLightNode,
 	DirectionalLightNode,
 	SpotLightNode,
+	IESSpotLightNode,
 	AmbientLightNode,
 	LightsNode,
 	LightingNode,
@@ -222,6 +227,7 @@ const nodeLib = {
 	// utils
 	ArrayElementNode,
 	ConvertNode,
+	DiscardNode,
 	EquirectUVNode,
 	JoinNode,
 	MatcapUVNode,
@@ -273,6 +279,7 @@ export {
 	FunctionCallNode,
 	FunctionNode,
 	InstanceIndexNode,
+	LightingModel,
 	Node,
 	NodeAttribute,
 	NodeBuilder,
@@ -338,6 +345,7 @@ export {
 	PointLightNode,
 	DirectionalLightNode,
 	SpotLightNode,
+	IESSpotLightNode,
 	AmbientLightNode,
 	LightsNode,
 	LightingNode,
@@ -350,6 +358,7 @@ export {
 	// utils
 	ArrayElementNode,
 	ConvertNode,
+	DiscardNode,
 	EquirectUVNode,
 	JoinNode,
 	MatcapUVNode,

+ 5 - 5
examples/jsm/nodes/accessors/BitangentNode.js

@@ -8,11 +8,6 @@ import TangentNode from './TangentNode.js';
 
 class BitangentNode extends Node {
 
-	static GEOMETRY = 'geometry';
-	static LOCAL = 'local';
-	static VIEW = 'view';
-	static WORLD = 'world';
-
 	constructor( scope = BitangentNode.LOCAL ) {
 
 		super( 'vec3' );
@@ -59,4 +54,9 @@ class BitangentNode extends Node {
 
 }
 
+BitangentNode.GEOMETRY = 'geometry';
+BitangentNode.LOCAL = 'local';
+BitangentNode.VIEW = 'view';
+BitangentNode.WORLD = 'world';
+
 export default BitangentNode;

+ 2 - 2
examples/jsm/nodes/accessors/CameraNode.js

@@ -2,8 +2,6 @@ import Object3DNode from './Object3DNode.js';
 
 class CameraNode extends Object3DNode {
 
-	static PROJECTION_MATRIX = 'projectionMatrix';
-
 	constructor( scope = CameraNode.POSITION ) {
 
 		super( scope );
@@ -64,4 +62,6 @@ class CameraNode extends Object3DNode {
 
 }
 
+CameraNode.PROJECTION_MATRIX = 'projectionMatrix';
+
 export default CameraNode;

+ 4 - 6
examples/jsm/nodes/accessors/CubeTextureNode.js

@@ -2,7 +2,7 @@ import TextureNode from './TextureNode.js';
 import UniformNode from '../core/UniformNode.js';
 import ReflectVectorNode from './ReflectVectorNode.js';
 
-import { negate, vec3, nodeObject } from '../shadernode/ShaderNodeBaseElements.js';
+import { vec3, nodeObject } from '../shadernode/ShaderNodeBaseElements.js';
 
 let defaultUV;
 
@@ -24,9 +24,7 @@ class CubeTextureNode extends TextureNode {
 
 	getDefaultUV() {
 
-		defaultUV ||= new ReflectVectorNode();
-
-		return defaultUV;
+		return defaultUV || ( defaultUV = new ReflectVectorNode() );
 
 	}
 
@@ -61,7 +59,7 @@ class CubeTextureNode extends TextureNode {
 			if ( propertyName === undefined ) {
 
 				const uvNodeObject = nodeObject( uvNode );
-				const cubeUV = vec3( negate( uvNodeObject.x ), uvNodeObject.yz );
+				const cubeUV = vec3( uvNodeObject.x.negate(), uvNodeObject.yz );
 				const uvSnippet = cubeUV.build( builder, 'vec3' );
 
 				const nodeVar = builder.getVarFromNode( this, 'vec4' );
@@ -70,7 +68,7 @@ class CubeTextureNode extends TextureNode {
 
 				let snippet = null;
 
-				if ( levelNode?.isNode === true) {
+				if ( levelNode && levelNode.isNode === true ) {
 
 					const levelSnippet = levelNode.build( builder, 'float' );
 

+ 52 - 0
examples/jsm/nodes/accessors/ExtendedMaterialNode.js

@@ -0,0 +1,52 @@
+import MaterialNode from './MaterialNode.js';
+import NormalMapNode from '../display/NormalMapNode.js';
+
+import {
+	normalView, materialReference
+} from '../shadernode/ShaderNodeElements.js';
+
+class ExtendedMaterialNode extends MaterialNode {
+
+	constructor( scope ) {
+
+		super( scope );
+
+	}
+
+	getNodeType( builder ) {
+
+		const scope = this.scope;
+		let type = null;
+
+		if ( scope === ExtendedMaterialNode.NORMAL ) {
+
+			type = 'vec3';
+
+		}
+
+		return type || super.getNodeType( builder );
+
+	}
+
+	construct( builder ) {
+
+		const material = builder.material;
+		const scope = this.scope;
+
+		let node = null;
+
+		if ( scope === ExtendedMaterialNode.NORMAL ) {
+
+			node = material.normalMap ? new NormalMapNode( this.getTexture( 'normalMap' ), materialReference( 'normalScale', 'vec2' ) ) : normalView;
+
+		}
+
+		return node || super.construct( builder );
+
+	}
+
+}
+
+ExtendedMaterialNode.NORMAL = 'normal';
+
+export default ExtendedMaterialNode;

+ 136 - 31
examples/jsm/nodes/accessors/MaterialNode.js

@@ -1,20 +1,15 @@
 import Node from '../core/Node.js';
+import UniformNode from '../core/UniformNode.js';
+import UVNode from '../accessors/UVNode.js';
+import ConstNode from '../core/ConstNode.js';
 import OperatorNode from '../math/OperatorNode.js';
+import JoinNode from '../utils/JoinNode.js';
 import MaterialReferenceNode from './MaterialReferenceNode.js';
-import TextureNode from './TextureNode.js';
 import SplitNode from '../utils/SplitNode.js';
 
 class MaterialNode extends Node {
 
-	static ALPHA_TEST = 'alphaTest';
-	static COLOR = 'color';
-	static OPACITY = 'opacity';
-	static ROUGHNESS = 'roughness';
-	static METALNESS = 'metalness';
-	static EMISSIVE = 'emissive';
-	static ROTATION = 'rotation';
-
-	constructor( scope = MaterialNode.COLOR ) {
+	constructor( scope ) {
 
 		super();
 
@@ -35,11 +30,15 @@ class MaterialNode extends Node {
 
 			return 'float';
 
+		} else if ( scope === MaterialNode.UV ) {
+
+			return 'vec2';
+
 		} else if ( scope === MaterialNode.EMISSIVE ) {
 
 			return 'vec3';
 
-		} else if ( scope === MaterialNode.ROUGHNESS || scope === MaterialNode.METALNESS ) {
+		} else if ( scope === MaterialNode.ROUGHNESS || scope === MaterialNode.METALNESS || scope === MaterialNode.SPECULAR || scope === MaterialNode.SHININESS ) {
 
 			return 'float';
 
@@ -47,6 +46,33 @@ class MaterialNode extends Node {
 
 	}
 
+	getFloat( property ) {
+
+		//@TODO: Check if it can be cached by property name.
+
+		return new MaterialReferenceNode( property, 'float' );
+
+	}
+
+	getColor( property ) {
+
+		//@TODO: Check if it can be cached by property name.
+
+		return new MaterialReferenceNode( property, 'color' );
+
+	}
+
+	getTexture( property ) {
+
+		//@TODO: Check if it can be cached by property name.
+
+		const textureRefNode = new MaterialReferenceNode( property, 'texture' );
+		textureRefNode.node.uvNode = new MaterialNode( MaterialNode.UV );
+
+		return textureRefNode;
+
+	}
+
 	construct( builder ) {
 
 		const material = builder.context.material;
@@ -56,18 +82,15 @@ class MaterialNode extends Node {
 
 		if ( scope === MaterialNode.ALPHA_TEST ) {
 
-			node = new MaterialReferenceNode( 'alphaTest', 'float' );
+			node = this.getFloat( 'alphaTest' );
 
 		} else if ( scope === MaterialNode.COLOR ) {
 
-			const colorNode = new MaterialReferenceNode( 'color', 'color' );
-
-			if ( material.map?.isTexture === true ) {
+			const colorNode = this.getColor( 'color' );
 
-				//new MaterialReferenceNode( 'map', 'texture' )
-				const map = new TextureNode( material.map );
+			if ( material.map && material.map.isTexture === true ) {
 
-				node = new OperatorNode( '*', colorNode, map );
+				node = new OperatorNode( '*', colorNode, this.getTexture( 'map' ) );
 
 			} else {
 
@@ -77,11 +100,11 @@ class MaterialNode extends Node {
 
 		} else if ( scope === MaterialNode.OPACITY ) {
 
-			const opacityNode = new MaterialReferenceNode( 'opacity', 'float' );
+			const opacityNode = this.getFloat( 'opacity' );
 
-			if ( material.alphaMap?.isTexture === true ) {
+			if ( material.alphaMap && material.alphaMap.isTexture === true ) {
 
-				node = new OperatorNode( '*', opacityNode, new MaterialReferenceNode( 'alphaMap', 'texture' ) );
+				node = new OperatorNode( '*', opacityNode, this.getTexture( 'alphaMap' ) );
 
 			} else {
 
@@ -89,13 +112,35 @@ class MaterialNode extends Node {
 
 			}
 
+		} else if ( scope === MaterialNode.SHININESS ) {
+
+			node = this.getFloat( 'shininess' );
+
+		} else if ( scope === MaterialNode.SPECULAR_COLOR ) {
+
+			node = this.getColor( 'specular' );
+
+		} else if ( scope === MaterialNode.REFLECTIVITY ) {
+
+			const reflectivityNode = this.getFloat( 'reflectivity' );
+
+			if ( material.specularMap && material.specularMap.isTexture === true ) {
+
+				node = new OperatorNode( '*', reflectivityNode, new SplitNode( this.getTexture( 'specularMap' ), 'r' ) );
+
+			} else {
+
+				node = reflectivityNode;
+
+			}
+
 		} else if ( scope === MaterialNode.ROUGHNESS ) {
 
-			const roughnessNode = new MaterialReferenceNode( 'roughness', 'float' );
+			const roughnessNode = this.getFloat( 'roughness' );
 
-			if ( material.roughnessMap?.isTexture === true ) {
+			if ( material.roughnessMap && material.roughnessMap.isTexture === true ) {
 
-				node = new OperatorNode( '*', roughnessNode, new SplitNode( new TextureNode( material.roughnessMap ), 'g' ) );
+				node = new OperatorNode( '*', roughnessNode, new SplitNode( this.getTexture( 'roughnessMap' ), 'g' ) );
 
 			} else {
 
@@ -105,11 +150,11 @@ class MaterialNode extends Node {
 
 		} else if ( scope === MaterialNode.METALNESS ) {
 
-			const metalnessNode = new MaterialReferenceNode( 'metalness', 'float' );
+			const metalnessNode = this.getFloat( 'metalness' );
 
-			if ( material.metalnessMap?.isTexture === true ) {
+			if ( material.metalnessMap && material.metalnessMap.isTexture === true ) {
 
-				node = new OperatorNode( '*', metalnessNode, new SplitNode( new TextureNode( material.metalnessMap ), 'b' ) );
+				node = new OperatorNode( '*', metalnessNode, new SplitNode( this.getTexture( 'metalnessMap' ), 'b' ) );
 
 			} else {
 
@@ -119,11 +164,11 @@ class MaterialNode extends Node {
 
 		} else if ( scope === MaterialNode.EMISSIVE ) {
 
-			const emissiveNode = new MaterialReferenceNode( 'emissive', 'color' );
+			const emissiveNode = this.getColor( 'emissive' );
 
-			if ( material.emissiveMap?.isTexture === true ) {
+			if ( material.emissiveMap && material.emissiveMap.isTexture === true ) {
 
-				node = new OperatorNode( '*', emissiveNode, new TextureNode( material.emissiveMap ) );
+				node = new OperatorNode( '*', emissiveNode, this.getTexture( 'emissiveMap' ) );
 
 			} else {
 
@@ -133,7 +178,55 @@ class MaterialNode extends Node {
 
 		} else if ( scope === MaterialNode.ROTATION ) {
 
-			node = new MaterialReferenceNode( 'rotation', 'float' );
+			node = this.getFloat( 'rotation' );
+
+		} else if ( scope === MaterialNode.UV ) {
+
+			// uv repeat and offset setting priorities
+
+			let uvNode;
+			let uvScaleMap =
+				material.map ||
+				material.specularMap ||
+				material.displacementMap ||
+				material.normalMap ||
+				material.bumpMap ||
+				material.roughnessMap ||
+				material.metalnessMap ||
+				material.alphaMap ||
+				material.emissiveMap ||
+				material.clearcoatMap ||
+				material.clearcoatNormalMap ||
+				material.clearcoatRoughnessMap ||
+				material.iridescenceMap ||
+				material.iridescenceThicknessMap ||
+				material.specularIntensityMap ||
+				material.specularColorMap ||
+				material.transmissionMap ||
+				material.thicknessMap ||
+				material.sheenColorMap ||
+				material.sheenRoughnessMap;
+
+			if ( uvScaleMap ) {
+
+				// backwards compatibility
+				if ( uvScaleMap.isWebGLRenderTarget ) {
+
+					uvScaleMap = uvScaleMap.texture;
+
+				}
+
+				if ( uvScaleMap.matrixAutoUpdate === true ) {
+
+					uvScaleMap.updateMatrix();
+
+				}
+
+				uvNode = new OperatorNode( '*', new UniformNode( uvScaleMap.matrix ), new JoinNode( [ new UVNode(), new ConstNode( 1 ) ] ) );
+
+			}
+
+			return uvNode || new UVNode();
 
 		} else {
 
@@ -149,4 +242,16 @@ class MaterialNode extends Node {
 
 }
 
+MaterialNode.ALPHA_TEST = 'alphaTest';
+MaterialNode.COLOR = 'color';
+MaterialNode.OPACITY = 'opacity';
+MaterialNode.SHININESS = 'shininess';
+MaterialNode.SPECULAR_COLOR = 'specularColor';
+MaterialNode.REFLECTIVITY = 'reflectivity';
+MaterialNode.ROUGHNESS = 'roughness';
+MaterialNode.METALNESS = 'metalness';
+MaterialNode.EMISSIVE = 'emissive';
+MaterialNode.ROTATION = 'rotation';
+MaterialNode.UV = 'uv';
+
 export default MaterialNode;

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