Mr.doob 1 ano atrás
pai
commit
037c05c989
100 arquivos alterados com 7310 adições e 6745 exclusões
  1. 5 8
      build/three.cjs
  2. 5 8
      build/three.module.js
  3. 0 0
      build/three.module.min.js
  4. 3596 4006
      build/three.webgpu.js
  5. 0 0
      build/three.webgpu.min.js
  6. 1 1
      docs/api/ar/audio/Audio.html
  7. 18 0
      docs/api/ar/materials/Material.html
  8. 1 1
      docs/api/en/audio/Audio.html
  9. 116 0
      docs/api/en/extras/Controls.html
  10. 18 0
      docs/api/en/materials/Material.html
  11. 1 1
      docs/api/fr/audio/Audio.html
  12. 18 0
      docs/api/fr/materials/Material.html
  13. 1 1
      docs/api/it/audio/Audio.html
  14. 18 0
      docs/api/it/materials/Material.html
  15. 1 1
      docs/api/ko/audio/Audio.html
  16. 1 1
      docs/api/pt-br/audio/Audio.html
  17. 1 1
      docs/api/zh/audio/Audio.html
  18. 116 0
      docs/api/zh/extras/Controls.html
  19. 18 0
      docs/api/zh/materials/Material.html
  20. 7 25
      docs/examples/en/controls/ArcballControls.html
  21. 18 31
      docs/examples/en/controls/DragControls.html
  22. 2 32
      docs/examples/en/controls/FirstPersonControls.html
  23. 6 32
      docs/examples/en/controls/FlyControls.html
  24. 6 21
      docs/examples/en/controls/OrbitControls.html
  25. 3 18
      docs/examples/en/controls/PointerLockControls.html
  26. 4 46
      docs/examples/en/controls/TrackballControls.html
  27. 7 25
      docs/examples/zh/controls/ArcballControls.html
  28. 24 26
      docs/examples/zh/controls/DragControls.html
  29. 18 43
      docs/examples/zh/controls/FirstPersonControls.html
  30. 10 35
      docs/examples/zh/controls/FlyControls.html
  31. 4 18
      docs/examples/zh/controls/PointerLockControls.html
  32. 71 0
      docs/examples/zh/geometries/TeapotGeometry.html
  33. 3 0
      docs/list.json
  34. 1 1
      docs/manual/en/introduction/Creating-a-scene.html
  35. 1 1
      docs/manual/zh/introduction/Installation.html
  36. 1 1
      editor/js/Config.js
  37. 0 24
      editor/js/Editor.js
  38. 1 0
      editor/js/Sidebar.Settings.js
  39. 401 2
      editor/js/Strings.js
  40. 27 6
      examples/files.json
  41. 3 1
      examples/jsm/Addons.js
  42. 1 1
      examples/jsm/animation/AnimationClipCreator.js
  43. 27 21
      examples/jsm/capabilities/WebGL.js
  44. 174 158
      examples/jsm/controls/ArcballControls.js
  45. 260 132
      examples/jsm/controls/DragControls.js
  46. 175 163
      examples/jsm/controls/FirstPersonControls.js
  47. 194 188
      examples/jsm/controls/FlyControls.js
  48. 777 786
      examples/jsm/controls/OrbitControls.js
  49. 24 15
      examples/jsm/controls/PointerLockControls.js
  50. 453 452
      examples/jsm/controls/TrackballControls.js
  51. 6 13
      examples/jsm/effects/AnaglyphEffect.js
  52. 17 11
      examples/jsm/effects/ParallaxBarrierEffect.js
  53. 6 1
      examples/jsm/effects/StereoEffect.js
  54. 1 1
      examples/jsm/environments/RoomEnvironment.js
  55. 6 1
      examples/jsm/exporters/GLTFExporter.js
  56. 29 8
      examples/jsm/exporters/USDZExporter.js
  57. 42 43
      examples/jsm/helpers/LightProbeHelper.js
  58. 19 7
      examples/jsm/loaders/KTX2Loader.js
  59. 0 243
      examples/jsm/loaders/LogLuvLoader.js
  60. 11 4
      examples/jsm/loaders/MaterialXLoader.js
  61. 9 9
      examples/jsm/loaders/PCDLoader.js
  62. 188 0
      examples/jsm/objects/SkyMesh.js
  63. 159 0
      examples/jsm/objects/Water2Mesh.js
  64. 102 0
      examples/jsm/objects/WaterMesh.js
  65. 20 0
      examples/jsm/physics/RapierPhysics.js
  66. 31 46
      examples/jsm/postprocessing/OutlinePass.js
  67. 3 1
      examples/jsm/postprocessing/SSAARenderPass.js
  68. 1 2
      examples/jsm/shaders/BleachBypassShader.js
  69. 1 1
      examples/jsm/shaders/OutputShader.js
  70. 8 8
      examples/jsm/transpiler/TSLEncoder.js
  71. 4 4
      examples/misc_controls_drag.html
  72. 3 3
      examples/misc_controls_fly.html
  73. 5 5
      examples/misc_controls_pointerlock.html
  74. 1 1
      examples/misc_exporter_usdz.html
  75. BIN
      examples/models/gltf/LightsPunctualLamp.glb
  76. BIN
      examples/models/gltf/coffeeMug.glb
  77. BIN
      examples/models/gltf/dungeon_warkarma.glb
  78. BIN
      examples/models/gltf/gears.glb
  79. BIN
      examples/screenshots/webgl_loader_gltf_lights.jpg
  80. BIN
      examples/screenshots/webgl_loader_texture_logluv.jpg
  81. BIN
      examples/screenshots/webgl_materials_curvature.jpg
  82. BIN
      examples/screenshots/webgl_performance.jpg
  83. BIN
      examples/screenshots/webgl_texture2darray_layerupdate.jpg
  84. BIN
      examples/screenshots/webgpu_backdrop.jpg
  85. BIN
      examples/screenshots/webgpu_backdrop_water.jpg
  86. BIN
      examples/screenshots/webgpu_clearcoat.jpg
  87. BIN
      examples/screenshots/webgpu_compute_birds.jpg
  88. BIN
      examples/screenshots/webgpu_cubemap_adjustments.jpg
  89. BIN
      examples/screenshots/webgpu_cubemap_dynamic.jpg
  90. BIN
      examples/screenshots/webgpu_display_stereo.jpg
  91. BIN
      examples/screenshots/webgpu_instance_points.jpg
  92. BIN
      examples/screenshots/webgpu_lightprobe.jpg
  93. BIN
      examples/screenshots/webgpu_loader_gltf.jpg
  94. BIN
      examples/screenshots/webgpu_loader_gltf_iridescence.jpg
  95. BIN
      examples/screenshots/webgpu_loader_gltf_sheen.jpg
  96. BIN
      examples/screenshots/webgpu_loader_materialx.jpg
  97. BIN
      examples/screenshots/webgpu_materials_basic.jpg
  98. BIN
      examples/screenshots/webgpu_materials_displacementmap.jpg
  99. BIN
      examples/screenshots/webgpu_materials_envmaps.jpg
  100. BIN
      examples/screenshots/webgpu_materials_transmission.jpg

Diferenças do arquivo suprimidas por serem muito extensas
+ 5 - 8
build/three.cjs


Diferenças do arquivo suprimidas por serem muito extensas
+ 5 - 8
build/three.module.js


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
build/three.module.min.js


Diferenças do arquivo suprimidas por serem muito extensas
+ 3596 - 4006
build/three.webgpu.js


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
build/three.webgpu.min.js


+ 1 - 1
docs/api/ar/audio/Audio.html

@@ -136,7 +136,7 @@
 		ترجع القيمة الخاصة بـ[page:Audio.playbackRate playbackRate].
 		</p>
 
-		<h3>[method:Float getVolume]( value )</h3>
+		<h3>[method:Float getVolume]()</h3>
 		<p>
 		إعادة الحجم الحالي.
 		</p>

+ 18 - 0
docs/api/ar/materials/Material.html

@@ -401,6 +401,24 @@
 		على عكس الخصائص، لا يتم دعم رد الاتصال بواسطة [page:Material.clone .clone]()، 
 		[page:Material.copy .copy]() و [page:Material.toJSON .toJSON]().
 		</p>
+		<p>
+			This callback is only supported in `WebGLRenderer` (not `WebGPURenderer`). 
+		</p>
+
+		<h3>
+			[method:undefined onBeforeRender]( [param:WebGLRenderer renderer], [param:Scene scene], [param:Camera camera], [param:BufferGeometry geometry], [param:Object3D object], [param:Group group] )
+		</h3>
+		<p>
+			An optional callback that is executed immediately before the material is used to 
+			render a 3D object.
+		</p>
+		<p>
+			Unlike properties, the callback is not supported by [page:Material.clone .clone](), 
+			[page:Material.copy .copy]() and [page:Material.toJSON .toJSON]().
+		</p>
+		<p>
+			This callback is only supported in `WebGLRenderer` (not `WebGPURenderer`). 
+		</p>
 		 
 		<h3>[method:String customProgramCacheKey]()</h3>
 		<p>

+ 1 - 1
docs/api/en/audio/Audio.html

@@ -159,7 +159,7 @@
 		<h3>[method:Float getPlaybackRate]()</h3>
 		<p>Return the value of [page:Audio.playbackRate playbackRate].</p>
 
-		<h3>[method:Float getVolume]( value )</h3>
+		<h3>[method:Float getVolume]()</h3>
 		<p>Return the current volume.</p>
 
 		<h3>[method:this play]( delay )</h3>

+ 116 - 0
docs/api/en/extras/Controls.html

@@ -0,0 +1,116 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<meta charset="utf-8" />
+		<base href="../../../" />
+		<script src="page.js"></script>
+		<link type="text/css" rel="stylesheet" href="page.css" />
+	</head>
+	<body>
+		[page:EventDispatcher] &rarr;
+
+		<h1>[name]</h1>
+
+		<p class="desc">
+			Abstract base class for controls.
+		</p>
+
+		<h2>Constructor</h2>
+
+		<h3>[name]( [param:Object3D object], [param:HTMLDOMElement domElement] )</h3>
+		
+		<p>
+		[page:Object3D object] - The object the controls should manage (usually the camera).
+		</p>
+		<p>
+		[page:HTMLDOMElement domElement]: The HTML element used for event listeners. (optional)
+		</p>
+		<p>
+			Creates a new instance of [name].
+		</p>
+		
+		<h2>Properties</h2>
+
+		<h3>[property:HTMLDOMElement domElement]</h3>
+		<p>
+			The HTML element used for event listeners. If not provided via the constructor, [page:.connect]() must be called after `domElement` has been set.
+		</p>
+
+		<h3>[property:Boolean enabled]</h3>
+		<p>
+			When set to `false`, the controls will not respond to user input. Default is `true`.
+		</p>
+
+		<h3>[property:Object keys]</h3>
+		<p>
+			This object defines the keyboard input of the controls.
+			Default is `{}`.
+		</p>
+
+		<h3>[property:Object mouseButtons]</h3>
+		<p>
+			This object defines what type of actions are assigned to the available mouse buttons.
+			It depends on the control implementation what kind of mouse buttons and actions are supported.
+			Default is `{ LEFT: null, MIDDLE: null, RIGHT: null }`.
+		</p>
+		<p>
+			Possible buttons are: `LEFT`, `MIDDLE`, `RIGHT`.
+		</p>
+		<p>
+			Possible actions are defined in the [page:Core Constants] page.
+		</p>
+
+		<h3>[property:Object3D object]</h3>
+		<p>
+			The 3D object that is managed by the controls.
+		</p>
+
+		<h3>[property:Integer state]</h3>
+		<p>
+			The internal state of the controls. Default is `-1` (`NONE`).
+		</p>
+
+		<h3>[property:Object touches]</h3>
+		<p>
+			This object defines what type of actions are assigned to what kind of touch interaction.
+			It depends on the control implementation what kind of touch interaction and actions are supported.
+			Default is `{ ONE: null, TWO: null }`.
+		</p>
+		<p>
+			Possible buttons are: `ONE`, `TWO`.
+		</p>
+		<p>
+			Possible actions are defined in the [page:Core Constants] page.
+		</p>
+
+		<h2>Methods</h2>
+
+		<p>See the base [page:EventDispatcher] class for common methods.</p>
+
+		<h3>[method:undefined connect] ()</h3>
+		<p>
+			Connects the controls to the DOM. This method has so called "side effects" since it adds the module's event listeners to the DOM.
+		</p>
+
+		<h3>[method:undefined disconnect] ()</h3>
+		<p>
+			Disconnects the controls from the DOM. 
+		</p>
+
+		<h3>[method:undefined dispose] ()</h3>
+		<p>
+			Call this method if you no longer want use to the controls. It frees all internal resources and removes all event listeners.
+		</p>
+
+		<h3>[method:undefined update] ( [param:Number delta] )</h3>
+		<p>
+			Controls should implement this method if they have to update their internal state per simulation step.
+		</p>
+
+		<h2>Source</h2>
+
+		<p>
+			[link:https://github.com/mrdoob/three.js/blob/master/src/[path].js src/[path].js]
+		</p>
+	</body>
+</html>

+ 18 - 0
docs/api/en/materials/Material.html

@@ -423,6 +423,24 @@
 			Unlike properties, the callback is not supported by [page:Material.clone .clone](), 
 			[page:Material.copy .copy]() and [page:Material.toJSON .toJSON]().
 		</p>
+		<p>
+			This callback is only supported in `WebGLRenderer` (not `WebGPURenderer`). 
+		</p>
+
+		<h3>
+			[method:undefined onBeforeRender]( [param:WebGLRenderer renderer], [param:Scene scene], [param:Camera camera], [param:BufferGeometry geometry], [param:Object3D object], [param:Group group] )
+		</h3>
+		<p>
+			An optional callback that is executed immediately before the material is used to 
+			render a 3D object.
+		</p>
+		<p>
+			Unlike properties, the callback is not supported by [page:Material.clone .clone](), 
+			[page:Material.copy .copy]() and [page:Material.toJSON .toJSON]().
+		</p>
+		<p>
+			This callback is only supported in `WebGLRenderer` (not `WebGPURenderer`). 
+		</p>
 
 		<h3>[method:String customProgramCacheKey]()</h3>
 		<p>

+ 1 - 1
docs/api/fr/audio/Audio.html

@@ -147,7 +147,7 @@
 		Renvoie la valeur de [page:Audio.playbackRate playbackRate].
 		</p>
 
-		<h3>[method:Float getVolume]( value )</h3>
+		<h3>[method:Float getVolume]()</h3>
 		<p>
 		Renvoie le volume actuel.
 		</p>

+ 18 - 0
docs/api/fr/materials/Material.html

@@ -355,6 +355,24 @@
 		<p>
 			Contrairement aux propriétés, le callback n'est pas pris en charge par [page:Material.clone .clone](), [page:Material.copy .copy]() et [page:Material.toJSON .toJSON]().
 		</p>
+		<p>
+			This callback is only supported in `WebGLRenderer` (not `WebGPURenderer`). 
+		</p>
+
+		<h3>
+			[method:undefined onBeforeRender]( [param:WebGLRenderer renderer], [param:Scene scene], [param:Camera camera], [param:BufferGeometry geometry], [param:Object3D object], [param:Group group] )
+		</h3>
+		<p>
+			An optional callback that is executed immediately before the material is used to 
+			render a 3D object.
+		</p>
+		<p>
+			Unlike properties, the callback is not supported by [page:Material.clone .clone](), 
+			[page:Material.copy .copy]() and [page:Material.toJSON .toJSON]().
+		</p>
+		<p>
+			This callback is only supported in `WebGLRenderer` (not `WebGPURenderer`). 
+		</p>
 
 		<h3>[method:String customProgramCacheKey]()</h3>
 		<p>

+ 1 - 1
docs/api/it/audio/Audio.html

@@ -165,7 +165,7 @@
       Restituisce il valore di [page:Audio.playbackRate playbackRate].
 		</p>
 
-		<h3>[method:Float getVolume]( value )</h3>
+		<h3>[method:Float getVolume]()</h3>
 		<p>
       Restituisce il volume corrente.
 		</p>

+ 18 - 0
docs/api/it/materials/Material.html

@@ -373,6 +373,24 @@
 		<p>
 			A differenza delle proprietà, la callback non è supportata da [page:Material.clone .clone](), [page:Material.copy .copy]() e [page:Material.toJSON .toJSON]().
 		</p>
+		<p>
+			This callback is only supported in `WebGLRenderer` (not `WebGPURenderer`). 
+		</p>
+
+		<h3>
+			[method:undefined onBeforeRender]( [param:WebGLRenderer renderer], [param:Scene scene], [param:Camera camera], [param:BufferGeometry geometry], [param:Object3D object], [param:Group group] )
+		</h3>
+		<p>
+			An optional callback that is executed immediately before the material is used to 
+			render a 3D object.
+		</p>
+		<p>
+			Unlike properties, the callback is not supported by [page:Material.clone .clone](), 
+			[page:Material.copy .copy]() and [page:Material.toJSON .toJSON]().
+		</p>
+		<p>
+			This callback is only supported in `WebGLRenderer` (not `WebGPURenderer`). 
+		</p>
 
 		<h3>[method:String customProgramCacheKey]()</h3>
 		<p>

+ 1 - 1
docs/api/ko/audio/Audio.html

@@ -139,7 +139,7 @@
 		[page:Audio.playbackRate playbackRate]의 값을 리턴합니다.
 		</p>
 
-		<h3>[method:Float getVolume]( value )</h3>
+		<h3>[method:Float getVolume]()</h3>
 		<p>
 		현재 볼륨을 리턴합니다.
 		</p>

+ 1 - 1
docs/api/pt-br/audio/Audio.html

@@ -147,7 +147,7 @@
 		Retorna o valor do [page:Audio.playbackRate playbackRate].
 		</p>
 
-		<h3>[method:Float getVolume]( value )</h3>
+		<h3>[method:Float getVolume]()</h3>
 		<p>
 		Retorna o volume atual.
 		</p>

+ 1 - 1
docs/api/zh/audio/Audio.html

@@ -137,7 +137,7 @@
 		返回[page:Audio.playbackRate playbackRate]的值.
 		</p>
 
-		<h3>[method:Float getVolume]( value )</h3>
+		<h3>[method:Float getVolume]()</h3>
 		<p>
 		返回音量.
 		</p>

+ 116 - 0
docs/api/zh/extras/Controls.html

@@ -0,0 +1,116 @@
+<!DOCTYPE html>
+<html lang="zh">
+	<head>
+		<meta charset="utf-8" />
+		<base href="../../../" />
+		<script src="page.js"></script>
+		<link type="text/css" rel="stylesheet" href="page.css" />
+	</head>
+	<body>
+		[page:EventDispatcher] &rarr;
+
+		<h1>控制器([name])</h1>
+
+		<p class="desc">
+			控制器的抽象基类。
+		</p>
+
+		<h2>构造函数</h2>
+
+		<h3>[name]( [param:Object3D object], [param:HTMLDOMElement domElement] )</h3>
+		
+		<p>
+		[page:Object3D object] - 控件应该管理的对象(通常是相机)。
+		</p>
+		<p>
+		[page:HTMLDOMElement domElement]: 用于添加事件侦听器的 HTML 元素。(可选)
+		</p>
+		<p>
+			创建一个 [name] 实例。
+		</p>
+		
+		<h2>属性</h2>
+
+		<h3>[property:HTMLDOMElement domElement]</h3>
+		<p>
+			用于添加事件侦听器的 HTML 元素。 如果没有在构造函数中提供,[page:.connect]() 必须在 `domElement` 设置后才能调用。
+		</p>
+
+		<h3>[property:Boolean enabled]</h3>
+		<p>
+			如果设置为 `false`,控制器将不再响应用户设备输入。 默认值为 `true`。
+		</p>
+
+		<h3>[property:Object keys]</h3>
+		<p>
+			该对象用于定义控制器的键盘输入。
+			默认值为 `{}`。
+		</p>
+
+		<h3>[property:Object mouseButtons]</h3>
+		<p>
+			此对象定义分配给可用鼠标按键的操作类型。
+			支持哪些鼠标按键和操作取决于控制器的具体实现。
+			默认值为 `{ LEFT: null, MIDDLE: null, RIGHT: null }`。
+		</p>
+		<p>
+			按键可能为: `LEFT`, `MIDDLE`, `RIGHT`。
+		</p>
+		<p>
+			可能的操作是定义在 [page:Core Constants] 中。
+		</p>
+
+		<h3>[property:Object3D object]</h3>
+		<p>
+			由控制器管理的 Object3D 对象。
+		</p>
+
+		<h3>[property:Integer state]</h3>
+		<p>
+			控制器的内部状态。默认值为 `-1` (`NONE`)。
+		</p>
+
+		<h3>[property:Object touches]</h3>
+		<p>
+			此对象定义将哪种类型的操作分配给哪种触摸交互。
+			支持哪种触摸交互和操作取决于控制器的具体实现。
+			默认值为 `{ ONE: null, TWO: null }`。
+		</p>
+		<p>
+			可能触摸点操作有: `ONE`, `TWO`.
+		</p>
+		<p>
+			可能的操作是定义在 [page:Core Constants] 中。
+		</p>
+
+		<h2>方法</h2>
+
+		<p>共有方法请参见其基类[page:EventDispatcher]。</p>
+
+		<h3>[method:undefined connect] ()</h3>
+		<p>
+			将控制器连接到 DOM。此方法具有所谓的“副作用”,因为它将模块的事件侦听器添加到 DOM。
+		</p>
+
+		<h3>[method:undefined disconnect] ()</h3>
+		<p>
+			断开控制器与 DOM 的连接。
+		</p>
+
+		<h3>[method:undefined dispose] ()</h3>
+		<p>
+			如果您不再需要使用这些控制器,请调用此方法。它将释放所有内部资源并删除所有事件侦听器。
+		</p>
+
+		<h3>[method:undefined update] ( [param:Number delta] )</h3>
+		<p>
+			如果控制器必须在每个模拟步骤中更新其内部状态,则应实现此方法。
+		</p>
+
+		<h2>源代码</h2>
+
+		<p>
+			[link:https://github.com/mrdoob/three.js/blob/master/src/[path].js src/[path].js]
+		</p>
+	</body>
+</html>

+ 18 - 0
docs/api/zh/materials/Material.html

@@ -312,6 +312,24 @@
 <p>
 和其他属性不一样的是,这个回调在[page:Material.clone .clone](),[page:Material.copy .copy]() 和 [page:Material.toJSON .toJSON]() 中不支持。
 </p>
+<p>
+	This callback is only supported in `WebGLRenderer` (not `WebGPURenderer`). 
+</p>
+
+<h3>
+	[method:undefined onBeforeRender]( [param:WebGLRenderer renderer], [param:Scene scene], [param:Camera camera], [param:BufferGeometry geometry], [param:Object3D object], [param:Group group] )
+</h3>
+<p>
+	An optional callback that is executed immediately before the material is used to 
+	render a 3D object.
+</p>
+<p>
+	Unlike properties, the callback is not supported by [page:Material.clone .clone](), 
+	[page:Material.copy .copy]() and [page:Material.toJSON .toJSON]().
+</p>
+<p>
+	This callback is only supported in `WebGLRenderer` (not `WebGPURenderer`). 
+</p>
 
 <h3>[method:String customProgramCacheKey]()</h3>
 <p>

+ 7 - 25
docs/examples/en/controls/ArcballControls.html

@@ -7,7 +7,7 @@
 		<link type="text/css" rel="stylesheet" href="page.css" />
 	</head>
 	<body>
-		[page:EventDispatcher] &rarr;
+		[page:Controls] &rarr;
 
 		<h1>[name]</h1>
 
@@ -78,9 +78,9 @@
 		<p>
 			[page:Camera camera]: (required) The camera to be controlled. The camera must not be a child of another object, unless that object is the scene itself.<br><br>
 
-			[page:HTMLDOMElement domElement]: The HTML element used for event listeners.<br><br>
+			[page:HTMLDOMElement domElement]: The HTML element used for event listeners. (optional)<br><br>
 
-			[page:Scene scene]: The scene rendered by the camera. If not given, gizmos cannot be shown.
+			[page:Scene scene]: The scene rendered by the camera. If not given, gizmos cannot be shown. (optional)
 		</p>
 
 		<h2>Events</h2>
@@ -102,6 +102,8 @@
 
 		<h2>Properties</h2>
 
+		<p>See the base [page:Controls] class for common properties.</p>
+
 		<h3>[property:Boolean adjustNearFar]</h3>
 		<p>
 			If true, camera's near and far values will be adjusted every time zoom is performed trying to mantain the same visible portion
@@ -125,17 +127,6 @@
 			The damping inertia used if [page:.enableAnimations] is set to true.
 		</p>
 
-		<h3>[property:HTMLDOMElement domElement]</h3>
-		<p>
-			The HTMLDOMElement used to listen for mouse / touch events. This must be passed in the constructor; changing it here will
-			not set up new event listeners.
-		</p>
-
-		<h3>[property:Boolean enabled]</h3>
-		<p>
-			When set to `false`, the controls will not respond to user input. Default is `true`.
-		</p>
-
 		<h3>[property:Boolean enableAnimations]</h3>
 		<p>
 			Set to true to enable animations for rotation (damping) and focus operation. Default is true.
@@ -211,9 +202,10 @@
 			Maximum angular velocity allowed on rotation animation start.
 		</p>
 
-
 		<h2>Methods</h2>
 
+		<p>See the base [page:Controls] class for common methods.</p>
+
 		<h3>[method:undefined activateGizmos] ( [param:Boolean isActive] )</h3>
 		<p>
 			Make gizmos more or less visible.
@@ -224,11 +216,6 @@
 			Copy the current state to clipboard (as a readable JSON text).
 		</p>
 
-		<h3>[method:undefined dispose] ()</h3>
-		<p>
-			Remove all the event listeners, cancel any pending animation and clean the scene from gizmos and grid.
-		</p>
-
 		<h3>[method:undefined pasteState] ()</h3>
 		<p>
 			Set the controls state from the clipboard, assumes that the clipboard stores a JSON text as saved from [page:.copyState].
@@ -274,11 +261,6 @@
 			Keyboard modifiers can be specified as 'CTRL', 'SHIFT' or null if not needed.
 		</p>
 
-		<h3>[method:undefined update] ()</h3>
-		<p>
-			Update the controls. Must be called after any manual changes to the camera's transform.
-		</p>
-
 		<h3>[method:Raycaster getRaycaster] ()</h3>
 		<p>
 			Returns the [page:Raycaster] object that is used for user interaction. This object is shared between all instances of

+ 18 - 31
docs/examples/en/controls/DragControls.html

@@ -7,7 +7,7 @@
 		<link type="text/css" rel="stylesheet" href="page.css" />
 	</head>
 	<body>
-		[page:EventDispatcher] &rarr;
+		[page:Controls] &rarr;
 
 		<h1>[name]</h1>
 
@@ -60,7 +60,7 @@
 			[page:Camera camera]: The camera of the rendered scene.
 			</p>
 			<p>
-			[page:HTMLDOMElement domElement]: The HTML element used for event listeners.
+			[page:HTMLDOMElement domElement]: The HTML element used for event listeners. (optional)
 			</p>
 			<p>
 				Creates a new instance of [name].
@@ -95,25 +95,21 @@
 
 		<h2>Properties</h2>
 
-		<h3>[property:Boolean enabled]</h3>
-		<p>
-			Whether or not the controls are enabled.
-		</p>
+		<p>See the base [page:Controls] class for common properties.</p>
 
-		<h3>[property:Boolean recursive]</h3>
+		<h3>[property:Array objects]</h3>
 		<p>
-			Whether children of draggable objects can be dragged independently from their parent. Default is `true`.
+			An array of draggable 3D objects.
 		</p>
 
-		<h3>[property:Boolean transformGroup]</h3>
+		<h3>[property:Raycaster raycaster]</h3>
 		<p>
-			This option only works if the [page:DragControls.objects] array contains a single draggable group object.
-			If set to `true`, [name] does not transform individual objects but the entire group. Default is `false`.
+			The internal raycaster used for detecting 3D objects.
 		</p>
 
-		<h3>[property:String mode]</h3>
+		<h3>[property:Boolean recursive]</h3>
 		<p>
-			The current transformation mode. Possible values are `translate`, and `rotate`. Default is `translate`.
+			Whether children of draggable objects can be dragged independently from their parent. Default is `true`.
 		</p>
 
 		<h3>[property:Float rotateSpeed]</h3>
@@ -121,16 +117,22 @@
 			The speed at which the object will rotate when dragged in `rotate` mode. The higher the number the faster the rotation. Default is `1`.
 		</p>
 
+		<h3>[property:Boolean transformGroup]</h3>
+		<p>
+			This option only works if the [page:DragControls.objects] array contains a single draggable group object.
+			If set to `true`, [name] does not transform individual objects but the entire group. Default is `false`.
+		</p>
+
 		<h2>Methods</h2>
 
-		<p>See the base [page:EventDispatcher] class for common methods.</p>
+		<p>See the base [page:Controls] class for common methods.</p>
 
-		<h3>[method:undefined activate] ()</h3>
+		<h3>[method:undefined connect] ()</h3>
 		<p>
 			Adds the event listeners of the controls.
 		</p>
 
-		<h3>[method:undefined deactivate] ()</h3>
+		<h3>[method:undefined disconnect] ()</h3>
 		<p>
 			Removes the event listeners of the controls.
 		</p>
@@ -140,21 +142,6 @@
 			Should be called if the controls is no longer required.
 		</p>
 
-		<h3>[method:Array getObjects] ()</h3>
-		<p>
-			Returns the array of draggable objects.
-		</p>
-
-		<h3>[method:Raycaster getRaycaster] ()</h3>
-		<p>
-			Returns the internal [page:Raycaster] instance that is used for intersection tests.
-		</p>
-
-		<h3>[method:undefined setObjects] ( [param:Array objects] )</h3>
-		<p>
-			Sets an array of draggable objects by overwriting the existing one.
-		</p>
-
 		<h2>Source</h2>
 
 		<p>

+ 2 - 32
docs/examples/en/controls/FirstPersonControls.html

@@ -7,6 +7,7 @@
 		<link type="text/css" rel="stylesheet" href="page.css" />
 	</head>
 	<body>
+		[page:Controls] &rarr;
 
 		<h1>[name]</h1>
 
@@ -37,7 +38,7 @@
 				[page:Camera object]: The camera to be controlled.
 			</p>
 			<p>
-				[page:HTMLDOMElement domElement]: The HTML element used for event listeners.
+				[page:HTMLDOMElement domElement]: The HTML element used for event listeners. (optional)
 			</p>
 			<p>
 				Creates a new instance of [name].
@@ -61,17 +62,6 @@
 			Whether or not looking around is vertically constrained by [[page:.verticalMin], [page:.verticalMax]]. Default is `false`.
 		</p>
 
-		<h3>[property:HTMLDOMElement domElement]</h3>
-		<p>
-			The HTMLDOMElement used to listen for mouse / touch events. This must be passed in the constructor; changing it here will
-			not set up new event listeners.
-		</p>
-
-		<h3>[property:Boolean enabled]</h3>
-		<p>
-			Whether or not the controls are enabled. Default is `true`.
-		</p>
-
 		<h3>[property:Number heightCoef]</h3>
 		<p>
 			Determines how much faster the camera moves when it's y-component is near [page:.heightMax]. Default is *1*.
@@ -113,11 +103,6 @@
 			The movement speed. Default is *1*.
 		</p>
 
-		<h3>[property:Camera object]</h3>
-		<p>
-			The camera to be controlled.
-		</p>
-
 		<h3>[property:Number verticalMax]</h3>
 		<p>
 			How far you can vertically look around, upper limit. Range is 0 to Math.PI radians. Default is `Math.PI`.
@@ -130,11 +115,6 @@
 
 		<h2>Methods</h2>
 
-		<h3>[method:undefined dispose] ()</h3>
-		<p>
-			Should be called if the controls is no longer required.
-		</p>
-
 		<h3>[method:undefined handleResize] ()</h3>
 		<p>
 			Should be called if the application window is resized.
@@ -156,16 +136,6 @@
 			</p>
 		</p>
 
-		<h3>[method:undefined update] ( [param:Number delta] )</h3>
-		<p>
-			<p>
-				[page:Number delta]: Time delta value.
-			</p>
-			<p>
-				Updates the controls. Usually called in the animation loop.
-			</p>
-		</p>
-
 		<h2>Source</h2>
 
 		<p>

+ 6 - 32
docs/examples/en/controls/FlyControls.html

@@ -7,6 +7,7 @@
 		<link type="text/css" rel="stylesheet" href="page.css" />
 	</head>
 	<body>
+		[page:Controls] &rarr;
 
 		<h1>[name]</h1>
 
@@ -38,7 +39,7 @@
 				[page:Camera object]: The camera to be controlled.
 			</p>
 			<p>
-				[page:HTMLDOMElement domElement]: The HTML element used for event listeners.
+				[page:HTMLDOMElement domElement]: The HTML element used for event listeners. (optional)
 			</p>
 			<p>
 				Creates a new instance of [name].
@@ -54,35 +55,21 @@
 
 		<h2>Properties</h2>
 
+		<p>See the base [page:Controls] class for common properties.</p>
+
 		<h3>[property:Boolean autoForward]</h3>
 		<p>
 			If set to `true`, the camera automatically moves forward (and does not stop) when initially translated. Default is `false`.
 		</p>
 
-		<h3>[property:HTMLDOMElement domElement]</h3>
-		<p>
-			The HTMLDOMElement used to listen for mouse / touch events. This must be passed in the constructor; changing it here will
-			not set up new event listeners.
-		</p>
-
 		<h3>[property:Boolean dragToLook]</h3>
 		<p>
 			If set to `true`, you can only look around by performing a drag interaction. Default is `false`.
 		</p>
 
-		<h3>[property:Boolean enabled]</h3>
-		<p>
-			When set to `false`, the controls will not respond to user input. Default is `true`.
-		</p>
-
 		<h3>[property:Number movementSpeed]</h3>
 		<p>
-			The movement speed. Default is *1*.
-		</p>
-
-		<h3>[property:Camera object]</h3>
-		<p>
-			The camera to be controlled.
+			The movement speed. Default is `1`.
 		</p>
 
 		<h3>[property:Number rollSpeed]</h3>
@@ -92,20 +79,7 @@
 
 		<h2>Methods</h2>
 
-		<h3>[method:undefined dispose] ()</h3>
-		<p>
-			Should be called if the controls is no longer required.
-		</p>
-
-		<h3>[method:undefined update] ( [param:Number delta] )</h3>
-		<p>
-			<p>
-				[page:Number delta]: Time delta value.
-			</p>
-			<p>
-				Updates the controls. Usually called in the animation loop.
-			</p>
-		</p>
+		<p>See the base [page:Controls] class for common methods.</p>
 
 		<h2>Source</h2>
 

+ 6 - 21
docs/examples/en/controls/OrbitControls.html

@@ -7,6 +7,8 @@
 		<link type="text/css" rel="stylesheet" href="page.css" />
 	</head>
 	<body>
+		[page:Controls] &rarr;
+
 		<h1>[name]</h1>
 
 		<p class="desc">
@@ -66,7 +68,7 @@
 		<p>
 			[page:Camera object]: (required) The camera to be controlled. The camera must not be a child of another object, unless that object is the scene itself.<br><br>
 
-			[page:HTMLDOMElement domElement]: The HTML element used for event listeners.
+			[page:HTMLDOMElement domElement]: The HTML element used for event listeners. (optional)
 		</p>
 
 		<h2>Events</h2>
@@ -88,6 +90,8 @@
 
 		<h2>Properties</h2>
 
+		<p>See the base [page:Controls] class for common properties.</p>
+
 		<h3>[property:Boolean autoRotate]</h3>
 		<p>
 			Set to true to automatically rotate around the target.<br> Note that if this is enabled, you must call [page:.update]
@@ -108,17 +112,6 @@
 			call [page:.update] () in your animation loop.
 		</p>
 
-		<h3>[property:HTMLDOMElement domElement]</h3>
-		<p>
-			The HTMLDOMElement used to listen for mouse / touch events. This must be passed in the constructor; changing it here will
-			not set up new event listeners.
-		</p>
-
-		<h3>[property:Boolean enabled]</h3>
-		<p>
-			When set to `false`, the controls will not respond to user input. Default is `true`.
-		</p>
-
 		<h3>[property:Boolean enableDamping]</h3>
 		<p>
 			Set to true to enable damping (inertia), which can be used to give a sense of weight to the controls. Default is false.<br>
@@ -224,11 +217,6 @@ controls.mouseButtons = {
 			</code>
 		</p>
 
-		<h3>[property:Camera object]</h3>
-		<p>
-			The camera being controlled.
-		</p>
-
 		<h3>[property:Float panSpeed]</h3>
 		<p>
 			Speed of panning. Default is 1.
@@ -295,10 +283,7 @@ controls.touches = {
 
 		<h2>Methods</h2>
 
-		<h3>[method:undefined dispose] ()</h3>
-		<p>
-			Remove all the event listeners.
-		</p>
+		<p>See the base [page:Controls] class for common methods.</p>
 
 		<h3>[method:radians getAzimuthalAngle] ()</h3>
 		<p>

+ 3 - 18
docs/examples/en/controls/PointerLockControls.html

@@ -7,7 +7,7 @@
 		<link type="text/css" rel="stylesheet" href="page.css" />
 	</head>
 	<body>
-		[page:EventDispatcher] &rarr;
+		[page:Controls] &rarr;
 
 		<h1>[name]</h1>
 
@@ -85,12 +85,7 @@
 
 		<h2>Properties</h2>
 
-		<h3>[property:HTMLDOMElement domElement]</h3>
-		<p>
-			The HTMLDOMElement used to listen for mouse / touch events. This must be passed in the constructor; changing it here will
-			not set up new event listeners.
-		</p>
-
+		<p>See the base [page:Controls] class for common properties.</p>
 
 		<h3>[property:Boolean isLocked]</h3>
 		<p>
@@ -114,17 +109,7 @@
 
 		<h2>Methods</h2>
 
-		<p>See the base [page:EventDispatcher] class for common methods.</p>
-
-		<h3>[method:undefined connect] ()</h3>
-		<p>
-			Adds the event listeners of the controls.
-		</p>
-
-		<h3>[method:undefined disconnect] ()</h3>
-		<p>
-			Removes the event listeners of the controls.
-		</p>
+		<p>See the base [page:Controls] class for common methods.</p>
 
 		<h3>[method:Vector3 getDirection] ( [param:Vector3 target] )</h3>
 		<p>

+ 4 - 46
docs/examples/en/controls/TrackballControls.html

@@ -7,7 +7,7 @@
 		<link type="text/css" rel="stylesheet" href="page.css" />
 	</head>
 	<body>
-		[page:EventDispatcher] &rarr;
+		[page:Controls] &rarr;
 
 		<h1>[name]</h1>
 
@@ -41,7 +41,7 @@
 				[page:Camera camera]: The camera of the rendered scene.
 			</p>
 			<p>
-				[page:HTMLDOMElement domElement]: The HTML element used for event listeners.
+				[page:HTMLDOMElement domElement]: The HTML element used for event listeners. (optional)
 			</p>
 			<p>
 				Creates a new instance of [name].
@@ -67,22 +67,13 @@
 
 		<h2>Properties</h2>
 
-		<h3>[property:HTMLDOMElement domElement]</h3>
-		<p>
-			The HTMLDOMElement used to listen for mouse / touch events. This must be passed in the constructor; changing it here will
-			not set up new event listeners.
-		</p>
+		<p>See the base [page:Controls] class for common properties.</p>
 
 		<h3>[property:Number dynamicDampingFactor]</h3>
 		<p>
 			Defines the intensity of damping. Only considered if [page:.staticMoving staticMoving] is set to `false`. Default is `0.2`.
 		</p>
 
-		<h3>[property:Boolean enabled]</h3>
-		<p>
-			Whether or not the controls are enabled.
-		</p>
-
 		<h3>[property:Array keys]</h3>
 	
 			This array holds keycodes for controlling interactions.
@@ -140,11 +131,6 @@
 			Whether or not zooming is disabled. Default is `false`.
 		</p>
 
-		<h3>[property:Camera object]</h3>
-		<p>
-			The camera being controlled.
-		</p>
-
 		<h3>[property:Number panSpeed]</h3>
 		<p>
 			The pan speed. Default is `0.3`.
@@ -183,46 +169,18 @@
 
 		<h2>Methods</h2>
 
-		<h3>[method:undefined checkDistances] ()</h3>
-		<p>
-			Ensures the controls stay in the range [minDistance, maxDistance]. Called by [page:.update update]().
-		</p>
-
-		<h3>[method:undefined dispose] ()</h3>
-		<p>
-			Should be called if the controls is no longer required.
-		</p>
+		<p>See the base [page:Controls] class for common methods.</p>
 
 		<h3>[method:undefined handleResize] ()</h3>
 		<p>
 			Should be called if the application window is resized.
 		</p>
 
-		<h3>[method:undefined panCamera] ()</h3>
-		<p>
-			Performs panning if necessary. Called by [page:.update update]().
-		</p>
-
 		<h3>[method:undefined reset] ()</h3>
 		<p>
 			Resets the controls to its initial state.
 		</p>
 
-		<h3>[method:undefined rotateCamera] ()</h3>
-		<p>
-			Rotates the camera if necessary. Called by [page:.update update]().
-		</p>
-
-		<h3>[method:undefined update] ()</h3>
-		<p>
-			Updates the controls. Usually called in the animation loop.
-		</p>
-
-		<h3>[method:undefined zoomCamera] ()</h3>
-		<p>
-			Performs zooming if necessary. Called by [page:.update update]().
-		</p>
-
 		<h2>Source</h2>
 
 		<p>

+ 7 - 25
docs/examples/zh/controls/ArcballControls.html

@@ -9,7 +9,7 @@
 </head>
 
 <body>
-	[page:EventDispatcher] &rarr;
+	[page:Controls] &rarr;
 
 	<h1>弧球控制器([name])</h1>
 
@@ -70,9 +70,9 @@
 	<p>
 		[page:Camera camera]:(必填)要控制的相机。相机不能是另一个对象的子对象,除非该对象是场景本身。<br><br>
 
-		[page:HTMLDOMElement domElement]: 用于事件侦听器的 HTML 元素。<br><br>
+		[page:HTMLDOMElement domElement]: 用于事件侦听器的 HTML 元素。(可选)<br><br>
 
-		[page:Scene scene]: 相机渲染的场景。如果未给出,则小控件无法显示。
+		[page:Scene scene]: 相机渲染的场景。如果未给出,则小控件无法显示。(可选)
 	</p>
 
 	<h2>事件</h2>
@@ -94,6 +94,8 @@
 
 	<h2>属性</h2>
 
+	<p>共有属性请参见其基类[page:Controls]。</p>
+
 	<h3>[property:Boolean adjustNearFar]</h3>
 	<p>
 		如果为 true,则每次执行缩放时都会调整相机的近端和远端值,尝试保持初始近端和远端值给出的相同可见部分(仅限 [page:PerspectiveCamera] )。默认为 false。
@@ -116,16 +118,6 @@
 		设置为 [page:.enableAnimations] 为true 时使用的阻尼惯性。
 	</p>
 
-	<h3>[property:HTMLDOMElement domElement]</h3>
-	<p>
-		HTMLDOMElement 用于监听鼠标/触摸事件。这必须在构造函数中传递;此处更改它不会设置新的事件侦听器。
-	</p>
-
-	<h3>[property:Boolean enabled]</h3>
-	<p>
-		当设置为 时 `false`,小控件将不再响应用户交互。默认为 `true`。
-	</p>
-
 	<h3>[property:Boolean enableAnimations]</h3>
 	<p>
 		设置为 true 以启用旋转(阻尼)和聚焦操作的动画。默认为 true。
@@ -201,9 +193,10 @@
 		旋转动画开始时允许的最大角速度。
 	</p>
 
-
 	<h2>方法</h2>
 
+	<p>共有方法请参见其基类[page:Controls]。</p>
+
 	<h3>[method:undefined activateGizmos] ( [param:Boolean isActive] )</h3>
 	<p>
 		使小控件或多或少可见。
@@ -214,11 +207,6 @@
 		将当前状态复制到剪贴板(作为可读的 JSON 文本)。
 	</p>
 
-	<h3>[method:undefined dispose] ()</h3>
-	<p>
-		删除所有事件侦听器,取消任何待处理的动画并清除场景中的小控件和网格。
-	</p>
-
 	<h3>[method:undefined pasteState] ()</h3>
 	<p>
 		从剪贴板设置控件状态,假设剪贴板存储从 [page:.copyState] 保存的 JSON 文本。
@@ -264,16 +252,10 @@
 		键盘修饰符可以指定为 'CTRL'、'SHIFT' 或 null(如果不再需要) 。
 	</p>
 
-	<h3>[method:undefined update] ()</h3>
-	<p>
-		更新控件。必须在对相机变换进行任何手动更改后调用。
-	</p>
-
 	<h3>[method:Raycaster getRaycaster] ()</h3>
 	<p>
 		返回用于用户交互的 [page:Raycaster] 对象。如果设置了 [name] 的 [page:Object3D.layers .layers] 属性,您还需要使用匹配的值设置 [page:Raycaster.layers
 		.layers] 的 [page:Raycaster] 属性,否则 [name] 将无法按预期工作。
-		won't work as expected.
 	</p>
 
 	<h2>源代码</h2>

+ 24 - 26
docs/examples/zh/controls/DragControls.html

@@ -7,7 +7,7 @@
 		<link type="text/css" rel="stylesheet" href="page.css" />
 	</head>
 	<body>
-		[page:EventDispatcher] &rarr;
+		[page:Controls] &rarr;
 
 		<h1>拖放控制器([name])</h1>
 
@@ -61,7 +61,7 @@
 			[page:Camera camera]: 渲染场景的摄像机。
 			</p>
 			<p>
-			[page:HTMLDOMElement domElement]: 用于事件监听的HTML元素。
+			[page:HTMLDOMElement domElement]: 用于事件监听的HTML元素。(可选)
 			</p>
 			<p>
 				创建一个新的 [name] 实例。
@@ -97,53 +97,51 @@
 
 		<h2>属性</h2>
 
-		<h3>[property:Boolean enabled]</h3>
-		<p>
-			是否启用控制器。
-		</p>
+		<p>共有属性请参见其基类[page:Controls]。</p>
 
-		<h3>[property:Boolean transformGroup]</h3>
+		<h3>[property:Array objects]</h3>
 		<p>
-			当[page:DragControls.objects]数组包含一个单个可拖拽的组对象时该选项生效。如果设置为`true`,[name]会转换整个组对象,而不对单个对象做转换。默认值为`false`
+			可拖放对象的数组
 		</p>
 
-		<h3>[property:String mode]</h3>
+		<h3>[property:Raycaster raycaster]</h3>
 		<p>
-			The current transformation mode. Possible values are `translate`, and `rotate`. Default is `translate`.
+			内部用于检测拾取对象的光线投射器。
 		</p>
 
-		<h2>方法</h2>
-
-		<p>共有方法请参见其基类[page:EventDispatcher]。</p>
-
-		<h3>[method:undefined activate] ()</h3>
+		<h3>[property:Boolean recursive]</h3>
 		<p>
-			添加控制器的事件监听
+			可拖放对象的子对象是否可以独立于其父对象进行拖放。 默认值为 `true`。
 		</p>
 
-		<h3>[method:undefined deactivate] ()</h3>
+		<h3>[property:Float rotateSpeed]</h3>
 		<p>
-			移除控制器的事件监听
+			执行 `rotate` 时的旋转速度。该值越大旋转速度越快。 默认值为 `1`
 		</p>
 
-		<h3>[method:undefined dispose] ()</h3>
+		<h3>[property:Boolean transformGroup]</h3>
 		<p>
-			若不再需要该控制器,则应当调用此函数。
+			当 [page:DragControls.objects] 数组包含一个单个可拖拽的组对象时该选项生效。
+			如果设置为`true`,[name]会转换整个组对象,而不对单个对象做转换。默认值为`false`。
 		</p>
 
-		<h3>[method:Array getObjects] ()</h3>
+		<h2>方法</h2>
+
+		<p>共有方法请参见其基类[page:Controls]。</p>
+
+		<h3>[method:undefined connect] ()</h3>
 		<p>
-			返回可拖拽的对象数组。
+			添加控制器的事件监听
 		</p>
 
-		<h3>[method:Raycaster getRaycaster] ()</h3>
+		<h3>[method:undefined disconnect] ()</h3>
 		<p>
-			返回用于相交测试的内部[page:Raycaster]实例
+			移除控制器的事件监听
 		</p>
 
-		<h3>[method:undefined setObjects] ( [param:Array objects] )</h3>
+		<h3>[method:undefined dispose] ()</h3>
 		<p>
-			Sets an array of draggable objects by overwriting the existing one.
+			若不再需要该控制器,则应当调用此函数。
 		</p>
 
 		<h2>源代码</h2>

+ 18 - 43
docs/examples/zh/controls/FirstPersonControls.html

@@ -7,6 +7,7 @@
 		<link type="text/css" rel="stylesheet" href="page.css" />
 	</head>
 	<body>
+		[page:Controls] &rarr;
 
 		<h1>第一人称控制器([name])</h1>
 
@@ -37,7 +38,7 @@
 				[page:Camera object]: 被控制的摄像机。
 			</p>
 			<p>
-				[page:HTMLDOMElement domElement]: 用于事件监听的HTML元素。
+				[page:HTMLDOMElement domElement]: 用于事件监听的HTML元素。(可选)
 			</p>
 			<p>
 				创建一个新的 [name] 实例。
@@ -46,60 +47,52 @@
 
 		<h2>属性</h2>
 
+		<p>共有属性请参见其基类[page:Controls]。</p>
+
 		<h3>[property:Boolean activeLook]</h3>
 		<p>
-			是否能够环视四周。默认为*true*。
+			是否能够环视四周。默认为 *true*。
 		</p>
 
 		<h3>[property:Boolean autoForward]</h3>
 		<p>
-			摄像机是否自动向前移动。默认为*false*。
+			摄像机是否自动向前移动。默认为 *false*。
 		</p>
 
 		<h3>[property:Boolean constrainVertical]</h3>
 		<p>
-			垂直环视是否约束在[[page:.verticalMin], [page:.verticalMax]]之间。默认值为*false*。
-		</p>
-
-		<h3>[property:HTMLDOMElement domElement]</h3>
-		<p>
-			该 HTMLDOMElement 用于监听鼠标/触摸事件,该属性必须在构造函数中传入。在此处改变它将不会设置新的事件监听。
-		</p>
-
-		<h3>[property:Boolean enabled]</h3>
-		<p>
-			是否启用控制器。默认为*true*。
+			垂直环视是否约束在[[page:.verticalMin], [page:.verticalMax]]之间。默认值为 *false*。
 		</p>
 
 		<h3>[property:Number heightCoef]</h3>
 		<p>
-			当Y坐标接近[page:.heightMax]时摄像机的移动速度。默认值为*1*。
+			当Y坐标接近[page:.heightMax]时摄像机的移动速度。默认值为 *1*。
 		</p>
 
 		<h3>[property:Number heightMax]</h3>
 		<p>
-			用于调节移动速度的摄像机最大高度限制。默认值为*1*。
+			用于调节移动速度的摄像机最大高度限制。默认值为 *1*。
 		</p>
 
 		<h3>[property:Number heightMin]</h3>
 		<p>
-			用于调节移动速度的摄像机最低高度限制。默认值为*0*。
+			用于调节移动速度的摄像机最低高度限制。默认值为 *0*。
 		</p>
 
 		<h3>[property:Boolean heightSpeed]</h3>
 		<p>
-			摄像机的高度是否影响向前移动的速度。默认值为*false*。
+			摄像机的高度是否影响向前移动的速度。默认值为 *false*。
 			使用属性 [page:.heightCoef]、 [page:.heightMin] 和 [page:.heightMax] 来进行配置。
 		</p>
 
 		<h3>[property:Boolean lookVertical]</h3>
 		<p>
-			是否能够垂直环视。默认为*true*。
+			是否能够垂直环视。默认为 *true*。
 		</p>
 
 		<h3>[property:Number lookSpeed]</h3>
 		<p>
-			环视速度。默认为*0.005*。
+			环视速度。默认为 *0.005*。
 		</p>
 
 		<h3>[property:Boolean mouseDragOn]</h3>
@@ -109,30 +102,22 @@
 
 		<h3>[property:Number movementSpeed]</h3>
 		<p>
-			移动速度。默认为*1*。
-		</p>
-
-		<h3>[property:Camera object]</h3>
-		<p>
-			被控制的摄像机。
+			移动速度。默认为 *1*。
 		</p>
 
 		<h3>[property:Number verticalMax]</h3>
 		<p>
-			你能够垂直环视角度的上限。范围在 0 到 Math.PI 弧度之间。默认为*Math.PI*。
+			你能够垂直环视角度的上限。范围在 0 到 Math.PI 弧度之间。默认为 *Math.PI*。
 		</p>
 
 		<h3>[property:Number verticalMin]</h3>
 		<p>
-			你能够垂直环视角度的下限。范围在 0 到 Math.PI 弧度之间。默认为*0*。
+			你能够垂直环视角度的下限。范围在 0 到 Math.PI 弧度之间。默认为 *0*。
 		</p>
 
 		<h2>方法</h2>
-
-		<h3>[method:undefined dispose] ()</h3>
-		<p>
-			若不再需要该控制器,则应当调用此函数。
-		</p>
+		
+		<p>共有方法请参见其基类[page:Controls]。</p>
 
 		<h3>[method:undefined handleResize] ()</h3>
 		<p>
@@ -155,16 +140,6 @@
 			</p>
 		</p>
 
-		<h3>[method:undefined update] ( [param:Number delta] )</h3>
-		<p>
-			<p>
-				[page:Number delta]:时间增量值。
-			</p>
-			<p>
-				更新控制器,常被用在动画循环中。
-			</p>
-		</p>
-
 		<h2>源代码</h2>
 
 		<p>

+ 10 - 35
docs/examples/zh/controls/FlyControls.html

@@ -7,6 +7,7 @@
 		<link type="text/css" rel="stylesheet" href="page.css" />
 	</head>
 	<body>
+		[page:Controls] &rarr;
 
 		<h1>飞行控制器([name])</h1>
 
@@ -38,7 +39,7 @@
 				[page:Camera object]: 被控制的摄像机。
 			</p>
 			<p>
-				[page:HTMLDOMElement domElement]: 用于事件监听的HTML元素。
+				[page:HTMLDOMElement domElement]: 用于事件监听的HTML元素。(可选)
 			</p>
 			<p>
 				创建一个新的 [name] 实例。
@@ -53,58 +54,32 @@
 		</p>
 
 		<h2>属性</h2>
+		
+		<p>共有属性请参见其基类[page:Controls]。</p>
 
 		<h3>[property:Boolean autoForward]</h3>
 		<p>
-			若该值设为*true*,初始变换后,摄像机将自动向前移动(且不会停止)。默认为*false*。
-		</p>
-
-		<h3>[property:HTMLDOMElement domElement]</h3>
-		<p>
-			该 HTMLDOMElement 用于监听鼠标/触摸事件,该属性必须在构造函数中传入。在此处改变它将不会设置新的事件监听。
+			若该值设为 *true*,初始变换后,摄像机将自动向前移动(且不会停止)。默认为 *false*。
 		</p>
 
 		<h3>[property:Boolean dragToLook]</h3>
 		<p>
-			若该值设为*true*,你将只能通过执行拖拽交互来环视四周。默认为*false*。
-		</p>
-
-		<h3>[property:Boolean enabled]</h3>
-		<p>
-			当设置为false时,控制器将不会响应用户的操作。默认值为true。
+			若该值设为 *true*,你将只能通过执行拖拽交互来环视四周。默认为 *false*。
 		</p>
 
 		<h3>[property:Number movementSpeed]</h3>
 		<p>
-			移动速度,默认为*1*。
-		</p>
-
-		<h3>[property:Camera object]</h3>
-		<p>
-			被控制的摄像机。
+			移动速度,默认为 *1*。
 		</p>
 
 		<h3>[property:Number rollSpeed]</h3>
 		<p>
-			旋转速度。默认为*0.005*。
+			旋转速度。默认为 *0.005*。
 		</p>
 
 		<h2>方法</h2>
-
-		<h3>[method:undefined dispose] ()</h3>
-		<p>
-			若不再需要该控制器,则应当调用此函数。
-		</p>
-
-		<h3>[method:undefined update] ( [param:Number delta] )</h3>
-		<p>
-			<p>
-				[page:Number delta]:时间增量值。
-			</p>
-			<p>
-				更新控制器,常被用在动画循环中。
-			</p>
-		</p>
+		
+		<p>共有方法请参见其基类[page:Controls]。</p>
 
 		<h2>源代码</h2>
 

+ 4 - 18
docs/examples/zh/controls/PointerLockControls.html

@@ -7,7 +7,7 @@
 		<link type="text/css" rel="stylesheet" href="page.css" />
 	</head>
 	<body>
-		[page:EventDispatcher] &rarr;
+		[page:Controls] &rarr;
 
 		<h1>指针锁定控制器([name])</h1>
 
@@ -85,11 +85,7 @@
 
 		<h2>属性</h2>
 
-		<h3>[property:HTMLDOMElement domElement]</h3>
-		<p>
-			该 HTMLDOMElement 用于监听鼠标/触摸事件,该属性必须在构造函数中传入。在此处改变它将不会设置新的事件监听。
-		</p>
-
+		<p>共有属性请参见其基类[page:Controls]。</p>
 
 		<h3>[property:Boolean isLocked]</h3>
 		<p>
@@ -106,19 +102,9 @@
 			摄像机的俯仰角下限限制。范围为0到Math.PI弧度之间。默认值为0。
 		</p>
 
-		<h2>Methods</h2>
+		<h2>方法</h2>
 
-		<p>共有方法请参见其基类[page:EventDispatcher]。</p>
-
-		<h3>[method:undefined connect] ()</h3>
-		<p>
-			添加控制器的事件监听。
-		</p>
-
-		<h3>[method:undefined disconnect] ()</h3>
-		<p>
-			移除控制器的事件监听。
-		</p>
+		<p>共有方法请参见其基类[page:Controls]。</p>
 
 		<h3>[method:Vector3 getDirection] ( [param:Vector3 target] )</h3>
 		<p>

+ 71 - 0
docs/examples/zh/geometries/TeapotGeometry.html

@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<html lang="zh">
+
+<head>
+	<meta charset="utf-8" />
+	<base href="../../../" />
+	<script src="page.js"></script>
+	<link type="text/css" rel="stylesheet" href="page.css" />
+</head>
+
+<body>
+	[page:BufferGeometry] &rarr;
+
+	<h1>[name]</h1>
+
+	<p class="desc">
+		[name] 通过 Martin Newell 的著名 Utah 茶壶数据库进行镶嵌。
+	</p>
+
+	<h2>导入</h2>
+
+	<p>
+		[name] 是一个附加组件,必须显式导入。
+		参见 [link:#manual/introduction/Installation Installation / Addons].
+	</p>
+
+	<code>
+			import { TeapotGeometry } from 'three/addons/geometries/TeapotGeometry.js';
+		</code>
+
+	<h2>代码示例</h2>
+
+	<code>
+			const geometry = new TeapotGeometry( 50, 18 );
+			const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
+			const teapot = new THREE.Mesh( geometry, material );
+			scene.add( teapot );
+		</code>
+
+	<h2>构造函数</h2>
+
+	<h3>
+		[name]([param:Integer size], [param:Integer segments], [param:Boolean bottom], [param:Boolean lid],
+		[param:Boolean body],
+		[param:Boolean fitLid], [param:Boolean blinn])属性
+	</h3>
+	<p>
+		size — 茶壶的相对尺寸。可选;默认为 `50`。<br>
+		segments — 每个面片边缘细分的线段数。可选;默认为 `10`。<br>
+		bottom — 是否生成茶壶底部。可选;默认为 `true`。<br>
+		lid — 是否生成盖子。可选;默认为 `true`。<br>
+		body — 是否生成壶身。可选;默认为 `true`。<br>
+		fitLid — 是否稍微拉伸盖子以防止壶身和盖子之间的间隙。可选;默认为 `true`。<br>
+		blinn — 是否垂直缩放茶壶以获得更好的外观。可选;默认为 `true`。
+	</p>
+
+	<h2>属性</h2>
+	<p>请参阅基础 [page:BufferGeometry] 类以获取通用属性。</p>
+
+	<h2>方法</h2>
+	<p>请参阅基础 [page:BufferGeometry] 类以获取通用方法。</p>
+
+	<h2>源代码</h2>
+
+	<p>
+		[link:https://github.com/mrdoob/three.js/blob/master/examples/jsm/geometries/TeapotGeometry.js
+		examples/jsm/geometries/TeapotGeometry.js]
+	</p>
+</body>
+
+</html>

+ 3 - 0
docs/list.json

@@ -99,6 +99,7 @@
 			},
 
 			"Extras": {
+				"Controls": "api/en/extras/Controls",
 				"DataUtils": "api/en/extras/DataUtils",
 				"Earcut": "api/en/extras/Earcut",
 				"ImageUtils": "api/en/extras/ImageUtils",
@@ -853,6 +854,7 @@
 			},
 
 			"附件": {
+				"Controls": "api/zh/extras/Controls",
 				"DataUtils": "api/zh/extras/DataUtils",
 				"Earcut": "api/zh/extras/Earcut",
 				"ImageUtils": "api/zh/extras/ImageUtils",
@@ -1097,6 +1099,7 @@
 				"ConvexGeometry": "examples/zh/geometries/ConvexGeometry",
 				"DecalGeometry": "examples/zh/geometries/DecalGeometry",
 				"ParametricGeometry": "examples/zh/geometries/ParametricGeometry",
+				"TeapotGeometry": "examples/zh/geometries/TeapotGeometry",
 				"TextGeometry": "examples/zh/geometries/TextGeometry",
 				"SDFGeometryGenerator": "examples/zh/geometries/SDFGeometryGenerator"
 			},

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

@@ -71,7 +71,7 @@
 
 		<h2>Rendering the scene</h2>
 
-		<p>If you copied the code from above into the HTML file we created earlier, you wouldn't be able to see anything. This is because we're not actually rendering anything yet. For that, we need what's called a render or animation loop.</p>
+		<p>If you copied the code from above into the main.js file we created earlier, you wouldn't be able to see anything. This is because we're not actually rendering anything yet. For that, we need what's called a render or animation loop.</p>
 
 		<code>
 		function animate() {

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

@@ -61,7 +61,7 @@ import * as THREE from 'three';
 		<h3>开发</h3>
 
 		<p>
-			对于大多数用户而,从 [link:https://www.npmjs.com/ npm 包注册表中心] 安装并使用 [link:https://eloquentjavascript.net/10_modules.html#h_zWTXAU93DC 构建工具] 会是一个更推荐的方案。因为项目需要的依赖越多,就越有可能遇到静态托管无法轻易解决的问题。而使用构建工具,导入本地 JavaScript 文件和 npm 软件包将会是开箱即用的,无需导入映射(import maps)。
+			对于大多数用户而,从 [link:https://www.npmjs.com/ npm 包注册表中心] 安装并使用 [link:https://eloquentjavascript.net/10_modules.html#h_zWTXAU93DC 构建工具] 会是一个更推荐的方案。因为项目需要的依赖越多,就越有可能遇到静态托管无法轻易解决的问题。而使用构建工具,导入本地 JavaScript 文件和 npm 软件包将会是开箱即用的,无需导入映射(import maps)。
 		</p>
 
 		<ol>

+ 1 - 1
editor/js/Config.js

@@ -4,7 +4,7 @@ function Config() {
 
 	const userLanguage = navigator.language.split( '-' )[ 0 ];
 
-	const suggestedLanguage = [ 'fr', 'ja', 'zh' ].includes( userLanguage ) ? userLanguage : 'en';
+	const suggestedLanguage = [ 'fr', 'ja', 'zh', 'ko' ].includes( userLanguage ) ? userLanguage : 'en';
 
 	const storage = {
 		'language': suggestedLanguage,

+ 0 - 24
editor/js/Editor.js

@@ -196,30 +196,6 @@ Editor.prototype = {
 
 	},
 
-	moveObject: function ( object, parent, before ) {
-
-		if ( parent === undefined ) {
-
-			parent = this.scene;
-
-		}
-
-		parent.add( object );
-
-		// sort children array
-
-		if ( before !== undefined ) {
-
-			var index = parent.children.indexOf( before );
-			parent.children.splice( index, 0, object );
-			parent.children.pop();
-
-		}
-
-		this.signals.sceneGraphChanged.dispatch();
-
-	},
-
 	nameObject: function ( object, name ) {
 
 		object.name = name;

+ 1 - 0
editor/js/Sidebar.Settings.js

@@ -22,6 +22,7 @@ function SidebarSettings( editor ) {
 		fr: 'Français',
 		zh: '中文',
 		ja: '日本語',
+		ko: '한국어',
 	};
 
 	const languageRow = new UIRow();

+ 401 - 2
editor/js/Strings.js

@@ -409,7 +409,7 @@ function Strings( config ) {
 		fr: {
 
 			'prompt/file/open': 'Toutes les données non enregistrées seront perdues Êtes-vous sûr ?',
-			'prompt/file/failedToOpenProject': 'Échec de l\'ouverture du projet !',
+			'prompt/file/failedToOpenProject': 'Échec de l\'ouverture du projet !',
 			'prompt/file/export/noMeshSelected': 'Aucun maillage sélectionné !',
 			'prompt/file/export/noObjectSelected': 'Aucun objet sélectionné !',
 			'prompt/script/remove': 'Es-tu sûr?',
@@ -1610,8 +1610,407 @@ function Strings( config ) {
 			'script/title/fragmentShader': 'フラグメントシェーダ',
 			'script/title/programInfo': 'プログラムのプロパティ'
 
-		}
+		},
 
+		ko: {
+			'prompt/file/open': '저장되지 않은 데이터는 손실됩니다. 진행하시겠습니까?',
+			'prompt/file/failedToOpenProject': '프로젝트를 여는 데 실패했습니다!',
+			'prompt/file/export/noMeshSelected': '메시가 선택되지 않았습니다!',
+			'prompt/file/export/noObjectSelected': '객체가 선택되지 않았습니다!',
+			'prompt/script/remove': '삭제하시겠습니까?',
+			'prompt/history/clear': '되돌리기/다시하기 기록이 지워집니다. 진행하시겠습니까?',
+			'prompt/history/preserve': '기록은 세션을 통해 저장됩니다. 이는 텍스처를 조작할 때 성능에 영향을 미칠 수 있습니다.',
+			'prompt/history/forbid': '씬을 재생하는 동안 되돌리기/다시하기는 비활성화됩니다.',
+			'prompt/rendering/realistic/unsupportedMaterial': 'REALISTIC 셰이딩: MeshStandardmaterial 및 MeshPhysicalmaterial만 지원됩니다',
+	
+			'command/AddObject': '객체 추가',
+			'command/AddScript': '스크립트 추가',
+			'command/MoveObject': '객체 이동',
+			'command/MultiCmds': '여러 변경',
+			'command/RemoveObject': '객체 삭제',
+			'command/RemoveScript': '스크립트 삭제',
+			'command/SetColor': '색 설정',
+			'command/SetGeometry': '지오메트리 설정',
+			'command/SetGeometryValue': '지오메트리 값 설정',
+			'command/SetMaterialColor': '머티리얼 색 설정',
+			'command/SetMaterial': '머티리얼 설정',
+			'command/SetMaterialMap': '머티리얼 맵 설정',
+			'command/SetMaterialRange': '머티리얼 범위 설정',
+			'command/SetMaterialValue': '머티리얼 값 설정',
+			'command/SetMaterialVector': '머티리얼 벡터 설정',
+			'command/SetPosition': '위치 설정',
+			'command/SetRotation': '회전 설정',
+			'command/SetScale': '스케일 설정',
+			'command/SetScene': '장면 설정',
+			'command/SetScriptValue': '스크립트 값 설정',
+			'command/SetShadowValue': '그림자 값 설정',
+			'command/SetUuid': 'UUID 설정',
+			'command/SetValue': '값 설정',
+	
+			'menubar/file': '파일',
+			'menubar/file/new': '새 프로젝트',
+			'menubar/file/new/empty': '비어 있음',
+			'menubar/file/new/Arkanoid': '아카노이드',
+			'menubar/file/new/Camera': '카메라',
+			'menubar/file/new/Particles': '파티클',
+			'menubar/file/new/Pong': '퐁',
+			'menubar/file/new/Shaders': '셰이더',
+			'menubar/file/open': '열기',
+			'menubar/file/save': '저장',
+			'menubar/file/import': '가져오기',
+			'menubar/file/export': '내보내기',
+	
+			'menubar/edit': '편집',
+			'menubar/edit/undo': '되돌리기',
+			'menubar/edit/redo': '다시하기',
+			'menubar/edit/center': '중앙으로 옮기기',
+			'menubar/edit/clone': '복제',
+			'menubar/edit/delete': '삭제',
+	
+			'menubar/add': '추가',
+			'menubar/add/group': '그룹',
+	
+			'menubar/add/mesh': '메시',
+			'menubar/add/mesh/plane': '평면',
+			'menubar/add/mesh/box': '직육면체',
+			'menubar/add/mesh/capsule': '캡슐',
+			'menubar/add/mesh/circle': '원',
+			'menubar/add/mesh/cylinder': '원통',
+			'menubar/add/mesh/ring': '링',
+			'menubar/add/mesh/sphere': '구',
+			'menubar/add/mesh/dodecahedron': '십이면체',
+			'menubar/add/mesh/icosahedron': '이십면체',
+			'menubar/add/mesh/octahedron': '팔면체',
+			'menubar/add/mesh/tetrahedron': '사면체',
+			'menubar/add/mesh/torus': '토러스',
+			'menubar/add/mesh/tube': '튜브',
+			'menubar/add/mesh/torusknot': '토러스 매듭',
+			'menubar/add/mesh/lathe': '선반형',
+			'menubar/add/mesh/sprite': '스프라이트',
+	
+			'menubar/add/light': '조명',
+			'menubar/add/light/ambient': '환경광',
+			'menubar/add/light/directional': '방향광',
+			'menubar/add/light/hemisphere': '반구광',
+			'menubar/add/light/point': '포인트',
+			'menubar/add/light/spot': '스포트',
+	
+			'menubar/add/camera': '카메라',
+			'menubar/add/camera/perspective': '투시 투영',
+			'menubar/add/camera/orthographic': '정사영',
+	
+			'menubar/status/autosave': '자동 저장',
+	
+			'menubar/view': '보기',
+			'menubar/view/fullscreen': '전체 화면',
+			'menubar/view/gridHelper': '그리드 도우미',
+			'menubar/view/cameraHelpers': '카메라 도우미',
+			'menubar/view/lightHelpers': '조명 도우미',
+			'menubar/view/skeletonHelpers': '골격 도우미',
+	
+			'menubar/help': '도움말',
+			'menubar/help/source_code': '소스 코드',
+			'menubar/help/icons': '아이콘 팩',
+			'menubar/help/about': 'Three.js 알아보기',
+			'menubar/help/manual': '매뉴얼',
+	
+			'sidebar/animations': '애니메이션',
+			'sidebar/animations/play': '재생',
+			'sidebar/animations/stop': '정지',
+			'sidebar/animations/timescale': '시간 스케일',
+	
+			'sidebar/scene': '장면',
+			'sidebar/scene/background': '배경',
+			'sidebar/scene/environment': '환경',
+			'sidebar/scene/fog': '안개',
+	
+			'sidebar/properties/object': '객체',
+			'sidebar/properties/geometry': '지오메트리',
+			'sidebar/properties/material': '머티리얼',
+			'sidebar/properties/script': '스크립트',
+	
+			'sidebar/object/type': '타입',
+			'sidebar/object/new': '새로 만들기',
+			'sidebar/object/uuid': 'UUID',
+			'sidebar/object/name': '이름',
+			'sidebar/object/position': '위치',
+			'sidebar/object/rotation': '회전',
+			'sidebar/object/scale': '스케일',
+			'sidebar/object/fov': '화각',
+			'sidebar/object/left': '왼쪽',
+			'sidebar/object/right': '오른쪽',
+			'sidebar/object/top': '위',
+			'sidebar/object/bottom': '아래',
+			'sidebar/object/near': '최소 시야',
+			'sidebar/object/far': '최대 시야',
+			'sidebar/object/intensity': '강도',
+			'sidebar/object/color': '색',
+			'sidebar/object/groundcolor': '지면 색',
+			'sidebar/object/distance': '거리',
+			'sidebar/object/angle': '각도',
+			'sidebar/object/penumbra': '반음영',
+			'sidebar/object/decay': '감쇠',
+			'sidebar/object/shadow': '그림자',
+			'sidebar/object/shadowIntensity': '그림자 강도',
+			'sidebar/object/shadowBias': '그림자 바이어스',
+			'sidebar/object/shadowNormalBias': '그림자 노멀 바이어스',
+			'sidebar/object/shadowRadius': '그림자 반지름',
+			'sidebar/object/cast': 'cast',
+			'sidebar/object/receive': 'receive',
+			'sidebar/object/visible': '보임',
+			'sidebar/object/frustumcull': '프러스텀 컬링',
+			'sidebar/object/renderorder': '렌더 순서',
+			'sidebar/object/userdata': '사용자 데이터',
+			'sidebar/object/export': 'JSON으로 내보내기',
+	
+			'sidebar/geometry/type': '타입',
+			'sidebar/geometry/new': '새로 만들기',
+			'sidebar/geometry/uuid': 'UUID',
+			'sidebar/geometry/name': '이름',
+			'sidebar/geometry/bounds': '경계',
+			'sidebar/geometry/userdata': '사용자 데이터',
+			'sidebar/geometry/show_vertex_normals': '버텍스 노멀 보기',
+			'sidebar/geometry/compute_vertex_normals': '버텍스 노멀 계산',
+			'sidebar/geometry/compute_vertex_tangents': '접선 계산',
+			'sidebar/geometry/center': '중앙',
+			'sidebar/geometry/export': 'JSON으로 내보내기',
+	
+			'sidebar/geometry/box_geometry/width': '너비',
+			'sidebar/geometry/box_geometry/height': '높이',
+			'sidebar/geometry/box_geometry/depth': '깊이',
+			'sidebar/geometry/box_geometry/widthseg': '너비 분할 수',
+			'sidebar/geometry/box_geometry/heightseg': '높이 분할 수',
+			'sidebar/geometry/box_geometry/depthseg': '깊이 분할 수',
+	
+			'sidebar/geometry/buffer_geometry/attributes': '속성',
+			'sidebar/geometry/buffer_geometry/index': '인덱스',
+			'sidebar/geometry/buffer_geometry/morphAttributes': '모프 속성',
+			'sidebar/geometry/buffer_geometry/morphRelative': '상대적 모프',
+	
+			'sidebar/geometry/capsule_geometry/radius': '반지름',
+			'sidebar/geometry/capsule_geometry/length': '길이',
+			'sidebar/geometry/capsule_geometry/capseg': '캡 분할 수',
+			'sidebar/geometry/capsule_geometry/radialseg': '방사 분할 수',
+	
+			'sidebar/geometry/circle_geometry/radius': '반지름',
+			'sidebar/geometry/circle_geometry/segments': '세그먼트',
+			'sidebar/geometry/circle_geometry/thetastart': '시작 각도',
+			'sidebar/geometry/circle_geometry/thetalength': '각도 길이',
+	
+			'sidebar/geometry/cylinder_geometry/radiustop': '상단 반지름',
+			'sidebar/geometry/cylinder_geometry/radiusbottom': '하단 반지름',
+			'sidebar/geometry/cylinder_geometry/height': '높이',
+			'sidebar/geometry/cylinder_geometry/radialsegments': '방사 분할 수',
+			'sidebar/geometry/cylinder_geometry/heightsegments': '높이 분할 수',
+			'sidebar/geometry/cylinder_geometry/openended': '끝 열림',
+	
+			'sidebar/geometry/extrude_geometry/curveSegments': '곡선 분할 수',
+			'sidebar/geometry/extrude_geometry/steps': '단계',
+			'sidebar/geometry/extrude_geometry/depth': '깊이',
+			'sidebar/geometry/extrude_geometry/bevelEnabled': '베벨 사용',
+			'sidebar/geometry/extrude_geometry/bevelThickness': '베벨 두께',
+			'sidebar/geometry/extrude_geometry/bevelSize': '베벨 크기',
+			'sidebar/geometry/extrude_geometry/bevelOffset': '베벨 오프셋',
+			'sidebar/geometry/extrude_geometry/bevelSegments': '베벨 분할 수',
+			'sidebar/geometry/extrude_geometry/shape': '형상으로 변환',
+	
+			'sidebar/geometry/dodecahedron_geometry/radius': '반지름',
+			'sidebar/geometry/dodecahedron_geometry/detail': '세부화',
+	
+			'sidebar/geometry/icosahedron_geometry/radius': '반지름',
+			'sidebar/geometry/icosahedron_geometry/detail': '세부화',
+	
+			'sidebar/geometry/octahedron_geometry/radius': '반지름',
+			'sidebar/geometry/octahedron_geometry/detail': '세부화',
+	
+			'sidebar/geometry/tetrahedron_geometry/radius': '반지름',
+			'sidebar/geometry/tetrahedron_geometry/detail': '세부화',
+	
+			'sidebar/geometry/lathe_geometry/segments': '분할 수',
+			'sidebar/geometry/lathe_geometry/phistart': '시작 각도',
+			'sidebar/geometry/lathe_geometry/philength': '각도 길이',
+			'sidebar/geometry/lathe_geometry/points': '포인트',
+	
+			'sidebar/geometry/plane_geometry/width': '너비',
+			'sidebar/geometry/plane_geometry/height': '높이',
+			'sidebar/geometry/plane_geometry/widthsegments': '너비 분할 수',
+			'sidebar/geometry/plane_geometry/heightsegments': '깊이 분할 수',
+	
+			'sidebar/geometry/ring_geometry/innerRadius': '내부 반지름',
+			'sidebar/geometry/ring_geometry/outerRadius': '외부 반지름',
+			'sidebar/geometry/ring_geometry/thetaSegments': '원 분할 수',
+			'sidebar/geometry/ring_geometry/phiSegments': '링 분할 수',
+			'sidebar/geometry/ring_geometry/thetastart': '시작 각도',
+			'sidebar/geometry/ring_geometry/thetalength': '각도 길이',
+	
+			'sidebar/geometry/shape_geometry/curveSegments': '곡선 분할 수',
+			'sidebar/geometry/shape_geometry/extrude': '압출',
+	
+			'sidebar/geometry/sphere_geometry/radius': '반지름',
+			'sidebar/geometry/sphere_geometry/widthsegments': '원 분할 수',
+			'sidebar/geometry/sphere_geometry/heightsegments': '링 분할 수',
+			'sidebar/geometry/sphere_geometry/phistart': '시작 각도',
+			'sidebar/geometry/sphere_geometry/philength': '각도 길이',
+			'sidebar/geometry/sphere_geometry/thetastart': '시작 각도',
+			'sidebar/geometry/sphere_geometry/thetalength': '각도 길이',
+	
+			'sidebar/geometry/torus_geometry/radius': '반지름',
+			'sidebar/geometry/torus_geometry/tube': '튜브 두께',
+			'sidebar/geometry/torus_geometry/radialsegments': '소 분할 수',
+			'sidebar/geometry/torus_geometry/tubularsegments': '대 분할 수',
+			'sidebar/geometry/torus_geometry/arc': '호',
+	
+			'sidebar/geometry/torusKnot_geometry/radius': '반지름',
+			'sidebar/geometry/torusKnot_geometry/tube': '튜브 두께',
+			'sidebar/geometry/torusKnot_geometry/tubularsegments': '소 분할 수',
+			'sidebar/geometry/torusKnot_geometry/radialsegments': '대 분할 수',
+			'sidebar/geometry/torusKnot_geometry/p': 'P',
+			'sidebar/geometry/torusKnot_geometry/q': 'Q',
+	
+			'sidebar/geometry/tube_geometry/path': '경로',
+			'sidebar/geometry/tube_geometry/radius': '반지름',
+			'sidebar/geometry/tube_geometry/tube': '튜브 두께',
+			'sidebar/geometry/tube_geometry/tubularsegments': '소 분할 수',
+			'sidebar/geometry/tube_geometry/radialsegments': '대 분할 수',
+			'sidebar/geometry/tube_geometry/closed': '닫기',
+			'sidebar/geometry/tube_geometry/curvetype': '곡선 타입',
+			'sidebar/geometry/tube_geometry/tension': '텐션',
+	
+			'sidebar/material/new': '새로 만들기',
+			'sidebar/material/copy': '복사',
+			'sidebar/material/paste': '붙여넣기',
+			'sidebar/material/slot': '슬롯',
+			'sidebar/material/type': '타입',
+			'sidebar/material/uuid': 'UUID',
+			'sidebar/material/name': '이름',
+			'sidebar/material/program': '프로그램',
+			'sidebar/material/info': '정보',
+			'sidebar/material/vertex': '버텍스',
+			'sidebar/material/fragment': '프래그먼트',
+			'sidebar/material/color': '색',
+			'sidebar/material/depthPacking': '깊이 패킹',
+			'sidebar/material/roughness': '거칠기',
+			'sidebar/material/metalness': '금속성',
+			'sidebar/material/reflectivity': '반사율',
+			'sidebar/material/emissive': '발광',
+			'sidebar/material/specular': '스펙큘러',
+			'sidebar/material/shininess': '광택',
+			'sidebar/material/clearcoat': '클리어 코트',
+			'sidebar/material/clearcoatroughness': '클리어 코트 거칠기',
+			'sidebar/material/dispersion': '분산',
+			'sidebar/material/ior': '굴절률',
+			'sidebar/material/iridescence': '훈색',
+			'sidebar/material/iridescenceIOR': '훈색 굴절률',
+			'sidebar/material/iridescenceThicknessMax': '훈색 두께',
+			'sidebar/material/sheen': '광택',
+			'sidebar/material/sheenroughness': '광택 거칠기',
+			'sidebar/material/sheencolor': '광택 색상',
+			'sidebar/material/transmission': '투명도',
+			'sidebar/material/attenuationDistance': '감쇠 거리',
+			'sidebar/material/attenuationColor': '감쇠 색상',
+			'sidebar/material/thickness': '두께',
+			'sidebar/material/vertexcolors': '버텍스 색상',
+			'sidebar/material/matcap': '매트 캡',
+			'sidebar/material/map': '맵',
+			'sidebar/material/alphamap': '알파맵',
+			'sidebar/material/bumpmap': '범프맵',
+			'sidebar/material/normalmap': '노멀맵',
+			'sidebar/material/clearcoatmap': '클리어 코트 맵',
+			'sidebar/material/clearcoatnormalmap': '클리어 코트 노멀맵',
+			'sidebar/material/clearcoatroughnessmap': '클리어 코트 거칠기 맵',
+			'sidebar/material/displacementmap': '변위 맵',
+			'sidebar/material/roughnessmap': '거칠기 맵',
+			'sidebar/material/metalnessmap': '금속성 맵',
+			'sidebar/material/specularmap': '스펙큘러 맵',
+			'sidebar/material/iridescencemap': '훈색 맵',
+			'sidebar/material/iridescencethicknessmap': '훈색 두께 맵',
+			'sidebar/material/sheencolormap': '광택 색상 맵',
+			'sidebar/material/sheenroughnessmap': '광택 거칠기 맵',
+			'sidebar/material/envmap': '환경 맵',
+			'sidebar/material/lightmap': '조명 맵',
+			'sidebar/material/aomap': 'AO 맵',
+			'sidebar/material/emissivemap': '발광 맵',
+			'sidebar/material/gradientmap': '그라디언트 맵',
+			'sidebar/material/transmissionmap': '투명 맵',
+			'sidebar/material/thicknessmap': '두께 맵',
+			'sidebar/material/side': '측면',
+			'sidebar/material/size': '크기',
+			'sidebar/material/sizeAttenuation': '크기 감쇠',
+			'sidebar/material/flatShading': '플랫 셰이딩',
+			'sidebar/material/blending': '블렌딩',
+			'sidebar/material/opacity': '불투명도',
+			'sidebar/material/transparent': '투명',
+			'sidebar/material/forcesinglepass': '단일 패스 강제',
+			'sidebar/material/alphatest': '알파 테스트',
+			'sidebar/material/depthtest': '깊이 테스트',
+			'sidebar/material/depthwrite': '깊이 쓰기',
+			'sidebar/material/wireframe': '와이어프레임',
+			'sidebar/material/userdata': '사용자 데이터',
+			'sidebar/material/export': 'JSON으로 내보내기',
+	
+			'sidebar/script/new': '새로 만들기',
+			'sidebar/script/edit': '편집',
+			'sidebar/script/remove': '삭제',
+	
+			'sidebar/project': '프로젝트',
+			'sidebar/project/antialias': '안티앨리어싱',
+			'sidebar/project/shadows': '그림자',
+			'sidebar/project/toneMapping': '톤 매핑',
+			'sidebar/project/materials': '머티리얼',
+			'sidebar/project/Assign': '할당',
+	
+			'sidebar/project/app': '앱',
+			'sidebar/project/app/play': '재생',
+			'sidebar/project/app/stop': '정지',
+			'sidebar/project/app/title': '제목',
+			'sidebar/project/app/editable': '편집 가능',
+			'sidebar/project/app/publish': '앱 파일로 저장',
+	
+			'sidebar/project/image': '이미지',
+			'sidebar/project/image/samples': '샘플',
+			'sidebar/project/video': '비디오',
+	
+			'sidebar/project/shading': '셰이딩',
+			'sidebar/project/resolution': '해상도',
+			'sidebar/project/duration': '길이',
+			'sidebar/project/render': '렌더',
+	
+			'sidebar/settings': '설정',
+			'sidebar/settings/language': '언어',
+	
+			'sidebar/settings/shortcuts': '단축키',
+			'sidebar/settings/shortcuts/translate': '이동',
+			'sidebar/settings/shortcuts/rotate': '회전',
+			'sidebar/settings/shortcuts/scale': '스케일',
+			'sidebar/settings/shortcuts/undo': '되돌리기',
+			'sidebar/settings/shortcuts/focus': '포커스',
+	
+			'sidebar/history': '기록',
+			'sidebar/history/clear': '지우기',
+			'sidebar/history/persistent': '영구적',
+	
+			'toolbar/translate': '이동',
+			'toolbar/rotate': '회전',
+			'toolbar/scale': '스케일',
+			'toolbar/local': '로컬',
+	
+			'viewport/controls/grid': '그리드',
+			'viewport/controls/helpers': '도우미 보기',
+	
+			'viewport/info/object': '객체',
+			'viewport/info/objects': '객체',
+			'viewport/info/vertex': '버텍스',
+			'viewport/info/vertices': '버텍스',
+			'viewport/info/triangle': '삼각형',
+			'viewport/info/triangles': '삼각형',
+			'viewport/info/sample': '샘플',
+			'viewport/info/samples': '샘플',
+			'viewport/info/rendertime': '렌더링 시간',
+	
+			'script/title/vertexShader': '버텍스 셰이더',
+			'script/title/fragmentShader': '프래그먼트 셰이더',
+			'script/title/programInfo': '프로그램 속성'
+		}
 	};
 
 	return {

+ 27 - 6
examples/files.json

@@ -91,7 +91,6 @@
 		"webgl_loader_gltf_dispersion",
 		"webgl_loader_gltf_instancing",
 		"webgl_loader_gltf_iridescence",
-		"webgl_loader_gltf_lights",
 		"webgl_loader_gltf_sheen",
 		"webgl_loader_gltf_transmission",
 		"webgl_loader_gltf_variants",
@@ -122,7 +121,6 @@
 		"webgl_loader_texture_hdr",
 		"webgl_loader_texture_ktx",
 		"webgl_loader_texture_ktx2",
-		"webgl_loader_texture_logluv",
 		"webgl_loader_texture_lottie",
 		"webgl_loader_texture_pvrtc",
 		"webgl_loader_texture_rgbm",
@@ -147,7 +145,6 @@
 		"webgl_materials_cubemap_refraction",
 		"webgl_materials_cubemap_mipmaps",
 		"webgl_materials_cubemap_render_to_mipmaps",
-		"webgl_materials_curvature",
 		"webgl_materials_displacementmap",
 		"webgl_materials_envmaps",
 		"webgl_materials_envmaps_exr",
@@ -301,7 +298,8 @@
 		"webgl_volume_cloud",
 		"webgl_volume_instancing",
 		"webgl_volume_perlin",
-		"webgl_worker_offscreencanvas"
+		"webgl_worker_offscreencanvas",
+		"webgl_performance"
 	],
 	"webgpu (wip)": [
 		"webgpu_backdrop",
@@ -311,6 +309,7 @@
 		"webgpu_clearcoat",
 		"webgpu_clipping",
 		"webgpu_compute_audio",
+		"webgpu_compute_birds",
 		"webgpu_compute_geometry",
 		"webgpu_compute_particles",
 		"webgpu_compute_particles_rain",
@@ -324,11 +323,13 @@
 		"webgpu_custom_fog",
 		"webgpu_custom_fog_background",
 		"webgpu_depth_texture",
+		"webgpu_display_stereo",
 		"webgpu_equirectangular",
 		"webgpu_instance_mesh",
 		"webgpu_instance_points",
 		"webgpu_instance_uniform",
 		"webgpu_instancing_morph",
+		"webgpu_lightprobe",
 		"webgpu_lights_custom",
 		"webgpu_lights_ies_spotlight",
 		"webgpu_lights_phong",
@@ -346,6 +347,7 @@
 		"webgpu_materials",
 		"webgpu_materials_basic",
 		"webgpu_materials_displacementmap",
+		"webgpu_materials_envmaps",
 		"webgpu_materials_lightmap",
 		"webgpu_materials_matcap",
 		"webgpu_materials_sss",
@@ -363,8 +365,10 @@
 		"webgpu_multiple_rendertargets_readback",
 		"webgpu_multisampled_renderbuffers",
 		"webgpu_occlusion",
+		"webgpu_ocean",
 		"webgpu_parallax_uv",
 		"webgpu_particles",
+		"webgpu_performance",
 		"webgpu_performance_renderbundle",
 		"webgpu_pmrem_cubemap",
 		"webgpu_pmrem_equirectangular",
@@ -377,10 +381,14 @@
 		"webgpu_postprocessing_bloom",
 		"webgpu_postprocessing_bloom_emissive",
 		"webgpu_postprocessing_bloom_selective",
+		"webgpu_postprocessing_difference",
 		"webgpu_postprocessing_dof",
 		"webgpu_postprocessing_pixel",
 		"webgpu_postprocessing_fxaa",
+		"webgpu_postprocessing_masking",
+		"webgpu_postprocessing_motion_blur",
 		"webgpu_postprocessing_sobel",
+		"webgpu_postprocessing_ssaa",
 		"webgpu_postprocessing_transition",
 		"webgpu_postprocessing",
 		"webgpu_procedural_texture",
@@ -389,23 +397,36 @@
 		"webgpu_rtt",
 		"webgpu_sandbox",
 		"webgpu_shadertoy",
-		"webgpu_tsl_interoperability",
 		"webgpu_shadowmap",
+		"webgpu_shadowmap_opacity",
 		"webgpu_skinning",
 		"webgpu_skinning_instancing",
 		"webgpu_skinning_points",
+		"webgpu_sky",
 		"webgpu_sprites",
 		"webgpu_storage_buffer",
 		"webgpu_texturegrad",
 		"webgpu_textures_2d-array",
+		"webgpu_textures_2d-array_compressed",
 		"webgpu_textures_anisotropy",
 		"webgpu_textures_partialupdate",
+		"webgpu_tsl_angular_slicing",
+		"webgpu_tsl_coffee_smoke",
+		"webgpu_tsl_compute_attractors_particles",
+		"webgpu_tsl_earth",
 		"webgpu_tsl_editor",
 		"webgpu_tsl_galaxy",
+		"webgpu_tsl_halftone",
+		"webgpu_tsl_interoperability",
+		"webgpu_tsl_procedural_terrain",
+		"webgpu_tsl_raging_sea",
 		"webgpu_tsl_transpiler",
+		"webgpu_tsl_vfx_flames",
+		"webgpu_tsl_vfx_tornado",
 		"webgpu_video_panorama",
 		"webgpu_volume_cloud",
-		"webgpu_volume_perlin"
+		"webgpu_volume_perlin",
+		"webgpu_water"
 	],
 	"webaudio": [
 		"webaudio_orientation",

+ 3 - 1
examples/jsm/Addons.js

@@ -104,7 +104,6 @@ export * from './loaders/LDrawLoader.js';
 export * from './loaders/LUT3dlLoader.js';
 export * from './loaders/LUTCubeLoader.js';
 export * from './loaders/LWOLoader.js';
-export * from './loaders/LogLuvLoader.js';
 export * from './loaders/LottieLoader.js';
 export * from './loaders/MD2Loader.js';
 export * from './loaders/MDDLoader.js';
@@ -173,6 +172,9 @@ export * from './objects/ShadowMesh.js';
 export * from './objects/Sky.js';
 export * from './objects/Water.js';
 export { Water as Water2 } from './objects/Water2.js';
+export * from './objects/SkyMesh.js';
+export * from './objects/WaterMesh.js';
+export { WaterMesh as Water2Mesh } from './objects/Water2Mesh.js';
 
 export * from './physics/AmmoPhysics.js';
 export * from './physics/RapierPhysics.js';

+ 1 - 1
examples/jsm/animation/AnimationClipCreator.js

@@ -92,7 +92,7 @@ class AnimationClipCreator {
 	static CreateMaterialColorAnimation( duration, colors ) {
 
 		const times = [], values = [],
-			timeStep = duration / colors.length;
+			timeStep = ( colors.length > 1 ) ? duration / ( colors.length - 1 ) : 0;
 
 		for ( let i = 0; i < colors.length; i ++ ) {
 

+ 27 - 21
examples/jsm/capabilities/WebGL.js

@@ -1,20 +1,5 @@
 class WebGL {
 
-	static isWebGLAvailable() {
-
-		try {
-
-			const canvas = document.createElement( 'canvas' );
-			return !! ( window.WebGLRenderingContext && ( canvas.getContext( 'webgl' ) || canvas.getContext( 'experimental-webgl' ) ) );
-
-		} catch ( e ) {
-
-			return false;
-
-		}
-
-	}
-
 	static isWebGL2Available() {
 
 		try {
@@ -47,12 +32,6 @@ class WebGL {
 
 	}
 
-	static getWebGLErrorMessage() {
-
-		return this.getErrorMessage( 1 );
-
-	}
-
 	static getWebGL2ErrorMessage() {
 
 		return this.getErrorMessage( 2 );
@@ -103,6 +82,33 @@ class WebGL {
 
 	}
 
+	// @deprecated, r168
+
+	static isWebGLAvailable() {
+
+		console.warn( 'isWebGLAvailable() has been deprecated and will be removed in r178. Use isWebGL2Available() instead.' );
+
+		try {
+
+			const canvas = document.createElement( 'canvas' );
+			return !! ( window.WebGLRenderingContext && ( canvas.getContext( 'webgl' ) || canvas.getContext( 'experimental-webgl' ) ) );
+
+		} catch ( e ) {
+
+			return false;
+
+		}
+
+	}
+
+	static getWebGLErrorMessage() {
+
+		console.warn( 'getWebGLErrorMessage() has been deprecated and will be removed in r178. Use getWebGL2ErrorMessage() instead.' );
+
+		return this.getErrorMessage( 1 );
+
+	}
+
 }
 
 export default WebGL;

+ 174 - 158
examples/jsm/controls/ArcballControls.js

@@ -1,4 +1,5 @@
 import {
+	Controls,
 	GridHelper,
 	EllipseCurve,
 	BufferGeometry,
@@ -12,8 +13,7 @@ import {
 	Vector2,
 	Vector3,
 	Matrix4,
-	MathUtils,
-	EventDispatcher
+	MathUtils
 } from 'three';
 
 //trackball state
@@ -76,13 +76,12 @@ const _scalePointTemp = new Vector3();
  * @param {HTMLElement} domElement Renderer's dom element
  * @param {Scene} scene The scene to be rendered
  */
-class ArcballControls extends EventDispatcher {
+class ArcballControls extends Controls {
 
-	constructor( camera, domElement, scene = null ) {
+	constructor( camera, domElement = null, scene = null ) {
+
+		super( camera, domElement );
 
-		super();
-		this.camera = null;
-		this.domElement = domElement;
 		this.scene = scene;
 		this.target = new Vector3();
 		this._currentTarget = new Vector3();
@@ -201,7 +200,6 @@ class ArcballControls extends EventDispatcher {
 		this.maxFov = 90;
 		this.rotateSpeed = 1;
 
-		this.enabled = true;
 		this.enablePan = true;
 		this.enableRotate = true;
 		this.enableZoom = true;
@@ -226,11 +224,10 @@ class ArcballControls extends EventDispatcher {
 
 		}
 
-		this.domElement.style.touchAction = 'none';
-		this._devPxRatio = window.devicePixelRatio;
-
 		this.initializeMouseActions();
 
+		// event listeners
+
 		this._onContextMenu = onContextMenu.bind( this );
 		this._onWheel = onWheel.bind( this );
 		this._onPointerUp = onPointerUp.bind( this );
@@ -239,6 +236,19 @@ class ArcballControls extends EventDispatcher {
 		this._onPointerCancel = onPointerCancel.bind( this );
 		this._onWindowResize = onWindowResize.bind( this );
 
+		if ( domElement !== null ) {
+
+			this.connect();
+
+		}
+
+	}
+
+	connect() {
+
+		this.domElement.style.touchAction = 'none';
+		this._devPxRatio = window.devicePixelRatio;
+
 		this.domElement.addEventListener( 'contextmenu', this._onContextMenu );
 		this.domElement.addEventListener( 'wheel', this._onWheel );
 		this.domElement.addEventListener( 'pointerdown', this._onPointerDown );
@@ -248,6 +258,20 @@ class ArcballControls extends EventDispatcher {
 
 	}
 
+	disconnect() {
+
+		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 );
+
+		window.removeEventListener( 'resize', this._onWindowResize );
+
+	}
+
 	onSinglePanStart( event, operation ) {
 
 		if ( this.enabled ) {
@@ -278,7 +302,7 @@ class ArcballControls extends EventDispatcher {
 					}
 
 					this.updateTbState( STATE.PAN, true );
-					this._startCursorPosition.copy( this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement ) );
+					this._startCursorPosition.copy( this.unprojectOnTbPlane( this.object, _center.x, _center.y, this.domElement ) );
 					if ( this.enableGrid ) {
 
 						this.drawGrid();
@@ -305,7 +329,7 @@ class ArcballControls extends EventDispatcher {
 					}
 
 					this.updateTbState( STATE.ROTATE, true );
-					this._startCursorPosition.copy( this.unprojectOnTbSurface( this.camera, _center.x, _center.y, this.domElement, this._tbRadius ) );
+					this._startCursorPosition.copy( this.unprojectOnTbSurface( this.object, _center.x, _center.y, this.domElement, this._tbRadius ) );
 					this.activateGizmos( true );
 					if ( this.enableAnimations ) {
 
@@ -323,7 +347,7 @@ class ArcballControls extends EventDispatcher {
 
 				case 'FOV':
 
-					if ( ! this.camera.isPerspectiveCamera || ! this.enableZoom ) {
+					if ( ! this.object.isPerspectiveCamera || ! this.enableZoom ) {
 
 						return;
 
@@ -396,7 +420,7 @@ class ArcballControls extends EventDispatcher {
 							this.dispatchEvent( _startEvent );
 
 							this.updateTbState( opState, true );
-							this._startCursorPosition.copy( this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement ) );
+							this._startCursorPosition.copy( this.unprojectOnTbPlane( this.object, _center.x, _center.y, this.domElement ) );
 							if ( this.enableGrid ) {
 
 								this.drawGrid();
@@ -408,7 +432,7 @@ class ArcballControls extends EventDispatcher {
 						} else {
 
 							//continue with pan operation
-							this._currentCursorPosition.copy( this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement ) );
+							this._currentCursorPosition.copy( this.unprojectOnTbPlane( this.object, _center.x, _center.y, this.domElement ) );
 							this.applyTransformMatrix( this.pan( this._startCursorPosition, this._currentCursorPosition ) );
 
 						}
@@ -429,7 +453,7 @@ class ArcballControls extends EventDispatcher {
 							this.dispatchEvent( _startEvent );
 
 							this.updateTbState( opState, true );
-							this._startCursorPosition.copy( this.unprojectOnTbSurface( this.camera, _center.x, _center.y, this.domElement, this._tbRadius ) );
+							this._startCursorPosition.copy( this.unprojectOnTbSurface( this.object, _center.x, _center.y, this.domElement, this._tbRadius ) );
 
 							if ( this.enableGrid ) {
 
@@ -442,7 +466,7 @@ class ArcballControls extends EventDispatcher {
 						} else {
 
 							//continue with rotate operation
-							this._currentCursorPosition.copy( this.unprojectOnTbSurface( this.camera, _center.x, _center.y, this.domElement, this._tbRadius ) );
+							this._currentCursorPosition.copy( this.unprojectOnTbSurface( this.object, _center.x, _center.y, this.domElement, this._tbRadius ) );
 
 							const distance = this._startCursorPosition.distanceTo( this._currentCursorPosition );
 							const angle = this._startCursorPosition.angleTo( this._currentCursorPosition );
@@ -524,7 +548,7 @@ class ArcballControls extends EventDispatcher {
 
 				case STATE.FOV:
 
-					if ( this.enableZoom && this.camera.isPerspectiveCamera ) {
+					if ( this.enableZoom && this.object.isPerspectiveCamera ) {
 
 						if ( restart ) {
 
@@ -588,7 +612,7 @@ class ArcballControls extends EventDispatcher {
 							this.applyTransformMatrix( this.scale( size, this._v3_2, false ) );
 
 							//adjusting distance
-							_offset.copy( this._gizmos.position ).sub( this.camera.position ).normalize().multiplyScalar( newDistance / x );
+							_offset.copy( this._gizmos.position ).sub( this.object.position ).normalize().multiplyScalar( newDistance / x );
 							this._m4_1.makeTranslation( _offset.x, _offset.y, _offset.z );
 
 						}
@@ -678,7 +702,7 @@ class ArcballControls extends EventDispatcher {
 			this.dispatchEvent( _startEvent );
 
 			this.setCenter( event.clientX, event.clientY );
-			const hitP = this.unprojectOnObj( this.getCursorNDC( _center.x, _center.y, this.domElement ), this.camera );
+			const hitP = this.unprojectOnObj( this.getCursorNDC( _center.x, _center.y, this.domElement ), this.object );
 
 			if ( hitP != null && this.enableAnimations ) {
 
@@ -721,7 +745,7 @@ class ArcballControls extends EventDispatcher {
 			this.updateTbState( STATE.PAN, true );
 
 			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._startCursorPosition.copy( this.unprojectOnTbPlane( this.object, _center.x, _center.y, this.domElement, true ) );
 			this._currentCursorPosition.copy( this._startCursorPosition );
 
 			this.activateGizmos( false );
@@ -743,7 +767,7 @@ class ArcballControls extends EventDispatcher {
 
 			}
 
-			this._currentCursorPosition.copy( this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement, true ) );
+			this._currentCursorPosition.copy( this.unprojectOnTbPlane( this.object, _center.x, _center.y, this.domElement, true ) );
 			this.applyTransformMatrix( this.pan( this._startCursorPosition, this._currentCursorPosition, true ) );
 			this.dispatchEvent( _changeEvent );
 
@@ -771,7 +795,7 @@ class ArcballControls extends EventDispatcher {
 			this._startFingerRotation = this.getAngle( this._touchCurrent[ 1 ], this._touchCurrent[ 0 ] ) + this.getAngle( this._touchStart[ 1 ], this._touchStart[ 0 ] );
 			this._currentFingerRotation = this._startFingerRotation;
 
-			this.camera.getWorldDirection( this._rotationAxis ); //rotation axis
+			this.object.getWorldDirection( this._rotationAxis ); //rotation axis
 
 			if ( ! this.enablePan && ! this.enableZoom ) {
 
@@ -807,7 +831,7 @@ class ArcballControls extends EventDispatcher {
 			} 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 );
+				rotationPoint = this.unprojectOnTbPlane( this.object, _center.x, _center.y, this.domElement ).applyQuaternion( this.object.quaternion ).multiplyScalar( 1 / this.object.zoom ).add( this._v3_2 );
 
 			}
 
@@ -869,17 +893,17 @@ class ArcballControls extends EventDispatcher {
 
 			} else {
 
-				if ( this.camera.isOrthographicCamera ) {
+				if ( this.object.isOrthographicCamera ) {
 
-					scalePoint = this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement )
-						.applyQuaternion( this.camera.quaternion )
-						.multiplyScalar( 1 / this.camera.zoom )
+					scalePoint = this.unprojectOnTbPlane( this.object, _center.x, _center.y, this.domElement )
+						.applyQuaternion( this.object.quaternion )
+						.multiplyScalar( 1 / this.object.zoom )
 						.add( this._gizmos.position );
 
-				} else if ( this.camera.isPerspectiveCamera ) {
+				} else if ( this.object.isPerspectiveCamera ) {
 
-					scalePoint = this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement )
-						.applyQuaternion( this.camera.quaternion )
+					scalePoint = this.unprojectOnTbPlane( this.object, _center.x, _center.y, this.domElement )
+						.applyQuaternion( this.object.quaternion )
 						.add( this._gizmos.position );
 
 				}
@@ -997,7 +1021,7 @@ class ArcballControls extends EventDispatcher {
 			this.applyTransformMatrix( this.scale( size, this._v3_2, false ) );
 
 			//adjusting distance
-			_offset.copy( this._gizmos.position ).sub( this.camera.position ).normalize().multiplyScalar( newDistance / x );
+			_offset.copy( this._gizmos.position ).sub( this.object.position ).normalize().multiplyScalar( newDistance / x );
 			this._m4_1.makeTranslation( _offset.x, _offset.y, _offset.z );
 
 			this.dispatchEvent( _changeEvent );
@@ -1297,13 +1321,13 @@ class ArcballControls extends EventDispatcher {
 		if ( transformation.camera != null ) {
 
 			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();
+			this._m4_1.decompose( this.object.position, this.object.quaternion, this.object.scale );
+			this.object.updateMatrix();
 
 			//update camera up vector
 			if ( this._state == STATE.ROTATE || this._state == STATE.ZROTATE || this._state == STATE.ANIMATION_ROTATE ) {
 
-				this.camera.up.copy( this._upState ).applyQuaternion( this.camera.quaternion );
+				this.object.up.copy( this._upState ).applyQuaternion( this.object.quaternion );
 
 			}
 
@@ -1319,11 +1343,11 @@ class ArcballControls extends EventDispatcher {
 
 		if ( this._state == STATE.SCALE || this._state == STATE.FOCUS || this._state == STATE.ANIMATION_FOCUS ) {
 
-			this._tbRadius = this.calculateTbRadius( this.camera );
+			this._tbRadius = this.calculateTbRadius( this.object );
 
 			if ( this.adjustNearFar ) {
 
-				const cameraDistance = this.camera.position.distanceTo( this._gizmos.position );
+				const cameraDistance = this.object.position.distanceTo( this._gizmos.position );
 
 				const bb = new Box3();
 				bb.setFromObject( this._gizmos );
@@ -1334,38 +1358,38 @@ class ArcballControls extends EventDispatcher {
 				const regularNearPosition = cameraDistance - this._initialNear;
 
 				const minNearPos = Math.min( adjustedNearPosition, regularNearPosition );
-				this.camera.near = cameraDistance - minNearPos;
+				this.object.near = cameraDistance - minNearPos;
 
 
 				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;
+				this.object.far = cameraDistance - minFarPos;
 
-				this.camera.updateProjectionMatrix();
+				this.object.updateProjectionMatrix();
 
 			} else {
 
 				let update = false;
 
-				if ( this.camera.near != this._initialNear ) {
+				if ( this.object.near != this._initialNear ) {
 
-					this.camera.near = this._initialNear;
+					this.object.near = this._initialNear;
 					update = true;
 
 				}
 
-				if ( this.camera.far != this._initialFar ) {
+				if ( this.object.far != this._initialFar ) {
 
-					this.camera.far = this._initialFar;
+					this.object.far = this._initialFar;
 					update = true;
 
 				}
 
 				if ( update ) {
 
-					this.camera.updateProjectionMatrix();
+					this.object.updateProjectionMatrix();
 
 				}
 
@@ -1465,7 +1489,7 @@ class ArcballControls extends EventDispatcher {
 
 		_cameraMatrixStateTemp.copy( this._cameraMatrixState );
 		this._cameraMatrixState.premultiply( this._translationMatrix );
-		this._cameraMatrixState.decompose( this.camera.position, this.camera.quaternion, this.camera.scale );
+		this._cameraMatrixState.decompose( this.object.position, this.object.quaternion, this.object.scale );
 
 		//apply zoom
 		if ( this.enableZoom ) {
@@ -1490,22 +1514,22 @@ class ArcballControls extends EventDispatcher {
 			const multiplier = 3;
 			let size, divisions, maxLength, tick;
 
-			if ( this.camera.isOrthographicCamera ) {
+			if ( this.object.isOrthographicCamera ) {
 
-				const width = this.camera.right - this.camera.left;
-				const height = this.camera.bottom - this.camera.top;
+				const width = this.object.right - this.object.left;
+				const height = this.object.bottom - this.object.top;
 
 				maxLength = Math.max( width, height );
 				tick = maxLength / 20;
 
-				size = maxLength / this.camera.zoom * multiplier;
-				divisions = size / tick * this.camera.zoom;
+				size = maxLength / this.object.zoom * multiplier;
+				divisions = size / tick * this.object.zoom;
 
-			} else if ( this.camera.isPerspectiveCamera ) {
+			} else if ( this.object.isPerspectiveCamera ) {
 
-				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 ) );
+				const distance = this.object.position.distanceTo( this._gizmos.position );
+				const halfFovV = MathUtils.DEG2RAD * this.object.fov * 0.5;
+				const halfFovH = Math.atan( ( this.object.aspect ) * Math.tan( halfFovV ) );
 
 				maxLength = Math.tan( Math.max( halfFovV, halfFovH ) ) * distance * 2;
 				tick = maxLength / 20;
@@ -1520,7 +1544,7 @@ class ArcballControls extends EventDispatcher {
 				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.quaternion.copy( this.object.quaternion );
 				this._grid.rotateX( Math.PI * 0.5 );
 
 				this.scene.add( this._grid );
@@ -1542,15 +1566,7 @@ class ArcballControls extends EventDispatcher {
 
 		}
 
-		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 );
-
-		window.removeEventListener( 'resize', this._onWindowResize );
+		this.disconnect();
 
 		if ( this.scene !== null ) this.scene.remove( this._gizmos );
 		this.disposeGrid();
@@ -1634,8 +1650,8 @@ class ArcballControls extends EventDispatcher {
 	getCursorPosition( cursorX, cursorY, canvas ) {
 
 		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;
+		this._v2_1.x *= ( this.object.right - this.object.left ) * 0.5;
+		this._v2_1.y *= ( this.object.top - this.object.bottom ) * 0.5;
 		return this._v2_1.clone();
 
 	}
@@ -1674,8 +1690,8 @@ class ArcballControls extends EventDispatcher {
 		this._up0.copy( camera.up );
 		this._upState.copy( camera.up );
 
-		this.camera = camera;
-		this.camera.updateProjectionMatrix();
+		this.object = camera;
+		this.object.updateProjectionMatrix();
 
 		//making gizmos
 		this._tbRadius = this.calculateTbRadius( camera );
@@ -1701,7 +1717,7 @@ class ArcballControls extends EventDispatcher {
 	setTbRadius( value ) {
 
 		this.radiusFactor = value;
-		this._tbRadius = this.calculateTbRadius( this.camera );
+		this._tbRadius = this.calculateTbRadius( this.object );
 
 		const curve = new EllipseCurve( 0, 0, this._tbRadius, this._tbRadius );
 		const points = curve.getPoints( this._curvePts );
@@ -1750,10 +1766,10 @@ class ArcballControls extends EventDispatcher {
 		this._gizmoMatrixState0.identity().setPosition( tbCenter );
 		this._gizmoMatrixState.copy( this._gizmoMatrixState0 );
 
-		if ( this.camera.zoom !== 1 ) {
+		if ( this.object.zoom !== 1 ) {
 
 			//adapt gizmos size to camera zoom
-			const size = 1 / this.camera.zoom;
+			const size = 1 / this.object.zoom;
 			this._scaleMatrix.makeScale( size, size, size );
 			this._translationMatrix.makeTranslation( - tbCenter.x, - tbCenter.y, - tbCenter.z );
 
@@ -1931,22 +1947,22 @@ class ArcballControls extends EventDispatcher {
 
 		const movement = p0.clone().sub( p1 );
 
-		if ( this.camera.isOrthographicCamera ) {
+		if ( this.object.isOrthographicCamera ) {
 
 			//adjust movement amount
-			movement.multiplyScalar( 1 / this.camera.zoom );
+			movement.multiplyScalar( 1 / this.object.zoom );
 
-		} else if ( this.camera.isPerspectiveCamera && adjust ) {
+		} else if ( this.object.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 );
+			const distanceFactor = this._v3_1.distanceTo( this._v3_2 ) / this.object.position.distanceTo( this._gizmos.position );
 			movement.multiplyScalar( 1 / distanceFactor );
 
 		}
 
-		this._v3_1.set( movement.x, movement.y, 0 ).applyQuaternion( this.camera.quaternion );
+		this._v3_1.set( movement.x, movement.y, 0 ).applyQuaternion( this.object.quaternion );
 
 		this._m4_1.makeTranslation( this._v3_1.x, this._v3_1.y, this._v3_1.z );
 
@@ -1960,31 +1976,31 @@ class ArcballControls extends EventDispatcher {
 	 */
 	reset() {
 
-		this.camera.zoom = this._zoom0;
+		this.object.zoom = this._zoom0;
 
-		if ( this.camera.isPerspectiveCamera ) {
+		if ( this.object.isPerspectiveCamera ) {
 
-			this.camera.fov = this._fov0;
+			this.object.fov = this._fov0;
 
 		}
 
-		this.camera.near = this._nearPos;
-		this.camera.far = this._farPos;
+		this.object.near = this._nearPos;
+		this.object.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._cameraMatrixState.decompose( this.object.position, this.object.quaternion, this.object.scale );
+		this.object.up.copy( this._up0 );
 
-		this.camera.updateMatrix();
-		this.camera.updateProjectionMatrix();
+		this.object.updateMatrix();
+		this.object.updateProjectionMatrix();
 
 		this._gizmoMatrixState.copy( this._gizmoMatrixState0 );
 		this._gizmoMatrixState0.decompose( this._gizmos.position, this._gizmos.quaternion, this._gizmos.scale );
 		this._gizmos.updateMatrix();
 
-		this._tbRadius = this.calculateTbRadius( this.camera );
+		this._tbRadius = this.calculateTbRadius( this.object );
 		this.makeGizmos( this._gizmos.position, this._tbRadius );
 
-		this.camera.lookAt( this._gizmos.position );
+		this.object.lookAt( this._gizmos.position );
 
 		this.updateTbState( STATE.IDLE, false );
 
@@ -2018,28 +2034,28 @@ class ArcballControls extends EventDispatcher {
 	copyState() {
 
 		let state;
-		if ( this.camera.isOrthographicCamera ) {
+		if ( this.object.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,
+				cameraFar: this.object.far,
+				cameraMatrix: this.object.matrix,
+				cameraNear: this.object.near,
+				cameraUp: this.object.up,
+				cameraZoom: this.object.zoom,
 				gizmoMatrix: this._gizmos.matrix
 
 			} } );
 
-		} else if ( this.camera.isPerspectiveCamera ) {
+		} else if ( this.object.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,
+				cameraFar: this.object.far,
+				cameraFov: this.object.fov,
+				cameraMatrix: this.object.matrix,
+				cameraNear: this.object.near,
+				cameraUp: this.object.up,
+				cameraZoom: this.object.zoom,
 				gizmoMatrix: this._gizmos.matrix
 
 			} } );
@@ -2066,16 +2082,16 @@ class ArcballControls extends EventDispatcher {
 	 */
 	saveState() {
 
-		this._cameraMatrixState0.copy( this.camera.matrix );
+		this._cameraMatrixState0.copy( this.object.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 );
+		this._nearPos = this.object.near;
+		this._farPos = this.object.far;
+		this._zoom0 = this.object.zoom;
+		this._up0.copy( this.object.up );
 
-		if ( this.camera.isPerspectiveCamera ) {
+		if ( this.object.isPerspectiveCamera ) {
 
-			this._fov0 = this.camera.fov;
+			this._fov0 = this.object.fov;
 
 		}
 
@@ -2093,26 +2109,26 @@ class ArcballControls extends EventDispatcher {
 		_scalePointTemp.copy( point );
 		let sizeInverse = 1 / size;
 
-		if ( this.camera.isOrthographicCamera ) {
+		if ( this.object.isOrthographicCamera ) {
 
 			//camera zoom
-			this.camera.zoom = this._zoomState;
-			this.camera.zoom *= size;
+			this.object.zoom = this._zoomState;
+			this.object.zoom *= size;
 
 			//check min and max zoom
-			if ( this.camera.zoom > this.maxZoom ) {
+			if ( this.object.zoom > this.maxZoom ) {
 
-				this.camera.zoom = this.maxZoom;
+				this.object.zoom = this.maxZoom;
 				sizeInverse = this._zoomState / this.maxZoom;
 
-			} else if ( this.camera.zoom < this.minZoom ) {
+			} else if ( this.object.zoom < this.minZoom ) {
 
-				this.camera.zoom = this.minZoom;
+				this.object.zoom = this.minZoom;
 				sizeInverse = this._zoomState / this.minZoom;
 
 			}
 
-			this.camera.updateProjectionMatrix();
+			this.object.updateProjectionMatrix();
 
 			this._v3_1.setFromMatrixPosition( this._gizmoMatrixState );	//gizmos position
 
@@ -2136,7 +2152,7 @@ class ArcballControls extends EventDispatcher {
 			this.setTransformationMatrices( this._m4_1, this._m4_2 );
 			return _transformation;
 
-		} else if ( this.camera.isPerspectiveCamera ) {
+		} else if ( this.object.isPerspectiveCamera ) {
 
 			this._v3_1.setFromMatrixPosition( this._cameraMatrixState );
 			this._v3_2.setFromMatrixPosition( this._gizmoMatrixState );
@@ -2203,10 +2219,10 @@ class ArcballControls extends EventDispatcher {
 	 */
 	setFov( value ) {
 
-		if ( this.camera.isPerspectiveCamera ) {
+		if ( this.object.isPerspectiveCamera ) {
 
-			this.camera.fov = MathUtils.clamp( value, this.minFov, this.maxFov );
-			this.camera.updateProjectionMatrix();
+			this.object.fov = MathUtils.clamp( value, this.minFov, this.maxFov );
+			this.object.updateProjectionMatrix();
 
 		}
 
@@ -2536,18 +2552,18 @@ class ArcballControls extends EventDispatcher {
 	updateMatrixState() {
 
 		//update camera and gizmos state
-		this._cameraMatrixState.copy( this.camera.matrix );
+		this._cameraMatrixState.copy( this.object.matrix );
 		this._gizmoMatrixState.copy( this._gizmos.matrix );
 
-		if ( this.camera.isOrthographicCamera ) {
+		if ( this.object.isOrthographicCamera ) {
 
-			this._cameraProjectionState.copy( this.camera.projectionMatrix );
-			this.camera.updateProjectionMatrix();
-			this._zoomState = this.camera.zoom;
+			this._cameraProjectionState.copy( this.object.projectionMatrix );
+			this.object.updateProjectionMatrix();
+			this._zoomState = this.object.zoom;
 
-		} else if ( this.camera.isPerspectiveCamera ) {
+		} else if ( this.object.isPerspectiveCamera ) {
 
-			this._fovState = this.camera.fov;
+			this._fovState = this.object.fov;
 
 		}
 
@@ -2576,27 +2592,27 @@ class ArcballControls extends EventDispatcher {
 		if ( this.target.equals( this._currentTarget ) === false ) {
 
 			this._gizmos.position.copy( this.target );	//for correct radius calculation
-			this._tbRadius = this.calculateTbRadius( this.camera );
+			this._tbRadius = this.calculateTbRadius( this.object );
 			this.makeGizmos( this.target, this._tbRadius );
 			this._currentTarget.copy( this.target );
 
 		}
 
 		//check min/max parameters
-		if ( this.camera.isOrthographicCamera ) {
+		if ( this.object.isOrthographicCamera ) {
 
 			//check zoom
-			if ( this.camera.zoom > this.maxZoom || this.camera.zoom < this.minZoom ) {
+			if ( this.object.zoom > this.maxZoom || this.object.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 ) );
+				const newZoom = MathUtils.clamp( this.object.zoom, this.minZoom, this.maxZoom );
+				this.applyTransformMatrix( this.scale( newZoom / this.object.zoom, this._gizmos.position, true ) );
 
 			}
 
-		} else if ( this.camera.isPerspectiveCamera ) {
+		} else if ( this.object.isPerspectiveCamera ) {
 
 			//check distance
-			const distance = this.camera.position.distanceTo( this._gizmos.position );
+			const distance = this.object.position.distanceTo( this._gizmos.position );
 
 			if ( distance > this.maxDistance + EPS || distance < this.minDistance - EPS ) {
 
@@ -2607,15 +2623,15 @@ class ArcballControls extends EventDispatcher {
 			 }
 
 			//check fov
-			if ( this.camera.fov < this.minFov || this.camera.fov > this.maxFov ) {
+			if ( this.object.fov < this.minFov || this.object.fov > this.maxFov ) {
 
-				this.camera.fov = MathUtils.clamp( this.camera.fov, this.minFov, this.maxFov );
-				this.camera.updateProjectionMatrix();
+				this.object.fov = MathUtils.clamp( this.object.fov, this.minFov, this.maxFov );
+				this.object.updateProjectionMatrix();
 
 			}
 
 			const oldRadius = this._tbRadius;
-			this._tbRadius = this.calculateTbRadius( this.camera );
+			this._tbRadius = this.calculateTbRadius( this.object );
 
 			if ( oldRadius < this._tbRadius - EPS || oldRadius > this._tbRadius + EPS ) {
 
@@ -2635,7 +2651,7 @@ class ArcballControls extends EventDispatcher {
 
 		}
 
-		this.camera.lookAt( this._gizmos.position );
+		this.object.lookAt( this._gizmos.position );
 
 	}
 
@@ -2646,34 +2662,34 @@ class ArcballControls extends EventDispatcher {
 		if ( state.arcballState != undefined ) {
 
 			this._cameraMatrixState.fromArray( state.arcballState.cameraMatrix.elements );
-			this._cameraMatrixState.decompose( this.camera.position, this.camera.quaternion, this.camera.scale );
+			this._cameraMatrixState.decompose( this.object.position, this.object.quaternion, this.object.scale );
 
-			this.camera.up.copy( state.arcballState.cameraUp );
-			this.camera.near = state.arcballState.cameraNear;
-			this.camera.far = state.arcballState.cameraFar;
+			this.object.up.copy( state.arcballState.cameraUp );
+			this.object.near = state.arcballState.cameraNear;
+			this.object.far = state.arcballState.cameraFar;
 
-			this.camera.zoom = state.arcballState.cameraZoom;
+			this.object.zoom = state.arcballState.cameraZoom;
 
-			if ( this.camera.isPerspectiveCamera ) {
+			if ( this.object.isPerspectiveCamera ) {
 
-				this.camera.fov = state.arcballState.cameraFov;
+				this.object.fov = state.arcballState.cameraFov;
 
 			}
 
 			this._gizmoMatrixState.fromArray( state.arcballState.gizmoMatrix.elements );
 			this._gizmoMatrixState.decompose( this._gizmos.position, this._gizmos.quaternion, this._gizmos.scale );
 
-			this.camera.updateMatrix();
-			this.camera.updateProjectionMatrix();
+			this.object.updateMatrix();
+			this.object.updateProjectionMatrix();
 
 			this._gizmos.updateMatrix();
 
-			this._tbRadius = this.calculateTbRadius( this.camera );
+			this._tbRadius = this.calculateTbRadius( this.object );
 			const gizmoTmp = new Matrix4().copy( this._gizmoMatrixState0 );
 			this.makeGizmos( this._gizmos.position, this._tbRadius );
 			this._gizmoMatrixState0.copy( gizmoTmp );
 
-			this.camera.lookAt( this._gizmos.position );
+			this.object.lookAt( this._gizmos.position );
 			this.updateTbState( STATE.IDLE, false );
 
 			this.dispatchEvent( _changeEvent );
@@ -2689,7 +2705,7 @@ class ArcballControls extends EventDispatcher {
 function onWindowResize() {
 
 	const scale = ( this._gizmos.scale.x + this._gizmos.scale.y + this._gizmos.scale.z ) / 3;
-	this._tbRadius = this.calculateTbRadius( this.camera );
+	this._tbRadius = this.calculateTbRadius( this.object );
 
 	const newRadius = this._tbRadius / scale;
 	const curve = new EllipseCurve( 0, 0, newRadius, newRadius );
@@ -3098,13 +3114,13 @@ function onWheel( event ) {
 
 						let scalePoint;
 
-						if ( this.camera.isOrthographicCamera ) {
+						if ( this.object.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 );
+							scalePoint = this.unprojectOnTbPlane( this.object, event.clientX, event.clientY, this.domElement ).applyQuaternion( this.object.quaternion ).multiplyScalar( 1 / this.object.zoom ).add( this._gizmos.position );
 
-						} else if ( this.camera.isPerspectiveCamera ) {
+						} else if ( this.object.isPerspectiveCamera ) {
 
-							scalePoint = this.unprojectOnTbPlane( this.camera, event.clientX, event.clientY, this.domElement ).applyQuaternion( this.camera.quaternion ).add( this._gizmos.position );
+							scalePoint = this.unprojectOnTbPlane( this.object, event.clientX, event.clientY, this.domElement ).applyQuaternion( this.object.quaternion ).add( this._gizmos.position );
 
 						}
 
@@ -3132,7 +3148,7 @@ function onWheel( event ) {
 
 				case 'FOV':
 
-					if ( this.camera.isPerspectiveCamera ) {
+					if ( this.object.isPerspectiveCamera ) {
 
 						this.updateTbState( STATE.FOV, true );
 
@@ -3175,7 +3191,7 @@ function onWheel( event ) {
 						//check min and max distance
 						xNew = MathUtils.clamp( xNew, this.minDistance, this.maxDistance );
 
-						const y = x * Math.tan( MathUtils.DEG2RAD * this.camera.fov * 0.5 );
+						const y = x * Math.tan( MathUtils.DEG2RAD * this.object.fov * 0.5 );
 
 						//calculate new fov
 						let newFov = MathUtils.RAD2DEG * ( Math.atan( y / xNew ) * 2 );

+ 260 - 132
examples/jsm/controls/DragControls.js

@@ -1,14 +1,15 @@
 import {
-	EventDispatcher,
+	Controls,
 	Matrix4,
 	Plane,
 	Raycaster,
 	Vector2,
-	Vector3
+	Vector3,
+	MOUSE,
+	TOUCH
 } from 'three';
 
 const _plane = new Plane();
-const _raycaster = new Raycaster();
 
 const _pointer = new Vector2();
 const _offset = new Vector3();
@@ -21,262 +22,389 @@ const _inverseMatrix = new Matrix4();
 const _up = new Vector3();
 const _right = new Vector3();
 
-class DragControls extends EventDispatcher {
+let _selected = null, _hovered = null;
+const _intersections = [];
 
-	constructor( _objects, _camera, _domElement ) {
+const STATE = {
+	NONE: - 1,
+	PAN: 0,
+	ROTATE: 1
+};
 
-		super();
+class DragControls extends Controls {
 
-		_domElement.style.touchAction = 'none'; // disable touch scroll
+	constructor( objects, camera, domElement = null ) {
 
-		let _selected = null, _hovered = null;
+		super( camera, domElement );
 
-		const _intersections = [];
-
-		this.mode = 'translate';
+		this.objects = objects;
 
+		this.recursive = true;
+		this.transformGroup = false;
 		this.rotateSpeed = 1;
 
-		//
+		this.raycaster = new Raycaster();
 
-		const scope = this;
+		// interaction
 
-		function activate() {
+		this.mouseButtons = { LEFT: MOUSE.PAN, MIDDLE: MOUSE.PAN, RIGHT: MOUSE.ROTATE };
+		this.touches = { ONE: TOUCH.PAN };
 
-			_domElement.addEventListener( 'pointermove', onPointerMove );
-			_domElement.addEventListener( 'pointerdown', onPointerDown );
-			_domElement.addEventListener( 'pointerup', onPointerCancel );
-			_domElement.addEventListener( 'pointerleave', onPointerCancel );
+		// event listeners
 
-		}
+		this._onPointerMove = onPointerMove.bind( this );
+		this._onPointerDown = onPointerDown.bind( this );
+		this._onPointerCancel = onPointerCancel.bind( this );
+		this._onContextMenu = onContextMenu.bind( this );
 
-		function deactivate() {
+		//
 
-			_domElement.removeEventListener( 'pointermove', onPointerMove );
-			_domElement.removeEventListener( 'pointerdown', onPointerDown );
-			_domElement.removeEventListener( 'pointerup', onPointerCancel );
-			_domElement.removeEventListener( 'pointerleave', onPointerCancel );
+		if ( domElement !== null ) {
 
-			_domElement.style.cursor = '';
+			this.connect();
 
 		}
 
-		function dispose() {
+	}
 
-			deactivate();
+	connect() {
 
-		}
+		this.domElement.addEventListener( 'pointermove', this._onPointerMove );
+		this.domElement.addEventListener( 'pointerdown', this._onPointerDown );
+		this.domElement.addEventListener( 'pointerup', this._onPointerCancel );
+		this.domElement.addEventListener( 'pointerleave', this._onPointerCancel );
+		this.domElement.addEventListener( 'contextmenu', this._onContextMenu );
 
-		function getObjects() {
+		this.domElement.style.touchAction = 'none'; // disable touch scroll
 
-			return _objects;
+	}
 
-		}
+	disconnect() {
 
-		function setObjects( objects ) {
+		this.domElement.removeEventListener( 'pointermove', this._onPointerMove );
+		this.domElement.removeEventListener( 'pointerdown', this._onPointerDown );
+		this.domElement.removeEventListener( 'pointerup', this._onPointerCancel );
+		this.domElement.removeEventListener( 'pointerleave', this._onPointerCancel );
+		this.domElement.removeEventListener( 'contextmenu', this._onContextMenu );
 
-			_objects = objects;
+		this.domElement.style.touchAction = 'auto';
+		this.domElement.style.cursor = '';
 
-		}
+	}
+
+	dispose() {
+
+		this.disconnect();
+
+	}
+
+	_updatePointer( event ) {
+
+		const rect = this.domElement.getBoundingClientRect();
+
+		_pointer.x = ( event.clientX - rect.left ) / rect.width * 2 - 1;
+		_pointer.y = - ( event.clientY - rect.top ) / rect.height * 2 + 1;
+
+	}
+
+	_updateState( event ) {
+
+		// determine action
+
+		let action;
+
+		if ( event.pointerType === 'touch' ) {
 
-		function getRaycaster() {
+			action = this.touches.ONE;
 
-			return _raycaster;
+		} else {
+
+			switch ( event.button ) {
+
+				case 0:
+
+					action = this.mouseButtons.LEFT;
+					break;
+
+				case 1:
+
+					action = this.mouseButtons.MIDDLE;
+					break;
+
+				case 2:
+
+					action = this.mouseButtons.RIGHT;
+					break;
+
+				default:
+
+					action = null;
+
+			}
 
 		}
 
-		function onPointerMove( event ) {
+		// determine state
 
-			if ( scope.enabled === false ) return;
+		switch ( action ) {
 
-			updatePointer( event );
+			case MOUSE.PAN:
+			case TOUCH.PAN:
 
-			_raycaster.setFromCamera( _pointer, _camera );
+				this.state = STATE.PAN;
 
-			if ( _selected ) {
+				break;
 
-				if ( scope.mode === 'translate' ) {
+			case MOUSE.ROTATE:
+			case TOUCH.ROTATE:
 
-					if ( _raycaster.ray.intersectPlane( _plane, _intersection ) ) {
+				this.state = STATE.ROTATE;
 
-						_selected.position.copy( _intersection.sub( _offset ).applyMatrix4( _inverseMatrix ) );
+				break;
 
-					}
+			default:
 
-				} else if ( scope.mode === 'rotate' ) {
+				this.state = STATE.NONE;
 
-					_diff.subVectors( _pointer, _previousPointer ).multiplyScalar( scope.rotateSpeed );
-					_selected.rotateOnWorldAxis( _up, _diff.x );
-					_selected.rotateOnWorldAxis( _right.normalize(), - _diff.y );
+		}
 
-				}
+	}
 
-				scope.dispatchEvent( { type: 'drag', object: _selected } );
+	getRaycaster() {
 
-				_previousPointer.copy( _pointer );
+		console.warn( 'THREE.DragControls: getRaycaster() has been deprecated. Use controls.raycaster instead.' ); // @deprecated r169
 
-			} else {
+		return this.raycaster;
+
+	}
 
-				// hover support
+	setObjects( objects ) {
 
-				if ( event.pointerType === 'mouse' || event.pointerType === 'pen' ) {
+		console.warn( 'THREE.DragControls: setObjects() has been deprecated. Use controls.objects instead.' ); // @deprecated r169
 
-					_intersections.length = 0;
+		this.objects = objects;
 
-					_raycaster.setFromCamera( _pointer, _camera );
-					_raycaster.intersectObjects( _objects, scope.recursive, _intersections );
+	}
 
-					if ( _intersections.length > 0 ) {
+	getObjects() {
 
-						const object = _intersections[ 0 ].object;
+		console.warn( 'THREE.DragControls: getObjects() has been deprecated. Use controls.objects instead.' ); // @deprecated r169
 
-						_plane.setFromNormalAndCoplanarPoint( _camera.getWorldDirection( _plane.normal ), _worldPosition.setFromMatrixPosition( object.matrixWorld ) );
+		return this.objects;
 
-						if ( _hovered !== object && _hovered !== null ) {
+	}
 
-							scope.dispatchEvent( { type: 'hoveroff', object: _hovered } );
+	activate() {
 
-							_domElement.style.cursor = 'auto';
-							_hovered = null;
+		console.warn( 'THREE.DragControls: activate() has been renamed to connect().' ); // @deprecated r169
+		this.connect();
 
-						}
+	}
 
-						if ( _hovered !== object ) {
+	deactivate() {
 
-							scope.dispatchEvent( { type: 'hoveron', object: object } );
+		console.warn( 'THREE.DragControls: deactivate() has been renamed to disconnect().' ); // @deprecated r169
+		this.disconnect();
 
-							_domElement.style.cursor = 'pointer';
-							_hovered = object;
+	}
 
-						}
+	set mode( value ) {
 
-					} else {
+		console.warn( 'THREE.DragControls: The .mode property has been removed. Define the type of transformation via the .mouseButtons or .touches properties.' ); // @deprecated r169
 
-						if ( _hovered !== null ) {
+	}
 
-							scope.dispatchEvent( { type: 'hoveroff', object: _hovered } );
+	get mode() {
 
-							_domElement.style.cursor = 'auto';
-							_hovered = null;
+		console.warn( 'THREE.DragControls: The .mode property has been removed. Define the type of transformation via the .mouseButtons or .touches properties.' ); // @deprecated r169
 
-						}
+	}
 
-					}
+}
 
-				}
+function onPointerMove( event ) {
+
+	const camera = this.object;
+	const domElement = this.domElement;
+	const raycaster = this.raycaster;
+
+	if ( this.enabled === false ) return;
+
+	this._updatePointer( event );
+
+	raycaster.setFromCamera( _pointer, camera );
+
+	if ( _selected ) {
+
+		if ( this.state === STATE.PAN ) {
+
+			if ( raycaster.ray.intersectPlane( _plane, _intersection ) ) {
+
+				_selected.position.copy( _intersection.sub( _offset ).applyMatrix4( _inverseMatrix ) );
 
 			}
 
-			_previousPointer.copy( _pointer );
+		} else if ( this.state === STATE.ROTATE ) {
+
+			_diff.subVectors( _pointer, _previousPointer ).multiplyScalar( this.rotateSpeed );
+			_selected.rotateOnWorldAxis( _up, _diff.x );
+			_selected.rotateOnWorldAxis( _right.normalize(), - _diff.y );
 
 		}
 
-		function onPointerDown( event ) {
+		this.dispatchEvent( { type: 'drag', object: _selected } );
+
+		_previousPointer.copy( _pointer );
 
-			if ( scope.enabled === false ) return;
+	} else {
 
-			updatePointer( event );
+		// hover support
+
+		if ( event.pointerType === 'mouse' || event.pointerType === 'pen' ) {
 
 			_intersections.length = 0;
 
-			_raycaster.setFromCamera( _pointer, _camera );
-			_raycaster.intersectObjects( _objects, scope.recursive, _intersections );
+			raycaster.setFromCamera( _pointer, camera );
+			raycaster.intersectObjects( this.objects, this.recursive, _intersections );
 
 			if ( _intersections.length > 0 ) {
 
-				if ( scope.transformGroup === true ) {
+				const object = _intersections[ 0 ].object;
 
-					// look for the outermost group in the object's upper hierarchy
+				_plane.setFromNormalAndCoplanarPoint( camera.getWorldDirection( _plane.normal ), _worldPosition.setFromMatrixPosition( object.matrixWorld ) );
 
-					_selected = findGroup( _intersections[ 0 ].object );
+				if ( _hovered !== object && _hovered !== null ) {
 
-				} else {
+					this.dispatchEvent( { type: 'hoveroff', object: _hovered } );
 
-					_selected = _intersections[ 0 ].object;
+					domElement.style.cursor = 'auto';
+					_hovered = null;
 
 				}
 
-				_plane.setFromNormalAndCoplanarPoint( _camera.getWorldDirection( _plane.normal ), _worldPosition.setFromMatrixPosition( _selected.matrixWorld ) );
+				if ( _hovered !== object ) {
 
-				if ( _raycaster.ray.intersectPlane( _plane, _intersection ) ) {
+					this.dispatchEvent( { type: 'hoveron', object: object } );
 
-					if ( scope.mode === 'translate' ) {
+					domElement.style.cursor = 'pointer';
+					_hovered = object;
 
-						_inverseMatrix.copy( _selected.parent.matrixWorld ).invert();
-						_offset.copy( _intersection ).sub( _worldPosition.setFromMatrixPosition( _selected.matrixWorld ) );
+				}
 
-					} else if ( scope.mode === 'rotate' ) {
+			} else {
 
-						// the controls only support Y+ up
-						_up.set( 0, 1, 0 ).applyQuaternion( _camera.quaternion ).normalize();
-						_right.set( 1, 0, 0 ).applyQuaternion( _camera.quaternion ).normalize();
+				if ( _hovered !== null ) {
 
-					}
+					this.dispatchEvent( { type: 'hoveroff', object: _hovered } );
+
+					domElement.style.cursor = 'auto';
+					_hovered = null;
 
 				}
 
-				_domElement.style.cursor = 'move';
+			}
 
-				scope.dispatchEvent( { type: 'dragstart', object: _selected } );
+		}
 
-			}
+	}
 
-			_previousPointer.copy( _pointer );
+	_previousPointer.copy( _pointer );
 
-		}
+}
 
-		function onPointerCancel() {
+function onPointerDown( event ) {
 
-			if ( scope.enabled === false ) return;
+	const camera = this.object;
+	const domElement = this.domElement;
+	const raycaster = this.raycaster;
 
-			if ( _selected ) {
+	if ( this.enabled === false ) return;
 
-				scope.dispatchEvent( { type: 'dragend', object: _selected } );
+	this._updatePointer( event );
+	this._updateState( event );
 
-				_selected = null;
+	_intersections.length = 0;
 
-			}
+	raycaster.setFromCamera( _pointer, camera );
+	raycaster.intersectObjects( this.objects, this.recursive, _intersections );
 
-			_domElement.style.cursor = _hovered ? 'pointer' : 'auto';
+	if ( _intersections.length > 0 ) {
 
-		}
+		if ( this.transformGroup === true ) {
 
-		function updatePointer( event ) {
+			// look for the outermost group in the object's upper hierarchy
 
-			const rect = _domElement.getBoundingClientRect();
+			_selected = findGroup( _intersections[ 0 ].object );
 
-			_pointer.x = ( event.clientX - rect.left ) / rect.width * 2 - 1;
-			_pointer.y = - ( event.clientY - rect.top ) / rect.height * 2 + 1;
+		} else {
+
+			_selected = _intersections[ 0 ].object;
 
 		}
 
-		function findGroup( obj, group = null ) {
+		_plane.setFromNormalAndCoplanarPoint( camera.getWorldDirection( _plane.normal ), _worldPosition.setFromMatrixPosition( _selected.matrixWorld ) );
+
+		if ( raycaster.ray.intersectPlane( _plane, _intersection ) ) {
+
+			if ( this.state === STATE.PAN ) {
 
-			if ( obj.isGroup ) group = obj;
+				_inverseMatrix.copy( _selected.parent.matrixWorld ).invert();
+				_offset.copy( _intersection ).sub( _worldPosition.setFromMatrixPosition( _selected.matrixWorld ) );
 
-			if ( obj.parent === null ) return group;
+			} else if ( this.state === STATE.ROTATE ) {
 
-			return findGroup( obj.parent, group );
+				// the controls only support Y+ up
+				_up.set( 0, 1, 0 ).applyQuaternion( camera.quaternion ).normalize();
+				_right.set( 1, 0, 0 ).applyQuaternion( camera.quaternion ).normalize();
+
+			}
 
 		}
 
-		activate();
+		domElement.style.cursor = 'move';
 
-		// API
+		this.dispatchEvent( { type: 'dragstart', object: _selected } );
 
-		this.enabled = true;
-		this.recursive = true;
-		this.transformGroup = false;
+	}
+
+	_previousPointer.copy( _pointer );
 
-		this.activate = activate;
-		this.deactivate = deactivate;
-		this.dispose = dispose;
-		this.getObjects = getObjects;
-		this.getRaycaster = getRaycaster;
-		this.setObjects = setObjects;
+}
+
+function onPointerCancel() {
+
+	if ( this.enabled === false ) return;
+
+	if ( _selected ) {
+
+		this.dispatchEvent( { type: 'dragend', object: _selected } );
+
+		_selected = null;
 
 	}
 
+	this.domElement.style.cursor = _hovered ? 'pointer' : 'auto';
+
+	this.state = STATE.NONE;
+
+}
+
+function onContextMenu( event ) {
+
+	if ( this.enabled === false ) return;
+
+	event.preventDefault();
+
+}
+
+function findGroup( obj, group = null ) {
+
+	if ( obj.isGroup ) group = obj;
+
+	if ( obj.parent === null ) return group;
+
+	return findGroup( obj.parent, group );
+
 }
 
 export { DragControls };

+ 175 - 163
examples/jsm/controls/FirstPersonControls.js

@@ -1,4 +1,5 @@
 import {
+	Controls,
 	MathUtils,
 	Spherical,
 	Vector3
@@ -7,18 +8,16 @@ import {
 const _lookDirection = new Vector3();
 const _spherical = new Spherical();
 const _target = new Vector3();
+const _targetPosition = new Vector3();
 
-class FirstPersonControls {
+class FirstPersonControls extends Controls {
 
-	constructor( object, domElement ) {
+	constructor( object, domElement = null ) {
 
-		this.object = object;
-		this.domElement = domElement;
+		super( object, domElement );
 
 		// API
 
-		this.enabled = true;
-
 		this.movementSpeed = 1.0;
 		this.lookSpeed = 0.005;
 
@@ -40,283 +39,296 @@ class FirstPersonControls {
 
 		// internals
 
-		this.autoSpeedFactor = 0.0;
+		this._autoSpeedFactor = 0.0;
+
+		this._pointerX = 0;
+		this._pointerY = 0;
 
-		this.pointerX = 0;
-		this.pointerY = 0;
+		this._moveForward = false;
+		this._moveBackward = false;
+		this._moveLeft = false;
+		this._moveRight = false;
 
-		this.moveForward = false;
-		this.moveBackward = false;
-		this.moveLeft = false;
-		this.moveRight = false;
+		this._viewHalfX = 0;
+		this._viewHalfY = 0;
 
-		this.viewHalfX = 0;
-		this.viewHalfY = 0;
+		this._lat = 0;
+		this._lon = 0;
 
-		// private variables
+		// event listeners
 
-		let lat = 0;
-		let lon = 0;
+		this._onPointerMove = onPointerMove.bind( this );
+		this._onPointerDown = onPointerDown.bind( this );
+		this._onPointerUp = onPointerUp.bind( this );
+		this._onContextMenu = onContextMenu.bind( this );
+		this._onKeyDown = onKeyDown.bind( this );
+		this._onKeyUp = onKeyUp.bind( this );
 
 		//
 
-		this.handleResize = function () {
+		if ( domElement !== null ) {
 
-			if ( this.domElement === document ) {
+			this.connect();
 
-				this.viewHalfX = window.innerWidth / 2;
-				this.viewHalfY = window.innerHeight / 2;
+			this.handleResize();
 
-			} else {
+		}
 
-				this.viewHalfX = this.domElement.offsetWidth / 2;
-				this.viewHalfY = this.domElement.offsetHeight / 2;
+		this._setOrientation();
 
-			}
+	}
 
-		};
+	connect() {
 
-		this.onPointerDown = function ( event ) {
+		window.addEventListener( 'keydown', this._onKeyDown );
+		window.addEventListener( 'keyup', this._onKeyUp );
 
-			if ( this.domElement !== document ) {
+		this.domElement.addEventListener( 'pointermove', this._onPointerMove );
+		this.domElement.addEventListener( 'pointerdown', this._onPointerDown );
+		this.domElement.addEventListener( 'pointerup', this._onPointerUp );
+		this.domElement.addEventListener( 'contextmenu', this._onContextMenu );
 
-				this.domElement.focus();
+	}
 
-			}
+	disconnect() {
 
-			if ( this.activeLook ) {
+		window.removeEventListener( 'keydown', this._onKeyDown );
+		window.removeEventListener( 'keyup', this._onKeyUp );
 
-				switch ( event.button ) {
+		this.domElement.removeEventListener( 'pointerdown', this._onPointerMove );
+		this.domElement.removeEventListener( 'pointermove', this._onPointerDown );
+		this.domElement.removeEventListener( 'pointerup', this._onPointerUp );
+		this.domElement.removeEventListener( 'contextmenu', this._onContextMenu );
 
-					case 0: this.moveForward = true; break;
-					case 2: this.moveBackward = true; break;
+	}
 
-				}
+	dispose() {
 
-			}
+		this.disconnect();
 
-			this.mouseDragOn = true;
+	}
 
-		};
+	handleResize() {
 
-		this.onPointerUp = function ( event ) {
+		if ( this.domElement === document ) {
 
-			if ( this.activeLook ) {
+			this._viewHalfX = window.innerWidth / 2;
+			this._viewHalfY = window.innerHeight / 2;
 
-				switch ( event.button ) {
+		} else {
 
-					case 0: this.moveForward = false; break;
-					case 2: this.moveBackward = false; break;
+			this._viewHalfX = this.domElement.offsetWidth / 2;
+			this._viewHalfY = this.domElement.offsetHeight / 2;
 
-				}
+		}
 
-			}
+	}
 
-			this.mouseDragOn = false;
+	lookAt( x, y, z ) {
 
-		};
+		if ( x.isVector3 ) {
 
-		this.onPointerMove = function ( event ) {
+			_target.copy( x );
 
-			if ( this.domElement === document ) {
+		} else {
 
-				this.pointerX = event.pageX - this.viewHalfX;
-				this.pointerY = event.pageY - this.viewHalfY;
+			_target.set( x, y, z );
 
-			} else {
+		}
 
-				this.pointerX = event.pageX - this.domElement.offsetLeft - this.viewHalfX;
-				this.pointerY = event.pageY - this.domElement.offsetTop - this.viewHalfY;
+		this.object.lookAt( _target );
 
-			}
+		this._setOrientation();
 
-		};
+		return this;
 
-		this.onKeyDown = function ( event ) {
+	}
 
-			switch ( event.code ) {
+	update( delta ) {
 
-				case 'ArrowUp':
-				case 'KeyW': this.moveForward = true; break;
+		if ( this.enabled === false ) return;
 
-				case 'ArrowLeft':
-				case 'KeyA': this.moveLeft = true; break;
+		if ( this.heightSpeed ) {
 
-				case 'ArrowDown':
-				case 'KeyS': this.moveBackward = true; break;
+			const y = MathUtils.clamp( this.object.position.y, this.heightMin, this.heightMax );
+			const heightDelta = y - this.heightMin;
 
-				case 'ArrowRight':
-				case 'KeyD': this.moveRight = true; break;
+			this._autoSpeedFactor = delta * ( heightDelta * this.heightCoef );
 
-				case 'KeyR': this.moveUp = true; break;
-				case 'KeyF': this.moveDown = true; break;
+		} else {
 
-			}
+			this._autoSpeedFactor = 0.0;
 
-		};
+		}
 
-		this.onKeyUp = function ( event ) {
+		const actualMoveSpeed = delta * this.movementSpeed;
 
-			switch ( event.code ) {
+		if ( this._moveForward || ( this.autoForward && ! this._moveBackward ) ) this.object.translateZ( - ( actualMoveSpeed + this._autoSpeedFactor ) );
+		if ( this._moveBackward ) this.object.translateZ( actualMoveSpeed );
 
-				case 'ArrowUp':
-				case 'KeyW': this.moveForward = false; break;
+		if ( this._moveLeft ) this.object.translateX( - actualMoveSpeed );
+		if ( this._moveRight ) this.object.translateX( actualMoveSpeed );
 
-				case 'ArrowLeft':
-				case 'KeyA': this.moveLeft = false; break;
+		if ( this._moveUp ) this.object.translateY( actualMoveSpeed );
+		if ( this._moveDown ) this.object.translateY( - actualMoveSpeed );
 
-				case 'ArrowDown':
-				case 'KeyS': this.moveBackward = false; break;
+		let actualLookSpeed = delta * this.lookSpeed;
 
-				case 'ArrowRight':
-				case 'KeyD': this.moveRight = false; break;
+		if ( ! this.activeLook ) {
 
-				case 'KeyR': this.moveUp = false; break;
-				case 'KeyF': this.moveDown = false; break;
+			actualLookSpeed = 0;
 
-			}
+		}
 
-		};
+		let verticalLookRatio = 1;
 
-		this.lookAt = function ( x, y, z ) {
+		if ( this.constrainVertical ) {
 
-			if ( x.isVector3 ) {
+			verticalLookRatio = Math.PI / ( this.verticalMax - this.verticalMin );
 
-				_target.copy( x );
+		}
 
-			} else {
+		this._lon -= this._pointerX * actualLookSpeed;
+		if ( this.lookVertical ) this._lat -= this._pointerY * actualLookSpeed * verticalLookRatio;
 
-				_target.set( x, y, z );
+		this._lat = Math.max( - 85, Math.min( 85, this._lat ) );
 
-			}
+		let phi = MathUtils.degToRad( 90 - this._lat );
+		const theta = MathUtils.degToRad( this._lon );
 
-			this.object.lookAt( _target );
+		if ( this.constrainVertical ) {
 
-			setOrientation( this );
+			phi = MathUtils.mapLinear( phi, 0, Math.PI, this.verticalMin, this.verticalMax );
 
-			return this;
+		}
 
-		};
+		const position = this.object.position;
 
-		this.update = function () {
+		_targetPosition.setFromSphericalCoords( 1, phi, theta ).add( position );
 
-			const targetPosition = new Vector3();
+		this.object.lookAt( _targetPosition );
 
-			return function update( delta ) {
+	}
 
-				if ( this.enabled === false ) return;
+	_setOrientation() {
 
-				if ( this.heightSpeed ) {
+		const quaternion = this.object.quaternion;
 
-					const y = MathUtils.clamp( this.object.position.y, this.heightMin, this.heightMax );
-					const heightDelta = y - this.heightMin;
+		_lookDirection.set( 0, 0, - 1 ).applyQuaternion( quaternion );
+		_spherical.setFromVector3( _lookDirection );
 
-					this.autoSpeedFactor = delta * ( heightDelta * this.heightCoef );
+		this._lat = 90 - MathUtils.radToDeg( _spherical.phi );
+		this._lon = MathUtils.radToDeg( _spherical.theta );
 
-				} else {
+	}
 
-					this.autoSpeedFactor = 0.0;
+}
 
-				}
+function onPointerDown( event ) {
 
-				const actualMoveSpeed = delta * this.movementSpeed;
+	if ( this.domElement !== document ) {
 
-				if ( this.moveForward || ( this.autoForward && ! this.moveBackward ) ) this.object.translateZ( - ( actualMoveSpeed + this.autoSpeedFactor ) );
-				if ( this.moveBackward ) this.object.translateZ( actualMoveSpeed );
+		this.domElement.focus();
 
-				if ( this.moveLeft ) this.object.translateX( - actualMoveSpeed );
-				if ( this.moveRight ) this.object.translateX( actualMoveSpeed );
+	}
 
-				if ( this.moveUp ) this.object.translateY( actualMoveSpeed );
-				if ( this.moveDown ) this.object.translateY( - actualMoveSpeed );
+	if ( this.activeLook ) {
 
-				let actualLookSpeed = delta * this.lookSpeed;
+		switch ( event.button ) {
 
-				if ( ! this.activeLook ) {
+			case 0: this._moveForward = true; break;
+			case 2: this._moveBackward = true; break;
 
-					actualLookSpeed = 0;
+		}
 
-				}
+	}
 
-				let verticalLookRatio = 1;
+	this.mouseDragOn = true;
 
-				if ( this.constrainVertical ) {
+}
 
-					verticalLookRatio = Math.PI / ( this.verticalMax - this.verticalMin );
+function onPointerUp( event ) {
 
-				}
+	if ( this.activeLook ) {
 
-				lon -= this.pointerX * actualLookSpeed;
-				if ( this.lookVertical ) lat -= this.pointerY * actualLookSpeed * verticalLookRatio;
+		switch ( event.button ) {
 
-				lat = Math.max( - 85, Math.min( 85, lat ) );
+			case 0: this._moveForward = false; break;
+			case 2: this._moveBackward = false; break;
 
-				let phi = MathUtils.degToRad( 90 - lat );
-				const theta = MathUtils.degToRad( lon );
+		}
 
-				if ( this.constrainVertical ) {
+	}
 
-					phi = MathUtils.mapLinear( phi, 0, Math.PI, this.verticalMin, this.verticalMax );
+	this.mouseDragOn = false;
 
-				}
+}
 
-				const position = this.object.position;
+function onPointerMove( event ) {
 
-				targetPosition.setFromSphericalCoords( 1, phi, theta ).add( position );
+	if ( this.domElement === document ) {
 
-				this.object.lookAt( targetPosition );
+		this._pointerX = event.pageX - this._viewHalfX;
+		this._pointerY = event.pageY - this._viewHalfY;
 
-			};
+	} else {
 
-		}();
+		this._pointerX = event.pageX - this.domElement.offsetLeft - this._viewHalfX;
+		this._pointerY = event.pageY - this.domElement.offsetTop - this._viewHalfY;
 
-		this.dispose = function () {
+	}
 
-			this.domElement.removeEventListener( 'contextmenu', contextmenu );
-			this.domElement.removeEventListener( 'pointerdown', _onPointerDown );
-			this.domElement.removeEventListener( 'pointermove', _onPointerMove );
-			this.domElement.removeEventListener( 'pointerup', _onPointerUp );
+}
 
-			window.removeEventListener( 'keydown', _onKeyDown );
-			window.removeEventListener( 'keyup', _onKeyUp );
+function onKeyDown( event ) {
 
-		};
+	switch ( event.code ) {
 
-		const _onPointerMove = this.onPointerMove.bind( this );
-		const _onPointerDown = this.onPointerDown.bind( this );
-		const _onPointerUp = this.onPointerUp.bind( this );
-		const _onKeyDown = this.onKeyDown.bind( this );
-		const _onKeyUp = this.onKeyUp.bind( this );
+		case 'ArrowUp':
+		case 'KeyW': this._moveForward = true; break;
 
-		this.domElement.addEventListener( 'contextmenu', contextmenu );
-		this.domElement.addEventListener( 'pointerdown', _onPointerDown );
-		this.domElement.addEventListener( 'pointermove', _onPointerMove );
-		this.domElement.addEventListener( 'pointerup', _onPointerUp );
+		case 'ArrowLeft':
+		case 'KeyA': this._moveLeft = true; break;
 
-		window.addEventListener( 'keydown', _onKeyDown );
-		window.addEventListener( 'keyup', _onKeyUp );
+		case 'ArrowDown':
+		case 'KeyS': this._moveBackward = true; break;
 
-		function setOrientation( controls ) {
+		case 'ArrowRight':
+		case 'KeyD': this._moveRight = true; break;
 
-			const quaternion = controls.object.quaternion;
+		case 'KeyR': this._moveUp = true; break;
+		case 'KeyF': this._moveDown = true; break;
 
-			_lookDirection.set( 0, 0, - 1 ).applyQuaternion( quaternion );
-			_spherical.setFromVector3( _lookDirection );
+	}
+
+}
 
-			lat = 90 - MathUtils.radToDeg( _spherical.phi );
-			lon = MathUtils.radToDeg( _spherical.theta );
+function onKeyUp( event ) {
 
-		}
+	switch ( event.code ) {
 
-		this.handleResize();
+		case 'ArrowUp':
+		case 'KeyW': this._moveForward = false; break;
 
-		setOrientation( this );
+		case 'ArrowLeft':
+		case 'KeyA': this._moveLeft = false; break;
+
+		case 'ArrowDown':
+		case 'KeyS': this._moveBackward = false; break;
+
+		case 'ArrowRight':
+		case 'KeyD': this._moveRight = false; break;
+
+		case 'KeyR': this._moveUp = false; break;
+		case 'KeyF': this._moveDown = false; break;
 
 	}
 
 }
 
-function contextmenu( event ) {
+function onContextMenu( event ) {
+
+	if ( this.enabled === false ) return;
 
 	event.preventDefault();
 

+ 194 - 188
examples/jsm/controls/FlyControls.js

@@ -1,24 +1,19 @@
 import {
-	EventDispatcher,
+	Controls,
 	Quaternion,
 	Vector3
 } from 'three';
 
 const _changeEvent = { type: 'change' };
 
-class FlyControls extends EventDispatcher {
+const _EPS = 0.000001;
+const _tmpQuaternion = new Quaternion();
 
-	constructor( object, domElement ) {
+class FlyControls extends Controls {
 
-		super();
+	constructor( object, domElement = null ) {
 
-		this.object = object;
-		this.domElement = domElement;
-
-		// API
-
-		// Set to false to disable this control
-		this.enabled = true;
+		super( object, domElement );
 
 		this.movementSpeed = 1.0;
 		this.rollSpeed = 0.005;
@@ -26,301 +21,312 @@ class FlyControls extends EventDispatcher {
 		this.dragToLook = false;
 		this.autoForward = false;
 
-		// disable default target object behavior
-
 		// internals
 
-		const scope = this;
+		this._moveState = { up: 0, down: 0, left: 0, right: 0, forward: 0, back: 0, pitchUp: 0, pitchDown: 0, yawLeft: 0, yawRight: 0, rollLeft: 0, rollRight: 0 };
+		this._moveVector = new Vector3( 0, 0, 0 );
+		this._rotationVector = new Vector3( 0, 0, 0 );
+		this._lastQuaternion = new Quaternion();
+		this._lastPosition = new Vector3();
+		this._status = 0;
 
-		const EPS = 0.000001;
+		// event listeners
 
-		const lastQuaternion = new Quaternion();
-		const lastPosition = new Vector3();
+		this._onKeyDown = onKeyDown.bind( this );
+		this._onKeyUp = onKeyUp.bind( this );
+		this._onPointerMove = onPointerMove.bind( this );
+		this._onPointerDown = onPointerDown.bind( this );
+		this._onPointerUp = onPointerUp.bind( this );
+		this._onPointerCancel = onPointerCancel.bind( this );
+		this._onContextMenu = onContextMenu.bind( this );
 
-		this.tmpQuaternion = new Quaternion();
+		//
 
-		this.status = 0;
+		if ( domElement !== null ) {
 
-		this.moveState = { up: 0, down: 0, left: 0, right: 0, forward: 0, back: 0, pitchUp: 0, pitchDown: 0, yawLeft: 0, yawRight: 0, rollLeft: 0, rollRight: 0 };
-		this.moveVector = new Vector3( 0, 0, 0 );
-		this.rotationVector = new Vector3( 0, 0, 0 );
+			this.connect();
 
-		this.keydown = function ( event ) {
+		}
 
-			if ( event.altKey || this.enabled === false ) {
+	}
 
-				return;
+	connect() {
 
-			}
+		window.addEventListener( 'keydown', this._onKeyDown );
+		window.addEventListener( 'keyup', this._onKeyUp );
 
-			switch ( event.code ) {
+		this.domElement.addEventListener( 'pointermove', this._onPointerMove );
+		this.domElement.addEventListener( 'pointerdown', this._onPointerDown );
+		this.domElement.addEventListener( 'pointerup', this._onPointerUp );
+		this.domElement.addEventListener( 'pointercancel', this._onPointerCancel );
+		this.domElement.addEventListener( 'contextmenu', this._onContextMenu );
 
-				case 'ShiftLeft':
-				case 'ShiftRight': this.movementSpeedMultiplier = .1; break;
+	}
 
-				case 'KeyW': this.moveState.forward = 1; break;
-				case 'KeyS': this.moveState.back = 1; break;
+	disconnect() {
 
-				case 'KeyA': this.moveState.left = 1; break;
-				case 'KeyD': this.moveState.right = 1; break;
+		window.removeEventListener( 'keydown', this._onKeyDown );
+		window.removeEventListener( 'keyup', this._onKeyUp );
 
-				case 'KeyR': this.moveState.up = 1; break;
-				case 'KeyF': this.moveState.down = 1; break;
+		this.domElement.removeEventListener( 'pointermove', this._onPointerMove );
+		this.domElement.removeEventListener( 'pointerdown', this._onPointerDown );
+		this.domElement.removeEventListener( 'pointerup', this._onPointerUp );
+		this.domElement.removeEventListener( 'pointercancel', this._onPointerCancel );
+		this.domElement.removeEventListener( 'contextmenu', this._onContextMenu );
 
-				case 'ArrowUp': this.moveState.pitchUp = 1; break;
-				case 'ArrowDown': this.moveState.pitchDown = 1; break;
+	}
 
-				case 'ArrowLeft': this.moveState.yawLeft = 1; break;
-				case 'ArrowRight': this.moveState.yawRight = 1; break;
+	dispose() {
 
-				case 'KeyQ': this.moveState.rollLeft = 1; break;
-				case 'KeyE': this.moveState.rollRight = 1; break;
+		this.disconnect();
 
-			}
+	}
 
-			this.updateMovementVector();
-			this.updateRotationVector();
+	update( delta ) {
 
-		};
+		if ( this.enabled === false ) return;
 
-		this.keyup = function ( event ) {
+		const object = this.object;
 
-			if ( this.enabled === false ) return;
+		const moveMult = delta * this.movementSpeed;
+		const rotMult = delta * this.rollSpeed;
 
-			switch ( event.code ) {
+		object.translateX( this._moveVector.x * moveMult );
+		object.translateY( this._moveVector.y * moveMult );
+		object.translateZ( this._moveVector.z * moveMult );
 
-				case 'ShiftLeft':
-				case 'ShiftRight': this.movementSpeedMultiplier = 1; break;
+		_tmpQuaternion.set( this._rotationVector.x * rotMult, this._rotationVector.y * rotMult, this._rotationVector.z * rotMult, 1 ).normalize();
+		object.quaternion.multiply( _tmpQuaternion );
 
-				case 'KeyW': this.moveState.forward = 0; break;
-				case 'KeyS': this.moveState.back = 0; break;
+		if (
+			this._lastPosition.distanceToSquared( object.position ) > _EPS ||
+			8 * ( 1 - this._lastQuaternion.dot( object.quaternion ) ) > _EPS
+		) {
 
-				case 'KeyA': this.moveState.left = 0; break;
-				case 'KeyD': this.moveState.right = 0; break;
+			this.dispatchEvent( _changeEvent );
+			this._lastQuaternion.copy( object.quaternion );
+			this._lastPosition.copy( object.position );
 
-				case 'KeyR': this.moveState.up = 0; break;
-				case 'KeyF': this.moveState.down = 0; break;
+		}
 
-				case 'ArrowUp': this.moveState.pitchUp = 0; break;
-				case 'ArrowDown': this.moveState.pitchDown = 0; break;
+	}
 
-				case 'ArrowLeft': this.moveState.yawLeft = 0; break;
-				case 'ArrowRight': this.moveState.yawRight = 0; break;
+	// private
 
-				case 'KeyQ': this.moveState.rollLeft = 0; break;
-				case 'KeyE': this.moveState.rollRight = 0; break;
+	_updateMovementVector() {
 
-			}
+		const forward = ( this._moveState.forward || ( this.autoForward && ! this._moveState.back ) ) ? 1 : 0;
 
-			this.updateMovementVector();
-			this.updateRotationVector();
+		this._moveVector.x = ( - this._moveState.left + this._moveState.right );
+		this._moveVector.y = ( - this._moveState.down + this._moveState.up );
+		this._moveVector.z = ( - forward + this._moveState.back );
 
-		};
+		//console.log( 'move:', [ this._moveVector.x, this._moveVector.y, this._moveVector.z ] );
 
-		this.pointerdown = function ( event ) {
+	}
 
-			if ( this.enabled === false ) return;
+	_updateRotationVector() {
 
-			if ( this.dragToLook ) {
+		this._rotationVector.x = ( - this._moveState.pitchDown + this._moveState.pitchUp );
+		this._rotationVector.y = ( - this._moveState.yawRight + this._moveState.yawLeft );
+		this._rotationVector.z = ( - this._moveState.rollRight + this._moveState.rollLeft );
 
-				this.status ++;
+		//console.log( 'rotate:', [ this._rotationVector.x, this._rotationVector.y, this._rotationVector.z ] );
 
-			} else {
+	}
 
-				switch ( event.button ) {
+	_getContainerDimensions() {
 
-					case 0: this.moveState.forward = 1; break;
-					case 2: this.moveState.back = 1; break;
+		if ( this.domElement != document ) {
 
-				}
+			return {
+				size: [ this.domElement.offsetWidth, this.domElement.offsetHeight ],
+				offset: [ this.domElement.offsetLeft, this.domElement.offsetTop ]
+			};
 
-				this.updateMovementVector();
+		} else {
 
-			}
+			return {
+				size: [ window.innerWidth, window.innerHeight ],
+				offset: [ 0, 0 ]
+			};
 
-		};
+		}
 
-		this.pointermove = function ( event ) {
+	}
 
-			if ( this.enabled === false ) return;
+}
 
-			if ( ! this.dragToLook || this.status > 0 ) {
+function onKeyDown( event ) {
 
-				const container = this.getContainerDimensions();
-				const halfWidth = container.size[ 0 ] / 2;
-				const halfHeight = container.size[ 1 ] / 2;
+	if ( event.altKey || this.enabled === false ) {
 
-				this.moveState.yawLeft = - ( ( event.pageX - container.offset[ 0 ] ) - halfWidth ) / halfWidth;
-				this.moveState.pitchDown = ( ( event.pageY - container.offset[ 1 ] ) - halfHeight ) / halfHeight;
+		return;
 
-				this.updateRotationVector();
+	}
+
+	switch ( event.code ) {
 
-			}
+		case 'ShiftLeft':
+		case 'ShiftRight': this.movementSpeedMultiplier = .1; break;
 
-		};
+		case 'KeyW': this._moveState.forward = 1; break;
+		case 'KeyS': this._moveState.back = 1; break;
 
-		this.pointerup = function ( event ) {
+		case 'KeyA': this._moveState.left = 1; break;
+		case 'KeyD': this._moveState.right = 1; break;
 
-			if ( this.enabled === false ) return;
+		case 'KeyR': this._moveState.up = 1; break;
+		case 'KeyF': this._moveState.down = 1; break;
 
-			if ( this.dragToLook ) {
+		case 'ArrowUp': this._moveState.pitchUp = 1; break;
+		case 'ArrowDown': this._moveState.pitchDown = 1; break;
 
-				this.status --;
+		case 'ArrowLeft': this._moveState.yawLeft = 1; break;
+		case 'ArrowRight': this._moveState.yawRight = 1; break;
 
-				this.moveState.yawLeft = this.moveState.pitchDown = 0;
+		case 'KeyQ': this._moveState.rollLeft = 1; break;
+		case 'KeyE': this._moveState.rollRight = 1; break;
 
-			} else {
+	}
 
-				switch ( event.button ) {
+	this._updateMovementVector();
+	this._updateRotationVector();
 
-					case 0: this.moveState.forward = 0; break;
-					case 2: this.moveState.back = 0; break;
+}
 
-				}
+function onKeyUp( event ) {
 
-				this.updateMovementVector();
+	if ( this.enabled === false ) return;
 
-			}
+	switch ( event.code ) {
 
-			this.updateRotationVector();
+		case 'ShiftLeft':
+		case 'ShiftRight': this.movementSpeedMultiplier = 1; break;
 
-		};
+		case 'KeyW': this._moveState.forward = 0; break;
+		case 'KeyS': this._moveState.back = 0; break;
 
-		this.pointercancel = function () {
+		case 'KeyA': this._moveState.left = 0; break;
+		case 'KeyD': this._moveState.right = 0; break;
 
-			if ( this.enabled === false ) return;
+		case 'KeyR': this._moveState.up = 0; break;
+		case 'KeyF': this._moveState.down = 0; break;
 
-			if ( this.dragToLook ) {
+		case 'ArrowUp': this._moveState.pitchUp = 0; break;
+		case 'ArrowDown': this._moveState.pitchDown = 0; break;
 
-				this.status = 0;
+		case 'ArrowLeft': this._moveState.yawLeft = 0; break;
+		case 'ArrowRight': this._moveState.yawRight = 0; break;
 
-				this.moveState.yawLeft = this.moveState.pitchDown = 0;
+		case 'KeyQ': this._moveState.rollLeft = 0; break;
+		case 'KeyE': this._moveState.rollRight = 0; break;
 
-			} else {
+	}
 
-				this.moveState.forward = 0;
-				this.moveState.back = 0;
+	this._updateMovementVector();
+	this._updateRotationVector();
 
-				this.updateMovementVector();
+}
 
-			}
+function onPointerDown( event ) {
 
-			this.updateRotationVector();
+	if ( this.enabled === false ) return;
 
-		};
+	if ( this.dragToLook ) {
 
-		this.contextMenu = function ( event ) {
+		this._status ++;
 
-			if ( this.enabled === false ) return;
+	} else {
 
-			event.preventDefault();
+		switch ( event.button ) {
 
-		};
+			case 0: this._moveState.forward = 1; break;
+			case 2: this._moveState.back = 1; break;
 
-		this.update = function ( delta ) {
+		}
 
-			if ( this.enabled === false ) return;
+		this._updateMovementVector();
 
-			const moveMult = delta * scope.movementSpeed;
-			const rotMult = delta * scope.rollSpeed;
+	}
 
-			scope.object.translateX( scope.moveVector.x * moveMult );
-			scope.object.translateY( scope.moveVector.y * moveMult );
-			scope.object.translateZ( scope.moveVector.z * moveMult );
+}
 
-			scope.tmpQuaternion.set( scope.rotationVector.x * rotMult, scope.rotationVector.y * rotMult, scope.rotationVector.z * rotMult, 1 ).normalize();
-			scope.object.quaternion.multiply( scope.tmpQuaternion );
+function onPointerMove( event ) {
 
-			if (
-				lastPosition.distanceToSquared( scope.object.position ) > EPS ||
-				8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS
-			) {
+	if ( this.enabled === false ) return;
 
-				scope.dispatchEvent( _changeEvent );
-				lastQuaternion.copy( scope.object.quaternion );
-				lastPosition.copy( scope.object.position );
+	if ( ! this.dragToLook || this._status > 0 ) {
 
-			}
+		const container = this._getContainerDimensions();
+		const halfWidth = container.size[ 0 ] / 2;
+		const halfHeight = container.size[ 1 ] / 2;
 
-		};
+		this._moveState.yawLeft = - ( ( event.pageX - container.offset[ 0 ] ) - halfWidth ) / halfWidth;
+		this._moveState.pitchDown = ( ( event.pageY - container.offset[ 1 ] ) - halfHeight ) / halfHeight;
 
-		this.updateMovementVector = function () {
+		this._updateRotationVector();
 
-			const forward = ( this.moveState.forward || ( this.autoForward && ! this.moveState.back ) ) ? 1 : 0;
+	}
 
-			this.moveVector.x = ( - this.moveState.left + this.moveState.right );
-			this.moveVector.y = ( - this.moveState.down + this.moveState.up );
-			this.moveVector.z = ( - forward + this.moveState.back );
+}
 
-			//console.log( 'move:', [ this.moveVector.x, this.moveVector.y, this.moveVector.z ] );
+function onPointerUp( event ) {
 
-		};
+	if ( this.enabled === false ) return;
 
-		this.updateRotationVector = function () {
+	if ( this.dragToLook ) {
 
-			this.rotationVector.x = ( - this.moveState.pitchDown + this.moveState.pitchUp );
-			this.rotationVector.y = ( - this.moveState.yawRight + this.moveState.yawLeft );
-			this.rotationVector.z = ( - this.moveState.rollRight + this.moveState.rollLeft );
+		this._status --;
 
-			//console.log( 'rotate:', [ this.rotationVector.x, this.rotationVector.y, this.rotationVector.z ] );
+		this._moveState.yawLeft = this._moveState.pitchDown = 0;
 
-		};
+	} else {
 
-		this.getContainerDimensions = function () {
+		switch ( event.button ) {
 
-			if ( this.domElement != document ) {
+			case 0: this._moveState.forward = 0; break;
+			case 2: this._moveState.back = 0; break;
 
-				return {
-					size: [ this.domElement.offsetWidth, this.domElement.offsetHeight ],
-					offset: [ this.domElement.offsetLeft, this.domElement.offsetTop ]
-				};
+		}
 
-			} else {
+		this._updateMovementVector();
 
-				return {
-					size: [ window.innerWidth, window.innerHeight ],
-					offset: [ 0, 0 ]
-				};
+	}
 
-			}
+	this._updateRotationVector();
 
-		};
+}
 
-		this.dispose = function () {
+function onPointerCancel() {
 
-			this.domElement.removeEventListener( 'contextmenu', _contextmenu );
-			this.domElement.removeEventListener( 'pointerdown', _pointerdown );
-			this.domElement.removeEventListener( 'pointermove', _pointermove );
-			this.domElement.removeEventListener( 'pointerup', _pointerup );
-			this.domElement.removeEventListener( 'pointercancel', _pointercancel );
+	if ( this.enabled === false ) return;
 
-			window.removeEventListener( 'keydown', _keydown );
-			window.removeEventListener( 'keyup', _keyup );
+	if ( this.dragToLook ) {
 
-		};
+		this._status = 0;
 
-		const _contextmenu = this.contextMenu.bind( this );
-		const _pointermove = this.pointermove.bind( this );
-		const _pointerdown = this.pointerdown.bind( this );
-		const _pointerup = this.pointerup.bind( this );
-		const _pointercancel = this.pointercancel.bind( this );
-		const _keydown = this.keydown.bind( this );
-		const _keyup = this.keyup.bind( this );
+		this._moveState.yawLeft = this._moveState.pitchDown = 0;
 
-		this.domElement.addEventListener( 'contextmenu', _contextmenu );
-		this.domElement.addEventListener( 'pointerdown', _pointerdown );
-		this.domElement.addEventListener( 'pointermove', _pointermove );
-		this.domElement.addEventListener( 'pointerup', _pointerup );
-		this.domElement.addEventListener( 'pointercancel', _pointercancel );
+	} else {
 
-		window.addEventListener( 'keydown', _keydown );
-		window.addEventListener( 'keyup', _keyup );
+		this._moveState.forward = 0;
+		this._moveState.back = 0;
 
-		this.updateMovementVector();
-		this.updateRotationVector();
+		this._updateMovementVector();
 
 	}
 
+	this._updateRotationVector();
+
+}
+
+function onContextMenu( event ) {
+
+	if ( this.enabled === false ) return;
+
+	event.preventDefault();
+
 }
 
 export { FlyControls };

+ 777 - 786
examples/jsm/controls/OrbitControls.js

@@ -1,5 +1,5 @@
 import {
-	EventDispatcher,
+	Controls,
 	MOUSE,
 	Quaternion,
 	Spherical,
@@ -23,17 +23,30 @@ const _startEvent = { type: 'start' };
 const _endEvent = { type: 'end' };
 const _ray = new Ray();
 const _plane = new Plane();
-const TILT_LIMIT = Math.cos( 70 * MathUtils.DEG2RAD );
+const _TILT_LIMIT = Math.cos( 70 * MathUtils.DEG2RAD );
 
-class OrbitControls extends EventDispatcher {
+const _v = new Vector3();
+const _twoPI = 2 * Math.PI;
 
-	constructor( object, domElement ) {
+const _STATE = {
+	NONE: - 1,
+	ROTATE: 0,
+	DOLLY: 1,
+	PAN: 2,
+	TOUCH_ROTATE: 3,
+	TOUCH_PAN: 4,
+	TOUCH_DOLLY_PAN: 5,
+	TOUCH_DOLLY_ROTATE: 6
+};
+const _EPS = 0.000001;
 
-		super();
+class OrbitControls extends Controls {
 
-		this.object = object;
-		this.domElement = domElement;
-		this.domElement.style.touchAction = 'none'; // disable touch scroll
+	constructor( object, domElement = null ) {
+
+		super( object, domElement );
+
+		this.state = _STATE.NONE;
 
 		// Set to false to disable this control
 		this.enabled = true;
@@ -109,1421 +122,1399 @@ class OrbitControls extends EventDispatcher {
 		// the target DOM element for key events
 		this._domElementKeyEvents = null;
 
-		//
-		// public methods
-		//
+		// internals
 
-		this.getPolarAngle = function () {
+		this._lastPosition = new Vector3();
+		this._lastQuaternion = new Quaternion();
+		this._lastTargetPosition = new Vector3();
 
-			return spherical.phi;
+		// so camera.up is the orbit axis
+		this._quat = new Quaternion().setFromUnitVectors( object.up, new Vector3( 0, 1, 0 ) );
+		this._quatInverse = this._quat.clone().invert();
 
-		};
+		// current position in spherical coordinates
+		this._spherical = new Spherical();
+		this._sphericalDelta = new Spherical();
 
-		this.getAzimuthalAngle = function () {
+		this._scale = 1;
+		this._panOffset = new Vector3();
 
-			return spherical.theta;
+		this._rotateStart = new Vector2();
+		this._rotateEnd = new Vector2();
+		this._rotateDelta = new Vector2();
 
-		};
+		this._panStart = new Vector2();
+		this._panEnd = new Vector2();
+		this._panDelta = new Vector2();
 
-		this.getDistance = function () {
+		this._dollyStart = new Vector2();
+		this._dollyEnd = new Vector2();
+		this._dollyDelta = new Vector2();
 
-			return this.object.position.distanceTo( this.target );
+		this._dollyDirection = new Vector3();
+		this._mouse = new Vector2();
+		this._performCursorZoom = false;
 
-		};
+		this._pointers = [];
+		this._pointerPositions = {};
 
-		this.listenToKeyEvents = function ( domElement ) {
+		this._controlActive = false;
 
-			domElement.addEventListener( 'keydown', onKeyDown );
-			this._domElementKeyEvents = domElement;
+		// event listeners
 
-		};
+		this._onPointerMove = onPointerMove.bind( this );
+		this._onPointerDown = onPointerDown.bind( this );
+		this._onPointerUp = onPointerUp.bind( this );
+		this._onContextMenu = onContextMenu.bind( this );
+		this._onMouseWheel = onMouseWheel.bind( this );
+		this._onKeyDown = onKeyDown.bind( this );
 
-		this.stopListenToKeyEvents = function () {
+		this._onTouchStart = onTouchStart.bind( this );
+		this._onTouchMove = onTouchMove.bind( this );
 
-			this._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown );
-			this._domElementKeyEvents = null;
+		this._onMouseDown = onMouseDown.bind( this );
+		this._onMouseMove = onMouseMove.bind( this );
 
-		};
+		this._interceptControlDown = interceptControlDown.bind( this );
+		this._interceptControlUp = interceptControlUp.bind( this );
 
-		this.saveState = function () {
+		//
 
-			scope.target0.copy( scope.target );
-			scope.position0.copy( scope.object.position );
-			scope.zoom0 = scope.object.zoom;
+		if ( this.domElement !== null ) {
 
-		};
+			this.connect();
 
-		this.reset = function () {
+		}
 
-			scope.target.copy( scope.target0 );
-			scope.object.position.copy( scope.position0 );
-			scope.object.zoom = scope.zoom0;
+		this.update();
 
-			scope.object.updateProjectionMatrix();
-			scope.dispatchEvent( _changeEvent );
+	}
 
-			scope.update();
+	connect() {
 
-			state = STATE.NONE;
+		this.domElement.addEventListener( 'pointerdown', this._onPointerDown );
+		this.domElement.addEventListener( 'pointercancel', this._onPointerUp );
 
-		};
+		this.domElement.addEventListener( 'contextmenu', this._onContextMenu );
+		this.domElement.addEventListener( 'wheel', this._onMouseWheel, { passive: false } );
 
-		// this method is exposed, but perhaps it would be better if we can make it private...
-		this.update = function () {
+		const document = this.domElement.getRootNode(); // offscreen canvas compatibility
+		document.addEventListener( 'keydown', this._interceptControlDown, { passive: true, capture: true } );
 
-			const offset = new Vector3();
+		this.domElement.style.touchAction = 'none'; // disable touch scroll
 
-			// so camera.up is the orbit axis
-			const quat = new Quaternion().setFromUnitVectors( object.up, new Vector3( 0, 1, 0 ) );
-			const quatInverse = quat.clone().invert();
+	}
 
-			const lastPosition = new Vector3();
-			const lastQuaternion = new Quaternion();
-			const lastTargetPosition = new Vector3();
+	disconnect() {
 
-			const twoPI = 2 * Math.PI;
+		this.domElement.removeEventListener( 'pointerdown', this._onPointerDown );
+		this.domElement.removeEventListener( 'pointermove', this._onPointerMove );
+		this.domElement.removeEventListener( 'pointerup', this._onPointerUp );
+		this.domElement.removeEventListener( 'pointercancel', this._onPointerUp );
 
-			return function update( deltaTime = null ) {
+		this.domElement.removeEventListener( 'wheel', this._onMouseWheel );
+		this.domElement.removeEventListener( 'contextmenu', this._onContextMenu );
 
-				const position = scope.object.position;
+		this.stopListenToKeyEvents();
 
-				offset.copy( position ).sub( scope.target );
+		const document = this.domElement.getRootNode(); // offscreen canvas compatibility
+		document.removeEventListener( 'keydown', this._interceptControlDown, { capture: true } );
 
-				// rotate offset to "y-axis-is-up" space
-				offset.applyQuaternion( quat );
+		this.domElement.style.touchAction = 'auto';
 
-				// angle from z-axis around y-axis
-				spherical.setFromVector3( offset );
+	}
 
-				if ( scope.autoRotate && state === STATE.NONE ) {
+	dispose() {
 
-					rotateLeft( getAutoRotationAngle( deltaTime ) );
+		this.disconnect();
 
-				}
+	}
 
-				if ( scope.enableDamping ) {
+	getPolarAngle() {
 
-					spherical.theta += sphericalDelta.theta * scope.dampingFactor;
-					spherical.phi += sphericalDelta.phi * scope.dampingFactor;
+		return this._spherical.phi;
 
-				} else {
+	}
 
-					spherical.theta += sphericalDelta.theta;
-					spherical.phi += sphericalDelta.phi;
+	getAzimuthalAngle() {
 
-				}
+		return this._spherical.theta;
 
-				// restrict theta to be between desired limits
+	}
 
-				let min = scope.minAzimuthAngle;
-				let max = scope.maxAzimuthAngle;
+	getDistance() {
 
-				if ( isFinite( min ) && isFinite( max ) ) {
+		return this.object.position.distanceTo( this.target );
 
-					if ( min < - Math.PI ) min += twoPI; else if ( min > Math.PI ) min -= twoPI;
+	}
 
-					if ( max < - Math.PI ) max += twoPI; else if ( max > Math.PI ) max -= twoPI;
+	listenToKeyEvents( domElement ) {
 
-					if ( min <= max ) {
+		domElement.addEventListener( 'keydown', this._onKeyDown );
+		this._domElementKeyEvents = domElement;
 
-						spherical.theta = Math.max( min, Math.min( max, spherical.theta ) );
+	}
 
-					} else {
+	stopListenToKeyEvents() {
 
-						spherical.theta = ( spherical.theta > ( min + max ) / 2 ) ?
-							Math.max( min, spherical.theta ) :
-							Math.min( max, spherical.theta );
+		if ( this._domElementKeyEvents !== null ) {
 
-					}
+			this._domElementKeyEvents.removeEventListener( 'keydown', this._onKeyDown );
+			this._domElementKeyEvents = null;
 
-				}
+		}
+
+	}
 
-				// restrict phi to be between desired limits
-				spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) );
+	saveState() {
 
-				spherical.makeSafe();
+		this.target0.copy( this.target );
+		this.position0.copy( this.object.position );
+		this.zoom0 = this.object.zoom;
 
+	}
 
-				// move target to panned location
+	reset() {
 
-				if ( scope.enableDamping === true ) {
+		this.target.copy( this.target0 );
+		this.object.position.copy( this.position0 );
+		this.object.zoom = this.zoom0;
 
-					scope.target.addScaledVector( panOffset, scope.dampingFactor );
+		this.object.updateProjectionMatrix();
+		this.dispatchEvent( _changeEvent );
 
-				} else {
+		this.update();
 
-					scope.target.add( panOffset );
+		this.state = _STATE.NONE;
 
-				}
+	}
 
-				// Limit the target distance from the cursor to create a sphere around the center of interest
-				scope.target.sub( scope.cursor );
-				scope.target.clampLength( scope.minTargetRadius, scope.maxTargetRadius );
-				scope.target.add( scope.cursor );
+	update( deltaTime = null ) {
 
-				let zoomChanged = false;
-				// adjust the camera position based on zoom only if we're not zooming to the cursor or if it's an ortho camera
-				// we adjust zoom later in these cases
-				if ( scope.zoomToCursor && performCursorZoom || scope.object.isOrthographicCamera ) {
+		const position = this.object.position;
 
-					spherical.radius = clampDistance( spherical.radius );
+		_v.copy( position ).sub( this.target );
 
-				} else {
+		// rotate offset to "y-axis-is-up" space
+		_v.applyQuaternion( this._quat );
 
-					const prevRadius = spherical.radius;
-					spherical.radius = clampDistance( spherical.radius * scale );
-					zoomChanged = prevRadius != spherical.radius;
+		// angle from z-axis around y-axis
+		this._spherical.setFromVector3( _v );
 
-				}
+		if ( this.autoRotate && this.state === _STATE.NONE ) {
 
-				offset.setFromSpherical( spherical );
+			this._rotateLeft( this._getAutoRotationAngle( deltaTime ) );
 
-				// rotate offset back to "camera-up-vector-is-up" space
-				offset.applyQuaternion( quatInverse );
+		}
 
-				position.copy( scope.target ).add( offset );
+		if ( this.enableDamping ) {
 
-				scope.object.lookAt( scope.target );
+			this._spherical.theta += this._sphericalDelta.theta * this.dampingFactor;
+			this._spherical.phi += this._sphericalDelta.phi * this.dampingFactor;
 
-				if ( scope.enableDamping === true ) {
+		} else {
 
-					sphericalDelta.theta *= ( 1 - scope.dampingFactor );
-					sphericalDelta.phi *= ( 1 - scope.dampingFactor );
+			this._spherical.theta += this._sphericalDelta.theta;
+			this._spherical.phi += this._sphericalDelta.phi;
 
-					panOffset.multiplyScalar( 1 - scope.dampingFactor );
+		}
 
-				} else {
+		// restrict theta to be between desired limits
 
-					sphericalDelta.set( 0, 0, 0 );
+		let min = this.minAzimuthAngle;
+		let max = this.maxAzimuthAngle;
 
-					panOffset.set( 0, 0, 0 );
+		if ( isFinite( min ) && isFinite( max ) ) {
 
-				}
+			if ( min < - Math.PI ) min += _twoPI; else if ( min > Math.PI ) min -= _twoPI;
 
-				// adjust camera position
-				if ( scope.zoomToCursor && performCursorZoom ) {
+			if ( max < - Math.PI ) max += _twoPI; else if ( max > Math.PI ) max -= _twoPI;
 
-					let newRadius = null;
-					if ( scope.object.isPerspectiveCamera ) {
+			if ( min <= max ) {
 
-						// move the camera down the pointer ray
-						// this method avoids floating point error
-						const prevRadius = offset.length();
-						newRadius = clampDistance( prevRadius * scale );
+				this._spherical.theta = Math.max( min, Math.min( max, this._spherical.theta ) );
 
-						const radiusDelta = prevRadius - newRadius;
-						scope.object.position.addScaledVector( dollyDirection, radiusDelta );
-						scope.object.updateMatrixWorld();
+			} else {
 
-						zoomChanged = !! radiusDelta;
+				this._spherical.theta = ( this._spherical.theta > ( min + max ) / 2 ) ?
+					Math.max( min, this._spherical.theta ) :
+					Math.min( max, this._spherical.theta );
 
-					} else if ( scope.object.isOrthographicCamera ) {
+			}
 
-						// adjust the ortho camera position based on zoom changes
-						const mouseBefore = new Vector3( mouse.x, mouse.y, 0 );
-						mouseBefore.unproject( scope.object );
+		}
 
-						const prevZoom = scope.object.zoom;
-						scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / scale ) );
-						scope.object.updateProjectionMatrix();
+		// restrict phi to be between desired limits
+		this._spherical.phi = Math.max( this.minPolarAngle, Math.min( this.maxPolarAngle, this._spherical.phi ) );
 
-						zoomChanged = prevZoom !== scope.object.zoom;
+		this._spherical.makeSafe();
 
-						const mouseAfter = new Vector3( mouse.x, mouse.y, 0 );
-						mouseAfter.unproject( scope.object );
 
-						scope.object.position.sub( mouseAfter ).add( mouseBefore );
-						scope.object.updateMatrixWorld();
+		// move target to panned location
 
-						newRadius = offset.length();
+		if ( this.enableDamping === true ) {
 
-					} else {
+			this.target.addScaledVector( this._panOffset, this.dampingFactor );
 
-						console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - zoom to cursor disabled.' );
-						scope.zoomToCursor = false;
+		} else {
 
-					}
+			this.target.add( this._panOffset );
 
-					// handle the placement of the target
-					if ( newRadius !== null ) {
+		}
 
-						if ( this.screenSpacePanning ) {
+		// Limit the target distance from the cursor to create a sphere around the center of interest
+		this.target.sub( this.cursor );
+		this.target.clampLength( this.minTargetRadius, this.maxTargetRadius );
+		this.target.add( this.cursor );
 
-							// position the orbit target in front of the new camera position
-							scope.target.set( 0, 0, - 1 )
-								.transformDirection( scope.object.matrix )
-								.multiplyScalar( newRadius )
-								.add( scope.object.position );
+		let zoomChanged = false;
+		// adjust the camera position based on zoom only if we're not zooming to the cursor or if it's an ortho camera
+		// we adjust zoom later in these cases
+		if ( this.zoomToCursor && this._performCursorZoom || this.object.isOrthographicCamera ) {
 
-						} else {
+			this._spherical.radius = this._clampDistance( this._spherical.radius );
 
-							// get the ray and translation plane to compute target
-							_ray.origin.copy( scope.object.position );
-							_ray.direction.set( 0, 0, - 1 ).transformDirection( scope.object.matrix );
+		} else {
 
-							// if the camera is 20 degrees above the horizon then don't adjust the focus target to avoid
-							// extremely large values
-							if ( Math.abs( scope.object.up.dot( _ray.direction ) ) < TILT_LIMIT ) {
+			const prevRadius = this._spherical.radius;
+			this._spherical.radius = this._clampDistance( this._spherical.radius * this._scale );
+			zoomChanged = prevRadius != this._spherical.radius;
 
-								object.lookAt( scope.target );
+		}
 
-							} else {
+		_v.setFromSpherical( this._spherical );
 
-								_plane.setFromNormalAndCoplanarPoint( scope.object.up, scope.target );
-								_ray.intersectPlane( _plane, scope.target );
+		// rotate offset back to "camera-up-vector-is-up" space
+		_v.applyQuaternion( this._quatInverse );
 
-							}
+		position.copy( this.target ).add( _v );
 
-						}
+		this.object.lookAt( this.target );
 
-					}
+		if ( this.enableDamping === true ) {
 
-				} else if ( scope.object.isOrthographicCamera ) {
+			this._sphericalDelta.theta *= ( 1 - this.dampingFactor );
+			this._sphericalDelta.phi *= ( 1 - this.dampingFactor );
 
-					const prevZoom = scope.object.zoom;
-					scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / scale ) );
+			this._panOffset.multiplyScalar( 1 - this.dampingFactor );
 
-					if ( prevZoom !== scope.object.zoom ) {
+		} else {
 
-						scope.object.updateProjectionMatrix();
-						zoomChanged = true;
+			this._sphericalDelta.set( 0, 0, 0 );
 
-					}
+			this._panOffset.set( 0, 0, 0 );
 
-				}
+		}
 
-				scale = 1;
-				performCursorZoom = false;
+		// adjust camera position
+		if ( this.zoomToCursor && this._performCursorZoom ) {
 
-				// update condition is:
-				// min(camera displacement, camera rotation in radians)^2 > EPS
-				// using small-angle approximation cos(x/2) = 1 - x^2 / 8
+			let newRadius = null;
+			if ( this.object.isPerspectiveCamera ) {
 
-				if ( zoomChanged ||
-					lastPosition.distanceToSquared( scope.object.position ) > EPS ||
-					8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ||
-					lastTargetPosition.distanceToSquared( scope.target ) > EPS ) {
+				// move the camera down the pointer ray
+				// this method avoids floating point error
+				const prevRadius = _v.length();
+				newRadius = this._clampDistance( prevRadius * this._scale );
 
-					scope.dispatchEvent( _changeEvent );
+				const radiusDelta = prevRadius - newRadius;
+				this.object.position.addScaledVector( this._dollyDirection, radiusDelta );
+				this.object.updateMatrixWorld();
 
-					lastPosition.copy( scope.object.position );
-					lastQuaternion.copy( scope.object.quaternion );
-					lastTargetPosition.copy( scope.target );
+				zoomChanged = !! radiusDelta;
 
-					return true;
+			} else if ( this.object.isOrthographicCamera ) {
 
-				}
+				// adjust the ortho camera position based on zoom changes
+				const mouseBefore = new Vector3( this._mouse.x, this._mouse.y, 0 );
+				mouseBefore.unproject( this.object );
 
-				return false;
+				const prevZoom = this.object.zoom;
+				this.object.zoom = Math.max( this.minZoom, Math.min( this.maxZoom, this.object.zoom / this._scale ) );
+				this.object.updateProjectionMatrix();
 
-			};
+				zoomChanged = prevZoom !== this.object.zoom;
 
-		}();
+				const mouseAfter = new Vector3( this._mouse.x, this._mouse.y, 0 );
+				mouseAfter.unproject( this.object );
 
-		this.dispose = function () {
+				this.object.position.sub( mouseAfter ).add( mouseBefore );
+				this.object.updateMatrixWorld();
 
-			scope.domElement.removeEventListener( 'contextmenu', onContextMenu );
+				newRadius = _v.length();
 
-			scope.domElement.removeEventListener( 'pointerdown', onPointerDown );
-			scope.domElement.removeEventListener( 'pointercancel', onPointerUp );
-			scope.domElement.removeEventListener( 'wheel', onMouseWheel );
+			} else {
 
-			scope.domElement.removeEventListener( 'pointermove', onPointerMove );
-			scope.domElement.removeEventListener( 'pointerup', onPointerUp );
+				console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - zoom to cursor disabled.' );
+				this.zoomToCursor = false;
 
-			const document = scope.domElement.getRootNode(); // offscreen canvas compatibility
+			}
 
-			document.removeEventListener( 'keydown', interceptControlDown, { capture: true } );
+			// handle the placement of the target
+			if ( newRadius !== null ) {
 
-			if ( scope._domElementKeyEvents !== null ) {
+				if ( this.screenSpacePanning ) {
 
-				scope._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown );
-				scope._domElementKeyEvents = null;
+					// position the orbit target in front of the new camera position
+					this.target.set( 0, 0, - 1 )
+						.transformDirection( this.object.matrix )
+						.multiplyScalar( newRadius )
+						.add( this.object.position );
 
-			}
+				} else {
 
-			//scope.dispatchEvent( { type: 'dispose' } ); // should this be added here?
+					// get the ray and translation plane to compute target
+					_ray.origin.copy( this.object.position );
+					_ray.direction.set( 0, 0, - 1 ).transformDirection( this.object.matrix );
 
-		};
+					// if the camera is 20 degrees above the horizon then don't adjust the focus target to avoid
+					// extremely large values
+					if ( Math.abs( this.object.up.dot( _ray.direction ) ) < _TILT_LIMIT ) {
 
-		//
-		// internals
-		//
+						this.object.lookAt( this.target );
 
-		const scope = this;
-
-		const STATE = {
-			NONE: - 1,
-			ROTATE: 0,
-			DOLLY: 1,
-			PAN: 2,
-			TOUCH_ROTATE: 3,
-			TOUCH_PAN: 4,
-			TOUCH_DOLLY_PAN: 5,
-			TOUCH_DOLLY_ROTATE: 6
-		};
+					} else {
 
-		let state = STATE.NONE;
+						_plane.setFromNormalAndCoplanarPoint( this.object.up, this.target );
+						_ray.intersectPlane( _plane, this.target );
 
-		const EPS = 0.000001;
+					}
 
-		// current position in spherical coordinates
-		const spherical = new Spherical();
-		const sphericalDelta = new Spherical();
+				}
 
-		let scale = 1;
-		const panOffset = new Vector3();
+			}
 
-		const rotateStart = new Vector2();
-		const rotateEnd = new Vector2();
-		const rotateDelta = new Vector2();
+		} else if ( this.object.isOrthographicCamera ) {
 
-		const panStart = new Vector2();
-		const panEnd = new Vector2();
-		const panDelta = new Vector2();
+			const prevZoom = this.object.zoom;
+			this.object.zoom = Math.max( this.minZoom, Math.min( this.maxZoom, this.object.zoom / this._scale ) );
 
-		const dollyStart = new Vector2();
-		const dollyEnd = new Vector2();
-		const dollyDelta = new Vector2();
+			if ( prevZoom !== this.object.zoom ) {
 
-		const dollyDirection = new Vector3();
-		const mouse = new Vector2();
-		let performCursorZoom = false;
+				this.object.updateProjectionMatrix();
+				zoomChanged = true;
 
-		const pointers = [];
-		const pointerPositions = {};
+			}
 
-		let controlActive = false;
+		}
 
-		function getAutoRotationAngle( deltaTime ) {
+		this._scale = 1;
+		this._performCursorZoom = false;
 
-			if ( deltaTime !== null ) {
+		// update condition is:
+		// min(camera displacement, camera rotation in radians)^2 > EPS
+		// using small-angle approximation cos(x/2) = 1 - x^2 / 8
 
-				return ( 2 * Math.PI / 60 * scope.autoRotateSpeed ) * deltaTime;
+		if ( zoomChanged ||
+			this._lastPosition.distanceToSquared( this.object.position ) > _EPS ||
+			8 * ( 1 - this._lastQuaternion.dot( this.object.quaternion ) ) > _EPS ||
+			this._lastTargetPosition.distanceToSquared( this.target ) > _EPS ) {
 
-			} else {
+			this.dispatchEvent( _changeEvent );
 
-				return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed;
+			this._lastPosition.copy( this.object.position );
+			this._lastQuaternion.copy( this.object.quaternion );
+			this._lastTargetPosition.copy( this.target );
 
-			}
+			return true;
 
 		}
 
-		function getZoomScale( delta ) {
+		return false;
 
-			const normalizedDelta = Math.abs( delta * 0.01 );
-			return Math.pow( 0.95, scope.zoomSpeed * normalizedDelta );
-
-		}
+	}
 
-		function rotateLeft( angle ) {
+	_getAutoRotationAngle( deltaTime ) {
 
-			sphericalDelta.theta -= angle;
+		if ( deltaTime !== null ) {
 
-		}
+			return ( _twoPI / 60 * this.autoRotateSpeed ) * deltaTime;
 
-		function rotateUp( angle ) {
+		} else {
 
-			sphericalDelta.phi -= angle;
+			return _twoPI / 60 / 60 * this.autoRotateSpeed;
 
 		}
 
-		const panLeft = function () {
+	}
 
-			const v = new Vector3();
+	_getZoomScale( delta ) {
 
-			return function panLeft( distance, objectMatrix ) {
+		const normalizedDelta = Math.abs( delta * 0.01 );
+		return Math.pow( 0.95, this.zoomSpeed * normalizedDelta );
 
-				v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix
-				v.multiplyScalar( - distance );
+	}
 
-				panOffset.add( v );
+	_rotateLeft( angle ) {
 
-			};
+		this._sphericalDelta.theta -= angle;
 
-		}();
+	}
 
-		const panUp = function () {
+	_rotateUp( angle ) {
 
-			const v = new Vector3();
+		this._sphericalDelta.phi -= angle;
 
-			return function panUp( distance, objectMatrix ) {
+	}
 
-				if ( scope.screenSpacePanning === true ) {
+	_panLeft( distance, objectMatrix ) {
 
-					v.setFromMatrixColumn( objectMatrix, 1 );
+		_v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix
+		_v.multiplyScalar( - distance );
 
-				} else {
+		this._panOffset.add( _v );
 
-					v.setFromMatrixColumn( objectMatrix, 0 );
-					v.crossVectors( scope.object.up, v );
+	}
 
-				}
+	_panUp( distance, objectMatrix ) {
 
-				v.multiplyScalar( distance );
+		if ( this.screenSpacePanning === true ) {
 
-				panOffset.add( v );
+			_v.setFromMatrixColumn( objectMatrix, 1 );
 
-			};
+		} else {
 
-		}();
+			_v.setFromMatrixColumn( objectMatrix, 0 );
+			_v.crossVectors( this.object.up, _v );
 
-		// deltaX and deltaY are in pixels; right and down are positive
-		const pan = function () {
+		}
 
-			const offset = new Vector3();
+		_v.multiplyScalar( distance );
 
-			return function pan( deltaX, deltaY ) {
+		this._panOffset.add( _v );
 
-				const element = scope.domElement;
+	}
 
-				if ( scope.object.isPerspectiveCamera ) {
+	// deltaX and deltaY are in pixels; right and down are positive
+	_pan( deltaX, deltaY ) {
 
-					// perspective
-					const position = scope.object.position;
-					offset.copy( position ).sub( scope.target );
-					let targetDistance = offset.length();
+		const element = this.domElement;
 
-					// half of the fov is center to top of screen
-					targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 );
+		if ( this.object.isPerspectiveCamera ) {
 
-					// we use only clientHeight here so aspect ratio does not distort speed
-					panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix );
-					panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix );
+			// perspective
+			const position = this.object.position;
+			_v.copy( position ).sub( this.target );
+			let targetDistance = _v.length();
 
-				} else if ( scope.object.isOrthographicCamera ) {
+			// half of the fov is center to top of screen
+			targetDistance *= Math.tan( ( this.object.fov / 2 ) * Math.PI / 180.0 );
 
-					// orthographic
-					panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix );
-					panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix );
+			// we use only clientHeight here so aspect ratio does not distort speed
+			this._panLeft( 2 * deltaX * targetDistance / element.clientHeight, this.object.matrix );
+			this._panUp( 2 * deltaY * targetDistance / element.clientHeight, this.object.matrix );
 
-				} else {
+		} else if ( this.object.isOrthographicCamera ) {
 
-					// camera neither orthographic nor perspective
-					console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' );
-					scope.enablePan = false;
+			// orthographic
+			this._panLeft( deltaX * ( this.object.right - this.object.left ) / this.object.zoom / element.clientWidth, this.object.matrix );
+			this._panUp( deltaY * ( this.object.top - this.object.bottom ) / this.object.zoom / element.clientHeight, this.object.matrix );
 
-				}
+		} else {
 
-			};
+			// camera neither orthographic nor perspective
+			console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' );
+			this.enablePan = false;
 
-		}();
+		}
 
-		function dollyOut( dollyScale ) {
+	}
 
-			if ( scope.object.isPerspectiveCamera || scope.object.isOrthographicCamera ) {
+	_dollyOut( dollyScale ) {
 
-				scale /= dollyScale;
+		if ( this.object.isPerspectiveCamera || this.object.isOrthographicCamera ) {
 
-			} else {
+			this._scale /= dollyScale;
 
-				console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
-				scope.enableZoom = false;
+		} else {
 
-			}
+			console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
+			this.enableZoom = false;
 
 		}
 
-		function dollyIn( dollyScale ) {
+	}
 
-			if ( scope.object.isPerspectiveCamera || scope.object.isOrthographicCamera ) {
+	_dollyIn( dollyScale ) {
 
-				scale *= dollyScale;
+		if ( this.object.isPerspectiveCamera || this.object.isOrthographicCamera ) {
 
-			} else {
+			this._scale *= dollyScale;
 
-				console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
-				scope.enableZoom = false;
+		} else {
 
-			}
+			console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
+			this.enableZoom = false;
 
 		}
 
-		function updateZoomParameters( x, y ) {
+	}
 
-			if ( ! scope.zoomToCursor ) {
+	_updateZoomParameters( x, y ) {
 
-				return;
-
-			}
+		if ( ! this.zoomToCursor ) {
 
-			performCursorZoom = true;
+			return;
 
-			const rect = scope.domElement.getBoundingClientRect();
-			const dx = x - rect.left;
-			const dy = y - rect.top;
-			const w = rect.width;
-			const h = rect.height;
+		}
 
-			mouse.x = ( dx / w ) * 2 - 1;
-			mouse.y = - ( dy / h ) * 2 + 1;
+		this._performCursorZoom = true;
 
-			dollyDirection.set( mouse.x, mouse.y, 1 ).unproject( scope.object ).sub( scope.object.position ).normalize();
+		const rect = this.domElement.getBoundingClientRect();
+		const dx = x - rect.left;
+		const dy = y - rect.top;
+		const w = rect.width;
+		const h = rect.height;
 
-		}
+		this._mouse.x = ( dx / w ) * 2 - 1;
+		this._mouse.y = - ( dy / h ) * 2 + 1;
 
-		function clampDistance( dist ) {
+		this._dollyDirection.set( this._mouse.x, this._mouse.y, 1 ).unproject( this.object ).sub( this.object.position ).normalize();
 
-			return Math.max( scope.minDistance, Math.min( scope.maxDistance, dist ) );
+	}
 
-		}
+	_clampDistance( dist ) {
 
-		//
-		// event callbacks - update the object state
-		//
-
-		function handleMouseDownRotate( event ) {
+		return Math.max( this.minDistance, Math.min( this.maxDistance, dist ) );
 
-			rotateStart.set( event.clientX, event.clientY );
+	}
 
-		}
+	//
+	// event callbacks - update the object state
+	//
 
-		function handleMouseDownDolly( event ) {
+	_handleMouseDownRotate( event ) {
 
-			updateZoomParameters( event.clientX, event.clientX );
-			dollyStart.set( event.clientX, event.clientY );
+		this._rotateStart.set( event.clientX, event.clientY );
 
-		}
+	}
 
-		function handleMouseDownPan( event ) {
+	_handleMouseDownDolly( event ) {
 
-			panStart.set( event.clientX, event.clientY );
+		this._updateZoomParameters( event.clientX, event.clientX );
+		this._dollyStart.set( event.clientX, event.clientY );
 
-		}
+	}
 
-		function handleMouseMoveRotate( event ) {
+	_handleMouseDownPan( event ) {
 
-			rotateEnd.set( event.clientX, event.clientY );
+		this._panStart.set( event.clientX, event.clientY );
 
-			rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed );
+	}
 
-			const element = scope.domElement;
+	_handleMouseMoveRotate( event ) {
 
-			rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height
+		this._rotateEnd.set( event.clientX, event.clientY );
 
-			rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight );
+		this._rotateDelta.subVectors( this._rotateEnd, this._rotateStart ).multiplyScalar( this.rotateSpeed );
 
-			rotateStart.copy( rotateEnd );
+		const element = this.domElement;
 
-			scope.update();
+		this._rotateLeft( _twoPI * this._rotateDelta.x / element.clientHeight ); // yes, height
 
-		}
+		this._rotateUp( _twoPI * this._rotateDelta.y / element.clientHeight );
 
-		function handleMouseMoveDolly( event ) {
+		this._rotateStart.copy( this._rotateEnd );
 
-			dollyEnd.set( event.clientX, event.clientY );
+		this.update();
 
-			dollyDelta.subVectors( dollyEnd, dollyStart );
+	}
 
-			if ( dollyDelta.y > 0 ) {
+	_handleMouseMoveDolly( event ) {
 
-				dollyOut( getZoomScale( dollyDelta.y ) );
+		this._dollyEnd.set( event.clientX, event.clientY );
 
-			} else if ( dollyDelta.y < 0 ) {
+		this._dollyDelta.subVectors( this._dollyEnd, this._dollyStart );
 
-				dollyIn( getZoomScale( dollyDelta.y ) );
+		if ( this._dollyDelta.y > 0 ) {
 
-			}
+			this._dollyOut( this._getZoomScale( this._dollyDelta.y ) );
 
-			dollyStart.copy( dollyEnd );
+		} else if ( this._dollyDelta.y < 0 ) {
 
-			scope.update();
+			this._dollyIn( this._getZoomScale( this._dollyDelta.y ) );
 
 		}
 
-		function handleMouseMovePan( event ) {
+		this._dollyStart.copy( this._dollyEnd );
 
-			panEnd.set( event.clientX, event.clientY );
+		this.update();
 
-			panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed );
+	}
 
-			pan( panDelta.x, panDelta.y );
+	_handleMouseMovePan( event ) {
 
-			panStart.copy( panEnd );
+		this._panEnd.set( event.clientX, event.clientY );
 
-			scope.update();
+		this._panDelta.subVectors( this._panEnd, this._panStart ).multiplyScalar( this.panSpeed );
 
-		}
+		this._pan( this._panDelta.x, this._panDelta.y );
 
-		function handleMouseWheel( event ) {
+		this._panStart.copy( this._panEnd );
 
-			updateZoomParameters( event.clientX, event.clientY );
+		this.update();
 
-			if ( event.deltaY < 0 ) {
+	}
 
-				dollyIn( getZoomScale( event.deltaY ) );
+	_handleMouseWheel( event ) {
 
-			} else if ( event.deltaY > 0 ) {
+		this._updateZoomParameters( event.clientX, event.clientY );
 
-				dollyOut( getZoomScale( event.deltaY ) );
+		if ( event.deltaY < 0 ) {
 
-			}
+			this._dollyIn( this._getZoomScale( event.deltaY ) );
 
-			scope.update();
+		} else if ( event.deltaY > 0 ) {
+
+			this._dollyOut( this._getZoomScale( event.deltaY ) );
 
 		}
 
-		function handleKeyDown( event ) {
+		this.update();
 
-			let needsUpdate = false;
+	}
 
-			switch ( event.code ) {
+	_handleKeyDown( event ) {
 
-				case scope.keys.UP:
+		let needsUpdate = false;
 
-					if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
+		switch ( event.code ) {
 
-						rotateUp( 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight );
+			case this.keys.UP:
 
-					} else {
+				if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
 
-						pan( 0, scope.keyPanSpeed );
+					this._rotateUp( _twoPI * this.rotateSpeed / this.domElement.clientHeight );
 
-					}
+				} else {
 
-					needsUpdate = true;
-					break;
+					this._pan( 0, this.keyPanSpeed );
 
-				case scope.keys.BOTTOM:
+				}
 
-					if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
+				needsUpdate = true;
+				break;
 
-						rotateUp( - 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight );
+			case this.keys.BOTTOM:
 
-					} else {
+				if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
 
-						pan( 0, - scope.keyPanSpeed );
+					this._rotateUp( - _twoPI * this.rotateSpeed / this.domElement.clientHeight );
 
-					}
+				} else {
 
-					needsUpdate = true;
-					break;
+					this._pan( 0, - this.keyPanSpeed );
 
-				case scope.keys.LEFT:
+				}
 
-					if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
+				needsUpdate = true;
+				break;
 
-						rotateLeft( 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight );
+			case this.keys.LEFT:
 
-					} else {
+				if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
 
-						pan( scope.keyPanSpeed, 0 );
+					this._rotateLeft( _twoPI * this.rotateSpeed / this.domElement.clientHeight );
 
-					}
+				} else {
 
-					needsUpdate = true;
-					break;
+					this._pan( this.keyPanSpeed, 0 );
 
-				case scope.keys.RIGHT:
+				}
 
-					if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
+				needsUpdate = true;
+				break;
 
-						rotateLeft( - 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight );
+			case this.keys.RIGHT:
 
-					} else {
+				if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
 
-						pan( - scope.keyPanSpeed, 0 );
+					this._rotateLeft( - _twoPI * this.rotateSpeed / this.domElement.clientHeight );
 
-					}
+				} else {
 
-					needsUpdate = true;
-					break;
+					this._pan( - this.keyPanSpeed, 0 );
 
-			}
+				}
 
-			if ( needsUpdate ) {
+				needsUpdate = true;
+				break;
 
-				// prevent the browser from scrolling on cursor keys
-				event.preventDefault();
+		}
 
-				scope.update();
+		if ( needsUpdate ) {
 
-			}
+			// prevent the browser from scrolling on cursor keys
+			event.preventDefault();
 
+			this.update();
 
 		}
 
-		function handleTouchStartRotate( event ) {
 
-			if ( pointers.length === 1 ) {
+	}
+
+	_handleTouchStartRotate( event ) {
 
-				rotateStart.set( event.pageX, event.pageY );
+		if ( this._pointers.length === 1 ) {
 
-			} else {
+			this._rotateStart.set( event.pageX, event.pageY );
 
-				const position = getSecondPointerPosition( event );
+		} else {
 
-				const x = 0.5 * ( event.pageX + position.x );
-				const y = 0.5 * ( event.pageY + position.y );
+			const position = this._getSecondPointerPosition( event );
 
-				rotateStart.set( x, y );
+			const x = 0.5 * ( event.pageX + position.x );
+			const y = 0.5 * ( event.pageY + position.y );
 
-			}
+			this._rotateStart.set( x, y );
 
 		}
 
-		function handleTouchStartPan( event ) {
+	}
 
-			if ( pointers.length === 1 ) {
+	_handleTouchStartPan( event ) {
 
-				panStart.set( event.pageX, event.pageY );
+		if ( this._pointers.length === 1 ) {
 
-			} else {
+			this._panStart.set( event.pageX, event.pageY );
 
-				const position = getSecondPointerPosition( event );
+		} else {
 
-				const x = 0.5 * ( event.pageX + position.x );
-				const y = 0.5 * ( event.pageY + position.y );
+			const position = this._getSecondPointerPosition( event );
 
-				panStart.set( x, y );
+			const x = 0.5 * ( event.pageX + position.x );
+			const y = 0.5 * ( event.pageY + position.y );
 
-			}
+			this._panStart.set( x, y );
 
 		}
 
-		function handleTouchStartDolly( event ) {
-
-			const position = getSecondPointerPosition( event );
-
-			const dx = event.pageX - position.x;
-			const dy = event.pageY - position.y;
-
-			const distance = Math.sqrt( dx * dx + dy * dy );
-
-			dollyStart.set( 0, distance );
-
-		}
+	}
 
-		function handleTouchStartDollyPan( event ) {
+	_handleTouchStartDolly( event ) {
 
-			if ( scope.enableZoom ) handleTouchStartDolly( event );
+		const position = this._getSecondPointerPosition( event );
 
-			if ( scope.enablePan ) handleTouchStartPan( event );
+		const dx = event.pageX - position.x;
+		const dy = event.pageY - position.y;
 
-		}
+		const distance = Math.sqrt( dx * dx + dy * dy );
 
-		function handleTouchStartDollyRotate( event ) {
+		this._dollyStart.set( 0, distance );
 
-			if ( scope.enableZoom ) handleTouchStartDolly( event );
+	}
 
-			if ( scope.enableRotate ) handleTouchStartRotate( event );
+	_handleTouchStartDollyPan( event ) {
 
-		}
+		if ( this.enableZoom ) this._handleTouchStartDolly( event );
 
-		function handleTouchMoveRotate( event ) {
+		if ( this.enablePan ) this._handleTouchStartPan( event );
 
-			if ( pointers.length == 1 ) {
+	}
 
-				rotateEnd.set( event.pageX, event.pageY );
+	_handleTouchStartDollyRotate( event ) {
 
-			} else {
+		if ( this.enableZoom ) this._handleTouchStartDolly( event );
 
-				const position = getSecondPointerPosition( event );
+		if ( this.enableRotate ) this._handleTouchStartRotate( event );
 
-				const x = 0.5 * ( event.pageX + position.x );
-				const y = 0.5 * ( event.pageY + position.y );
+	}
 
-				rotateEnd.set( x, y );
+	_handleTouchMoveRotate( event ) {
 
-			}
+		if ( this._pointers.length == 1 ) {
 
-			rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed );
+			this._rotateEnd.set( event.pageX, event.pageY );
 
-			const element = scope.domElement;
+		} else {
 
-			rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height
+			const position = this._getSecondPointerPosition( event );
 
-			rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight );
+			const x = 0.5 * ( event.pageX + position.x );
+			const y = 0.5 * ( event.pageY + position.y );
 
-			rotateStart.copy( rotateEnd );
+			this._rotateEnd.set( x, y );
 
 		}
 
-		function handleTouchMovePan( event ) {
+		this._rotateDelta.subVectors( this._rotateEnd, this._rotateStart ).multiplyScalar( this.rotateSpeed );
 
-			if ( pointers.length === 1 ) {
+		const element = this.domElement;
 
-				panEnd.set( event.pageX, event.pageY );
+		this._rotateLeft( _twoPI * this._rotateDelta.x / element.clientHeight ); // yes, height
 
-			} else {
+		this._rotateUp( _twoPI * this._rotateDelta.y / element.clientHeight );
 
-				const position = getSecondPointerPosition( event );
+		this._rotateStart.copy( this._rotateEnd );
 
-				const x = 0.5 * ( event.pageX + position.x );
-				const y = 0.5 * ( event.pageY + position.y );
+	}
 
-				panEnd.set( x, y );
+	_handleTouchMovePan( event ) {
 
-			}
+		if ( this._pointers.length === 1 ) {
 
-			panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed );
+			this._panEnd.set( event.pageX, event.pageY );
 
-			pan( panDelta.x, panDelta.y );
+		} else {
 
-			panStart.copy( panEnd );
+			const position = this._getSecondPointerPosition( event );
 
-		}
+			const x = 0.5 * ( event.pageX + position.x );
+			const y = 0.5 * ( event.pageY + position.y );
 
-		function handleTouchMoveDolly( event ) {
+			this._panEnd.set( x, y );
 
-			const position = getSecondPointerPosition( event );
+		}
 
-			const dx = event.pageX - position.x;
-			const dy = event.pageY - position.y;
+		this._panDelta.subVectors( this._panEnd, this._panStart ).multiplyScalar( this.panSpeed );
 
-			const distance = Math.sqrt( dx * dx + dy * dy );
+		this._pan( this._panDelta.x, this._panDelta.y );
 
-			dollyEnd.set( 0, distance );
+		this._panStart.copy( this._panEnd );
 
-			dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) );
+	}
 
-			dollyOut( dollyDelta.y );
+	_handleTouchMoveDolly( event ) {
 
-			dollyStart.copy( dollyEnd );
+		const position = this._getSecondPointerPosition( event );
 
-			const centerX = ( event.pageX + position.x ) * 0.5;
-			const centerY = ( event.pageY + position.y ) * 0.5;
+		const dx = event.pageX - position.x;
+		const dy = event.pageY - position.y;
 
-			updateZoomParameters( centerX, centerY );
+		const distance = Math.sqrt( dx * dx + dy * dy );
 
-		}
+		this._dollyEnd.set( 0, distance );
 
-		function handleTouchMoveDollyPan( event ) {
+		this._dollyDelta.set( 0, Math.pow( this._dollyEnd.y / this._dollyStart.y, this.zoomSpeed ) );
 
-			if ( scope.enableZoom ) handleTouchMoveDolly( event );
+		this._dollyOut( this._dollyDelta.y );
 
-			if ( scope.enablePan ) handleTouchMovePan( event );
+		this._dollyStart.copy( this._dollyEnd );
 
-		}
+		const centerX = ( event.pageX + position.x ) * 0.5;
+		const centerY = ( event.pageY + position.y ) * 0.5;
 
-		function handleTouchMoveDollyRotate( event ) {
+		this._updateZoomParameters( centerX, centerY );
 
-			if ( scope.enableZoom ) handleTouchMoveDolly( event );
+	}
 
-			if ( scope.enableRotate ) handleTouchMoveRotate( event );
+	_handleTouchMoveDollyPan( event ) {
 
-		}
+		if ( this.enableZoom ) this._handleTouchMoveDolly( event );
 
-		//
-		// event handlers - FSM: listen for events and reset state
-		//
+		if ( this.enablePan ) this._handleTouchMovePan( event );
 
-		function onPointerDown( event ) {
+	}
 
-			if ( scope.enabled === false ) return;
+	_handleTouchMoveDollyRotate( event ) {
 
-			if ( pointers.length === 0 ) {
+		if ( this.enableZoom ) this._handleTouchMoveDolly( event );
 
-				scope.domElement.setPointerCapture( event.pointerId );
+		if ( this.enableRotate ) this._handleTouchMoveRotate( event );
 
-				scope.domElement.addEventListener( 'pointermove', onPointerMove );
-				scope.domElement.addEventListener( 'pointerup', onPointerUp );
+	}
 
-			}
+	// pointers
 
-			//
+	_addPointer( event ) {
 
-			if ( isTrackingPointer( event ) ) return;
+		this._pointers.push( event.pointerId );
 
-			//
+	}
 
-			addPointer( event );
+	_removePointer( event ) {
 
-			if ( event.pointerType === 'touch' ) {
+		delete this._pointerPositions[ event.pointerId ];
 
-				onTouchStart( event );
+		for ( let i = 0; i < this._pointers.length; i ++ ) {
 
-			} else {
+			if ( this._pointers[ i ] == event.pointerId ) {
 
-				onMouseDown( event );
+				this._pointers.splice( i, 1 );
+				return;
 
 			}
 
 		}
 
-		function onPointerMove( event ) {
-
-			if ( scope.enabled === false ) return;
+	}
 
-			if ( event.pointerType === 'touch' ) {
+	_isTrackingPointer( event ) {
 
-				onTouchMove( event );
+		for ( let i = 0; i < this._pointers.length; i ++ ) {
 
-			} else {
+			if ( this._pointers[ i ] == event.pointerId ) return true;
 
-				onMouseMove( event );
+		}
 
-			}
+		return false;
 
-		}
+	}
 
-		function onPointerUp( event ) {
+	_trackPointer( event ) {
 
-			removePointer( event );
+		let position = this._pointerPositions[ event.pointerId ];
 
-			switch ( pointers.length ) {
+		if ( position === undefined ) {
 
-				case 0:
+			position = new Vector2();
+			this._pointerPositions[ event.pointerId ] = position;
 
-					scope.domElement.releasePointerCapture( event.pointerId );
+		}
 
-					scope.domElement.removeEventListener( 'pointermove', onPointerMove );
-					scope.domElement.removeEventListener( 'pointerup', onPointerUp );
+		position.set( event.pageX, event.pageY );
 
-					scope.dispatchEvent( _endEvent );
+	}
 
-					state = STATE.NONE;
+	_getSecondPointerPosition( event ) {
 
-					break;
+		const pointerId = ( event.pointerId === this._pointers[ 0 ] ) ? this._pointers[ 1 ] : this._pointers[ 0 ];
 
-				case 1:
+		return this._pointerPositions[ pointerId ];
 
-					const pointerId = pointers[ 0 ];
-					const position = pointerPositions[ pointerId ];
+	}
 
-					// minimal placeholder event - allows state correction on pointer-up
-					onTouchStart( { pointerId: pointerId, pageX: position.x, pageY: position.y } );
+	//
 
-					break;
+	_customWheelEvent( event ) {
 
-			}
+		const mode = event.deltaMode;
 
-		}
+		// minimal wheel event altered to meet delta-zoom demand
+		const newEvent = {
+			clientX: event.clientX,
+			clientY: event.clientY,
+			deltaY: event.deltaY,
+		};
 
-		function onMouseDown( event ) {
+		switch ( mode ) {
 
-			let mouseAction;
+			case 1: // LINE_MODE
+				newEvent.deltaY *= 16;
+				break;
 
-			switch ( event.button ) {
+			case 2: // PAGE_MODE
+				newEvent.deltaY *= 100;
+				break;
 
-				case 0:
+		}
 
-					mouseAction = scope.mouseButtons.LEFT;
-					break;
+		// detect if event was triggered by pinching
+		if ( event.ctrlKey && ! this._controlActive ) {
 
-				case 1:
+			newEvent.deltaY *= 10;
 
-					mouseAction = scope.mouseButtons.MIDDLE;
-					break;
+		}
 
-				case 2:
+		return newEvent;
 
-					mouseAction = scope.mouseButtons.RIGHT;
-					break;
+	}
 
-				default:
+}
 
-					mouseAction = - 1;
+function onPointerDown( event ) {
 
-			}
+	if ( this.enabled === false ) return;
 
-			switch ( mouseAction ) {
+	if ( this._pointers.length === 0 ) {
 
-				case MOUSE.DOLLY:
+		this.domElement.setPointerCapture( event.pointerId );
 
-					if ( scope.enableZoom === false ) return;
+		this.domElement.addEventListener( 'pointermove', this._onPointerMove );
+		this.domElement.addEventListener( 'pointerup', this._onPointerUp );
 
-					handleMouseDownDolly( event );
+	}
 
-					state = STATE.DOLLY;
+	//
 
-					break;
+	if ( this._isTrackingPointer( event ) ) return;
 
-				case MOUSE.ROTATE:
+	//
 
-					if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
+	this._addPointer( event );
 
-						if ( scope.enablePan === false ) return;
+	if ( event.pointerType === 'touch' ) {
 
-						handleMouseDownPan( event );
+		this._onTouchStart( event );
 
-						state = STATE.PAN;
+	} else {
 
-					} else {
+		this._onMouseDown( event );
 
-						if ( scope.enableRotate === false ) return;
+	}
 
-						handleMouseDownRotate( event );
+}
 
-						state = STATE.ROTATE;
+function onPointerMove( event ) {
 
-					}
+	if ( this.enabled === false ) return;
 
-					break;
+	if ( event.pointerType === 'touch' ) {
 
-				case MOUSE.PAN:
+		this._onTouchMove( event );
 
-					if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
+	} else {
 
-						if ( scope.enableRotate === false ) return;
+		this._onMouseMove( event );
 
-						handleMouseDownRotate( event );
+	}
 
-						state = STATE.ROTATE;
+}
 
-					} else {
+function onPointerUp( event ) {
 
-						if ( scope.enablePan === false ) return;
+	this._removePointer( event );
 
-						handleMouseDownPan( event );
+	switch ( this._pointers.length ) {
 
-						state = STATE.PAN;
+		case 0:
 
-					}
+			this.domElement.releasePointerCapture( event.pointerId );
 
-					break;
+			this.domElement.removeEventListener( 'pointermove', this._onPointerMove );
+			this.domElement.removeEventListener( 'pointerup', this._onPointerUp );
 
-				default:
+			this.dispatchEvent( _endEvent );
 
-					state = STATE.NONE;
+			this.state = _STATE.NONE;
 
-			}
+			break;
 
-			if ( state !== STATE.NONE ) {
+		case 1:
 
-				scope.dispatchEvent( _startEvent );
+			const pointerId = this._pointers[ 0 ];
+			const position = this._pointerPositions[ pointerId ];
 
-			}
+			// minimal placeholder event - allows state correction on pointer-up
+			this._onTouchStart( { pointerId: pointerId, pageX: position.x, pageY: position.y } );
 
-		}
+			break;
 
-		function onMouseMove( event ) {
+	}
 
-			switch ( state ) {
+}
 
-				case STATE.ROTATE:
+function onMouseDown( event ) {
 
-					if ( scope.enableRotate === false ) return;
+	let mouseAction;
 
-					handleMouseMoveRotate( event );
+	switch ( event.button ) {
 
-					break;
+		case 0:
 
-				case STATE.DOLLY:
+			mouseAction = this.mouseButtons.LEFT;
+			break;
 
-					if ( scope.enableZoom === false ) return;
+		case 1:
 
-					handleMouseMoveDolly( event );
+			mouseAction = this.mouseButtons.MIDDLE;
+			break;
 
-					break;
+		case 2:
 
-				case STATE.PAN:
+			mouseAction = this.mouseButtons.RIGHT;
+			break;
 
-					if ( scope.enablePan === false ) return;
+		default:
 
-					handleMouseMovePan( event );
+			mouseAction = - 1;
 
-					break;
+	}
 
-			}
+	switch ( mouseAction ) {
 
-		}
+		case MOUSE.DOLLY:
 
-		function onMouseWheel( event ) {
+			if ( this.enableZoom === false ) return;
 
-			if ( scope.enabled === false || scope.enableZoom === false || state !== STATE.NONE ) return;
+			this._handleMouseDownDolly( event );
 
-			event.preventDefault();
+			this.state = _STATE.DOLLY;
 
-			scope.dispatchEvent( _startEvent );
+			break;
 
-			handleMouseWheel( customWheelEvent( event ) );
+		case MOUSE.ROTATE:
 
-			scope.dispatchEvent( _endEvent );
+			if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
 
-		}
+				if ( this.enablePan === false ) return;
 
-		function customWheelEvent( event ) {
+				this._handleMouseDownPan( event );
 
-			const mode = event.deltaMode;
+				this.state = _STATE.PAN;
 
-			// minimal wheel event altered to meet delta-zoom demand
-			const newEvent = {
-				clientX: event.clientX,
-				clientY: event.clientY,
-				deltaY: event.deltaY,
-			};
+			} else {
 
-			switch ( mode ) {
+				if ( this.enableRotate === false ) return;
 
-				case 1: // LINE_MODE
-					newEvent.deltaY *= 16;
-					break;
+				this._handleMouseDownRotate( event );
 
-				case 2: // PAGE_MODE
-					newEvent.deltaY *= 100;
-					break;
+				this.state = _STATE.ROTATE;
 
 			}
 
-			// detect if event was triggered by pinching
-			if ( event.ctrlKey && ! controlActive ) {
+			break;
 
-				newEvent.deltaY *= 10;
+		case MOUSE.PAN:
 
-			}
+			if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
 
-			return newEvent;
+				if ( this.enableRotate === false ) return;
 
-		}
+				this._handleMouseDownRotate( event );
 
-		function interceptControlDown( event ) {
+				this.state = _STATE.ROTATE;
 
-			if ( event.key === 'Control' ) {
-
-				controlActive = true;
+			} else {
 
+				if ( this.enablePan === false ) return;
 
-				const document = scope.domElement.getRootNode(); // offscreen canvas compatibility
+				this._handleMouseDownPan( event );
 
-				document.addEventListener( 'keyup', interceptControlUp, { passive: true, capture: true } );
+				this.state = _STATE.PAN;
 
 			}
 
-		}
-
-		function interceptControlUp( event ) {
+			break;
 
-			if ( event.key === 'Control' ) {
+		default:
 
-				controlActive = false;
+			this.state = _STATE.NONE;
 
+	}
 
-				const document = scope.domElement.getRootNode(); // offscreen canvas compatibility
+	if ( this.state !== _STATE.NONE ) {
 
-				document.removeEventListener( 'keyup', interceptControlUp, { passive: true, capture: true } );
+		this.dispatchEvent( _startEvent );
 
-			}
+	}
 
-		}
+}
 
-		function onKeyDown( event ) {
+function onMouseMove( event ) {
 
-			if ( scope.enabled === false || scope.enablePan === false ) return;
+	switch ( this.state ) {
 
-			handleKeyDown( event );
+		case _STATE.ROTATE:
 
-		}
+			if ( this.enableRotate === false ) return;
 
-		function onTouchStart( event ) {
+			this._handleMouseMoveRotate( event );
 
-			trackPointer( event );
+			break;
 
-			switch ( pointers.length ) {
+		case _STATE.DOLLY:
 
-				case 1:
+			if ( this.enableZoom === false ) return;
 
-					switch ( scope.touches.ONE ) {
+			this._handleMouseMoveDolly( event );
 
-						case TOUCH.ROTATE:
+			break;
 
-							if ( scope.enableRotate === false ) return;
+		case _STATE.PAN:
 
-							handleTouchStartRotate( event );
+			if ( this.enablePan === false ) return;
 
-							state = STATE.TOUCH_ROTATE;
+			this._handleMouseMovePan( event );
 
-							break;
+			break;
 
-						case TOUCH.PAN:
+	}
 
-							if ( scope.enablePan === false ) return;
+}
 
-							handleTouchStartPan( event );
+function onMouseWheel( event ) {
 
-							state = STATE.TOUCH_PAN;
+	if ( this.enabled === false || this.enableZoom === false || this.state !== _STATE.NONE ) return;
 
-							break;
+	event.preventDefault();
 
-						default:
+	this.dispatchEvent( _startEvent );
 
-							state = STATE.NONE;
+	this._handleMouseWheel( this._customWheelEvent( event ) );
 
-					}
+	this.dispatchEvent( _endEvent );
 
-					break;
+}
 
-				case 2:
+function onKeyDown( event ) {
 
-					switch ( scope.touches.TWO ) {
+	if ( this.enabled === false || this.enablePan === false ) return;
 
-						case TOUCH.DOLLY_PAN:
+	this._handleKeyDown( event );
 
-							if ( scope.enableZoom === false && scope.enablePan === false ) return;
+}
 
-							handleTouchStartDollyPan( event );
+function onTouchStart( event ) {
 
-							state = STATE.TOUCH_DOLLY_PAN;
+	this._trackPointer( event );
 
-							break;
+	switch ( this._pointers.length ) {
 
-						case TOUCH.DOLLY_ROTATE:
+		case 1:
 
-							if ( scope.enableZoom === false && scope.enableRotate === false ) return;
+			switch ( this.touches.ONE ) {
 
-							handleTouchStartDollyRotate( event );
+				case TOUCH.ROTATE:
 
-							state = STATE.TOUCH_DOLLY_ROTATE;
+					if ( this.enableRotate === false ) return;
 
-							break;
+					this._handleTouchStartRotate( event );
 
-						default:
+					this.state = _STATE.TOUCH_ROTATE;
 
-							state = STATE.NONE;
+					break;
 
-					}
+				case TOUCH.PAN:
 
-					break;
+					if ( this.enablePan === false ) return;
 
-				default:
+					this._handleTouchStartPan( event );
 
-					state = STATE.NONE;
+					this.state = _STATE.TOUCH_PAN;
 
-			}
+					break;
 
-			if ( state !== STATE.NONE ) {
+				default:
 
-				scope.dispatchEvent( _startEvent );
+					this.state = _STATE.NONE;
 
 			}
 
-		}
+			break;
 
-		function onTouchMove( event ) {
+		case 2:
 
-			trackPointer( event );
+			switch ( this.touches.TWO ) {
 
-			switch ( state ) {
+				case TOUCH.DOLLY_PAN:
 
-				case STATE.TOUCH_ROTATE:
+					if ( this.enableZoom === false && this.enablePan === false ) return;
 
-					if ( scope.enableRotate === false ) return;
+					this._handleTouchStartDollyPan( event );
 
-					handleTouchMoveRotate( event );
-
-					scope.update();
+					this.state = _STATE.TOUCH_DOLLY_PAN;
 
 					break;
 
-				case STATE.TOUCH_PAN:
+				case TOUCH.DOLLY_ROTATE:
 
-					if ( scope.enablePan === false ) return;
+					if ( this.enableZoom === false && this.enableRotate === false ) return;
 
-					handleTouchMovePan( event );
+					this._handleTouchStartDollyRotate( event );
 
-					scope.update();
+					this.state = _STATE.TOUCH_DOLLY_ROTATE;
 
 					break;
 
-				case STATE.TOUCH_DOLLY_PAN:
+				default:
 
-					if ( scope.enableZoom === false && scope.enablePan === false ) return;
+					this.state = _STATE.NONE;
 
-					handleTouchMoveDollyPan( event );
+			}
 
-					scope.update();
+			break;
 
-					break;
+		default:
 
-				case STATE.TOUCH_DOLLY_ROTATE:
+			this.state = _STATE.NONE;
 
-					if ( scope.enableZoom === false && scope.enableRotate === false ) return;
+	}
 
-					handleTouchMoveDollyRotate( event );
+	if ( this.state !== _STATE.NONE ) {
 
-					scope.update();
+		this.dispatchEvent( _startEvent );
 
-					break;
+	}
 
-				default:
+}
 
-					state = STATE.NONE;
+function onTouchMove( event ) {
 
-			}
+	this._trackPointer( event );
 
-		}
+	switch ( this.state ) {
 
-		function onContextMenu( event ) {
+		case _STATE.TOUCH_ROTATE:
 
-			if ( scope.enabled === false ) return;
+			if ( this.enableRotate === false ) return;
 
-			event.preventDefault();
+			this._handleTouchMoveRotate( event );
 
-		}
+			this.update();
 
-		function addPointer( event ) {
+			break;
 
-			pointers.push( event.pointerId );
+		case _STATE.TOUCH_PAN:
 
-		}
+			if ( this.enablePan === false ) return;
 
-		function removePointer( event ) {
+			this._handleTouchMovePan( event );
 
-			delete pointerPositions[ event.pointerId ];
+			this.update();
 
-			for ( let i = 0; i < pointers.length; i ++ ) {
+			break;
 
-				if ( pointers[ i ] == event.pointerId ) {
+		case _STATE.TOUCH_DOLLY_PAN:
 
-					pointers.splice( i, 1 );
-					return;
+			if ( this.enableZoom === false && this.enablePan === false ) return;
 
-				}
+			this._handleTouchMoveDollyPan( event );
 
-			}
+			this.update();
 
-		}
+			break;
 
-		function isTrackingPointer( event ) {
+		case _STATE.TOUCH_DOLLY_ROTATE:
 
-			for ( let i = 0; i < pointers.length; i ++ ) {
+			if ( this.enableZoom === false && this.enableRotate === false ) return;
 
-				if ( pointers[ i ] == event.pointerId ) return true;
+			this._handleTouchMoveDollyRotate( event );
 
-			}
+			this.update();
 
-			return false;
+			break;
 
-		}
+		default:
 
-		function trackPointer( event ) {
+			this.state = _STATE.NONE;
 
-			let position = pointerPositions[ event.pointerId ];
+	}
 
-			if ( position === undefined ) {
+}
 
-				position = new Vector2();
-				pointerPositions[ event.pointerId ] = position;
+function onContextMenu( event ) {
 
-			}
+	if ( this.enabled === false ) return;
 
-			position.set( event.pageX, event.pageY );
+	event.preventDefault();
 
-		}
+}
 
-		function getSecondPointerPosition( event ) {
+function interceptControlDown( event ) {
 
-			const pointerId = ( event.pointerId === pointers[ 0 ] ) ? pointers[ 1 ] : pointers[ 0 ];
+	if ( event.key === 'Control' ) {
 
-			return pointerPositions[ pointerId ];
+		this._controlActive = true;
 
-		}
+		const document = this.domElement.getRootNode(); // offscreen canvas compatibility
 
-		//
+		document.addEventListener( 'keyup', this._interceptControlUp, { passive: true, capture: true } );
 
-		scope.domElement.addEventListener( 'contextmenu', onContextMenu );
+	}
+
+}
 
-		scope.domElement.addEventListener( 'pointerdown', onPointerDown );
-		scope.domElement.addEventListener( 'pointercancel', onPointerUp );
-		scope.domElement.addEventListener( 'wheel', onMouseWheel, { passive: false } );
+function interceptControlUp( event ) {
 
-		const document = scope.domElement.getRootNode(); // offscreen canvas compatibility
+	if ( event.key === 'Control' ) {
 
-		document.addEventListener( 'keydown', interceptControlDown, { passive: true, capture: true } );
+		this._controlActive = false;
 
-		// force an update at start
+		const document = this.domElement.getRootNode(); // offscreen canvas compatibility
 
-		this.update();
+		document.removeEventListener( 'keyup', this._interceptControlUp, { passive: true, capture: true } );
 
 	}
 

+ 24 - 15
examples/jsm/controls/PointerLockControls.js

@@ -1,6 +1,6 @@
 import {
+	Controls,
 	Euler,
-	EventDispatcher,
 	Vector3
 } from 'three';
 
@@ -13,14 +13,11 @@ const _unlockEvent = { type: 'unlock' };
 
 const _PI_2 = Math.PI / 2;
 
-class PointerLockControls extends EventDispatcher {
+class PointerLockControls extends Controls {
 
-	constructor( camera, domElement ) {
+	constructor( camera, domElement = null ) {
 
-		super();
-
-		this.camera = camera;
-		this.domElement = domElement;
+		super( camera, domElement );
 
 		this.isLocked = false;
 
@@ -31,11 +28,17 @@ class PointerLockControls extends EventDispatcher {
 
 		this.pointerSpeed = 1.0;
 
+		// event listeners
+
 		this._onMouseMove = onMouseMove.bind( this );
 		this._onPointerlockChange = onPointerlockChange.bind( this );
 		this._onPointerlockError = onPointerlockError.bind( this );
 
-		this.connect();
+		if ( this.domElement !== null ) {
+
+			this.connect();
+
+		}
 
 	}
 
@@ -61,24 +64,28 @@ class PointerLockControls extends EventDispatcher {
 
 	}
 
-	getObject() { // retaining this method for backward compatibility
+	getObject() {
 
-		return this.camera;
+		console.warn( 'THREE.PointerLockControls: getObject() has been deprecated. Use controls.object instead.' ); // @deprecated r169
+
+		return this.object;
 
 	}
 
 	getDirection( v ) {
 
-		return v.set( 0, 0, - 1 ).applyQuaternion( this.camera.quaternion );
+		return v.set( 0, 0, - 1 ).applyQuaternion( this.object.quaternion );
 
 	}
 
 	moveForward( distance ) {
 
+		if ( this.enabled === false ) return;
+
 		// move forward parallel to the xz-plane
 		// assumes camera.up is y-up
 
-		const camera = this.camera;
+		const camera = this.object;
 
 		_vector.setFromMatrixColumn( camera.matrix, 0 );
 
@@ -90,7 +97,9 @@ class PointerLockControls extends EventDispatcher {
 
 	moveRight( distance ) {
 
-		const camera = this.camera;
+		if ( this.enabled === false ) return;
+
+		const camera = this.object;
 
 		_vector.setFromMatrixColumn( camera.matrix, 0 );
 
@@ -116,12 +125,12 @@ class PointerLockControls extends EventDispatcher {
 
 function onMouseMove( event ) {
 
-	if ( this.isLocked === false ) return;
+	if ( this.enabled === false || this.isLocked === false ) return;
 
 	const movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0;
 	const movementY = event.movementY || event.mozMovementY || event.webkitMovementY || 0;
 
-	const camera = this.camera;
+	const camera = this.object;
 	_euler.setFromQuaternion( camera.quaternion );
 
 	_euler.y -= movementX * 0.002 * this.pointerSpeed;

+ 453 - 452
examples/jsm/controls/TrackballControls.js

@@ -1,5 +1,5 @@
 import {
-	EventDispatcher,
+	Controls,
 	MathUtils,
 	MOUSE,
 	Quaternion,
@@ -11,18 +11,25 @@ const _changeEvent = { type: 'change' };
 const _startEvent = { type: 'start' };
 const _endEvent = { type: 'end' };
 
-class TrackballControls extends EventDispatcher {
+const _EPS = 0.000001;
+const _STATE = { NONE: - 1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM_PAN: 4 };
 
-	constructor( object, domElement ) {
+const _v2 = new Vector2();
+const _mouseChange = new Vector2();
+const _objectUp = new Vector3();
+const _pan = new Vector3();
+const _axis = new Vector3();
+const _quaternion = new Quaternion();
+const _eyeDirection = new Vector3();
+const _objectUpDirection = new Vector3();
+const _objectSidewaysDirection = new Vector3();
+const _moveDirection = new Vector3();
 
-		super();
+class TrackballControls extends Controls {
 
-		const scope = this;
-		const STATE = { NONE: - 1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM_PAN: 4 };
+	constructor( object, domElement = null ) {
 
-		this.object = object;
-		this.domElement = domElement;
-		this.domElement.style.touchAction = 'none'; // disable touch scroll
+		super( object, domElement );
 
 		// API
 
@@ -50,779 +57,773 @@ class TrackballControls extends EventDispatcher {
 		this.keys = [ 'KeyA' /*A*/, 'KeyS' /*S*/, 'KeyD' /*D*/ ];
 
 		this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN };
-
-		// internals
+		this.state = _STATE.NONE;
+		this.keyState = _STATE.NONE;
 
 		this.target = new Vector3();
 
-		const EPS = 0.000001;
-
-		const lastPosition = new Vector3();
-		let lastZoom = 1;
-
-		let _state = STATE.NONE,
-			_keyState = STATE.NONE,
-
-			_touchZoomDistanceStart = 0,
-			_touchZoomDistanceEnd = 0,
+		// internals
 
-			_lastAngle = 0;
+		this._lastPosition = new Vector3();
+		this._lastZoom = 1;
+		this._touchZoomDistanceStart = 0;
+		this._touchZoomDistanceEnd = 0;
+		this._lastAngle = 0;
 
-		const _eye = new Vector3(),
+		this._eye = new Vector3();
 
-			_movePrev = new Vector2(),
-			_moveCurr = new Vector2(),
+		this._movePrev = new Vector2();
+		this._moveCurr = new Vector2();
 
-			_lastAxis = new Vector3(),
+		this._lastAxis = new Vector3();
 
-			_zoomStart = new Vector2(),
-			_zoomEnd = new Vector2(),
+		this._zoomStart = new Vector2();
+		this._zoomEnd = new Vector2();
 
-			_panStart = new Vector2(),
-			_panEnd = new Vector2(),
+		this._panStart = new Vector2();
+		this._panEnd = new Vector2();
 
-			_pointers = [],
-			_pointerPositions = {};
+		this._pointers = [];
+		this._pointerPositions = {};
 
-		// for reset
+		// event listeners
 
-		this.target0 = this.target.clone();
-		this.position0 = this.object.position.clone();
-		this.up0 = this.object.up.clone();
-		this.zoom0 = this.object.zoom;
+		this._onPointerMove = onPointerMove.bind( this );
+		this._onPointerDown = onPointerDown.bind( this );
+		this._onPointerUp = onPointerUp.bind( this );
+		this._onPointerCancel = onPointerCancel.bind( this );
+		this._onContextMenu = onContextMenu.bind( this );
+		this._onMouseWheel = onMouseWheel.bind( this );
+		this._onKeyDown = onKeyDown.bind( this );
+		this._onKeyUp = onKeyUp.bind( this );
 
-		// methods
+		this._onTouchStart = onTouchStart.bind( this );
+		this._onTouchMove = onTouchMove.bind( this );
+		this._onTouchEnd = onTouchEnd.bind( this );
 
-		this.handleResize = function () {
+		this._onMouseDown = onMouseDown.bind( this );
+		this._onMouseMove = onMouseMove.bind( this );
+		this._onMouseUp = onMouseUp.bind( this );
 
-			const box = scope.domElement.getBoundingClientRect();
-			// adjustments come from similar code in the jquery offset() function
-			const d = scope.domElement.ownerDocument.documentElement;
-			scope.screen.left = box.left + window.pageXOffset - d.clientLeft;
-			scope.screen.top = box.top + window.pageYOffset - d.clientTop;
-			scope.screen.width = box.width;
-			scope.screen.height = box.height;
+		// for reset
 
-		};
+		this._target0 = this.target.clone();
+		this._position0 = this.object.position.clone();
+		this._up0 = this.object.up.clone();
+		this._zoom0 = this.object.zoom;
 
-		const getMouseOnScreen = ( function () {
+		if ( domElement !== null ) {
 
-			const vector = new Vector2();
+			this.connect();
 
-			return function getMouseOnScreen( pageX, pageY ) {
+			this.handleResize();
 
-				vector.set(
-					( pageX - scope.screen.left ) / scope.screen.width,
-					( pageY - scope.screen.top ) / scope.screen.height
-				);
+		}
 
-				return vector;
+		// force an update at start
+		this.update();
 
-			};
+	}
 
-		}() );
+	connect() {
 
-		const getMouseOnCircle = ( function () {
+		window.addEventListener( 'keydown', this._onKeyDown );
+		window.addEventListener( 'keyup', this._onKeyUp );
 
-			const vector = new Vector2();
+		this.domElement.addEventListener( 'pointerdown', this._onPointerDown );
+		this.domElement.addEventListener( 'pointercancel', this._onPointerCancel );
+		this.domElement.addEventListener( 'wheel', this._onMouseWheel, { passive: false } );
+		this.domElement.addEventListener( 'contextmenu', this._onContextMenu );
 
-			return function getMouseOnCircle( pageX, pageY ) {
+		this.domElement.style.touchAction = 'none'; // disable touch scroll
 
-				vector.set(
-					( ( pageX - scope.screen.width * 0.5 - scope.screen.left ) / ( scope.screen.width * 0.5 ) ),
-					( ( scope.screen.height + 2 * ( scope.screen.top - pageY ) ) / scope.screen.width ) // screen.width intentional
-				);
+	}
 
-				return vector;
+	disconnect() {
 
-			};
+		window.removeEventListener( 'keydown', this._onKeyDown );
+		window.removeEventListener( 'keyup', this._onKeyUp );
 
-		}() );
+		this.domElement.removeEventListener( 'pointerdown', this._onPointerDown );
+		this.domElement.removeEventListener( 'pointermove', this._onPointerMove );
+		this.domElement.removeEventListener( 'pointerup', this._onPointerUp );
+		this.domElement.removeEventListener( 'pointercancel', this._onPointerCancel );
+		this.domElement.removeEventListener( 'wheel', this._onMouseWheel );
+		this.domElement.removeEventListener( 'contextmenu', this._onContextMenu );
 
-		this.rotateCamera = ( function () {
+		this.domElement.style.touchAction = 'auto'; // disable touch scroll
 
-			const axis = new Vector3(),
-				quaternion = new Quaternion(),
-				eyeDirection = new Vector3(),
-				objectUpDirection = new Vector3(),
-				objectSidewaysDirection = new Vector3(),
-				moveDirection = new Vector3();
+	}
 
-			return function rotateCamera() {
+	dispose() {
 
-				moveDirection.set( _moveCurr.x - _movePrev.x, _moveCurr.y - _movePrev.y, 0 );
-				let angle = moveDirection.length();
+		this.disconnect();
 
-				if ( angle ) {
+	}
 
-					_eye.copy( scope.object.position ).sub( scope.target );
+	handleResize() {
 
-					eyeDirection.copy( _eye ).normalize();
-					objectUpDirection.copy( scope.object.up ).normalize();
-					objectSidewaysDirection.crossVectors( objectUpDirection, eyeDirection ).normalize();
+		const box = this.domElement.getBoundingClientRect();
+		// adjustments come from similar code in the jquery offset() function
+		const d = this.domElement.ownerDocument.documentElement;
 
-					objectUpDirection.setLength( _moveCurr.y - _movePrev.y );
-					objectSidewaysDirection.setLength( _moveCurr.x - _movePrev.x );
+		this.screen.left = box.left + window.pageXOffset - d.clientLeft;
+		this.screen.top = box.top + window.pageYOffset - d.clientTop;
+		this.screen.width = box.width;
+		this.screen.height = box.height;
 
-					moveDirection.copy( objectUpDirection.add( objectSidewaysDirection ) );
+	}
 
-					axis.crossVectors( moveDirection, _eye ).normalize();
+	update() {
 
-					angle *= scope.rotateSpeed;
-					quaternion.setFromAxisAngle( axis, angle );
+		this._eye.subVectors( this.object.position, this.target );
 
-					_eye.applyQuaternion( quaternion );
-					scope.object.up.applyQuaternion( quaternion );
+		if ( ! this.noRotate ) {
 
-					_lastAxis.copy( axis );
-					_lastAngle = angle;
+			this._rotateCamera();
 
-				} else if ( ! scope.staticMoving && _lastAngle ) {
+		}
 
-					_lastAngle *= Math.sqrt( 1.0 - scope.dynamicDampingFactor );
-					_eye.copy( scope.object.position ).sub( scope.target );
-					quaternion.setFromAxisAngle( _lastAxis, _lastAngle );
-					_eye.applyQuaternion( quaternion );
-					scope.object.up.applyQuaternion( quaternion );
+		if ( ! this.noZoom ) {
 
-				}
+			this._zoomCamera();
 
-				_movePrev.copy( _moveCurr );
+		}
 
-			};
+		if ( ! this.noPan ) {
 
-		}() );
+			this._panCamera();
 
+		}
 
-		this.zoomCamera = function () {
+		this.object.position.addVectors( this.target, this._eye );
 
-			let factor;
+		if ( this.object.isPerspectiveCamera ) {
 
-			if ( _state === STATE.TOUCH_ZOOM_PAN ) {
+			this._checkDistances();
 
-				factor = _touchZoomDistanceStart / _touchZoomDistanceEnd;
-				_touchZoomDistanceStart = _touchZoomDistanceEnd;
+			this.object.lookAt( this.target );
 
-				if ( scope.object.isPerspectiveCamera ) {
+			if ( this._lastPosition.distanceToSquared( this.object.position ) > _EPS ) {
 
-					_eye.multiplyScalar( factor );
+				this.dispatchEvent( _changeEvent );
 
-				} else if ( scope.object.isOrthographicCamera ) {
+				this._lastPosition.copy( this.object.position );
 
-					scope.object.zoom = MathUtils.clamp( scope.object.zoom / factor, scope.minZoom, scope.maxZoom );
+			}
 
-					if ( lastZoom !== scope.object.zoom ) {
+		} else if ( this.object.isOrthographicCamera ) {
 
-						scope.object.updateProjectionMatrix();
+			this.object.lookAt( this.target );
 
-					}
+			if ( this._lastPosition.distanceToSquared( this.object.position ) > _EPS || this._lastZoom !== this.object.zoom ) {
 
-				} else {
+				this.dispatchEvent( _changeEvent );
 
-					console.warn( 'THREE.TrackballControls: Unsupported camera type' );
+				this._lastPosition.copy( this.object.position );
+				this._lastZoom = this.object.zoom;
 
-				}
+			}
 
-			} else {
+		} else {
 
-				factor = 1.0 + ( _zoomEnd.y - _zoomStart.y ) * scope.zoomSpeed;
+			console.warn( 'THREE.TrackballControls: Unsupported camera type.' );
 
-				if ( factor !== 1.0 && factor > 0.0 ) {
+		}
 
-					if ( scope.object.isPerspectiveCamera ) {
+	}
 
-						_eye.multiplyScalar( factor );
+	reset() {
 
-					} else if ( scope.object.isOrthographicCamera ) {
+		this.state = _STATE.NONE;
+		this.keyState = _STATE.NONE;
 
-						scope.object.zoom = MathUtils.clamp( scope.object.zoom / factor, scope.minZoom, scope.maxZoom );
+		this.target.copy( this._target0 );
+		this.object.position.copy( this._position0 );
+		this.object.up.copy( this._up0 );
+		this.object.zoom = this._zoom0;
 
-						if ( lastZoom !== scope.object.zoom ) {
+		this.object.updateProjectionMatrix();
 
-							scope.object.updateProjectionMatrix();
+		this._eye.subVectors( this.object.position, this.target );
 
-						}
+		this.object.lookAt( this.target );
 
-					} else {
+		this.dispatchEvent( _changeEvent );
 
-						console.warn( 'THREE.TrackballControls: Unsupported camera type' );
+		this._lastPosition.copy( this.object.position );
+		this._lastZoom = this.object.zoom;
 
-					}
+	}
 
-				}
+	_panCamera() {
 
-				if ( scope.staticMoving ) {
+		_mouseChange.copy( this._panEnd ).sub( this._panStart );
 
-					_zoomStart.copy( _zoomEnd );
+		if ( _mouseChange.lengthSq() ) {
 
-				} else {
+			if ( this.object.isOrthographicCamera ) {
 
-					_zoomStart.y += ( _zoomEnd.y - _zoomStart.y ) * this.dynamicDampingFactor;
+				const scale_x = ( this.object.right - this.object.left ) / this.object.zoom / this.domElement.clientWidth;
+				const scale_y = ( this.object.top - this.object.bottom ) / this.object.zoom / this.domElement.clientWidth;
 
-				}
+				_mouseChange.x *= scale_x;
+				_mouseChange.y *= scale_y;
 
 			}
 
-		};
-
-		this.panCamera = ( function () {
-
-			const mouseChange = new Vector2(),
-				objectUp = new Vector3(),
-				pan = new Vector3();
+			_mouseChange.multiplyScalar( this._eye.length() * this.panSpeed );
 
-			return function panCamera() {
+			_pan.copy( this._eye ).cross( this.object.up ).setLength( _mouseChange.x );
+			_pan.add( _objectUp.copy( this.object.up ).setLength( _mouseChange.y ) );
 
-				mouseChange.copy( _panEnd ).sub( _panStart );
+			this.object.position.add( _pan );
+			this.target.add( _pan );
 
-				if ( mouseChange.lengthSq() ) {
+			if ( this.staticMoving ) {
 
-					if ( scope.object.isOrthographicCamera ) {
+				this._panStart.copy( this._panEnd );
 
-						const scale_x = ( scope.object.right - scope.object.left ) / scope.object.zoom / scope.domElement.clientWidth;
-						const scale_y = ( scope.object.top - scope.object.bottom ) / scope.object.zoom / scope.domElement.clientWidth;
+			} else {
 
-						mouseChange.x *= scale_x;
-						mouseChange.y *= scale_y;
+				this._panStart.add( _mouseChange.subVectors( this._panEnd, this._panStart ).multiplyScalar( this.dynamicDampingFactor ) );
 
-					}
+			}
 
-					mouseChange.multiplyScalar( _eye.length() * scope.panSpeed );
+		}
 
-					pan.copy( _eye ).cross( scope.object.up ).setLength( mouseChange.x );
-					pan.add( objectUp.copy( scope.object.up ).setLength( mouseChange.y ) );
+	}
 
-					scope.object.position.add( pan );
-					scope.target.add( pan );
+	_rotateCamera() {
 
-					if ( scope.staticMoving ) {
+		_moveDirection.set( this._moveCurr.x - this._movePrev.x, this._moveCurr.y - this._movePrev.y, 0 );
+		let angle = _moveDirection.length();
 
-						_panStart.copy( _panEnd );
+		if ( angle ) {
 
-					} else {
+			this._eye.copy( this.object.position ).sub( this.target );
 
-						_panStart.add( mouseChange.subVectors( _panEnd, _panStart ).multiplyScalar( scope.dynamicDampingFactor ) );
+			_eyeDirection.copy( this._eye ).normalize();
+			_objectUpDirection.copy( this.object.up ).normalize();
+			_objectSidewaysDirection.crossVectors( _objectUpDirection, _eyeDirection ).normalize();
 
-					}
+			_objectUpDirection.setLength( this._moveCurr.y - this._movePrev.y );
+			_objectSidewaysDirection.setLength( this._moveCurr.x - this._movePrev.x );
 
-				}
+			_moveDirection.copy( _objectUpDirection.add( _objectSidewaysDirection ) );
 
-			};
+			_axis.crossVectors( _moveDirection, this._eye ).normalize();
 
-		}() );
+			angle *= this.rotateSpeed;
+			_quaternion.setFromAxisAngle( _axis, angle );
 
-		this.checkDistances = function () {
+			this._eye.applyQuaternion( _quaternion );
+			this.object.up.applyQuaternion( _quaternion );
 
-			if ( ! scope.noZoom || ! scope.noPan ) {
+			this._lastAxis.copy( _axis );
+			this._lastAngle = angle;
 
-				if ( _eye.lengthSq() > scope.maxDistance * scope.maxDistance ) {
+		} else if ( ! this.staticMoving && this._lastAngle ) {
 
-					scope.object.position.addVectors( scope.target, _eye.setLength( scope.maxDistance ) );
-					_zoomStart.copy( _zoomEnd );
+			this._lastAngle *= Math.sqrt( 1.0 - this.dynamicDampingFactor );
+			this._eye.copy( this.object.position ).sub( this.target );
+			_quaternion.setFromAxisAngle( this._lastAxis, this._lastAngle );
+			this._eye.applyQuaternion( _quaternion );
+			this.object.up.applyQuaternion( _quaternion );
 
-				}
+		}
 
-				if ( _eye.lengthSq() < scope.minDistance * scope.minDistance ) {
+		this._movePrev.copy( this._moveCurr );
 
-					scope.object.position.addVectors( scope.target, _eye.setLength( scope.minDistance ) );
-					_zoomStart.copy( _zoomEnd );
+	}
 
-				}
+	_zoomCamera() {
 
-			}
+		let factor;
 
-		};
+		if ( this.state === _STATE.TOUCH_ZOOM_PAN ) {
 
-		this.update = function () {
+			factor = this._touchZoomDistanceStart / this._touchZoomDistanceEnd;
+			this._touchZoomDistanceStart = this._touchZoomDistanceEnd;
 
-			_eye.subVectors( scope.object.position, scope.target );
+			if ( this.object.isPerspectiveCamera ) {
 
-			if ( ! scope.noRotate ) {
+				this._eye.multiplyScalar( factor );
 
-				scope.rotateCamera();
+			} else if ( this.object.isOrthographicCamera ) {
 
-			}
+				this.object.zoom = MathUtils.clamp( this.object.zoom / factor, this.minZoom, this.maxZoom );
 
-			if ( ! scope.noZoom ) {
+				if ( this._lastZoom !== this.object.zoom ) {
 
-				scope.zoomCamera();
+					this.object.updateProjectionMatrix();
 
-			}
+				}
 
-			if ( ! scope.noPan ) {
+			} else {
 
-				scope.panCamera();
+				console.warn( 'THREE.TrackballControls: Unsupported camera type' );
 
 			}
 
-			scope.object.position.addVectors( scope.target, _eye );
+		} else {
 
-			if ( scope.object.isPerspectiveCamera ) {
+			factor = 1.0 + ( this._zoomEnd.y - this._zoomStart.y ) * this.zoomSpeed;
 
-				scope.checkDistances();
+			if ( factor !== 1.0 && factor > 0.0 ) {
 
-				scope.object.lookAt( scope.target );
+				if ( this.object.isPerspectiveCamera ) {
 
-				if ( lastPosition.distanceToSquared( scope.object.position ) > EPS ) {
+					this._eye.multiplyScalar( factor );
 
-					scope.dispatchEvent( _changeEvent );
+				} else if ( this.object.isOrthographicCamera ) {
 
-					lastPosition.copy( scope.object.position );
+					this.object.zoom = MathUtils.clamp( this.object.zoom / factor, this.minZoom, this.maxZoom );
 
-				}
-
-			} else if ( scope.object.isOrthographicCamera ) {
+					if ( this._lastZoom !== this.object.zoom ) {
 
-				scope.object.lookAt( scope.target );
+						this.object.updateProjectionMatrix();
 
-				if ( lastPosition.distanceToSquared( scope.object.position ) > EPS || lastZoom !== scope.object.zoom ) {
+					}
 
-					scope.dispatchEvent( _changeEvent );
+				} else {
 
-					lastPosition.copy( scope.object.position );
-					lastZoom = scope.object.zoom;
+					console.warn( 'THREE.TrackballControls: Unsupported camera type' );
 
 				}
 
-			} else {
-
-				console.warn( 'THREE.TrackballControls: Unsupported camera type' );
-
 			}
 
-		};
+			if ( this.staticMoving ) {
 
-		this.reset = function () {
+				this._zoomStart.copy( this._zoomEnd );
 
-			_state = STATE.NONE;
-			_keyState = STATE.NONE;
+			} else {
 
-			scope.target.copy( scope.target0 );
-			scope.object.position.copy( scope.position0 );
-			scope.object.up.copy( scope.up0 );
-			scope.object.zoom = scope.zoom0;
+				this._zoomStart.y += ( this._zoomEnd.y - this._zoomStart.y ) * this.dynamicDampingFactor;
 
-			scope.object.updateProjectionMatrix();
+			}
 
-			_eye.subVectors( scope.object.position, scope.target );
+		}
 
-			scope.object.lookAt( scope.target );
+	}
 
-			scope.dispatchEvent( _changeEvent );
+	_getMouseOnScreen( pageX, pageY ) {
 
-			lastPosition.copy( scope.object.position );
-			lastZoom = scope.object.zoom;
+		_v2.set(
+			( pageX - this.screen.left ) / this.screen.width,
+			( pageY - this.screen.top ) / this.screen.height
+		);
 
-		};
+		return _v2;
 
-		// listeners
+	}
 
-		function onPointerDown( event ) {
+	_getMouseOnCircle( pageX, pageY ) {
 
-			if ( scope.enabled === false ) return;
+		_v2.set(
+			( ( pageX - this.screen.width * 0.5 - this.screen.left ) / ( this.screen.width * 0.5 ) ),
+			( ( this.screen.height + 2 * ( this.screen.top - pageY ) ) / this.screen.width ) // screen.width intentional
+		);
 
-			if ( _pointers.length === 0 ) {
+		return _v2;
 
-				scope.domElement.setPointerCapture( event.pointerId );
+	}
 
-				scope.domElement.addEventListener( 'pointermove', onPointerMove );
-				scope.domElement.addEventListener( 'pointerup', onPointerUp );
+	_addPointer( event ) {
 
-			}
+		this._pointers.push( event );
 
-			//
+	}
 
-			addPointer( event );
+	_removePointer( event ) {
 
-			if ( event.pointerType === 'touch' ) {
+		delete this._pointerPositions[ event.pointerId ];
 
-				onTouchStart( event );
+		for ( let i = 0; i < this._pointers.length; i ++ ) {
 
-			} else {
+			if ( this._pointers[ i ].pointerId == event.pointerId ) {
 
-				onMouseDown( event );
+				this._pointers.splice( i, 1 );
+				return;
 
 			}
 
 		}
 
-		function onPointerMove( event ) {
-
-			if ( scope.enabled === false ) return;
+	}
 
-			if ( event.pointerType === 'touch' ) {
+	_trackPointer( event ) {
 
-				onTouchMove( event );
+		let position = this._pointerPositions[ event.pointerId ];
 
-			} else {
+		if ( position === undefined ) {
 
-				onMouseMove( event );
-
-			}
+			position = new Vector2();
+			this._pointerPositions[ event.pointerId ] = position;
 
 		}
 
-		function onPointerUp( event ) {
+		position.set( event.pageX, event.pageY );
+
+	}
 
-			if ( scope.enabled === false ) return;
+	_getSecondPointerPosition( event ) {
 
-			if ( event.pointerType === 'touch' ) {
+		const pointer = ( event.pointerId === this._pointers[ 0 ].pointerId ) ? this._pointers[ 1 ] : this._pointers[ 0 ];
 
-				onTouchEnd( event );
+		return this._pointerPositions[ pointer.pointerId ];
 
-			} else {
+	}
 
-				onMouseUp();
+	_checkDistances() {
 
-			}
+		if ( ! this.noZoom || ! this.noPan ) {
 
-			//
+			if ( this._eye.lengthSq() > this.maxDistance * this.maxDistance ) {
 
-			removePointer( event );
+				this.object.position.addVectors( this.target, this._eye.setLength( this.maxDistance ) );
+				this._zoomStart.copy( this._zoomEnd );
 
-			if ( _pointers.length === 0 ) {
+			}
 
-				scope.domElement.releasePointerCapture( event.pointerId );
+			if ( this._eye.lengthSq() < this.minDistance * this.minDistance ) {
 
-				scope.domElement.removeEventListener( 'pointermove', onPointerMove );
-				scope.domElement.removeEventListener( 'pointerup', onPointerUp );
+				this.object.position.addVectors( this.target, this._eye.setLength( this.minDistance ) );
+				this._zoomStart.copy( this._zoomEnd );
 
 			}
 
-
 		}
 
-		function onPointerCancel( event ) {
+	}
 
-			removePointer( event );
+}
 
-		}
+function onPointerDown( event ) {
 
-		function keydown( event ) {
+	if ( this.enabled === false ) return;
 
-			if ( scope.enabled === false ) return;
+	if ( this._pointers.length === 0 ) {
 
-			window.removeEventListener( 'keydown', keydown );
+		this.domElement.setPointerCapture( event.pointerId );
 
-			if ( _keyState !== STATE.NONE ) {
+		this.domElement.addEventListener( 'pointermove', this._onPointerMove );
+		this.domElement.addEventListener( 'pointerup', this._onPointerUp );
 
-				return;
+	}
 
-			} else if ( event.code === scope.keys[ STATE.ROTATE ] && ! scope.noRotate ) {
+	//
 
-				_keyState = STATE.ROTATE;
+	this._addPointer( event );
 
-			} else if ( event.code === scope.keys[ STATE.ZOOM ] && ! scope.noZoom ) {
+	if ( event.pointerType === 'touch' ) {
 
-				_keyState = STATE.ZOOM;
+		this._onTouchStart( event );
 
-			} else if ( event.code === scope.keys[ STATE.PAN ] && ! scope.noPan ) {
+	} else {
 
-				_keyState = STATE.PAN;
+		this._onMouseDown( event );
 
-			}
+	}
 
-		}
+}
 
-		function keyup() {
+function onPointerMove( event ) {
 
-			if ( scope.enabled === false ) return;
+	if ( this.enabled === false ) return;
 
-			_keyState = STATE.NONE;
+	if ( event.pointerType === 'touch' ) {
 
-			window.addEventListener( 'keydown', keydown );
+		this._onTouchMove( event );
 
-		}
+	} else {
 
-		function onMouseDown( event ) {
+		this._onMouseMove( event );
 
-			if ( _state === STATE.NONE ) {
+	}
 
-				switch ( event.button ) {
+}
 
-					case scope.mouseButtons.LEFT:
-						_state = STATE.ROTATE;
-						break;
+function onPointerUp( event ) {
 
-					case scope.mouseButtons.MIDDLE:
-						_state = STATE.ZOOM;
-						break;
+	if ( this.enabled === false ) return;
 
-					case scope.mouseButtons.RIGHT:
-						_state = STATE.PAN;
-						break;
+	if ( event.pointerType === 'touch' ) {
 
-				}
+		this._onTouchEnd( event );
 
-			}
+	} else {
 
-			const state = ( _keyState !== STATE.NONE ) ? _keyState : _state;
+		this._onMouseUp();
 
-			if ( state === STATE.ROTATE && ! scope.noRotate ) {
+	}
 
-				_moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) );
-				_movePrev.copy( _moveCurr );
+	//
 
-			} else if ( state === STATE.ZOOM && ! scope.noZoom ) {
+	this._removePointer( event );
 
-				_zoomStart.copy( getMouseOnScreen( event.pageX, event.pageY ) );
-				_zoomEnd.copy( _zoomStart );
+	if ( this._pointers.length === 0 ) {
 
-			} else if ( state === STATE.PAN && ! scope.noPan ) {
+		this.domElement.releasePointerCapture( event.pointerId );
 
-				_panStart.copy( getMouseOnScreen( event.pageX, event.pageY ) );
-				_panEnd.copy( _panStart );
+		this.domElement.removeEventListener( 'pointermove', this._onPointerMove );
+		this.domElement.removeEventListener( 'pointerup', this._onPointerUp );
 
-			}
+	}
 
-			scope.dispatchEvent( _startEvent );
+}
 
-		}
+function onPointerCancel( event ) {
 
-		function onMouseMove( event ) {
+	this._removePointer( event );
 
-			const state = ( _keyState !== STATE.NONE ) ? _keyState : _state;
+}
 
-			if ( state === STATE.ROTATE && ! scope.noRotate ) {
+function onKeyUp() {
 
-				_movePrev.copy( _moveCurr );
-				_moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) );
+	if ( this.enabled === false ) return;
 
-			} else if ( state === STATE.ZOOM && ! scope.noZoom ) {
+	this.keyState = _STATE.NONE;
 
-				_zoomEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) );
+	window.addEventListener( 'keydown', this._onKeyDown );
 
-			} else if ( state === STATE.PAN && ! scope.noPan ) {
+}
 
-				_panEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) );
+function onKeyDown( event ) {
 
-			}
+	if ( this.enabled === false ) return;
 
-		}
+	window.removeEventListener( 'keydown', this._onKeyDown );
 
-		function onMouseUp() {
+	if ( this.keyState !== _STATE.NONE ) {
 
-			_state = STATE.NONE;
+		return;
 
-			scope.dispatchEvent( _endEvent );
+	} else if ( event.code === this.keys[ _STATE.ROTATE ] && ! this.noRotate ) {
 
-		}
+		this.keyState = _STATE.ROTATE;
 
-		function onMouseWheel( event ) {
+	} else if ( event.code === this.keys[ _STATE.ZOOM ] && ! this.noZoom ) {
 
-			if ( scope.enabled === false ) return;
+		this.keyState = _STATE.ZOOM;
 
-			if ( scope.noZoom === true ) return;
+	} else if ( event.code === this.keys[ _STATE.PAN ] && ! this.noPan ) {
 
-			event.preventDefault();
+		this.keyState = _STATE.PAN;
 
-			switch ( event.deltaMode ) {
+	}
 
-				case 2:
-					// Zoom in pages
-					_zoomStart.y -= event.deltaY * 0.025;
-					break;
+}
 
-				case 1:
-					// Zoom in lines
-					_zoomStart.y -= event.deltaY * 0.01;
-					break;
+function onMouseDown( event ) {
 
-				default:
-					// undefined, 0, assume pixels
-					_zoomStart.y -= event.deltaY * 0.00025;
-					break;
+	if ( this.state === _STATE.NONE ) {
 
-			}
+		switch ( event.button ) {
 
-			scope.dispatchEvent( _startEvent );
-			scope.dispatchEvent( _endEvent );
+			case this.mouseButtons.LEFT:
+				this.state = _STATE.ROTATE;
+				break;
 
-		}
+			case this.mouseButtons.MIDDLE:
+				this.state = _STATE.ZOOM;
+				break;
 
-		function onTouchStart( event ) {
+			case this.mouseButtons.RIGHT:
+				this.state = _STATE.PAN;
+				break;
 
-			trackPointer( event );
+		}
 
-			switch ( _pointers.length ) {
+	}
 
-				case 1:
-					_state = STATE.TOUCH_ROTATE;
-					_moveCurr.copy( getMouseOnCircle( _pointers[ 0 ].pageX, _pointers[ 0 ].pageY ) );
-					_movePrev.copy( _moveCurr );
-					break;
+	const state = ( this.keyState !== _STATE.NONE ) ? this.keyState : this.state;
 
-				default: // 2 or more
-					_state = STATE.TOUCH_ZOOM_PAN;
-					const dx = _pointers[ 0 ].pageX - _pointers[ 1 ].pageX;
-					const dy = _pointers[ 0 ].pageY - _pointers[ 1 ].pageY;
-					_touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt( dx * dx + dy * dy );
+	if ( state === _STATE.ROTATE && ! this.noRotate ) {
 
-					const x = ( _pointers[ 0 ].pageX + _pointers[ 1 ].pageX ) / 2;
-					const y = ( _pointers[ 0 ].pageY + _pointers[ 1 ].pageY ) / 2;
-					_panStart.copy( getMouseOnScreen( x, y ) );
-					_panEnd.copy( _panStart );
-					break;
+		this._moveCurr.copy( this._getMouseOnCircle( event.pageX, event.pageY ) );
+		this._movePrev.copy( this._moveCurr );
 
-			}
+	} else if ( state === _STATE.ZOOM && ! this.noZoom ) {
 
-			scope.dispatchEvent( _startEvent );
+		this._zoomStart.copy( this._getMouseOnScreen( event.pageX, event.pageY ) );
+		this._zoomEnd.copy( this._zoomStart );
 
-		}
+	} else if ( state === _STATE.PAN && ! this.noPan ) {
 
-		function onTouchMove( event ) {
+		this._panStart.copy( this._getMouseOnScreen( event.pageX, event.pageY ) );
+		this._panEnd.copy( this._panStart );
 
-			trackPointer( event );
+	}
 
-			switch ( _pointers.length ) {
+	this.dispatchEvent( _startEvent );
 
-				case 1:
-					_movePrev.copy( _moveCurr );
-					_moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) );
-					break;
+}
 
-				default: // 2 or more
+function onMouseMove( event ) {
 
-					const position = getSecondPointerPosition( event );
+	const state = ( this.keyState !== _STATE.NONE ) ? this.keyState : this.state;
 
-					const dx = event.pageX - position.x;
-					const dy = event.pageY - position.y;
-					_touchZoomDistanceEnd = Math.sqrt( dx * dx + dy * dy );
+	if ( state === _STATE.ROTATE && ! this.noRotate ) {
 
-					const x = ( event.pageX + position.x ) / 2;
-					const y = ( event.pageY + position.y ) / 2;
-					_panEnd.copy( getMouseOnScreen( x, y ) );
-					break;
+		this._movePrev.copy( this._moveCurr );
+		this._moveCurr.copy( this._getMouseOnCircle( event.pageX, event.pageY ) );
 
-			}
+	} else if ( state === _STATE.ZOOM && ! this.noZoom ) {
 
-		}
+		this._zoomEnd.copy( this._getMouseOnScreen( event.pageX, event.pageY ) );
 
-		function onTouchEnd( event ) {
+	} else if ( state === _STATE.PAN && ! this.noPan ) {
 
-			switch ( _pointers.length ) {
+		this._panEnd.copy( this._getMouseOnScreen( event.pageX, event.pageY ) );
 
-				case 0:
-					_state = STATE.NONE;
-					break;
+	}
 
-				case 1:
-					_state = STATE.TOUCH_ROTATE;
-					_moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) );
-					_movePrev.copy( _moveCurr );
-					break;
+}
 
-				case 2:
-					_state = STATE.TOUCH_ZOOM_PAN;
+function onMouseUp() {
 
-					for ( let i = 0; i < _pointers.length; i ++ ) {
+	this.state = _STATE.NONE;
 
-						if ( _pointers[ i ].pointerId !== event.pointerId ) {
+	this.dispatchEvent( _endEvent );
 
-							const position = _pointerPositions[ _pointers[ i ].pointerId ];
-							_moveCurr.copy( getMouseOnCircle( position.x, position.y ) );
-							_movePrev.copy( _moveCurr );
-							break;
+}
 
-						}
+function onMouseWheel( event ) {
 
-					}
+	if ( this.enabled === false ) return;
 
-					break;
+	if ( this.noZoom === true ) return;
 
-			}
+	event.preventDefault();
 
-			scope.dispatchEvent( _endEvent );
+	switch ( event.deltaMode ) {
 
-		}
+		case 2:
+			// Zoom in pages
+			this._zoomStart.y -= event.deltaY * 0.025;
+			break;
 
-		function contextmenu( event ) {
+		case 1:
+			// Zoom in lines
+			this._zoomStart.y -= event.deltaY * 0.01;
+			break;
 
-			if ( scope.enabled === false ) return;
+		default:
+			// undefined, 0, assume pixels
+			this._zoomStart.y -= event.deltaY * 0.00025;
+			break;
 
-			event.preventDefault();
+	}
 
-		}
+	this.dispatchEvent( _startEvent );
+	this.dispatchEvent( _endEvent );
 
-		function addPointer( event ) {
+}
 
-			_pointers.push( event );
+function onContextMenu( event ) {
 
-		}
+	if ( this.enabled === false ) return;
 
-		function removePointer( event ) {
+	event.preventDefault();
 
-			delete _pointerPositions[ event.pointerId ];
+}
 
-			for ( let i = 0; i < _pointers.length; i ++ ) {
+function onTouchStart( event ) {
 
-				if ( _pointers[ i ].pointerId == event.pointerId ) {
+	this._trackPointer( event );
 
-					_pointers.splice( i, 1 );
-					return;
+	switch ( this._pointers.length ) {
 
-				}
+		case 1:
+			this.state = _STATE.TOUCH_ROTATE;
+			this._moveCurr.copy( this._getMouseOnCircle( this._pointers[ 0 ].pageX, this._pointers[ 0 ].pageY ) );
+			this._movePrev.copy( this._moveCurr );
+			break;
 
-			}
+		default: // 2 or more
+			this.state = _STATE.TOUCH_ZOOM_PAN;
+			const dx = this._pointers[ 0 ].pageX - this._pointers[ 1 ].pageX;
+			const dy = this._pointers[ 0 ].pageY - this._pointers[ 1 ].pageY;
+			this._touchZoomDistanceEnd = this._touchZoomDistanceStart = Math.sqrt( dx * dx + dy * dy );
 
-		}
+			const x = ( this._pointers[ 0 ].pageX + this._pointers[ 1 ].pageX ) / 2;
+			const y = ( this._pointers[ 0 ].pageY + this._pointers[ 1 ].pageY ) / 2;
+			this._panStart.copy( this._getMouseOnScreen( x, y ) );
+			this._panEnd.copy( this._panStart );
+			break;
 
-		function trackPointer( event ) {
+	}
 
-			let position = _pointerPositions[ event.pointerId ];
+	this.dispatchEvent( _startEvent );
 
-			if ( position === undefined ) {
+}
 
-				position = new Vector2();
-				_pointerPositions[ event.pointerId ] = position;
+function onTouchMove( event ) {
 
-			}
+	this._trackPointer( event );
 
-			position.set( event.pageX, event.pageY );
+	switch ( this._pointers.length ) {
 
-		}
+		case 1:
+			this._movePrev.copy( this._moveCurr );
+			this._moveCurr.copy( this._getMouseOnCircle( event.pageX, event.pageY ) );
+			break;
 
-		function getSecondPointerPosition( event ) {
+		default: // 2 or more
 
-			const pointer = ( event.pointerId === _pointers[ 0 ].pointerId ) ? _pointers[ 1 ] : _pointers[ 0 ];
+			const position = this._getSecondPointerPosition( event );
 
-			return _pointerPositions[ pointer.pointerId ];
+			const dx = event.pageX - position.x;
+			const dy = event.pageY - position.y;
+			this._touchZoomDistanceEnd = Math.sqrt( dx * dx + dy * dy );
 
-		}
+			const x = ( event.pageX + position.x ) / 2;
+			const y = ( event.pageY + position.y ) / 2;
+			this._panEnd.copy( this._getMouseOnScreen( x, y ) );
+			break;
 
-		this.dispose = function () {
+	}
 
-			scope.domElement.removeEventListener( 'contextmenu', contextmenu );
+}
 
-			scope.domElement.removeEventListener( 'pointerdown', onPointerDown );
-			scope.domElement.removeEventListener( 'pointercancel', onPointerCancel );
-			scope.domElement.removeEventListener( 'wheel', onMouseWheel );
+function onTouchEnd( event ) {
 
-			scope.domElement.removeEventListener( 'pointermove', onPointerMove );
-			scope.domElement.removeEventListener( 'pointerup', onPointerUp );
+	switch ( this._pointers.length ) {
 
-			window.removeEventListener( 'keydown', keydown );
-			window.removeEventListener( 'keyup', keyup );
+		case 0:
+			this.state = _STATE.NONE;
+			break;
 
-		};
+		case 1:
+			this.state = _STATE.TOUCH_ROTATE;
+			this._moveCurr.copy( this._getMouseOnCircle( event.pageX, event.pageY ) );
+			this._movePrev.copy( this._moveCurr );
+			break;
 
-		this.domElement.addEventListener( 'contextmenu', contextmenu );
+		case 2:
+			this.state = _STATE.TOUCH_ZOOM_PAN;
 
-		this.domElement.addEventListener( 'pointerdown', onPointerDown );
-		this.domElement.addEventListener( 'pointercancel', onPointerCancel );
-		this.domElement.addEventListener( 'wheel', onMouseWheel, { passive: false } );
+			for ( let i = 0; i < this._pointers.length; i ++ ) {
 
+				if ( this._pointers[ i ].pointerId !== event.pointerId ) {
+
+					const position = this._pointerPositions[ this._pointers[ i ].pointerId ];
+					this._moveCurr.copy( this._getMouseOnCircle( position.x, position.y ) );
+					this._movePrev.copy( this._moveCurr );
+					break;
 
-		window.addEventListener( 'keydown', keydown );
-		window.addEventListener( 'keyup', keyup );
+				}
 
-		this.handleResize();
+			}
 
-		// force an update at start
-		this.update();
+			break;
 
 	}
 
+	this.dispatchEvent( _endEvent );
+
 }
 
 export { TrackballControls };

+ 6 - 13
examples/jsm/effects/AnaglyphEffect.js

@@ -1,16 +1,13 @@
 import {
 	LinearFilter,
 	Matrix3,
-	Mesh,
 	NearestFilter,
-	OrthographicCamera,
-	PlaneGeometry,
 	RGBAFormat,
-	Scene,
 	ShaderMaterial,
 	StereoCamera,
 	WebGLRenderTarget
 } from 'three';
+import { FullScreenQuad } from '../postprocessing/Pass.js';
 
 class AnaglyphEffect {
 
@@ -30,10 +27,6 @@ class AnaglyphEffect {
 			- 0.00155529, - 0.0184503, 1.2264
 		] );
 
-		const _camera = new OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
-
-		const _scene = new Scene();
-
 		const _stereo = new StereoCamera();
 
 		const _params = { minFilter: LinearFilter, magFilter: NearestFilter, format: RGBAFormat };
@@ -99,8 +92,7 @@ class AnaglyphEffect {
 
 		} );
 
-		const _mesh = new Mesh( new PlaneGeometry( 2, 2 ), _material );
-		_scene.add( _mesh );
+		const _quad = new FullScreenQuad( _material );
 
 		this.setSize = function ( width, height ) {
 
@@ -132,7 +124,7 @@ class AnaglyphEffect {
 			renderer.render( scene, _stereo.cameraR );
 
 			renderer.setRenderTarget( null );
-			renderer.render( _scene, _camera );
+			_quad.render( renderer );
 
 			renderer.setRenderTarget( currentRenderTarget );
 
@@ -142,8 +134,9 @@ class AnaglyphEffect {
 
 			_renderTargetL.dispose();
 			_renderTargetR.dispose();
-			_mesh.geometry.dispose();
-			_mesh.material.dispose();
+
+			_material.dispose();
+			_quad.dispose();
 
 		};
 

+ 17 - 11
examples/jsm/effects/ParallaxBarrierEffect.js

@@ -1,24 +1,17 @@
 import {
 	LinearFilter,
-	Mesh,
 	NearestFilter,
-	OrthographicCamera,
-	PlaneGeometry,
 	RGBAFormat,
-	Scene,
 	ShaderMaterial,
 	StereoCamera,
 	WebGLRenderTarget
 } from 'three';
+import { FullScreenQuad } from '../postprocessing/Pass.js';
 
 class ParallaxBarrierEffect {
 
 	constructor( renderer ) {
 
-		const _camera = new OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
-
-		const _scene = new Scene();
-
 		const _stereo = new StereoCamera();
 
 		const _params = { minFilter: LinearFilter, magFilter: NearestFilter, format: RGBAFormat };
@@ -77,8 +70,7 @@ class ParallaxBarrierEffect {
 
 		} );
 
-		const mesh = new Mesh( new PlaneGeometry( 2, 2 ), _material );
-		_scene.add( mesh );
+		const _quad = new FullScreenQuad( _material );
 
 		this.setSize = function ( width, height ) {
 
@@ -93,6 +85,8 @@ class ParallaxBarrierEffect {
 
 		this.render = function ( scene, camera ) {
 
+			const currentRenderTarget = renderer.getRenderTarget();
+
 			if ( scene.matrixWorldAutoUpdate === true ) scene.updateMatrixWorld();
 
 			if ( camera.parent === null && camera.matrixWorldAutoUpdate === true ) camera.updateMatrixWorld();
@@ -108,7 +102,19 @@ class ParallaxBarrierEffect {
 			renderer.render( scene, _stereo.cameraR );
 
 			renderer.setRenderTarget( null );
-			renderer.render( _scene, _camera );
+			_quad.render( renderer );
+
+			renderer.setRenderTarget( currentRenderTarget );
+
+		};
+
+		this.dispose = function () {
+
+			_renderTargetL.dispose();
+			_renderTargetR.dispose();
+
+			_material.dispose();
+			_quad.dispose();
 
 		};
 

+ 6 - 1
examples/jsm/effects/StereoEffect.js

@@ -31,9 +31,12 @@ class StereoEffect {
 
 			_stereo.update( camera );
 
+			const currentAutoClear = renderer.autoClear;
 			renderer.getSize( size );
 
-			if ( renderer.autoClear ) renderer.clear();
+			renderer.autoClear = false;
+			renderer.clear();
+
 			renderer.setScissorTest( true );
 
 			renderer.setScissor( 0, 0, size.width / 2, size.height );
@@ -46,6 +49,8 @@ class StereoEffect {
 
 			renderer.setScissorTest( false );
 
+			renderer.autoClear = currentAutoClear;
+
 		};
 
 	}

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

@@ -14,7 +14,7 @@ import {
 
 class RoomEnvironment extends Scene {
 
-	constructor( renderer = null ) {
+	constructor() {
 
 		super();
 

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

@@ -2837,7 +2837,12 @@ class GLTFMaterialsVolumeExtension {
 
 		}
 
-		extensionDef.attenuationDistance = material.attenuationDistance;
+		if ( material.attenuationDistance !== Infinity ) {
+
+			extensionDef.attenuationDistance = material.attenuationDistance;
+
+		}
+
 		extensionDef.attenuationColor = material.attenuationColor.toArray();
 
 		materialDef.extensions = materialDef.extensions || {};

+ 29 - 8
examples/jsm/exporters/USDZExporter.js

@@ -1,6 +1,7 @@
 import {
 	NoColorSpace,
 	DoubleSide,
+	Color,
 } from 'three';
 
 import {
@@ -584,7 +585,7 @@ function buildMaterial( material, textures, quickLookCompatible = false ) {
 
 		inputs.push( `${ pad }color3f inputs:emissiveColor.connect = </Materials/Material_${ material.id }/Texture_${ material.emissiveMap.id }_emissive.outputs:rgb>` );
 
-		samplers.push( buildTexture( material.emissiveMap, 'emissive' ) );
+		samplers.push( buildTexture( material.emissiveMap, 'emissive', new Color( material.emissive.r * material.emissiveIntensity, material.emissive.g * material.emissiveIntensity, material.emissive.b * material.emissiveIntensity ) ) );
 
 	} else if ( material.emissive.getHex() > 0 ) {
 
@@ -604,15 +605,15 @@ function buildMaterial( material, textures, quickLookCompatible = false ) {
 
 		inputs.push( `${ pad }float inputs:occlusion.connect = </Materials/Material_${ material.id }/Texture_${ material.aoMap.id }_occlusion.outputs:r>` );
 
-		samplers.push( buildTexture( material.aoMap, 'occlusion' ) );
+		samplers.push( buildTexture( material.aoMap, 'occlusion', new Color( material.aoMapIntensity, material.aoMapIntensity, material.aoMapIntensity ) ) );
 
 	}
 
-	if ( material.roughnessMap !== null && material.roughness === 1 ) {
+	if ( material.roughnessMap !== null ) {
 
 		inputs.push( `${ pad }float inputs:roughness.connect = </Materials/Material_${ material.id }/Texture_${ material.roughnessMap.id }_roughness.outputs:g>` );
 
-		samplers.push( buildTexture( material.roughnessMap, 'roughness' ) );
+		samplers.push( buildTexture( material.roughnessMap, 'roughness', new Color( material.roughness, material.roughness, material.roughness ) ) );
 
 	} else {
 
@@ -620,11 +621,11 @@ function buildMaterial( material, textures, quickLookCompatible = false ) {
 
 	}
 
-	if ( material.metalnessMap !== null && material.metalness === 1 ) {
+	if ( material.metalnessMap !== null ) {
 
 		inputs.push( `${ pad }float inputs:metallic.connect = </Materials/Material_${ material.id }/Texture_${ material.metalnessMap.id }_metallic.outputs:b>` );
 
-		samplers.push( buildTexture( material.metalnessMap, 'metallic' ) );
+		samplers.push( buildTexture( material.metalnessMap, 'metallic', new Color( material.metalness, material.metalness, material.metalness ) ) );
 
 	} else {
 
@@ -647,8 +648,28 @@ function buildMaterial( material, textures, quickLookCompatible = false ) {
 
 	if ( material.isMeshPhysicalMaterial ) {
 
-		inputs.push( `${ pad }float inputs:clearcoat = ${ material.clearcoat }` );
-		inputs.push( `${ pad }float inputs:clearcoatRoughness = ${ material.clearcoatRoughness }` );
+		if ( material.clearcoatMap !== null ) {
+
+			inputs.push( `${pad}float inputs:clearcoat.connect = </Materials/Material_${material.id}/Texture_${material.clearcoatMap.id}_clearcoat.outputs:r>` );
+			samplers.push( buildTexture( material.clearcoatMap, 'clearcoat', new Color( material.clearcoat, material.clearcoat, material.clearcoat ) ) );
+
+		} else {
+
+			inputs.push( `${pad}float inputs:clearcoat = ${material.clearcoat}` );
+
+		}
+
+		if ( material.clearcoatRoughnessMap !== null ) {
+
+			inputs.push( `${pad}float inputs:clearcoatRoughness.connect = </Materials/Material_${material.id}/Texture_${material.clearcoatRoughnessMap.id}_clearcoatRoughness.outputs:g>` );
+			samplers.push( buildTexture( material.clearcoatRoughnessMap, 'clearcoatRoughness', new Color( material.clearcoatRoughness, material.clearcoatRoughness, material.clearcoatRoughness ) ) );
+
+		} else {
+
+			inputs.push( `${pad}float inputs:clearcoatRoughness = ${material.clearcoatRoughness}` );
+
+		}
+
 		inputs.push( `${ pad }float inputs:ior = ${ material.ior }` );
 
 	}

+ 42 - 43
examples/jsm/helpers/LightProbeHelper.js

@@ -20,79 +20,78 @@ class LightProbeHelper extends Mesh {
 
 			},
 
-			vertexShader: [
+			vertexShader: /* glsl */`
 
-				'varying vec3 vNormal;',
+				varying vec3 vNormal;
 
-				'void main() {',
+				void main() {
 
-				'	vNormal = normalize( normalMatrix * normal );',
+					vNormal = normalize( normalMatrix * normal );
 
-				'	gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );',
+					gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
 
-				'}',
+				}
 
-			].join( '\n' ),
+			`,
 
-			fragmentShader: [
+			fragmentShader: /* glsl */`
 
-				'#define RECIPROCAL_PI 0.318309886',
+				#define RECIPROCAL_PI 0.318309886
 
-				'vec3 inverseTransformDirection( in vec3 normal, in mat4 matrix ) {',
+				vec3 inverseTransformDirection( in vec3 normal, in mat4 matrix ) {
 
-				'	// matrix is assumed to be orthogonal',
+					// matrix is assumed to be orthogonal
 
-				'	return normalize( ( vec4( normal, 0.0 ) * matrix ).xyz );',
+					return normalize( ( vec4( normal, 0.0 ) * matrix ).xyz );
 
-				'}',
+				}
 
-				'// source: https://graphics.stanford.edu/papers/envmap/envmap.pdf',
-				'vec3 shGetIrradianceAt( in vec3 normal, in vec3 shCoefficients[ 9 ] ) {',
+				// source: https://graphics.stanford.edu/papers/envmap/envmap.pdf,
+				vec3 shGetIrradianceAt( in vec3 normal, in vec3 shCoefficients[ 9 ] ) {
 
-				'	// normal is assumed to have unit length',
+					// normal is assumed to have unit length,
 
-				'	float x = normal.x, y = normal.y, z = normal.z;',
+					float x = normal.x, y = normal.y, z = normal.z;
 
-				'	// band 0',
-				'	vec3 result = shCoefficients[ 0 ] * 0.886227;',
+					// band 0,
+					vec3 result = shCoefficients[ 0 ] * 0.886227;
 
-				'	// band 1',
-				'	result += shCoefficients[ 1 ] * 2.0 * 0.511664 * y;',
-				'	result += shCoefficients[ 2 ] * 2.0 * 0.511664 * z;',
-				'	result += shCoefficients[ 3 ] * 2.0 * 0.511664 * x;',
+					// band 1,
+					result += shCoefficients[ 1 ] * 2.0 * 0.511664 * y;
+					result += shCoefficients[ 2 ] * 2.0 * 0.511664 * z;
+					result += shCoefficients[ 3 ] * 2.0 * 0.511664 * x;
 
-				'	// band 2',
-				'	result += shCoefficients[ 4 ] * 2.0 * 0.429043 * x * y;',
-				'	result += shCoefficients[ 5 ] * 2.0 * 0.429043 * y * z;',
-				'	result += shCoefficients[ 6 ] * ( 0.743125 * z * z - 0.247708 );',
-				'	result += shCoefficients[ 7 ] * 2.0 * 0.429043 * x * z;',
-				'	result += shCoefficients[ 8 ] * 0.429043 * ( x * x - y * y );',
+					// band 2,
+					result += shCoefficients[ 4 ] * 2.0 * 0.429043 * x * y;
+					result += shCoefficients[ 5 ] * 2.0 * 0.429043 * y * z;
+					result += shCoefficients[ 6 ] * ( 0.743125 * z * z - 0.247708 );
+					result += shCoefficients[ 7 ] * 2.0 * 0.429043 * x * z;
+					result += shCoefficients[ 8 ] * 0.429043 * ( x * x - y * y );
+					return result;
 
-				'	return result;',
+				}
 
-				'}',
+				uniform vec3 sh[ 9 ]; // sh coefficients
 
-				'uniform vec3 sh[ 9 ]; // sh coefficients',
+				uniform float intensity; // light probe intensity
 
-				'uniform float intensity; // light probe intensity',
+				varying vec3 vNormal;
 
-				'varying vec3 vNormal;',
+				void main() {
 
-				'void main() {',
+					vec3 normal = normalize( vNormal );
 
-				'	vec3 normal = normalize( vNormal );',
+					vec3 worldNormal = inverseTransformDirection( normal, viewMatrix );
 
-				'	vec3 worldNormal = inverseTransformDirection( normal, viewMatrix );',
+					vec3 irradiance = shGetIrradianceAt( worldNormal, sh );
 
-				'	vec3 irradiance = shGetIrradianceAt( worldNormal, sh );',
+					vec3 outgoingLight = RECIPROCAL_PI * irradiance * intensity;
 
-				'	vec3 outgoingLight = RECIPROCAL_PI * irradiance * intensity;',
+					gl_FragColor = linearToOutputTexel( vec4( outgoingLight, 1.0 ) );
 
-				'	gl_FragColor = linearToOutputTexel( vec4( outgoingLight, 1.0 ) );',
+				}
 
-				'}'
-
-			].join( '\n' )
+			`,
 
 		} );
 

+ 19 - 7
examples/jsm/loaders/KTX2Loader.js

@@ -251,22 +251,34 @@ class KTX2Loader extends Loader {
 
 		loader.load( url, ( buffer ) => {
 
-			// Check for an existing task using this buffer. A transferred buffer cannot be transferred
-			// again from this thread.
-			if ( _taskCache.has( buffer ) ) {
+			this.parse( buffer, onLoad, onError);
+
+		}, onProgress, onError );
+
+	}
+
+	parse( buffer, onLoad, onError ) {
+
+		if ( this.workerConfig === null ) {
+
+			throw new Error( 'THREE.KTX2Loader: Missing initialization with `.detectSupport( renderer )`.' );
+
+		}
+
+		// Check for an existing task using this buffer. A transferred buffer cannot be transferred
+		// again from this thread.
+		if ( _taskCache.has( buffer ) ) {
 
 				const cachedTask = _taskCache.get( buffer );
 
 				return cachedTask.promise.then( onLoad ).catch( onError );
 
-			}
+		}
 
-			this._createTexture( buffer )
+		this._createTexture( buffer )
 				.then( ( texture ) => onLoad ? onLoad( texture ) : null )
 				.catch( onError );
 
-		}, onProgress, onError );
-
 	}
 
 	_createTextureFrom( transcodeResult, container ) {

Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 243
examples/jsm/loaders/LogLuvLoader.js


+ 11 - 4
examples/jsm/loaders/MaterialXLoader.js

@@ -1,5 +1,6 @@
+import { FileLoader, Loader, TextureLoader, MeshBasicNodeMaterial, MeshPhysicalNodeMaterial, RepeatWrapping } from 'three';
+
 import {
-	FileLoader, Loader, TextureLoader, MeshBasicNodeMaterial, MeshPhysicalNodeMaterial, RepeatWrapping,
 	float, bool, int, vec2, vec3, vec4, color, texture,
 	positionLocal, positionWorld, uv, vertexColor,
 	normalLocal, normalWorld, tangentLocal, tangentWorld,
@@ -12,8 +13,9 @@ import {
 	mx_transform_uv,
 	mx_safepower, mx_contrast,
 	mx_srgb_texture_to_lin_rec709,
-	saturation
-} from 'three';
+	saturation,
+	timerLocal, frameId
+} from 'three/tsl';
 
 const colorSpaceLib = {
 	mx_srgb_texture_to_lin_rec709
@@ -21,7 +23,7 @@ const colorSpaceLib = {
 
 class MXElement {
 
-	constructor( name, nodeFunc, params = null ) {
+	constructor( name, nodeFunc, params = [] ) {
 
 		this.name = name;
 		this.nodeFunc = nodeFunc;
@@ -40,6 +42,8 @@ const mx_divide = ( in1, in2 = float( 1 ) ) => div( in1, in2 );
 const mx_modulo = ( in1, in2 = float( 1 ) ) => mod( in1, in2 );
 const mx_power = ( in1, in2 = float( 1 ) ) => pow( in1, in2 );
 const mx_atan2 = ( in1 = float( 0 ), in2 = float( 1 ) ) => atan2( in1, in2 );
+const mx_timer = () => timerLocal();
+const mx_frame = () => frameId;
 
 const MXElements = [
 
@@ -128,6 +132,9 @@ const MXElements = [
 	//new MtlXElement( 'separate3', ... ),
 	//new MtlXElement( 'separate4', ... )
 
+	new MXElement( 'time', mx_timer ),
+	new MXElement( 'frame', mx_frame )
+
 ];
 
 const MtlXLibrary = {};

+ 9 - 9
examples/jsm/loaders/PCDLoader.js

@@ -127,15 +127,15 @@ class PCDLoader extends Loader {
 
 			// parse
 
-			PCDheader.version = /VERSION (.*)/i.exec( PCDheader.str );
-			PCDheader.fields = /FIELDS (.*)/i.exec( PCDheader.str );
-			PCDheader.size = /SIZE (.*)/i.exec( PCDheader.str );
-			PCDheader.type = /TYPE (.*)/i.exec( PCDheader.str );
-			PCDheader.count = /COUNT (.*)/i.exec( PCDheader.str );
-			PCDheader.width = /WIDTH (.*)/i.exec( PCDheader.str );
-			PCDheader.height = /HEIGHT (.*)/i.exec( PCDheader.str );
-			PCDheader.viewpoint = /VIEWPOINT (.*)/i.exec( PCDheader.str );
-			PCDheader.points = /POINTS (.*)/i.exec( PCDheader.str );
+			PCDheader.version = /^VERSION (.*)/im.exec( PCDheader.str );
+			PCDheader.fields = /^FIELDS (.*)/im.exec( PCDheader.str );
+			PCDheader.size = /^SIZE (.*)/im.exec( PCDheader.str );
+			PCDheader.type = /^TYPE (.*)/im.exec( PCDheader.str );
+			PCDheader.count = /^COUNT (.*)/im.exec( PCDheader.str );
+			PCDheader.width = /^WIDTH (.*)/im.exec( PCDheader.str );
+			PCDheader.height = /^HEIGHT (.*)/im.exec( PCDheader.str );
+			PCDheader.viewpoint = /^VIEWPOINT (.*)/im.exec( PCDheader.str );
+			PCDheader.points = /^POINTS (.*)/im.exec( PCDheader.str );
 
 			// evaluate
 

+ 188 - 0
examples/jsm/objects/SkyMesh.js

@@ -0,0 +1,188 @@
+import {
+	BackSide,
+	BoxGeometry,
+	Mesh,
+	NodeMaterial,
+	Vector3
+} from 'three';
+import { float, Fn, vec3, acos, add, mul, clamp, cos, dot, exp, max, mix, modelViewProjection, normalize, positionWorld, pow, smoothstep, sub, varying, varyingProperty, vec4, uniform, cameraPosition } from 'three/tsl';
+
+/**
+ * Based on "A Practical Analytic Model for Daylight"
+ * aka The Preetham Model, the de facto standard analytic skydome model
+ * https://www.researchgate.net/publication/220720443_A_Practical_Analytic_Model_for_Daylight
+ *
+ * First implemented by Simon Wallner
+ * http://simonwallner.at/project/atmospheric-scattering/
+ *
+ * Improved by Martin Upitis
+ * http://blenderartists.org/forum/showthread.php?245954-preethams-sky-impementation-HDR
+ *
+ * Three.js integration by zz85 http://twitter.com/blurspline
+*/
+
+class SkyMesh extends Mesh {
+
+	constructor() {
+
+		const material = new NodeMaterial();
+
+		super( new BoxGeometry( 1, 1, 1 ), material );
+
+		this.turbidity = uniform( 2 );
+		this.rayleigh = uniform( 1 );
+		this.mieCoefficient = uniform( 0.005 );
+		this.mieDirectionalG = uniform( 0.8 );
+		this.sunPosition = uniform( new Vector3() );
+		this.upUniform = uniform( new Vector3( 0, 1, 0 ) );
+
+		this.isSky = true;
+
+		const vertexNode = /*@__PURE__*/ Fn( () => {
+
+			// constants for atmospheric scattering
+			const e = float( 2.71828182845904523536028747135266249775724709369995957 );
+			// const pi = float( 3.141592653589793238462643383279502884197169 );
+
+			// wavelength of used primaries, according to preetham
+			// const lambda = vec3( 680E-9, 550E-9, 450E-9 );
+			// this pre-calcuation replaces older TotalRayleigh(vec3 lambda) function:
+			// (8.0 * pow(pi, 3.0) * pow(pow(n, 2.0) - 1.0, 2.0) * (6.0 + 3.0 * pn)) / (3.0 * N * pow(lambda, vec3(4.0)) * (6.0 - 7.0 * pn))
+			const totalRayleigh = vec3( 5.804542996261093E-6, 1.3562911419845635E-5, 3.0265902468824876E-5 );
+
+			// mie stuff
+			// K coefficient for the primaries
+			// const v = float( 4.0 );
+			// const K = vec3( 0.686, 0.678, 0.666 );
+			// MieConst = pi * pow( ( 2.0 * pi ) / lambda, vec3( v - 2.0 ) ) * K
+			const MieConst = vec3( 1.8399918514433978E14, 2.7798023919660528E14, 4.0790479543861094E14 );
+
+			// earth shadow hack
+			// cutoffAngle = pi / 1.95;
+			const cutoffAngle = float( 1.6110731556870734 );
+			const steepness = float( 1.5 );
+			const EE = float( 1000.0 );
+
+			// varying sun position
+
+			const vSunDirection = normalize( this.sunPosition );
+			varyingProperty( 'vec3', 'vSunDirection' ).assign( vSunDirection );
+
+			// varying sun intensity
+
+			const angle = dot( vSunDirection, this.upUniform );
+			const zenithAngleCos = clamp( angle, - 1, 1 );
+			const sunIntensity = EE.mul( max( 0.0, float( 1.0 ).sub( pow( e, cutoffAngle.sub( acos( zenithAngleCos ) ).div( steepness ).negate() ) ) ) );
+			varyingProperty( 'float', 'vSunE' ).assign( sunIntensity );
+
+			// varying sun fade
+
+			const vSunfade = float( 1.0 ).sub( clamp( float( 1.0 ).sub( exp( this.sunPosition.y.div( 450000.0 ) ) ), 0, 1 ) );
+			varyingProperty( 'float', 'vSunfade' ).assign( vSunfade );
+
+			// varying vBetaR
+
+			const rayleighCoefficient = this.rayleigh.sub( float( 1.0 ).mul( float( 1.0 ).sub( vSunfade ) ) );
+
+			// extinction (absorbtion + out scattering)
+			// rayleigh coefficients
+			varyingProperty( 'vec3', 'vBetaR' ).assign( totalRayleigh.mul( rayleighCoefficient ) );
+
+			// varying vBetaM
+
+			const c = float( 0.2 ).mul( this.turbidity ).mul( 10E-18 );
+			const totalMie = float( 0.434 ).mul( c ).mul( MieConst );
+
+			varyingProperty( 'vec3', 'vBetaM' ).assign( totalMie.mul( this.mieCoefficient ) );
+
+			// position
+
+			const position = modelViewProjection();
+			position.z.assign( position.w ); // set z to camera.far
+
+			return position;
+
+		} )();
+
+		const fragmentNode = /*@__PURE__*/ Fn( () => {
+
+			const vSunDirection = varying( vec3(), 'vSunDirection' );
+			const vSunE = varying( float(), 'vSunE' );
+			const vSunfade = varying( float(), 'vSunfade' );
+			const vBetaR = varying( vec3(), 'vBetaR' );
+			const vBetaM = varying( vec3(), 'vBetaM' );
+
+			// constants for atmospheric scattering
+			const pi = float( 3.141592653589793238462643383279502884197169 );
+
+			// optical length at zenith for molecules
+			const rayleighZenithLength = float( 8.4E3 );
+			const mieZenithLength = float( 1.25E3 );
+			// 66 arc seconds -> degrees, and the cosine of that
+			const sunAngularDiameterCos = float( 0.999956676946448443553574619906976478926848692873900859324 );
+
+			// 3.0 / ( 16.0 * pi )
+			const THREE_OVER_SIXTEENPI = float( 0.05968310365946075 );
+			// 1.0 / ( 4.0 * pi )
+			const ONE_OVER_FOURPI = float( 0.07957747154594767 );
+
+			//
+
+			const direction = normalize( positionWorld.sub( cameraPosition ) );
+
+			// optical length
+			// cutoff angle at 90 to avoid singularity in next formula.
+			const zenithAngle = acos( max( 0.0, dot( this.upUniform, direction ) ) );
+			const inverse = float( 1.0 ).div( cos( zenithAngle ).add( float( 0.15 ).mul( pow( float( 93.885 ).sub( zenithAngle.mul( 180.0 ).div( pi ) ), - 1.253 ) ) ) );
+			const sR = rayleighZenithLength.mul( inverse );
+			const sM = mieZenithLength.mul( inverse );
+
+			// combined extinction factor
+			const Fex = exp( mul( vBetaR, sR ).add( mul( vBetaM, sM ) ).negate() );
+
+			// in scattering
+			const cosTheta = dot( direction, vSunDirection );
+
+			// betaRTheta
+
+			const c = cosTheta.mul( 0.5 ).add( 0.5 );
+			const rPhase = THREE_OVER_SIXTEENPI.mul( float( 1.0 ).add( pow( c, 2.0 ) ) );
+			const betaRTheta = vBetaR.mul( rPhase );
+
+			// betaMTheta
+
+			const g2 = pow( this.mieDirectionalG, 2.0 );
+			const inv = float( 1.0 ).div( pow( float( 1.0 ).sub( float( 2.0 ).mul( this.mieDirectionalG ).mul( cosTheta ) ).add( g2 ), 1.5 ) );
+			const mPhase = ONE_OVER_FOURPI.mul( float( 1.0 ).sub( g2 ) ).mul( inv );
+			const betaMTheta = vBetaM.mul( mPhase );
+
+			const Lin = pow( vSunE.mul( add( betaRTheta, betaMTheta ).div( add( vBetaR, vBetaM ) ) ).mul( sub( 1.0, Fex ) ), vec3( 1.5 ) );
+			Lin.mulAssign( mix( vec3( 1.0 ), pow( vSunE.mul( add( betaRTheta, betaMTheta ).div( add( vBetaR, vBetaM ) ) ).mul( Fex ), vec3( 1.0 / 2.0 ) ), clamp( pow( sub( 1.0, dot( this.upUniform, vSunDirection ) ), 5.0 ), 0.0, 1.0 ) ) );
+
+			// nightsky
+
+			const L0 = vec3( 0.1 ).mul( Fex );
+
+			// composition + solar disc
+			const sundisk = smoothstep( sunAngularDiameterCos, sunAngularDiameterCos.add( 0.00002 ), cosTheta );
+			L0.addAssign( vSunE.mul( 19000.0 ).mul( Fex ).mul( sundisk ) );
+
+			const texColor = add( Lin, L0 ).mul( 0.04 ).add( vec3( 0.0, 0.0003, 0.00075 ) );
+
+			const retColor = pow( texColor, vec3( float( 1.0 ).div( float( 1.2 ).add( vSunfade.mul( 1.2 ) ) ) ) );
+
+			return vec4( retColor, 1.0 );
+
+		} )();
+
+		material.side = BackSide;
+		material.depthWrite = false;
+
+		material.vertexNode = vertexNode;
+		material.fragmentNode = fragmentNode;
+
+	}
+
+}
+
+export { SkyMesh };

+ 159 - 0
examples/jsm/objects/Water2Mesh.js

@@ -0,0 +1,159 @@
+import {
+	Color,
+	Mesh,
+	NodeMaterial,
+	Vector2,
+	Vector3
+} from 'three';
+import { vec2, viewportSafeUV, viewportSharedTexture, reflector, pow, float, abs, texture, uniform, TempNode, NodeUpdateType, vec4, Fn, cameraPosition, positionWorld, uv, mix, vec3, normalize, max, dot, viewportUV } from 'three/tsl';
+
+/**
+ * References:
+ *	https://alex.vlachos.com/graphics/Vlachos-SIGGRAPH10-WaterFlow.pdf
+ *	http://graphicsrunner.blogspot.de/2010/08/water-using-flow-maps.html
+ *
+ */
+
+class WaterMesh extends Mesh {
+
+	constructor( geometry, options = {} ) {
+
+		const material = new NodeMaterial();
+
+		super( geometry, material );
+
+		this.isWater = true;
+
+		material.fragmentNode = new WaterNode( options, this );
+
+	}
+
+}
+
+class WaterNode extends TempNode {
+
+	constructor( options, waterBody ) {
+
+		super( 'vec4' );
+
+		this.waterBody = waterBody;
+
+		this.normalMap0 = texture( options.normalMap0 );
+		this.normalMap1 = texture( options.normalMap1 );
+		this.flowMap = texture( options.flowMap !== undefined ? options.flowMap : null );
+
+		this.color = uniform( options.color !== undefined ? new Color( options.color ) : new Color( 0xffffff ) );
+		this.flowDirection = uniform( options.flowDirection !== undefined ? options.flowDirection : new Vector2( 1, 0 ) );
+		this.flowSpeed = uniform( options.flowSpeed !== undefined ? options.flowSpeed : 0.03 );
+		this.reflectivity = uniform( options.reflectivity !== undefined ? options.reflectivity : 0.02 );
+		this.scale = uniform( options.scale !== undefined ? options.scale : 1 );
+		this.flowConfig = uniform( new Vector3() );
+
+		this.updateBeforeType = NodeUpdateType.RENDER;
+
+		this._cycle = 0.15; // a cycle of a flow map phase
+		this._halfCycle = this._cycle * 0.5;
+
+		this._USE_FLOW = options.flowMap !== undefined;
+
+	}
+
+	updateFlow( delta ) {
+
+		this.flowConfig.value.x += this.flowSpeed.value * delta; // flowMapOffset0
+		this.flowConfig.value.y = this.flowConfig.value.x + this._halfCycle; // flowMapOffset1
+
+		// Important: The distance between offsets should be always the value of "halfCycle".
+		// Moreover, both offsets should be in the range of [ 0, cycle ].
+		// This approach ensures a smooth water flow and avoids "reset" effects.
+
+		if ( this.flowConfig.value.x >= this._cycle ) {
+
+			this.flowConfig.value.x = 0;
+			this.flowConfig.value.y = this._halfCycle;
+
+		} else if ( this.flowConfig.value.y >= this._cycle ) {
+
+			this.flowConfig.value.y = this.flowConfig.value.y - this._cycle;
+
+		}
+
+		this.flowConfig.value.z = this._halfCycle;
+
+	}
+
+	updateBefore( frame ) {
+
+		this.updateFlow( frame.deltaTime );
+
+	}
+
+	setup() {
+
+		const outputNode = Fn( () => {
+
+			const flowMapOffset0 = this.flowConfig.x;
+			const flowMapOffset1 = this.flowConfig.y;
+			const halfCycle = this.flowConfig.z;
+
+			const toEye = normalize( cameraPosition.sub( positionWorld ) );
+
+			let flow;
+
+			if ( this._USE_FLOW === true ) {
+
+				flow = this.flowMap.rg.mul( 2 ).sub( 1 );
+
+			} else {
+
+				flow = vec2( this.flowDirection.x, this.flowDirection.y );
+
+			}
+
+			flow.x.mulAssign( - 1 );
+
+			// sample normal maps (distort uvs with flowdata)
+
+			const uvs = uv();
+
+			const normalUv0 = uvs.mul( this.scale ).add( flow.mul( flowMapOffset0 ) );
+			const normalUv1 = uvs.mul( this.scale ).add( flow.mul( flowMapOffset1 ) );
+
+			const normalColor0 = this.normalMap0.uv( normalUv0 );
+			const normalColor1 = this.normalMap1.uv( normalUv1 );
+
+			// linear interpolate to get the final normal color
+			const flowLerp = abs( halfCycle.sub( flowMapOffset0 ) ).div( halfCycle );
+			const normalColor = mix( normalColor0, normalColor1, flowLerp );
+
+			// calculate normal vector
+			const normal = normalize( vec3( normalColor.r.mul( 2 ).sub( 1 ), normalColor.b, normalColor.g.mul( 2 ).sub( 1 ) ) );
+
+			// calculate the fresnel term to blend reflection and refraction maps
+			const theta = max( dot( toEye, normal ), 0 );
+			const reflectance = pow( float( 1.0 ).sub( theta ), 5.0 ).mul( float( 1.0 ).sub( this.reflectivity ) ).add( this.reflectivity );
+
+			// reflector, refractor
+
+			const offset = normal.xz.mul( 0.05 ).toVar();
+
+			const reflectionSampler = reflector();
+			this.waterBody.add( reflectionSampler.target );
+			reflectionSampler.uvNode = reflectionSampler.uvNode.add( offset );
+
+			const refractorUV = viewportUV.add( offset );
+			const refractionSampler = viewportSharedTexture( viewportSafeUV( refractorUV ) );
+
+			// calculate final uv coords
+
+			return vec4( this.color, 1.0 ).mul( mix( refractionSampler, reflectionSampler, reflectance ) );
+
+		} )();
+
+		return outputNode;
+
+	}
+
+}
+
+export { WaterMesh };

+ 102 - 0
examples/jsm/objects/WaterMesh.js

@@ -0,0 +1,102 @@
+import {
+	Color,
+	Mesh,
+	NodeMaterial,
+	Vector3
+} from 'three';
+import { add, cameraPosition, div, normalize, positionWorld, sub, timerLocal, Fn, texture, vec2, vec3, vec4, max, dot, reflect, pow, length, float, uniform, reflector, mul, mix } from 'three/tsl';
+
+/**
+ * Work based on :
+ * https://github.com/Slayvin: Flat mirror for three.js
+ * https://home.adelphi.edu/~stemkoski/ : An implementation of water shader based on the flat mirror
+ * http://29a.ch/ && http://29a.ch/slides/2012/webglwater/ : Water shader explanations in WebGL
+ */
+
+class WaterMesh extends Mesh {
+
+	constructor( geometry, options ) {
+
+		const material = new NodeMaterial();
+
+		super( geometry, material );
+
+		this.isWater = true;
+
+		this.resolution = options.resolution !== undefined ? options.resolution : 0.5;
+
+		// uniforms
+
+		this.waterNormals = texture( options.waterNormals );
+		this.alpha = uniform( options.alpha !== undefined ? options.alpha : 1.0 );
+		this.size = uniform( options.size !== undefined ? options.size : 1.0 );
+		this.sunColor = uniform( new Color( options.sunColor !== undefined ? options.sunColor : 0xffffff ) );
+		this.sunDirection = uniform( options.sunDirection !== undefined ? options.sunDirection : new Vector3( 0.70707, 0.70707, 0.0 ) );
+		this.waterColor = uniform( new Color( options.waterColor !== undefined ? options.waterColor : 0x7f7f7f ) );
+		this.distortionScale = uniform( options.distortionScale !== undefined ? options.distortionScale : 20.0 );
+
+		// TSL
+
+		const timeNode = timerLocal();
+
+		const getNoise = Fn( ( [ uv ] ) => {
+
+			const uv0 = add( div( uv, 103 ), vec2( div( timeNode, 17 ), div( timeNode, 29 ) ) ).toVar();
+			const uv1 = div( uv, 107 ).sub( vec2( div( timeNode, - 19 ), div( timeNode, 31 ) ) ).toVar();
+			const uv2 = add( div( uv, vec2( 8907.0, 9803.0 ) ), vec2( div( timeNode, 101 ), div( timeNode, 97 ) ) ).toVar();
+			const uv3 = sub( div( uv, vec2( 1091.0, 1027.0 ) ), vec2( div( timeNode, 109 ), div( timeNode, - 113 ) ) ).toVar();
+
+			const sample0 = this.waterNormals.uv( uv0 );
+			const sample1 = this.waterNormals.uv( uv1 );
+			const sample2 = this.waterNormals.uv( uv2 );
+			const sample3 = this.waterNormals.uv( uv3 );
+
+			const noise = sample0.add( sample1 ).add( sample2 ).add( sample3 );
+
+			return noise.mul( 0.5 ).sub( 1 );
+
+		} );
+
+		const fragmentNode = Fn( () => {
+
+			const noise = getNoise( positionWorld.xz.mul( this.size ) );
+			const surfaceNormal = normalize( noise.xzy.mul( 1.5, 1.0, 1.5 ) );
+
+			const diffuseLight = vec3( 0 ).toVar();
+			const specularLight = vec3( 0 ).toVar();
+
+			const worldToEye = cameraPosition.sub( positionWorld );
+			const eyeDirection = normalize( worldToEye );
+
+			const reflection = normalize( reflect( this.sunDirection.negate(), surfaceNormal ) );
+			const direction = max( 0.0, dot( eyeDirection, reflection ) );
+			specularLight.addAssign( pow( direction, 100 ).mul( this.sunColor ).mul( 2.0 ) );
+			diffuseLight.addAssign( max( dot( this.sunDirection, surfaceNormal ), 0.0 ).mul( this.sunColor ).mul( 0.5 ) );
+
+			const distance = length( worldToEye );
+
+			const distortion = surfaceNormal.xy.mul( float( 0.001 ).add( float( 1.0 ).div( distance ) ) ).mul( this.distortionScale );
+
+			const mirrorSampler = reflector();
+			mirrorSampler.uvNode = mirrorSampler.uvNode.add( distortion );
+			mirrorSampler.resolution = this.resolution;
+
+			this.add( mirrorSampler.target );
+
+			const theta = max( dot( eyeDirection, surfaceNormal ), 0.0 );
+			const rf0 = float( 0.3 );
+			const reflectance = mul( pow( float( 1.0 ).sub( theta ), 5.0 ), float( 1.0 ).sub( rf0 ) ).add( rf0 );
+			const scatter = max( 0.0, dot( surfaceNormal, eyeDirection ) ).mul( this.waterColor );
+			const albedo = mix( this.sunColor.mul( diffuseLight ).mul( 0.3 ).add( scatter ), mirrorSampler.rgb.mul( specularLight ).add( mirrorSampler.rgb.mul( 0.9 ) ).add( vec3( 0.1 ) ), reflectance );
+
+			return vec4( albedo, this.alpha );
+
+		} )();
+
+		material.fragmentNode = fragmentNode;
+
+	}
+
+}
+
+export { WaterMesh };

+ 20 - 0
examples/jsm/physics/RapierPhysics.js

@@ -28,6 +28,26 @@ function getShape( geometry ) {
 		const radius = parameters.radius !== undefined ? parameters.radius : 1;
 		return RAPIER.ColliderDesc.ball( radius );
 
+	} else if ( geometry.type === 'BufferGeometry' ) {
+
+		const vertices = [];
+		const vertex = new Vector3();
+		const position = geometry.getAttribute( 'position' );
+
+		for ( let i = 0; i < position.count; i ++ ) {
+
+			vertex.fromBufferAttribute( position, i );
+			vertices.push( vertex.x, vertex.y, vertex.z );
+
+		}
+
+		// if the buffer is non-indexed, generate an index buffer
+		const indices = geometry.getIndex() === null
+							? Uint32Array.from( Array( parseInt( vertices.length / 3 ) ).keys() )
+							: geometry.getIndex().array;
+
+		return RAPIER.ColliderDesc.trimesh( vertices, indices );
+
 	}
 
 	return null;

+ 31 - 46
examples/jsm/postprocessing/OutlinePass.js

@@ -35,7 +35,7 @@ class OutlinePass extends Pass {
 		this.pulsePeriod = 0;
 
 		this._visibilityCache = new Map();
-
+		this._selectionCache = new Set();
 
 		this.resolution = ( resolution !== undefined ) ? new Vector2( resolution.x, resolution.y ) : new Vector2( 256, 256 );
 
@@ -172,29 +172,18 @@ class OutlinePass extends Pass {
 
 	}
 
-	changeVisibilityOfSelectedObjects( bVisible ) {
+	updateSelectionCache() {
 
-		const cache = this._visibilityCache;
+		const cache = this._selectionCache;
 
 		function gatherSelectedMeshesCallBack( object ) {
 
-			if ( object.isMesh ) {
-
-				if ( bVisible === true ) {
-
-					object.visible = cache.get( object );
-
-				} else {
-
-					cache.set( object, object.visible );
-					object.visible = bVisible;
-
-				}
-
-			}
+			if ( object.isMesh ) cache.add( object );
 
 		}
 
+		cache.clear();
+
 		for ( let i = 0; i < this.selectedObjects.length; i ++ ) {
 
 			const selectedObject = this.selectedObjects[ i ];
@@ -204,56 +193,49 @@ class OutlinePass extends Pass {
 
 	}
 
-	changeVisibilityOfNonSelectedObjects( bVisible ) {
+	changeVisibilityOfSelectedObjects( bVisible ) {
 
 		const cache = this._visibilityCache;
-		const selectedMeshes = [];
-
-		function gatherSelectedMeshesCallBack( object ) {
-
-			if ( object.isMesh ) selectedMeshes.push( object );
 
-		}
-
-		for ( let i = 0; i < this.selectedObjects.length; i ++ ) {
+		for ( const mesh of this._selectionCache ) {
 
-			const selectedObject = this.selectedObjects[ i ];
-			selectedObject.traverse( gatherSelectedMeshesCallBack );
+			if ( bVisible === true ) {
 
-		}
+				mesh.visible = cache.get( mesh );
 
-		function VisibilityChangeCallBack( object ) {
+			} else {
 
-			if ( object.isMesh || object.isSprite ) {
+				cache.set( mesh, mesh.visible );
+				mesh.visible = bVisible;
 
-				// only meshes and sprites are supported by OutlinePass
+			}
 
-				let bFound = false;
+		}
 
-				for ( let i = 0; i < selectedMeshes.length; i ++ ) {
+	}
 
-					const selectedObjectId = selectedMeshes[ i ].id;
+	changeVisibilityOfNonSelectedObjects( bVisible ) {
 
-					if ( selectedObjectId === object.id ) {
+		const visibilityCache = this._visibilityCache;
+		const selectionCache = this._selectionCache;
 
-						bFound = true;
-						break;
+		function VisibilityChangeCallBack( object ) {
 
-					}
+			if ( object.isMesh || object.isSprite ) {
 
-				}
+				// only meshes and sprites are supported by OutlinePass
 
-				if ( bFound === false ) {
+				if ( ! selectionCache.has( object ) ) {
 
 					const visibility = object.visible;
 
-					if ( bVisible === false || cache.get( object ) === true ) {
+					if ( bVisible === false || visibilityCache.get( object ) === true ) {
 
 						object.visible = bVisible;
 
 					}
 
-					cache.set( object, visibility );
+					visibilityCache.set( object, visibility );
 
 				}
 
@@ -264,11 +246,11 @@ class OutlinePass extends Pass {
 
 				if ( bVisible === true ) {
 
-					object.visible = cache.get( object ); // restore
+					object.visible = visibilityCache.get( object ); // restore
 
 				} else {
 
-					cache.set( object, object.visible );
+					visibilityCache.set( object, object.visible );
 					object.visible = bVisible;
 
 				}
@@ -306,6 +288,8 @@ class OutlinePass extends Pass {
 
 			renderer.setClearColor( 0xffffff, 1 );
 
+			this.updateSelectionCache();
+
 			// Make selected objects invisible
 			this.changeVisibilityOfSelectedObjects( false );
 
@@ -337,6 +321,7 @@ class OutlinePass extends Pass {
 			this.renderScene.overrideMaterial = null;
 			this.changeVisibilityOfNonSelectedObjects( true );
 			this._visibilityCache.clear();
+			this._selectionCache.clear();
 
 			this.renderScene.background = currentBackground;
 
@@ -462,7 +447,7 @@ class OutlinePass extends Pass {
 						worldPosition = instanceMatrix * worldPosition;
 
 					#endif
-					
+
 					worldPosition = modelMatrix * worldPosition;
 
 					projTexCoord = textureMatrix * worldPosition;

+ 3 - 1
examples/jsm/postprocessing/SSAARenderPass.js

@@ -31,6 +31,8 @@ class SSAARenderPass extends Pass {
 		this.sampleLevel = 4; // specified as n, where the number of samples is 2^n, so sampleLevel = 4, is 2^4 samples, 16.
 		this.unbiased = true;
 
+		this.stencilBuffer = false;
+
 		// as we need to clear the buffer in this pass, clearColor must be set to something, defaults to black.
 		this.clearColor = ( clearColor !== undefined ) ? clearColor : 0x000000;
 		this.clearAlpha = ( clearAlpha !== undefined ) ? clearAlpha : 0;
@@ -79,7 +81,7 @@ class SSAARenderPass extends Pass {
 
 		if ( ! this.sampleRenderTarget ) {
 
-			this.sampleRenderTarget = new WebGLRenderTarget( readBuffer.width, readBuffer.height, { type: HalfFloatType } );
+			this.sampleRenderTarget = new WebGLRenderTarget( readBuffer.width, readBuffer.height, { type: HalfFloatType, stencilBuffer: this.stencilBuffer } );
 			this.sampleRenderTarget.texture.name = 'SSAARenderPass.sample';
 
 		}

+ 1 - 2
examples/jsm/shaders/BleachBypassShader.js

@@ -38,8 +38,7 @@ const BleachBypassShader = {
 
 			vec4 base = texture2D( tDiffuse, vUv );
 
-			vec3 lumCoeff = vec3( 0.25, 0.65, 0.1 );
-			float lum = dot( lumCoeff, base.rgb );
+			float lum = luminance( base.rgb );
 			vec3 blend = vec3( lum );
 
 			float L = min( 1.0, max( 0.0, 10.0 * ( lum - 0.45 ) ) );

+ 1 - 1
examples/jsm/shaders/OutputShader.js

@@ -54,7 +54,7 @@ const OutputShader = {
 
 			#elif defined( CINEON_TONE_MAPPING )
 
-				gl_FragColor.rgb = OptimizedCineonToneMapping( gl_FragColor.rgb );
+				gl_FragColor.rgb = CineonToneMapping( gl_FragColor.rgb );
 
 			#elif defined( ACES_FILMIC_TONE_MAPPING )
 

+ 8 - 8
examples/jsm/transpiler/TSLEncoder.js

@@ -322,9 +322,9 @@ class TSLEncoder {
 		const leftStr = this.emitExpression( node.left );
 		const rightStr = this.emitExpression( node.right );
 
-		this.addImport( 'cond' );
+		this.addImport( 'select' );
 
-		return `cond( ${ condStr }, ${ leftStr }, ${ rightStr } )`;
+		return `select( ${ condStr }, ${ leftStr }, ${ rightStr } )`;
 
 	}
 
@@ -349,7 +349,7 @@ ${ this.tab }} )`;
 
 				const elseCondStr = this.emitExpression( current.elseConditional.cond );
 
-				ifStr += `.elseif( ${ elseCondStr }, () => {
+				ifStr += `.ElseIf( ${ elseCondStr }, () => {
 
 ${ elseBodyStr }
 
@@ -357,7 +357,7 @@ ${ this.tab }} )`;
 
 			} else {
 
-				ifStr += `.else( () => {
+				ifStr += `.Else( () => {
 
 ${ elseBodyStr }
 
@@ -391,13 +391,13 @@ ${ this.tab }} )`;
 		const conditionParam = condition !== '<' ? `, condition: '${ condition }'` : '';
 		const updateParam = update !== '++' ? `, update: '${ update }'` : '';
 
-		let loopStr = `loop( { start: ${ start }, end: ${ end + nameParam + typeParam + conditionParam + updateParam } }, ( { ${ name } } ) => {\n\n`;
+		let loopStr = `Loop( { start: ${ start }, end: ${ end + nameParam + typeParam + conditionParam + updateParam } }, ( { ${ name } } ) => {\n\n`;
 
 		loopStr += this.emitBody( node.body ) + '\n\n';
 
 		loopStr += this.tab + '} )';
 
-		this.imports.add( 'loop' );
+		this.imports.add( 'Loop' );
 
 		return loopStr;
 
@@ -586,7 +586,7 @@ ${ this.tab }} )`;
 
 		const prefix = this.iife === false ? 'export ' : '';
 
-		let funcStr = `${ prefix }const ${ fnName } = /*#__PURE__*/ tslFn( (${ paramsStr }) => {
+		let funcStr = `${ prefix }const ${ fnName } = /*#__PURE__*/ Fn( (${ paramsStr }) => {
 
 ${ bodyStr }
 
@@ -608,7 +608,7 @@ ${ this.tab }} )`;
 
 		funcStr += ';\n';
 
-		this.imports.add( 'tslFn' );
+		this.imports.add( 'Fn' );
 
 		this.global.add( node.name );
 

+ 4 - 4
examples/misc_controls_drag.html

@@ -20,7 +20,7 @@
 		<div id="info">
 			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgl - drag controls<br />
 			Use "Shift+Click" to add/remove objects to/from a group.<br />
-			Use "M" to toggle between rotate and translate mode.<br />
+			Use "M" to toggle between rotate and pan mode (touch only).<br />
 			Grouped objects can be transformed as a union.
 		</div>
 
@@ -143,10 +143,10 @@
 			function onKeyDown( event ) {
 
 				enableSelection = ( event.keyCode === 16 ) ? true : false;
-				
+			
 				if ( event.keyCode === 77 ) {
 
-					controls.mode = ( controls.mode === 'translate' ) ? 'rotate' : 'translate';
+					controls.touches.ONE = ( controls.touches.ONE === THREE.TOUCH.PAN ) ? THREE.TOUCH.ROTATE : THREE.TOUCH.PAN;
 			
 				}
 
@@ -164,7 +164,7 @@
 
 				if ( enableSelection === true ) {
 
-					const draggableObjects = controls.getObjects();
+					const draggableObjects = controls.objects;
 					draggableObjects.length = 0;
 
 					mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;

+ 3 - 3
examples/misc_controls_fly.html

@@ -1,7 +1,7 @@
 <!DOCTYPE html>
 <html lang="en">
 	<head>
-		<title>three.js webgl - fly controls - earth</title>
+		<title>three.js webgpu - fly controls - earth</title>
 		<meta charset="utf-8">
 		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
 		<link type="text/css" rel="stylesheet" href="main.css">
@@ -40,7 +40,7 @@
 		<script type="module">
 
 			import * as THREE from 'three';
-			import { pass } from 'three/tsl';
+			import { pass, film } from 'three/tsl';
 
 			import Stats from 'three/addons/libs/stats.module.js';
 			import { FlyControls } from 'three/addons/controls/FlyControls.js';
@@ -221,7 +221,7 @@
 				const scenePass = pass( scene, camera );
 				const scenePassColor = scenePass.getTextureNode();
 
-				postProcessing.outputNode = scenePassColor.film();
+				postProcessing.outputNode = film( scenePassColor );
 
 			}
 

+ 5 - 5
examples/misc_controls_pointerlock.html

@@ -115,7 +115,7 @@
 
 				} );
 
-				scene.add( controls.getObject() );
+				scene.add( controls.object );
 
 				const onKeyDown = function ( event ) {
 
@@ -283,7 +283,7 @@
 
 				if ( controls.isLocked === true ) {
 
-					raycaster.ray.origin.copy( controls.getObject().position );
+					raycaster.ray.origin.copy( controls.object.position );
 					raycaster.ray.origin.y -= 10;
 
 					const intersections = raycaster.intersectObjects( objects, false );
@@ -314,12 +314,12 @@
 					controls.moveRight( - velocity.x * delta );
 					controls.moveForward( - velocity.z * delta );
 
-					controls.getObject().position.y += ( velocity.y * delta ); // new behavior
+					controls.object.position.y += ( velocity.y * delta ); // new behavior
 
-					if ( controls.getObject().position.y < 10 ) {
+					if ( controls.object.position.y < 10 ) {
 
 						velocity.y = 0;
-						controls.getObject().position.y = 10;
+						controls.object.position.y = 10;
 
 						canJump = true;
 

+ 1 - 1
examples/misc_exporter_usdz.html

@@ -79,7 +79,7 @@
 
 				scene = new THREE.Scene();
 				scene.background = new THREE.Color( 0xf0f0f0 );
-				scene.environment = pmremGenerator.fromScene( new RoomEnvironment( renderer ), 0.04 ).texture;
+				scene.environment = pmremGenerator.fromScene( new RoomEnvironment(), 0.04 ).texture;
 
 				const loader = new GLTFLoader().setPath( 'models/gltf/DamagedHelmet/glTF/' );
 				loader.load( 'DamagedHelmet.gltf', async function ( gltf ) {

BIN
examples/models/gltf/LightsPunctualLamp.glb


BIN
examples/models/gltf/coffeeMug.glb


BIN
examples/models/gltf/dungeon_warkarma.glb


BIN
examples/models/gltf/gears.glb


BIN
examples/screenshots/webgl_loader_gltf_lights.jpg


BIN
examples/screenshots/webgl_loader_texture_logluv.jpg


BIN
examples/screenshots/webgl_materials_curvature.jpg


BIN
examples/screenshots/webgl_performance.jpg


BIN
examples/screenshots/webgl_texture2darray_layerupdate.jpg


BIN
examples/screenshots/webgpu_backdrop.jpg


BIN
examples/screenshots/webgpu_backdrop_water.jpg


BIN
examples/screenshots/webgpu_clearcoat.jpg


BIN
examples/screenshots/webgpu_compute_birds.jpg


BIN
examples/screenshots/webgpu_cubemap_adjustments.jpg


BIN
examples/screenshots/webgpu_cubemap_dynamic.jpg


BIN
examples/screenshots/webgpu_display_stereo.jpg


BIN
examples/screenshots/webgpu_instance_points.jpg


BIN
examples/screenshots/webgpu_lightprobe.jpg


BIN
examples/screenshots/webgpu_loader_gltf.jpg


BIN
examples/screenshots/webgpu_loader_gltf_iridescence.jpg


BIN
examples/screenshots/webgpu_loader_gltf_sheen.jpg


BIN
examples/screenshots/webgpu_loader_materialx.jpg


BIN
examples/screenshots/webgpu_materials_basic.jpg


BIN
examples/screenshots/webgpu_materials_displacementmap.jpg


BIN
examples/screenshots/webgpu_materials_envmaps.jpg


BIN
examples/screenshots/webgpu_materials_transmission.jpg


Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff

粤ICP备19079148号