3dtiles-inspector 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1219 @@
1
+ import {
2
+ EventDispatcher,
3
+ Matrix4,
4
+ Plane,
5
+ Quaternion,
6
+ Ray,
7
+ Raycaster,
8
+ Vector2,
9
+ Vector3,
10
+ } from 'three';
11
+
12
+ const createUninitializedCallback = (name) => () => {
13
+ console.warn(`${name} was called before initialization.`);
14
+ };
15
+ const CAMERA_CENTER_MODE_DISTANCE = 3000000;
16
+ const CAMERA_CENTER_MODE_DISTANCE_SQ = CAMERA_CENTER_MODE_DISTANCE ** 2;
17
+
18
+ class PointerTracker {
19
+ buttons;
20
+ pointerType;
21
+ pointerOrder;
22
+ previousPositions;
23
+ pointerPositions;
24
+ startPositions;
25
+ hoverPosition;
26
+ hoverSet;
27
+ constructor() {
28
+ this.buttons = 0;
29
+ this.pointerType = null;
30
+ this.pointerOrder = [];
31
+ this.previousPositions = {};
32
+ this.pointerPositions = {};
33
+ this.startPositions = {};
34
+ this.hoverPosition = new Vector2();
35
+ this.hoverSet = false;
36
+ }
37
+ reset() {
38
+ this.buttons = 0;
39
+ this.pointerType = null;
40
+ this.pointerOrder = [];
41
+ this.previousPositions = {};
42
+ this.pointerPositions = {};
43
+ this.startPositions = {};
44
+ this.hoverPosition = new Vector2();
45
+ this.hoverSet = false;
46
+ }
47
+ // The pointers can be set multiple times per frame so track whether the pointer has
48
+ // been set this frame or not so we don't overwrite the previous position and lose information
49
+ // about pointer movement
50
+ updateFrame() {
51
+ const { previousPositions, pointerPositions } = this;
52
+ for (const id in pointerPositions) {
53
+ previousPositions[id].copy(pointerPositions[id]);
54
+ }
55
+ }
56
+ setHoverEvent(e) {
57
+ // @ts-expect-error
58
+ if (e.pointerType === 'mouse' || e.type === 'wheel') {
59
+ this.getAdjustedPointer(e, this.hoverPosition);
60
+ this.hoverSet = true;
61
+ }
62
+ }
63
+ getLatestPoint(target) {
64
+ if (this.pointerType !== null) {
65
+ this.getCenterPoint(target);
66
+ return target;
67
+ } else if (this.hoverSet) {
68
+ target.copy(this.hoverPosition);
69
+ return target;
70
+ } else {
71
+ return null;
72
+ }
73
+ }
74
+ // get the pointer position in the coordinate system of the target element
75
+ getAdjustedPointer(e, target) {
76
+ const domRef = e.target;
77
+ const rect = domRef.getBoundingClientRect();
78
+ const x = e.clientX - rect.left;
79
+ const y = e.clientY - rect.top;
80
+ target.set(x, y);
81
+ }
82
+ addPointer(e) {
83
+ // @ts-expect-error
84
+ const id = e.pointerId;
85
+ const position = new Vector2();
86
+ this.getAdjustedPointer(e, position);
87
+ if (this.pointerOrder.indexOf(id) === -1) {
88
+ this.pointerOrder.push(id);
89
+ }
90
+ this.pointerPositions[id] = position;
91
+ this.previousPositions[id] = position.clone();
92
+ this.startPositions[id] = position.clone();
93
+ if (this.getPointerCount() === 1) {
94
+ // @ts-expect-error
95
+ this.pointerType = e.pointerType;
96
+ this.buttons = e.buttons;
97
+ }
98
+ }
99
+ updatePointer(e) {
100
+ // @ts-expect-error
101
+ const id = e.pointerId;
102
+ if (!(id in this.pointerPositions)) {
103
+ return false;
104
+ }
105
+ this.getAdjustedPointer(e, this.pointerPositions[id]);
106
+ return true;
107
+ }
108
+ deletePointer(e) {
109
+ const id = e.pointerId;
110
+ const pointerOrder = this.pointerOrder;
111
+ pointerOrder.splice(pointerOrder.indexOf(id), 1);
112
+ delete this.pointerPositions[id];
113
+ delete this.previousPositions[id];
114
+ delete this.startPositions[id];
115
+ if (this.getPointerCount() === 0) {
116
+ this.buttons = 0;
117
+ this.pointerType = null;
118
+ }
119
+ }
120
+ getPointerCount() {
121
+ return this.pointerOrder.length;
122
+ }
123
+ getCenterPoint(target, pointerPositions = this.pointerPositions) {
124
+ const pointerOrder = this.pointerOrder;
125
+ if (this.getPointerCount() === 1 || this.getPointerType() === 'mouse') {
126
+ const id = pointerOrder[0];
127
+ target.copy(pointerPositions[id]);
128
+ return target;
129
+ } else if (this.getPointerCount() === 2) {
130
+ const id0 = this.pointerOrder[0];
131
+ const id1 = this.pointerOrder[1];
132
+ const p0 = pointerPositions[id0];
133
+ const p1 = pointerPositions[id1];
134
+ target.addVectors(p0, p1).multiplyScalar(0.5);
135
+ return target;
136
+ } else if (this.getPointerCount() > 2) {
137
+ target.set(0, 0);
138
+ for (let i = 0; i < pointerOrder.length; i++) {
139
+ const id = pointerOrder[i];
140
+ target.add(pointerPositions[id]);
141
+ }
142
+ target.divideScalar(pointerOrder.length);
143
+ return target;
144
+ }
145
+ return null;
146
+ }
147
+ getPreviousCenterPoint(target) {
148
+ return this.getCenterPoint(target, this.previousPositions);
149
+ }
150
+ getStartCenterPoint(target) {
151
+ return this.getCenterPoint(target, this.startPositions);
152
+ }
153
+ getMoveDistance() {
154
+ this.getCenterPoint(_vec);
155
+ this.getPreviousCenterPoint(_vec2);
156
+ return _vec.sub(_vec2).length();
157
+ }
158
+ getTouchPointerDistance(pointerPositions = this.pointerPositions) {
159
+ if (this.getPointerCount() <= 1 || this.getPointerType() === 'mouse') {
160
+ return 0;
161
+ }
162
+ const { pointerOrder } = this;
163
+ const id0 = pointerOrder[0];
164
+ const id1 = pointerOrder[1];
165
+ const p0 = pointerPositions[id0];
166
+ const p1 = pointerPositions[id1];
167
+ return p0.distanceTo(p1);
168
+ }
169
+ getPreviousTouchPointerDistance() {
170
+ return this.getTouchPointerDistance(this.previousPositions);
171
+ }
172
+ getStartTouchPointerDistance() {
173
+ return this.getTouchPointerDistance(this.startPositions);
174
+ }
175
+ getPointerType() {
176
+ return this.pointerType;
177
+ }
178
+ isPointerTouch() {
179
+ return this.getPointerType() === 'touch';
180
+ }
181
+ getPointerButtons() {
182
+ return this.buttons;
183
+ }
184
+ isLeftClicked() {
185
+ return Boolean(this.buttons & 1);
186
+ }
187
+ isRightClicked() {
188
+ return Boolean(this.buttons & 2);
189
+ }
190
+ isMiddleClicked() {
191
+ return Boolean(this.buttons & 4);
192
+ }
193
+ }
194
+ const _matrix = new Matrix4();
195
+ // custom version of set raycaster from camera that relies on the underlying matrices
196
+ // so the ray origin is position at the camera near clip.
197
+ function setRaycasterFromCamera(raycaster, coords, camera) {
198
+ const ray = raycaster instanceof Ray ? raycaster : raycaster.ray;
199
+ const { origin, direction } = ray;
200
+ // With reversed depth the NDC z range is [1, 0] (near→1, far→0)
201
+ // instead of the standard [-1, 1] (near→-1, far→1).
202
+ const nearZ = camera.reversedDepth ? 1 : -1;
203
+ const farZ = camera.reversedDepth ? 0 : 1;
204
+ // get the origin and direction of the frustum ray
205
+ origin.set(coords.x, coords.y, nearZ).unproject(camera);
206
+ direction.set(coords.x, coords.y, farZ).unproject(camera).sub(origin);
207
+ // @ts-expect-error
208
+ if (!raycaster.isRay) {
209
+ // compute the far value based on the distance from point on the near
210
+ // plane and point on the far plane. Then normalize the direction.
211
+ // @ts-expect-error
212
+ raycaster.near = 0;
213
+ // @ts-expect-error
214
+ raycaster.far = direction.length();
215
+ // @ts-expect-error
216
+ raycaster.camera = camera;
217
+ }
218
+ // normalize the ray direction
219
+ direction.normalize();
220
+ }
221
+ function mouseToCoords(clientX, clientY, element, target) {
222
+ const rect = element.getBoundingClientRect();
223
+ target.x = ((clientX - rect.left) / rect.width) * 2 - 1;
224
+ target.y = -((clientY - rect.top) / rect.height) * 2 + 1;
225
+ }
226
+ function makeRotateAroundPoint(point, quat, target) {
227
+ target.makeTranslation(-point.x, -point.y, -point.z);
228
+ _matrix.makeRotationFromQuaternion(quat);
229
+ target.premultiply(_matrix);
230
+ _matrix.makeTranslation(point.x, point.y, point.z);
231
+ target.premultiply(_matrix);
232
+ return target;
233
+ }
234
+ const NONE = 0;
235
+ const DRAG = 1;
236
+ const ROTATE = 2;
237
+ const IDLE = 3;
238
+ const START_EVENT = { type: 'start' };
239
+ const UPDATE_EVENT = { type: 'update' };
240
+ const FINISH_EVENT = { type: 'finish' };
241
+ const THRESHOLD = 1e-3;
242
+ const MAX = 1e8;
243
+ const VIRTUAL_HIT_DISTANCE = 50;
244
+ const ZOOM_OUT_TRANSITION_COS_THRESHOLD = Math.cos((105 * Math.PI) / 180);
245
+ const ZOOM_OUT_TRANSITION_COS_MAX_THRESHOLD = Math.cos((95 * Math.PI) / 180);
246
+ const _pointer = new Vector2();
247
+ const _pointer1 = new Vector2();
248
+ const _pointer2 = new Vector2();
249
+ const _pivotPoint = new Vector3();
250
+ const _up = new Vector3(0, 1, 0);
251
+ const _right = new Vector3(1, 0, 0);
252
+ const _forward = new Vector3(0, 0, -1);
253
+ const _worldZ = new Vector3(0, 0, 1);
254
+ const _vec = new Vector3(1, 1, 1);
255
+ const _vec1 = new Vector3();
256
+ const _vec2 = new Vector3();
257
+ const _vec3 = new Vector3();
258
+ const _vec4 = new Vector3();
259
+ const _vec5 = new Vector3();
260
+ const _vec6 = new Vector3();
261
+ const _axis = new Vector3();
262
+ const _localUp = new Vector3();
263
+ const _localRight = new Vector3();
264
+ const _localForward = new Vector3();
265
+ const _rotMatrix = new Matrix4();
266
+ const _invMatrix = new Matrix4();
267
+ const _quaternion = new Quaternion();
268
+ const _plane = new Plane();
269
+ const _ray = new Ray();
270
+ const _zoomOutMetrics = { distanceScale: 1, transitionWeight: 0 };
271
+ class CameraController extends EventDispatcher {
272
+ enableDamping;
273
+ dampingFactor;
274
+ state;
275
+ zooming;
276
+ touchZooming;
277
+ minDistance;
278
+ minZoomLimit;
279
+ #pointerTracker;
280
+ #domElement;
281
+ #camera;
282
+ #scene;
283
+ #raycaster;
284
+ #zoomDelta;
285
+ #zoomInertia;
286
+ #rotateInertia;
287
+ #dragInertia;
288
+ #dragAnchorPoint;
289
+ #dragStartPosition;
290
+ #dragStartQuaternion;
291
+ #dragPlaneNormal;
292
+ #inertiaValue;
293
+ #enabled;
294
+ #ellipsoid;
295
+ #ellipsoidMaxRadius;
296
+ #lastTime;
297
+ #hit;
298
+ #contextMenuEvent = createUninitializedCallback(
299
+ 'CameraController.#contextMenuEvent',
300
+ );
301
+ #pointerDownEvent = createUninitializedCallback(
302
+ 'CameraController.#pointerDownEvent',
303
+ );
304
+ #pointerMoveEvent = createUninitializedCallback(
305
+ 'CameraController.#pointerMoveEvent',
306
+ );
307
+ #pointerUpEvent = createUninitializedCallback(
308
+ 'CameraController.#pointerUpEvent',
309
+ );
310
+ #wheelEvent = createUninitializedCallback('CameraController.#wheelEvent');
311
+ #pointerEnterEvent = createUninitializedCallback(
312
+ 'CameraController.#pointerEnterEvent',
313
+ );
314
+ #zoomTimeout;
315
+ constructor(renderer, scene, camera, options = {}) {
316
+ super();
317
+ if (typeof options !== 'object' || options === null) {
318
+ options = {};
319
+ }
320
+ this.#scene = scene;
321
+ this.#camera = camera;
322
+ this.#domElement = options.domElement ?? renderer.domElement;
323
+ this.enableDamping = options.enableDamping ?? true;
324
+ this.dampingFactor = 0.8;
325
+ this.minDistance = 0.5;
326
+ this.minZoomLimit = false;
327
+ this.state = NONE;
328
+ this.zooming = false;
329
+ this.touchZooming = false;
330
+ this.#pointerTracker = new PointerTracker();
331
+ this.#raycaster = new Raycaster();
332
+ this.#raycaster.params.Points.threshold = 0.1;
333
+ this.#zoomDelta = 0;
334
+ this.#zoomInertia = 0;
335
+ this.#rotateInertia = new Vector2();
336
+ this.#dragInertia = new Vector3();
337
+ this.#dragAnchorPoint = new Vector3();
338
+ this.#dragStartPosition = new Vector3();
339
+ this.#dragStartQuaternion = new Quaternion();
340
+ this.#dragPlaneNormal = new Vector3();
341
+ this.#inertiaValue = 0;
342
+ this.#enabled = false;
343
+ this.#ellipsoid = null;
344
+ this.#ellipsoidMaxRadius = 0;
345
+ this.#lastTime = 0;
346
+ this.#hit = null;
347
+ this.#zoomTimeout = null;
348
+ this.init();
349
+ }
350
+ get enabled() {
351
+ return this.#enabled;
352
+ }
353
+ set enabled(v) {
354
+ if (v !== this.enabled) {
355
+ this.#enabled = v;
356
+ this.#resetState();
357
+ this.#pointerTracker.reset();
358
+ if (!this.enabled) {
359
+ this.#dragInertia.set(0, 0, 0);
360
+ this.#rotateInertia.set(0, 0);
361
+ this.#zoomInertia = 0;
362
+ this.#inertiaValue = 0;
363
+ }
364
+ this.dispatchEvent(UPDATE_EVENT);
365
+ this.dispatchEvent(FINISH_EVENT);
366
+ }
367
+ }
368
+ get camera() {
369
+ return this.#camera;
370
+ }
371
+ #setState(state = this.state) {
372
+ if (this.state === state) {
373
+ return;
374
+ }
375
+ this.state = state;
376
+ }
377
+ #setZooming(zooming, touchZooming = false) {
378
+ this.zooming = zooming;
379
+ this.touchZooming = touchZooming;
380
+ }
381
+ #resetState() {
382
+ this.state = NONE;
383
+ this.zooming = false;
384
+ this.touchZooming = false;
385
+ this.#inertiaValue = 0;
386
+ this.#rotateInertia.set(0, 0);
387
+ this.#dragInertia.set(0, 0, 0);
388
+ this.#dragAnchorPoint.set(0, 0, 0);
389
+ this.#dragStartPosition.set(0, 0, 0);
390
+ this.#dragStartQuaternion.identity();
391
+ this.#dragPlaneNormal.set(0, 0, 0);
392
+ this.#zoomInertia = 0;
393
+ this.#hit = null;
394
+ }
395
+ setCamera(camera) {
396
+ this.#camera = camera;
397
+ this.#resetState();
398
+ this.dispatchEvent(UPDATE_EVENT);
399
+ this.dispatchEvent(FINISH_EVENT);
400
+ }
401
+ setEllipsoid(ellipsoid) {
402
+ this.#ellipsoid = ellipsoid;
403
+ const r = ellipsoid.radius;
404
+ this.#ellipsoidMaxRadius = Math.max(r.x, r.y, r.z);
405
+ }
406
+ init() {
407
+ this.#domElement.style.touchAction = 'none';
408
+ this.#contextMenuEvent = this.#contextMenu.bind(this);
409
+ this.#pointerDownEvent = this.#pointerDown.bind(this);
410
+ this.#pointerMoveEvent = this.#pointerMove.bind(this);
411
+ this.#pointerUpEvent = this.#pointerUp.bind(this);
412
+ this.#wheelEvent = this.#wheel.bind(this);
413
+ this.#pointerEnterEvent = this.#pointerEnter.bind(this);
414
+ this.#bindEvents();
415
+ this.#enabled = true;
416
+ }
417
+ update(time = performance.now()) {
418
+ const deltaTime = time - this.#lastTime;
419
+ if (!this.#enabled || !this.#camera || deltaTime === 0) {
420
+ return;
421
+ }
422
+ this.#lastTime = time;
423
+ if (this.state === NONE && !this.zooming) {
424
+ return;
425
+ }
426
+ const factor =
427
+ (deltaTime * (1 - this.dampingFactor)) /
428
+ (50 +
429
+ 50 * (1 - this.dampingFactor) +
430
+ Math.max(0.001, (1 - this.#inertiaValue) ** 3) * 50);
431
+ this.#inertiaValue -= factor;
432
+ this.#inertiaValue = Math.max(this.#inertiaValue, 0);
433
+ if (this.state === ROTATE) {
434
+ this.#pointerTracker.getCenterPoint(_pointer1);
435
+ this.#pointerTracker.getPreviousCenterPoint(_pointer2);
436
+ if (!_pointer1.equals(_pointer2)) {
437
+ _pointer
438
+ .subVectors(_pointer2, _pointer1)
439
+ .multiplyScalar((2 * Math.PI) / this.#domElement.clientHeight);
440
+ this.#rotate(_pointer);
441
+ this.#rotateInertia.copy(_pointer);
442
+ this.#inertiaValue = 1;
443
+ this.#zoomInertia = 0;
444
+ this.#dragInertia.set(0, 0, 0);
445
+ this.#finalizeCamera();
446
+ this.dispatchEvent(UPDATE_EVENT);
447
+ }
448
+ } else if (this.state === DRAG) {
449
+ this.#pointerTracker.getCenterPoint(_pointer1);
450
+ this.#pointerTracker.getPreviousCenterPoint(_pointer2);
451
+ if (!_pointer1.equals(_pointer2) && this.#hit && this.#hit.distance > 0) {
452
+ mouseToCoords(_pointer1.x, _pointer1.y, this.#domElement, _pointer1);
453
+ mouseToCoords(_pointer2.x, _pointer2.y, this.#domElement, _pointer2);
454
+ this.#restoreDragStartCamera();
455
+ if (
456
+ this.#intersectDragPlane(_pointer1, _vec1) &&
457
+ this.#intersectDragPlane(_pointer2, _vec2)
458
+ ) {
459
+ _vec.subVectors(_vec1, this.#dragAnchorPoint);
460
+ _vec5.subVectors(_vec1, _vec2);
461
+ if (this.#shouldDragModified()) {
462
+ this.#modifiedDrag(_vec);
463
+ } else {
464
+ this.#camera.position.sub(_vec);
465
+ }
466
+ this.#dragInertia.copy(_vec5);
467
+ this.#inertiaValue = 1;
468
+ this.#rotateInertia.set(0, 0);
469
+ this.#zoomInertia = 0;
470
+ this.#finalizeCamera();
471
+ this.dispatchEvent(UPDATE_EVENT);
472
+ }
473
+ }
474
+ } else if (this.state === IDLE) {
475
+ if (this.enableDamping) {
476
+ if (this.#rotateInertia.lengthSq() > 0 && this.#inertiaValue > 0) {
477
+ _pointer.copy(this.#rotateInertia).multiplyScalar(this.#inertiaValue);
478
+ this.#rotate(_pointer);
479
+ this.#finalizeCamera();
480
+ } else if (this.#dragInertia.lengthSq() > 0 && this.#inertiaValue > 0) {
481
+ if (this.#shouldDragModified()) {
482
+ _vec.copy(this.#dragInertia).multiplyScalar(this.#inertiaValue);
483
+ this.#modifiedDrag(_vec);
484
+ this.#finalizeCamera();
485
+ } else {
486
+ _vec.copy(this.#dragInertia).multiplyScalar(this.#inertiaValue);
487
+ this.#camera.position.sub(_vec);
488
+ this.#finalizeCamera();
489
+ }
490
+ }
491
+ if (
492
+ (this.#rotateInertia.lengthSq() === 0 &&
493
+ this.#dragInertia.lengthSq() === 0) ||
494
+ this.#inertiaValue === 0
495
+ ) {
496
+ this.#rotateInertia.set(0, 0);
497
+ this.#dragInertia.set(0, 0, 0);
498
+ if (!this.zooming) {
499
+ this.#resetState();
500
+ this.dispatchEvent(UPDATE_EVENT);
501
+ this.dispatchEvent(FINISH_EVENT);
502
+ } else {
503
+ this.#setState(NONE);
504
+ this.dispatchEvent(UPDATE_EVENT);
505
+ }
506
+ } else {
507
+ this.dispatchEvent(UPDATE_EVENT);
508
+ }
509
+ } else {
510
+ this.#rotateInertia.set(0, 0);
511
+ this.#dragInertia.set(0, 0, 0);
512
+ this.#zoomInertia = 0;
513
+ this.#resetState();
514
+ this.dispatchEvent(UPDATE_EVENT);
515
+ this.dispatchEvent(FINISH_EVENT);
516
+ }
517
+ }
518
+ if (this.zooming) {
519
+ if (this.touchZooming) {
520
+ const previousDistance =
521
+ this.#pointerTracker.getPreviousTouchPointerDistance();
522
+ const currentDistance = this.#pointerTracker.getTouchPointerDistance();
523
+ const delta =
524
+ (currentDistance - previousDistance) /
525
+ Math.sqrt(
526
+ this.#domElement.clientWidth ** 2 +
527
+ this.#domElement.clientHeight ** 2,
528
+ );
529
+ this.#zoomDelta = delta * 4000;
530
+ }
531
+ if (this.#zoomDelta !== 0) {
532
+ if (this.#zoomTimeout !== null) {
533
+ clearTimeout(this.#zoomTimeout);
534
+ this.#zoomTimeout = null;
535
+ }
536
+ if (this.#zoomDelta <= 0 && this.#reachCameraMaxDistance()) {
537
+ this.#zoomDelta = 0;
538
+ } else {
539
+ this.#applyZoom(this.#zoomDelta);
540
+ }
541
+ this.#zoomInertia = this.#zoomDelta;
542
+ this.#inertiaValue = 1;
543
+ this.#zoomDelta = 0;
544
+ this.dispatchEvent(UPDATE_EVENT);
545
+ if (!this.enableDamping) {
546
+ this.#zoomTimeout = setTimeout(() => {
547
+ this.#zoomInertia = 0;
548
+ this.#zoomTimeout = null;
549
+ if (this.state === NONE || this.state === IDLE) {
550
+ this.#resetState();
551
+ this.dispatchEvent(UPDATE_EVENT);
552
+ this.dispatchEvent(FINISH_EVENT);
553
+ }
554
+ }, 500);
555
+ }
556
+ } else if (this.enableDamping && this.#inertiaValue > 0) {
557
+ if (this.#zoomInertia <= 0 && this.#reachCameraMaxDistance()) {
558
+ this.#zoomInertia = 0;
559
+ this.dispatchEvent(UPDATE_EVENT);
560
+ } else {
561
+ if (
562
+ this.#zoomInertia !== 0 &&
563
+ this.#inertiaValue > 0 &&
564
+ this.#hit &&
565
+ this.#hit.distance > 0
566
+ ) {
567
+ this.#applyZoom(this.#zoomInertia * this.#inertiaValue);
568
+ this.dispatchEvent(UPDATE_EVENT);
569
+ } else {
570
+ this.#zoomInertia = 0;
571
+ if (this.state === NONE) {
572
+ this.#resetState();
573
+ this.dispatchEvent(UPDATE_EVENT);
574
+ this.dispatchEvent(FINISH_EVENT);
575
+ }
576
+ }
577
+ }
578
+ } else if (this.enableDamping && this.state === NONE) {
579
+ this.#resetState();
580
+ this.dispatchEvent(UPDATE_EVENT);
581
+ this.dispatchEvent(FINISH_EVENT);
582
+ } else {
583
+ this.#zoomDelta = 0;
584
+ this.#zoomInertia = 0;
585
+ }
586
+ }
587
+ this.#pointerTracker.updateFrame();
588
+ }
589
+ dispose() {
590
+ if (this.#zoomTimeout !== null) {
591
+ clearTimeout(this.#zoomTimeout);
592
+ this.#zoomTimeout = null;
593
+ }
594
+ this.#domElement.removeEventListener('contextmenu', this.#contextMenuEvent);
595
+ this.#domElement.removeEventListener('pointerdown', this.#pointerDownEvent);
596
+ this.#domElement.removeEventListener('pointermove', this.#pointerMoveEvent);
597
+ this.#domElement.removeEventListener('pointerup', this.#pointerUpEvent);
598
+ this.#domElement.removeEventListener('wheel', this.#wheelEvent);
599
+ this.#domElement.removeEventListener(
600
+ 'pointerenter',
601
+ this.#pointerEnterEvent,
602
+ );
603
+ this.#domElement.style.touchAction = '';
604
+ this.#enabled = false;
605
+ this.#ellipsoid = null;
606
+ }
607
+ #bindEvents() {
608
+ this.#domElement.addEventListener('contextmenu', this.#contextMenuEvent);
609
+ this.#domElement.addEventListener('pointerdown', this.#pointerDownEvent);
610
+ this.#domElement.addEventListener('pointermove', this.#pointerMoveEvent);
611
+ this.#domElement.addEventListener('pointerup', this.#pointerUpEvent);
612
+ this.#domElement.addEventListener('wheel', this.#wheelEvent);
613
+ this.#domElement.addEventListener('pointerenter', this.#pointerEnterEvent);
614
+ }
615
+ #contextMenu = (e) => {
616
+ e.preventDefault();
617
+ };
618
+ #pointerDown = (e) => {
619
+ if (!this.#enabled) {
620
+ return;
621
+ }
622
+ this.#pointerTracker.addPointer(e);
623
+ if (
624
+ (this.#pointerTracker.getPointerCount() === 2 &&
625
+ this.#pointerTracker.isPointerTouch()) ||
626
+ (!this.#pointerTracker.isPointerTouch() &&
627
+ this.#pointerTracker.isRightClicked()) ||
628
+ (this.#pointerTracker.isLeftClicked() && e.shiftKey)
629
+ ) {
630
+ this.#setState(DRAG);
631
+ this.#setZooming(false);
632
+ } else if (
633
+ (this.#pointerTracker.getPointerCount() === 1 &&
634
+ this.#pointerTracker.isPointerTouch()) ||
635
+ (!this.#pointerTracker.isPointerTouch() &&
636
+ this.#pointerTracker.isLeftClicked() &&
637
+ !e.shiftKey)
638
+ ) {
639
+ this.#setState(ROTATE);
640
+ this.#setZooming(false);
641
+ }
642
+ if (
643
+ this.#pointerTracker.getPointerCount() === 2 &&
644
+ this.#pointerTracker.isPointerTouch()
645
+ ) {
646
+ this.#setZooming(true, true);
647
+ }
648
+ if (this.state === NONE) {
649
+ this.#setState(IDLE);
650
+ }
651
+ if (this.state === ROTATE || this.state === DRAG || this.zooming) {
652
+ this.#pointerTracker.getCenterPoint(_pointer1);
653
+ mouseToCoords(_pointer1.x, _pointer1.y, this.#domElement, _pointer1);
654
+ setRaycasterFromCamera(this.#raycaster, _pointer1, this.#camera);
655
+ this.#hit = this.#raycast(this.#raycaster);
656
+ if (this.state === DRAG && this.#hit.distance > 0) {
657
+ this.#initializeDragAnchor();
658
+ }
659
+ this.dispatchEvent(START_EVENT);
660
+ }
661
+ this.#rotateInertia.set(0, 0);
662
+ this.#dragInertia.set(0, 0, 0);
663
+ this.#zoomInertia = 0;
664
+ this.#zoomDelta = 0;
665
+ this.dispatchEvent(UPDATE_EVENT);
666
+ };
667
+ #pointerMove = (e) => {
668
+ e.preventDefault();
669
+ if (!this.#enabled) {
670
+ return;
671
+ }
672
+ this.#pointerTracker.setHoverEvent(e);
673
+ if (!this.#pointerTracker.updatePointer(e)) {
674
+ return;
675
+ }
676
+ };
677
+ #pointerUp = (e) => {
678
+ this.#pointerTracker.deletePointer(e);
679
+ if (!this.#enabled) {
680
+ return;
681
+ }
682
+ if (this.zooming || this.state !== NONE) {
683
+ this.#setState(IDLE);
684
+ }
685
+ this.dispatchEvent(UPDATE_EVENT);
686
+ };
687
+ #wheel = (e) => {
688
+ e.preventDefault();
689
+ if (!this.#enabled) {
690
+ return;
691
+ }
692
+ const tooClose = this.#hit
693
+ ? this.#hit.point.distanceTo(this.#camera.position) <= this.#camera.near
694
+ : false;
695
+ if (!this.zooming || tooClose) {
696
+ this.#rotateInertia.set(0, 0);
697
+ this.#dragInertia.set(0, 0, 0);
698
+ this.#zoomInertia = 0;
699
+ this.#zoomDelta = 0;
700
+ }
701
+ this.#pointerTracker.setHoverEvent(e);
702
+ this.#pointerTracker.updatePointer(e);
703
+ this.#pointerTracker.getLatestPoint(_pointer1);
704
+ mouseToCoords(_pointer1.x, _pointer1.y, this.#domElement, _pointer1);
705
+ setRaycasterFromCamera(this.#raycaster, _pointer1, this.#camera);
706
+ if ((!this.zooming && this.state === NONE) || tooClose) {
707
+ this.#hit = this.#raycast(this.#raycaster);
708
+ }
709
+ let delta = 0;
710
+ switch (e.deltaMode) {
711
+ case 2: // Pages
712
+ delta = e.deltaY * 800;
713
+ break;
714
+ case 1: // Lines
715
+ delta = e.deltaY * 40;
716
+ break;
717
+ case 0: // Pixels
718
+ delta = e.deltaY;
719
+ break;
720
+ }
721
+ // use LOG to scale the scroll delta and hopefully normalize them across platforms
722
+ const deltaSign = Math.sign(delta);
723
+ const normalizedDelta = Math.max(40, Math.abs(delta));
724
+ this.#zoomDelta =
725
+ -0.8 *
726
+ deltaSign *
727
+ normalizedDelta *
728
+ (this.enableDamping ? 1 - this.dampingFactor : 1);
729
+ this.#setZooming(true);
730
+ this.dispatchEvent(START_EVENT);
731
+ this.dispatchEvent(UPDATE_EVENT);
732
+ };
733
+ #pointerEnter = (e) => {
734
+ if (!this.#enabled) {
735
+ return;
736
+ }
737
+ if (e.buttons !== this.#pointerTracker.getPointerButtons()) {
738
+ this.#pointerTracker.deletePointer(e);
739
+ this.#resetState();
740
+ this.dispatchEvent(UPDATE_EVENT);
741
+ this.dispatchEvent(FINISH_EVENT);
742
+ }
743
+ };
744
+ #finalizeCamera() {
745
+ this.#limitCameraDistance();
746
+ this.#keepCameraUp();
747
+ this.#camera.updateMatrixWorld();
748
+ }
749
+ #alignCameraRightToXYPlane() {
750
+ this.#camera.getWorldDirection(_forward);
751
+ _up.copy(this.#camera.up).transformDirection(this.#camera.matrixWorld);
752
+ _vec1.crossVectors(_forward, _up).normalize();
753
+ _right.copy(_vec1).projectOnPlane(_worldZ);
754
+ if (_right.lengthSq() <= THRESHOLD * THRESHOLD) {
755
+ _right.crossVectors(_forward, _worldZ);
756
+ }
757
+ if (_right.lengthSq() <= THRESHOLD * THRESHOLD) {
758
+ return;
759
+ }
760
+ _right.normalize();
761
+ if (_right.dot(_vec1) < 0) {
762
+ _right.negate();
763
+ }
764
+ _localUp.crossVectors(_right, _forward).normalize();
765
+ _vec2.copy(_forward).negate();
766
+ _rotMatrix.makeBasis(_right, _localUp, _vec2);
767
+ this.#camera.quaternion.setFromRotationMatrix(_rotMatrix);
768
+ }
769
+ #rotateNearAnchor(rotateVec) {
770
+ if (!this.#hit) {
771
+ return;
772
+ }
773
+ this.#camera.getWorldDirection(_forward);
774
+ const cameraVerticalAngle = Math.PI - _forward.angleTo(_worldZ);
775
+ const maxVerticalAngle = Math.PI - THRESHOLD;
776
+ const minVerticalAngle = THRESHOLD;
777
+ const verticalAngle = Math.min(
778
+ Math.max(rotateVec.y, minVerticalAngle - cameraVerticalAngle),
779
+ maxVerticalAngle - cameraVerticalAngle,
780
+ );
781
+ const horizontalAngle = rotateVec.x;
782
+ _quaternion.setFromAxisAngle(_worldZ, horizontalAngle);
783
+ makeRotateAroundPoint(this.#hit.point, _quaternion, _rotMatrix);
784
+ this.#camera.matrixWorld.premultiply(_rotMatrix);
785
+ this.#camera.matrixWorld.decompose(
786
+ this.#camera.position,
787
+ this.#camera.quaternion,
788
+ _vec6,
789
+ );
790
+ this.#camera.getWorldDirection(_forward);
791
+ _up.copy(this.#camera.up).transformDirection(this.#camera.matrixWorld);
792
+ _vec1.crossVectors(_forward, _up).normalize();
793
+ _right.copy(_vec1).projectOnPlane(_worldZ);
794
+ if (_right.lengthSq() <= THRESHOLD * THRESHOLD) {
795
+ _right.crossVectors(_forward, _worldZ);
796
+ }
797
+ if (_right.lengthSq() <= THRESHOLD * THRESHOLD) {
798
+ return;
799
+ }
800
+ _right.normalize();
801
+ if (_right.dot(_vec1) < 0) {
802
+ _right.negate();
803
+ }
804
+ _quaternion.setFromAxisAngle(_right, verticalAngle);
805
+ makeRotateAroundPoint(this.#hit.point, _quaternion, _rotMatrix);
806
+ this.#camera.matrixWorld.premultiply(_rotMatrix);
807
+ this.#camera.matrixWorld.decompose(
808
+ this.#camera.position,
809
+ this.#camera.quaternion,
810
+ _vec6,
811
+ );
812
+ }
813
+ #clampVerticalRotateAngle(axis, pivotPoint, verticalAngle) {
814
+ if (verticalAngle <= 0) {
815
+ return verticalAngle;
816
+ }
817
+ _up.copy(this.#camera.up).transformDirection(this.#camera.matrixWorld);
818
+ const axisDotUp = axis.dot(_up);
819
+ const axisDotPivot = axis.dot(pivotPoint);
820
+ const axisProjection = axisDotPivot * axisDotUp;
821
+ const a = pivotPoint.dot(_up) - axisProjection;
822
+ const b = pivotPoint.dot(_vec1.crossVectors(axis, _up));
823
+ const d = this.#camera.position.dot(_up) - a;
824
+ const amplitude = Math.hypot(a, b);
825
+ if (amplitude <= THRESHOLD) {
826
+ return verticalAngle;
827
+ }
828
+ const cosValue = -d / amplitude;
829
+ if (cosValue < -1 - THRESHOLD || cosValue > 1 + THRESHOLD) {
830
+ return verticalAngle;
831
+ }
832
+ const clampedCosValue = Math.min(1, Math.max(-1, cosValue));
833
+ const phase = Math.atan2(b, a);
834
+ const delta = Math.acos(clampedCosValue);
835
+ let result = verticalAngle;
836
+ for (const candidate of [phase - delta, phase + delta]) {
837
+ for (const offset of [-2 * Math.PI, 0, 2 * Math.PI]) {
838
+ const angle = candidate + offset;
839
+ if (angle > THRESHOLD && angle < result && angle <= verticalAngle) {
840
+ result = angle;
841
+ }
842
+ }
843
+ }
844
+ return result;
845
+ }
846
+ #rotate(rotateVec) {
847
+ if (!this.#hit) {
848
+ return;
849
+ }
850
+ if (this.#isCameraCenterMode()) {
851
+ this.#rotateNearAnchor(rotateVec);
852
+ return;
853
+ }
854
+ this.#camera.getWorldDirection(_forward);
855
+ _up.copy(this.#camera.up).transformDirection(this.#camera.matrixWorld);
856
+ _right.crossVectors(_forward, _up).normalize();
857
+ _localUp.copy(this.#hit.point).normalize();
858
+ _vec6.copy(this.#camera.position).normalize();
859
+ const cameraVerticalAngle = Math.PI - _forward.angleTo(_vec6);
860
+ const maxVerticalAngle = Math.PI - THRESHOLD;
861
+ const minVerticalAngle = THRESHOLD;
862
+ let verticalAngle = Math.min(
863
+ Math.max(rotateVec.y, minVerticalAngle - cameraVerticalAngle),
864
+ maxVerticalAngle - cameraVerticalAngle,
865
+ );
866
+ _ray.set(
867
+ _pivotPoint
868
+ .copy(this.#hit.point)
869
+ .sub(_vec6.copy(_right).multiplyScalar(MAX)),
870
+ _right,
871
+ );
872
+ _plane.setFromNormalAndCoplanarPoint(_right, this.#camera.position);
873
+ _ray.intersectPlane(_plane, _pivotPoint);
874
+ verticalAngle = this.#clampVerticalRotateAngle(
875
+ _right,
876
+ _pivotPoint,
877
+ verticalAngle,
878
+ );
879
+ // Rotate around the right axis
880
+ _quaternion.setFromAxisAngle(_right, verticalAngle);
881
+ makeRotateAroundPoint(_pivotPoint, _quaternion, _rotMatrix);
882
+ this.#camera.matrixWorld.premultiply(_rotMatrix);
883
+ // Rotate around the up axis
884
+ const horizontalAngle = rotateVec.x;
885
+ _quaternion.setFromAxisAngle(_localUp, horizontalAngle);
886
+ makeRotateAroundPoint(this.#hit.point, _quaternion, _rotMatrix);
887
+ this.#camera.matrixWorld.premultiply(_rotMatrix);
888
+ // Explicitly set the quaternion before decomposing
889
+ this.#camera.matrixWorld.decompose(
890
+ this.#camera.position,
891
+ this.#camera.quaternion,
892
+ _vec6,
893
+ );
894
+ }
895
+ #initializeDragAnchor() {
896
+ if (!this.#hit || this.#hit.distance <= 0) {
897
+ return;
898
+ }
899
+ this.#dragAnchorPoint.copy(this.#hit.point);
900
+ this.#dragStartPosition.copy(this.#camera.position);
901
+ this.#dragStartQuaternion.copy(this.#camera.quaternion);
902
+ this.#camera.getWorldDirection(this.#dragPlaneNormal);
903
+ }
904
+ #restoreDragStartCamera() {
905
+ this.#camera.position.copy(this.#dragStartPosition);
906
+ this.#camera.quaternion.copy(this.#dragStartQuaternion);
907
+ this.#camera.updateMatrixWorld();
908
+ }
909
+ #intersectDragPlane(pointer, target) {
910
+ _plane.setFromNormalAndCoplanarPoint(
911
+ this.#dragPlaneNormal,
912
+ this.#dragAnchorPoint,
913
+ );
914
+ setRaycasterFromCamera(this.#raycaster, pointer, this.#camera);
915
+ return this.#raycaster.ray.intersectPlane(_plane, target) !== null;
916
+ }
917
+ #modifiedDrag(rotateVec) {
918
+ if (!this.#hit || this.#hit.distance <= 0) {
919
+ return;
920
+ }
921
+ this.#camera.getWorldDirection(_forward);
922
+ _up.copy(this.#camera.up).transformDirection(this.#camera.matrixWorld);
923
+ _right.crossVectors(_forward, _up).normalize();
924
+ _vec1.copy(rotateVec).projectOnVector(_right);
925
+ _vec2.copy(rotateVec).projectOnVector(_up);
926
+ const length = this.#hit.point.length();
927
+ let verticalAngle =
928
+ Math.atan2(_vec2.length(), length) * Math.sign(_vec2.dot(_up));
929
+ let horizontalAngle =
930
+ -Math.atan2(_vec1.length(), length) * Math.sign(_vec1.dot(_right));
931
+ this.#camera.getWorldDirection(_vec4).negate();
932
+ const angle = _vec4.angleTo(this.#camera.position);
933
+ const cos = Math.cos(angle);
934
+ verticalAngle /= cos;
935
+ horizontalAngle /= cos;
936
+ // Rotate around the right axis
937
+ _quaternion.setFromAxisAngle(_right, verticalAngle);
938
+ makeRotateAroundPoint(_vec3.set(0, 0, 0), _quaternion, _rotMatrix);
939
+ this.#camera.matrixWorld.premultiply(_rotMatrix);
940
+ // Rotate around the up axis
941
+ _quaternion.setFromAxisAngle(_up, horizontalAngle);
942
+ makeRotateAroundPoint(_vec3.set(0, 0, 0), _quaternion, _rotMatrix);
943
+ this.#camera.matrixWorld.premultiply(_rotMatrix);
944
+ // Explicitly set the quaternion before decomposing
945
+ this.#camera.matrixWorld.decompose(
946
+ this.#camera.position,
947
+ this.#camera.quaternion,
948
+ _vec3,
949
+ );
950
+ }
951
+ #keepCameraUpAtFixedPoint(fixedPoint) {
952
+ this.#camera.updateMatrixWorld();
953
+ _invMatrix.copy(this.#camera.matrixWorld).invert();
954
+ _vec1.copy(fixedPoint).applyMatrix4(_invMatrix);
955
+ this.#keepCameraUp();
956
+ this.#camera.updateMatrixWorld();
957
+ _vec2.copy(_vec1).applyMatrix4(this.#camera.matrixWorld);
958
+ this.#camera.position.add(_vec3.subVectors(fixedPoint, _vec2));
959
+ }
960
+ #getZoomOutMetrics(source, hit, baseScale = 1) {
961
+ const metrics = _zoomOutMetrics;
962
+ const minScale = 0;
963
+ metrics.distanceScale = baseScale;
964
+ metrics.transitionWeight = 0;
965
+ if (!this.#ellipsoid) {
966
+ return metrics;
967
+ }
968
+ const taperStartRadius = this.#ellipsoidMaxRadius * 1.5;
969
+ const maxRadius = this.#ellipsoidMaxRadius * 2;
970
+ const currentDistance = source.length();
971
+ if (currentDistance > taperStartRadius) {
972
+ if (currentDistance >= maxRadius) {
973
+ metrics.distanceScale = minScale;
974
+ } else {
975
+ const factor =
976
+ (maxRadius - currentDistance) / (maxRadius - taperStartRadius);
977
+ metrics.distanceScale = minScale + (baseScale - minScale) * factor;
978
+ }
979
+ }
980
+ if (!hit || hit.virtual) {
981
+ return metrics;
982
+ }
983
+ const distanceASquared = hit.point.distanceToSquared(source);
984
+ const distanceBSquared = hit.point.lengthSq();
985
+ const distanceCSquared = source.lengthSq();
986
+ const denominator =
987
+ 2 * Math.sqrt(distanceASquared) * Math.sqrt(distanceBSquared);
988
+ if (denominator <= THRESHOLD) {
989
+ return metrics;
990
+ }
991
+ const angleCos =
992
+ (distanceASquared + distanceBSquared - distanceCSquared) / denominator;
993
+ if (angleCos <= ZOOM_OUT_TRANSITION_COS_THRESHOLD) {
994
+ return metrics;
995
+ }
996
+ if (angleCos >= ZOOM_OUT_TRANSITION_COS_MAX_THRESHOLD) {
997
+ metrics.transitionWeight = 1;
998
+ return metrics;
999
+ }
1000
+ metrics.transitionWeight =
1001
+ (angleCos - ZOOM_OUT_TRANSITION_COS_THRESHOLD) /
1002
+ (ZOOM_OUT_TRANSITION_COS_MAX_THRESHOLD -
1003
+ ZOOM_OUT_TRANSITION_COS_THRESHOLD);
1004
+ return metrics;
1005
+ }
1006
+ #getScaledZoomTarget(hit, zoomFactor, source, distanceScale, target) {
1007
+ target
1008
+ .copy(source)
1009
+ .sub(hit.point)
1010
+ .multiplyScalar(1 + (zoomFactor - 1) * distanceScale)
1011
+ .add(hit.point);
1012
+ }
1013
+ #getZoomOutTransitionTarget(hit, zoomFactor, source, distanceScale, target) {
1014
+ this.#camera.getWorldDirection(_forward);
1015
+ target
1016
+ .copy(source)
1017
+ .addScaledVector(
1018
+ _forward,
1019
+ -source.distanceTo(hit.point) * (zoomFactor - 1) * distanceScale,
1020
+ );
1021
+ }
1022
+ #getZoomPosition(hit, zoomAmount, zoomFactor, target) {
1023
+ const source = _vec4.copy(this.#camera.position);
1024
+ let distanceScale = 1;
1025
+ let transitionWeight = 0;
1026
+ if (zoomAmount < 0 && this.#ellipsoid) {
1027
+ const metrics = this.#getZoomOutMetrics(source, hit.virtual ? null : hit);
1028
+ distanceScale = metrics.distanceScale;
1029
+ transitionWeight = metrics.transitionWeight;
1030
+ }
1031
+ this.#getScaledZoomTarget(hit, zoomFactor, source, distanceScale, target);
1032
+ if (transitionWeight <= 0) {
1033
+ return;
1034
+ }
1035
+ const transitionDistanceScale = this.#getZoomOutMetrics(
1036
+ source,
1037
+ null,
1038
+ 1 / 3,
1039
+ ).distanceScale;
1040
+ this.#getZoomOutTransitionTarget(
1041
+ hit,
1042
+ zoomFactor,
1043
+ source,
1044
+ transitionDistanceScale,
1045
+ _vec6,
1046
+ );
1047
+ target.lerp(_vec6, transitionWeight);
1048
+ }
1049
+ #applyZoom(zoomAmount) {
1050
+ const hit = this.#hit;
1051
+ if (!hit || hit.distance <= 0) return;
1052
+ // Regenerate virtual hit at 50m from current camera position
1053
+ if (hit.virtual) {
1054
+ _forward.subVectors(hit.point, this.#camera.position).normalize();
1055
+ hit.point
1056
+ .copy(this.#camera.position)
1057
+ .addScaledVector(_forward, VIRTUAL_HIT_DISTANCE);
1058
+ hit.distance = VIRTUAL_HIT_DISTANCE;
1059
+ }
1060
+ let zoomFactor = Math.exp(-zoomAmount * 0.001);
1061
+ if (this.minDistance > 0 && zoomFactor < 1) {
1062
+ const distance = this.#camera.position.distanceTo(hit.point);
1063
+ if (distance * zoomFactor < this.minDistance) {
1064
+ zoomFactor = this.minDistance / distance;
1065
+ }
1066
+ }
1067
+ //@ts-expect-error
1068
+ if (this.#camera.isOrthographicCamera) {
1069
+ this.#camera.zoom /= zoomFactor;
1070
+ this.#camera.updateProjectionMatrix();
1071
+ }
1072
+ this.#getZoomPosition(hit, zoomAmount, zoomFactor, this.#camera.position);
1073
+ this.#limitCameraDistance(hit.point);
1074
+ if (this.#isCameraCenterMode()) {
1075
+ this.#camera.updateMatrixWorld();
1076
+ } else {
1077
+ this.#keepCameraUpAtFixedPoint(hit.point);
1078
+ }
1079
+ this.#camera.updateMatrixWorld();
1080
+ if (this.state === DRAG) {
1081
+ this.#initializeDragAnchor();
1082
+ }
1083
+ }
1084
+ #reachCameraMaxDistance() {
1085
+ return (
1086
+ !!this.#ellipsoid &&
1087
+ !this.#isCameraCenterMode() &&
1088
+ this.#camera.position.length() >= this.#ellipsoidMaxRadius * 2
1089
+ );
1090
+ }
1091
+ #isCameraCenterMode() {
1092
+ return this.#camera.position.lengthSq() <= CAMERA_CENTER_MODE_DISTANCE_SQ;
1093
+ }
1094
+ #limitCameraDistance(pivotPosition) {
1095
+ if (!this.#ellipsoid || this.#isCameraCenterMode()) return;
1096
+ const maxRadius = this.#ellipsoidMaxRadius * 2;
1097
+ const currentDistance = this.#camera.position.length();
1098
+ if (currentDistance <= maxRadius) return;
1099
+ if (pivotPosition) {
1100
+ _vec6.subVectors(this.#camera.position, pivotPosition);
1101
+ const a = _vec6.lengthSq();
1102
+ if (a <= THRESHOLD * THRESHOLD) {
1103
+ this.#camera.position.setLength(maxRadius);
1104
+ return;
1105
+ }
1106
+ // Solve for t in: |pivotPosition + t * (cameraPosition - pivotPosition)| = maxRadius
1107
+ const b = 2 * pivotPosition.dot(_vec6);
1108
+ const c = pivotPosition.lengthSq() - maxRadius ** 2;
1109
+ const discriminant = b * b - 4 * a * c;
1110
+ if (discriminant < 0) {
1111
+ this.#camera.position.setLength(maxRadius);
1112
+ return;
1113
+ }
1114
+ const sqrtDiscriminant = Math.sqrt(discriminant);
1115
+ const t0 = (-b - sqrtDiscriminant) / (2 * a);
1116
+ const t1 = (-b + sqrtDiscriminant) / (2 * a);
1117
+ // Clamp to the intersection on the pivot->camera segment; fall back to
1118
+ // radial clamp if the line only intersects outside the segment, to avoid
1119
+ // large jumps when the pivot is virtual or near-tangent.
1120
+ let t = Number.NaN;
1121
+ if (t0 >= 0 && t0 <= 1) {
1122
+ t = t0;
1123
+ }
1124
+ if (t1 >= 0 && t1 <= 1) {
1125
+ t = Number.isNaN(t) ? t1 : Math.max(t, t1);
1126
+ }
1127
+ if (Number.isNaN(t)) {
1128
+ this.#camera.position.setLength(maxRadius);
1129
+ return;
1130
+ }
1131
+ this.#camera.position.copy(pivotPosition).addScaledVector(_vec6, t);
1132
+ } else {
1133
+ this.#camera.position.setLength(maxRadius);
1134
+ }
1135
+ }
1136
+ #shouldDragModified() {
1137
+ return (
1138
+ !!this.#ellipsoid &&
1139
+ !this.#isCameraCenterMode() &&
1140
+ this.#camera.position.length() >= this.#ellipsoidMaxRadius + 100000
1141
+ );
1142
+ }
1143
+ #keepCameraUp() {
1144
+ _vec6.copy(this.#camera.position);
1145
+ const cameraPositionLength = _vec6.length();
1146
+ if (cameraPositionLength < CAMERA_CENTER_MODE_DISTANCE) {
1147
+ this.#alignCameraRightToXYPlane();
1148
+ return;
1149
+ }
1150
+ _localForward.copy(this.#camera.position).normalize();
1151
+ this.#camera.getWorldDirection(_forward);
1152
+ _up.copy(this.#camera.up).transformDirection(this.#camera.matrixWorld);
1153
+ _right.crossVectors(_forward, _up).normalize();
1154
+ _localRight.crossVectors(_up, _localForward);
1155
+ if (_localRight.dot(_right) < 0) {
1156
+ _localRight.negate();
1157
+ }
1158
+ _quaternion.setFromUnitVectors(_right, _localRight);
1159
+ this.#camera.quaternion.premultiply(_quaternion);
1160
+ // _localForward unchanged (position didn't change, only quaternion)
1161
+ this.#camera.getWorldDirection(_forward);
1162
+ _up.copy(this.#camera.up).transformDirection(this.#camera.matrixWorld);
1163
+ _right.crossVectors(_forward, _up).normalize();
1164
+ _localUp.crossVectors(_forward, _localForward);
1165
+ if (_localUp.dot(_right) < 0) {
1166
+ const forwardAngle = _forward.angleTo(_localForward);
1167
+ if (forwardAngle < Math.PI / 2) {
1168
+ _axis.crossVectors(_forward, _localForward).normalize();
1169
+ _quaternion.setFromAxisAngle(_axis, forwardAngle);
1170
+ this.#camera.quaternion.premultiply(_quaternion);
1171
+ } else {
1172
+ _axis
1173
+ .crossVectors(_forward, _vec4.copy(_localForward).negate())
1174
+ .normalize();
1175
+ const negatedAngle = _forward.angleTo(_vec4);
1176
+ _quaternion.setFromAxisAngle(_axis, negatedAngle);
1177
+ this.#camera.quaternion.premultiply(_quaternion);
1178
+ }
1179
+ }
1180
+ }
1181
+ #normalRaycastClosest(raycaster, objects) {
1182
+ const targets = Array.isArray(objects) ? objects : [objects];
1183
+ if (targets.length === 0) return null;
1184
+ const intersects = raycaster.intersectObjects(targets, true);
1185
+ if (intersects.length > 0) {
1186
+ return {
1187
+ point: intersects[0].point.clone(),
1188
+ distance: intersects[0].point.distanceTo(this.#camera.position),
1189
+ };
1190
+ }
1191
+ return null;
1192
+ }
1193
+ #raycast(raycaster) {
1194
+ const sceneHit = this.#normalRaycastClosest(raycaster, this.#scene);
1195
+ if (sceneHit) {
1196
+ return sceneHit;
1197
+ }
1198
+ const result = this.#isCameraCenterMode()
1199
+ ? undefined
1200
+ : this.#ellipsoid?.intersectRay(raycaster.ray, _vec6);
1201
+ this.#ellipsoid?.getPositionToNormal(_vec6, _vec5);
1202
+ const distance = _vec6.distanceTo(this.#camera.position);
1203
+ if (result) {
1204
+ return {
1205
+ point: _vec6.clone(),
1206
+ distance: distance,
1207
+ onGlobe: true,
1208
+ };
1209
+ }
1210
+ return {
1211
+ point: raycaster.ray.at(VIRTUAL_HIT_DISTANCE, _vec6).clone(),
1212
+ distance: VIRTUAL_HIT_DISTANCE,
1213
+ onGlobe: true,
1214
+ virtual: true,
1215
+ };
1216
+ }
1217
+ }
1218
+
1219
+ export { CameraController };