3dtiles-inspector 0.2.11 → 0.2.13

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.
@@ -11,10 +11,8 @@ import {
11
11
  Vector2,
12
12
  Vector3,
13
13
  } from 'three';
14
+ import { Ellipsoid } from '3d-tiles-renderer';
14
15
 
15
- const createUninitializedCallback = (name) => () => {
16
- console.warn(`${name} was called before initialization.`);
17
- };
18
16
  const CAMERA_CENTER_MODE_DISTANCE = 3000000;
19
17
  const CAMERA_CENTER_MODE_DISTANCE_SQ = CAMERA_CENTER_MODE_DISTANCE ** 2;
20
18
 
@@ -347,16 +345,14 @@ const _vec5 = new Vector3();
347
345
  const _vec6 = new Vector3();
348
346
  const _axis = new Vector3();
349
347
  const _localUp = new Vector3();
348
+ const _positionUp = new Vector3();
350
349
  const _localRight = new Vector3();
351
- const _localForward = new Vector3();
352
350
  const _rotMatrix = new Matrix4();
353
- const _invMatrix = new Matrix4();
354
351
  const _quaternion = new Quaternion();
352
+ const _rollRotation = new Quaternion();
355
353
  const _plane = new Plane();
356
354
  const _ray = new Ray();
357
- const _zoomOutMetrics = { distanceScale: 1 };
358
- const _fixedPointSurface = new Vector3();
359
- const _fixedPointNormal = new Vector3();
355
+ const _dragEllipsoid = new Ellipsoid(1, 1, 1);
360
356
  class CameraController extends EventDispatcher {
361
357
  enableDamping;
362
358
  dampingFactor;
@@ -376,8 +372,6 @@ class CameraController extends EventDispatcher {
376
372
  #rotateInertia;
377
373
  #dragInertia;
378
374
  #dragAnchorPoint;
379
- #dragStartPosition;
380
- #dragStartQuaternion;
381
375
  #dragPlaneNormal;
382
376
  #inertiaValue;
383
377
  #enabled;
@@ -385,22 +379,6 @@ class CameraController extends EventDispatcher {
385
379
  #ellipsoidMaxRadius;
386
380
  #lastTime;
387
381
  #hit;
388
- #contextMenuEvent = createUninitializedCallback(
389
- 'CameraController.#contextMenuEvent',
390
- );
391
- #pointerDownEvent = createUninitializedCallback(
392
- 'CameraController.#pointerDownEvent',
393
- );
394
- #pointerMoveEvent = createUninitializedCallback(
395
- 'CameraController.#pointerMoveEvent',
396
- );
397
- #pointerUpEvent = createUninitializedCallback(
398
- 'CameraController.#pointerUpEvent',
399
- );
400
- #wheelEvent = createUninitializedCallback('CameraController.#wheelEvent');
401
- #pointerEnterEvent = createUninitializedCallback(
402
- 'CameraController.#pointerEnterEvent',
403
- );
404
382
  #pointerDownFilter;
405
383
  #zoomTimeout;
