├── 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 };
--------------------------------------------------------------------------------