3dtiles-inspector 0.2.5 → 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,12 @@ 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
+
9
15
  ## [0.2.5] - 2026-05-05
10
16
 
11
17
  ### Fixed
@@ -68245,6 +68245,10 @@ function createFlyToController({
68245
68245
  }
68246
68246
  return true;
68247
68247
  }
68248
+ function cancelCameraFlight() {
68249
+ activeCameraFlight = null;
68250
+ activeCameraFlightStatus = "";
68251
+ }
68248
68252
  async function applyTilesSetPositionFromPointerEvent(event) {
68249
68253
  const coordinate = pickCoordinateFromPointerEvent(event);
68250
68254
  if (!coordinate) {
@@ -68309,6 +68313,7 @@ function createFlyToController({
68309
68313
  }
68310
68314
  return {
68311
68315
  applyTilesSetPositionFromPointerEvent,
68316
+ cancelCameraFlight,
68312
68317
  frameTileset,
68313
68318
  getActiveEllipsoid,
68314
68319
  moveCameraToCoordinate,
@@ -68325,6 +68330,148 @@ var init_flyTo = __esm({
68325
68330
  }
68326
68331
  });
68327
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
+
68328
68475
  // src/viewer/screenSelection/state.js
68329
68476
  function clamp3(value, min, max) {
68330
68477
  return Math.min(max, Math.max(min, value));
@@ -71052,6 +71199,7 @@ var require_app = __commonJS({
71052
71199
  init_shutdown();
71053
71200
  init_setPositionController();
71054
71201
  init_flyTo();
71202
+ init_cameraUrlPose();
71055
71203
  init_cropController();
71056
71204
  init_rootTransformController();
71057
71205
  init_transformModeController();
@@ -71256,6 +71404,14 @@ var require_app = __commonJS({
71256
71404
  getTiles: () => tiles,
71257
71405
  getTilesetBoundingSphere
71258
71406
  });
71407
+ var cameraUrlPose = createCameraUrlPoseController({
71408
+ camera,
71409
+ cameraController,
71410
+ setStatus
71411
+ });
71412
+ var appliedInitialCameraPose = cameraUrlPose.applyFromUrl({
71413
+ showStatus: true
71414
+ });
71259
71415
  setPositionController = createSetPositionController({
71260
71416
  cameraController,
71261
71417
  maxClickDistanceSq: SET_POSITION_CLICK_MAX_DISTANCE_SQ,
@@ -71559,10 +71715,19 @@ var require_app = __commonJS({
71559
71715
  renderer,
71560
71716
  setStatus
71561
71717
  });
71562
- 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 });
71563
71727
  function frame() {
71564
71728
  cameraController.update();
71565
71729
  flyTo2.update();
71730
+ cameraUrlPose.update();
71566
71731
  rootTransform.flush();
71567
71732
  globeController.update();
71568
71733
  tiles?.update();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "3dtiles-inspector",
3
- "version": "0.2.5",
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,
@@ -605,11 +615,21 @@ bindViewerEvents({
605
615
  setStatus,
606
616
  });
607
617
 
608
- 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 });
609
628
 
610
629
  function frame() {
611
630
  cameraController.update();
612
631
  flyTo.update();
632
+ cameraUrlPose.update();
613
633
  rootTransform.flush();
614
634
  globeController.update();
615
635
  tiles?.update();
@@ -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,