3dtiles-inspector 0.2.4 → 0.2.6

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.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,19 @@ The format is based on Keep a Changelog and this project follows Semantic Versio
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.2.6] - 2026-05-05
10
+
11
+ ### Added
12
+
13
+ - Added realtime `camerapose` URL synchronization so shared viewer URLs can restore the current camera pose.
14
+
15
+ ## [0.2.5] - 2026-05-05
16
+
17
+ ### Fixed
18
+
19
+ - Fixed coordinate move actions leaving the camera at the previous view by flying back to the relocated tileset after the root transform is moved.
20
+ - Fixed near-linear fly-to paths snapping to the start direction instead of interpolating toward the destination.
21
+
9
22
  ## [0.2.4] - 2026-05-04
10
23
 
11
24
  ### Added
@@ -67762,7 +67762,12 @@ function getFlyToPosition(flight, t2) {
67762
67762
  }
67763
67763
  _flyDirection.copy(_flyDirectionStart).applyAxisAngle(_flyAxis, angle * t2).normalize();
67764
67764
  } else {
67765
- _flyDirection.copy(_flyDirectionStart);
67765
+ _flyDirection.lerpVectors(_flyDirectionStart, _flyDirectionEnd, t2);
67766
+ if (_flyDirection.lengthSq() < 1e-12) {
67767
+ _flyDirection.copy(_flyDirectionEnd);
67768
+ } else {
67769
+ _flyDirection.normalize();
67770
+ }
67766
67771
  }
67767
67772
  const radius = MathUtils.lerp(startLength, endLength, t2) + Math.sin(Math.PI * t2) * arcHeight;
67768
67773
  return _flyDirection.multiplyScalar(radius);
@@ -68034,11 +68039,7 @@ function getCenterModeHeadingPitchRollForward(heading, pitch) {
68034
68039
  const sinPitch = Math.sin(pitch);
68035
68040
  const cosHeading = Math.cos(heading);
68036
68041
  const sinHeading = Math.sin(heading);
68037
- _flyForward.set(
68038
- sinHeading * cosPitch,
68039
- cosHeading * cosPitch,
68040
- sinPitch
68041
- );
68042
+ _flyForward.set(sinHeading * cosPitch, cosHeading * cosPitch, sinPitch);
68042
68043
  return _flyForward.normalize();
68043
68044
  }
