• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

iTowns / itowns / 17800125246

17 Sep 2025 02:00PM UTC coverage: 87.129% (+0.2%) from 86.933%
17800125246

Pull #2589

github

web-flow
Merge 6fe8e828d into 60a94655f
Pull Request #2589: Three-geospatial integration

2806 of 3746 branches covered (74.91%)

Branch coverage included in aggregate %.

222 of 253 new or added lines in 10 files covered. (87.75%)

111 existing lines in 4 files now uncovered.

25787 of 29071 relevant lines covered (88.7%)

1114.96 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.28
/packages/Main/src/Controls/PlanarControls.js
1
import * as THREE from 'three';
1✔
2
import { MAIN_LOOP_EVENTS } from 'Core/MainLoop';
1✔
3
import { VIEW_EVENTS } from 'Core/View';
1✔
4

1✔
5
// event keycode
1✔
6
export const keys = {
1✔
7
    CTRL: 17,
1✔
8
    SPACE: 32,
1✔
9
    T: 84,
1✔
10
    Y: 89,
1✔
11
};
1✔
12

1✔
13
const mouseButtons = {
1✔
14
    LEFTCLICK: THREE.MOUSE.LEFT,
1✔
15
    MIDDLECLICK: THREE.MOUSE.MIDDLE,
1✔
16
    RIGHTCLICK: THREE.MOUSE.RIGHT,
1✔
17
};
1✔
18
let currentPressedButton;
1✔
19

1✔
20
// starting camera position and orientation target
1✔
21
const startPosition = new THREE.Vector3();
1✔
22
const startQuaternion = new THREE.Quaternion();
1✔
23
// camera initial zoom value if orthographic
1✔
24
let cameraInitialZoom = 0;
1✔
25

1✔
26
// point under the cursor
1✔
27
const pointUnderCursor = new THREE.Vector3();
1✔
28

1✔
29
// control state
1✔
30
export const STATE = {
1✔
31
    NONE: -1,
1✔
32
    DRAG: 0,
1✔
33
    PAN: 1,
1✔
34
    ROTATE: 2,
1✔
35
    TRAVEL: 3,
1✔
36
    ORTHO_ZOOM: 4,
1✔
37
};
1✔
38

1✔
39
// cursor shape linked to control state
1✔
40
const cursor = {
1✔
41
    default: 'auto',
1✔
42
    drag: 'move',
1✔
43
    pan: 'cell',
1✔
44
    travel: 'wait',
1✔
45
    rotate: 'move',
1✔
46
    ortho_zoom: 'wait',
1✔
47
};
1✔
48

1✔
49
const vectorZero = new THREE.Vector3();
1✔
50

1✔
51
// mouse movement
1✔
52
const mousePosition = new THREE.Vector2();
1✔
53
const lastMousePosition = new THREE.Vector2();
1✔
54
const deltaMousePosition = new THREE.Vector2(0, 0);
1✔
55

1✔
56
// drag movement
1✔
57
const dragStart = new THREE.Vector3();
1✔
58
const dragEnd = new THREE.Vector3();
1✔
59
const dragDelta = new THREE.Vector3();
1✔
60

1✔
61
// camera focus point : ground point at screen center
1✔
62
const centerPoint = new THREE.Vector3(0, 0, 0);
1✔
63

1✔
64
// camera rotation
1✔
65
let phi = 0.0;
1✔
66

1✔
67
// displacement and rotation vectors
1✔
68
const vect = new THREE.Vector3();
1✔
69
const quat = new THREE.Quaternion();
1✔
70
const vect2 = new THREE.Vector2();
1✔
71

1✔
72
// animated travel
1✔
73
const travelEndPos = new THREE.Vector3();
1✔
74
const travelStartPos = new THREE.Vector3();
1✔
75
const travelStartRot = new THREE.Quaternion();
1✔
76
const travelEndRot = new THREE.Quaternion();
1✔
77
let travelAlpha = 0;
1✔
78
let travelDuration = 0;
1✔
79
let travelUseRotation = false;
1✔
80
let travelUseSmooth = false;
1✔
81

1✔
82
// zoom changes (for orthographic camera)
1✔
83
let startZoom = 0;
1✔
84
let endZoom = 0;
1✔
85

1✔
86
// ray caster for drag movement
1✔
87
const rayCaster = new THREE.Raycaster();
1✔
88
const plane = new THREE.Plane(
1✔
89
    new THREE.Vector3(0, 0, -1),
1✔
90
);
1✔
91

1✔
92
// default parameters :
1✔
93
const defaultOptions = {
1✔
94
    enabled: true,
1✔
95
    enableRotation: true,
1✔
96
    rotateSpeed: 2.0,
1✔
97
    minPanSpeed: 0.05,
1✔
98
    maxPanSpeed: 15,
1✔
99
    zoomTravelTime: 0.2,  // must be a number
1✔
100
    zoomFactor: 2,
1✔
101
    maxResolution: 1 / Infinity,
1✔
102
    minResolution: Infinity,
1✔
103
    maxAltitude: 50000000,
1✔
104
    groundLevel: 200,
1✔
105
    autoTravelTimeMin: 1.5,
1✔
106
    autoTravelTimeMax: 4,
1✔
107
    autoTravelTimeDist: 50000,
1✔
108
    smartTravelHeightMin: 75,
1✔
109
    smartTravelHeightMax: 500,
1✔
110
    instantTravel: false,
1✔
111
    minZenithAngle: 0,
1✔
112
    maxZenithAngle: 82.5,
1✔
113
    handleCollision: true,
1✔
114
    minDistanceCollision: 30,
1✔
115
    enableSmartTravel: true,
1✔
116
    enablePan: true,
1✔
117
};
1✔
118

1✔
119
export const PLANAR_CONTROL_EVENT = {
1✔
120
    MOVED: 'moved',
1✔
121
};
1✔
122

