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

iTowns / itowns / 10635241580

30 Aug 2024 03:26PM UTC coverage: 86.966% (-2.8%) from 89.766%
10635241580

push

github

jailln
feat(3dtiles): add new OGC3DTilesLayer using 3d-tiles-renderer-js

Deprecate C3DTilesLayer (replaced by OGC3DTilesLayer).
Add new iGLTFLoader that loads gltf 1.0 and 2.0 files.

2791 of 3694 branches covered (75.55%)

Branch coverage included in aggregate %.

480 of 644 new or added lines in 8 files covered. (74.53%)

2144 existing lines in 111 files now uncovered.

24319 of 27479 relevant lines covered (88.5%)

1024.72 hits per line

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

91.18
/src/Controls/PlanarControls.js
1
import * as THREE from 'three';
1✔
2
import { MAIN_LOOP_EVENTS } from 'Core/MainLoop';
1✔
3

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1✔
410
        // update the camera position
1✔
411
        this.camera.position.add(dragDelta);
1✔
412

1✔
413
        dragDelta.set(0, 0, 0);
1✔
414
    }
1✔
415

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

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

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

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

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

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

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

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

×
UNCOV
479
                vect.set(0, 0, 1);
×
UNCOV
480
                quat.setFromUnitVectors(this.camera.up, vect);
×
UNCOV
481
                offset.applyQuaternion(quat);
×
UNCOV
482

×
UNCOV
483
                vect.setFromMatrixColumn(this.camera.matrix, 0);
×
UNCOV
484
                quat.setFromAxisAngle(vect, phiDelta);
×
UNCOV
485
                offset.applyQuaternion(quat);
×
UNCOV
486

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2✔
590
            this.camera.updateMatrixWorld(true);
2✔
591
        }
2✔
592

2✔
593
        // completion test
2✔
594
        this.testAnimationEnd();
2✔
595
    }
2✔
596

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

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

6✔
612
        const distanceToPoint = this.camera.position.distanceTo(pointUnderCursor);
6✔
613

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

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

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

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

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

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

10✔
678
        // start position (current camera position)
10✔
679
        travelStartPos.copy(this.camera.position);
10✔
680

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

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

10✔
705
        // end position
10✔
706
        travelEndPos.copy(targetPos);
10✔
707

10✔
708

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

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

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

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

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

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

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

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

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

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

5✔
779
        // completion test
5✔
780
        this.testAnimationEnd();
5✔
781
    }
5✔
782

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

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

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

1✔
814
        targetQuat.setFromAxisAngle(new THREE.Vector3(1, 0, 0), 0);
1✔
815

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

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

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

1✔
860
        return target;
1✔
861
    }
1✔
862

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

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

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

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

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

26✔
953
        deltaMousePosition.copy(mousePosition).sub(lastMousePosition);
26✔
954

26✔
955
        lastMousePosition.copy(mousePosition);
26✔
956
    }
26✔
957

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

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

22✔
981
        event.preventDefault();
22✔
982

22✔
983
        this.view.domElement.focus();
22✔
984

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

21✔
990
        this.updateMousePositionAndDelta(event);
21✔
991

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

14✔
1016
        this.updateMouseCursorType();
14✔
1017
    }
14✔
1018

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

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

9✔
1038
        this.updateMouseCursorType();
9✔
1039
    }
9✔
1040

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

5✔
1052
        event.preventDefault();
5✔
1053

5✔
1054
        this.updateMousePositionAndDelta(event);
5✔
1055

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

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

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

5✔
1103
        event.preventDefault();
5✔
1104
        event.stopPropagation();
5✔
1105

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

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

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

1✔
1136
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