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.
|
|
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
|
-
|
|
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.
|
|
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: () =>
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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,
|