1✔
123
/**
1✔
124
 * Planar controls is a camera controller adapted for a planar view, with animated movements.
1✔
125
 * Usage is as follow :
1✔
126
 * <ul>
1✔
127
 *     <li><b>Left mouse button:</b> drag the camera (translation on the (xy) world plane).</li>
1✔
128
 *     <li><b>Right mouse button:</b> pan the camera (translation on the vertical (z) axis of the world plane).</li>
1✔
129
 *     <li><b>CTRL + Left mouse button:</b> rotate the camera around the focus point.</li>
1✔
130
 *     <li><b>Wheel scrolling:</b> zoom toward the cursor position.</li>
1✔
131
 *     <li><b>Wheel clicking:</b> smart zoom toward the cursor position (animated).</li>
1✔
132
 *     <li><b>Y key:</b> go to the starting view (animated).</li>
1✔
133
 *     <li><b>T key:</b> go to the top view (animated).</li>
1✔
134
 * </ul>
1✔
135
 *
1✔
136
 * @class   PlanarControls
1✔
137
 * @param   {PlanarView}    view                                the view where the controls will be used
1✔
138
 * @param   {object}        options
1✔
139
 * @param   {boolean}       [options.enabled=true]              Set to false to disable this control
1✔
140
 * @param   {boolean}       [options.enableRotation=true]       Enable the rotation with the `CTRL + Left mouse button`
1✔
141
 * and in animations, like the smart zoom.
1✔
142
 * @param   {boolean}       [options.enableSmartTravel=true]    Enable smart travel with the `wheel-click / space-bar`.
1✔
143
 * @param   {boolean}       [options.enablePan=true]            Enable pan movements with the `right-click`.
1✔
144
 * @param   {number}        [options.rotateSpeed=2.0]           Rotate speed.
1✔
145
 * @param   {number}        [options.maxPanSpeed=15]            Pan speed when close to maxAltitude.
1✔
146
 * @param   {number}        [options.minPanSpeed=0.05]          Pan speed when close to the ground.
1✔
147
 * @param   {number}        [options.zoomTravelTime=0.2]        Animation time when zooming.
1✔
148
 * @param   {number}        [options.zoomFactor=2]              The factor the scale is multiplied by when zooming
1✔
149
 * in and divided by when zooming out. This factor can't be null.
1✔
150
 * @param   {number}        [options.maxResolution=0]           The smallest size in meters a pixel at the center of the
1✔
151
 * view can represent.
1✔
152
 * @param   {number}        [options.minResolution=Infinity]    The biggest size in meters a pixel at the center of the
1✔
153
 * view can represent.
1✔
154
 * @param   {number}        [options.maxAltitude=12000]         Maximum altitude reachable when panning or zooming out.
1✔
155
 * @param   {number}        [options.groundLevel=200]           Minimum altitude reachable when panning.
1✔
156
 * @param   {number}        [options.autoTravelTimeMin=1.5]     Minimum duration for animated travels with the `auto`
1✔
157
 * parameter.
1✔
158
 * @param   {number}        [options.autoTravelTimeMax=4]       Maximum duration for animated travels with the `auto`
1✔
159
 * parameter.
1✔
160
 * @param   {number}        [options.autoTravelTimeDist=20000]  Maximum travel distance for animated travel with the
1✔
161
 * `auto` parameter.
1✔
162
 * @param   {number}        [options.smartTravelHeightMin=75]     Minimum height above ground reachable after a smart
1✔
163
 * travel.
1✔
164
 * @param   {number}        [options.smartTravelHeightMax=500]    Maximum height above ground reachable after a smart
1✔
165
 * travel.
1✔
166
 * @param   {boolean}       [options.instantTravel=false]       If set to true, animated travels will have no duration.
1✔
167
 * @param   {number}        [options.minZenithAngle=0]          The minimum reachable zenith angle for a camera
1✔
168
 * rotation, in degrees.
1✔
169
 * @param   {number}        [options.maxZenithAngle=82.5]       The maximum reachable zenith angle for a camera
1✔
170
 * rotation, in degrees.
1✔
171
 * @param   {boolean}       [options.handleCollision=true]
1✔
172
 */
