├── demo ├── ThreeObitControlsGizmo.gif ├── index.html ├── style.css └── main.js ├── non-module ├── README.md ├── OrbitControlsGizmo.js └── OrbitControls.js ├── LICENSE ├── README.md ├── OrbitControlsGizmo.js └── OrbitControls.js /demo/ThreeObitControlsGizmo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fennec-hub/ThreeOrbitControlsGizmo/HEAD/demo/ThreeObitControlsGizmo.gif -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Demo - Orbit Controls Gizmo 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /non-module/README.md: -------------------------------------------------------------------------------- 1 | This non-module version of the OrbitControlsGizmo can be used directly in the `HTML` with either of below: 2 | - `` - if no path is required 3 | - `` - with some path added 4 | 5 | It has been used as such in Three.js viewers found on the [webpage](https://githubdragonfly.github.io/). 6 | -------------------------------------------------------------------------------- /demo/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #000; 3 | margin: 0px; 4 | overflow: hidden; 5 | } 6 | 7 | .obit-controls-gizmo { 8 | position: absolute; 9 | top: 2em; 10 | right: 2em; 11 | z-index: 1000; 12 | background-color: #FFF0; 13 | border-radius: 100%; 14 | transition: background-color .15s linear; 15 | cursor: pointer; 16 | } 17 | 18 | .obit-controls-gizmo.dragging, .obit-controls-gizmo:hover { 19 | background-color: #FFF3; 20 | 21 | } 22 | 23 | .obit-controls-gizmo.inactive { 24 | pointer-events: none; 25 | background-color: #FFF0 !important; 26 | } 27 | 28 | .dg.a { 29 | float: left !important; 30 | margin-left: 2em !important; 31 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Millennium-Fennec 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /demo/main.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.module.js"; 2 | import { GUI } from "https://unpkg.com/dat.gui@0.7.7/build/dat.gui.module.js"; 3 | 4 | import { OrbitControls } from "../OrbitControls.js"; 5 | import { OrbitControlsGizmo } from "../OrbitControlsGizmo.js"; 6 | 7 | var mesh, renderer, scene, camera, controls, controlsGizmo; 8 | 9 | init(); 10 | animate(); 11 | 12 | function init() { 13 | 14 | // renderer 15 | renderer = new THREE.WebGLRenderer({ antialias: true }); 16 | renderer.setSize( window.innerWidth, window.innerHeight ); 17 | renderer.setClearColor(new THREE.Color(0x333333)); 18 | renderer.setPixelRatio( window.devicePixelRatio ); 19 | document.body.appendChild( renderer.domElement ); 20 | 21 | // scene 22 | scene = new THREE.Scene(); 23 | 24 | // camera 25 | camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.1, 10000 ); 26 | camera.position.set( 15, 12, 12 ); 27 | 28 | // Orbit Controls 29 | controls = new OrbitControls( camera, renderer.domElement ); 30 | 31 | // Obit Controls Gizmo 32 | controlsGizmo = new OrbitControlsGizmo(controls, { size: 100, padding: 8 }); 33 | 34 | // Add the Gizmo to the document 35 | document.body.appendChild(controlsGizmo.domElement); 36 | 37 | // ambient light 38 | scene.add( new THREE.AmbientLight( 0x222222 ) ); 39 | 40 | // directional light 41 | var light = new THREE.DirectionalLight( 0xffffff, 1 ); 42 | light.position.set( 2,2, 0 ); 43 | scene.add( light ); 44 | 45 | // axes Helper 46 | const axesHelper = new THREE.AxesHelper( 15 ); 47 | scene.add( axesHelper ); 48 | 49 | // Grid Helper 50 | scene.add(new THREE.GridHelper(10, 10, "#666666", "#222222")); 51 | 52 | // geometry 53 | var geometry = new THREE.BoxGeometry( 1, 1, 1 ); 54 | 55 | // material 56 | var material = new THREE.MeshPhongMaterial( { 57 | color: 0x00ffff, 58 | transparent: true, 59 | opacity: 0.7, 60 | }); 61 | 62 | // mesh 63 | mesh = new THREE.Mesh( geometry, material ); 64 | mesh.position.set(0, 0.5, 0); 65 | scene.add( mesh ); 66 | 67 | // GUI 68 | const gui = new GUI(); 69 | gui.add(controls, 'enabled').name("Enable Orbit Controls"); 70 | gui.add(controlsGizmo, 'lock').name("Lock Gizmo"); 71 | gui.add(controlsGizmo, 'lockX').name("Lock Gizmo's X Axis"); 72 | gui.add(controlsGizmo, 'lockY').name("Lock Gizmo's Y Axis"); 73 | 74 | } 75 | 76 | function animate() { 77 | 78 | requestAnimationFrame( animate ); 79 | renderer.render( scene, camera ); 80 | controls.update(); 81 | 82 | } 83 | 84 | function resize() { 85 | renderer.setSize( window.innerWidth, window.innerHeight ); 86 | camera.aspect = ( window.innerWidth / window.innerHeight ); 87 | camera.updateProjectionMatrix(); 88 | } 89 | 90 | window.onresize = resize; 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚠️ DEPRECATED ⚠️ 2 | 🚀 **New Repository:** We've replaced this version with a modernized one. Please check out [three-viewport-gizmo](https://github.com/Fennec-hub/three-viewport-gizmo) for the latest updates and improvements. 3 | 4 | 📢 **Why the change?** We've upgraded to a more advanced version to provide a better experience and enhanced features. Please use the new repository for all future usage and contributions. 5 | 6 | 🙏 **Thanks for your consideration!** We appreciate your support. If you have any questions or need assistance, don't hesitate to reach out. 7 | 8 | # Three Orbit Controls Gizmo 9 | 10 | This library is the continuation of **[jrj2211](https://github.com/jrj2211/three-orientation-gizmo)**'s work, a lightweight **Blender** like orientation **gizmo** for **Three.js** using an internal HTML5 Canvas, adapted here to work in tandem with a slightly modified version of **`THREE.OrbitControls`**, it follow the orbit controls changes and you can now change the camera angle by dragging the gizmo or select an axis by clicking on it. 11 | 12 | **[Live Demo](https://fennec-hub.github.io/ThreeOrbitControlsGizmo/)** 13 |

14 | 15 | 16 | 17 |

18 | 19 | ### `THREE.OrbitControls` modifications : 20 | In order to programmatically change the `OrbitControls` camera sphere angles (`spherical.theta` , `spherical.phi`) I had to access these two methods (`rotateLeft`, `rotateUp`), since they are internal functions I simply exposed them by adding these two lines : 21 | 22 | ```javascript 23 | // These two lines are the only modifications done to THREE.OrbitControls 24 | this.rotateLeft = rotateLeft; 25 | this.rotateUp = rotateUp; 26 | ``` 27 | 28 | ### Usage 29 | 30 | ```javascript 31 | import { OrbitControls } from "ThreeOrbitControlsGizmo/OrbitControls.js"; 32 | import { OrbitControlsGizmo } from "ThreeOrbitControlsGizmo/OrbitControlsGizmo.js"; 33 | 34 | // Add the Orbit Controls 35 | const controls = new OrbitControls( camera, renderer.domElement ); 36 | 37 | // Add the Obit Controls Gizmo 38 | const controlsGizmo = new OrbitControlsGizmo(controls, { size: 100, padding: 8 }); 39 | 40 | // Add the Gizmo domElement to the dom 41 | document.body.appendChild(controlsGizmo.domElement); 42 | ``` 43 | 44 | #### Direct integration - Add by [GitHubDragonFly](https://github.com/GitHubDragonFly) 45 | 46 | This non-module version of the OrbitControlsGizmo can be used directly in the `HTML` with either of below: 47 | - `` - if no path is required 48 | - `` - with some path added 49 | 50 | It has been used as such in Three.js viewers found on the [webpage](https://githubdragonfly.github.io/). 51 | 52 | ### Options 53 | | Property | Default | description | 54 | |--|--|--| 55 | | size | 90 | Size of the gizmo `domElement` (`canvas`) | 56 | | padding | 8 | Adds padding around the gizmo (makes it look nicer when using a circular background) | 57 | | bubbleSizePrimary | 8 | Size of the circle for the positive axes (X,Y,Z) | 58 | | bubbleSizeSecondary | 6 | Size of the circle for the negative axes (-x,-Y,-Z) | 59 | | lineWidth | 2 | Size of the stroke to use for connecting the bubble to the center point | 60 | | fontSize | 2 | Primary axes label font size | 61 | | fontFamily | "arial" | Primary axes label font family | 62 | | fontWeight | "bold" | Primary axes label font weight | 63 | | fontColor | "#222222" | Primary axes label font color | 64 | | className | "obit-controls-gizmo" | the `domElement` class name | 65 | | colors | `{ x: ["#f73c3c", "#942424"], y: ["#6ccb26", "#417a17"], z: ["#178cf0", "#0e5490"] }` | Each axis [foreground, background] colors | 66 | 67 | ### Properties 68 | - `.lock` Boolean : Lock all axes 69 | - `.lockX` Boolean : Lock `X` axis 70 | - `.lockY` Boolean : Lock `Y` axis 71 | 72 | ### Methods 73 | - `.update()` : Update the gizmo orientation 74 | - `.dispose()` : Dispose of the gizmo, remove the canvas from the dom, remove all event listeners 75 | 76 | ### Styling 77 | To get a **Blender** like orientation gizmo style and effects add this to your css : 78 | 79 | ```css 80 | .obit-controls-gizmo { 81 | position: absolute; 82 | top: 2em; 83 | right: 2em; 84 | z-index: 1000; 85 | background-color: #FFF0; 86 | border-radius: 100%; 87 | transition: background-color .15s linear; 88 | cursor: pointer; 89 | } 90 | 91 | .obit-controls-gizmo.dragging, 92 | .obit-controls-gizmo:hover { 93 | background-color: #FFF3; 94 | } 95 | 96 | .obit-controls-gizmo.inactive { 97 | pointer-events: none; 98 | background-color: #FFF0 !important; 99 | } 100 | ``` 101 | 102 | -------------------------------------------------------------------------------- /non-module/OrbitControlsGizmo.js: -------------------------------------------------------------------------------- 1 | class OrbitControlsGizmo { 2 | constructor(orbitControls, options) { 3 | 4 | options = Object.assign({ 5 | size: 90, 6 | padding: 8, 7 | bubbleSizePrimary: 8, 8 | bubbleSizeSecondary: 6, 9 | lineWidth: 2, 10 | fontSize: "12px", 11 | fontFamily: "arial", 12 | fontWeight: "bold", 13 | fontColor: "#222222", 14 | className: "obit-controls-gizmo", 15 | colors: { 16 | x: ["#f73c3c", "#942424"], 17 | y: ["#6ccb26", "#417a17"], 18 | z: ["#178cf0", "#0e5490"], 19 | } 20 | }, options); 21 | 22 | this.lock = false; 23 | this.lockX = false; 24 | this.lockY = false; 25 | 26 | this.update = () => { 27 | if(this.lock) 28 | return; 29 | 30 | camera.updateMatrix(); 31 | invRotMat.extractRotation(camera.matrix).invert(); 32 | 33 | for (let i = 0, length = axes.length; i < length; i++) 34 | setAxisPosition(axes[i], invRotMat); 35 | 36 | // Sort the layers where the +Z position is last so its drawn on top of anything below it 37 | axes.sort((a, b) => (a.position.z > b.position.z) ? 1 : -1); 38 | 39 | // Draw the layers 40 | drawLayers(true); 41 | 42 | } 43 | 44 | this.dispose = () => { 45 | orbit.removeEventListener("change", this.update); 46 | orbit.removeEventListener("start", () => this.domElement.classList.add("inactive")); 47 | orbit.removeEventListener("end", () => this.domElement.classList.remove("inactive")); 48 | 49 | this.domElement.removeEventListener('pointerdown', onPointerDown, false); 50 | this.domElement.removeEventListener('pointerenter', onPointerEnter, false); 51 | this.domElement.removeEventListener('pointermove', onPointerMove, false); 52 | this.domElement.removeEventListener('click', onMouseClick, false); 53 | window.removeEventListener('pointermove', onDrag, false); 54 | window.removeEventListener('pointerup', onPointerUp, false); 55 | this.domElement.remove(); 56 | } 57 | 58 | // Internals 59 | const scoped = this; 60 | const orbit = orbitControls; 61 | const camera = orbitControls.object; 62 | const invRotMat = new THREE.Matrix4(); 63 | const mouse = new THREE.Vector3(); 64 | const rotateStart = new THREE.Vector2(); 65 | const rotateEnd = new THREE.Vector2(); 66 | const rotateDelta = new THREE.Vector2(); 67 | const center = new THREE.Vector3(options.size / 2, options.size / 2, 0); 68 | const axes = createAxes(); 69 | let selectedAxis = null; 70 | let isDragging = false; 71 | let context; 72 | let rect; 73 | let orbitState; 74 | 75 | orbit.addEventListener("change", this.update); 76 | orbit.addEventListener("start", () => this.domElement.classList.add("inactive")); 77 | orbit.addEventListener("end", () => this.domElement.classList.remove("inactive")); 78 | 79 | function createAxes () { 80 | // Generate list of axes 81 | const colors = options.colors; 82 | const line = options.lineWidth; 83 | const size = { 84 | primary: options.bubbleSizePrimary, 85 | secondary: options.bubbleSizeSecondary, 86 | } 87 | return [ 88 | { axis: "x", direction: new THREE.Vector3(1, 0, 0), size: size.primary, color: colors.x, line, label: "X", position: new THREE.Vector3(0, 0, 0) }, 89 | { axis: "y", direction: new THREE.Vector3(0, 1, 0), size: size.primary, color: colors.y, line, label: "Y", position: new THREE.Vector3(0, 0, 0) }, 90 | { axis: "z", direction: new THREE.Vector3(0, 0, 1), size: size.primary, color: colors.z, line, label: "Z", position: new THREE.Vector3(0, 0, 0) }, 91 | { axis: "-x", direction: new THREE.Vector3(-1, 0, 0), size: size.secondary, color: colors.x, position: new THREE.Vector3(0, 0, 0) }, 92 | { axis: "-y", direction: new THREE.Vector3(0, -1, 0), size: size.secondary, color: colors.y, position: new THREE.Vector3(0, 0, 0) }, 93 | { axis: "-z", direction: new THREE.Vector3(0, 0, -1), size: size.secondary, color: colors.z, position: new THREE.Vector3(0, 0, 0) }, 94 | ]; 95 | } 96 | 97 | function createCanvas () { 98 | const canvas = document.createElement('canvas'); 99 | canvas.width = options.size; 100 | canvas.height = options.size; 101 | canvas.classList.add(options.className); 102 | 103 | canvas.addEventListener('pointerdown', onPointerDown, false); 104 | canvas.addEventListener('pointerenter', onPointerEnter, false); 105 | canvas.addEventListener('pointermove', onPointerMove, false); 106 | canvas.addEventListener('click', onMouseClick, false); 107 | 108 | context = canvas.getContext("2d"); 109 | 110 | return canvas; 111 | } 112 | 113 | function onPointerDown ( e ) { 114 | rotateStart.set( e.clientX, e.clientY ); 115 | orbitState = orbit.enabled; 116 | orbit.enabled = false; 117 | window.addEventListener('pointermove', onDrag, false); 118 | window.addEventListener('pointerup', onPointerUp, false); 119 | } 120 | 121 | function onPointerUp () { 122 | setTimeout(() => isDragging = false, 0); 123 | scoped.domElement.classList.remove("dragging"); 124 | orbit.enabled = orbitState; 125 | window.removeEventListener('pointermove', onDrag, false); 126 | window.removeEventListener('pointerup', onPointerUp, false); 127 | } 128 | 129 | function onPointerEnter () { 130 | rect = scoped.domElement.getBoundingClientRect(); 131 | } 132 | 133 | function onPointerMove ( e ) { 134 | if(isDragging || scoped.lock) 135 | return; 136 | 137 | const currentAxis = selectedAxis; 138 | 139 | selectedAxis = null; 140 | if(e) 141 | mouse.set(e.clientX - rect.left, e.clientY - rect.top, 0); 142 | 143 | // Loop through each layer 144 | for (let i = 0, length = axes.length; i < length; i++) { 145 | const distance = mouse.distanceTo(axes[i].position); 146 | 147 | if (distance < axes[i].size) 148 | selectedAxis = axes[i]; 149 | } 150 | 151 | if(currentAxis !== selectedAxis) 152 | drawLayers(); 153 | } 154 | 155 | function onDrag ( e ) { 156 | if(scoped.lock) 157 | return; 158 | 159 | if(!isDragging) 160 | scoped.domElement.classList.add("dragging"); 161 | 162 | isDragging = true; 163 | 164 | selectedAxis = null; 165 | 166 | rotateEnd.set( e.clientX, e.clientY ); 167 | 168 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( 0.5 ); 169 | 170 | if(!scoped.lockX) 171 | orbit.rotateLeft( 2 * Math.PI * rotateDelta.x / scoped.domElement.height ); 172 | 173 | if(!scoped.lockY) 174 | orbit.rotateUp( 2 * Math.PI * rotateDelta.y / scoped.domElement.height ); 175 | 176 | rotateStart.copy( rotateEnd ); 177 | 178 | orbit.update(); 179 | } 180 | 181 | function onMouseClick () { 182 | //FIXME Don't like the current animation 183 | if(isDragging || !selectedAxis) 184 | return; 185 | 186 | const vec = selectedAxis.direction.clone(); 187 | const distance = camera.position.distanceTo(orbit.target); 188 | vec.multiplyScalar(distance); 189 | 190 | const duration = 400; 191 | const start = performance.now(); 192 | const maxAlpha = 1; 193 | function loop () { 194 | const now = performance.now(); 195 | const delta = now - start; 196 | const alpha = Math.min(delta / duration, maxAlpha); 197 | camera.position.lerp(vec, alpha); 198 | orbit.update(); 199 | 200 | if(alpha !== maxAlpha) 201 | return requestAnimationFrame(loop) 202 | 203 | onPointerMove(); 204 | 205 | } 206 | 207 | loop(); 208 | 209 | 210 | selectedAxis = null; 211 | } 212 | 213 | function drawCircle ( p, radius = 10, color = "#FF0000" ) { 214 | context.beginPath(); 215 | context.arc(p.x, p.y, radius, 0, 2 * Math.PI, false); 216 | context.fillStyle = color; 217 | context.fill(); 218 | context.closePath(); 219 | } 220 | 221 | function drawLine ( p1, p2, width = 1, color = "#FF0000" ) { 222 | context.beginPath(); 223 | context.moveTo(p1.x, p1.y); 224 | context.lineTo(p2.x, p2.y); 225 | context.lineWidth = width; 226 | context.strokeStyle = color; 227 | context.stroke(); 228 | context.closePath(); 229 | } 230 | 231 | function drawLayers ( clear ) { 232 | if(clear) 233 | context.clearRect(0, 0, scoped.domElement.width, scoped.domElement.height); 234 | 235 | // For each layer, draw the axis 236 | for(let i = 0, length = axes.length; i < length; i ++) { 237 | const axis = axes[i]; 238 | 239 | // Set the color 240 | const highlight = selectedAxis === axis 241 | const color = (axis.position.z >= -0.01) 242 | ? axis.color[0] 243 | : axis.color[1]; 244 | 245 | // Draw the line that connects it to the center if enabled 246 | if (axis.line) 247 | drawLine(center, axis.position, axis.line, color); 248 | 249 | // Draw the circle for the axis 250 | drawCircle(axis.position, axis.size, highlight ? "#FFFFFF" : color); 251 | 252 | // Write the axis label (X,Y,Z) if provided 253 | if (axis.label) { 254 | context.font = [options.fontWeight, options.fontSize, options.fontFamily].join(" "); 255 | context.fillStyle = options.fontColor; 256 | context.textBaseline = 'middle'; 257 | context.textAlign = 'center'; 258 | context.fillText(axis.label, axis.position.x, axis.position.y); 259 | } 260 | } 261 | } 262 | 263 | function setAxisPosition ( axis ) { 264 | const position = axis.direction.clone().applyMatrix4(invRotMat) 265 | const size = axis.size; 266 | axis.position.set( 267 | (position.x * (center.x - (size / 2) - options.padding)) + center.x, 268 | center.y - (position.y * (center.y - (size / 2) - options.padding)), 269 | position.z 270 | ); 271 | } 272 | 273 | // Initialization 274 | this.domElement = createCanvas(); 275 | this.update(); 276 | } 277 | 278 | } 279 | -------------------------------------------------------------------------------- /OrbitControlsGizmo.js: -------------------------------------------------------------------------------- 1 | import { 2 | Vector2, 3 | Vector3, 4 | Matrix4 5 | } from "https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.module.js"; 6 | 7 | class OrbitControlsGizmo { 8 | constructor(orbitControls, options) { 9 | 10 | options = Object.assign({ 11 | size: 90, 12 | padding: 8, 13 | bubbleSizePrimary: 8, 14 | bubbleSizeSecondary: 6, 15 | lineWidth: 2, 16 | fontSize: "12px", 17 | fontFamily: "arial", 18 | fontWeight: "bold", 19 | fontColor: "#222222", 20 | className: "obit-controls-gizmo", 21 | colors: { 22 | x: ["#f73c3c", "#942424"], 23 | y: ["#6ccb26", "#417a17"], 24 | z: ["#178cf0", "#0e5490"], 25 | } 26 | }, options); 27 | 28 | this.lock = false; 29 | this.lockX = false; 30 | this.lockY = false; 31 | 32 | this.update = () => { 33 | if(this.lock) 34 | return; 35 | 36 | camera.updateMatrix(); 37 | invRotMat.extractRotation(camera.matrix).invert(); 38 | 39 | for (let i = 0, length = axes.length; i < length; i++) 40 | setAxisPosition(axes[i], invRotMat); 41 | 42 | // Sort the layers where the +Z position is last so its drawn on top of anything below it 43 | axes.sort((a, b) => (a.position.z > b.position.z) ? 1 : -1); 44 | 45 | // Draw the layers 46 | drawLayers(true); 47 | 48 | } 49 | 50 | this.dispose = () => { 51 | orbit.removeEventListener("change", this.update); 52 | orbit.removeEventListener("start", () => this.domElement.classList.add("inactive")); 53 | orbit.removeEventListener("end", () => this.domElement.classList.remove("inactive")); 54 | 55 | this.domElement.removeEventListener('pointerdown', onPointerDown, false); 56 | this.domElement.removeEventListener('pointerenter', onPointerEnter, false); 57 | this.domElement.removeEventListener('pointermove', onPointerMove, false); 58 | this.domElement.removeEventListener('click', onMouseClick, false); 59 | window.removeEventListener('pointermove', onDrag, false); 60 | window.removeEventListener('pointerup', onPointerUp, false); 61 | this.domElement.remove(); 62 | } 63 | 64 | // Internals 65 | const scoped = this; 66 | const orbit = orbitControls; 67 | const camera = orbitControls.object; 68 | const invRotMat = new Matrix4(); 69 | const mouse = new Vector3(); 70 | const rotateStart = new Vector2(); 71 | const rotateEnd = new Vector2(); 72 | const rotateDelta = new Vector2(); 73 | const center = new Vector3(options.size / 2, options.size / 2, 0); 74 | const axes = createAxes(); 75 | let selectedAxis = null; 76 | let isDragging = false; 77 | let context; 78 | let rect; 79 | let orbitState; 80 | 81 | orbit.addEventListener("change", this.update); 82 | orbit.addEventListener("start", () => this.domElement.classList.add("inactive")); 83 | orbit.addEventListener("end", () => this.domElement.classList.remove("inactive")); 84 | 85 | function createAxes () { 86 | // Generate list of axes 87 | const colors = options.colors; 88 | const line = options.lineWidth; 89 | const size = { 90 | primary: options.bubbleSizePrimary, 91 | secondary: options.bubbleSizeSecondary, 92 | } 93 | return [ 94 | { axis: "x", direction: new Vector3(1, 0, 0), size: size.primary, color: colors.x, line, label: "X", position: new Vector3(0, 0, 0) }, 95 | { axis: "y", direction: new Vector3(0, 1, 0), size: size.primary, color: colors.y, line, label: "Y", position: new Vector3(0, 0, 0) }, 96 | { axis: "z", direction: new Vector3(0, 0, 1), size: size.primary, color: colors.z, line, label: "Z", position: new Vector3(0, 0, 0) }, 97 | { axis: "-x", direction: new Vector3(-1, 0, 0), size: size.secondary, color: colors.x, position: new Vector3(0, 0, 0) }, 98 | { axis: "-y", direction: new Vector3(0, -1, 0), size: size.secondary, color: colors.y, position: new Vector3(0, 0, 0) }, 99 | { axis: "-z", direction: new Vector3(0, 0, -1), size: size.secondary, color: colors.z, position: new Vector3(0, 0, 0) }, 100 | ]; 101 | } 102 | 103 | function createCanvas () { 104 | const canvas = document.createElement('canvas'); 105 | canvas.width = options.size; 106 | canvas.height = options.size; 107 | canvas.classList.add(options.className); 108 | 109 | canvas.addEventListener('pointerdown', onPointerDown, false); 110 | canvas.addEventListener('pointerenter', onPointerEnter, false); 111 | canvas.addEventListener('pointermove', onPointerMove, false); 112 | canvas.addEventListener('click', onMouseClick, false); 113 | 114 | context = canvas.getContext("2d"); 115 | 116 | return canvas; 117 | } 118 | 119 | function onPointerDown ( e ) { 120 | rotateStart.set( e.clientX, e.clientY ); 121 | orbitState = orbit.enabled; 122 | orbit.enabled = false; 123 | window.addEventListener('pointermove', onDrag, false); 124 | window.addEventListener('pointerup', onPointerUp, false); 125 | } 126 | 127 | function onPointerUp () { 128 | setTimeout(() => isDragging = false, 0); 129 | scoped.domElement.classList.remove("dragging"); 130 | orbit.enabled = orbitState; 131 | window.removeEventListener('pointermove', onDrag, false); 132 | window.removeEventListener('pointerup', onPointerUp, false); 133 | } 134 | 135 | function onPointerEnter () { 136 | rect = scoped.domElement.getBoundingClientRect(); 137 | } 138 | 139 | function onPointerMove ( e ) { 140 | if(isDragging || scoped.lock) 141 | return; 142 | 143 | const currentAxis = selectedAxis; 144 | 145 | selectedAxis = null; 146 | if(e) 147 | mouse.set(e.clientX - rect.left, e.clientY - rect.top, 0); 148 | 149 | // Loop through each layer 150 | for (let i = 0, length = axes.length; i < length; i++) { 151 | const distance = mouse.distanceTo(axes[i].position); 152 | 153 | if (distance < axes[i].size) 154 | selectedAxis = axes[i]; 155 | } 156 | 157 | if(currentAxis !== selectedAxis) 158 | drawLayers(); 159 | } 160 | 161 | function onDrag ( e ) { 162 | if(scoped.lock) 163 | return; 164 | 165 | if(!isDragging) 166 | scoped.domElement.classList.add("dragging"); 167 | 168 | isDragging = true; 169 | 170 | selectedAxis = null; 171 | 172 | rotateEnd.set( e.clientX, e.clientY ); 173 | 174 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( 0.5 ); 175 | 176 | if(!scoped.lockX) 177 | orbit.rotateLeft( 2 * Math.PI * rotateDelta.x / scoped.domElement.height ); 178 | 179 | if(!scoped.lockY) 180 | orbit.rotateUp( 2 * Math.PI * rotateDelta.y / scoped.domElement.height ); 181 | 182 | rotateStart.copy( rotateEnd ); 183 | 184 | orbit.update(); 185 | } 186 | 187 | function onMouseClick () { 188 | //FIXME Don't like the current animation 189 | if(isDragging || !selectedAxis) 190 | return; 191 | 192 | const vec = selectedAxis.direction.clone(); 193 | const distance = camera.position.distanceTo(orbit.target); 194 | vec.multiplyScalar(distance); 195 | 196 | const duration = 400; 197 | const start = performance.now(); 198 | const maxAlpha = 1; 199 | function loop () { 200 | const now = performance.now(); 201 | const delta = now - start; 202 | const alpha = Math.min(delta / duration, maxAlpha); 203 | camera.position.lerp(vec, alpha); 204 | orbit.update(); 205 | 206 | if(alpha !== maxAlpha) 207 | return requestAnimationFrame(loop) 208 | 209 | onPointerMove(); 210 | 211 | } 212 | 213 | loop(); 214 | 215 | 216 | selectedAxis = null; 217 | } 218 | 219 | function drawCircle ( p, radius = 10, color = "#FF0000" ) { 220 | context.beginPath(); 221 | context.arc(p.x, p.y, radius, 0, 2 * Math.PI, false); 222 | context.fillStyle = color; 223 | context.fill(); 224 | context.closePath(); 225 | } 226 | 227 | function drawLine ( p1, p2, width = 1, color = "#FF0000" ) { 228 | context.beginPath(); 229 | context.moveTo(p1.x, p1.y); 230 | context.lineTo(p2.x, p2.y); 231 | context.lineWidth = width; 232 | context.strokeStyle = color; 233 | context.stroke(); 234 | context.closePath(); 235 | } 236 | 237 | function drawLayers ( clear ) { 238 | if(clear) 239 | context.clearRect(0, 0, scoped.domElement.width, scoped.domElement.height); 240 | 241 | // For each layer, draw the axis 242 | for(let i = 0, length = axes.length; i < length; i ++) { 243 | const axis = axes[i]; 244 | 245 | // Set the color 246 | const highlight = selectedAxis === axis 247 | const color = (axis.position.z >= -0.01) 248 | ? axis.color[0] 249 | : axis.color[1]; 250 | 251 | // Draw the line that connects it to the center if enabled 252 | if (axis.line) 253 | drawLine(center, axis.position, axis.line, color); 254 | 255 | // Draw the circle for the axis 256 | drawCircle(axis.position, axis.size, highlight ? "#FFFFFF" : color); 257 | 258 | // Write the axis label (X,Y,Z) if provided 259 | if (axis.label) { 260 | context.font = [options.fontWeight, options.fontSize, options.fontFamily].join(" "); 261 | context.fillStyle = options.fontColor; 262 | context.textBaseline = 'middle'; 263 | context.textAlign = 'center'; 264 | context.fillText(axis.label, axis.position.x, axis.position.y); 265 | } 266 | } 267 | } 268 | 269 | function setAxisPosition ( axis ) { 270 | const position = axis.direction.clone().applyMatrix4(invRotMat) 271 | const size = axis.size; 272 | axis.position.set( 273 | (position.x * (center.x - (size / 2) - options.padding)) + center.x, 274 | center.y - (position.y * (center.y - (size / 2) - options.padding)), 275 | position.z 276 | ); 277 | } 278 | 279 | // Initialization 280 | this.domElement = createCanvas(); 281 | this.update(); 282 | } 283 | 284 | } 285 | 286 | export { OrbitControlsGizmo } -------------------------------------------------------------------------------- /non-module/OrbitControls.js: -------------------------------------------------------------------------------- 1 | ( function () { 2 | 3 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 4 | // 5 | // Orbit - left mouse / touch: one-finger move 6 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish 7 | // Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move 8 | 9 | const _changeEvent = { 10 | type: 'change' 11 | }; 12 | const _startEvent = { 13 | type: 'start' 14 | }; 15 | const _endEvent = { 16 | type: 'end' 17 | }; 18 | 19 | class OrbitControls extends THREE.EventDispatcher { 20 | 21 | constructor( object, domElement ) { 22 | 23 | super(); 24 | if ( domElement === undefined ) console.warn( 'THREE.OrbitControls: The second parameter "domElement" is now mandatory.' ); 25 | if ( domElement === document ) console.error( 'THREE.OrbitControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.' ); 26 | this.object = object; 27 | this.domElement = domElement; 28 | this.domElement.style.touchAction = 'none'; // disable touch scroll 29 | // Set to false to disable this control 30 | 31 | this.enabled = true; // "target" sets the location of focus, where the object orbits around 32 | 33 | this.target = new THREE.Vector3(); // How far you can dolly in and out ( PerspectiveCamera only ) 34 | 35 | this.minDistance = 0; 36 | this.maxDistance = Infinity; // How far you can zoom in and out ( OrthographicCamera only ) 37 | 38 | this.minZoom = 0; 39 | this.maxZoom = Infinity; // How far you can orbit vertically, upper and lower limits. 40 | // Range is 0 to Math.PI radians. 41 | 42 | this.minPolarAngle = 0; // radians 43 | 44 | this.maxPolarAngle = Math.PI; // radians 45 | // How far you can orbit horizontally, upper and lower limits. 46 | // If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI ) 47 | 48 | this.minAzimuthAngle = - Infinity; // radians 49 | 50 | this.maxAzimuthAngle = Infinity; // radians 51 | // Set to true to enable damping (inertia) 52 | // If damping is enabled, you must call controls.update() in your animation loop 53 | 54 | this.enableDamping = false; 55 | this.dampingFactor = 0.05; // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. 56 | // Set to false to disable zooming 57 | 58 | this.enableZoom = true; 59 | this.zoomSpeed = 1.0; // Set to false to disable rotating 60 | 61 | this.enableRotate = true; 62 | this.rotateSpeed = 1.0; // Set to false to disable panning 63 | 64 | this.enablePan = true; 65 | this.panSpeed = 1.0; 66 | this.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.up 67 | 68 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push 69 | // Set to true to automatically rotate around the target 70 | // If auto-rotate is enabled, you must call controls.update() in your animation loop 71 | 72 | this.autoRotate = false; 73 | this.autoRotateSpeed = 2.0; // 30 seconds per orbit when fps is 60 74 | // The four arrow keys 75 | 76 | this.keys = { 77 | LEFT: 'ArrowLeft', 78 | UP: 'ArrowUp', 79 | RIGHT: 'ArrowRight', 80 | BOTTOM: 'ArrowDown' 81 | }; // Mouse buttons 82 | 83 | this.mouseButtons = { 84 | LEFT: THREE.MOUSE.ROTATE, 85 | MIDDLE: THREE.MOUSE.DOLLY, 86 | RIGHT: THREE.MOUSE.PAN 87 | }; // Touch fingers 88 | 89 | this.touches = { 90 | ONE: THREE.TOUCH.ROTATE, 91 | TWO: THREE.TOUCH.DOLLY_PAN 92 | }; // for reset 93 | 94 | this.target0 = this.target.clone(); 95 | this.position0 = this.object.position.clone(); 96 | this.zoom0 = this.object.zoom; // the target DOM element for key events 97 | 98 | this._domElementKeyEvents = null; // 99 | // public methods 100 | // 101 | 102 | this.getPolarAngle = function () { 103 | 104 | return spherical.phi; 105 | 106 | }; 107 | 108 | this.getAzimuthalAngle = function () { 109 | 110 | return spherical.theta; 111 | 112 | }; 113 | 114 | this.listenToKeyEvents = function ( domElement ) { 115 | 116 | domElement.addEventListener( 'keydown', onKeyDown ); 117 | this._domElementKeyEvents = domElement; 118 | 119 | }; 120 | 121 | this.saveState = function () { 122 | 123 | scope.target0.copy( scope.target ); 124 | scope.position0.copy( scope.object.position ); 125 | scope.zoom0 = scope.object.zoom; 126 | 127 | }; 128 | 129 | this.reset = function () { 130 | 131 | scope.target.copy( scope.target0 ); 132 | scope.object.position.copy( scope.position0 ); 133 | scope.object.zoom = scope.zoom0; 134 | scope.object.updateProjectionMatrix(); 135 | scope.dispatchEvent( _changeEvent ); 136 | scope.update(); 137 | state = STATE.NONE; 138 | 139 | }; // this method is exposed, but perhaps it would be better if we can make it private... 140 | 141 | 142 | this.update = function () { 143 | 144 | const offset = new THREE.Vector3(); // so camera.up is the orbit axis 145 | 146 | const quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) ); 147 | const quatInverse = quat.clone().invert(); 148 | const lastPosition = new THREE.Vector3(); 149 | const lastQuaternion = new THREE.Quaternion(); 150 | const twoPI = 2 * Math.PI; 151 | return function update() { 152 | 153 | const position = scope.object.position; 154 | offset.copy( position ).sub( scope.target ); // rotate offset to "y-axis-is-up" space 155 | 156 | offset.applyQuaternion( quat ); // angle from z-axis around y-axis 157 | 158 | spherical.setFromVector3( offset ); 159 | 160 | if ( scope.autoRotate && state === STATE.NONE ) { 161 | 162 | rotateLeft( getAutoRotationAngle() ); 163 | 164 | } 165 | 166 | if ( scope.enableDamping ) { 167 | 168 | spherical.theta += sphericalDelta.theta * scope.dampingFactor; 169 | spherical.phi += sphericalDelta.phi * scope.dampingFactor; 170 | 171 | } else { 172 | 173 | spherical.theta += sphericalDelta.theta; 174 | spherical.phi += sphericalDelta.phi; 175 | 176 | } // restrict theta to be between desired limits 177 | 178 | 179 | let min = scope.minAzimuthAngle; 180 | let max = scope.maxAzimuthAngle; 181 | 182 | if ( isFinite( min ) && isFinite( max ) ) { 183 | 184 | if ( min < - Math.PI ) min += twoPI; else if ( min > Math.PI ) min -= twoPI; 185 | if ( max < - Math.PI ) max += twoPI; else if ( max > Math.PI ) max -= twoPI; 186 | 187 | if ( min <= max ) { 188 | 189 | spherical.theta = Math.max( min, Math.min( max, spherical.theta ) ); 190 | 191 | } else { 192 | 193 | spherical.theta = spherical.theta > ( min + max ) / 2 ? Math.max( min, spherical.theta ) : Math.min( max, spherical.theta ); 194 | 195 | } 196 | 197 | } // restrict phi to be between desired limits 198 | 199 | 200 | spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) ); 201 | spherical.makeSafe(); 202 | spherical.radius *= scale; // restrict radius to be between desired limits 203 | 204 | spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) ); // move target to panned location 205 | 206 | if ( scope.enableDamping === true ) { 207 | 208 | scope.target.addScaledVector( panOffset, scope.dampingFactor ); 209 | 210 | } else { 211 | 212 | scope.target.add( panOffset ); 213 | 214 | } 215 | 216 | offset.setFromSpherical( spherical ); // rotate offset back to "camera-up-vector-is-up" space 217 | 218 | offset.applyQuaternion( quatInverse ); 219 | position.copy( scope.target ).add( offset ); 220 | scope.object.lookAt( scope.target ); 221 | 222 | if ( scope.enableDamping === true ) { 223 | 224 | sphericalDelta.theta *= 1 - scope.dampingFactor; 225 | sphericalDelta.phi *= 1 - scope.dampingFactor; 226 | panOffset.multiplyScalar( 1 - scope.dampingFactor ); 227 | 228 | } else { 229 | 230 | sphericalDelta.set( 0, 0, 0 ); 231 | panOffset.set( 0, 0, 0 ); 232 | 233 | } 234 | 235 | scale = 1; // update condition is: 236 | // min(camera displacement, camera rotation in radians)^2 > EPS 237 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8 238 | 239 | if ( zoomChanged || lastPosition.distanceToSquared( scope.object.position ) > EPS || 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) { 240 | 241 | scope.dispatchEvent( _changeEvent ); 242 | lastPosition.copy( scope.object.position ); 243 | lastQuaternion.copy( scope.object.quaternion ); 244 | zoomChanged = false; 245 | return true; 246 | 247 | } 248 | 249 | return false; 250 | 251 | }; 252 | 253 | }(); 254 | 255 | this.dispose = function () { 256 | 257 | scope.domElement.removeEventListener( 'contextmenu', onContextMenu ); 258 | scope.domElement.removeEventListener( 'pointerdown', onPointerDown ); 259 | scope.domElement.removeEventListener( 'pointercancel', onPointerCancel ); 260 | scope.domElement.removeEventListener( 'wheel', onMouseWheel ); 261 | scope.domElement.ownerDocument.removeEventListener( 'pointermove', onPointerMove ); 262 | scope.domElement.ownerDocument.removeEventListener( 'pointerup', onPointerUp ); 263 | 264 | if ( scope._domElementKeyEvents !== null ) { 265 | 266 | scope._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown ); 267 | 268 | } //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? 269 | 270 | }; // 271 | // internals 272 | // 273 | 274 | 275 | const scope = this; 276 | const STATE = { 277 | NONE: - 1, 278 | ROTATE: 0, 279 | DOLLY: 1, 280 | PAN: 2, 281 | TOUCH_ROTATE: 3, 282 | TOUCH_PAN: 4, 283 | TOUCH_DOLLY_PAN: 5, 284 | TOUCH_DOLLY_ROTATE: 6 285 | }; 286 | let state = STATE.NONE; 287 | const EPS = 0.000001; // current position in spherical coordinates 288 | 289 | const spherical = new THREE.Spherical(); 290 | const sphericalDelta = new THREE.Spherical(); 291 | let scale = 1; 292 | const panOffset = new THREE.Vector3(); 293 | let zoomChanged = false; 294 | const rotateStart = new THREE.Vector2(); 295 | const rotateEnd = new THREE.Vector2(); 296 | const rotateDelta = new THREE.Vector2(); 297 | const panStart = new THREE.Vector2(); 298 | const panEnd = new THREE.Vector2(); 299 | const panDelta = new THREE.Vector2(); 300 | const dollyStart = new THREE.Vector2(); 301 | const dollyEnd = new THREE.Vector2(); 302 | const dollyDelta = new THREE.Vector2(); 303 | const pointers = []; 304 | const pointerPositions = {}; 305 | 306 | function getAutoRotationAngle() { 307 | 308 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; 309 | 310 | } 311 | 312 | function getZoomScale() { 313 | 314 | return Math.pow( 0.95, scope.zoomSpeed ); 315 | 316 | } 317 | 318 | function rotateLeft( angle ) { 319 | 320 | sphericalDelta.theta -= angle; 321 | 322 | } 323 | 324 | function rotateUp( angle ) { 325 | 326 | sphericalDelta.phi -= angle; 327 | 328 | } 329 | 330 | // Exposing rotateLeft and rotateUp 331 | this.rotateLeft = rotateLeft; 332 | this.rotateUp = rotateUp; 333 | 334 | const panLeft = function () { 335 | 336 | const v = new THREE.Vector3(); 337 | return function panLeft( distance, objectMatrix ) { 338 | 339 | v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix 340 | 341 | v.multiplyScalar( - distance ); 342 | panOffset.add( v ); 343 | 344 | }; 345 | 346 | }(); 347 | 348 | const panUp = function () { 349 | 350 | const v = new THREE.Vector3(); 351 | return function panUp( distance, objectMatrix ) { 352 | 353 | if ( scope.screenSpacePanning === true ) { 354 | 355 | v.setFromMatrixColumn( objectMatrix, 1 ); 356 | 357 | } else { 358 | 359 | v.setFromMatrixColumn( objectMatrix, 0 ); 360 | v.crossVectors( scope.object.up, v ); 361 | 362 | } 363 | 364 | v.multiplyScalar( distance ); 365 | panOffset.add( v ); 366 | 367 | }; 368 | 369 | }(); // deltaX and deltaY are in pixels; right and down are positive 370 | 371 | 372 | const pan = function () { 373 | 374 | const offset = new THREE.Vector3(); 375 | return function pan( deltaX, deltaY ) { 376 | 377 | const element = scope.domElement; 378 | 379 | if ( scope.object.isPerspectiveCamera ) { 380 | 381 | // perspective 382 | const position = scope.object.position; 383 | offset.copy( position ).sub( scope.target ); 384 | let targetDistance = offset.length(); // half of the fov is center to top of screen 385 | 386 | targetDistance *= Math.tan( scope.object.fov / 2 * Math.PI / 180.0 ); // we use only clientHeight here so aspect ratio does not distort speed 387 | 388 | panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix ); 389 | panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix ); 390 | 391 | } else if ( scope.object.isOrthographicCamera ) { 392 | 393 | // orthographic 394 | panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix ); 395 | panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix ); 396 | 397 | } else { 398 | 399 | // camera neither orthographic nor perspective 400 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); 401 | scope.enablePan = false; 402 | 403 | } 404 | 405 | }; 406 | 407 | }(); 408 | 409 | function dollyOut( dollyScale ) { 410 | 411 | if ( scope.object.isPerspectiveCamera ) { 412 | 413 | scale /= dollyScale; 414 | 415 | } else if ( scope.object.isOrthographicCamera ) { 416 | 417 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) ); 418 | scope.object.updateProjectionMatrix(); 419 | zoomChanged = true; 420 | 421 | } else { 422 | 423 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 424 | scope.enableZoom = false; 425 | 426 | } 427 | 428 | } 429 | 430 | function dollyIn( dollyScale ) { 431 | 432 | if ( scope.object.isPerspectiveCamera ) { 433 | 434 | scale *= dollyScale; 435 | 436 | } else if ( scope.object.isOrthographicCamera ) { 437 | 438 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) ); 439 | scope.object.updateProjectionMatrix(); 440 | zoomChanged = true; 441 | 442 | } else { 443 | 444 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 445 | scope.enableZoom = false; 446 | 447 | } 448 | 449 | } // 450 | // event callbacks - update the object state 451 | // 452 | 453 | 454 | function handleMouseDownRotate( event ) { 455 | 456 | rotateStart.set( event.clientX, event.clientY ); 457 | 458 | } 459 | 460 | function handleMouseDownDolly( event ) { 461 | 462 | dollyStart.set( event.clientX, event.clientY ); 463 | 464 | } 465 | 466 | function handleMouseDownPan( event ) { 467 | 468 | panStart.set( event.clientX, event.clientY ); 469 | 470 | } 471 | 472 | function handleMouseMoveRotate( event ) { 473 | 474 | rotateEnd.set( event.clientX, event.clientY ); 475 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); 476 | const element = scope.domElement; 477 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height 478 | 479 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); 480 | rotateStart.copy( rotateEnd ); 481 | scope.update(); 482 | 483 | } 484 | 485 | function handleMouseMoveDolly( event ) { 486 | 487 | dollyEnd.set( event.clientX, event.clientY ); 488 | dollyDelta.subVectors( dollyEnd, dollyStart ); 489 | 490 | if ( dollyDelta.y > 0 ) { 491 | 492 | dollyOut( getZoomScale() ); 493 | 494 | } else if ( dollyDelta.y < 0 ) { 495 | 496 | dollyIn( getZoomScale() ); 497 | 498 | } 499 | 500 | dollyStart.copy( dollyEnd ); 501 | scope.update(); 502 | 503 | } 504 | 505 | function handleMouseMovePan( event ) { 506 | 507 | panEnd.set( event.clientX, event.clientY ); 508 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); 509 | pan( panDelta.x, panDelta.y ); 510 | panStart.copy( panEnd ); 511 | scope.update(); 512 | 513 | } 514 | 515 | function handleMouseUp( ) { // no-op 516 | } 517 | 518 | function handleMouseWheel( event ) { 519 | 520 | if ( event.deltaY < 0 ) { 521 | 522 | dollyIn( getZoomScale() ); 523 | 524 | } else if ( event.deltaY > 0 ) { 525 | 526 | dollyOut( getZoomScale() ); 527 | 528 | } 529 | 530 | scope.update(); 531 | 532 | } 533 | 534 | function handleKeyDown( event ) { 535 | 536 | let needsUpdate = false; 537 | 538 | switch ( event.code ) { 539 | 540 | case scope.keys.UP: 541 | pan( 0, scope.keyPanSpeed ); 542 | needsUpdate = true; 543 | break; 544 | 545 | case scope.keys.BOTTOM: 546 | pan( 0, - scope.keyPanSpeed ); 547 | needsUpdate = true; 548 | break; 549 | 550 | case scope.keys.LEFT: 551 | pan( scope.keyPanSpeed, 0 ); 552 | needsUpdate = true; 553 | break; 554 | 555 | case scope.keys.RIGHT: 556 | pan( - scope.keyPanSpeed, 0 ); 557 | needsUpdate = true; 558 | break; 559 | 560 | } 561 | 562 | if ( needsUpdate ) { 563 | 564 | // prevent the browser from scrolling on cursor keys 565 | event.preventDefault(); 566 | scope.update(); 567 | 568 | } 569 | 570 | } 571 | 572 | function handleTouchStartRotate() { 573 | 574 | if ( pointers.length === 1 ) { 575 | 576 | rotateStart.set( pointers[ 0 ].pageX, pointers[ 0 ].pageY ); 577 | 578 | } else { 579 | 580 | const x = 0.5 * ( pointers[ 0 ].pageX + pointers[ 1 ].pageX ); 581 | const y = 0.5 * ( pointers[ 0 ].pageY + pointers[ 1 ].pageY ); 582 | rotateStart.set( x, y ); 583 | 584 | } 585 | 586 | } 587 | 588 | function handleTouchStartPan() { 589 | 590 | if ( pointers.length === 1 ) { 591 | 592 | panStart.set( pointers[ 0 ].pageX, pointers[ 0 ].pageY ); 593 | 594 | } else { 595 | 596 | const x = 0.5 * ( pointers[ 0 ].pageX + pointers[ 1 ].pageX ); 597 | const y = 0.5 * ( pointers[ 0 ].pageY + pointers[ 1 ].pageY ); 598 | panStart.set( x, y ); 599 | 600 | } 601 | 602 | } 603 | 604 | function handleTouchStartDolly() { 605 | 606 | const dx = pointers[ 0 ].pageX - pointers[ 1 ].pageX; 607 | const dy = pointers[ 0 ].pageY - pointers[ 1 ].pageY; 608 | const distance = Math.sqrt( dx * dx + dy * dy ); 609 | dollyStart.set( 0, distance ); 610 | 611 | } 612 | 613 | function handleTouchStartDollyPan() { 614 | 615 | if ( scope.enableZoom ) handleTouchStartDolly(); 616 | if ( scope.enablePan ) handleTouchStartPan(); 617 | 618 | } 619 | 620 | function handleTouchStartDollyRotate() { 621 | 622 | if ( scope.enableZoom ) handleTouchStartDolly(); 623 | if ( scope.enableRotate ) handleTouchStartRotate(); 624 | 625 | } 626 | 627 | function handleTouchMoveRotate( event ) { 628 | 629 | if ( pointers.length == 1 ) { 630 | 631 | rotateEnd.set( event.pageX, event.pageY ); 632 | 633 | } else { 634 | 635 | const position = getSecondPointerPosition( event ); 636 | const x = 0.5 * ( event.pageX + position.x ); 637 | const y = 0.5 * ( event.pageY + position.y ); 638 | rotateEnd.set( x, y ); 639 | 640 | } 641 | 642 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); 643 | const element = scope.domElement; 644 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height 645 | 646 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); 647 | rotateStart.copy( rotateEnd ); 648 | 649 | } 650 | 651 | function handleTouchMovePan( event ) { 652 | 653 | if ( pointers.length === 1 ) { 654 | 655 | panEnd.set( event.pageX, event.pageY ); 656 | 657 | } else { 658 | 659 | const position = getSecondPointerPosition( event ); 660 | const x = 0.5 * ( event.pageX + position.x ); 661 | const y = 0.5 * ( event.pageY + position.y ); 662 | panEnd.set( x, y ); 663 | 664 | } 665 | 666 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); 667 | pan( panDelta.x, panDelta.y ); 668 | panStart.copy( panEnd ); 669 | 670 | } 671 | 672 | function handleTouchMoveDolly( event ) { 673 | 674 | const position = getSecondPointerPosition( event ); 675 | const dx = event.pageX - position.x; 676 | const dy = event.pageY - position.y; 677 | const distance = Math.sqrt( dx * dx + dy * dy ); 678 | dollyEnd.set( 0, distance ); 679 | dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) ); 680 | dollyOut( dollyDelta.y ); 681 | dollyStart.copy( dollyEnd ); 682 | 683 | } 684 | 685 | function handleTouchMoveDollyPan( event ) { 686 | 687 | if ( scope.enableZoom ) handleTouchMoveDolly( event ); 688 | if ( scope.enablePan ) handleTouchMovePan( event ); 689 | 690 | } 691 | 692 | function handleTouchMoveDollyRotate( event ) { 693 | 694 | if ( scope.enableZoom ) handleTouchMoveDolly( event ); 695 | if ( scope.enableRotate ) handleTouchMoveRotate( event ); 696 | 697 | } 698 | 699 | function handleTouchEnd( ) { // no-op 700 | } // 701 | // event handlers - FSM: listen for events and reset state 702 | // 703 | 704 | 705 | function onPointerDown( event ) { 706 | 707 | if ( scope.enabled === false ) return; 708 | 709 | if ( pointers.length === 0 ) { 710 | 711 | scope.domElement.ownerDocument.addEventListener( 'pointermove', onPointerMove ); 712 | scope.domElement.ownerDocument.addEventListener( 'pointerup', onPointerUp ); 713 | 714 | } // 715 | 716 | 717 | addPointer( event ); 718 | 719 | if ( event.pointerType === 'touch' ) { 720 | 721 | onTouchStart( event ); 722 | 723 | } else { 724 | 725 | onMouseDown( event ); 726 | 727 | } 728 | 729 | } 730 | 731 | function onPointerMove( event ) { 732 | 733 | if ( scope.enabled === false ) return; 734 | 735 | if ( event.pointerType === 'touch' ) { 736 | 737 | onTouchMove( event ); 738 | 739 | } else { 740 | 741 | onMouseMove( event ); 742 | 743 | } 744 | 745 | } 746 | 747 | function onPointerUp( event ) { 748 | 749 | if ( scope.enabled === false ) return; 750 | 751 | if ( event.pointerType === 'touch' ) { 752 | 753 | onTouchEnd(); 754 | 755 | } else { 756 | 757 | onMouseUp( event ); 758 | 759 | } 760 | 761 | removePointer( event ); // 762 | 763 | if ( pointers.length === 0 ) { 764 | 765 | scope.domElement.ownerDocument.removeEventListener( 'pointermove', onPointerMove ); 766 | scope.domElement.ownerDocument.removeEventListener( 'pointerup', onPointerUp ); 767 | 768 | } 769 | 770 | } 771 | 772 | function onPointerCancel( event ) { 773 | 774 | removePointer( event ); 775 | 776 | } 777 | 778 | function onMouseDown( event ) { 779 | 780 | let mouseAction; 781 | 782 | switch ( event.button ) { 783 | 784 | case 0: 785 | mouseAction = scope.mouseButtons.LEFT; 786 | break; 787 | 788 | case 1: 789 | mouseAction = scope.mouseButtons.MIDDLE; 790 | break; 791 | 792 | case 2: 793 | mouseAction = scope.mouseButtons.RIGHT; 794 | break; 795 | 796 | default: 797 | mouseAction = - 1; 798 | 799 | } 800 | 801 | switch ( mouseAction ) { 802 | 803 | case THREE.MOUSE.DOLLY: 804 | if ( scope.enableZoom === false ) return; 805 | handleMouseDownDolly( event ); 806 | state = STATE.DOLLY; 807 | break; 808 | 809 | case THREE.MOUSE.ROTATE: 810 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 811 | 812 | if ( scope.enablePan === false ) return; 813 | handleMouseDownPan( event ); 814 | state = STATE.PAN; 815 | 816 | } else { 817 | 818 | if ( scope.enableRotate === false ) return; 819 | handleMouseDownRotate( event ); 820 | state = STATE.ROTATE; 821 | 822 | } 823 | 824 | break; 825 | 826 | case THREE.MOUSE.PAN: 827 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 828 | 829 | if ( scope.enableRotate === false ) return; 830 | handleMouseDownRotate( event ); 831 | state = STATE.ROTATE; 832 | 833 | } else { 834 | 835 | if ( scope.enablePan === false ) return; 836 | handleMouseDownPan( event ); 837 | state = STATE.PAN; 838 | 839 | } 840 | 841 | break; 842 | 843 | default: 844 | state = STATE.NONE; 845 | 846 | } 847 | 848 | if ( state !== STATE.NONE ) { 849 | 850 | scope.dispatchEvent( _startEvent ); 851 | 852 | } 853 | 854 | } 855 | 856 | function onMouseMove( event ) { 857 | 858 | if ( scope.enabled === false ) return; 859 | 860 | switch ( state ) { 861 | 862 | case STATE.ROTATE: 863 | if ( scope.enableRotate === false ) return; 864 | handleMouseMoveRotate( event ); 865 | break; 866 | 867 | case STATE.DOLLY: 868 | if ( scope.enableZoom === false ) return; 869 | handleMouseMoveDolly( event ); 870 | break; 871 | 872 | case STATE.PAN: 873 | if ( scope.enablePan === false ) return; 874 | handleMouseMovePan( event ); 875 | break; 876 | 877 | } 878 | 879 | } 880 | 881 | function onMouseUp( event ) { 882 | 883 | handleMouseUp( event ); 884 | scope.dispatchEvent( _endEvent ); 885 | state = STATE.NONE; 886 | 887 | } 888 | 889 | function onMouseWheel( event ) { 890 | 891 | if ( scope.enabled === false || scope.enableZoom === false || state !== STATE.NONE && state !== STATE.ROTATE ) return; 892 | event.preventDefault(); 893 | scope.dispatchEvent( _startEvent ); 894 | handleMouseWheel( event ); 895 | scope.dispatchEvent( _endEvent ); 896 | 897 | } 898 | 899 | function onKeyDown( event ) { 900 | 901 | if ( scope.enabled === false || scope.enablePan === false ) return; 902 | handleKeyDown( event ); 903 | 904 | } 905 | 906 | function onTouchStart( event ) { 907 | 908 | trackPointer( event ); 909 | 910 | switch ( pointers.length ) { 911 | 912 | case 1: 913 | switch ( scope.touches.ONE ) { 914 | 915 | case THREE.TOUCH.ROTATE: 916 | if ( scope.enableRotate === false ) return; 917 | handleTouchStartRotate(); 918 | state = STATE.TOUCH_ROTATE; 919 | break; 920 | 921 | case THREE.TOUCH.PAN: 922 | if ( scope.enablePan === false ) return; 923 | handleTouchStartPan(); 924 | state = STATE.TOUCH_PAN; 925 | break; 926 | 927 | default: 928 | state = STATE.NONE; 929 | 930 | } 931 | 932 | break; 933 | 934 | case 2: 935 | switch ( scope.touches.TWO ) { 936 | 937 | case THREE.TOUCH.DOLLY_PAN: 938 | if ( scope.enableZoom === false && scope.enablePan === false ) return; 939 | handleTouchStartDollyPan(); 940 | state = STATE.TOUCH_DOLLY_PAN; 941 | break; 942 | 943 | case THREE.TOUCH.DOLLY_ROTATE: 944 | if ( scope.enableZoom === false && scope.enableRotate === false ) return; 945 | handleTouchStartDollyRotate(); 946 | state = STATE.TOUCH_DOLLY_ROTATE; 947 | break; 948 | 949 | default: 950 | state = STATE.NONE; 951 | 952 | } 953 | 954 | break; 955 | 956 | default: 957 | state = STATE.NONE; 958 | 959 | } 960 | 961 | if ( state !== STATE.NONE ) { 962 | 963 | scope.dispatchEvent( _startEvent ); 964 | 965 | } 966 | 967 | } 968 | 969 | function onTouchMove( event ) { 970 | 971 | trackPointer( event ); 972 | 973 | switch ( state ) { 974 | 975 | case STATE.TOUCH_ROTATE: 976 | if ( scope.enableRotate === false ) return; 977 | handleTouchMoveRotate( event ); 978 | scope.update(); 979 | break; 980 | 981 | case STATE.TOUCH_PAN: 982 | if ( scope.enablePan === false ) return; 983 | handleTouchMovePan( event ); 984 | scope.update(); 985 | break; 986 | 987 | case STATE.TOUCH_DOLLY_PAN: 988 | if ( scope.enableZoom === false && scope.enablePan === false ) return; 989 | handleTouchMoveDollyPan( event ); 990 | scope.update(); 991 | break; 992 | 993 | case STATE.TOUCH_DOLLY_ROTATE: 994 | if ( scope.enableZoom === false && scope.enableRotate === false ) return; 995 | handleTouchMoveDollyRotate( event ); 996 | scope.update(); 997 | break; 998 | 999 | default: 1000 | state = STATE.NONE; 1001 | 1002 | } 1003 | 1004 | } 1005 | 1006 | function onTouchEnd( event ) { 1007 | 1008 | handleTouchEnd( event ); 1009 | scope.dispatchEvent( _endEvent ); 1010 | state = STATE.NONE; 1011 | 1012 | } 1013 | 1014 | function onContextMenu( event ) { 1015 | 1016 | if ( scope.enabled === false ) return; 1017 | event.preventDefault(); 1018 | 1019 | } 1020 | 1021 | function addPointer( event ) { 1022 | 1023 | pointers.push( event ); 1024 | 1025 | } 1026 | 1027 | function removePointer( event ) { 1028 | 1029 | delete pointerPositions[ event.pointerId ]; 1030 | 1031 | for ( let i = 0; i < pointers.length; i ++ ) { 1032 | 1033 | if ( pointers[ i ].pointerId == event.pointerId ) { 1034 | 1035 | pointers.splice( i, 1 ); 1036 | return; 1037 | 1038 | } 1039 | 1040 | } 1041 | 1042 | } 1043 | 1044 | function trackPointer( event ) { 1045 | 1046 | let position = pointerPositions[ event.pointerId ]; 1047 | 1048 | if ( position === undefined ) { 1049 | 1050 | position = new THREE.Vector2(); 1051 | pointerPositions[ event.pointerId ] = position; 1052 | 1053 | } 1054 | 1055 | position.set( event.pageX, event.pageY ); 1056 | 1057 | } 1058 | 1059 | function getSecondPointerPosition( event ) { 1060 | 1061 | const pointer = event.pointerId === pointers[ 0 ].pointerId ? pointers[ 1 ] : pointers[ 0 ]; 1062 | return pointerPositions[ pointer.pointerId ]; 1063 | 1064 | } // 1065 | 1066 | 1067 | scope.domElement.addEventListener( 'contextmenu', onContextMenu ); 1068 | scope.domElement.addEventListener( 'pointerdown', onPointerDown ); 1069 | scope.domElement.addEventListener( 'pointercancel', onPointerCancel ); 1070 | scope.domElement.addEventListener( 'wheel', onMouseWheel, { 1071 | passive: false 1072 | } ); // force an update at start 1073 | 1074 | this.update(); 1075 | 1076 | } 1077 | 1078 | } // This set of controls performs orbiting, dollying (zooming), and panning. 1079 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 1080 | // This is very similar to OrbitControls, another set of touch behavior 1081 | // 1082 | // Orbit - right mouse, or left mouse + ctrl/meta/shiftKey / touch: two-finger rotate 1083 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish 1084 | // Pan - left mouse, or arrow keys / touch: one-finger move 1085 | 1086 | 1087 | class MapControls extends OrbitControls { 1088 | 1089 | constructor( object, domElement ) { 1090 | 1091 | super( object, domElement ); 1092 | this.screenSpacePanning = false; // pan orthogonal to world-space direction camera.up 1093 | 1094 | this.mouseButtons.LEFT = THREE.MOUSE.PAN; 1095 | this.mouseButtons.RIGHT = THREE.MOUSE.ROTATE; 1096 | this.touches.ONE = THREE.TOUCH.PAN; 1097 | this.touches.TWO = THREE.TOUCH.DOLLY_ROTATE; 1098 | 1099 | } 1100 | 1101 | } 1102 | 1103 | THREE.MapControls = MapControls; 1104 | THREE.OrbitControls = OrbitControls; 1105 | 1106 | } )(); 1107 | -------------------------------------------------------------------------------- /OrbitControls.js: -------------------------------------------------------------------------------- 1 | import { 2 | EventDispatcher, 3 | MOUSE, 4 | Quaternion, 5 | Spherical, 6 | TOUCH, 7 | Vector2, 8 | Vector3 9 | } from "https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.module.js"; 10 | 11 | // This set of controls performs orbiting, dollying (zooming), and panning. 12 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 13 | // 14 | // Orbit - left mouse / touch: one-finger move 15 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish 16 | // Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move 17 | 18 | const _changeEvent = { type: 'change' }; 19 | const _startEvent = { type: 'start' }; 20 | const _endEvent = { type: 'end' }; 21 | 22 | class OrbitControls extends EventDispatcher { 23 | 24 | constructor( object, domElement ) { 25 | 26 | super(); 27 | 28 | if ( domElement === undefined ) console.warn( 'THREE.OrbitControls: The second parameter "domElement" is now mandatory.' ); 29 | if ( domElement === document ) console.error( 'THREE.OrbitControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.' ); 30 | 31 | this.object = object; 32 | this.domElement = domElement; 33 | 34 | // Set to false to disable this control 35 | this.enabled = true; 36 | 37 | // "target" sets the location of focus, where the object orbits around 38 | this.target = new Vector3(); 39 | 40 | // How far you can dolly in and out ( PerspectiveCamera only ) 41 | this.minDistance = 0; 42 | this.maxDistance = Infinity; 43 | 44 | // How far you can zoom in and out ( OrthographicCamera only ) 45 | this.minZoom = 0; 46 | this.maxZoom = Infinity; 47 | 48 | // How far you can orbit vertically, upper and lower limits. 49 | // Range is 0 to Math.PI radians. 50 | this.minPolarAngle = 0; // radians 51 | this.maxPolarAngle = Math.PI; // radians 52 | 53 | // How far you can orbit horizontally, upper and lower limits. 54 | // If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI ) 55 | this.minAzimuthAngle = - Infinity; // radians 56 | this.maxAzimuthAngle = Infinity; // radians 57 | 58 | // Set to true to enable damping (inertia) 59 | // If damping is enabled, you must call controls.update() in your animation loop 60 | this.enableDamping = false; 61 | this.dampingFactor = 0.05; 62 | 63 | // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. 64 | // Set to false to disable zooming 65 | this.enableZoom = true; 66 | this.zoomSpeed = 1.0; 67 | 68 | // Set to false to disable rotating 69 | this.enableRotate = true; 70 | this.rotateSpeed = 1.0; 71 | 72 | // Set to false to disable panning 73 | this.enablePan = true; 74 | this.panSpeed = 1.0; 75 | this.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.up 76 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push 77 | 78 | // Set to true to automatically rotate around the target 79 | // If auto-rotate is enabled, you must call controls.update() in your animation loop 80 | this.autoRotate = false; 81 | this.autoRotateSpeed = 2.0; // 30 seconds per orbit when fps is 60 82 | 83 | // The four arrow keys 84 | this.keys = { LEFT: 'ArrowLeft', UP: 'ArrowUp', RIGHT: 'ArrowRight', BOTTOM: 'ArrowDown' }; 85 | 86 | // Mouse buttons 87 | this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN }; 88 | 89 | // Touch fingers 90 | this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN }; 91 | 92 | // for reset 93 | this.target0 = this.target.clone(); 94 | this.position0 = this.object.position.clone(); 95 | this.zoom0 = this.object.zoom; 96 | 97 | // the target DOM element for key events 98 | this._domElementKeyEvents = null; 99 | 100 | // 101 | // public methods 102 | // 103 | 104 | this.getPolarAngle = function () { 105 | 106 | return spherical.phi; 107 | 108 | }; 109 | 110 | this.getAzimuthalAngle = function () { 111 | 112 | return spherical.theta; 113 | 114 | }; 115 | 116 | this.listenToKeyEvents = function ( domElement ) { 117 | 118 | domElement.addEventListener( 'keydown', onKeyDown ); 119 | this._domElementKeyEvents = domElement; 120 | 121 | }; 122 | 123 | this.saveState = function () { 124 | 125 | scope.target0.copy( scope.target ); 126 | scope.position0.copy( scope.object.position ); 127 | scope.zoom0 = scope.object.zoom; 128 | 129 | }; 130 | 131 | this.reset = function () { 132 | 133 | scope.target.copy( scope.target0 ); 134 | scope.object.position.copy( scope.position0 ); 135 | scope.object.zoom = scope.zoom0; 136 | 137 | scope.object.updateProjectionMatrix(); 138 | scope.dispatchEvent( _changeEvent ); 139 | 140 | scope.update(); 141 | 142 | state = STATE.NONE; 143 | 144 | }; 145 | 146 | // this method is exposed, but perhaps it would be better if we can make it private... 147 | this.update = function () { 148 | 149 | const offset = new Vector3(); 150 | 151 | // so camera.up is the orbit axis 152 | const quat = new Quaternion().setFromUnitVectors( object.up, new Vector3( 0, 1, 0 ) ); 153 | const quatInverse = quat.clone().invert(); 154 | 155 | const lastPosition = new Vector3(); 156 | const lastQuaternion = new Quaternion(); 157 | 158 | const twoPI = 2 * Math.PI; 159 | 160 | return function update() { 161 | 162 | const position = scope.object.position; 163 | 164 | offset.copy( position ).sub( scope.target ); 165 | 166 | // rotate offset to "y-axis-is-up" space 167 | offset.applyQuaternion( quat ); 168 | 169 | // angle from z-axis around y-axis 170 | spherical.setFromVector3( offset ); 171 | 172 | if ( scope.autoRotate && state === STATE.NONE ) { 173 | 174 | rotateLeft( getAutoRotationAngle() ); 175 | 176 | } 177 | 178 | if ( scope.enableDamping ) { 179 | 180 | spherical.theta += sphericalDelta.theta * scope.dampingFactor; 181 | spherical.phi += sphericalDelta.phi * scope.dampingFactor; 182 | 183 | } else { 184 | 185 | spherical.theta += sphericalDelta.theta; 186 | spherical.phi += sphericalDelta.phi; 187 | 188 | } 189 | 190 | // restrict theta to be between desired limits 191 | 192 | let min = scope.minAzimuthAngle; 193 | let max = scope.maxAzimuthAngle; 194 | 195 | if ( isFinite( min ) && isFinite( max ) ) { 196 | 197 | if ( min < - Math.PI ) min += twoPI; else if ( min > Math.PI ) min -= twoPI; 198 | 199 | if ( max < - Math.PI ) max += twoPI; else if ( max > Math.PI ) max -= twoPI; 200 | 201 | if ( min <= max ) { 202 | 203 | spherical.theta = Math.max( min, Math.min( max, spherical.theta ) ); 204 | 205 | } else { 206 | 207 | spherical.theta = ( spherical.theta > ( min + max ) / 2 ) ? 208 | Math.max( min, spherical.theta ) : 209 | Math.min( max, spherical.theta ); 210 | 211 | } 212 | 213 | } 214 | 215 | // restrict phi to be between desired limits 216 | spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) ); 217 | 218 | spherical.makeSafe(); 219 | 220 | spherical.radius *= scale; 221 | 222 | // restrict radius to be between desired limits 223 | spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) ); 224 | 225 | // move target to panned location 226 | 227 | if ( scope.enableDamping === true ) { 228 | 229 | scope.target.addScaledVector( panOffset, scope.dampingFactor ); 230 | 231 | } else { 232 | 233 | scope.target.add( panOffset ); 234 | 235 | } 236 | 237 | offset.setFromSpherical( spherical ); 238 | 239 | // rotate offset back to "camera-up-vector-is-up" space 240 | offset.applyQuaternion( quatInverse ); 241 | 242 | position.copy( scope.target ).add( offset ); 243 | 244 | scope.object.lookAt( scope.target ); 245 | 246 | if ( scope.enableDamping === true ) { 247 | 248 | sphericalDelta.theta *= ( 1 - scope.dampingFactor ); 249 | sphericalDelta.phi *= ( 1 - scope.dampingFactor ); 250 | 251 | panOffset.multiplyScalar( 1 - scope.dampingFactor ); 252 | 253 | } else { 254 | 255 | sphericalDelta.set( 0, 0, 0 ); 256 | 257 | panOffset.set( 0, 0, 0 ); 258 | 259 | } 260 | 261 | scale = 1; 262 | 263 | // update condition is: 264 | // min(camera displacement, camera rotation in radians)^2 > EPS 265 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8 266 | 267 | if ( zoomChanged || 268 | lastPosition.distanceToSquared( scope.object.position ) > EPS || 269 | 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) { 270 | 271 | scope.dispatchEvent( _changeEvent ); 272 | 273 | lastPosition.copy( scope.object.position ); 274 | lastQuaternion.copy( scope.object.quaternion ); 275 | zoomChanged = false; 276 | 277 | return true; 278 | 279 | } 280 | 281 | return false; 282 | 283 | }; 284 | 285 | }(); 286 | 287 | this.dispose = function () { 288 | 289 | scope.domElement.removeEventListener( 'contextmenu', onContextMenu ); 290 | 291 | scope.domElement.removeEventListener( 'pointerdown', onPointerDown ); 292 | scope.domElement.removeEventListener( 'wheel', onMouseWheel ); 293 | 294 | scope.domElement.removeEventListener( 'touchstart', onTouchStart ); 295 | scope.domElement.removeEventListener( 'touchend', onTouchEnd ); 296 | scope.domElement.removeEventListener( 'touchmove', onTouchMove ); 297 | 298 | scope.domElement.ownerDocument.removeEventListener( 'pointermove', onPointerMove ); 299 | scope.domElement.ownerDocument.removeEventListener( 'pointerup', onPointerUp ); 300 | 301 | 302 | if ( scope._domElementKeyEvents !== null ) { 303 | 304 | scope._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown ); 305 | 306 | } 307 | 308 | //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? 309 | 310 | }; 311 | 312 | // 313 | // internals 314 | // 315 | 316 | const scope = this; 317 | 318 | const STATE = { 319 | NONE: - 1, 320 | ROTATE: 0, 321 | DOLLY: 1, 322 | PAN: 2, 323 | TOUCH_ROTATE: 3, 324 | TOUCH_PAN: 4, 325 | TOUCH_DOLLY_PAN: 5, 326 | TOUCH_DOLLY_ROTATE: 6 327 | }; 328 | 329 | let state = STATE.NONE; 330 | 331 | const EPS = 0.000001; 332 | 333 | // current position in spherical coordinates 334 | const spherical = new Spherical(); 335 | const sphericalDelta = new Spherical(); 336 | 337 | let scale = 1; 338 | const panOffset = new Vector3(); 339 | let zoomChanged = false; 340 | 341 | const rotateStart = new Vector2(); 342 | const rotateEnd = new Vector2(); 343 | const rotateDelta = new Vector2(); 344 | 345 | const panStart = new Vector2(); 346 | const panEnd = new Vector2(); 347 | const panDelta = new Vector2(); 348 | 349 | const dollyStart = new Vector2(); 350 | const dollyEnd = new Vector2(); 351 | const dollyDelta = new Vector2(); 352 | 353 | function getAutoRotationAngle() { 354 | 355 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; 356 | 357 | } 358 | 359 | function getZoomScale() { 360 | 361 | return Math.pow( 0.95, scope.zoomSpeed ); 362 | 363 | } 364 | 365 | function rotateLeft( angle ) { 366 | 367 | sphericalDelta.theta -= angle; 368 | 369 | } 370 | 371 | function rotateUp( angle ) { 372 | 373 | sphericalDelta.phi -= angle; 374 | 375 | } 376 | 377 | // Exposing rotateLeft and rotateUp 378 | this.rotateLeft = rotateLeft; 379 | this.rotateUp = rotateUp; 380 | 381 | const panLeft = function () { 382 | 383 | const v = new Vector3(); 384 | 385 | return function panLeft( distance, objectMatrix ) { 386 | 387 | v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix 388 | v.multiplyScalar( - distance ); 389 | 390 | panOffset.add( v ); 391 | 392 | }; 393 | 394 | }(); 395 | 396 | const panUp = function () { 397 | 398 | const v = new Vector3(); 399 | 400 | return function panUp( distance, objectMatrix ) { 401 | 402 | if ( scope.screenSpacePanning === true ) { 403 | 404 | v.setFromMatrixColumn( objectMatrix, 1 ); 405 | 406 | } else { 407 | 408 | v.setFromMatrixColumn( objectMatrix, 0 ); 409 | v.crossVectors( scope.object.up, v ); 410 | 411 | } 412 | 413 | v.multiplyScalar( distance ); 414 | 415 | panOffset.add( v ); 416 | 417 | }; 418 | 419 | }(); 420 | 421 | // deltaX and deltaY are in pixels; right and down are positive 422 | const pan = function () { 423 | 424 | const offset = new Vector3(); 425 | 426 | return function pan( deltaX, deltaY ) { 427 | 428 | const element = scope.domElement; 429 | 430 | if ( scope.object.isPerspectiveCamera ) { 431 | 432 | // perspective 433 | const position = scope.object.position; 434 | offset.copy( position ).sub( scope.target ); 435 | let targetDistance = offset.length(); 436 | 437 | // half of the fov is center to top of screen 438 | targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 ); 439 | 440 | // we use only clientHeight here so aspect ratio does not distort speed 441 | panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix ); 442 | panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix ); 443 | 444 | } else if ( scope.object.isOrthographicCamera ) { 445 | 446 | // orthographic 447 | panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix ); 448 | panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix ); 449 | 450 | } else { 451 | 452 | // camera neither orthographic nor perspective 453 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); 454 | scope.enablePan = false; 455 | 456 | } 457 | 458 | }; 459 | 460 | }(); 461 | 462 | function dollyOut( dollyScale ) { 463 | 464 | if ( scope.object.isPerspectiveCamera ) { 465 | 466 | scale /= dollyScale; 467 | 468 | } else if ( scope.object.isOrthographicCamera ) { 469 | 470 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) ); 471 | scope.object.updateProjectionMatrix(); 472 | zoomChanged = true; 473 | 474 | } else { 475 | 476 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 477 | scope.enableZoom = false; 478 | 479 | } 480 | 481 | } 482 | 483 | function dollyIn( dollyScale ) { 484 | 485 | if ( scope.object.isPerspectiveCamera ) { 486 | 487 | scale *= dollyScale; 488 | 489 | } else if ( scope.object.isOrthographicCamera ) { 490 | 491 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) ); 492 | scope.object.updateProjectionMatrix(); 493 | zoomChanged = true; 494 | 495 | } else { 496 | 497 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 498 | scope.enableZoom = false; 499 | 500 | } 501 | 502 | } 503 | 504 | // 505 | // event callbacks - update the object state 506 | // 507 | 508 | function handleMouseDownRotate( event ) { 509 | 510 | rotateStart.set( event.clientX, event.clientY ); 511 | 512 | } 513 | 514 | function handleMouseDownDolly( event ) { 515 | 516 | dollyStart.set( event.clientX, event.clientY ); 517 | 518 | } 519 | 520 | function handleMouseDownPan( event ) { 521 | 522 | panStart.set( event.clientX, event.clientY ); 523 | 524 | } 525 | 526 | function handleMouseMoveRotate( event ) { 527 | 528 | rotateEnd.set( event.clientX, event.clientY ); 529 | 530 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); 531 | 532 | const element = scope.domElement; 533 | 534 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height 535 | 536 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); 537 | 538 | rotateStart.copy( rotateEnd ); 539 | 540 | scope.update(); 541 | 542 | } 543 | 544 | function handleMouseMoveDolly( event ) { 545 | 546 | dollyEnd.set( event.clientX, event.clientY ); 547 | 548 | dollyDelta.subVectors( dollyEnd, dollyStart ); 549 | 550 | if ( dollyDelta.y > 0 ) { 551 | 552 | dollyOut( getZoomScale() ); 553 | 554 | } else if ( dollyDelta.y < 0 ) { 555 | 556 | dollyIn( getZoomScale() ); 557 | 558 | } 559 | 560 | dollyStart.copy( dollyEnd ); 561 | 562 | scope.update(); 563 | 564 | } 565 | 566 | function handleMouseMovePan( event ) { 567 | 568 | panEnd.set( event.clientX, event.clientY ); 569 | 570 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); 571 | 572 | pan( panDelta.x, panDelta.y ); 573 | 574 | panStart.copy( panEnd ); 575 | 576 | scope.update(); 577 | 578 | } 579 | 580 | function handleMouseUp( /*event*/ ) { 581 | 582 | // no-op 583 | 584 | } 585 | 586 | function handleMouseWheel( event ) { 587 | 588 | if ( event.deltaY < 0 ) { 589 | 590 | dollyIn( getZoomScale() ); 591 | 592 | } else if ( event.deltaY > 0 ) { 593 | 594 | dollyOut( getZoomScale() ); 595 | 596 | } 597 | 598 | scope.update(); 599 | 600 | } 601 | 602 | function handleKeyDown( event ) { 603 | 604 | let needsUpdate = false; 605 | 606 | switch ( event.code ) { 607 | 608 | case scope.keys.UP: 609 | pan( 0, scope.keyPanSpeed ); 610 | needsUpdate = true; 611 | break; 612 | 613 | case scope.keys.BOTTOM: 614 | pan( 0, - scope.keyPanSpeed ); 615 | needsUpdate = true; 616 | break; 617 | 618 | case scope.keys.LEFT: 619 | pan( scope.keyPanSpeed, 0 ); 620 | needsUpdate = true; 621 | break; 622 | 623 | case scope.keys.RIGHT: 624 | pan( - scope.keyPanSpeed, 0 ); 625 | needsUpdate = true; 626 | break; 627 | 628 | } 629 | 630 | if ( needsUpdate ) { 631 | 632 | // prevent the browser from scrolling on cursor keys 633 | event.preventDefault(); 634 | 635 | scope.update(); 636 | 637 | } 638 | 639 | 640 | } 641 | 642 | function handleTouchStartRotate( event ) { 643 | 644 | if ( event.touches.length == 1 ) { 645 | 646 | rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 647 | 648 | } else { 649 | 650 | const x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); 651 | const y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); 652 | 653 | rotateStart.set( x, y ); 654 | 655 | } 656 | 657 | } 658 | 659 | function handleTouchStartPan( event ) { 660 | 661 | if ( event.touches.length == 1 ) { 662 | 663 | panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 664 | 665 | } else { 666 | 667 | const x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); 668 | const y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); 669 | 670 | panStart.set( x, y ); 671 | 672 | } 673 | 674 | } 675 | 676 | function handleTouchStartDolly( event ) { 677 | 678 | const dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 679 | const dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 680 | 681 | const distance = Math.sqrt( dx * dx + dy * dy ); 682 | 683 | dollyStart.set( 0, distance ); 684 | 685 | } 686 | 687 | function handleTouchStartDollyPan( event ) { 688 | 689 | if ( scope.enableZoom ) handleTouchStartDolly( event ); 690 | 691 | if ( scope.enablePan ) handleTouchStartPan( event ); 692 | 693 | } 694 | 695 | function handleTouchStartDollyRotate( event ) { 696 | 697 | if ( scope.enableZoom ) handleTouchStartDolly( event ); 698 | 699 | if ( scope.enableRotate ) handleTouchStartRotate( event ); 700 | 701 | } 702 | 703 | function handleTouchMoveRotate( event ) { 704 | 705 | if ( event.touches.length == 1 ) { 706 | 707 | rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 708 | 709 | } else { 710 | 711 | const x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); 712 | const y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); 713 | 714 | rotateEnd.set( x, y ); 715 | 716 | } 717 | 718 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); 719 | 720 | const element = scope.domElement; 721 | 722 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height 723 | 724 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); 725 | 726 | rotateStart.copy( rotateEnd ); 727 | 728 | } 729 | 730 | function handleTouchMovePan( event ) { 731 | 732 | if ( event.touches.length == 1 ) { 733 | 734 | panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 735 | 736 | } else { 737 | 738 | const x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); 739 | const y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); 740 | 741 | panEnd.set( x, y ); 742 | 743 | } 744 | 745 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); 746 | 747 | pan( panDelta.x, panDelta.y ); 748 | 749 | panStart.copy( panEnd ); 750 | 751 | } 752 | 753 | function handleTouchMoveDolly( event ) { 754 | 755 | const dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 756 | const dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 757 | 758 | const distance = Math.sqrt( dx * dx + dy * dy ); 759 | 760 | dollyEnd.set( 0, distance ); 761 | 762 | dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) ); 763 | 764 | dollyOut( dollyDelta.y ); 765 | 766 | dollyStart.copy( dollyEnd ); 767 | 768 | } 769 | 770 | function handleTouchMoveDollyPan( event ) { 771 | 772 | if ( scope.enableZoom ) handleTouchMoveDolly( event ); 773 | 774 | if ( scope.enablePan ) handleTouchMovePan( event ); 775 | 776 | } 777 | 778 | function handleTouchMoveDollyRotate( event ) { 779 | 780 | if ( scope.enableZoom ) handleTouchMoveDolly( event ); 781 | 782 | if ( scope.enableRotate ) handleTouchMoveRotate( event ); 783 | 784 | } 785 | 786 | function handleTouchEnd( /*event*/ ) { 787 | 788 | // no-op 789 | 790 | } 791 | 792 | // 793 | // event handlers - FSM: listen for events and reset state 794 | // 795 | 796 | function onPointerDown( event ) { 797 | 798 | if ( scope.enabled === false ) return; 799 | 800 | switch ( event.pointerType ) { 801 | 802 | case 'mouse': 803 | case 'pen': 804 | onMouseDown( event ); 805 | break; 806 | 807 | // TODO touch 808 | 809 | } 810 | 811 | } 812 | 813 | function onPointerMove( event ) { 814 | 815 | if ( scope.enabled === false ) return; 816 | 817 | switch ( event.pointerType ) { 818 | 819 | case 'mouse': 820 | case 'pen': 821 | onMouseMove( event ); 822 | break; 823 | 824 | // TODO touch 825 | 826 | } 827 | 828 | } 829 | 830 | function onPointerUp( event ) { 831 | 832 | switch ( event.pointerType ) { 833 | 834 | case 'mouse': 835 | case 'pen': 836 | onMouseUp( event ); 837 | break; 838 | 839 | // TODO touch 840 | 841 | } 842 | 843 | } 844 | 845 | function onMouseDown( event ) { 846 | 847 | // Prevent the browser from scrolling. 848 | event.preventDefault(); 849 | 850 | // Manually set the focus since calling preventDefault above 851 | // prevents the browser from setting it automatically. 852 | 853 | scope.domElement.focus ? scope.domElement.focus() : window.focus(); 854 | 855 | let mouseAction; 856 | 857 | switch ( event.button ) { 858 | 859 | case 0: 860 | 861 | mouseAction = scope.mouseButtons.LEFT; 862 | break; 863 | 864 | case 1: 865 | 866 | mouseAction = scope.mouseButtons.MIDDLE; 867 | break; 868 | 869 | case 2: 870 | 871 | mouseAction = scope.mouseButtons.RIGHT; 872 | break; 873 | 874 | default: 875 | 876 | mouseAction = - 1; 877 | 878 | } 879 | 880 | switch ( mouseAction ) { 881 | 882 | case MOUSE.DOLLY: 883 | 884 | if ( scope.enableZoom === false ) return; 885 | 886 | handleMouseDownDolly( event ); 887 | 888 | state = STATE.DOLLY; 889 | 890 | break; 891 | 892 | case MOUSE.ROTATE: 893 | 894 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 895 | 896 | if ( scope.enablePan === false ) return; 897 | 898 | handleMouseDownPan( event ); 899 | 900 | state = STATE.PAN; 901 | 902 | } else { 903 | 904 | if ( scope.enableRotate === false ) return; 905 | 906 | handleMouseDownRotate( event ); 907 | 908 | state = STATE.ROTATE; 909 | 910 | } 911 | 912 | break; 913 | 914 | case MOUSE.PAN: 915 | 916 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 917 | 918 | if ( scope.enableRotate === false ) return; 919 | 920 | handleMouseDownRotate( event ); 921 | 922 | state = STATE.ROTATE; 923 | 924 | } else { 925 | 926 | if ( scope.enablePan === false ) return; 927 | 928 | handleMouseDownPan( event ); 929 | 930 | state = STATE.PAN; 931 | 932 | } 933 | 934 | break; 935 | 936 | default: 937 | 938 | state = STATE.NONE; 939 | 940 | } 941 | 942 | if ( state !== STATE.NONE ) { 943 | 944 | scope.domElement.ownerDocument.addEventListener( 'pointermove', onPointerMove ); 945 | scope.domElement.ownerDocument.addEventListener( 'pointerup', onPointerUp ); 946 | 947 | scope.dispatchEvent( _startEvent ); 948 | 949 | } 950 | 951 | } 952 | 953 | function onMouseMove( event ) { 954 | 955 | if ( scope.enabled === false ) return; 956 | 957 | event.preventDefault(); 958 | 959 | switch ( state ) { 960 | 961 | case STATE.ROTATE: 962 | 963 | if ( scope.enableRotate === false ) return; 964 | 965 | handleMouseMoveRotate( event ); 966 | 967 | break; 968 | 969 | case STATE.DOLLY: 970 | 971 | if ( scope.enableZoom === false ) return; 972 | 973 | handleMouseMoveDolly( event ); 974 | 975 | break; 976 | 977 | case STATE.PAN: 978 | 979 | if ( scope.enablePan === false ) return; 980 | 981 | handleMouseMovePan( event ); 982 | 983 | break; 984 | 985 | } 986 | 987 | } 988 | 989 | function onMouseUp( event ) { 990 | 991 | scope.domElement.ownerDocument.removeEventListener( 'pointermove', onPointerMove ); 992 | scope.domElement.ownerDocument.removeEventListener( 'pointerup', onPointerUp ); 993 | 994 | if ( scope.enabled === false ) return; 995 | 996 | //handleMouseUp( event ); 997 | 998 | scope.dispatchEvent( _endEvent ); 999 | 1000 | state = STATE.NONE; 1001 | 1002 | } 1003 | 1004 | function onMouseWheel( event ) { 1005 | 1006 | if ( scope.enabled === false || scope.enableZoom === false || ( state !== STATE.NONE && state !== STATE.ROTATE ) ) return; 1007 | 1008 | event.preventDefault(); 1009 | 1010 | scope.dispatchEvent( _startEvent ); 1011 | 1012 | handleMouseWheel( event ); 1013 | 1014 | scope.dispatchEvent( _endEvent ); 1015 | 1016 | } 1017 | 1018 | function onKeyDown( event ) { 1019 | 1020 | if ( scope.enabled === false || scope.enablePan === false ) return; 1021 | 1022 | handleKeyDown( event ); 1023 | 1024 | } 1025 | 1026 | function onTouchStart( event ) { 1027 | 1028 | if ( scope.enabled === false ) return; 1029 | 1030 | event.preventDefault(); // prevent scrolling 1031 | 1032 | switch ( event.touches.length ) { 1033 | 1034 | case 1: 1035 | 1036 | switch ( scope.touches.ONE ) { 1037 | 1038 | case TOUCH.ROTATE: 1039 | 1040 | if ( scope.enableRotate === false ) return; 1041 | 1042 | handleTouchStartRotate( event ); 1043 | 1044 | state = STATE.TOUCH_ROTATE; 1045 | 1046 | break; 1047 | 1048 | case TOUCH.PAN: 1049 | 1050 | if ( scope.enablePan === false ) return; 1051 | 1052 | handleTouchStartPan( event ); 1053 | 1054 | state = STATE.TOUCH_PAN; 1055 | 1056 | break; 1057 | 1058 | default: 1059 | 1060 | state = STATE.NONE; 1061 | 1062 | } 1063 | 1064 | break; 1065 | 1066 | case 2: 1067 | 1068 | switch ( scope.touches.TWO ) { 1069 | 1070 | case TOUCH.DOLLY_PAN: 1071 | 1072 | if ( scope.enableZoom === false && scope.enablePan === false ) return; 1073 | 1074 | handleTouchStartDollyPan( event ); 1075 | 1076 | state = STATE.TOUCH_DOLLY_PAN; 1077 | 1078 | break; 1079 | 1080 | case TOUCH.DOLLY_ROTATE: 1081 | 1082 | if ( scope.enableZoom === false && scope.enableRotate === false ) return; 1083 | 1084 | handleTouchStartDollyRotate( event ); 1085 | 1086 | state = STATE.TOUCH_DOLLY_ROTATE; 1087 | 1088 | break; 1089 | 1090 | default: 1091 | 1092 | state = STATE.NONE; 1093 | 1094 | } 1095 | 1096 | break; 1097 | 1098 | default: 1099 | 1100 | state = STATE.NONE; 1101 | 1102 | } 1103 | 1104 | if ( state !== STATE.NONE ) { 1105 | 1106 | scope.dispatchEvent( _startEvent ); 1107 | 1108 | } 1109 | 1110 | } 1111 | 1112 | function onTouchMove( event ) { 1113 | 1114 | if ( scope.enabled === false ) return; 1115 | 1116 | event.preventDefault(); // prevent scrolling 1117 | 1118 | switch ( state ) { 1119 | 1120 | case STATE.TOUCH_ROTATE: 1121 | 1122 | if ( scope.enableRotate === false ) return; 1123 | 1124 | handleTouchMoveRotate( event ); 1125 | 1126 | scope.update(); 1127 | 1128 | break; 1129 | 1130 | case STATE.TOUCH_PAN: 1131 | 1132 | if ( scope.enablePan === false ) return; 1133 | 1134 | handleTouchMovePan( event ); 1135 | 1136 | scope.update(); 1137 | 1138 | break; 1139 | 1140 | case STATE.TOUCH_DOLLY_PAN: 1141 | 1142 | if ( scope.enableZoom === false && scope.enablePan === false ) return; 1143 | 1144 | handleTouchMoveDollyPan( event ); 1145 | 1146 | scope.update(); 1147 | 1148 | break; 1149 | 1150 | case STATE.TOUCH_DOLLY_ROTATE: 1151 | 1152 | if ( scope.enableZoom === false && scope.enableRotate === false ) return; 1153 | 1154 | handleTouchMoveDollyRotate( event ); 1155 | 1156 | scope.update(); 1157 | 1158 | break; 1159 | 1160 | default: 1161 | 1162 | state = STATE.NONE; 1163 | 1164 | } 1165 | 1166 | } 1167 | 1168 | function onTouchEnd( event ) { 1169 | 1170 | if ( scope.enabled === false ) return; 1171 | 1172 | //handleTouchEnd( event ); 1173 | 1174 | scope.dispatchEvent( _endEvent ); 1175 | 1176 | state = STATE.NONE; 1177 | 1178 | } 1179 | 1180 | function onContextMenu( event ) { 1181 | 1182 | if ( scope.enabled === false ) return; 1183 | 1184 | event.preventDefault(); 1185 | 1186 | } 1187 | 1188 | // 1189 | 1190 | scope.domElement.addEventListener( 'contextmenu', onContextMenu ); 1191 | 1192 | scope.domElement.addEventListener( 'pointerdown', onPointerDown ); 1193 | scope.domElement.addEventListener( 'wheel', onMouseWheel, { passive: false } ); 1194 | 1195 | scope.domElement.addEventListener( 'touchstart', onTouchStart, { passive: false } ); 1196 | scope.domElement.addEventListener( 'touchend', onTouchEnd ); 1197 | scope.domElement.addEventListener( 'touchmove', onTouchMove, { passive: false } ); 1198 | 1199 | // force an update at start 1200 | 1201 | this.update(); 1202 | 1203 | } 1204 | 1205 | } 1206 | 1207 | 1208 | // This set of controls performs orbiting, dollying (zooming), and panning. 1209 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 1210 | // This is very similar to OrbitControls, another set of touch behavior 1211 | // 1212 | // Orbit - right mouse, or left mouse + ctrl/meta/shiftKey / touch: two-finger rotate 1213 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish 1214 | // Pan - left mouse, or arrow keys / touch: one-finger move 1215 | 1216 | class MapControls extends OrbitControls { 1217 | 1218 | constructor( object, domElement ) { 1219 | 1220 | super( object, domElement ); 1221 | 1222 | this.screenSpacePanning = false; // pan orthogonal to world-space direction camera.up 1223 | 1224 | this.mouseButtons.LEFT = MOUSE.PAN; 1225 | this.mouseButtons.RIGHT = MOUSE.ROTATE; 1226 | 1227 | this.touches.ONE = TOUCH.PAN; 1228 | this.touches.TWO = TOUCH.DOLLY_ROTATE; 1229 | 1230 | } 1231 | 1232 | } 1233 | 1234 | export { OrbitControls, MapControls }; --------------------------------------------------------------------------------