68044
68045
  function getHeadingPitchRollBasis(referencePoint, heading, pitch, roll) {
@@ -68130,11 +68131,7 @@ var init_cameraFlyTo = __esm({
68130
68131
  _flyReferenceUp = new Vector3();
68131
68132
  _flyReferenceRight = new Vector3();
68132
68133
  _flyQuaternion = new Quaternion();
68133
- _ellipsoidRadii = new Vector3(
68134
- 6378137,
68135
- 6378137,
68136
- 6356752314245179e-9
68137
- );
68134
+ _ellipsoidRadii = new Vector3(6378137, 6378137, 6356752314245179e-9);
68138
68135
  _oneOverRadiiSquared = new Vector3(
68139
68136
  1 / (_ellipsoidRadii.x * _ellipsoidRadii.x),
68140
68137
  1 / (_ellipsoidRadii.y * _ellipsoidRadii.y),
@@ -68248,6 +68245,10 @@ function createFlyToController({
68248
68245
  }
68249
68246
  return true;
68250
68247
  }
68248
+ function cancelCameraFlight() {
68249
+ activeCameraFlight = null;
68250
+ activeCameraFlightStatus = "";
68251
+ }
68251
68252
  async function applyTilesSetPositionFromPointerEvent(event) {
68252
68253
  const coordinate = pickCoordinateFromPointerEvent(event);
68253
68254
  if (!coordinate) {
@@ -68312,6 +68313,7 @@ function createFlyToController({
68312
68313
  }
68313
68314
  return {
68314
68315
  applyTilesSetPositionFromPointerEvent,
68316
+ cancelCameraFlight,
68315
68317
  frameTileset,
68316
68318
  getActiveEllipsoid,
68317
68319
  moveCameraToCoordinate,
@@ -68328,6 +68330,148 @@ var init_flyTo = __esm({
68328
68330
  }
68329
68331
  });
68330
68332
 
68333
+ // src/viewer/navigation/cameraUrlPose.js
68334
+ function formatPoseNumber(value) {
68335
+ const fixed = value.toFixed(9);
68336
+ const trimmed = fixed.replace(/(\.\d*?[1-9])0+$/, "$1").replace(/\.0+$/, "");
68337
+ return trimmed === "-0" ? "0" : trimmed;
68338
+ }
68339
+ function serializeCameraPose(camera) {
68340
+ camera.updateMatrixWorld(true);
68341
+ const values = [
68342
+ camera.position.x,
68343
+ camera.position.y,
68344
+ camera.position.z,
68345
+ camera.quaternion.x,
68346
+ camera.quaternion.y,
68347
+ camera.quaternion.z,
68348
+ camera.quaternion.w
68349
+ ];
68350
+ if (camera.isOrthographicCamera) {
68351
+ values.push(camera.zoom);
68352
+ }
68353
+ if (!values.every(Number.isFinite)) {
68354
+ return null;
68355
+ }
68356
+ return values.map(formatPoseNumber).join(CAMERA_POSE_VALUE_SEPARATOR);
68357
+ }
68358
+ function getCameraPoseParam(url) {
68359
+ return url.searchParams.get(CAMERA_POSE_PARAM) ?? url.searchParams.get(CAMERA_POSE_ALIAS_PARAM);
68360
+ }
68361
+ function parseCameraPose(value) {
68362
+ if (typeof value !== "string" || value.trim().length === 0) {
68363
+ return null;
68364
+ }
68365
+ const parts = value.trim().split(/[,_\s]+/).filter(Boolean);
68366
+ if (parts.length !== CAMERA_POSE_COMPONENT_COUNT && parts.length !== CAMERA_POSE_COMPONENT_COUNT_WITH_ZOOM) {
68367
+ return null;
68368
+ }
68369
+ const values = parts.map(Number);
68370
+ if (!values.every(Number.isFinite)) {
68371
+ return null;
68372
+ }
68373
+ const quaternionLength = Math.hypot(
68374
+ values[3],
68375
+ values[4],
68376
+ values[5],
68377
+ values[6]
68378
+ );
68379
+ if (quaternionLength <= 1e-12) {
68380
+ return null;
68381
+ }
68382
+ const zoom = values.length === CAMERA_POSE_COMPONENT_COUNT_WITH_ZOOM ? values[7] : null;
68383
+ if (zoom !== null && zoom <= 0) {
68384
+ return null;
68385
+ }
68386
+ return {
68387
+ position: values.slice(0, 3),
68388
+ quaternion: values.slice(3, 7),
68389
+ zoom
68390
+ };
68391
+ }
68392
+ function replaceCameraPoseUrl(serializedPose) {
68393
+ const url = new URL(window.location.href);
68394
+ url.searchParams.delete(CAMERA_POSE_ALIAS_PARAM);
68395
+ url.searchParams.set(CAMERA_POSE_PARAM, serializedPose);
68396
+ window.history.replaceState(window.history.state, "", url.href);
68397
+ }
68398
+ function getInvalidCameraPoseMessage() {
68399
+ return "Invalid camerapose URL parameter ignored. Expected x_y_z_qx_qy_qz_qw.";
68400
+ }
68401
+ function createCameraUrlPoseController({
68402
+ camera,
68403
+ cameraController,
68404
+ setStatus
68405
+ }) {
68406
+ let lastSerializedPose = null;
68407
+ let lastUrlWriteTime = 0;
68408
+ function applyPose(pose) {
68409
+ camera.position.fromArray(pose.position);
68410
+ camera.quaternion.fromArray(pose.quaternion).normalize();
68411
+ if (pose.zoom !== null && camera.isOrthographicCamera) {
68412
+ camera.zoom = Math.max(pose.zoom, 1e-6);
68413
+ camera.updateProjectionMatrix();
68414
+ }
68415
+ camera.updateMatrixWorld(true);
68416
+ cameraController.setCamera(camera);
68417
+ }
68418
+ function applyFromUrl({ showStatus = false } = {}) {
68419
+ const url = new URL(window.location.href);
68420
+ const value = getCameraPoseParam(url);
68421
+ if (value === null) {
68422
+ return false;
68423
+ }
68424
+ const pose = parseCameraPose(value);
68425
+ if (!pose) {
68426
+ if (showStatus && setStatus) {
68427
+ setStatus(getInvalidCameraPoseMessage(), true);
68428
+ }
68429
+ return false;
68430
+ }
68431
+ applyPose(pose);
68432
+ lastSerializedPose = serializeCameraPose(camera);
68433
+ lastUrlWriteTime = performance.now();
68434
+ if (lastSerializedPose) {
68435
+ replaceCameraPoseUrl(lastSerializedPose);
68436
+ }
68437
+ if (showStatus && setStatus) {
68438
+ setStatus("Applied camera pose from URL.");
68439
+ }
68440
+ return true;
68441
+ }
68442
+ function flush() {
68443
+ const serializedPose = serializeCameraPose(camera);
68444
+ if (!serializedPose || serializedPose === lastSerializedPose) {
68445
+ return;
68446
+ }
68447
+ replaceCameraPoseUrl(serializedPose);
68448
+ lastSerializedPose = serializedPose;
68449
+ lastUrlWriteTime = performance.now();
68450
+ }
68451
+ function update(time = performance.now()) {
68452
+ if (time - lastUrlWriteTime < CAMERA_POSE_UPDATE_INTERVAL_MS) {
68453
+ return;
68454
+ }
68455
+ flush();
68456
+ }
68457
+ return {
68458
+ applyFromUrl,
68459
+ flush,
68460
+ update
68461
+ };
68462
+ }
68463
+ var CAMERA_POSE_PARAM, CAMERA_POSE_ALIAS_PARAM, CAMERA_POSE_COMPONENT_COUNT, CAMERA_POSE_COMPONENT_COUNT_WITH_ZOOM, CAMERA_POSE_UPDATE_INTERVAL_MS, CAMERA_POSE_VALUE_SEPARATOR;
68464
+ var init_cameraUrlPose = __esm({
68465
+ "src/viewer/navigation/cameraUrlPose.js"() {
68466
+ CAMERA_POSE_PARAM = "camerapose";
68467
+ CAMERA_POSE_ALIAS_PARAM = "cameraPose";
68468
+ CAMERA_POSE_COMPONENT_COUNT = 7;
68469
+ CAMERA_POSE_COMPONENT_COUNT_WITH_ZOOM = 8;
68470
+ CAMERA_POSE_UPDATE_INTERVAL_MS = 100;
68471
+ CAMERA_POSE_VALUE_SEPARATOR = "_";
68472
+ }
68473
+ });
68474
+
68331
68475
  // src/viewer/screenSelection/state.js
68332
68476
  function clamp3(value, min, max) {
68333
68477
  return Math.min(max, Math.max(min, value));
@@ -71055,6 +71199,7 @@ var require_app = __commonJS({
71055
71199
  init_shutdown();
71056
71200
  init_setPositionController();
71057
71201
  init_flyTo();
71202
+ init_cameraUrlPose();
71058
71203
  init_cropController();
71059
71204
  init_rootTransformController();
71060
71205
  init_transformModeController();
@@ -71259,6 +71404,14 @@ var require_app = __commonJS({
71259
71404
  getTiles: () => tiles,
71260
71405
  getTilesetBoundingSphere
71261
71406
  });
71407
+ var cameraUrlPose = createCameraUrlPoseController({
71408
+ camera,
71409
+ cameraController,
71410
+ setStatus
71411
+ });
71412
+ var appliedInitialCameraPose = cameraUrlPose.applyFromUrl({
71413
+ showStatus: true
71414
+ });
71262
71415
  setPositionController = createSetPositionController({
71263
71416
  cameraController,
71264
71417
  maxClickDistanceSq: SET_POSITION_CLICK_MAX_DISTANCE_SQ,
@@ -71378,6 +71531,7 @@ var require_app = __commonJS({
71378
71531
  coordinate.longitude,
71379
71532
  coordinate.height
71380
71533
  );
71534
+ flyTo2.moveCameraToTiles();
71381
71535
  setStatus(
71382
71536
  "Moved tileset root to the specified coordinate using ENU orientation. Click Save to persist."
71383
71537
  );
@@ -71561,10 +71715,19 @@ var require_app = __commonJS({
71561
71715
  renderer,
71562
71716
  setStatus
71563
71717
  });
71564
- loadTileset(TILESET_URL);
71718
+ window.addEventListener("popstate", () => {
71719
+ if (cameraUrlPose.applyFromUrl({ showStatus: true })) {
71720
+ flyTo2.cancelCameraFlight();
71721
+ cancelPositionPickModes();
71722
+ }
71723
+ });
71724
+ window.addEventListener("pagehide", cameraUrlPose.flush);
71725
+ cameraController.addEventListener("finish", cameraUrlPose.flush);
71726
+ loadTileset(TILESET_URL, { frameOnLoad: !appliedInitialCameraPose });
71565
71727
  function frame() {
71566
71728
  cameraController.update();
71567
71729
  flyTo2.update();
71730
+ cameraUrlPose.update();
71568
71731
  rootTransform.flush();
71569
71732
  globeController.update();
71570
71733
  tiles?.update();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "3dtiles-inspector",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Inspect, align, and save local 3D Tiles root transforms in an interactive browser session.",
5
5
  "author": "William Liu <lyz15972107087@gmail.com>",
6
6
  "license": "Apache-2.0",
package/src/viewer/app.js CHANGED
@@ -17,6 +17,7 @@ import { bindViewerEvents } from './dom/events.js';
17
17
  import { createViewerShutdownRequester } from './io/shutdown.js';
18
18
  import { createSetPositionController } from './io/setPositionController.js';
19
19
  import { createFlyToController } from './navigation/flyTo.js';
20
+ import { createCameraUrlPoseController } from './navigation/cameraUrlPose.js';
20
21
  import { createCropController } from './screenSelection/cropController.js';
21
22
  import { createRootTransformController } from './transform/rootTransformController.js';
22
23
  import { createTransformModeController } from './transform/transformModeController.js';
@@ -269,6 +270,15 @@ const flyTo = createFlyToController({
269
270
  getTilesetBoundingSphere,
270
271
  });
271
272
 
273
+ const cameraUrlPose = createCameraUrlPoseController({
274
+ camera,
275
+ cameraController,
276
+ setStatus,
277
+ });
278
+ const appliedInitialCameraPose = cameraUrlPose.applyFromUrl({
279
+ showStatus: true,
280
+ });
281
+
272
282
  setPositionController = createSetPositionController({
273
283
  cameraController,
274
284
  maxClickDistanceSq: SET_POSITION_CLICK_MAX_DISTANCE_SQ,
@@ -308,7 +318,8 @@ cropController = createCropController({
308
318
  transformControls,
309
319
  viewerElements,
310
320
  cancelOtherPositionPickModes: () => setPositionController.cancelMode(),
311
- getCurrentRootTransformArray: () => rootTransform.getCurrentRootTransformArray(),
321
+ getCurrentRootTransformArray: () =>
322
+ rootTransform.getCurrentRootTransformArray(),
312
323
  getTilesetBoundingSphere,
313
324
  });
314
325
 
@@ -402,6 +413,7 @@ async function moveTilesToCoordinate() {
402
413
  coordinate.longitude,
403
414
  coordinate.height,
404
415
  );
416
+ flyTo.moveCameraToTiles();
405
417
  setStatus(
406
418
  'Moved tileset root to the specified coordinate using ENU orientation. Click Save to persist.',
407
419
  );
@@ -510,7 +522,8 @@ async function saveTransform() {
510
522
  );
511
523
 
512
524
  const currentMatrix = rootTransform.getCurrentMatrix();
513
- const incrementalMatrix = rootTransform.getIncrementalSinceSaved(currentMatrix);
525
+ const incrementalMatrix =
526
+ rootTransform.getIncrementalSinceSaved(currentMatrix);
514
527
  const saveState = geometricError.getSaveState();
515
528
  let unlockSaveUi = true;
516
529
 
@@ -602,11 +615,21 @@ bindViewerEvents({
602
615
  setStatus,
603
616
  });
604
617
 
605
- loadTileset(TILESET_URL);
618
+ window.addEventListener('popstate', () => {
619
+ if (cameraUrlPose.applyFromUrl({ showStatus: true })) {
620
+ flyTo.cancelCameraFlight();
621
+ cancelPositionPickModes();
622
+ }
623
+ });
624
+ window.addEventListener('pagehide', cameraUrlPose.flush);
625
+ cameraController.addEventListener('finish', cameraUrlPose.flush);
626
+
627
+ loadTileset(TILESET_URL, { frameOnLoad: !appliedInitialCameraPose });
606
628
 
607
629
  function frame() {
608
630
  cameraController.update();
609
631
  flyTo.update();
632
+ cameraUrlPose.update();
610
633
  rootTransform.flush();
611
634
  globeController.update();
612
635
  tiles?.update();
@@ -30,11 +30,7 @@ const _flyCameraRight = new Vector3();
30
30
  const _flyReferenceUp = new Vector3();
31
31
  const _flyReferenceRight = new Vector3();
32
32
  const _flyQuaternion = new Quaternion();
33
- const _ellipsoidRadii = new Vector3(
34
- 6378137.0,
35
- 6378137.0,
36
- 6356752.3142451793,
37
- );
33
+ const _ellipsoidRadii = new Vector3(6378137.0, 6378137.0, 6356752.3142451793);
38
34
  const _oneOverRadiiSquared = new Vector3(
39
35
  1 / (_ellipsoidRadii.x * _ellipsoidRadii.x),
40
36
  1 / (_ellipsoidRadii.y * _ellipsoidRadii.y),
@@ -72,12 +68,7 @@ function eastNorthUpToFixedFrame(origin) {
72
68
  );
73
69
  }
74
70
 
75
- export function createCameraFlight(
76
- camera,
77
- position,
78
- target,
79
- options = {},
80
- ) {
71
+ export function createCameraFlight(camera, position, target, options = {}) {
81
72
  const duration = options.duration ?? 2500;
82
73
  const endPosition = position.clone();
83
74
  const endQuaternion = getEndQuaternion(endPosition, target, options);
@@ -98,11 +89,7 @@ export function createCameraFlight(
98
89
  });
99
90
  }
100
91
 
101
- export function createCameraPoseFlight(
102
- camera,
103
- position,
104
- options,
105
- ) {
92
+ export function createCameraPoseFlight(camera, position, options) {
106
93
  const duration = options.duration ?? 2500;
107
94
  const endPosition = position.clone();
108
95
  const endQuaternion = getHeadingPitchRollQuaternion(
@@ -165,11 +152,7 @@ export function getFlyToParamsFromBoundingSphere(
165
152
  };
166
153
  }
167
154
 
168
- export function flyTo(
169
- camera,
170
- flight,
171
- time,
172
- ) {
155
+ export function flyTo(camera, flight, time) {
173
156
  if (flight.startTime === null) {
174
157
  flight.startTime = time;
175
158
  }
@@ -227,7 +210,12 @@ function getFlyToPosition(flight, t) {
227
210
  .applyAxisAngle(_flyAxis, angle * t)
228
211
  .normalize();
229
212
  } else {
230
- _flyDirection.copy(_flyDirectionStart);
213
+ _flyDirection.lerpVectors(_flyDirectionStart, _flyDirectionEnd, t);
214
+ if (_flyDirection.lengthSq() < 1e-12) {
215
+ _flyDirection.copy(_flyDirectionEnd);
216
+ } else {
217
+ _flyDirection.normalize();
218
+ }
231
219
  }
232
220
 
233
221
  const radius =
@@ -236,11 +224,7 @@ function getFlyToPosition(flight, t) {
236
224
  return _flyDirection.multiplyScalar(radius);
237
225
  }
238
226
 
239
- function getUprightInterpolatedQuaternion(
240
- flight,
241
- position,
242
- t,
243
- ) {
227
+ function getUprightInterpolatedQuaternion(flight, position, t) {
244
228
  const heading = lerpAngle(flight.startHeading, flight.endHeading, t);
245
229
  const pitch = MathUtils.lerp(flight.startPitch, flight.endPitch, t);
246
230
  return getHeadingPitchRollQuaternion(position, heading, pitch, 0);
@@ -366,10 +350,7 @@ function getForwardFromQuaternion(quaternion, target) {
366
350
  return target.set(0, 0, -1).applyQuaternion(quaternion).normalize();
367
351
  }
368
352
 
369
- function getUprightHeadingPitchAtPose(
370
- position,
371
- quaternion,
372
- ) {
353
+ function getUprightHeadingPitchAtPose(position, quaternion) {
373
354
  if (isCenterModePosition(position)) {
374
355
  const forward = getForwardFromQuaternion(quaternion, _flyForward);
375
356
  _flyCameraRight.set(1, 0, 0).applyQuaternion(quaternion);
@@ -489,11 +470,7 @@ function getRollAtPose(position, quaternion) {
489
470
  );
490
471
  }
491
472
 
492
- function getEndQuaternion(
493
- position,
494
- target,
495
- options,
496
- ) {
473
+ function getEndQuaternion(position, target, options) {
497
474
  const { heading, pitch, roll } = options;
498
475
  if (heading === undefined && pitch === undefined && roll === undefined) {
499
476
  return getLookAtQuaternion(position, target);
@@ -516,12 +493,7 @@ function getEndQuaternion(
516
493
  );
517
494
  }
518
495
 
519
- function getBoundingSphereFlyToPosition(
520
- camera,
521
- target,
522
- range,
523
- options,
524
- ) {
496
+ function getBoundingSphereFlyToPosition(camera, target, range, options) {
525
497
  const { heading, pitch } = options;
526
498
  if (heading === undefined && pitch === undefined) {
527
499
  const direction =
@@ -554,12 +526,7 @@ function getBoundingSphereFlyToPosition(
554
526
  return _flyDirection.copy(target).addScaledVector(forward, -range);
555
527
  }
556
528
 
557
- function getHeadingPitchRollQuaternion(
558
- referencePoint,
559
- heading,
560
- pitch,
561
- roll,
562
- ) {
529
+ function getHeadingPitchRollQuaternion(referencePoint, heading, pitch, roll) {
563
530
  if (isCenterModePosition(referencePoint)) {
564
531
  getCenterModeHeadingPitchRollBasis(heading, pitch, roll);
565
532
  _matrix1.makeBasis(_flyRight, _flyUp, _flyBackward);
@@ -575,11 +542,7 @@ function getHeadingPitchRollQuaternion(
575
542
  return new Quaternion().setFromRotationMatrix(_matrix1);
576
543
  }
577
544
 
578
- function getHeadingPitchRollForward(
579
- referencePoint,
580
- heading,
581
- pitch,
582
- ) {
545
+ function getHeadingPitchRollForward(referencePoint, heading, pitch) {
583
546
  if (isCenterModePosition(referencePoint)) {
584
547
  return getCenterModeHeadingPitchRollForward(heading, pitch);
585
548
  }
@@ -604,30 +567,18 @@ function getHeadingPitchRollForward(
604
567
  return _flyForward;
605
568
  }
606
569
 
607
- function getCenterModeHeadingPitchRollForward(
608
- heading,
609
- pitch,
610
- ) {
570
+ function getCenterModeHeadingPitchRollForward(heading, pitch) {
611
571
  const cosPitch = Math.cos(pitch);
612
572
  const sinPitch = Math.sin(pitch);
613
573
  const cosHeading = Math.cos(heading);
614
574
  const sinHeading = Math.sin(heading);
615
575
 
616
- _flyForward.set(
617
- sinHeading * cosPitch,
618
- cosHeading * cosPitch,
619
- sinPitch,
620
- );
576
+ _flyForward.set(sinHeading * cosPitch, cosHeading * cosPitch, sinPitch);
621
577
 
622
578
  return _flyForward.normalize();
623
579
  }
624
580
 
625
- function getHeadingPitchRollBasis(
626
- referencePoint,
627
- heading,
628
- pitch,
629
- roll,
630
- ) {
581
+ function getHeadingPitchRollBasis(referencePoint, heading, pitch, roll) {
631
582
  if (isCenterModePosition(referencePoint)) {
632
583
  getCenterModeHeadingPitchRollBasis(heading, pitch, roll);
633
584
  return;
@@ -650,11 +601,7 @@ function getHeadingPitchRollBasis(
650
601
  _flyBackward.copy(_flyForward).negate();
651
602
  }
652
603
 
653
- function getCenterModeHeadingPitchRollBasis(
654
- heading,
655
- pitch,
656
- roll,
657
- ) {
604
+ function getCenterModeHeadingPitchRollBasis(heading, pitch, roll) {
658
605
  getCenterModeHeadingPitchRollForward(heading, pitch);
659
606
 
660
607
  _flyRight
@@ -683,13 +630,7 @@ function isCenterModePosition(position) {
683
630
  return position.lengthSq() <= CAMERA_CENTER_MODE_DISTANCE_SQ;
684
631
  }
685
632
 
686
- function getReferenceBasis(
687
- forward,
688
- east,
689
- up,
690
- rightTarget,
691
- upTarget,
692
- ) {
633
+ function getReferenceBasis(forward, east, up, rightTarget, upTarget) {
693
634
  rightTarget.crossVectors(forward, up);
694
635
  if (rightTarget.lengthSq() < 1e-6) {
695
636
  rightTarget.copy(east).projectOnPlane(forward);
@@ -712,12 +653,7 @@ function lerpAngle(start, end, t) {
712
653
  return start + delta * t;
713
654
  }
714
655
 
715
- function applyFlyToPose(
716
- camera,
717
- position,
718
- quaternion,
719
- zoom = null,
720
- ) {
656
+ function applyFlyToPose(camera, position, quaternion, zoom = null) {
721
657
  camera.position.copy(position);
722
658
  camera.quaternion.copy(quaternion);
723
659
 
@@ -0,0 +1,172 @@
1
+ const CAMERA_POSE_PARAM = 'camerapose';
2
+ const CAMERA_POSE_ALIAS_PARAM = 'cameraPose';
3
+ const CAMERA_POSE_COMPONENT_COUNT = 7;
4
+ const CAMERA_POSE_COMPONENT_COUNT_WITH_ZOOM = 8;
5
+ const CAMERA_POSE_UPDATE_INTERVAL_MS = 100;
6
+ const CAMERA_POSE_VALUE_SEPARATOR = '_';
7
+
8
+ function formatPoseNumber(value) {
9
+ const fixed = value.toFixed(9);
10
+ const trimmed = fixed
11
+ .replace(/(\.\d*?[1-9])0+$/, '$1')
12
+ .replace(/\.0+$/, '');
13
+
14
+ return trimmed === '-0' ? '0' : trimmed;
15
+ }
16
+
17
+ function serializeCameraPose(camera) {
18
+ camera.updateMatrixWorld(true);
19
+
20
+ const values = [
21
+ camera.position.x,
22
+ camera.position.y,
23
+ camera.position.z,
24
+ camera.quaternion.x,
25
+ camera.quaternion.y,
26
+ camera.quaternion.z,
27
+ camera.quaternion.w,
28
+ ];
29
+
30
+ if (camera.isOrthographicCamera) {
31
+ values.push(camera.zoom);
32
+ }
33
+
34
+ if (!values.every(Number.isFinite)) {
35
+ return null;
36
+ }
37
+
38
+ return values.map(formatPoseNumber).join(CAMERA_POSE_VALUE_SEPARATOR);
39
+ }
40
+
41
+ function getCameraPoseParam(url) {
42
+ return (
43
+ url.searchParams.get(CAMERA_POSE_PARAM) ??
44
+ url.searchParams.get(CAMERA_POSE_ALIAS_PARAM)
45
+ );
46
+ }
47
+
48
+ function parseCameraPose(value) {
49
+ if (typeof value !== 'string' || value.trim().length === 0) {
50
+ return null;
51
+ }
52
+
53
+ const parts = value.trim().split(/[,_\s]+/).filter(Boolean);
54
+ if (
55
+ parts.length !== CAMERA_POSE_COMPONENT_COUNT &&
56
+ parts.length !== CAMERA_POSE_COMPONENT_COUNT_WITH_ZOOM
57
+ ) {
58
+ return null;
59
+ }
60
+
61
+ const values = parts.map(Number);
62
+ if (!values.every(Number.isFinite)) {
63
+ return null;
64
+ }
65
+
66
+ const quaternionLength = Math.hypot(
67
+ values[3],
68
+ values[4],
69
+ values[5],
70
+ values[6],
71
+ );
72
+ if (quaternionLength <= 1e-12) {
73
+ return null;
74
+ }
75
+
76
+ const zoom = values.length === CAMERA_POSE_COMPONENT_COUNT_WITH_ZOOM
77
+ ? values[7]
78
+ : null;
79
+ if (zoom !== null && zoom <= 0) {
80
+ return null;
81
+ }
82
+
83
+ return {
84
+ position: values.slice(0, 3),
85
+ quaternion: values.slice(3, 7),
86
+ zoom,
87
+ };
88
+ }
89
+
90
+ function replaceCameraPoseUrl(serializedPose) {
91
+ const url = new URL(window.location.href);
92
+ url.searchParams.delete(CAMERA_POSE_ALIAS_PARAM);
93
+ url.searchParams.set(CAMERA_POSE_PARAM, serializedPose);
94
+ window.history.replaceState(window.history.state, '', url.href);
95
+ }
96
+
97
+ function getInvalidCameraPoseMessage() {
98
+ return 'Invalid camerapose URL parameter ignored. Expected x_y_z_qx_qy_qz_qw.';
99
+ }
100
+
101
+ export function createCameraUrlPoseController({
102
+ camera,
103
+ cameraController,
104
+ setStatus,
105
+ }) {
106
+ let lastSerializedPose = null;
107
+ let lastUrlWriteTime = 0;
108
+
109
+ function applyPose(pose) {
110
+ camera.position.fromArray(pose.position);
111
+ camera.quaternion.fromArray(pose.quaternion).normalize();
112
+
113
+ if (pose.zoom !== null && camera.isOrthographicCamera) {
114
+ camera.zoom = Math.max(pose.zoom, 1e-6);
115
+ camera.updateProjectionMatrix();
116
+ }
117
+
118
+ camera.updateMatrixWorld(true);
119
+ cameraController.setCamera(camera);
120
+ }
121
+
122
+ function applyFromUrl({ showStatus = false } = {}) {
123
+ const url = new URL(window.location.href);
124
+ const value = getCameraPoseParam(url);
125
+ if (value === null) {
126
+ return false;
127
+ }
128
+
129
+ const pose = parseCameraPose(value);
130
+ if (!pose) {
131
+ if (showStatus && setStatus) {
132
+ setStatus(getInvalidCameraPoseMessage(), true);
133
+ }
134
+ return false;
135
+ }
136
+
137
+ applyPose(pose);
138
+ lastSerializedPose = serializeCameraPose(camera);
139
+ lastUrlWriteTime = performance.now();
140
+ if (lastSerializedPose) {
141
+ replaceCameraPoseUrl(lastSerializedPose);
142
+ }
143
+ if (showStatus && setStatus) {
144
+ setStatus('Applied camera pose from URL.');
145
+ }
146
+ return true;
147
+ }
148
+
149
+ function flush() {
150
+ const serializedPose = serializeCameraPose(camera);
151
+ if (!serializedPose || serializedPose === lastSerializedPose) {
152
+ return;
153
+ }
154
+
155
+ replaceCameraPoseUrl(serializedPose);
156
+ lastSerializedPose = serializedPose;
157
+ lastUrlWriteTime = performance.now();
158
+ }
159
+
160
+ function update(time = performance.now()) {
161
+ if (time - lastUrlWriteTime < CAMERA_POSE_UPDATE_INTERVAL_MS) {
162
+ return;
163
+ }
164
+ flush();
165
+ }
166
+
167
+ return {
168
+ applyFromUrl,
169
+ flush,
170
+ update,
171
+ };
172
+ }
@@ -133,6 +133,11 @@ export function createFlyToController({
133
133
  return true;
134
134
  }
135
135
 
136
+ function cancelCameraFlight() {
137
+ activeCameraFlight = null;
138
+ activeCameraFlightStatus = '';
139
+ }
140
+
136
141
  async function applyTilesSetPositionFromPointerEvent(event) {
137
142
  const coordinate = pickCoordinateFromPointerEvent(event);
138
143
  if (!coordinate) {
@@ -204,6 +209,7 @@ export function createFlyToController({
204
209
 
205
210
  return {
206
211
  applyTilesSetPositionFromPointerEvent,
212
+ cancelCameraFlight,
207
213
  frameTileset,
208
214
  getActiveEllipsoid,
209
215
  moveCameraToCoordinate,