1✔
173
class PlanarControls extends THREE.EventDispatcher {
1✔
174
    constructor(view, options = {}) {
1!
175
        super();
5✔
176
        this.view = view;
5✔
177
        this.camera = view.camera3D;
5✔
178

5✔
179
        // Set to false to disable this control
5✔
180
        this.enabled = typeof options.enabled == 'boolean' ? options.enabled : defaultOptions.enabled;
5!
181

5✔
182
        if (this.camera.isOrthographicCamera) {
5✔
183
            cameraInitialZoom = this.camera.zoom;
1✔
184

1✔
185
            // enable rotation movements
1✔
186
            this.enableRotation = false;
1✔
187

1✔
188
            // enable pan movements
1✔
189
            this.enablePan = false;
1✔
190

1✔
191
            // Camera altitude is clamped under maxAltitude.
1✔
192
            // This is not relevant for an orthographic camera (since the orthographic camera altitude won't change).
1✔
193
            // Therefore, neutralizing by default the maxAltitude limit allows zooming out with an orthographic camera,
1✔
194
            // no matter its initial position.
1✔
195
            this.maxAltitude = Infinity;
1✔
196

1✔
197
            // the zoom travel time (stored in `this.zoomTravelTime`) can't be `auto` with an orthographic camera
1✔
198
            this.zoomTravelTime = typeof options.zoomTravelTime === 'number' ?
1!
199
                options.zoomTravelTime : defaultOptions.zoomTravelTime;
1✔
200
        } else {
5✔
201
            // enable rotation movements
4✔
202
            this.enableRotation = options.enableRotation === undefined ?
4✔
203
                defaultOptions.enableRotation : options.enableRotation;
4!
204
            this.rotateSpeed = options.rotateSpeed || defaultOptions.rotateSpeed;
4✔
205

4✔
206
            // enable pan movements
4✔
207
            this.enablePan = options.enablePan === undefined ? defaultOptions.enablePan : options.enablePan;
4!
208

4✔
209
            // minPanSpeed when close to the ground, maxPanSpeed when close to maxAltitude
4✔
210
            this.minPanSpeed = options.minPanSpeed || defaultOptions.minPanSpeed;
4✔
211
            this.maxPanSpeed = options.maxPanSpeed || defaultOptions.maxPanSpeed;
4✔
212

4✔
213
            // camera altitude is clamped under maxAltitude
4✔
214
            this.maxAltitude = options.maxAltitude || defaultOptions.maxAltitude;
4✔
215

4✔
216
            // animation duration for the zoom
4✔
217
            this.zoomTravelTime = options.zoomTravelTime || defaultOptions.zoomTravelTime;
4✔
218
        }
4✔
219

5✔
220
        // zoom movement is equal to the distance to the zoom target, multiplied by zoomFactor
5✔
221
        if (options.zoomInFactor) {
5!
222
            console.warn('Controls zoomInFactor parameter is deprecated. Use zoomFactor instead.');
×
223
            options.zoomFactor = options.zoomFactor || options.zoomInFactor;
×
UNCOV
224
        }
×
225
        if (options.zoomOutFactor) {
5!
226
            console.warn('Controls zoomOutFactor parameter is deprecated. Use zoomFactor instead.');
×
227
            options.zoomFactor = options.zoomFactor || options.zoomInFactor || 1 / options.zoomOutFactor;
×
UNCOV
228
        }
×
229
        if (options.zoomFactor === 0) {
5!
230
            console.warn('Controls zoomFactor parameter can not be equal to 0. Its value will be set to default.');
×
231
            options.zoomFactor = defaultOptions.zoomFactor;
×
UNCOV
232
        }
×
233
        this.zoomInFactor = options.zoomFactor || defaultOptions.zoomFactor;
5✔
234
        this.zoomOutFactor = 1 / (options.zoomFactor || defaultOptions.zoomFactor);
5✔
235

5✔
236
        // the maximum and minimum size (in meters) a pixel at the center of the view can represent
5✔
237
        this.maxResolution = options.maxResolution || defaultOptions.maxResolution;
5✔
238
        this.minResolution = options.minResolution || defaultOptions.minResolution;
5✔
239

5✔
240
        // approximate ground altitude value. Camera altitude is clamped above groundLevel
5✔
241
        this.groundLevel = options.groundLevel || defaultOptions.groundLevel;
5✔
242

5✔
243
        // min and max duration in seconds, for animated travels with `auto` parameter
5✔
244
        this.autoTravelTimeMin = options.autoTravelTimeMin || defaultOptions.autoTravelTimeMin;
5✔
245
        this.autoTravelTimeMax = options.autoTravelTimeMax || defaultOptions.autoTravelTimeMax;
5✔
246

5✔
247
        // max travel duration is reached for this travel distance (empirical smoothing value)
5✔
248
        this.autoTravelTimeDist = options.autoTravelTimeDist || defaultOptions.autoTravelTimeDist;
5✔
249

5✔
250
        // after a smartZoom, camera height above ground will be between these two values
5✔
251
        if (options.smartZoomHeightMin) {
5!
252
            console.warn('Controls smartZoomHeightMin parameter is deprecated. Use smartTravelHeightMin instead.');
×
253
            options.smartTravelHeightMin = options.smartTravelHeightMin || options.smartZoomHeightMin;
×
UNCOV
254
        }
×
255
        if (options.smartZoomHeightMax) {
5!
256
            console.warn('Controls smartZoomHeightMax parameter is deprecated. Use smartTravelHeightMax instead.');
×
257
            options.smartTravelHeightMax = options.smartTravelHeightMax || options.smartZoomHeightMax;
×
UNCOV
258
        }
×
259
        this.smartTravelHeightMin = options.smartTravelHeightMin || defaultOptions.smartTravelHeightMin;
5✔
260
        this.smartTravelHeightMax = options.smartTravelHeightMax || defaultOptions.smartTravelHeightMax;
5✔
261

5✔
262
        // if set to true, animated travels have 0 duration
5✔
263
        this.instantTravel = options.instantTravel || defaultOptions.instantTravel;
5✔
264

5✔
265
        // the zenith angle for a camera rotation will be between these two values
5✔
266
        this.minZenithAngle = (options.minZenithAngle || defaultOptions.minZenithAngle) * Math.PI / 180;
5✔
267
        // max value should be less than 90 deg (90 = parallel to the ground)
5✔
268
        this.maxZenithAngle = (options.maxZenithAngle || defaultOptions.maxZenithAngle) * Math.PI / 180;
5✔
269

5✔
270
        // focus policy options
5✔
271
        if (options.focusOnMouseOver) {
5!
272
            console.warn('Planar controls \'focusOnMouseOver\' optional parameter has been removed.');
×
UNCOV
273
        }
×
274
        if (options.focusOnMouseClick) {
5!
275
            console.warn('Planar controls \'focusOnMouseClick\' optional parameter has been removed.');
×
UNCOV
276
        }
×
277

5✔
278
        // set collision options
5✔
279
        this.handleCollision = options.handleCollision === undefined ?
5✔
280
            defaultOptions.handleCollision : options.handleCollision;
5!
281
        this.minDistanceCollision = defaultOptions.minDistanceCollision;
5✔
282

5✔
283
        // enable smart travel
5✔
284
        this.enableSmartTravel = options.enableSmartTravel === undefined ? defaultOptions.enableSmartTravel : options.enableSmartTravel;
5!
285

5✔
286
        startPosition.copy(this.camera.position);
5✔
287
        startQuaternion.copy(this.camera.quaternion);
5✔
288

5✔
289
        // control state
5✔
290
        this.state = STATE.NONE;
5✔
291
        this.cursor = cursor;
5✔
292

5✔
293
        if (this.view.controls) {
5!
294
            // esLint-disable-next-line no-console
×
295
            console.warn('Deprecated use of PlanarControls. See examples to correct PlanarControls implementation.');
×
296
            this.view.controls.dispose();
×
UNCOV
297
        }
×
298
        this.view.controls = this;
5✔
299

5✔
300
        // eventListeners handlers
5✔
301
        this._handlerOnKeyDown = this.onKeyDown.bind(this);
5✔
302
        this._handlerOnMouseDown = this.onMouseDown.bind(this);
5✔
303
        this._handlerOnMouseUp = this.onMouseUp.bind(this);
5✔
304
        this._handlerOnMouseMove = this.onMouseMove.bind(this);
5✔
305
        this._handlerOnMouseWheel = this.onMouseWheel.bind(this);
5✔
306
        this._handlerContextMenu = this.onContextMenu.bind(this);
5✔
307
        this._handlerUpdate = this.update.bind(this);
5✔
308

5✔
309
        // add this PlanarControl instance to the view's frameRequesters
5✔
310
        // with this, PlanarControl.update() will be called each frame
5✔
311
        this.view.addFrameRequester(
5✔
312
            MAIN_LOOP_EVENTS.AFTER_CAMERA_UPDATE,
5✔
313
            this._handlerUpdate,
5✔
314
        );
5✔
315

5✔
316
        // event listeners for user input (to activate the controls)
5✔
317
        this.addInputListeners();
5✔
318
    }
5✔
319

1✔
320
    dispose() {
1✔
321
        this.removeInputListeners();
×
322
        this.view.removeFrameRequester(
×
323
            MAIN_LOOP_EVENTS.AFTER_CAMERA_UPDATE,
×
324
            this._handlerUpdate,
×
325
        );
×
UNCOV
326
    }
×
327

1✔
328
    /**
1✔
329
     * update the view and camera if needed, and handles the animated travel
1✔
330
     * @param   {number}    dt                  the delta time between two updates in millisecond
1✔
331
     * @param   {boolean}   updateLoopRestarted true if we just started rendering
1✔
332
     * @ignore
1✔
333
     */
1✔
334
    update(dt, updateLoopRestarted) {
1✔
335
        // dt will not be relevant when we just started rendering. We consider a 1-frame move in this case
15✔
336
        if (updateLoopRestarted) {
15!
337
            dt = 16;
×
UNCOV
338
        }
×
339
        const onMovement = this.state !== STATE.NONE;
15✔
340
        let camChanged = true;
15✔
341
        switch (this.state) {
15✔
342
            case STATE.TRAVEL:
15✔
343
                this.handleTravel(dt);
5✔
344
                break;
5✔
345
            case STATE.ORTHO_ZOOM:
15✔
346
                this.handleZoomOrtho(dt);
2✔
347
                break;
2✔
348
            case STATE.DRAG:
15✔
349
                this.handleDragMovement();
1✔
350
                break;
1✔
351
            case STATE.ROTATE:
15✔
352
                this.handleRotation();
1✔
353
                break;
1✔
354
            case STATE.PAN:
15✔
355
                this.handlePanMovement();
1✔
356
                break;
1✔
357
            case STATE.NONE:
15✔
358
            default:
15✔
359
                camChanged = false;
5✔
360
                break;
5✔
361
        }
15✔
362
        if (camChanged) {
15✔
363
            this.view.dispatchEvent({ type: VIEW_EVENTS.CAMERA_MOVED });
10✔
364
            this.view.notifyChange(this.camera);
10✔
365
        }
10✔
366
        // We test if camera collides to the geometry layer or is too close to the ground, and adjust its altitude in
15✔
367
        // case
15✔
368
        if (this.handleCollision) { // check distance to the ground/surface geometry (could be another geometry layer)
15✔
369
            this.view.camera.adjustAltitudeToAvoidCollisionWithLayer(
15✔
370
                this.view,
15✔
371
                this.view.tileLayer,
15✔
372
                this.minDistanceCollision,
15✔
373
            );
15✔
374
        }
15✔
375
        if (onMovement) {
15✔
376
            this.view.dispatchEvent({ type: PLANAR_CONTROL_EVENT.MOVED });
10✔
377
        }
10✔
378
        deltaMousePosition.set(0, 0);
15✔
379
    }
15✔
380

1✔
381
    /**
1✔
382
     * Initiate a drag movement (translation on (xy) plane). The movement value is derived from the actual world
1✔
383
     * point under the mouse cursor. This allows user to 'grab' a world point and drag it to move.
1✔
384
     *
1✔
385
     * @ignore
1✔
386
     */
1✔
387
    initiateDrag() {
1✔
388
        this.state = STATE.DRAG;
3✔
389

3✔
390
        // the world point under mouse cursor when the drag movement is started
3✔
391
        dragStart.copy(this.getWorldPointAtScreenXY(mousePosition));
3✔
392

3✔
393
        // the difference between start and end cursor position
3✔
394
        dragDelta.set(0, 0, 0);
3✔
395
    }
3✔
396

1✔
397
    /**
1✔
398
     * Handle the drag movement (translation on (xy) plane) when user moves the mouse while in STATE.DRAG. The
1✔
399
     * drag movement is previously initiated by [initiateDrag]{@link PlanarControls#initiateDrag}. Compute the
1✔
400
     * drag value and update the camera controls. The movement value is derived from the actual world point under
1✔
401
     * the mouse cursor. This allows the user to 'grab' a world point and drag it to move.
1✔
402
     *
1✔
403
     * @ignore
1✔
404
     */
1✔
405
    handleDragMovement() {
1✔
406
        // the world point under the current mouse cursor position, at same altitude than dragStart
1✔
407
        this.getWorldPointFromMathPlaneAtScreenXY(mousePosition, dragStart.z, dragEnd);
1✔
408

1✔
409
        // the difference between start and end cursor position
1✔
410
        dragDelta.subVectors(dragStart, dragEnd);
1✔
411

1✔
412
        // update the camera position
1✔
413
        this.camera.position.add(dragDelta);
1✔
414

1✔
415
        dragDelta.set(0, 0, 0);
1✔
416
    }
1✔
417

1✔
418
    /**
1✔
419
     * Initiate a pan movement (local translation on (xz) plane).
1✔
420
     *
1✔
421
     * @ignore
1✔
422
     */
1✔
423
    initiatePan() {
1✔
424
        this.state = STATE.PAN;
3✔
425
    }
3✔
426

1✔
427
    /**
1✔
428
     * Handle the pan movement (translation on local x / world z plane) when user moves the mouse while
1✔
429
     * STATE.PAN. The drag movement is previously initiated by [initiatePan]{@link PlanarControls#initiatePan}.
1✔
430
     * Compute the pan value and update the camera controls.
1✔
431
     *
1✔
432
     * @ignore
1✔
433
     */
1✔
434
    handlePanMovement() {
1✔
435
        vect.set(-deltaMousePosition.x, deltaMousePosition.y, 0);
1✔
436
        this.camera.localToWorld(vect);
1✔
437
        this.camera.position.copy(vect);
1✔
438
    }
1✔
439

1✔
440
    /**
1✔
441
     * Initiate a rotate (orbit) movement.
1✔
442
     *
1✔
443
     * @ignore
1✔
444
     */
1✔
445
    initiateRotation() {
1✔
446
        this.state = STATE.ROTATE;
3✔
447

3✔
448
        centerPoint.copy(this.getWorldPointAtScreenXY(new THREE.Vector2(
3✔
449
            0.5 * this.view.mainLoop.gfxEngine.width,
3✔
450
            0.5 * this.view.mainLoop.gfxEngine.height,
3✔
451
        )));
3✔
452

3✔
453
        const radius = this.camera.position.distanceTo(centerPoint);
3✔
454
        phi = Math.acos((this.camera.position.z - centerPoint.z) / radius);
3✔
455
    }
3✔
456

1✔
457
    /**
1✔
458
     * Handle the rotate movement (orbit) when user moves the mouse while in STATE.ROTATE. The movement is an
1✔
459
     * orbit around `centerPoint`, the camera focus point (ground point at screen center). The rotate movement
1✔
460
     * is previously initiated in [initiateRotation]{@link PlanarControls#initiateRotation}.
1✔
461
     * Compute the new position value and update the camera controls.
1✔
462
     *
1✔
463
     * @ignore
1✔
464
     */
1✔
465
    handleRotation() {
1✔
466
        // angle deltas
1✔
467
        // deltaMousePosition is computed in onMouseMove / onMouseDowns
1✔
468
        const thetaDelta = -this.rotateSpeed * deltaMousePosition.x / this.view.mainLoop.gfxEngine.width;
1✔
469
        const phiDelta = -this.rotateSpeed * deltaMousePosition.y / this.view.mainLoop.gfxEngine.height;
1✔
470

1✔
471
        // the vector from centerPoint (focus point) to camera position
1✔
472
        const offset = this.camera.position.clone().sub(centerPoint);
1✔
473

1✔
474
        if (thetaDelta !== 0 || phiDelta !== 0) {
1!
475
            if ((phi + phiDelta >= this.minZenithAngle)
1✔
476
            && (phi + phiDelta <= this.maxZenithAngle)
1✔
477
            && (phiDelta !== 0)) {
1!
478
                // rotation around X (altitude)
×
479
                phi += phiDelta;
×
480

×
481
                vect.set(0, 0, 1);
×
482
                quat.setFromUnitVectors(this.camera.up, vect);
×
483
                offset.applyQuaternion(quat);
×
484

×
485
                vect.setFromMatrixColumn(this.camera.matrix, 0);
×
486
                quat.setFromAxisAngle(vect, phiDelta);
×
487
                offset.applyQuaternion(quat);
×
488

×
489
                vect.set(0, 0, 1);
×
490
                quat.setFromUnitVectors(this.camera.up, vect).invert();
×
UNCOV
491
                offset.applyQuaternion(quat);
×
UNCOV
492
            }
×
493
            if (thetaDelta !== 0) {
1✔
494
                // rotation around Z (azimuth)
1✔
495
                vect.set(0, 0, 1);
1✔
496
                quat.setFromAxisAngle(vect, thetaDelta);
1✔
497
                offset.applyQuaternion(quat);
1✔
498
            }
1✔
499
        }
1✔
500

1✔
501
        this.camera.position.copy(offset);
1✔
502
        // TODO : lookAt calls an updateMatrixWorld(). It should be replaced by a new method that does not.
1✔
503
        this.camera.lookAt(vectorZero);
1✔
504
        this.camera.position.add(centerPoint);
1✔
505
        this.camera.updateMatrixWorld();
1✔
506
    }
1✔
507

1✔
508
    /**
1✔
509
     * Triggers a Zoom animated movement (travel) toward / away from the world point under the mouse cursor. The
1✔
510
     * zoom intensity varies according to the distance between the camera and the point. The closer to the ground,
1✔
511
     * the lower the intensity. Orientation will not change (null parameter in the call to
1✔
512
     * [initiateTravel]{@link PlanarControls#initiateTravel} function).
1✔
513
     *
1✔
514
     * @param   {Event} event   the mouse wheel event.
1✔
515
     * @ignore
1✔
516
     */
1✔
517
    initiateZoom(event) {
1✔
518
        const delta = -event.deltaY;
5✔
519

5✔
520
        pointUnderCursor.copy(this.getWorldPointAtScreenXY(mousePosition));
5✔
521
        const newPos = new THREE.Vector3();
5✔
522

5✔
523
        if (delta > 0 || (delta < 0 && this.maxAltitude > this.camera.position.z)) {
5✔
524
            const zoomFactor = delta > 0 ? this.zoomInFactor : this.zoomOutFactor;
5✔
525

5✔
526
            // do not zoom if the resolution after the zoom is outside resolution limits
5✔
527
            const endResolution = this.view.getPixelsToMeters() / zoomFactor;
5✔
528
            if (this.maxResolution > endResolution || endResolution > this.minResolution) {
5✔
529
                return;
1✔
530
            }
1✔
531

4✔
532
            // change the camera field of view if the camera is orthographic
4✔
533
            if (this.camera.isOrthographicCamera) {
5✔
534
                // switch state to STATE.ZOOM
2✔
535
                this.state = STATE.ORTHO_ZOOM;
2✔
536
                this.view.notifyChange(this.camera);
2✔
537

2✔
538
                // camera zoom at the beginning of zoom movement
2✔
539
                startZoom = this.camera.zoom;
2✔
540
                // camera zoom at the end of zoom movement
2✔
541
                endZoom = startZoom * zoomFactor;
2✔
542
                // the altitude of the target must be the same as camera's
2✔
543
                pointUnderCursor.z = this.camera.position.z;
2✔
544

2✔
545
                travelAlpha = 0;
2✔
546
                travelDuration = this.zoomTravelTime;
2✔
547
                this.updateMouseCursorType();
2✔
548
            } else {
2✔
549
                // target position
2✔
550
                newPos.lerpVectors(
2✔
551
                    this.camera.position,
2✔
552
                    pointUnderCursor,
2✔
553
                    (1 - 1 / zoomFactor),
2✔
554
                );
2✔
555
                // initiate travel
2✔
556
                this.initiateTravel(
2✔
557
                    newPos,
2✔
558
                    this.zoomTravelTime,
2✔
559
                    null,
2✔
560
                    false,
2✔
561
                );
2✔
562
            }
2✔
563
        }
5✔
564
    }
5✔
565

1✔
566
    /**
1✔
567
     * Handle the animated zoom change for an orthographic camera, when state is `ZOOM`.
1✔
568
     *
1✔
569
     * @param   {number}    dt  the delta time between two updates in milliseconds
1✔
570
     * @ignore
1✔
571
     */
1✔
572
    handleZoomOrtho(dt) {
1✔
573
        travelAlpha = Math.min(travelAlpha + (dt / 1000) / travelDuration, 1);
2✔
574

2✔
575
        // new zoom
2✔
576
        const zoom = startZoom + travelAlpha * (endZoom - startZoom);
2✔
577

2✔
578
        if (this.camera.zoom !== zoom) {
2✔
579
            // zoom has changed
2✔
580
            this.camera.zoom = zoom;
2✔
581
            this.camera.updateProjectionMatrix();
2✔
582

2✔
583
            // current world coordinates under the mouse
2✔
584
            this.view.viewToNormalizedCoords(mousePosition, vect);
2✔
585
            vect.z = 0;
2✔
586
            vect.unproject(this.camera);
2✔
587

2✔
588
            // new camera position
2✔
589
            this.camera.position.x += pointUnderCursor.x - vect.x;
2✔
590
            this.camera.position.y += pointUnderCursor.y - vect.y;
2✔
591

2✔
592
            this.camera.updateMatrixWorld(true);
2✔
593
        }
2✔
594

2✔
595
        // completion test
2✔
596
        this.testAnimationEnd();
2✔
597
    }
2✔
598

1✔
599
    /**
1✔
600
     * Triggers a 'smart zoom' animated movement (travel) toward the point under mouse cursor. The camera will be
1✔
601
     * smoothly moved and oriented close to the target, at a determined height and distance.
1✔
602
     *
1✔
603
     * @ignore
1✔
604
     */
1✔
605
    initiateSmartTravel() {
1✔
606
        const pointUnderCursor = this.getWorldPointAtScreenXY(mousePosition);
6✔
607

6✔
608
        // direction of the movement, projected on xy plane and normalized
6✔
609
        const dir = new THREE.Vector3();
6✔
610
        dir.copy(pointUnderCursor).sub(this.camera.position);
6✔
611
        dir.z = 0;
6✔
612
        dir.normalize();
6✔
613

6✔
614
        const distanceToPoint = this.camera.position.distanceTo(pointUnderCursor);
6✔
615

6✔
616
        // camera height (altitude above ground) at the end of the travel, 5000 is an empirical smoothing distance
6✔
617
        const targetHeight = THREE.MathUtils.lerp(
6✔
618
            this.smartTravelHeightMin,
6✔
619
            this.smartTravelHeightMax,
6✔
620
            Math.min(distanceToPoint / 5000, 1),
6✔
621
        );
6✔
622

6✔
623
        // camera position at the end of the travel
6✔
624
        const moveTarget = new THREE.Vector3();
6✔
625
        moveTarget.copy(pointUnderCursor);
6✔
626
        if (this.enableRotation) {
6✔
627
            moveTarget.add(dir.multiplyScalar(-targetHeight * 2));
5✔
628
        }
5✔
629
        moveTarget.z = pointUnderCursor.z + targetHeight;
6✔
630

6✔
631
        if (this.camera.isOrthographicCamera) {
6✔
632
            startZoom = this.camera.zoom;
1✔
633
            // camera zoom at the end of the travel, 5000 is an empirical smoothing distance
1✔
634
            endZoom = startZoom * (1 + Math.min(distanceToPoint / 5000, 1));
1✔
635
            moveTarget.z = this.camera.position.z;
1✔
636
        }
1✔
637

6✔
638
        // initiate the travel
6✔
639
        this.initiateTravel(
6✔
640
            moveTarget,
6✔
641
            'auto',
6✔
642
            pointUnderCursor,
6✔
643
            true,
6✔
644
        );
6✔
645
    }
6✔
646

1✔
647
    /**
1✔
648
     * Triggers an animated movement and rotation for the camera.
1✔
649
     *
1✔
650
     * @param   {THREE.Vector3} targetPos   The target position of the camera (reached at the end).
1✔
651
     * @param   {number|string}        travelTime  Set to `auto` or set to a duration in seconds. If set to `auto`,
1✔
652
     * travel time will be set to a duration between `autoTravelTimeMin` and `autoTravelTimeMax` according to
1✔
653
     * the distance and the angular difference between start and finish.
1✔
654
     * @param   {(string|THREE.Vector3|THREE.Quaternion)}   targetOrientation   define the target rotation of
1✔
655
     * the camera :
1✔
656
     * <ul>
1✔
657
     *     <li>if targetOrientation is a world point (Vector3) : the camera will lookAt() this point</li>
1✔
658
     *     <li>if targetOrientation is a quaternion : this quaternion will define the final camera orientation </li>
1✔
659
     *     <li>if targetOrientation is neither a world point nor a quaternion : the camera will keep its starting
1✔
660
     *     orientation</li>
1✔
661
     * </ul>
1✔
662
     * @param   {boolean}       useSmooth   animation is smoothed using the `smooth(value)` function (slower
1✔
663
     * at start and finish).
1✔
664
     *
1✔
665
     * @ignore
1✔
666
     */
1✔
667
    initiateTravel(targetPos, travelTime, targetOrientation, useSmooth) {
1✔
668
        this.state = STATE.TRAVEL;
10✔
669
        this.view.notifyChange(this.camera);
10✔
670
        // the progress of the travel (animation alpha)
10✔
671
        travelAlpha = 0;
10✔
672
        // update cursor
10✔
673
        this.updateMouseCursorType();
10✔
674

10✔
675
        travelUseRotation = this.enableRotation
10✔
676
            && targetOrientation
9✔
677
            && (targetOrientation.isQuaternion || targetOrientation.isVector3);
10✔
678
        travelUseSmooth = useSmooth;
10✔
679

10✔
680
        // start position (current camera position)
10✔
681
        travelStartPos.copy(this.camera.position);
10✔
682

10✔
683
        // start rotation (current camera rotation)
10✔
684
        travelStartRot.copy(this.camera.quaternion);
10✔
685

10✔
686
        // setup the end rotation :
10✔
687
        if (travelUseRotation) {
10✔
688
            if (targetOrientation.isQuaternion) {
7✔
689
                // case where targetOrientation is a quaternion
2✔
690
                travelEndRot.copy(targetOrientation);
2✔
691
            } else if (targetOrientation.isVector3) {
7✔
692
                // case where targetOrientation is a Vector3
5✔
693
                if (targetPos === targetOrientation) {
5!
694
                    this.camera.lookAt(targetOrientation);
×
UNCOV
695
                    travelEndRot.copy(this.camera.quaternion);
×
UNCOV
696
                    this.camera.quaternion.copy(travelStartRot);
×
697
                } else {
5✔
698
                    this.camera.position.copy(targetPos);
5✔
699
                    this.camera.lookAt(targetOrientation);
5✔
700
                    travelEndRot.copy(this.camera.quaternion);
5✔
701
                    this.camera.quaternion.copy(travelStartRot);
5✔
702
                    this.camera.position.copy(travelStartPos);
5✔
703
                }
5✔
704
            }
5✔
705
        }
7✔
706

10✔
707
        // end position
10✔
708
        travelEndPos.copy(targetPos);
10✔
709

10✔
710

10✔
711
        // beginning of the travel duration setup
10✔
712
        if (this.instantTravel) {
10✔
713
            travelDuration = 0;
1✔
714
        } else if (travelTime === 'auto') {
10✔
715
            // case where travelTime is set to `auto` : travelDuration will be a value between autoTravelTimeMin and
7✔
716
            // autoTravelTimeMax depending on travel distance and travel angular difference
7✔
717

7✔
718
            // a value between 0 and 1 according to the travel distance. Adjusted by autoTravelTimeDist parameter
7✔
719
            const normalizedDistance = Math.min(
7✔
720
                1,
7✔
721
                targetPos.distanceTo(this.camera.position) / this.autoTravelTimeDist,
7✔
722
            );
7✔
723

7✔
724
            travelDuration = THREE.MathUtils.lerp(
7✔
725
                this.autoTravelTimeMin,
7✔
726
                this.autoTravelTimeMax,
7✔
727
                normalizedDistance,
7✔
728
            );
7✔
729

7✔
730
            // if travel changes camera orientation, travel duration is adjusted according to angularDifference
7✔
731
            // this allows for a smoother travel (more time for the camera to rotate)
7✔
732
            // final duration will not exceed autoTravelTimeMax
7✔
733
            if (travelUseRotation) {
7✔
734
                // value is normalized between 0 and 1
6✔
735
                const angularDifference = 0.5 - 0.5 * travelEndRot.normalize().dot(this.camera.quaternion.normalize());
6✔
736

6✔
737
                travelDuration *= 1 + 2 * angularDifference;
6✔
738
                travelDuration = Math.min(travelDuration, this.autoTravelTimeMax);
6✔
739
            }
6✔
740
        } else {
9✔
741
            // case where travelTime !== `auto` : travelTime is a duration in seconds given as parameter
2✔
742
            travelDuration = travelTime;
2✔
743
        }
2✔
744
    }
10✔
745

1✔
746
    /**
1✔
747
     * Handle the animated movement and rotation of the camera in `travel` state.
1✔
748
     *
1✔
749
     * @param   {number}    dt  the delta time between two updates in milliseconds
1✔
750
     * @ignore
1✔
751
     */
1✔
752
    handleTravel(dt) {
1✔
753
        travelAlpha = Math.min(travelAlpha + (dt / 1000) / travelDuration, 1);
5✔
754

5✔
755
        // the animation alpha, between 0 (start) and 1 (finish)
5✔
756
        const alpha = (travelUseSmooth) ? this.smooth(travelAlpha) : travelAlpha;
5✔
757

5✔
758
        // new position
5✔
759
        this.camera.position.lerpVectors(
5✔
760
            travelStartPos,
5✔
761
            travelEndPos,
5✔
762
            alpha,
5✔
763
        );
5✔
764

5✔
765
        const zoom = startZoom + alpha * (endZoom - startZoom);
5✔
766
        // new zoom
5✔
767
        if (this.camera.isOrthographicCamera && this.camera.zoom !== zoom) {
5✔
768
            this.camera.zoom = zoom;
1✔
769
            this.camera.updateProjectionMatrix();
1✔
770
        }
1✔
771

5✔
772
        // new rotation
5✔
773
        if (travelUseRotation === true) {
5✔
774
            this.camera.quaternion.slerpQuaternions(
2✔
775
                travelStartRot,
2✔
776
                travelEndRot,
2✔
777
                alpha,
2✔
778
            );
2✔
779
        }
2✔
780

5✔
781
        // completion test
5✔
782
        this.testAnimationEnd();
5✔
783
    }
5✔
784

1✔
785
    /**
1✔
786
     * Test if the currently running animation is finished (travelAlpha reached 1).
1✔
787
     * If it is, reset controls to state NONE.
1✔
788
     *
1✔
789
     * @ignore
1✔
790
     */
1✔
791
    testAnimationEnd() {
1✔
792
        if (travelAlpha === 1) {
7✔
793
            // Resume normal behaviour after animation is completed
1✔
794
            this.state = STATE.NONE;
1✔
795
            this.updateMouseCursorType();
1✔
796
        }
1✔
797
    }
7✔
798

1✔
799
    /**
1✔
800
     * Triggers an animated movement (travel) to set the camera to top view, above the focus point,
1✔
801
     * at altitude = distanceToFocusPoint.
1✔
802
     *
1✔
803
     * @ignore
1✔
804
     */
1✔
805
    goToTopView() {
1✔
806
        const topViewPos = new THREE.Vector3();
1✔
807
        const targetQuat = new THREE.Quaternion();
1✔
808

1✔
809
        // the top view position is above the camera focus point, at an altitude = distanceToPoint
1✔
810
        topViewPos.copy(this.getWorldPointAtScreenXY(new THREE.Vector2(
1✔
811
            0.5 * this.view.mainLoop.gfxEngine.width,
1✔
812
            0.5 * this.view.mainLoop.gfxEngine.height,
1✔
813
        )));
1✔
814
        topViewPos.z += Math.min(this.maxAltitude, this.camera.position.distanceTo(topViewPos));
1✔
815

1✔
816
        targetQuat.setFromAxisAngle(new THREE.Vector3(1, 0, 0), 0);
1✔
817

1✔
818
        // initiate the travel
1✔
819
        this.initiateTravel(
1✔
820
            topViewPos,
1✔
821
            'auto',
1✔
822
            targetQuat,
1✔
823
            true,
1✔
824
        );
1✔
825
    }
1✔
826

1✔
827
    /**
1✔
828
     * Triggers an animated movement (travel) to set the camera to starting view
1✔
829
     *
1✔
830
     * @ignore
1✔
831
     */
1✔
832
    goToStartView() {
1✔
833
        // if startZoom and endZoom have not been set yet, give them neutral values
1✔
834
        if (this.camera.isOrthographicCamera) {
1!
835
            startZoom = this.camera.zoom;
×
UNCOV
836
            endZoom = cameraInitialZoom;
×
UNCOV
837
        }
×
838
        this.initiateTravel(
1✔
839
            startPosition,
1✔
840
            'auto',
1✔
841
            startQuaternion,
1✔
842
            true,
1✔
843
        );
1✔
844
    }
1✔
845

1✔
846
    /**
1✔
847
     * Returns the world point (xyz) under the posXY screen point. The point belong to an abstract mathematical
1✔
848
     * plane of specified altitude (does not us actual geometry).
1✔
849
     *
1✔
850
     * @param   {THREE.Vector2} posXY       the mouse position in screen space (unit : pixel)
1✔
851
     * @param   {number}        altitude    the altitude (z) of the mathematical plane
1✔
852
     * @param   {THREE.Vector3} target      the target vector3
1✔
853
     * @return  {THREE.Vector3}
1✔
854
     * @ignore
1✔
855
     */
1✔
856
    getWorldPointFromMathPlaneAtScreenXY(posXY, altitude, target = new THREE.Vector3()) {
1!
857
        vect2.copy(this.view.viewToNormalizedCoords(posXY));
1✔
858
        rayCaster.setFromCamera(vect2, this.camera);
1✔
859
        plane.constant = altitude;
1✔
860
        rayCaster.ray.intersectPlane(plane, target);
1✔
861

1✔
862
        return target;
1✔
863
    }
1✔
864

1✔
865
    /**
1✔
866
     * Returns the world point (xyz) under the posXY screen point. If geometry is under the cursor, the point is
1✔
867
     * obtained with getPickingPositionFromDepth. If no geometry is under the cursor, the point is obtained with
1✔
868
     * [getWorldPointFromMathPlaneAtScreenXY]{@link PlanarControls#getWorldPointFromMathPlaneAtScreenXY}.
1✔
869
     *
1✔
870
     * @param   {THREE.Vector2} posXY   the mouse position in screen space (unit : pixel)
1✔
871
     * @param   {THREE.Vector3} target  the target World coordinates.
1✔
872
     * @return  {THREE.Vector3}
1✔
873
     * @ignore
1✔
874
     */
1✔
875
    getWorldPointAtScreenXY(posXY, target = new THREE.Vector3()) {
1!
876
        // check if there is a valid geometry under cursor
18✔
877
        if (this.view.getPickingPositionFromDepth(posXY, target)) {
18✔
878
            return target;
18✔
879
        } else {
18!
880
            // if not, we use the mathematical plane at altitude = groundLevel
×
881
            this.getWorldPointFromMathPlaneAtScreenXY(
×
882
                posXY,
×
883
                this.groundLevel,
×
884
                target,
×
885
            );
×
UNCOV
886
            return target;
×
UNCOV
887
        }
×
888
    }
18✔
889

1✔
890
    /**
1✔
891
     * Add all the input event listeners (activate the controls).
1✔
892
     *
1✔
893
     * @ignore
1✔
894
     */
1✔
895
    addInputListeners() {
1✔
896
        this.view.domElement.addEventListener('keydown', this._handlerOnKeyDown, false);
5✔
897
        this.view.domElement.addEventListener('mousedown', this._handlerOnMouseDown, false);
5✔
898
        this.view.domElement.addEventListener('mouseup', this._handlerOnMouseUp, false);
5✔
899
        this.view.domElement.addEventListener('mouseleave', this._handlerOnMouseUp, false);
5✔
900
        this.view.domElement.addEventListener('mousemove', this._handlerOnMouseMove, false);
5✔
901
        this.view.domElement.addEventListener('wheel', this._handlerOnMouseWheel, false);
5✔
902
        // prevent the default context menu from appearing when right-clicking
5✔
903
        // this allows to use right-click for input without the menu appearing
5✔
904
        this.view.domElement.addEventListener('contextmenu', this._handlerContextMenu, false);
5✔
905
    }
5✔
906

1✔
907
    /**
1✔
908
     * Removes all the input listeners (deactivate the controls).
1✔
909
     *
1✔
910
     * @ignore
1✔
911
     */
1✔
912
    removeInputListeners() {
1✔
913
        this.view.domElement.removeEventListener('keydown', this._handlerOnKeyDown, true);
×
914
        this.view.domElement.removeEventListener('mousedown', this._handlerOnMouseDown, false);
×
915
        this.view.domElement.removeEventListener('mouseup', this._handlerOnMouseUp, false);
×
916
        this.view.domElement.removeEventListener('mouseleave', this._handlerOnMouseUp, false);
×
917
        this.view.domElement.removeEventListener('mousemove', this._handlerOnMouseMove, false);
×
918
        this.view.domElement.removeEventListener('wheel', this._handlerOnMouseWheel, false);
×
UNCOV
919
        this.view.domElement.removeEventListener('contextmenu', this._handlerContextMenu, false);
×
UNCOV
920
    }
×
921

1✔
922
    /**
1✔
923
     * Update the cursor image according to the control state.
1✔
924
     *
1✔
925
     * @ignore
1✔
926
     */
1✔
927
    updateMouseCursorType() {
1✔
928
        switch (this.state) {
36✔
929
            case STATE.NONE:
36✔
930
                this.view.domElement.style.cursor = this.cursor.default;
9✔
931
                break;
9✔
932
            case STATE.DRAG:
36✔
933
                this.view.domElement.style.cursor = this.cursor.drag;
3✔
934
                break;
3✔
935
            case STATE.PAN:
36✔
936
                this.view.domElement.style.cursor = this.cursor.pan;
3✔
937
                break;
3✔
938
            case STATE.TRAVEL:
36✔
939
                this.view.domElement.style.cursor = this.cursor.travel;
16✔
940
                break;
16✔
941
            case STATE.ORTHO_ZOOM:
36✔
942
                this.view.domElement.style.cursor = this.cursor.ortho_zoom;
2✔
943
                break;
2✔
944
            case STATE.ROTATE:
36✔
945
                this.view.domElement.style.cursor = this.cursor.rotate;
3✔
946
                break;
3✔
947
            default:
36!
UNCOV
948
                break;
×
949
        }
36✔
950
    }
36✔
951

1✔
952
    updateMousePositionAndDelta(event) {
1✔
953
        this.view.eventToViewCoords(event, mousePosition);
26✔
954

26✔
955
        deltaMousePosition.copy(mousePosition).sub(lastMousePosition);
26✔
956

26✔
957
        lastMousePosition.copy(mousePosition);
26✔
958
    }
26✔
959

1✔
960
    /**
1✔
961
     * cursor modification for a specifique state.
1✔
962
     *
1✔
963
     * @param   {string} state   the state in which we want to change the cursor ('default', 'drag', 'pan', 'travel', 'rotate').
1✔
964
     * @param   {string} newCursor   the css cursor we want to have for the specified state.
1✔
965
     * @ignore
1✔
966
     */
1✔
967
    setCursor(state, newCursor) {
1✔
968
        this.cursor[state] = newCursor;
×
UNCOV
969
        this.updateMouseCursorType();
×
UNCOV
970
    }
×
971

1✔
972
    /**
1✔
973
     * Catch and manage the event when a touch on the mouse is downs.
1✔
974
     *
1✔
975
     * @param   {Event} event   the current event (mouse left or right button clicked, mouse wheel button actioned).
1✔
976
     * @ignore
1✔
977
     */
1✔
978
    onMouseDown(event) {
1✔
979
        if (!this.enabled) {
22!
UNCOV
980
            return;
×
UNCOV
981
        }
×
982

22✔
983
        event.preventDefault();
22✔
984

22✔
985
        this.view.domElement.focus();
22✔
986

22✔
987
        if (STATE.NONE !== this.state) {
22✔
988
            return;
1✔
989
        }
1✔
990
        currentPressedButton = event.button;
21✔
991

21✔
992
        this.updateMousePositionAndDelta(event);
21✔
993

21✔
994
        if (mouseButtons.LEFTCLICK === event.button) {
22✔
995
            if (event.ctrlKey) {
8✔
996
                if (this.enableRotation) {
5✔
997
                    this.initiateRotation();
3✔
998
                } else {
5✔
999
                    return;
2✔
1000
                }
2✔
1001
            } else {
8✔
1002
                this.initiateDrag();
3✔
1003
            }
3✔
1004
        } else if (mouseButtons.MIDDLECLICK === event.button) {
22✔
1005
            if (this.enableSmartTravel) {
8✔
1006
                this.initiateSmartTravel();
5✔
1007
            } else {
8✔
1008
                return;
3✔
1009
            }
3✔
1010
        } else if (mouseButtons.RIGHTCLICK === event.button) {
13✔
1011
            if (this.enablePan) {
5✔
1012
                this.initiatePan();
3✔
1013
            } else {
5✔
1014
                return;
2✔
1015
            }
2✔
1016
        }
5✔
1017

14✔
1018
        this.updateMouseCursorType();
14✔
1019
    }
14✔
1020

1✔
1021
    /**
1✔
1022
     * Catch and manage the event when a touch on the mouse is released.
1✔
1023
     *
1✔
1024
     * @param   {Event} event   the current event
1✔
1025
     * @ignore
1✔
1026
     */
1✔
1027
    onMouseUp(event) {
1✔
1028
        event.preventDefault();
9✔
1029

9✔
1030
        // Does not interrupt ongoing camera action if state is TRAVEL or CAMERA_OTHO. This prevents interrupting a zoom
9✔
1031
        // movement or a smart travel by pressing any movement key.
9✔
1032
        // The camera action is also uninterrupted if the released button does not match the button triggering the
9✔
1033
        // ongoing action. This prevents for instance exiting drag mode when right-clicking while dragging the view.
9✔
1034
        if (STATE.TRAVEL !== this.state
9✔
1035
            && STATE.ORTHO_ZOOM !== this.state
8✔
1036
            && currentPressedButton === event.button) {
9✔
1037
            this.state = STATE.NONE;
8✔
1038
        }
8✔
1039

9✔
1040
        this.updateMouseCursorType();
9✔
1041
    }
9✔
1042

1✔
1043
    /**
1✔
1044
     * Catch and manage the event when the mouse is moved.
1✔
1045
     *
1✔
1046
     * @param   {Event} event   the current event.
1✔
1047
     * @ignore
1✔
1048
     */
1✔
1049
    onMouseMove(event) {
1✔
1050
        if (!this.enabled) {
5!
UNCOV
1051
            return;
×
UNCOV
1052
        }
×
1053

5✔
1054
        event.preventDefault();
5✔
1055

5✔
1056
        this.updateMousePositionAndDelta(event);
5✔
1057

5✔
1058
        // notify change if moving
5✔
1059
        if (STATE.NONE !== this.state) {
5✔
1060
            this.view.notifyChange();
3✔
1061
        }
3✔
1062
    }
5✔
1063

1✔
1064
    /**
1✔
1065
     * Catch and manage the event when a key is down.
1✔
1066
     *
1✔
1067
     * @param   {Event} event   the current event
1✔
1068
     * @ignore
1✔
1069
     */
1✔
1070
    onKeyDown(event) {
1✔
1071
        if (STATE.NONE !== this.state || !this.enabled) {
4✔
1072
            return;
1✔
1073
        }
1✔
1074
        switch (event.keyCode) {
3✔
1075
            case keys.T:
4✔
1076
                // going to top view is not relevant for an orthographic camera, since it is always top view
1✔
1077
                if (!this.camera.isOrthographicCamera) {
1✔
1078
                    this.goToTopView();
1✔
1079
                }
1✔
1080
                break;
1✔
1081
            case keys.Y:
4✔
1082
                this.goToStartView();
1✔
1083
                break;
1✔
1084
            case keys.SPACE:
4✔
1085
                if (this.enableSmartTravel) {
1✔
1086
                    this.initiateSmartTravel(event);
1✔
1087
                }
1✔
1088
                break;
1✔
1089
            default:
4!
UNCOV
1090
                break;
×
1091
        }
4✔
1092
    }
4✔
1093

1✔
1094
    /**
1✔
1095
     * Catch and manage the event when the mouse wheel is rolled.
1✔
1096
     *
1✔
1097
     * @param   {Event} event   the current event
1✔
1098
     * @ignore
1✔
1099
     */
1✔
1100
    onMouseWheel(event) {
1✔
1101
        if (!this.enabled) {
5!
UNCOV
1102
            return;
×
UNCOV
1103
        }
×
1104

5✔
1105
        event.preventDefault();
5✔
1106
        event.stopPropagation();
5✔
1107

5✔
1108
        if (STATE.NONE === this.state) {
5✔
1109
            this.initiateZoom(event);
5✔
1110
        }
5✔
1111
    }
5✔
1112

1✔
1113
    /**
1✔
1114
     * Catch and manage the event when the context menu is called (by a right-click on the window). We use this
1✔
1115
     * to prevent the context menu from appearing so we can use right click for other inputs.
1✔
1116
     *
1✔
1117
     * @param   {Event} event   the current event
1✔
1118
     * @ignore
1✔
1119
     */
1✔
1120
    onContextMenu(event) {
1✔
UNCOV
1121
        event.preventDefault();
×
UNCOV
1122
    }
×
1123

1✔
1124
    /**
1✔
1125
     * Smoothing function (sigmoid) : based on h01 Hermite function.
1✔
1126
     *
1✔
1127
     * @param   {number}    value   the value to be smoothed, between 0 and 1.
1✔
1128
     * @return  {number}            a value between 0 and 1.
1✔
1129
     * @ignore
1✔
1130
     */
1✔
1131
    smooth(value) {
1✔
1132
        // p between 1.0 and 1.5 (empirical)
3✔
1133
        const p = 1.20;
3✔
1134
        return (value ** 2 * (3 - 2 * value)) ** p;
3✔
1135
    }
3✔
1136
}
1✔
1137

1✔
1138
export default PlanarControls;
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc