3dtiles-inspector 0.1.7 → 0.1.8

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,17 @@ The format is based on Keep a Changelog and this project follows Semantic Versio
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.1.8] - 2026-05-03
10
+
11
+ ### Added
12
+
13
+ - Added a camera pivot indicator for rotate, pan, and zoom interactions.
14
+ - Added bottom-right tile runtime stats for downloading, parsing, loaded, and visible tile counts.
15
+
16
+ ### Changed
17
+
18
+ - Moved the `Canvas` toolbar section above `Transform`.
19
+
9
20
  ## [0.1.7] - 2026-04-27
10
21
 
11
22
  ### Changed
package/README.md CHANGED
@@ -14,6 +14,13 @@
14
14
 
15
15
  Requires Node.js 18 or newer.
16
16
 
17
+ ## Built On
18
+
19
+ This project is based on and integrates work from:
20
+
21
+ - [WilliamLiu-1997/3D-Tiles-RendererJS-3DGS-Plugin](https://github.com/WilliamLiu-1997/3D-Tiles-RendererJS-3DGS-Plugin)
22
+ - [NASA-AMMOS/3DTilesRendererJS](https://github.com/NASA-AMMOS/3DTilesRendererJS)
23
+
17
24
  ## Install
18
25
 
19
26
  ```bash
@@ -64485,6 +64485,88 @@ var PointerTracker = class {
64485
64485
  return Boolean(this.buttons & 4);
64486
64486
  }
64487
64487
  };
64488
+ var PivotPointMesh = class extends Mesh {
64489
+ constructor(size = 15, thickness = 3) {
64490
+ super(new PlaneGeometry(0, 0), new PivotMaterial(size, thickness));
64491
+ this.renderOrder = Infinity;
64492
+ }
64493
+ set focus(value) {
64494
+ this.material.uniforms.opacity.value = value ? 1 : 0.5;
64495
+ }
64496
+ onBeforeRender(renderer2) {
64497
+ renderer2.getSize(this.material.uniforms.resolution.value);
64498
+ }
64499
+ updateMatrixWorld() {
64500
+ this.matrixWorld.makeTranslation(this.position);
64501
+ }
64502
+ dispose() {
64503
+ this.geometry.dispose();
64504
+ this.material.dispose();
64505
+ }
64506
+ };
64507
+ var PivotMaterial = class extends ShaderMaterial {
64508
+ constructor(size, thickness) {
64509
+ const coreD = size + thickness;
64510
+ const planeD = coreD + 3 * thickness;
64511
+ const normThk = thickness / coreD;
64512
+ const ringR = (coreD - 0.4 * thickness - 4) / coreD;
64513
+ const hw = 0.4 * normThk;
64514
+ super({
64515
+ depthWrite: false,
64516
+ depthTest: false,
64517
+ transparent: true,
64518
+ uniforms: {
64519
+ resolution: { value: new Vector2() },
64520
+ opacity: { value: 1 },
64521
+ planeD: { value: planeD },
64522
+ hw: { value: hw },
64523
+ ringR: { value: ringR },
64524
+ shadowW: { value: hw * 5 },
64525
+ uvScale: { value: planeD / coreD }
64526
+ },
64527
+ vertexShader: `
64528
+ uniform float planeD;
64529
+ uniform vec2 resolution;
64530
+ varying vec2 vUv;
64531
+
64532
+ void main() {
64533
+ vUv = uv;
64534
+ float aspect = resolution.x / resolution.y;
64535
+ vec2 offset = uv * 2.0 - vec2(1.0);
64536
+ offset.y *= aspect;
64537
+ vec4 screenPoint = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
64538
+ screenPoint.xy += offset * planeD * screenPoint.w / resolution.x;
64539
+ gl_Position = screenPoint;
64540
+ }
64541
+ `,
64542
+ fragmentShader: `
64543
+ uniform float hw;
64544
+ uniform float ringR;
64545
+ uniform float shadowW;
64546
+ uniform float opacity;
64547
+ uniform float uvScale;
64548
+ varying vec2 vUv;
64549
+
64550
+ void main() {
64551
+ vec2 uv = (vUv * 2.0 - 1.0) * uvScale;
64552
+ float len = length(uv);
64553
+ float fw = fwidth(len) * 0.5;
64554
+ float d = abs(len - ringR);
64555
+
64556
+ float ring = 1.0 - smoothstep(hw - fw, hw + fw, d);
64557
+
64558
+ float shadow = (1.0 - smoothstep(hw, shadowW, d)) * (1.0 - smoothstep(ringR - fw, ringR + fw, len)) * 0.5;
64559
+
64560
+ float white = ring;
64561
+ float black = shadow * (1.0 - white);
64562
+ float alpha = (white + black) * opacity;
64563
+ if (alpha < 0.001) discard;
64564
+ gl_FragColor = vec4(vec3(white / max(alpha / opacity, 0.001)), alpha);
64565
+ }
64566
+ `
64567
+ });
64568
+ }
64569
+ };
64488
64570
  var _matrix2 = new Matrix4();
64489
64571
  function setRaycasterFromCamera(raycaster, coords, camera2) {
64490
64572
  const ray = raycaster instanceof Ray ? raycaster : raycaster.ray;
@@ -64522,6 +64604,8 @@ var UPDATE_EVENT = { type: "update" };
64522
64604
  var FINISH_EVENT = { type: "finish" };
64523
64605
  var THRESHOLD = 1e-3;
64524
64606
  var MAX = 1e8;
64607
+ var PIVOT_SIZE = 22;
64608
+ var PIVOT_THICKNESS = 2.5;
64525
64609
  var VIRTUAL_HIT_DISTANCE = 50;
64526
64610
  var ZOOM_OUT_TRANSITION_COS_THRESHOLD = Math.cos(105 * Math.PI / 180);
64527
64611
  var ZOOM_OUT_TRANSITION_COS_MAX_THRESHOLD = Math.cos(95 * Math.PI / 180);
@@ -64550,7 +64634,7 @@ var _quaternion2 = new Quaternion();
64550
64634
  var _plane = new Plane();
64551
64635
  var _ray2 = new Ray();
64552
64636
  var _zoomOutMetrics = { distanceScale: 1, transitionWeight: 0 };
64553
- var _pointerTracker, _domElement, _camera3, _scene2, _raycaster2, _zoomDelta, _zoomInertia, _rotateInertia, _dragInertia, _dragAnchorPoint, _dragStartPosition, _dragStartQuaternion, _dragPlaneNormal, _inertiaValue, _enabled, _ellipsoid, _ellipsoidMaxRadius, _lastTime, _hit, _contextMenuEvent, _pointerDownEvent, _pointerMoveEvent, _pointerUpEvent, _wheelEvent, _pointerEnterEvent, _zoomTimeout, _CameraController_instances, setState_fn, setZooming_fn, resetState_fn, bindEvents_fn, _contextMenu, _pointerDown, _pointerMove, _pointerUp, _wheel, _pointerEnter, finalizeCamera_fn, alignCameraRightToXYPlane_fn, rotateNearAnchor_fn, clampVerticalRotateAngle_fn, rotate_fn, initializeDragAnchor_fn, restoreDragStartCamera_fn, intersectDragPlane_fn, modifiedDrag_fn, keepCameraUpAtFixedPoint_fn, getZoomOutMetrics_fn, getScaledZoomTarget_fn, getZoomOutTransitionTarget_fn, getZoomPosition_fn, applyZoom_fn, reachCameraMaxDistance_fn, isCameraCenterMode_fn, limitCameraDistance_fn, shouldDragModified_fn, keepCameraUp_fn, normalRaycastClosest_fn, raycast_fn;
64637
+ var _pointerTracker, _domElement, _camera3, _scene2, _raycaster2, _pivotMesh, _zoomDelta, _zoomInertia, _rotateInertia, _dragInertia, _dragAnchorPoint, _dragStartPosition, _dragStartQuaternion, _dragPlaneNormal, _inertiaValue, _enabled, _ellipsoid, _ellipsoidMaxRadius, _lastTime, _hit, _contextMenuEvent, _pointerDownEvent, _pointerMoveEvent, _pointerUpEvent, _wheelEvent, _pointerEnterEvent, _zoomTimeout, _CameraController_instances, setState_fn, setZooming_fn, resetState_fn, bindEvents_fn, _contextMenu, updateIndicatorFromHit_fn, _pointerDown, _pointerMove, _pointerUp, _wheel, _pointerEnter, finalizeCamera_fn, alignCameraRightToXYPlane_fn, rotateNearAnchor_fn, clampVerticalRotateAngle_fn, rotate_fn, initializeDragAnchor_fn, restoreDragStartCamera_fn, intersectDragPlane_fn, modifiedDrag_fn, keepCameraUpAtFixedPoint_fn, getZoomOutMetrics_fn, getScaledZoomTarget_fn, getZoomOutTransitionTarget_fn, getZoomPosition_fn, applyZoom_fn, reachCameraMaxDistance_fn, isCameraCenterMode_fn, limitCameraDistance_fn, shouldDragModified_fn, keepCameraUp_fn, normalRaycastClosest_fn, raycast_fn;
64554
64638
  var CameraController = class extends EventDispatcher {
64555
64639
  constructor(renderer2, scene2, camera2, options = {}) {
64556
64640
  super();
@@ -64567,6 +64651,7 @@ var CameraController = class extends EventDispatcher {
64567
64651
  __privateAdd(this, _camera3);
64568
64652
  __privateAdd(this, _scene2);
64569
64653
  __privateAdd(this, _raycaster2);
64654
+ __privateAdd(this, _pivotMesh);
64570
64655
  __privateAdd(this, _zoomDelta);
64571
64656
  __privateAdd(this, _zoomInertia);
64572
64657
  __privateAdd(this, _rotateInertia);
@@ -64624,6 +64709,7 @@ var CameraController = class extends EventDispatcher {
64624
64709
  mouseToCoords(_pointer1.x, _pointer1.y, __privateGet(this, _domElement), _pointer1);
64625
64710
  setRaycasterFromCamera(__privateGet(this, _raycaster2), _pointer1, __privateGet(this, _camera3));
64626
64711
  __privateSet(this, _hit, __privateMethod(this, _CameraController_instances, raycast_fn).call(this, __privateGet(this, _raycaster2)));
64712
+ __privateMethod(this, _CameraController_instances, updateIndicatorFromHit_fn).call(this);
64627
64713
  if (this.state === DRAG && __privateGet(this, _hit).distance > 0) {
64628
64714
  __privateMethod(this, _CameraController_instances, initializeDragAnchor_fn).call(this);
64629
64715
  }
@@ -64660,7 +64746,7 @@ var CameraController = class extends EventDispatcher {
64660
64746
  if (!__privateGet(this, _enabled)) {
64661
64747
  return;
64662
64748
  }
64663
- const tooClose = __privateGet(this, _hit) ? __privateGet(this, _hit).point.distanceTo(__privateGet(this, _camera3).position) <= __privateGet(this, _camera3).near : false;
64749
+ const tooClose = __privateGet(this, _pivotMesh).position.distanceTo(__privateGet(this, _camera3).position) <= __privateGet(this, _camera3).near;
64664
64750
  if (!this.zooming || tooClose) {
64665
64751
  __privateGet(this, _rotateInertia).set(0, 0);
64666
64752
  __privateGet(this, _dragInertia).set(0, 0, 0);
@@ -64674,6 +64760,7 @@ var CameraController = class extends EventDispatcher {
64674
64760
  setRaycasterFromCamera(__privateGet(this, _raycaster2), _pointer1, __privateGet(this, _camera3));
64675
64761
  if (!this.zooming && this.state === NONE || tooClose) {
64676
64762
  __privateSet(this, _hit, __privateMethod(this, _CameraController_instances, raycast_fn).call(this, __privateGet(this, _raycaster2)));
64763
+ __privateMethod(this, _CameraController_instances, updateIndicatorFromHit_fn).call(this);
64677
64764
  }
64678
64765
  let delta = 0;
64679
64766
  switch (e.deltaMode) {
@@ -64721,6 +64808,8 @@ var CameraController = class extends EventDispatcher {
64721
64808
  __privateSet(this, _pointerTracker, new PointerTracker());
64722
64809
  __privateSet(this, _raycaster2, new Raycaster());
64723
64810
  __privateGet(this, _raycaster2).params.Points.threshold = 0.1;
64811
+ __privateSet(this, _pivotMesh, new PivotPointMesh(PIVOT_SIZE, PIVOT_THICKNESS));
64812
+ __privateGet(this, _pivotMesh).visible = false;
64724
64813
  __privateSet(this, _zoomDelta, 0);
64725
64814
  __privateSet(this, _zoomInertia, 0);
64726
64815
  __privateSet(this, _rotateInertia, new Vector2());
@@ -64759,6 +64848,9 @@ var CameraController = class extends EventDispatcher {
64759
64848
  get camera() {
64760
64849
  return __privateGet(this, _camera3);
64761
64850
  }
64851
+ get indicator() {
64852
+ return __privateGet(this, _pivotMesh);
64853
+ }
64762
64854
  setCamera(camera2) {
64763
64855
  __privateSet(this, _camera3, camera2);
64764
64856
  __privateMethod(this, _CameraController_instances, resetState_fn).call(this);
@@ -64772,6 +64864,9 @@ var CameraController = class extends EventDispatcher {
64772
64864
  }
64773
64865
  init() {
64774
64866
  __privateGet(this, _domElement).style.touchAction = "none";
64867
+ __privateGet(this, _pivotMesh).raycast = () => {
64868
+ };
64869
+ __privateGet(this, _scene2).add(__privateGet(this, _pivotMesh));
64775
64870
  __privateSet(this, _contextMenuEvent, __privateGet(this, _contextMenu).bind(this));
64776
64871
  __privateSet(this, _pointerDownEvent, __privateGet(this, _pointerDown).bind(this));
64777
64872
  __privateSet(this, _pointerMoveEvent, __privateGet(this, _pointerMove).bind(this));
@@ -64945,6 +65040,8 @@ var CameraController = class extends EventDispatcher {
64945
65040
  "pointerenter",
64946
65041
  __privateGet(this, _pointerEnterEvent)
64947
65042
  );
65043
+ __privateGet(this, _pivotMesh).removeFromParent();
65044
+ __privateGet(this, _pivotMesh).dispose();
64948
65045
  __privateGet(this, _domElement).style.touchAction = "";
64949
65046
  __privateSet(this, _enabled, false);
64950
65047
  __privateSet(this, _ellipsoid, null);
@@ -64955,6 +65052,7 @@ _domElement = new WeakMap();
64955
65052
  _camera3 = new WeakMap();
64956
65053
  _scene2 = new WeakMap();
64957
65054
  _raycaster2 = new WeakMap();
65055
+ _pivotMesh = new WeakMap();
64958
65056
  _zoomDelta = new WeakMap();
64959
65057
  _zoomInertia = new WeakMap();
64960
65058
  _rotateInertia = new WeakMap();
@@ -64982,8 +65080,14 @@ setState_fn = function(state = this.state) {
64982
65080
  return;
64983
65081
  }
64984
65082
  this.state = state;
65083
+ if (state !== NONE) {
65084
+ __privateGet(this, _pivotMesh).visible = true;
65085
+ }
64985
65086
  };
64986
65087
  setZooming_fn = function(zooming, touchZooming = false) {
65088
+ if (!this.zooming && this.state === NONE && zooming) {
65089
+ __privateGet(this, _pivotMesh).visible = true;
65090
+ }
64987
65091
  this.zooming = zooming;
64988
65092
  this.touchZooming = touchZooming;
64989
65093
  };
@@ -65000,6 +65104,7 @@ resetState_fn = function() {
65000
65104
  __privateGet(this, _dragPlaneNormal).set(0, 0, 0);
65001
65105
  __privateSet(this, _zoomInertia, 0);
65002
65106
  __privateSet(this, _hit, null);
65107
+ __privateGet(this, _pivotMesh).visible = false;
65003
65108
  };
65004
65109
  bindEvents_fn = function() {
65005
65110
  __privateGet(this, _domElement).addEventListener("contextmenu", __privateGet(this, _contextMenuEvent));
@@ -65010,6 +65115,15 @@ bindEvents_fn = function() {
65010
65115
  __privateGet(this, _domElement).addEventListener("pointerenter", __privateGet(this, _pointerEnterEvent));
65011
65116
  };
65012
65117
  _contextMenu = new WeakMap();
65118
+ updateIndicatorFromHit_fn = function() {
65119
+ if (__privateGet(this, _hit) && __privateGet(this, _hit).distance > 0) {
65120
+ __privateGet(this, _pivotMesh).visible = true;
65121
+ __privateGet(this, _pivotMesh).position.copy(__privateGet(this, _hit).point);
65122
+ __privateGet(this, _pivotMesh).focus = !__privateGet(this, _hit).onGlobe;
65123
+ } else {
65124
+ __privateGet(this, _pivotMesh).visible = false;
65125
+ }
65126
+ };
65013
65127
  _pointerDown = new WeakMap();
65014
65128
  _pointerMove = new WeakMap();
65015
65129
  _pointerUp = new WeakMap();
@@ -65291,6 +65405,7 @@ applyZoom_fn = function(zoomAmount) {
65291
65405
  _forward.subVectors(hit.point, __privateGet(this, _camera3).position).normalize();
65292
65406
  hit.point.copy(__privateGet(this, _camera3).position).addScaledVector(_forward, VIRTUAL_HIT_DISTANCE);
65293
65407
  hit.distance = VIRTUAL_HIT_DISTANCE;
65408
+ __privateGet(this, _pivotMesh).position.copy(hit.point);
65294
65409
  }
65295
65410
  let zoomFactor = Math.exp(-zoomAmount * 1e-3);
65296
65411
  if (this.minDistance > 0 && zoomFactor < 1) {
@@ -65463,6 +65578,12 @@ var MOVE_TO_COORDINATE_RADIUS = 10;
65463
65578
  var statusEl = document.getElementById("status");
65464
65579
  var cacheBytesValueEl = document.getElementById("cache-bytes-value");
65465
65580
  var splatsCountValueEl = document.getElementById("splats-count-value");
65581
+ var tilesDownloadingValueEl = document.getElementById(
65582
+ "tiles-downloading-value"
65583
+ );
65584
+ var tilesParsingValueEl = document.getElementById("tiles-parsing-value");
65585
+ var tilesLoadedValueEl = document.getElementById("tiles-loaded-value");
65586
+ var tilesVisibleValueEl = document.getElementById("tiles-visible-value");
65466
65587
  var toolbarEl = document.getElementById("toolbar");
65467
65588
  var toolbarDockEl = toolbarEl.parentElement;
65468
65589
  var toolbarToggleButton = document.getElementById("toolbar-toggle");
@@ -65963,7 +66084,7 @@ function getActiveSparkSplatsCount() {
65963
66084
  return count;
65964
66085
  }
65965
66086
  function updateRuntimeStats(force = false) {
65966
- if (!cacheBytesValueEl || !splatsCountValueEl) {
66087
+ if (!cacheBytesValueEl || !splatsCountValueEl || !tilesDownloadingValueEl || !tilesParsingValueEl || !tilesLoadedValueEl || !tilesVisibleValueEl) {
65967
66088
  return;
65968
66089
  }
65969
66090
  const now = performance.now();
@@ -65972,10 +66093,19 @@ function updateRuntimeStats(force = false) {
65972
66093
  }
65973
66094
  lastRuntimeStatsUpdateTime = now;
65974
66095
  const cacheBytes = tiles?.lruCache?.cachedBytes ?? 0;
66096
+ const tilesStats = tiles?.stats;
66097
+ const downloadingTiles = tilesStats?.downloading ?? 0;
66098
+ const parsingTiles = tilesStats?.parsing ?? 0;
66099
+ const loadedTiles = tilesStats?.loaded ?? 0;
66100
+ const visibleTiles = tiles?.visibleTiles?.size ?? tilesStats?.visible ?? 0;
65975
66101
  const activeSparkSplats = getActiveSparkSplatsCount();
65976
66102
  const splatCount = activeSparkSplats !== null ? activeSparkSplats : getLoadedGaussianSplatCount();
65977
66103
  cacheBytesValueEl.textContent = formatBytes(cacheBytes);
65978
66104
  splatsCountValueEl.textContent = formatInteger(splatCount);
66105
+ tilesDownloadingValueEl.textContent = formatInteger(downloadingTiles);
66106
+ tilesParsingValueEl.textContent = formatInteger(parsingTiles);
66107
+ tilesLoadedValueEl.textContent = formatInteger(loadedTiles);
66108
+ tilesVisibleValueEl.textContent = formatInteger(visibleTiles);
65979
66109
  }
65980
66110
  function setGeometricErrorScaleExponent(exponent) {
65981
66111
  geometricErrorScaleExponent = clamp2(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "3dtiles-inspector",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
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
@@ -79,6 +79,12 @@ const MOVE_TO_COORDINATE_RADIUS = 10;
79
79
  const statusEl = document.getElementById('status');
80
80
  const cacheBytesValueEl = document.getElementById('cache-bytes-value');
81
81
  const splatsCountValueEl = document.getElementById('splats-count-value');
82
+ const tilesDownloadingValueEl = document.getElementById(
83
+ 'tiles-downloading-value',
84
+ );
85
+ const tilesParsingValueEl = document.getElementById('tiles-parsing-value');
86
+ const tilesLoadedValueEl = document.getElementById('tiles-loaded-value');
87
+ const tilesVisibleValueEl = document.getElementById('tiles-visible-value');
82
88
  const toolbarEl = document.getElementById('toolbar');
83
89
  const toolbarDockEl = toolbarEl.parentElement;
84
90
  const toolbarToggleButton = document.getElementById('toolbar-toggle');
@@ -692,7 +698,14 @@ function getActiveSparkSplatsCount() {
692
698
  }
693
699
 
694
700
  function updateRuntimeStats(force = false) {
695
- if (!cacheBytesValueEl || !splatsCountValueEl) {
701
+ if (
702
+ !cacheBytesValueEl ||
703
+ !splatsCountValueEl ||
704
+ !tilesDownloadingValueEl ||
705
+ !tilesParsingValueEl ||
706
+ !tilesLoadedValueEl ||
707
+ !tilesVisibleValueEl
708
+ ) {
696
709
  return;
697
710
  }
698
711
 
@@ -707,6 +720,11 @@ function updateRuntimeStats(force = false) {
707
720
  lastRuntimeStatsUpdateTime = now;
708
721
 
709
722
  const cacheBytes = tiles?.lruCache?.cachedBytes ?? 0;
723
+ const tilesStats = tiles?.stats;
724
+ const downloadingTiles = tilesStats?.downloading ?? 0;
725
+ const parsingTiles = tilesStats?.parsing ?? 0;
726
+ const loadedTiles = tilesStats?.loaded ?? 0;
727
+ const visibleTiles = tiles?.visibleTiles?.size ?? tilesStats?.visible ?? 0;
710
728
  const activeSparkSplats = getActiveSparkSplatsCount();
711
729
  const splatCount =
712
730
  activeSparkSplats !== null
@@ -715,6 +733,10 @@ function updateRuntimeStats(force = false) {
715
733
 
716
734
  cacheBytesValueEl.textContent = formatBytes(cacheBytes);
717
735
  splatsCountValueEl.textContent = formatInteger(splatCount);
736
+ tilesDownloadingValueEl.textContent = formatInteger(downloadingTiles);
737
+ tilesParsingValueEl.textContent = formatInteger(parsingTiles);
738
+ tilesLoadedValueEl.textContent = formatInteger(loadedTiles);
739
+ tilesVisibleValueEl.textContent = formatInteger(visibleTiles);
718
740
  }
719
741
 
720
742
  function setGeometricErrorScaleExponent(exponent) {
@@ -1,10 +1,13 @@
1
1
  import {
2
2
  EventDispatcher,
3
3
  Matrix4,
4
+ Mesh,
4
5
  Plane,
6
+ PlaneGeometry,
5
7
  Quaternion,
6
8
  Ray,
7
9
  Raycaster,
10
+ ShaderMaterial,
8
11
  Vector2,
9
12
  Vector3,
10
13
  } from 'three';
@@ -191,6 +194,98 @@ class PointerTracker {
191
194
  return Boolean(this.buttons & 4);
192
195
  }
193
196
  }
197
+
198
+ class PivotPointMesh extends Mesh {
199
+ constructor(size = 15, thickness = 3) {
200
+ super(new PlaneGeometry(0, 0), new PivotMaterial(size, thickness));
201
+ this.renderOrder = Infinity;
202
+ }
203
+
204
+ set focus(value) {
205
+ this.material.uniforms.opacity.value = value ? 1 : 0.5;
206
+ }
207
+
208
+ onBeforeRender(renderer) {
209
+ renderer.getSize(this.material.uniforms.resolution.value);
210
+ }
211
+
212
+ updateMatrixWorld() {
213
+ this.matrixWorld.makeTranslation(this.position);
214
+ }
215
+
216
+ dispose() {
217
+ this.geometry.dispose();
218
+ this.material.dispose();
219
+ }
220
+ }
221
+
222
+ class PivotMaterial extends ShaderMaterial {
223
+ constructor(size, thickness) {
224
+ const coreD = size + thickness;
225
+ const planeD = coreD + 3 * thickness;
226
+ const normThk = thickness / coreD;
227
+ const ringR = (coreD - 0.4 * thickness - 4.0) / coreD;
228
+ const hw = 0.4 * normThk;
229
+
230
+ super({
231
+ depthWrite: false,
232
+ depthTest: false,
233
+ transparent: true,
234
+
235
+ uniforms: {
236
+ resolution: { value: new Vector2() },
237
+ opacity: { value: 1 },
238
+ planeD: { value: planeD },
239
+ hw: { value: hw },
240
+ ringR: { value: ringR },
241
+ shadowW: { value: hw * 5.0 },
242
+ uvScale: { value: planeD / coreD },
243
+ },
244
+
245
+ vertexShader: `
246
+ uniform float planeD;
247
+ uniform vec2 resolution;
248
+ varying vec2 vUv;
249
+
250
+ void main() {
251
+ vUv = uv;
252
+ float aspect = resolution.x / resolution.y;
253
+ vec2 offset = uv * 2.0 - vec2(1.0);
254
+ offset.y *= aspect;
255
+ vec4 screenPoint = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
256
+ screenPoint.xy += offset * planeD * screenPoint.w / resolution.x;
257
+ gl_Position = screenPoint;
258
+ }
259
+ `,
260
+ fragmentShader: `
261
+ uniform float hw;
262
+ uniform float ringR;
263
+ uniform float shadowW;
264
+ uniform float opacity;
265
+ uniform float uvScale;
266
+ varying vec2 vUv;
267
+
268
+ void main() {
269
+ vec2 uv = (vUv * 2.0 - 1.0) * uvScale;
270
+ float len = length(uv);
271
+ float fw = fwidth(len) * 0.5;
272
+ float d = abs(len - ringR);
273
+
274
+ float ring = 1.0 - smoothstep(hw - fw, hw + fw, d);
275
+
276
+ float shadow = (1.0 - smoothstep(hw, shadowW, d)) * (1.0 - smoothstep(ringR - fw, ringR + fw, len)) * 0.5;
277
+
278
+ float white = ring;
279
+ float black = shadow * (1.0 - white);
280
+ float alpha = (white + black) * opacity;
281
+ if (alpha < 0.001) discard;
282
+ gl_FragColor = vec4(vec3(white / max(alpha / opacity, 0.001)), alpha);
283
+ }
284
+ `,
285
+ });
286
+ }
287
+ }
288
+
194
289
  const _matrix = new Matrix4();
195
290
  // custom version of set raycaster from camera that relies on the underlying matrices
196
291
  // so the ray origin is position at the camera near clip.
@@ -240,6 +335,8 @@ const UPDATE_EVENT = { type: 'update' };
240
335
  const FINISH_EVENT = { type: 'finish' };
241
336
  const THRESHOLD = 1e-3;
242
337
  const MAX = 1e8;
338
+ const PIVOT_SIZE = 22;
339
+ const PIVOT_THICKNESS = 2.5;
243
340
  const VIRTUAL_HIT_DISTANCE = 50;
244
341
  const ZOOM_OUT_TRANSITION_COS_THRESHOLD = Math.cos((105 * Math.PI) / 180);
245
342
  const ZOOM_OUT_TRANSITION_COS_MAX_THRESHOLD = Math.cos((95 * Math.PI) / 180);
@@ -281,6 +378,7 @@ class CameraController extends EventDispatcher {
281
378
  #camera;
282
379
  #scene;
283
380
  #raycaster;
381
+ #pivotMesh;
284
382
  #zoomDelta;
285
383
  #zoomInertia;
286
384
  #rotateInertia;
@@ -330,6 +428,8 @@ class CameraController extends EventDispatcher {
330
428
  this.#pointerTracker = new PointerTracker();
331
429
  this.#raycaster = new Raycaster();
332
430
  this.#raycaster.params.Points.threshold = 0.1;
431
+ this.#pivotMesh = new PivotPointMesh(PIVOT_SIZE, PIVOT_THICKNESS);
432
+ this.#pivotMesh.visible = false;
333
433
  this.#zoomDelta = 0;
334
434
  this.#zoomInertia = 0;
335
435
  this.#rotateInertia = new Vector2();
@@ -368,13 +468,22 @@ class CameraController extends EventDispatcher {
368
468
  get camera() {
369
469
  return this.#camera;
370
470
  }
471
+ get indicator() {
472
+ return this.#pivotMesh;
473
+ }
371
474
  #setState(state = this.state) {
372
475
  if (this.state === state) {
373
476
  return;
374
477
  }
375
478
  this.state = state;
479
+ if (state !== NONE) {
480
+ this.#pivotMesh.visible = true;
481
+ }
376
482
  }
377
483
  #setZooming(zooming, touchZooming = false) {
484
+ if (!this.zooming && this.state === NONE && zooming) {
485
+ this.#pivotMesh.visible = true;
486
+ }
378
487
  this.zooming = zooming;
379
488
  this.touchZooming = touchZooming;
380
489
  }
@@ -391,6 +500,7 @@ class CameraController extends EventDispatcher {
391
500
  this.#dragPlaneNormal.set(0, 0, 0);
392
501
  this.#zoomInertia = 0;
393
502
  this.#hit = null;
503
+ this.#pivotMesh.visible = false;
394
504
  }
395
505
  setCamera(camera) {
396
506
  this.#camera = camera;
@@ -405,6 +515,8 @@ class CameraController extends EventDispatcher {
405
515
  }
406
516
  init() {
407
517
  this.#domElement.style.touchAction = 'none';
518
+ this.#pivotMesh.raycast = () => {};
519
+ this.#scene.add(this.#pivotMesh);
408
520
  this.#contextMenuEvent = this.#contextMenu.bind(this);
409
521
  this.#pointerDownEvent = this.#pointerDown.bind(this);
410
522
  this.#pointerMoveEvent = this.#pointerMove.bind(this);
@@ -600,6 +712,8 @@ class CameraController extends EventDispatcher {
600
712
  'pointerenter',
601
713
  this.#pointerEnterEvent,
602
714
  );
715
+ this.#pivotMesh.removeFromParent();
716
+ this.#pivotMesh.dispose();
603
717
  this.#domElement.style.touchAction = '';
604
718
  this.#enabled = false;
605
719
  this.#ellipsoid = null;
@@ -615,6 +729,15 @@ class CameraController extends EventDispatcher {
615
729
  #contextMenu = (e) => {
616
730
  e.preventDefault();
617
731
  };
732
+ #updateIndicatorFromHit() {
733
+ if (this.#hit && this.#hit.distance > 0) {
734
+ this.#pivotMesh.visible = true;
735
+ this.#pivotMesh.position.copy(this.#hit.point);
736
+ this.#pivotMesh.focus = !this.#hit.onGlobe;
737
+ } else {
738
+ this.#pivotMesh.visible = false;
739
+ }
740
+ }
618
741
  #pointerDown = (e) => {
619
742
  if (!this.#enabled) {
620
743
  return;
@@ -653,6 +776,7 @@ class CameraController extends EventDispatcher {
653
776
  mouseToCoords(_pointer1.x, _pointer1.y, this.#domElement, _pointer1);
654
777
  setRaycasterFromCamera(this.#raycaster, _pointer1, this.#camera);
655
778
  this.#hit = this.#raycast(this.#raycaster);
779
+ this.#updateIndicatorFromHit();
656
780
  if (this.state === DRAG && this.#hit.distance > 0) {
657
781
  this.#initializeDragAnchor();
658
782
  }
@@ -689,9 +813,9 @@ class CameraController extends EventDispatcher {
689
813
  if (!this.#enabled) {
690
814
  return;
691
815
  }
692
- const tooClose = this.#hit
693
- ? this.#hit.point.distanceTo(this.#camera.position) <= this.#camera.near
694
- : false;
816
+ const tooClose =
817
+ this.#pivotMesh.position.distanceTo(this.#camera.position) <=
818
+ this.#camera.near;
695
819
  if (!this.zooming || tooClose) {
696
820
  this.#rotateInertia.set(0, 0);
697
821
  this.#dragInertia.set(0, 0, 0);
@@ -705,6 +829,7 @@ class CameraController extends EventDispatcher {
705
829
  setRaycasterFromCamera(this.#raycaster, _pointer1, this.#camera);
706
830
  if ((!this.zooming && this.state === NONE) || tooClose) {
707
831
  this.#hit = this.#raycast(this.#raycaster);
832
+ this.#updateIndicatorFromHit();
708
833
  }
709
834
  let delta = 0;
710
835
  switch (e.deltaMode) {
@@ -1056,6 +1181,7 @@ class CameraController extends EventDispatcher {
1056
1181
  .copy(this.#camera.position)
1057
1182
  .addScaledVector(_forward, VIRTUAL_HIT_DISTANCE);
1058
1183
  hit.distance = VIRTUAL_HIT_DISTANCE;
1184
+ this.#pivotMesh.position.copy(hit.point);
1059
1185
  }
1060
1186
  let zoomFactor = Math.exp(-zoomAmount * 0.001);
1061
1187
  if (this.minDistance > 0 && zoomFactor < 1) {
@@ -900,6 +900,24 @@ function buildViewerHtml(viewerConfig) {
900
900
  pointer-events: none;
901
901
  }
902
902
 
903
+ .tile-runtime-stats {
904
+ position: fixed;
905
+ right: 16px;
906
+ bottom: 16px;
907
+ display: flex;
908
+ flex-wrap: nowrap;
909
+ justify-content: flex-end;
910
+ gap: 14px;
911
+ box-sizing: border-box;
912
+ max-width: calc(100vw - 32px);
913
+ padding: 4px 8px;
914
+ border-radius: 6px;
915
+ background: rgba(255, 255, 255, 0.15);
916
+ backdrop-filter: blur(6px);
917
+ z-index: 12;
918
+ pointer-events: none;
919
+ }
920
+
903
921
  .runtime-stat {
904
922
  display: grid;
905
923
  gap: 4px;
@@ -912,6 +930,19 @@ function buildViewerHtml(viewerConfig) {
912
930
  backdrop-filter: blur(14px);
913
931
  }
914
932
 
933
+ .tile-runtime-stats .runtime-stat {
934
+ display: inline-flex;
935
+ align-items: center;
936
+ gap: 0;
937
+ min-width: 0;
938
+ padding: 0;
939
+ border: 0;
940
+ border-radius: 0;
941
+ background: transparent;
942
+ box-shadow: none;
943
+ backdrop-filter: none;
944
+ }
945
+
915
946
  .runtime-stat-label {
916
947
  margin: 0;
917
948
  font-size: 10px;
@@ -921,6 +952,18 @@ function buildViewerHtml(viewerConfig) {
921
952
  color: #5d738b;
922
953
  }
923
954
 
955
+ .tile-runtime-stats .runtime-stat-label,
956
+ .tile-runtime-stats .runtime-stat-value {
957
+ display: inline-flex;
958
+ align-items: center;
959
+ font-size: 12px;
960
+ font-weight: 400;
961
+ letter-spacing: 0;
962
+ line-height: 14px;
963
+ text-transform: none;
964
+ color: #24292f;
965
+ }
966
+
924
967
  .runtime-stat-value {
925
968
  margin: 0;
926
969
  font-size: 15px;
@@ -929,6 +972,10 @@ function buildViewerHtml(viewerConfig) {
929
972
  color: #16324f;
930
973
  }
931
974
 
975
+ .tile-runtime-stats .runtime-stat-value {
976
+ font-variant-numeric: tabular-nums;
977
+ }
978
+
932
979
  canvas {
933
980
  display: block;
934
981
  }
@@ -1216,6 +1263,16 @@ function buildViewerHtml(viewerConfig) {
1216
1263
  top: 16px;
1217
1264
  right: 16px;
1218
1265
  left: 16px;
1266
+ flex-wrap: wrap;
1267
+ justify-content: stretch;
1268
+ max-width: none;
1269
+ }
1270
+
1271
+ .tile-runtime-stats {
1272
+ right: 16px;
1273
+ bottom: 16px;
1274
+ left: 16px;
1275
+ flex-wrap: wrap;
1219
1276
  justify-content: stretch;
1220
1277
  max-width: none;
1221
1278
  }
@@ -1225,6 +1282,11 @@ function buildViewerHtml(viewerConfig) {
1225
1282
  min-width: 0;
1226
1283
  }
1227
1284
 
1285
+ .tile-runtime-stats .runtime-stat {
1286
+ flex: 0 0 auto;
1287
+ min-width: 0;
1288
+ }
1289
+
1228
1290
  .toolbar-dock {
1229
1291
  top: auto;
1230
1292
  bottom: 16px;
@@ -1257,6 +1319,24 @@ function buildViewerHtml(viewerConfig) {
1257
1319
  <p id="splats-count-value" class="runtime-stat-value">0</p>
1258
1320
  </div>
1259
1321
  </div>
1322
+ <div class="tile-runtime-stats" aria-live="polite">
1323
+ <div class="runtime-stat">
1324
+ <p class="runtime-stat-label">Downloading:&nbsp;</p>
1325
+ <p id="tiles-downloading-value" class="runtime-stat-value">0</p>
1326
+ </div>
1327
+ <div class="runtime-stat">
1328
+ <p class="runtime-stat-label">Parsing:&nbsp;</p>
1329
+ <p id="tiles-parsing-value" class="runtime-stat-value">0</p>
1330
+ </div>
1331
+ <div class="runtime-stat">
1332
+ <p class="runtime-stat-label">Loaded:&nbsp;</p>
1333
+ <p id="tiles-loaded-value" class="runtime-stat-value">0</p>
1334
+ </div>
1335
+ <div class="runtime-stat">
1336
+ <p class="runtime-stat-label">Visible:&nbsp;</p>
1337
+ <p id="tiles-visible-value" class="runtime-stat-value">0</p>
1338
+ </div>
1339
+ </div>
1260
1340
  <div class="toolbar-dock expanded">
1261
1341
  <button
1262
1342
  id="toolbar-toggle"
@@ -1269,16 +1349,6 @@ function buildViewerHtml(viewerConfig) {
1269
1349
  Hide Sidebar
1270
1350
  </button>
1271
1351
  <div id="toolbar" class="toolbar">
1272
- <div class="toolbar-section">
1273
- <div class="toolbar-section-header">
1274
- <p class="toolbar-section-title">Transform</p>
1275
- </div>
1276
- <div class="transform-actions">
1277
- <button id="translate" type="button">Translate</button>
1278
- <button id="rotate" type="button">Rotate</button>
1279
- <button id="set-position" class="full-span" type="button">Set Position</button>
1280
- </div>
1281
- </div>
1282
1352
  <div class="toolbar-section">
1283
1353
  <div class="toolbar-section-header">
1284
1354
  <p class="toolbar-section-title">Canvas</p>
@@ -1289,6 +1359,16 @@ function buildViewerHtml(viewerConfig) {
1289
1359
  <button id="move-to-tiles" type="button">Move To Tiles</button>
1290
1360
  </div>
1291
1361
  </div>
1362
+ <div class="toolbar-section">
1363
+ <div class="toolbar-section-header">
1364
+ <p class="toolbar-section-title">Transform</p>
1365
+ </div>
1366
+ <div class="transform-actions">
1367
+ <button id="translate" type="button">Translate</button>
1368
+ <button id="rotate" type="button">Rotate</button>
1369
+ <button id="set-position" class="full-span" type="button">Set Position</button>
1370
+ </div>
1371
+ </div>
1292
1372
  <div class="toolbar-section">
1293
1373
  <div class="toolbar-section-header">
1294
1374
  <p class="toolbar-section-title">Coordinate</p>
@@ -1299,8 +1379,8 @@ function buildViewerHtml(viewerConfig) {
1299
1379
  <label><span>Height</span><input id="height" type="number" step="any" value="0" /></label>
1300
1380
  </div>
1301
1381
  <div class="coordinate-actions">
1302
- <button id="move-tiles-to-coordinate" class="wide" type="button">Move Tiles</button>
1303
1382
  <button id="move-camera-to-coordinate" class="wide" type="button">Move Camera</button>
1383
+ <button id="move-tiles-to-coordinate" class="wide" type="button">Move Tiles</button>
1304
1384
  </div>
1305
1385
  </div>
1306
1386
  <div class="toolbar-section">