406
384
  constructor(renderer, scene, camera, options = {}) {
@@ -428,8 +406,6 @@ class CameraController extends EventDispatcher {
428
406
  this.#rotateInertia = new Vector2();
429
407
  this.#dragInertia = new Vector3();
430
408
  this.#dragAnchorPoint = new Vector3();
431
- this.#dragStartPosition = new Vector3();
432
- this.#dragStartQuaternion = new Quaternion();
433
409
  this.#dragPlaneNormal = new Vector3();
434
410
  this.#inertiaValue = 0;
435
411
  this.#enabled = false;
@@ -452,12 +428,6 @@ class CameraController extends EventDispatcher {
452
428
  this.#enabled = v;
453
429
  this.#resetState();
454
430
  this.#pointerTracker.reset();
455
- if (!this.enabled) {
456
- this.#dragInertia.set(0, 0, 0);
457
- this.#rotateInertia.set(0, 0);
458
- this.#zoomInertia = 0;
459
- this.#inertiaValue = 0;
460
- }
461
431
  this.dispatchEvent(UPDATE_EVENT);
462
432
  this.dispatchEvent(FINISH_EVENT);
463
433
  }
@@ -492,8 +462,6 @@ class CameraController extends EventDispatcher {
492
462
  this.#rotateInertia.set(0, 0);
493
463
  this.#dragInertia.set(0, 0, 0);
494
464
  this.#dragAnchorPoint.set(0, 0, 0);
495
- this.#dragStartPosition.set(0, 0, 0);
496
- this.#dragStartQuaternion.identity();
497
465
  this.#dragPlaneNormal.set(0, 0, 0);
498
466
  this.#zoomInertia = 0;
499
467
  this.#hit = null;
@@ -519,12 +487,6 @@ class CameraController extends EventDispatcher {
519
487
  this.#domElement.style.touchAction = 'none';
520
488
  this.#pivotMesh.raycast = () => {};
521
489
  this.#scene.add(this.#pivotMesh);
522
- this.#contextMenuEvent = this.#contextMenu.bind(this);
523
- this.#pointerDownEvent = this.#pointerDown.bind(this);
524
- this.#pointerMoveEvent = this.#pointerMove.bind(this);
525
- this.#pointerUpEvent = this.#pointerUp.bind(this);
526
- this.#wheelEvent = this.#wheel.bind(this);
527
- this.#pointerEnterEvent = this.#pointerEnter.bind(this);
528
490
  this.#bindEvents();
529
491
  this.#enabled = true;
530
492
  }
@@ -565,23 +527,32 @@ class CameraController extends EventDispatcher {
565
527
  if (!_pointer1.equals(_pointer2) && this.#hit && this.#hit.distance > 0) {
566
528
  mouseToCoords(_pointer1.x, _pointer1.y, this.#domElement, _pointer1);
567
529
  mouseToCoords(_pointer2.x, _pointer2.y, this.#domElement, _pointer2);
568
- this.#restoreDragStartCamera();
569
- if (
530
+ const shouldDragModified = this.#shouldDragModified();
531
+ if (shouldDragModified) {
532
+ if (this.#modifiedDrag(_pointer1)) {
533
+ this.#inertiaValue = 1;
534
+ this.#rotateInertia.set(0, 0);
535
+ this.#zoomInertia = 0;
536
+ this.#finalizeCamera();
537
+ } else {
538
+ this.#setState(IDLE);
539
+ }
540
+ this.dispatchEvent(UPDATE_EVENT);
541
+ } else if (
570
542
  this.#intersectDragPlane(_pointer1, _vec1) &&
571
543
  this.#intersectDragPlane(_pointer2, _vec2)
572
544
  ) {
573
545
  _vec.subVectors(_vec1, this.#dragAnchorPoint);
574
546
  _vec5.subVectors(_vec1, _vec2);
575
- if (this.#shouldDragModified()) {
576
- this.#modifiedDrag(_vec);
577
- } else {
578
- this.#camera.position.sub(_vec);
579
- }
547
+ this.#camera.position.sub(_vec);
580
548
  this.#dragInertia.copy(_vec5);
581
549
  this.#inertiaValue = 1;
582
550
  this.#rotateInertia.set(0, 0);
583
551
  this.#zoomInertia = 0;
584
552
  this.#finalizeCamera();
553
+ if (!shouldDragModified && this.#shouldDragModified()) {
554
+ this.#initializeDragAnchor();
555
+ }
585
556
  this.dispatchEvent(UPDATE_EVENT);
586
557
  }
587
558
  }
@@ -592,15 +563,24 @@ class CameraController extends EventDispatcher {
592
563
  this.#rotate(_pointer);
593
564
  this.#finalizeCamera();
594
565
  } else if (this.#dragInertia.lengthSq() > 0 && this.#inertiaValue > 0) {
595
- if (this.#shouldDragModified()) {
566
+ const shouldDragModified = this.#shouldDragModified();
567
+ if (shouldDragModified) {
596
568
  _vec.copy(this.#dragInertia).multiplyScalar(this.#inertiaValue);
597
- this.#modifiedDrag(_vec);
569
+ const angle = _vec.length();
570
+ if (angle > THRESHOLD) {
571
+ _axis.copy(_vec).multiplyScalar(1 / angle);
572
+ _quaternion.setFromAxisAngle(_axis, angle);
573
+ this.#applyCameraRotationAroundOrigin(_quaternion);
574
+ }
598
575
  this.#finalizeCamera();
599
576
  } else {
600
577
  _vec.copy(this.#dragInertia).multiplyScalar(this.#inertiaValue);
601
578
  this.#camera.position.sub(_vec);
602
579
  this.#finalizeCamera();
603
580
  }
581
+ if (!shouldDragModified && this.#shouldDragModified()) {
582
+ this.#initializeDragAnchor();
583
+ }
604
584
  }
605
585
  if (
606
586
  (this.#rotateInertia.lengthSq() === 0 &&
@@ -643,6 +623,8 @@ class CameraController extends EventDispatcher {
643
623
  this.#zoomDelta = delta * 4000;
644
624
  }
645
625
  if (this.#zoomDelta !== 0) {
626
+ this.#rotateInertia.set(0, 0);
627
+ this.#dragInertia.set(0, 0, 0);
646
628
  if (this.#zoomTimeout !== null) {
647
629
  clearTimeout(this.#zoomTimeout);
648
630
  this.#zoomTimeout = null;
@@ -705,15 +687,12 @@ class CameraController extends EventDispatcher {
705
687
  clearTimeout(this.#zoomTimeout);
706
688
  this.#zoomTimeout = null;
707
689
  }
708
- this.#domElement.removeEventListener('contextmenu', this.#contextMenuEvent);
709
- this.#domElement.removeEventListener('pointerdown', this.#pointerDownEvent);
710
- this.#domElement.removeEventListener('pointermove', this.#pointerMoveEvent);
711
- this.#domElement.removeEventListener('pointerup', this.#pointerUpEvent);
712
- this.#domElement.removeEventListener('wheel', this.#wheelEvent);
713
- this.#domElement.removeEventListener(
714
- 'pointerenter',
715
- this.#pointerEnterEvent,
716
- );
690
+ this.#domElement.removeEventListener('contextmenu', this.#contextMenu);
691
+ this.#domElement.removeEventListener('pointerdown', this.#pointerDown);
692
+ this.#domElement.removeEventListener('pointermove', this.#pointerMove);
693
+ this.#domElement.removeEventListener('pointerup', this.#pointerUp);
694
+ this.#domElement.removeEventListener('wheel', this.#wheel);
695
+ this.#domElement.removeEventListener('pointerenter', this.#pointerEnter);
717
696
  this.#pivotMesh.removeFromParent();
718
697
  this.#pivotMesh.dispose();
719
698
  this.#domElement.style.touchAction = '';
@@ -721,12 +700,12 @@ class CameraController extends EventDispatcher {
721
700
  this.#ellipsoid = null;
722
701
  }
723
702
  #bindEvents() {
724
- this.#domElement.addEventListener('contextmenu', this.#contextMenuEvent);
725
- this.#domElement.addEventListener('pointerdown', this.#pointerDownEvent);
726
- this.#domElement.addEventListener('pointermove', this.#pointerMoveEvent);
727
- this.#domElement.addEventListener('pointerup', this.#pointerUpEvent);
728
- this.#domElement.addEventListener('wheel', this.#wheelEvent);
729
- this.#domElement.addEventListener('pointerenter', this.#pointerEnterEvent);
703
+ this.#domElement.addEventListener('contextmenu', this.#contextMenu);
704
+ this.#domElement.addEventListener('pointerdown', this.#pointerDown);
705
+ this.#domElement.addEventListener('pointermove', this.#pointerMove);
706
+ this.#domElement.addEventListener('pointerup', this.#pointerUp);
707
+ this.#domElement.addEventListener('wheel', this.#wheel);
708
+ this.#domElement.addEventListener('pointerenter', this.#pointerEnter);
730
709
  }
731
710
  #contextMenu = (e) => {
732
711
  e.preventDefault();
@@ -870,29 +849,27 @@ class CameraController extends EventDispatcher {
870
849
  }
871
850
  };
872
851
  #finalizeCamera() {
873
- this.#limitCameraDistance();
874
- this.#keepCameraUp();
852
+ const fixedPoint =
853
+ this.#hit && this.#hit.distance > 0 ? this.#hit.point : undefined;
854
+ this.#limitCameraDistance(fixedPoint);
855
+ if (fixedPoint && !this.#isCameraCenterMode()) {
856
+ this.#keepCameraUp(fixedPoint);
857
+ } else {
858
+ this.#keepCameraUp();
859
+ }
875
860
  this.#camera.updateMatrixWorld();
876
861
  }
877
- #alignCameraRightToXYPlane() {
878
- this.#camera.getWorldDirection(_forward);
879
- _up.copy(this.#camera.up).transformDirection(this.#camera.matrixWorld);
880
- _vec1.crossVectors(_forward, _up).normalize();
881
- _right.copy(_vec1).projectOnPlane(_worldZ);
882
- if (_right.lengthSq() <= THRESHOLD * THRESHOLD) {
883
- _right.crossVectors(_forward, _worldZ);
884
- }
885
- if (_right.lengthSq() <= THRESHOLD * THRESHOLD) {
886
- return;
862
+ #getPositionUpDirection(position, target) {
863
+ if (this.#ellipsoid) {
864
+ this.#ellipsoid.getPositionToNormal(position, target);
865
+ if (target.lengthSq() > THRESHOLD * THRESHOLD) {
866
+ return target;
867
+ }
887
868
  }
888
- _right.normalize();
889
- if (_right.dot(_vec1) < 0) {
890
- _right.negate();
869
+ if (position.lengthSq() > THRESHOLD * THRESHOLD) {
870
+ return target.copy(position).normalize();
891
871
  }
892
- _localUp.crossVectors(_right, _forward).normalize();
893
- _vec2.copy(_forward).negate();
894
- _rotMatrix.makeBasis(_right, _localUp, _vec2);
895
- this.#camera.quaternion.setFromRotationMatrix(_rotMatrix);
872
+ return target.copy(_worldZ);
896
873
  }
897
874
  #rotateNearAnchor(rotateVec) {
898
875
  if (!this.#hit) {
@@ -982,9 +959,9 @@ class CameraController extends EventDispatcher {
982
959
  this.#camera.getWorldDirection(_forward);
983
960
  _up.copy(this.#camera.up).transformDirection(this.#camera.matrixWorld);
984
961
  _right.crossVectors(_forward, _up).normalize();
985
- _localUp.copy(this.#hit.point).normalize();
986
- _vec6.copy(this.#camera.position).normalize();
987
- const cameraVerticalAngle = Math.PI - _forward.angleTo(_vec6);
962
+ this.#getPositionUpDirection(this.#hit.point, _localUp);
963
+ this.#getPositionUpDirection(this.#camera.position, _positionUp);
964
+ const cameraVerticalAngle = Math.PI - _forward.angleTo(_positionUp);
988
965
  const maxVerticalAngle = Math.PI - THRESHOLD;
989
966
  const minVerticalAngle = THRESHOLD;
990
967
  let verticalAngle = Math.min(
@@ -1025,16 +1002,9 @@ class CameraController extends EventDispatcher {
1025
1002
  return;
1026
1003
  }
1027
1004
  this.#dragAnchorPoint.copy(this.#hit.point);
1028
- this.#dragStartPosition.copy(this.#camera.position);
1029
- this.#dragStartQuaternion.copy(this.#camera.quaternion);
1030
- this.#camera.getWorldDirection(this.#dragPlaneNormal);
1031
- }
1032
- #restoreDragStartCamera() {
1033
- this.#camera.position.copy(this.#dragStartPosition);
1034
- this.#camera.quaternion.copy(this.#dragStartQuaternion);
1035
- this.#camera.updateMatrixWorld();
1036
1005
  }
1037
1006
  #intersectDragPlane(pointer, target) {
1007
+ this.#camera.getWorldDirection(this.#dragPlaneNormal);
1038
1008
  _plane.setFromNormalAndCoplanarPoint(
1039
1009
  this.#dragPlaneNormal,
1040
1010
  this.#dragAnchorPoint,
@@ -1042,111 +1012,66 @@ class CameraController extends EventDispatcher {
1042
1012
  setRaycasterFromCamera(this.#raycaster, pointer, this.#camera);
1043
1013
  return this.#raycaster.ray.intersectPlane(_plane, target) !== null;
1044
1014
  }
1045
- #modifiedDrag(rotateVec) {
1046
- if (!this.#hit || this.#hit.distance <= 0) {
1015
+ #modifiedDrag(pointer) {
1016
+ if (!this.#hit || this.#hit.distance <= 0 || this.#hit.virtual) {
1017
+ return false;
1018
+ }
1019
+ const radius = this.#dragAnchorPoint.length();
1020
+ if (radius <= THRESHOLD) {
1021
+ return false;
1022
+ }
1023
+ _dragEllipsoid.radius.setScalar(radius);
1024
+ setRaycasterFromCamera(this.#raycaster, pointer, this.#camera);
1025
+ if (!_dragEllipsoid.intersectRay(this.#raycaster.ray, _vec1)) {
1026
+ return false;
1027
+ }
1028
+ _vec2.copy(_vec1).normalize();
1029
+ if (this.#raycaster.ray.direction.dot(_vec2) > 0) {
1030
+ return false;
1031
+ }
1032
+ _vec1.normalize();
1033
+ _vec2.copy(this.#dragAnchorPoint).normalize();
1034
+ _quaternion.setFromUnitVectors(_vec1, _vec2);
1035
+ this.#setModifiedDragInertia(_quaternion);
1036
+ this.#applyCameraRotationAroundOrigin(_quaternion);
1037
+ return true;
1038
+ }
1039
+ #setModifiedDragInertia(rotation) {
1040
+ _axis.set(rotation.x, rotation.y, rotation.z);
1041
+ if (rotation.w < 0) {
1042
+ _axis.negate();
1043
+ }
1044
+ const axisLength = _axis.length();
1045
+ if (axisLength <= THRESHOLD) {
1046
+ this.#dragInertia.set(0, 0, 0);
1047
1047
  return;
1048
1048
  }
1049
- this.#camera.getWorldDirection(_forward);
1050
- _up.copy(this.#camera.up).transformDirection(this.#camera.matrixWorld);
1051
- _right.crossVectors(_forward, _up).normalize();
1052
- _vec1.copy(rotateVec).projectOnVector(_right);
1053
- _vec2.copy(rotateVec).projectOnVector(_up);
1054
- const length = this.#hit.point.length();
1055
- let verticalAngle =
1056
- Math.atan2(_vec2.length(), length) * Math.sign(_vec2.dot(_up));
1057
- let horizontalAngle =
1058
- -Math.atan2(_vec1.length(), length) * Math.sign(_vec1.dot(_right));
1059
- this.#camera.getWorldDirection(_vec4).negate();
1060
- const angle = _vec4.angleTo(this.#camera.position);
1061
- const cos = Math.cos(angle);
1062
- verticalAngle /= cos;
1063
- horizontalAngle /= cos;
1064
- // Rotate around the right axis
1065
- _quaternion.setFromAxisAngle(_right, verticalAngle);
1066
- makeRotateAroundPoint(_vec3.set(0, 0, 0), _quaternion, _rotMatrix);
1067
- this.#camera.matrixWorld.premultiply(_rotMatrix);
1068
- // Rotate around the up axis
1069
- _quaternion.setFromAxisAngle(_up, horizontalAngle);
1070
- makeRotateAroundPoint(_vec3.set(0, 0, 0), _quaternion, _rotMatrix);
1049
+ const angle = 2 * Math.atan2(axisLength, Math.abs(rotation.w));
1050
+ this.#dragInertia.copy(_axis).multiplyScalar(angle / axisLength);
1051
+ }
1052
+ #applyCameraRotationAroundOrigin(rotation) {
1053
+ makeRotateAroundPoint(_vec3.set(0, 0, 0), rotation, _rotMatrix);
1071
1054
  this.#camera.matrixWorld.premultiply(_rotMatrix);
1072
- // Explicitly set the quaternion before decomposing
1073
1055
  this.#camera.matrixWorld.decompose(
1074
1056
  this.#camera.position,
1075
1057
  this.#camera.quaternion,
1076
1058
  _vec3,
1077
1059
  );
1078
1060
  }
1079
- #keepCameraUpAtFixedPoint(fixedPoint) {
1080
- this.#camera.updateMatrixWorld();
1081
- _invMatrix.copy(this.#camera.matrixWorld).invert();
1082
- _vec1.copy(fixedPoint).applyMatrix4(_invMatrix);
1083
- let clampToGlobeHorizon = false;
1084
- if (this.#ellipsoid && !this.#isCameraCenterMode()) {
1085
- const surfacePoint = this.#ellipsoid.getPositionToSurfacePoint(
1086
- fixedPoint,
1087
- _fixedPointSurface,
1088
- );
1089
- if (surfacePoint) {
1090
- this.#ellipsoid.getPositionToNormal(
1091
- _fixedPointSurface,
1092
- _fixedPointNormal,
1093
- );
1094
- clampToGlobeHorizon =
1095
- _fixedPointNormal.lengthSq() > THRESHOLD * THRESHOLD;
1096
- }
1097
- }
1098
- this.#keepCameraUp();
1099
- this.#camera.updateMatrixWorld();
1100
- _vec2.copy(_vec1).applyMatrix4(this.#camera.matrixWorld);
1101
- _vec3.subVectors(fixedPoint, _vec2);
1102
- if (clampToGlobeHorizon) {
1103
- _vec4.copy(this.#camera.position).add(_vec3);
1104
- const candidateClearance = _vec5
1105
- .subVectors(_vec4, _fixedPointSurface)
1106
- .dot(_fixedPointNormal);
1107
-
1108
- if (candidateClearance < 0) {
1109
- return;
1110
- }
1111
- }
1112
- this.#camera.position.add(_vec3);
1113
- }
1114
- #getZoomOutMetrics(source, baseScale = 1) {
1115
- const metrics = _zoomOutMetrics;
1116
- const minScale = 0;
1117
- metrics.distanceScale = baseScale;
1118
- if (!this.#ellipsoid || this.#isCameraCenterMode()) {
1119
- return metrics;
1061
+ #getZoomDistanceScale(zoomAmount, source) {
1062
+ if (zoomAmount >= 0 || !this.#ellipsoid || this.#isCameraCenterMode()) {
1063
+ return 1;
1120
1064
  }
1121
1065
  const taperStartRadius = this.#ellipsoidMaxRadius * 1.5;
1122
1066
  const maxRadius = this.#ellipsoidMaxRadius * 2;
1123
1067
  const currentDistance = source.length();
1124
- if (currentDistance > taperStartRadius) {
1125
- if (currentDistance >= maxRadius) {
1126
- metrics.distanceScale = minScale;
1127
- } else {
1128
- const factor =
1129
- (maxRadius - currentDistance) / (maxRadius - taperStartRadius);
1130
- metrics.distanceScale = minScale + (baseScale - minScale) * factor;
1131
- }
1068
+ if (currentDistance <= taperStartRadius) {
1069
+ return 1;
1132
1070
  }
1133
- return metrics;
1134
- }
1135
- #getScaledZoomTarget(hit, zoomFactor, source, distanceScale, target) {
1136
- target
1137
- .copy(source)
1138
- .sub(hit.point)
1139
- .multiplyScalar(1 + (zoomFactor - 1) * distanceScale)
1140
- .add(hit.point);
1141
- }
1142
- #getZoomPosition(hit, zoomAmount, zoomFactor, target) {
1143
- const source = _vec4.copy(this.#camera.position);
1144
- let distanceScale = 1;
1145
- if (zoomAmount < 0 && this.#ellipsoid && !this.#isCameraCenterMode()) {
1146
- const metrics = this.#getZoomOutMetrics(source);
1147
- distanceScale = metrics.distanceScale;
1071
+ if (currentDistance >= maxRadius) {
1072
+ return 0;
1148
1073
  }
1149
- this.#getScaledZoomTarget(hit, zoomFactor, source, distanceScale, target);
1074
+ return (maxRadius - currentDistance) / (maxRadius - taperStartRadius);
1150
1075
  }
1151
1076
  #applyZoom(zoomAmount) {
1152
1077
  const hit = this.#hit;
@@ -1171,12 +1096,18 @@ class CameraController extends EventDispatcher {
1171
1096
  this.#camera.zoom /= zoomFactor;
1172
1097
  this.#camera.updateProjectionMatrix();
1173
1098
  }
1174
- this.#getZoomPosition(hit, zoomAmount, zoomFactor, this.#camera.position);
1099
+ const source = _vec4.copy(this.#camera.position);
1100
+ const distanceScale = this.#getZoomDistanceScale(zoomAmount, source);
1101
+ this.#camera.position
1102
+ .copy(source)
1103
+ .sub(hit.point)
1104
+ .multiplyScalar(1 + (zoomFactor - 1) * distanceScale)
1105
+ .add(hit.point);
1175
1106
  this.#limitCameraDistance(hit.point);
1176
1107
  if (this.#isCameraCenterMode()) {
1177
1108
  this.#camera.updateMatrixWorld();
1178
1109
  } else {
1179
- this.#keepCameraUpAtFixedPoint(hit.point);
1110
+ this.#keepCameraUp(hit.point);
1180
1111
  }
1181
1112
  this.#camera.updateMatrixWorld();
1182
1113
  if (this.state === DRAG) {
@@ -1239,46 +1170,64 @@ class CameraController extends EventDispatcher {
1239
1170
  return (
1240
1171
  !!this.#ellipsoid &&
1241
1172
  !this.#isCameraCenterMode() &&
1242
- this.#camera.position.length() >= this.#ellipsoidMaxRadius + 100000
1173
+ this.#camera.position.length() >= this.#ellipsoidMaxRadius * 1.05
1243
1174
  );
1244
1175
  }
1245
- #keepCameraUp() {
1246
- _vec6.copy(this.#camera.position);
1247
- const cameraPositionLength = _vec6.length();
1248
- if (cameraPositionLength < CAMERA_CENTER_MODE_DISTANCE) {
1249
- this.#alignCameraRightToXYPlane();
1250
- return;
1251
- }
1252
- _localForward.copy(this.#camera.position).normalize();
1176
+ #alignCameraRoll(referenceUp) {
1177
+ _rollRotation.identity();
1253
1178
  this.#camera.getWorldDirection(_forward);
1254
1179
  _up.copy(this.#camera.up).transformDirection(this.#camera.matrixWorld);
1255
1180
  _right.crossVectors(_forward, _up).normalize();
1256
- _localRight.crossVectors(_up, _localForward);
1181
+ _localRight.crossVectors(_up, referenceUp);
1257
1182
  if (_localRight.dot(_right) < 0) {
1258
1183
  _localRight.negate();
1259
1184
  }
1260
1185
  _quaternion.setFromUnitVectors(_right, _localRight);
1261
1186
  this.#camera.quaternion.premultiply(_quaternion);
1262
- // _localForward unchanged (position didn't change, only quaternion)
1187
+ _rollRotation.premultiply(_quaternion);
1263
1188
  this.#camera.getWorldDirection(_forward);
1264
1189
  _up.copy(this.#camera.up).transformDirection(this.#camera.matrixWorld);
1265
1190
  _right.crossVectors(_forward, _up).normalize();
1266
- _localUp.crossVectors(_forward, _localForward);
1191
+ _localUp.crossVectors(_forward, referenceUp);
1267
1192
  if (_localUp.dot(_right) < 0) {
1268
- const forwardAngle = _forward.angleTo(_localForward);
1193
+ const forwardAngle = _forward.angleTo(referenceUp);
1269
1194
  if (forwardAngle < Math.PI / 2) {
1270
- _axis.crossVectors(_forward, _localForward).normalize();
1195
+ _axis.crossVectors(_forward, referenceUp).normalize();
1271
1196
  _quaternion.setFromAxisAngle(_axis, forwardAngle);
1272
1197
  this.#camera.quaternion.premultiply(_quaternion);
1198
+ _rollRotation.premultiply(_quaternion);
1273
1199
  } else {
1274
1200
  _axis
1275
- .crossVectors(_forward, _vec4.copy(_localForward).negate())
1201
+ .crossVectors(_forward, _vec4.copy(referenceUp).negate())
1276
1202
  .normalize();
1277
1203
  const negatedAngle = _forward.angleTo(_vec4);
1278
1204
  _quaternion.setFromAxisAngle(_axis, negatedAngle);
1279
1205
  this.#camera.quaternion.premultiply(_quaternion);
1206
+ _rollRotation.premultiply(_quaternion);
1280
1207
  }
1281
1208
  }
1209
+ return _rollRotation;
1210
+ }
1211
+ #canApplyCameraRollAroundAnchor(anchorPoint) {
1212
+ return (
1213
+ anchorPoint.dot(_vec3.subVectors(this.#camera.position, anchorPoint)) >= 0
1214
+ );
1215
+ }
1216
+ #keepCameraUp(anchorPoint) {
1217
+ // Near the ellipsoid centre the surface normal is unstable, so fall back to
1218
+ // the world up direction.
1219
+ if (this.#isCameraCenterMode()) {
1220
+ this.#alignCameraRoll(_worldZ);
1221
+ return;
1222
+ }
1223
+ this.#getPositionUpDirection(this.#camera.position, _positionUp);
1224
+ const rollRotation = this.#alignCameraRoll(_positionUp);
1225
+ if (anchorPoint && this.#canApplyCameraRollAroundAnchor(anchorPoint)) {
1226
+ this.#camera.position
1227
+ .sub(anchorPoint)
1228
+ .applyQuaternion(rollRotation)
1229
+ .add(anchorPoint);
1230
+ }
1282
1231
  }
1283
1232
  #normalRaycastClosest(raycaster, objects) {
1284
1233
  const targets = Array.isArray(objects) ? objects : [objects];
@@ -1300,12 +1249,10 @@ class CameraController extends EventDispatcher {
1300
1249
  const result = this.#isCameraCenterMode()
1301
1250
  ? undefined
1302
1251
  : this.#ellipsoid?.intersectRay(raycaster.ray, _vec6);
1303
- this.#ellipsoid?.getPositionToNormal(_vec6, _vec5);
1304
- const distance = _vec6.distanceTo(this.#camera.position);
1305
1252
  if (result) {
1306
1253
  return {
1307
1254
  point: _vec6.clone(),
1308
- distance: distance,
1255
+ distance: _vec6.distanceTo(this.#camera.position),
1309
1256
  onGlobe: true,
1310
1257
  };
1311
1258
  }
@@ -11,11 +11,12 @@ export function createGlobeController({
11
11
  renderer,
12
12
  }) {
13
13
  let tiles = null;
14
- let terrainEnabled = true;
14
+ let terrainEnabled = false;
15
15
 
16
- function setTerrainEnabled(enabled) {
16
+ function setTerrainEnabled(enabled, { cesiumIonToken = '' } = {}) {
17
17
  const globeTileOptions = {
18
18
  camera,
19
+ cesiumIonToken,
19
20
  preprocessURL: normalizeLocalResourceUrl,
20
21
  renderer,
21
22
  };
@@ -1,4 +1,3 @@
1
- import { Ion } from 'cesium';
2
1
  import { TilesRenderer } from '3d-tiles-renderer';
3
2
  import { ImplicitTilingPlugin } from '3d-tiles-renderer/src/core/plugins/ImplicitTilingPlugin.js';
4
3
  import { CesiumIonAuthPlugin } from '3d-tiles-renderer/src/three/plugins/CesiumIonAuthPlugin.js';
@@ -22,7 +21,6 @@ const SATELLITE_IMAGERY = {
22
21
  levels: 18,
23
22
  };
24
23
  const CESIUM_ION_TERRAIN = {
25
- apiToken: Ion.defaultAccessToken,
26
24
  assetId: 1,
27
25
  };
28
26
 
@@ -73,13 +71,21 @@ export function createImageryGlobeTiles(options) {
73
71
  }
74
72
 
75
73
  export function createTerrainGlobeTiles(options) {
74
+ const apiToken =
75
+ typeof options.cesiumIonToken === 'string'
76
+ ? options.cesiumIonToken.trim()
77
+ : '';
78
+ if (!apiToken) {
79
+ throw new Error('Cesium ion token is required to enable terrain.');
80
+ }
81
+
76
82
  const next = new TilesRenderer();
77
83
  const satelliteOverlay = createSatelliteOverlay(options.preprocessURL);
78
84
  next.downloadQueue.maxJobs = 8;
79
85
  next.parseQueue.maxJobs = 2;
80
86
  next.registerPlugin(
81
87
  new CesiumIonAuthPlugin({
82
- apiToken: CESIUM_ION_TERRAIN.apiToken,
88
+ apiToken,
83
89
  assetId: String(CESIUM_ION_TERRAIN.assetId),
84
90
  autoRefreshToken: true,
85
91
  assetTypeHandler: (type, tilesRenderer) => {