3dtiles-inspector 0.2.6 → 0.2.7

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.2.7] - 2026-05-10
10
+
11
+ ### Changed
12
+
13
+ - Changed crop saves to remove local orphaned Gaussian Splat `.glb` / `.gltf` resources and private external buffers after fully cropped content is pruned from the tileset.
14
+ - Updated `3d-tiles-rendererjs-3dgs-plugin` from `0.1.5` to `0.1.7`.
15
+
16
+ ### Fixed
17
+
18
+ - Fixed cropped Gaussian Splat resources keeping stale glTF accessor counts after SPZ data is rewritten, which could make Cesium fail while generating splat textures.
19
+
9
20
  ## [0.2.6] - 2026-05-05
10
21
 
11
22
  ### Added
@@ -43855,6 +43855,12 @@ async function buildGaussianMeshSource(descriptor, abortSignal) {
43855
43855
  }
43856
43856
  return { extSplats };
43857
43857
  }
43858
+ function isXrPresenting(renderer) {
43859
+ return renderer.xr.isPresenting;
43860
+ }
43861
+ function getUpdateSourceCamera(renderer, camera, xrPresenting = isXrPresenting(renderer)) {
43862
+ return xrPresenting ? renderer.xr.getCamera() : camera;
43863
+ }
43858
43864
  function ensureCameraClone(cached, source) {
43859
43865
  if (!cached || cached.constructor !== source.constructor) {
43860
43866
  return source.clone();
@@ -43862,111 +43868,11 @@ function ensureCameraClone(cached, source) {
43862
43868
  cached.copy(source, false);
43863
43869
  return cached;
43864
43870
  }
43865
- function hasGaussianSplatAncestor(node) {
43866
- let ancestor = node.parent;
43867
- while (ancestor) {
43868
- if (ancestor instanceof SplatMesh || isGaussianSplat(ancestor)) {
43869
- return true;
43870
- }
43871
- ancestor = ancestor.parent;
43872
- }
43873
- return false;
43874
- }
43875
- function isGlobalSplatEdit(node) {
43876
- return node instanceof SplatEdit && !hasGaussianSplatAncestor(node);
43877
- }
43878
- function isCameraRelativeNode(node) {
43879
- return isGaussianSplat(node) || isGlobalSplatEdit(node);
43880
- }
43881
- function hasCameraRelativeRootAncestor(node) {
43882
- let ancestor = node.parent;
43883
- while (ancestor) {
43884
- if (isCameraRelativeNode(ancestor)) {
43885
- return true;
43886
- }
43887
- ancestor = ancestor.parent;
43888
- }
43889
- return false;
43890
- }
43891
- function cloneSplatRootSnapshot(node) {
43892
- return {
43893
- kind: "splat",
43894
- opacity: node.opacity,
43895
- matrixWorld: node.matrixWorld.clone()
43896
- };
43897
- }
43898
- function cloneSplatEditSdfSnapshot(sdf) {
43899
- return {
43900
- uuid: sdf.uuid,
43901
- matrixWorld: sdf.matrixWorld.clone(),
43902
- type: sdf.type,
43903
- invert: sdf.invert,
43904
- opacity: sdf.opacity,
43905
- color: sdf.color.clone(),
43906
- radius: sdf.radius,
43907
- displace: sdf.displace.clone(),
43908
- scale: sdf.scale.clone()
43909
- };
43910
- }
43911
- function cloneSplatEditRootSnapshot(edit) {
43912
- edit.updateMatrixWorld(true);
43913
- const sdfs = [];
43914
- const sourceSdfs = edit.sdfs;
43915
- if (sourceSdfs != null) {
43916
- for (const sdf of sourceSdfs) {
43917
- sdf.updateMatrixWorld(true);
43918
- sdfs.push(cloneSplatEditSdfSnapshot(sdf));
43919
- }
43920
- } else {
43921
- edit.traverseVisible((child) => {
43922
- if (child instanceof SplatEditSdf) {
43923
- child.updateMatrixWorld(true);
43924
- sdfs.push(cloneSplatEditSdfSnapshot(child));
43925
- }
43926
- });
43927
- }
43928
- return {
43929
- kind: "edit",
43930
- matrixWorld: edit.matrixWorld.clone(),
43931
- ordering: edit.ordering,
43932
- rgbaBlendMode: edit.rgbaBlendMode,
43933
- sdfSmooth: edit.sdfSmooth,
43934
- softEdge: edit.softEdge,
43935
- invert: edit.invert,
43936
- sdfs
43937
- };
43938
- }
43939
- function cloneCameraRelativeRootSnapshot(node) {
43940
- return node instanceof SplatEdit ? cloneSplatEditRootSnapshot(node) : cloneSplatRootSnapshot(node);
43941
- }
43942
- function areSplatRootStatesEqual(a2, b5) {
43943
- return a2.opacity === b5.opacity && a2.matrixWorld.equals(b5.matrixWorld);
43944
- }
43945
- function areSplatEditSdfStatesEqual(a2, b5) {
43946
- return a2.uuid === b5.uuid && a2.type === b5.type && a2.invert === b5.invert && a2.opacity === b5.opacity && a2.radius === b5.radius && a2.matrixWorld.equals(b5.matrixWorld) && a2.color.equals(b5.color) && a2.displace.equals(b5.displace) && a2.scale.equals(b5.scale);
43871
+ function isGaussianSplatNode(node) {
43872
+ return node instanceof SplatMesh || isGaussianSplat(node);
43947
43873
  }
43948
- function areSplatEditRootStatesEqual(a2, b5) {
43949
- if (!a2.matrixWorld.equals(b5.matrixWorld) || a2.ordering !== b5.ordering || a2.rgbaBlendMode !== b5.rgbaBlendMode || a2.sdfSmooth !== b5.sdfSmooth || a2.softEdge !== b5.softEdge || a2.invert !== b5.invert || a2.sdfs.length !== b5.sdfs.length) {
43950
- return false;
43951
- }
43952
- for (let i = 0; i < a2.sdfs.length; i++) {
43953
- if (!areSplatEditSdfStatesEqual(a2.sdfs[i], b5.sdfs[i])) {
43954
- return false;
43955
- }
43956
- }
43957
- return true;
43958
- }
43959
- function areCameraRelativeRootStatesEqual(a2, b5) {
43960
- if (a2 === b5) {
43961
- return true;
43962
- }
43963
- if (!a2 || !b5 || a2.kind !== b5.kind) {
43964
- return false;
43965
- }
43966
- if (a2.kind === "splat") {
43967
- return b5.kind === "splat" && areSplatRootStatesEqual(a2, b5);
43968
- }
43969
- return b5.kind === "edit" && areSplatEditRootStatesEqual(a2, b5);
43874
+ function isCameraRelativeEdit(node, hasGaussianSplatAncestor) {
43875
+ return node instanceof SplatEdit && !hasGaussianSplatAncestor;
43970
43876
  }
43971
43877
  function normalizeSparkRendererOptions(host, includeCustomDefaults = true) {
43972
43878
  const source = host.sparkRendererOptions ?? {};
@@ -44154,7 +44060,7 @@ function isGaussianSplat(object) {
44154
44060
  function isGaussianSplatScene(scene) {
44155
44061
  return Boolean(scene?.userData?.gaussianSplatScene);
44156
44062
  }
44157
- var _translation, _rotation, _scale, _identityMatrix2, _tempNodeMatrix, _textDecoder, _identityMatrix22, _cameraInverseWorldMatrix, _parentInverseWorldMatrix, _rebasedLocalMatrix, _displayFrameInverseWorldMatrix, _relativeRenderCameraMatrix, _cameraWorldPosition, _cameraWorldDirection, _cameraPositionEpsilonSq, _cameraDirectionDotThreshold, _updateCamera, _renderCamera, _lastCameraPosition, _lastCameraDirection, _hasLastCameraPose, _lastRootStates, _currentRootStates, _rebasedRootsPool, _rebasedRootsCount, _hadRebasedLastFrame, _CameraRelativeSparkRenderer_instances, shouldUpdate_fn, getUpdateCamera_fn, getRenderCamera_fn, rebaseCameraRelativeRoots_fn, restoreCameraRelativeRoots_fn, _a2, CameraRelativeSparkRenderer, _sharedSparkManagersByScene, _sharedSparkManagersByRenderer, CUSTOM_DEFAULT_OPTIONS, _sparkRenderer, _scene, _sparkRendererOptions, _notifyHandle, _disposeHandle, _tilesRenderers, _disposed, _SharedSparkRendererManager_instances, dispose_fn, stopScheduledNotifications_fn, scheduleSortUpdatedNotification_fn, waitForSortAndDispose_fn, _a3, SharedSparkRendererManager, SPARK_RENDERER_OPTION_KEYS, MAX_GAUSSIAN_MESH_INIT_CONCURRENCY, _sceneMatrix, _gaussianFadeValueWatched, _host, _sparkManager, _GaussianSplatPlugin_instances, disposeSplatScene_fn, getSplatMeshes_fn, createMeshForDescriptor_fn, _a4, GaussianSplatPlugin;
44063
+ var _translation, _rotation, _scale, _identityMatrix2, _tempNodeMatrix, _textDecoder, _identityMatrix22, _cameraInverseWorldMatrix, _parentInverseWorldMatrix, _rebasedLocalMatrix, _displayFrameInverseWorldMatrix, _relativeRenderCameraMatrix, _updateCamera, _renderCamera, _cameraWorldSnapshot, _lastXrHandledFrame, _rebasedRootsPool, _rebasedRootsCount, _hadRebasedLastFrame, _CameraRelativeSparkRenderer_instances, updateSparkIfNeeded_fn, getUpdateCamera_fn, getRenderCamera_fn, rebaseCameraRelativeRoots_fn, visitVisibleCameraRelativeRoots_fn, rebaseCameraRelativeRoot_fn, restoreCameraRelativeRoots_fn, _a2, CameraRelativeSparkRenderer, _sharedSparkManagersByScene, _sharedSparkManagersByRenderer, CUSTOM_DEFAULT_OPTIONS, _sparkRenderer, _scene, _sparkRendererOptions, _notifyHandle, _disposeHandle, _tilesRenderers, _disposed, _SharedSparkRendererManager_instances, dispose_fn, stopScheduledNotifications_fn, scheduleSortUpdatedNotification_fn, waitForSortAndDispose_fn, _a3, SharedSparkRendererManager, SPARK_RENDERER_OPTION_KEYS, MAX_GAUSSIAN_MESH_INIT_CONCURRENCY, _sceneMatrix, _gaussianFadeValueWatched, _host, _sparkManager, _GaussianSplatPlugin_instances, disposeSplatScene_fn, getSplatMeshes_fn, createMeshForDescriptor_fn, _a4, GaussianSplatPlugin;
44158
44064
  var init_dist = __esm({
44159
44065
  "node_modules/3d-tiles-rendererjs-3dgs-plugin/dist/index.js"() {
44160
44066
  init_spark_module();
@@ -44175,10 +44081,6 @@ var init_dist = __esm({
44175
44081
  _rebasedLocalMatrix = new Matrix4();
44176
44082
  _displayFrameInverseWorldMatrix = new Matrix4();
44177
44083
  _relativeRenderCameraMatrix = new Matrix4();
44178
- _cameraWorldPosition = new Vector3();
44179
- _cameraWorldDirection = new Vector3();
44180
- _cameraPositionEpsilonSq = 1e-6;
44181
- _cameraDirectionDotThreshold = 1 - 1e-3;
44182
44084
  CameraRelativeSparkRenderer = (_a2 = class extends SparkRenderer {
44183
44085
  constructor(renderer, options = {}) {
44184
44086
  super({
@@ -44190,11 +44092,8 @@ var init_dist = __esm({
44190
44092
  __privateAdd(this, _CameraRelativeSparkRenderer_instances);
44191
44093
  __privateAdd(this, _updateCamera, null);
44192
44094
  __privateAdd(this, _renderCamera, null);
44193
- __privateAdd(this, _lastCameraPosition, new Vector3());
44194
- __privateAdd(this, _lastCameraDirection, new Vector3());
44195
- __privateAdd(this, _hasLastCameraPose, false);
44196
- __privateAdd(this, _lastRootStates, /* @__PURE__ */ new Map());
44197
- __privateAdd(this, _currentRootStates, /* @__PURE__ */ new Map());
44095
+ __privateAdd(this, _cameraWorldSnapshot, new Matrix4());
44096
+ __privateAdd(this, _lastXrHandledFrame, -1);
44198
44097
  __privateAdd(this, _rebasedRootsPool, []);
44199
44098
  __privateAdd(this, _rebasedRootsCount, 0);
44200
44099
  __privateAdd(this, _hadRebasedLastFrame, false);
@@ -44203,56 +44102,64 @@ var init_dist = __esm({
44203
44102
  };
44204
44103
  }
44205
44104
  onBeforeRender(renderer, scene, camera) {
44206
- camera.updateMatrixWorld(true);
44207
- const rebasedCount = __privateMethod(this, _CameraRelativeSparkRenderer_instances, rebaseCameraRelativeRoots_fn).call(this, scene, camera);
44105
+ const xrPresenting = isXrPresenting(renderer);
44106
+ const updateSourceCamera = getUpdateSourceCamera(
44107
+ renderer,
44108
+ camera,
44109
+ xrPresenting
44110
+ );
44111
+ if (!xrPresenting) {
44112
+ camera.updateMatrixWorld(true);
44113
+ }
44114
+ const rebasedCount = __privateMethod(this, _CameraRelativeSparkRenderer_instances, rebaseCameraRelativeRoots_fn).call(this, scene, updateSourceCamera);
44208
44115
  const hasRebased = rebasedCount > 0;
44116
+ const renderFrame = renderer.info.render.frame;
44117
+ const shouldHandleFrameState = !xrPresenting || __privateGet(this, _lastXrHandledFrame) !== renderFrame;
44209
44118
  try {
44210
- if ((hasRebased || __privateGet(this, _hadRebasedLastFrame)) && __privateMethod(this, _CameraRelativeSparkRenderer_instances, shouldUpdate_fn).call(this, camera)) {
44211
- const updateCamera = __privateMethod(this, _CameraRelativeSparkRenderer_instances, getUpdateCamera_fn).call(this, camera);
44119
+ if (shouldHandleFrameState) {
44120
+ __privateSet(this, _lastXrHandledFrame, renderFrame);
44121
+ }
44122
+ if ((hasRebased || __privateGet(this, _hadRebasedLastFrame)) && shouldHandleFrameState) {
44123
+ const updateCamera = __privateMethod(this, _CameraRelativeSparkRenderer_instances, getUpdateCamera_fn).call(this, updateSourceCamera);
44212
44124
  const prevDisplay = this.display;
44213
44125
  const prevCurrent = this.current;
44214
- const cameraWorldSnapshot = camera.matrixWorld.clone();
44215
- void this.update({
44126
+ const cameraWorldSnapshot = __privateGet(this, _cameraWorldSnapshot).copy(
44127
+ updateSourceCamera.matrixWorld
44128
+ );
44129
+ void __privateMethod(this, _CameraRelativeSparkRenderer_instances, updateSparkIfNeeded_fn).call(this, {
44216
44130
  scene,
44217
44131
  camera: updateCamera
44132
+ }).catch((error) => {
44133
+ console.error(
44134
+ "CameraRelativeSparkRenderer: Spark update failed",
44135
+ error
44136
+ );
44218
44137
  });
44219
- const updateAccepted = this.current !== prevCurrent || this.display !== prevDisplay;
44220
44138
  if (this.current !== prevCurrent) {
44221
44139
  this.current.viewToWorld.copy(cameraWorldSnapshot);
44222
44140
  }
44223
44141
  if (this.display !== prevDisplay) {
44224
44142
  this.display.viewToWorld.copy(cameraWorldSnapshot);
44225
44143
  }
44226
- if (updateAccepted) {
44227
- __privateGet(this, _lastCameraPosition).copy(_cameraWorldPosition);
44228
- __privateGet(this, _lastCameraDirection).copy(_cameraWorldDirection);
44229
- __privateSet(this, _hasLastCameraPose, true);
44230
- __privateSet(this, _lastRootStates, new Map(__privateGet(this, _currentRootStates)));
44231
- }
44232
44144
  }
44233
- __privateSet(this, _hadRebasedLastFrame, hasRebased);
44145
+ if (shouldHandleFrameState) {
44146
+ __privateSet(this, _hadRebasedLastFrame, hasRebased);
44147
+ }
44234
44148
  const renderCamera = hasRebased ? __privateMethod(this, _CameraRelativeSparkRenderer_instances, getRenderCamera_fn).call(this, camera) : camera;
44235
44149
  super.onBeforeRender(renderer, scene, renderCamera);
44236
44150
  } finally {
44237
44151
  __privateMethod(this, _CameraRelativeSparkRenderer_instances, restoreCameraRelativeRoots_fn).call(this);
44238
44152
  }
44239
44153
  }
44240
- }, _updateCamera = new WeakMap(), _renderCamera = new WeakMap(), _lastCameraPosition = new WeakMap(), _lastCameraDirection = new WeakMap(), _hasLastCameraPose = new WeakMap(), _lastRootStates = new WeakMap(), _currentRootStates = new WeakMap(), _rebasedRootsPool = new WeakMap(), _rebasedRootsCount = new WeakMap(), _hadRebasedLastFrame = new WeakMap(), _CameraRelativeSparkRenderer_instances = new WeakSet(), shouldUpdate_fn = function(camera) {
44241
- camera.getWorldPosition(_cameraWorldPosition);
44242
- camera.getWorldDirection(_cameraWorldDirection);
44243
- const poseChanged = !__privateGet(this, _hasLastCameraPose) || _cameraWorldPosition.distanceToSquared(__privateGet(this, _lastCameraPosition)) > _cameraPositionEpsilonSq || _cameraWorldDirection.dot(__privateGet(this, _lastCameraDirection)) < _cameraDirectionDotThreshold;
44244
- const current = __privateGet(this, _currentRootStates);
44245
- const last = __privateGet(this, _lastRootStates);
44246
- let rootsChanged = current.size !== last.size;
44247
- if (!rootsChanged) {
44248
- for (const [uuid, state] of current) {
44249
- if (!areCameraRelativeRootStatesEqual(state, last.get(uuid))) {
44250
- rootsChanged = true;
44251
- break;
44252
- }
44253
- }
44254
- }
44255
- return poseChanged || rootsChanged;
44154
+ }, _updateCamera = new WeakMap(), _renderCamera = new WeakMap(), _cameraWorldSnapshot = new WeakMap(), _lastXrHandledFrame = new WeakMap(), _rebasedRootsPool = new WeakMap(), _rebasedRootsCount = new WeakMap(), _hadRebasedLastFrame = new WeakMap(), _CameraRelativeSparkRenderer_instances = new WeakSet(), updateSparkIfNeeded_fn = function({
44155
+ scene,
44156
+ camera
44157
+ }) {
44158
+ return this.updateInternal({
44159
+ scene,
44160
+ camera,
44161
+ autoUpdate: true
44162
+ });
44256
44163
  }, /**
44257
44164
  * Identity camera for the update pass - makes Spark treat
44258
44165
  * the camera's own frame as the reference frame.
@@ -44291,55 +44198,63 @@ var init_dist = __esm({
44291
44198
  return renderCamera;
44292
44199
  }, rebaseCameraRelativeRoots_fn = function(scene, camera) {
44293
44200
  __privateSet(this, _rebasedRootsCount, 0);
44294
- __privateGet(this, _currentRootStates).clear();
44295
44201
  _cameraInverseWorldMatrix.copy(camera.matrixWorld).invert();
44296
- scene.traverseVisible((node) => {
44297
- if (!isCameraRelativeNode(node)) {
44298
- return;
44299
- }
44300
- __privateGet(this, _currentRootStates).set(
44301
- node.uuid,
44302
- cloneCameraRelativeRootSnapshot(node)
44303
- );
44304
- if (hasCameraRelativeRootAncestor(node)) {
44305
- return;
44306
- }
44307
- const idx = __privateWrapper(this, _rebasedRootsCount)._++;
44308
- const pool = __privateGet(this, _rebasedRootsPool);
44309
- if (idx >= pool.length) {
44310
- pool.push({
44311
- target: node,
44312
- originalMatrix: node.matrix.clone(),
44313
- originalMatrixAutoUpdate: node.matrixAutoUpdate
44314
- });
44315
- } else {
44316
- const entry = pool[idx];
44317
- entry.target = node;
44318
- entry.originalMatrix.copy(node.matrix);
44319
- entry.originalMatrixAutoUpdate = node.matrixAutoUpdate;
44320
- }
44321
- const parent2 = node.parent;
44322
- if (!parent2 || parent2 === scene) {
44323
- _rebasedLocalMatrix.copy(_cameraInverseWorldMatrix).multiply(node.matrixWorld);
44324
- } else {
44325
- _rebasedLocalMatrix.copy(_parentInverseWorldMatrix.copy(parent2.matrixWorld).invert()).multiply(_cameraInverseWorldMatrix).multiply(node.matrixWorld);
44326
- }
44327
- node.matrixAutoUpdate = false;
44328
- node.matrix.copy(_rebasedLocalMatrix);
44329
- node.matrixWorldNeedsUpdate = true;
44330
- node.updateMatrixWorld(true);
44331
- });
44202
+ __privateMethod(this, _CameraRelativeSparkRenderer_instances, visitVisibleCameraRelativeRoots_fn).call(this, scene, scene, false, false);
44332
44203
  return __privateGet(this, _rebasedRootsCount);
44204
+ }, visitVisibleCameraRelativeRoots_fn = function(node, scene, hasGaussianSplatAncestor, hasCameraRelativeAncestor) {
44205
+ if (!node.visible) {
44206
+ return;
44207
+ }
44208
+ const isSplatNode = isGaussianSplatNode(node);
44209
+ const isCameraRelativeNode = isSplatNode || isCameraRelativeEdit(node, hasGaussianSplatAncestor);
44210
+ if (isCameraRelativeNode && !hasCameraRelativeAncestor) {
44211
+ __privateMethod(this, _CameraRelativeSparkRenderer_instances, rebaseCameraRelativeRoot_fn).call(this, node);
44212
+ }
44213
+ const nextHasGaussianSplatAncestor = hasGaussianSplatAncestor || isSplatNode;
44214
+ const nextHasCameraRelativeAncestor = hasCameraRelativeAncestor || isCameraRelativeNode;
44215
+ const { children } = node;
44216
+ for (let i = 0, l = children.length; i < l; i++) {
44217
+ __privateMethod(this, _CameraRelativeSparkRenderer_instances, visitVisibleCameraRelativeRoots_fn).call(this, children[i], scene, nextHasGaussianSplatAncestor, nextHasCameraRelativeAncestor);
44218
+ }
44219
+ }, rebaseCameraRelativeRoot_fn = function(node) {
44220
+ const idx = __privateWrapper(this, _rebasedRootsCount)._++;
44221
+ const pool = __privateGet(this, _rebasedRootsPool);
44222
+ if (idx >= pool.length) {
44223
+ pool.push({
44224
+ target: node,
44225
+ originalMatrix: node.matrix.clone(),
44226
+ originalMatrixAutoUpdate: node.matrixAutoUpdate
44227
+ });
44228
+ } else {
44229
+ const entry = pool[idx];
44230
+ entry.target = node;
44231
+ entry.originalMatrix.copy(node.matrix);
44232
+ entry.originalMatrixAutoUpdate = node.matrixAutoUpdate;
44233
+ }
44234
+ const parent2 = node.parent;
44235
+ if (!parent2) {
44236
+ _rebasedLocalMatrix.copy(_cameraInverseWorldMatrix).multiply(node.matrixWorld);
44237
+ } else {
44238
+ _rebasedLocalMatrix.copy(_parentInverseWorldMatrix.copy(parent2.matrixWorld).invert()).multiply(_cameraInverseWorldMatrix).multiply(node.matrixWorld);
44239
+ }
44240
+ node.matrixAutoUpdate = false;
44241
+ node.matrix.copy(_rebasedLocalMatrix);
44242
+ node.matrixWorldNeedsUpdate = true;
44243
+ node.updateMatrixWorld(true);
44333
44244
  }, restoreCameraRelativeRoots_fn = function() {
44334
44245
  const pool = __privateGet(this, _rebasedRootsPool);
44335
44246
  for (let i = __privateGet(this, _rebasedRootsCount) - 1; i >= 0; i--) {
44336
44247
  const { target, originalMatrix, originalMatrixAutoUpdate } = pool[i];
44248
+ if (!target) continue;
44337
44249
  target.matrix.copy(originalMatrix);
44338
44250
  target.matrixAutoUpdate = originalMatrixAutoUpdate;
44339
44251
  target.matrixWorldNeedsUpdate = true;
44340
44252
  }
44341
44253
  for (let i = 0; i < __privateGet(this, _rebasedRootsCount); i++) {
44342
- pool[i].target.updateMatrixWorld(true);
44254
+ pool[i].target?.updateMatrixWorld(true);
44255
+ }
44256
+ for (let i = __privateGet(this, _rebasedRootsCount); i < pool.length; i++) {
44257
+ pool[i].target = null;
44343
44258
  }
44344
44259
  }, _a2);
44345
44260
  _sharedSparkManagersByScene = /* @__PURE__ */ new WeakMap();
@@ -71652,10 +71567,11 @@ var require_app = __commonJS({
71652
71567
  const processedSplatResources = Number(
71653
71568
  payload.processedSplatResources || 0
71654
71569
  );
71570
+ const deletedSplatFiles = Number(payload.deletedSplatFiles || 0);
71655
71571
  cropController.clearAll();
71656
71572
  loadTileset(TILESET_URL, { frameOnLoad: false });
71657
71573
  setStatus(
71658
- `Saved transform and deleted ${deletedSplats} cropped splats from ${processedSplatResources} splat resource${processedSplatResources === 1 ? "" : "s"}. Reloading tileset.`
71574
+ `Saved transform and deleted ${deletedSplats} cropped splats from ${processedSplatResources} splat resource${processedSplatResources === 1 ? "" : "s"}${deletedSplatFiles > 0 ? `, removing ${deletedSplatFiles} orphaned file${deletedSplatFiles === 1 ? "" : "s"}` : ""}. Reloading tileset.`
71659
71575
  );
71660
71576
  } else {
71661
71577
  setStatus(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "3dtiles-inspector",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
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",
@@ -55,7 +55,7 @@
55
55
  },
56
56
  "devDependencies": {
57
57
  "3d-tiles-renderer": "0.4.24",
58
- "3d-tiles-rendererjs-3dgs-plugin": "0.1.5",
58
+ "3d-tiles-rendererjs-3dgs-plugin": "0.1.7",
59
59
  "cesium": "1.140.0",
60
60
  "esbuild": "^0.25.11"
61
61
  },
@@ -471,6 +471,8 @@ async function saveViewerTransform(
471
471
  return {
472
472
  transform: nextRoot,
473
473
  deletedSplats: cropResult.deletedSplats,
474
+ deletedSplatFiles: cropResult.deletedSplatFiles,
475
+ failedSplatFileDeletes: cropResult.failedSplatFileDeletes,
474
476
  processedSplatResources: cropResult.processedSplatResources,
475
477
  };
476
478
  }
@@ -23,6 +23,8 @@ function createSaveTransformResponsePayload(
23
23
  transform: saveResult.transform,
24
24
  geometricErrorLayerScale: normalizedGeometricErrorLayerScale,
25
25
  geometricErrorScale: normalizedGeometricErrorScale,
26
+ deletedSplatFiles: saveResult.deletedSplatFiles,
27
+ failedSplatFileDeletes: saveResult.failedSplatFileDeletes,
26
28
  deletedSplats: saveResult.deletedSplats,
27
29
  processedSplatResources: saveResult.processedSplatResources,
28
30
  };
@@ -239,6 +239,53 @@ function removeMeshPrimitives(resource, descriptors) {
239
239
  return removed;
240
240
  }
241
241
 
242
+ function updateGaussianPrimitiveAccessorCounts(resource, descriptors, count) {
243
+ if (!Number.isInteger(count) || count < 0) {
244
+ throw new InspectorError('Gaussian accessor count must be a non-negative integer.');
245
+ }
246
+
247
+ const updatedAccessors = new Set();
248
+ descriptors.forEach((descriptor) => {
249
+ if (
250
+ !Number.isInteger(descriptor.meshIndex) ||
251
+ !Number.isInteger(descriptor.primitiveIndex)
252
+ ) {
253
+ return;
254
+ }
255
+
256
+ const primitive =
257
+ resource.json.meshes?.[descriptor.meshIndex]?.primitives?.[
258
+ descriptor.primitiveIndex
259
+ ];
260
+ const attributes = primitive?.attributes;
261
+ if (!attributes || typeof attributes !== 'object') {
262
+ return;
263
+ }
264
+
265
+ Object.values(attributes).forEach((accessorIndex) => {
266
+ if (
267
+ !Number.isInteger(accessorIndex) ||
268
+ accessorIndex < 0 ||
269
+ accessorIndex >= (resource.json.accessors?.length || 0)
270
+ ) {
271
+ return;
272
+ }
273
+
274
+ const accessor = resource.json.accessors[accessorIndex];
275
+ if (!accessor || typeof accessor !== 'object') {
276
+ return;
277
+ }
278
+
279
+ if (accessor.count !== count) {
280
+ accessor.count = count;
281
+ updatedAccessors.add(accessorIndex);
282
+ }
283
+ });
284
+ });
285
+
286
+ return updatedAccessors.size;
287
+ }
288
+
242
289
  module.exports = {
243
290
  collectGaussianPrimitiveDescriptors,
244
291
  getRootUpRotationMatrix,
@@ -247,4 +294,5 @@ module.exports = {
247
294
  hasNonGaussianScenePrimitives,
248
295
  hasScenePrimitives,
249
296
  removeMeshPrimitives,
297
+ updateGaussianPrimitiveAccessorCounts,
250
298
  };
@@ -6,6 +6,7 @@ const { assertPathInsideRoot } = require('./gltfResource');
6
6
  const { getRootUpRotationMatrix } = require('./gaussianPrimitives');
7
7
  const {
8
8
  collectCandidateSplatResources,
9
+ deleteOrphanedSplatResources,
9
10
  readTilesetJson,
10
11
  traverseTileset,
11
12
  } = require('./traversal');
@@ -35,6 +36,8 @@ async function deleteSplatsInNormalizedSelections(
35
36
  if (normalizedScreenSelections.length === 0) {
36
37
  return {
37
38
  deletedSplats: 0,
39
+ deletedSplatFiles: 0,
40
+ failedSplatFileDeletes: 0,
38
41
  processedSplatResources: 0,
39
42
  };
40
43
  }
@@ -45,11 +48,12 @@ async function deleteSplatsInNormalizedSelections(
45
48
 
46
49
  const { THREE } = await getSplatCropModules();
47
50
  const rootTileset = readTilesetJson(tilesetPath);
48
- const totalResources = collectCandidateSplatResources({
51
+ const initialResourcePaths = collectCandidateSplatResources({
49
52
  rootDir,
50
53
  tileset: rootTileset,
51
54
  tilesetPath,
52
- }).size;
55
+ });
56
+ const totalResources = initialResourcePaths.size;
53
57
  if (typeof onProgress === 'function') {
54
58
  const readStreamHint = tileReadStreamsClosed
55
59
  ? ' Tile read streams closed.'
@@ -77,7 +81,7 @@ async function deleteSplatsInNormalizedSelections(
77
81
  const workerPool = new SplatCropWorkerPool(SPLAT_CROP_WORKER_COUNT);
78
82
 
79
83
  try {
80
- return await traverseTileset({
84
+ const traversalResult = await traverseTileset({
81
85
  THREE,
82
86
  tilesetPath,
83
87
  tileset: rootTileset,
@@ -102,6 +106,20 @@ async function deleteSplatsInNormalizedSelections(
102
106
  resourceLocks: new Map(),
103
107
  workerPool,
104
108
  });
109
+ const remainingResourcePaths = collectCandidateSplatResources({
110
+ rootDir,
111
+ tilesetPath,
112
+ });
113
+ const cleanupResult = await deleteOrphanedSplatResources({
114
+ initialResourcePaths,
115
+ remainingResourcePaths,
116
+ rootDir,
117
+ });
118
+ return {
119
+ ...traversalResult,
120
+ deletedSplatFiles: cleanupResult.deletedFiles.length,
121
+ failedSplatFileDeletes: cleanupResult.failedFiles.length,
122
+ };
105
123
  } finally {
106
124
  await workerPool.close();
107
125
  }
@@ -20,6 +20,7 @@ const {
20
20
  hasNonGaussianScenePrimitives,
21
21
  hasScenePrimitives,
22
22
  removeMeshPrimitives,
23
+ updateGaussianPrimitiveAccessorCounts,
23
24
  } = require('./gaussianPrimitives');
24
25
 
25
26
  function getContentSlots(tile) {
@@ -399,6 +400,126 @@ function collectCandidateSplatResources({
399
400
  return resourcePaths;
400
401
  }
401
402
 
403
+ function collectLocalGltfBufferPaths(filePath, rootDir) {
404
+ const bufferPaths = new Set();
405
+ if (path.extname(filePath).toLowerCase() !== '.gltf') {
406
+ return bufferPaths;
407
+ }
408
+ if (!fs.existsSync(filePath)) {
409
+ return bufferPaths;
410
+ }
411
+
412
+ const json = JSON.parse(fs.readFileSync(filePath, 'utf8'));
413
+ const buffers = Array.isArray(json.buffers) ? json.buffers : [];
414
+ const resourceDir = path.dirname(filePath);
415
+ buffers.forEach((buffer, index) => {
416
+ const uri = buffer?.uri;
417
+ if (
418
+ typeof uri !== 'string' ||
419
+ uri.length === 0 ||
420
+ /^data:/i.test(uri) ||
421
+ isRemoteOrProtocolUri(uri)
422
+ ) {
423
+ return;
424
+ }
425
+
426
+ bufferPaths.add(
427
+ resolveLocalUri(
428
+ resourceDir,
429
+ rootDir,
430
+ uri,
431
+ `${filePath}.buffers[${index}].uri`,
432
+ ),
433
+ );
434
+ });
435
+ return bufferPaths;
436
+ }
437
+
438
+ function collectReferencedGltfBufferPaths(resourcePaths, rootDir) {
439
+ const bufferPaths = new Set();
440
+ resourcePaths.forEach((resourcePath) => {
441
+ collectLocalGltfBufferPaths(resourcePath, rootDir).forEach((bufferPath) => {
442
+ bufferPaths.add(bufferPath);
443
+ });
444
+ });
445
+ return bufferPaths;
446
+ }
447
+
448
+ async function deleteFileIfSafe(filePath, rootDir) {
449
+ const resolvedPath = assertPathInsideRoot(
450
+ filePath,
451
+ rootDir,
452
+ 'Orphaned splat resource path',
453
+ );
454
+ let stats;
455
+ try {
456
+ stats = await fs.promises.stat(resolvedPath);
457
+ } catch (err) {
458
+ if (err && err.code === 'ENOENT') {
459
+ return { deleted: false };
460
+ }
461
+ return {
462
+ deleted: false,
463
+ error: err && err.message ? err.message : String(err),
464
+ };
465
+ }
466
+
467
+ if (!stats.isFile()) {
468
+ return { deleted: false };
469
+ }
470
+
471
+ try {
472
+ await fs.promises.unlink(resolvedPath);
473
+ return { deleted: true };
474
+ } catch (err) {
475
+ return {
476
+ deleted: false,
477
+ error: err && err.message ? err.message : String(err),
478
+ };
479
+ }
480
+ }
481
+
482
+ async function deleteOrphanedSplatResources({
483
+ initialResourcePaths,
484
+ remainingResourcePaths,
485
+ rootDir,
486
+ }) {
487
+ const remainingBuffers = collectReferencedGltfBufferPaths(
488
+ remainingResourcePaths,
489
+ rootDir,
490
+ );
491
+ const deletePaths = new Set();
492
+
493
+ initialResourcePaths.forEach((resourcePath) => {
494
+ if (remainingResourcePaths.has(resourcePath)) {
495
+ return;
496
+ }
497
+
498
+ deletePaths.add(resourcePath);
499
+ collectLocalGltfBufferPaths(resourcePath, rootDir).forEach((bufferPath) => {
500
+ if (
501
+ !remainingResourcePaths.has(bufferPath) &&
502
+ !remainingBuffers.has(bufferPath)
503
+ ) {
504
+ deletePaths.add(bufferPath);
505
+ }
506
+ });
507
+ });
508
+
509
+ const deletedFiles = [];
510
+ const failedFiles = [];
511
+ for (const filePath of deletePaths) {
512
+ const result = await deleteFileIfSafe(filePath, rootDir);
513
+ if (result.deleted) {
514
+ deletedFiles.push(filePath);
515
+ } else if (result.error) {
516
+ failedFiles.push({ error: result.error, filePath });
517
+ }
518
+ }
519
+
520
+ return { deletedFiles, failedFiles };
521
+ }
522
+
402
523
  function markSplatResourceProgress(context, resourcePath) {
403
524
  const progress = context.progress;
404
525
  if (
@@ -472,6 +593,7 @@ async function processGltfResource({
472
593
  const resourceBounds = createBounds();
473
594
  let hasResourceBounds = false;
474
595
  let deletedSplats = 0;
596
+ let modified = false;
475
597
  const rewriteTasks = [];
476
598
 
477
599
  for (const [bufferViewIndex, viewDescriptors] of byBufferView) {
@@ -511,6 +633,14 @@ async function processGltfResource({
511
633
  emptyDescriptors.push(...viewDescriptors);
512
634
  continue;
513
635
  }
636
+ if (Number.isInteger(rewrite.survivorCount)) {
637
+ modified =
638
+ updateGaussianPrimitiveAccessorCounts(
639
+ resource,
640
+ viewDescriptors,
641
+ rewrite.survivorCount,
642
+ ) > 0 || modified;
643
+ }
514
644
  if (!rewrite.bytes) {
515
645
  continue;
516
646
  }
@@ -526,7 +656,6 @@ async function processGltfResource({
526
656
  });
527
657
  }
528
658
 
529
- let modified = false;
530
659
  if (emptyDescriptors.length > 0) {
531
660
  modified = removeMeshPrimitives(resource, emptyDescriptors) > 0 || modified;
532
661
  }
@@ -948,6 +1077,7 @@ async function traverseTileset({
948
1077
 
949
1078
  module.exports = {
950
1079
  collectCandidateSplatResources,
1080
+ deleteOrphanedSplatResources,
951
1081
  readTilesetJson,
952
1082
  traverseTileset,
953
1083
  };
@@ -344,10 +344,24 @@ async function rewriteSpzBytes({
344
344
  descriptors,
345
345
  );
346
346
  if (survivors.length === 0) {
347
- return { bounds, bytes: null, deleted, empty: true };
347
+ return {
348
+ bounds,
349
+ bytes: null,
350
+ deleted,
351
+ empty: true,
352
+ splatCount: spz.numSplats,
353
+ survivorCount: 0,
354
+ };
348
355
  }
349
356
  if (deleted === 0) {
350
- return { bounds, bytes: null, deleted: 0, empty: false };
357
+ return {
358
+ bounds,
359
+ bytes: null,
360
+ deleted: 0,
361
+ empty: false,
362
+ splatCount: spz.numSplats,
363
+ survivorCount: survivors.length,
364
+ };
351
365
  }
352
366
 
353
367
  return {
@@ -355,6 +369,8 @@ async function rewriteSpzBytes({
355
369
  bytes: await writeSurvivingSpzBytes(SpzWriter, spz, splatData, survivors),
356
370
  deleted,
357
371
  empty: false,
372
+ splatCount: spz.numSplats,
373
+ survivorCount: survivors.length,
358
374
  };
359
375
  }
360
376
 
@@ -380,6 +396,8 @@ parentPort.on('message', async (message) => {
380
396
  bytes,
381
397
  deleted: result.deleted,
382
398
  empty: result.empty,
399
+ splatCount: result.splatCount,
400
+ survivorCount: result.survivorCount,
383
401
  },
384
402
  },
385
403
  transferList,
@@ -77,6 +77,8 @@ class SplatCropWorkerPool {
77
77
  bytes: result.bytes ? Buffer.from(result.bytes) : null,
78
78
  deleted: Number(result.deleted || 0),
79
79
  empty: !!result.empty,
80
+ splatCount: Number(result.splatCount || 0),
81
+ survivorCount: Number(result.survivorCount || 0),
80
82
  });
81
83
  }
82
84
 
package/src/viewer/app.js CHANGED
@@ -550,10 +550,11 @@ async function saveTransform() {
550
550
  const processedSplatResources = Number(
551
551
  payload.processedSplatResources || 0,
552
552
  );
553
+ const deletedSplatFiles = Number(payload.deletedSplatFiles || 0);
553
554
  cropController.clearAll();
554
555
  loadTileset(TILESET_URL, { frameOnLoad: false });
555
556
  setStatus(
556
- `Saved transform and deleted ${deletedSplats} cropped splats from ${processedSplatResources} splat resource${processedSplatResources === 1 ? '' : 's'}. Reloading tileset.`,
557
+ `Saved transform and deleted ${deletedSplats} cropped splats from ${processedSplatResources} splat resource${processedSplatResources === 1 ? '' : 's'}${deletedSplatFiles > 0 ? `, removing ${deletedSplatFiles} orphaned file${deletedSplatFiles === 1 ? '' : 's'}` : ''}. Reloading tileset.`,
557
558
  );
558
559
  } else {
559
560
  setStatus(