@loaders.gl/tiles 4.4.2 → 5.0.0-alpha.1

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.
Files changed (198) hide show
  1. package/README.md +110 -1
  2. package/dist/dist.dev.js +4359 -1950
  3. package/dist/dist.min.js +1 -1
  4. package/dist/image-set.d.ts +94 -0
  5. package/dist/image-set.d.ts.map +1 -0
  6. package/dist/image-set.js +243 -0
  7. package/dist/image-set.js.map +1 -0
  8. package/dist/index.cjs +4178 -1892
  9. package/dist/index.cjs.map +4 -4
  10. package/dist/index.d.ts +27 -9
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +17 -8
  13. package/dist/index.js.map +1 -1
  14. package/dist/point-cloud/point-cloud-tile.d.ts +47 -0
  15. package/dist/point-cloud/point-cloud-tile.d.ts.map +1 -0
  16. package/dist/point-cloud/point-cloud-tile.js +67 -0
  17. package/dist/point-cloud/point-cloud-tile.js.map +1 -0
  18. package/dist/point-cloud/point-cloud-tileset.d.ts +131 -0
  19. package/dist/point-cloud/point-cloud-tileset.d.ts.map +1 -0
  20. package/dist/point-cloud/point-cloud-tileset.js +507 -0
  21. package/dist/point-cloud/point-cloud-tileset.js.map +1 -0
  22. package/dist/point-cloud/types.d.ts +64 -0
  23. package/dist/point-cloud/types.d.ts.map +1 -0
  24. package/dist/point-cloud/types.js +2 -0
  25. package/dist/point-cloud/types.js.map +1 -0
  26. package/dist/raster-set.d.ts +107 -0
  27. package/dist/raster-set.d.ts.map +1 -0
  28. package/dist/raster-set.js +268 -0
  29. package/dist/raster-set.js.map +1 -0
  30. package/dist/tileset-2d/adapter.d.ts +38 -0
  31. package/dist/tileset-2d/adapter.d.ts.map +1 -0
  32. package/dist/tileset-2d/adapter.js +5 -0
  33. package/dist/tileset-2d/adapter.js.map +1 -0
  34. package/dist/tileset-2d/index.d.ts +7 -0
  35. package/dist/tileset-2d/index.d.ts.map +1 -0
  36. package/dist/tileset-2d/index.js +6 -0
  37. package/dist/tileset-2d/index.js.map +1 -0
  38. package/dist/tileset-2d/tile-2d-header.d.ts +68 -0
  39. package/dist/tileset-2d/tile-2d-header.d.ts.map +1 -0
  40. package/dist/tileset-2d/tile-2d-header.js +181 -0
  41. package/dist/tileset-2d/tile-2d-header.js.map +1 -0
  42. package/dist/tileset-2d/tileset-2d.d.ts +201 -0
  43. package/dist/tileset-2d/tileset-2d.d.ts.map +1 -0
  44. package/dist/tileset-2d/tileset-2d.js +532 -0
  45. package/dist/tileset-2d/tileset-2d.js.map +1 -0
  46. package/dist/tileset-2d/types.d.ts +44 -0
  47. package/dist/tileset-2d/types.d.ts.map +1 -0
  48. package/dist/tileset-2d/types.js +5 -0
  49. package/dist/tileset-2d/types.js.map +1 -0
  50. package/dist/tileset-2d.d.ts +2 -0
  51. package/dist/tileset-2d.d.ts.map +1 -0
  52. package/dist/tileset-2d.js +5 -0
  53. package/dist/tileset-2d.js.map +1 -0
  54. package/dist/tileset-3d/common/indexed-archive-tileset-source.d.ts +60 -0
  55. package/dist/tileset-3d/common/indexed-archive-tileset-source.d.ts.map +1 -0
  56. package/dist/tileset-3d/common/indexed-archive-tileset-source.js +125 -0
  57. package/dist/tileset-3d/common/indexed-archive-tileset-source.js.map +1 -0
  58. package/dist/{tileset → tileset-3d/common}/tile-3d.d.ts +36 -27
  59. package/dist/tileset-3d/common/tile-3d.d.ts.map +1 -0
  60. package/dist/{tileset → tileset-3d/common}/tile-3d.js +53 -62
  61. package/dist/tileset-3d/common/tile-3d.js.map +1 -0
  62. package/dist/tileset-3d/common/tileset-3d.d.ts +177 -0
  63. package/dist/tileset-3d/common/tileset-3d.d.ts.map +1 -0
  64. package/dist/tileset-3d/common/tileset-3d.js +566 -0
  65. package/dist/tileset-3d/common/tileset-3d.js.map +1 -0
  66. package/dist/tileset-3d/common/tileset-cache.d.ts.map +1 -0
  67. package/dist/{tileset → tileset-3d/common}/tileset-cache.js +1 -1
  68. package/dist/tileset-3d/common/tileset-cache.js.map +1 -0
  69. package/dist/tileset-3d/common/tileset-source.d.ts +186 -0
  70. package/dist/tileset-3d/common/tileset-source.d.ts.map +1 -0
  71. package/dist/tileset-3d/common/tileset-source.js +17 -0
  72. package/dist/tileset-3d/common/tileset-source.js.map +1 -0
  73. package/dist/{tileset → tileset-3d/common}/tileset-traverser.d.ts +2 -2
  74. package/dist/tileset-3d/common/tileset-traverser.d.ts.map +1 -0
  75. package/dist/{tileset → tileset-3d/common}/tileset-traverser.js +2 -2
  76. package/dist/tileset-3d/common/tileset-traverser.js.map +1 -0
  77. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.d.ts +100 -0
  78. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.d.ts.map +1 -0
  79. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.js +312 -0
  80. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.js.map +1 -0
  81. package/dist/{tileset → tileset-3d}/format-3d-tiles/tileset-3d-traverser.d.ts +1 -1
  82. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.d.ts.map +1 -0
  83. package/dist/{tileset → tileset-3d}/format-3d-tiles/tileset-3d-traverser.js +1 -1
  84. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.js.map +1 -0
  85. package/dist/tileset-3d/format-i3s/i3s-pending-tiles-register.d.ts.map +1 -0
  86. package/dist/tileset-3d/format-i3s/i3s-pending-tiles-register.js.map +1 -0
  87. package/dist/tileset-3d/format-i3s/i3s-source.d.ts +99 -0
  88. package/dist/tileset-3d/format-i3s/i3s-source.d.ts.map +1 -0
  89. package/dist/tileset-3d/format-i3s/i3s-source.js +299 -0
  90. package/dist/tileset-3d/format-i3s/i3s-source.js.map +1 -0
  91. package/dist/tileset-3d/format-i3s/i3s-tile-manager.d.ts.map +1 -0
  92. package/dist/{tileset → tileset-3d}/format-i3s/i3s-tile-manager.js +2 -2
  93. package/dist/tileset-3d/format-i3s/i3s-tile-manager.js.map +1 -0
  94. package/dist/{tileset → tileset-3d}/format-i3s/i3s-tileset-traverser.d.ts +2 -2
  95. package/dist/tileset-3d/format-i3s/i3s-tileset-traverser.d.ts.map +1 -0
  96. package/dist/{tileset → tileset-3d}/format-i3s/i3s-tileset-traverser.js +14 -23
  97. package/dist/tileset-3d/format-i3s/i3s-tileset-traverser.js.map +1 -0
  98. package/dist/{tileset → tileset-3d}/helpers/3d-tiles-options.d.ts +1 -1
  99. package/dist/tileset-3d/helpers/3d-tiles-options.d.ts.map +1 -0
  100. package/dist/tileset-3d/helpers/3d-tiles-options.js.map +1 -0
  101. package/dist/tileset-3d/helpers/bounding-volume.d.ts.map +1 -0
  102. package/dist/tileset-3d/helpers/bounding-volume.js.map +1 -0
  103. package/dist/{tileset → tileset-3d}/helpers/frame-state.d.ts +1 -1
  104. package/dist/tileset-3d/helpers/frame-state.d.ts.map +1 -0
  105. package/dist/tileset-3d/helpers/frame-state.js.map +1 -0
  106. package/dist/{tileset → tileset-3d}/helpers/i3s-lod.d.ts +1 -1
  107. package/dist/tileset-3d/helpers/i3s-lod.d.ts.map +1 -0
  108. package/dist/{tileset → tileset-3d}/helpers/i3s-lod.js +1 -1
  109. package/dist/tileset-3d/helpers/i3s-lod.js.map +1 -0
  110. package/dist/tileset-3d/helpers/tiles-3d-lod.d.ts +20 -0
  111. package/dist/tileset-3d/helpers/tiles-3d-lod.d.ts.map +1 -0
  112. package/dist/{tileset → tileset-3d}/helpers/tiles-3d-lod.js +43 -4
  113. package/dist/tileset-3d/helpers/tiles-3d-lod.js.map +1 -0
  114. package/dist/{tileset → tileset-3d}/helpers/transform-utils.d.ts +1 -1
  115. package/dist/tileset-3d/helpers/transform-utils.d.ts.map +1 -0
  116. package/dist/tileset-3d/helpers/transform-utils.js.map +1 -0
  117. package/dist/tileset-3d/helpers/zoom.d.ts.map +1 -0
  118. package/dist/tileset-3d/helpers/zoom.js.map +1 -0
  119. package/dist/types.d.ts +7 -0
  120. package/dist/types.d.ts.map +1 -1
  121. package/package.json +7 -6
  122. package/src/image-set.ts +328 -0
  123. package/src/index.ts +80 -9
  124. package/src/point-cloud/point-cloud-tile.ts +76 -0
  125. package/src/point-cloud/point-cloud-tileset.ts +709 -0
  126. package/src/point-cloud/types.ts +78 -0
  127. package/src/raster-set.ts +374 -0
  128. package/src/tileset-2d/adapter.ts +47 -0
  129. package/src/tileset-2d/index.ts +33 -0
  130. package/src/tileset-2d/tile-2d-header.ts +224 -0
  131. package/src/tileset-2d/tileset-2d.ts +716 -0
  132. package/src/tileset-2d/types.ts +39 -0
  133. package/src/tileset-2d.ts +5 -0
  134. package/src/tileset-3d/common/indexed-archive-tileset-source.ts +205 -0
  135. package/src/{tileset → tileset-3d/common}/tile-3d.ts +64 -73
  136. package/src/tileset-3d/common/tileset-3d.ts +736 -0
  137. package/src/{tileset → tileset-3d/common}/tileset-cache.ts +1 -1
  138. package/src/tileset-3d/common/tileset-source.ts +234 -0
  139. package/src/{tileset → tileset-3d/common}/tileset-traverser.ts +3 -3
  140. package/src/tileset-3d/format-3d-tiles/tiles-3d-source.ts +379 -0
  141. package/src/{tileset → tileset-3d}/format-3d-tiles/tileset-3d-traverser.ts +1 -1
  142. package/src/tileset-3d/format-i3s/i3s-source.ts +366 -0
  143. package/src/{tileset → tileset-3d}/format-i3s/i3s-tile-manager.ts +2 -2
  144. package/src/{tileset → tileset-3d}/format-i3s/i3s-tileset-traverser.ts +15 -24
  145. package/src/{tileset → tileset-3d}/helpers/3d-tiles-options.ts +1 -1
  146. package/src/{tileset → tileset-3d}/helpers/frame-state.ts +1 -1
  147. package/src/{tileset → tileset-3d}/helpers/i3s-lod.ts +2 -2
  148. package/src/{tileset → tileset-3d}/helpers/tiles-3d-lod.ts +46 -4
  149. package/src/{tileset → tileset-3d}/helpers/transform-utils.ts +1 -1
  150. package/src/types.ts +7 -0
  151. package/dist/tileset/format-3d-tiles/tileset-3d-traverser.d.ts.map +0 -1
  152. package/dist/tileset/format-3d-tiles/tileset-3d-traverser.js.map +0 -1
  153. package/dist/tileset/format-i3s/i3s-pending-tiles-register.d.ts.map +0 -1
  154. package/dist/tileset/format-i3s/i3s-pending-tiles-register.js.map +0 -1
  155. package/dist/tileset/format-i3s/i3s-tile-manager.d.ts.map +0 -1
  156. package/dist/tileset/format-i3s/i3s-tile-manager.js.map +0 -1
  157. package/dist/tileset/format-i3s/i3s-tileset-traverser.d.ts.map +0 -1
  158. package/dist/tileset/format-i3s/i3s-tileset-traverser.js.map +0 -1
  159. package/dist/tileset/helpers/3d-tiles-options.d.ts.map +0 -1
  160. package/dist/tileset/helpers/3d-tiles-options.js.map +0 -1
  161. package/dist/tileset/helpers/bounding-volume.d.ts.map +0 -1
  162. package/dist/tileset/helpers/bounding-volume.js.map +0 -1
  163. package/dist/tileset/helpers/frame-state.d.ts.map +0 -1
  164. package/dist/tileset/helpers/frame-state.js.map +0 -1
  165. package/dist/tileset/helpers/i3s-lod.d.ts.map +0 -1
  166. package/dist/tileset/helpers/i3s-lod.js.map +0 -1
  167. package/dist/tileset/helpers/tiles-3d-lod.d.ts +0 -8
  168. package/dist/tileset/helpers/tiles-3d-lod.d.ts.map +0 -1
  169. package/dist/tileset/helpers/tiles-3d-lod.js.map +0 -1
  170. package/dist/tileset/helpers/transform-utils.d.ts.map +0 -1
  171. package/dist/tileset/helpers/transform-utils.js.map +0 -1
  172. package/dist/tileset/helpers/zoom.d.ts.map +0 -1
  173. package/dist/tileset/helpers/zoom.js.map +0 -1
  174. package/dist/tileset/tile-3d.d.ts.map +0 -1
  175. package/dist/tileset/tile-3d.js.map +0 -1
  176. package/dist/tileset/tileset-3d.d.ts +0 -284
  177. package/dist/tileset/tileset-3d.d.ts.map +0 -1
  178. package/dist/tileset/tileset-3d.js +0 -814
  179. package/dist/tileset/tileset-3d.js.map +0 -1
  180. package/dist/tileset/tileset-cache.d.ts.map +0 -1
  181. package/dist/tileset/tileset-cache.js.map +0 -1
  182. package/dist/tileset/tileset-traverser.d.ts.map +0 -1
  183. package/dist/tileset/tileset-traverser.js.map +0 -1
  184. package/src/tileset/tileset-3d.ts +0 -1037
  185. /package/dist/{tileset → tileset-3d/common}/tileset-cache.d.ts +0 -0
  186. /package/dist/{tileset → tileset-3d}/format-i3s/i3s-pending-tiles-register.d.ts +0 -0
  187. /package/dist/{tileset → tileset-3d}/format-i3s/i3s-pending-tiles-register.js +0 -0
  188. /package/dist/{tileset → tileset-3d}/format-i3s/i3s-tile-manager.d.ts +0 -0
  189. /package/dist/{tileset → tileset-3d}/helpers/3d-tiles-options.js +0 -0
  190. /package/dist/{tileset → tileset-3d}/helpers/bounding-volume.d.ts +0 -0
  191. /package/dist/{tileset → tileset-3d}/helpers/bounding-volume.js +0 -0
  192. /package/dist/{tileset → tileset-3d}/helpers/frame-state.js +0 -0
  193. /package/dist/{tileset → tileset-3d}/helpers/transform-utils.js +0 -0
  194. /package/dist/{tileset → tileset-3d}/helpers/zoom.d.ts +0 -0
  195. /package/dist/{tileset → tileset-3d}/helpers/zoom.js +0 -0
  196. /package/src/{tileset → tileset-3d}/format-i3s/i3s-pending-tiles-register.ts +0 -0
  197. /package/src/{tileset → tileset-3d}/helpers/bounding-volume.ts +0 -0
  198. /package/src/{tileset → tileset-3d}/helpers/zoom.ts +0 -0
@@ -0,0 +1,709 @@
1
+ import {Vector3} from '@math.gl/core';
2
+ import {BoundingSphere, CullingVolume, Plane} from '@math.gl/culling';
3
+ import type {Viewport} from '../types';
4
+ import {PointCloudTile} from './point-cloud-tile';
5
+ import type {
6
+ PointCloudBoundingVolume,
7
+ PointCloudTileHeader,
8
+ PointCloudTilesetSource
9
+ } from './types';
10
+
11
+ export type PointCloudTilesetOptions = {
12
+ debounceTime?: number;
13
+ minimumNodePixelSize?: number;
14
+ maximumScreenSpaceError?: number;
15
+ pointBudget?: number;
16
+ maxDepth?: number;
17
+ onTileLoad?: (tile: PointCloudTile) => void;
18
+ onTileError?: (tile: PointCloudTile, error: Error) => void;
19
+ onTraversalComplete?: (selectedTiles: PointCloudTile[]) => PointCloudTile[];
20
+ onUpdate?: () => void;
21
+ };
22
+
23
+ type PointCloudTilesetProps = Required<PointCloudTilesetOptions>;
24
+
25
+ const DEFAULT_PROPS: PointCloudTilesetProps = {
26
+ debounceTime: 0,
27
+ minimumNodePixelSize: 150,
28
+ maximumScreenSpaceError: 1,
29
+ pointBudget: 2_000_000,
30
+ maxDepth: Number.POSITIVE_INFINITY,
31
+ onTileLoad: () => {},
32
+ onTileError: () => {},
33
+ onTraversalComplete: (selectedTiles: PointCloudTile[]) => selectedTiles,
34
+ onUpdate: () => {}
35
+ };
36
+
37
+ const VISIBILITY_MARGIN_PIXELS = 64;
38
+
39
+ type TraversalCandidate = {
40
+ tile: PointCloudTile;
41
+ weight: number;
42
+ };
43
+
44
+ type ProjectedBounds = {
45
+ minX: number;
46
+ minY: number;
47
+ maxX: number;
48
+ maxY: number;
49
+ };
50
+
51
+ type ProjectedFootprint = {
52
+ x: number;
53
+ y: number;
54
+ radius: number;
55
+ };
56
+
57
+ type FrustumPlane = {
58
+ distance: number;
59
+ normal: number[] | Vector3;
60
+ };
61
+
62
+ type PointCloudViewport = Viewport & {
63
+ cameraPosition?: number[] | Vector3;
64
+ getFrustumPlanes?: () => Record<string, FrustumPlane>;
65
+ projectPosition?: (coordinates: number[] | Vector3) => number[];
66
+ projectionMatrix?: number[];
67
+ };
68
+
69
+ type TraversalContext = {
70
+ viewport: Viewport;
71
+ cullingVolume: CullingVolume | null;
72
+ };
73
+
74
+ /**
75
+ * A minimal point-cloud-oriented tileset manager backed by a DataSource.
76
+ */
77
+ export class PointCloudTileset {
78
+ readonly dataSource: PointCloudTilesetSource;
79
+ options: PointCloudTilesetProps;
80
+ root: PointCloudTile | null = null;
81
+ cartographicCenter: Vector3 | null = null;
82
+ zoom = 1;
83
+ boundingVolume: PointCloudBoundingVolume | null = null;
84
+ visibleTilesCount = 0;
85
+
86
+ private frameNumberValue = 0;
87
+ private pendingCount = 0;
88
+ private lastUpdatedViewports: Viewport[] | Viewport | null = null;
89
+ private updatePromise: Promise<number> | null = null;
90
+ private readonly tilesMap: Map<string, PointCloudTile> = new Map();
91
+
92
+ constructor(
93
+ dataSource: PointCloudTilesetSource,
94
+ options: Partial<PointCloudTilesetOptions> = {}
95
+ ) {
96
+ this.dataSource = dataSource;
97
+ this.options = {...DEFAULT_PROPS, ...options};
98
+ this.tilesetInitializationPromise = this.initialize();
99
+ }
100
+
101
+ readonly tilesetInitializationPromise: Promise<void>;
102
+
103
+ /**
104
+ * Whether the tileset finished initialization.
105
+ */
106
+ get isReady(): boolean {
107
+ return this.dataSource.isReady;
108
+ }
109
+
110
+ /**
111
+ * Runtime frame number.
112
+ */
113
+ get frameNumber(): number {
114
+ return this.frameNumberValue;
115
+ }
116
+
117
+ /**
118
+ * All discovered tiles.
119
+ */
120
+ get tiles(): PointCloudTile[] {
121
+ return Array.from(this.tilesMap.values());
122
+ }
123
+
124
+ /**
125
+ * Whether all currently selected tiles have finished loading.
126
+ */
127
+ isLoaded(): boolean {
128
+ return (
129
+ this.frameNumberValue > 0 &&
130
+ this.pendingCount === 0 &&
131
+ this.selectedTiles.every(tile => tile.contentAvailable || tile.contentFailed)
132
+ );
133
+ }
134
+
135
+ /**
136
+ * Currently selected tiles.
137
+ */
138
+ selectedTiles: PointCloudTile[] = [];
139
+
140
+ /**
141
+ * Fire-and-forget update helper.
142
+ */
143
+ update(viewports: Viewport[] | Viewport | null = null): void {
144
+ this.selectTiles(viewports).catch(() => undefined);
145
+ }
146
+
147
+ /**
148
+ * Releases traversal state held by this tileset.
149
+ */
150
+ destroy(): void {
151
+ this.selectedTiles = [];
152
+ this.tilesMap.clear();
153
+ this.root = null;
154
+ this.cartographicCenter = null;
155
+ this.boundingVolume = null;
156
+ this.visibleTilesCount = 0;
157
+ this.pendingCount = 0;
158
+ this.lastUpdatedViewports = null;
159
+ this.updatePromise = null;
160
+ }
161
+
162
+ /**
163
+ * Initialize the underlying source and the root tile.
164
+ */
165
+ async initialize(): Promise<void> {
166
+ await this.dataSource.initialize();
167
+ const rootHeader = await this.dataSource.getRootTile();
168
+ this.root = this.getOrCreateTile(rootHeader, null);
169
+
170
+ const viewState = await this.dataSource.getViewState?.();
171
+ this.boundingVolume = viewState?.boundingVolume || this.root.boundingVolume;
172
+
173
+ const center = viewState?.cartographicCenter || this.root.boundingVolume.center;
174
+ this.cartographicCenter = new Vector3(center);
175
+ this.zoom = viewState?.zoom || this.estimateZoom(this.boundingVolume);
176
+ }
177
+
178
+ /**
179
+ * Debounced async tile selection.
180
+ */
181
+ async selectTiles(viewports: Viewport[] | Viewport | null = null): Promise<number> {
182
+ await this.tilesetInitializationPromise;
183
+ if (viewports) {
184
+ this.lastUpdatedViewports = viewports;
185
+ }
186
+
187
+ if (!this.updatePromise) {
188
+ this.updatePromise = new Promise<number>((resolve, reject) => {
189
+ setTimeout(() => {
190
+ this.doSelectTiles()
191
+ .then(() => resolve(this.frameNumberValue))
192
+ .catch(reject)
193
+ .finally(() => {
194
+ this.updatePromise = null;
195
+ });
196
+ }, this.options.debounceTime);
197
+ });
198
+ }
199
+
200
+ return this.updatePromise;
201
+ }
202
+
203
+ private async doSelectTiles(): Promise<void> {
204
+ if (!this.root || !this.lastUpdatedViewports) {
205
+ return;
206
+ }
207
+
208
+ const previousSelectedIds = new Set(this.selectedTiles.map(tile => tile.id));
209
+ for (const tile of this.tilesMap.values()) {
210
+ tile.clearSelection();
211
+ }
212
+
213
+ this.frameNumberValue++;
214
+
215
+ const preparedViewports = Array.isArray(this.lastUpdatedViewports)
216
+ ? this.lastUpdatedViewports
217
+ : [this.lastUpdatedViewports];
218
+
219
+ const selectedTilesById = new Map<string, PointCloudTile>();
220
+ const visibleTileIds = new Set<string>();
221
+ let selectedPointCount = 0;
222
+ for (const viewport of preparedViewports) {
223
+ const traversalContext = this.getTraversalContext(viewport);
224
+ selectedPointCount = await this.traverseTiles(
225
+ traversalContext,
226
+ selectedTilesById,
227
+ selectedPointCount,
228
+ visibleTileIds
229
+ );
230
+ }
231
+ this.visibleTilesCount = visibleTileIds.size;
232
+
233
+ this.selectedTiles = this.options.onTraversalComplete(Array.from(selectedTilesById.values()));
234
+
235
+ for (const tile of this.selectedTiles) {
236
+ this.loadTile(tile).catch(() => undefined);
237
+ }
238
+
239
+ const nextSelectedIds = new Set(this.selectedTiles.map(tile => tile.id));
240
+ if (!this.haveSameIds(previousSelectedIds, nextSelectedIds)) {
241
+ this.options.onUpdate();
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Traverses visible tiles in descending projected-size order, similar to Potree's viewer update loop.
247
+ */
248
+ private async traverseTiles(
249
+ traversalContext: TraversalContext,
250
+ selectedTilesById: Map<string, PointCloudTile>,
251
+ initialSelectedPointCount: number,
252
+ visibleTileIds: Set<string>
253
+ ): Promise<number> {
254
+ if (!this.root) {
255
+ return initialSelectedPointCount;
256
+ }
257
+
258
+ let selectedPointCount = initialSelectedPointCount;
259
+ const traversalQueue: TraversalCandidate[] = [
260
+ {tile: this.root, weight: this.estimateScreenRadius(this.root, traversalContext.viewport)}
261
+ ];
262
+
263
+ while (traversalQueue.length > 0) {
264
+ traversalQueue.sort((candidateA, candidateB) => candidateB.weight - candidateA.weight);
265
+ const candidate = traversalQueue.shift();
266
+ if (!candidate) {
267
+ break;
268
+ }
269
+
270
+ const {tile, weight} = candidate;
271
+ if (!this.isVisible(tile, traversalContext)) {
272
+ continue;
273
+ }
274
+ visibleTileIds.add(tile.id);
275
+
276
+ if (!selectedTilesById.has(tile.id) && tile.pointCount > 0) {
277
+ const exceedsPointBudget =
278
+ selectedPointCount > 0 && selectedPointCount + tile.pointCount > this.options.pointBudget;
279
+ if (exceedsPointBudget) {
280
+ break;
281
+ }
282
+
283
+ selectedTilesById.set(tile.id, tile);
284
+ selectedPointCount += tile.pointCount;
285
+ }
286
+
287
+ if (selectedTilesById.has(tile.id)) {
288
+ tile.setSelected(traversalContext.viewport.id);
289
+ }
290
+
291
+ if (!this.shouldRefine(tile, weight)) {
292
+ continue;
293
+ }
294
+
295
+ const visibleChildren = await this.getVisibleChildren(tile, traversalContext);
296
+ if (visibleChildren.length > 0) {
297
+ traversalQueue.push(...visibleChildren);
298
+ }
299
+ }
300
+
301
+ return selectedPointCount;
302
+ }
303
+
304
+ /** Whether a tile should refine to children based on its projected screen radius. */
305
+ private shouldRefine(tile: PointCloudTile, traversalWeight: number): boolean {
306
+ return (
307
+ tile.level < this.options.maxDepth &&
308
+ Number.isFinite(traversalWeight) &&
309
+ traversalWeight >= this.options.minimumNodePixelSize
310
+ );
311
+ }
312
+
313
+ /**
314
+ * Build per-viewport traversal state once per traversal pass.
315
+ */
316
+ private getTraversalContext(viewport: Viewport): TraversalContext {
317
+ const cullingVolume = this.getCullingVolume(viewport as PointCloudViewport);
318
+ return {
319
+ viewport,
320
+ cullingVolume: this.shouldUseCullingVolume(viewport, cullingVolume) ? cullingVolume : null
321
+ };
322
+ }
323
+
324
+ /**
325
+ * Check tile visibility against the viewport frustum when possible.
326
+ */
327
+ private isVisible(tile: PointCloudTile, traversalContext: TraversalContext): boolean {
328
+ if (traversalContext.cullingVolume) {
329
+ const boundingSphere = this.getCommonSpaceBoundingSphere(
330
+ tile.boundingVolume,
331
+ traversalContext.viewport as PointCloudViewport
332
+ );
333
+
334
+ if (boundingSphere) {
335
+ return this.isBoundingSphereInsideCullingVolume(
336
+ boundingSphere,
337
+ traversalContext.cullingVolume
338
+ );
339
+ }
340
+ }
341
+
342
+ const footprint = this.getProjectedFootprint(tile.boundingVolume, traversalContext.viewport);
343
+ if (!footprint) {
344
+ return false;
345
+ }
346
+
347
+ return this.isProjectedFootprintVisible(footprint, traversalContext.viewport);
348
+ }
349
+
350
+ /**
351
+ * Returns true when the viewport frustum appears usable for this tileset's coordinate space.
352
+ */
353
+ private shouldUseCullingVolume(
354
+ viewport: Viewport,
355
+ cullingVolume: CullingVolume | null
356
+ ): cullingVolume is CullingVolume {
357
+ if (!this.root || !cullingVolume) {
358
+ return false;
359
+ }
360
+
361
+ const rootBoundingSphere = this.getCommonSpaceBoundingSphere(
362
+ this.root.boundingVolume,
363
+ viewport as PointCloudViewport
364
+ );
365
+ if (!rootBoundingSphere) {
366
+ return false;
367
+ }
368
+
369
+ if (this.isBoundingSphereInsideCullingVolume(rootBoundingSphere, cullingVolume)) {
370
+ return true;
371
+ }
372
+
373
+ const projectedFootprint = this.getProjectedFootprint(this.root.boundingVolume, viewport);
374
+ return !projectedFootprint || !this.isProjectedFootprintVisible(projectedFootprint, viewport);
375
+ }
376
+
377
+ /**
378
+ * Checks whether a common-space bounding sphere intersects a culling volume.
379
+ */
380
+ private isBoundingSphereInsideCullingVolume(
381
+ boundingSphere: BoundingSphere,
382
+ cullingVolume: CullingVolume
383
+ ): boolean {
384
+ return (
385
+ cullingVolume.computeVisibilityWithPlaneMask(
386
+ boundingSphere,
387
+ CullingVolume.MASK_INDETERMINATE
388
+ ) !== CullingVolume.MASK_OUTSIDE
389
+ );
390
+ }
391
+
392
+ /**
393
+ * Checks a projected screen-space footprint against the viewport rectangle.
394
+ */
395
+ private isProjectedFootprintVisible(footprint: ProjectedFootprint, viewport: Viewport): boolean {
396
+ return !(
397
+ footprint.x + footprint.radius < -VISIBILITY_MARGIN_PIXELS ||
398
+ footprint.x - footprint.radius > viewport.width + VISIBILITY_MARGIN_PIXELS ||
399
+ footprint.y + footprint.radius < -VISIBILITY_MARGIN_PIXELS ||
400
+ footprint.y - footprint.radius > viewport.height + VISIBILITY_MARGIN_PIXELS
401
+ );
402
+ }
403
+
404
+ /** Estimate traversal priority from projected node radius in screen pixels. */
405
+ private estimateScreenRadius(tile: PointCloudTile, viewport: Viewport): number {
406
+ const footprint = this.getProjectedFootprint(tile.boundingVolume, viewport);
407
+ return footprint ? footprint.radius : 0;
408
+ }
409
+
410
+ /**
411
+ * Project a tile AABB into screen space and return its 2D footprint.
412
+ */
413
+ private projectBounds(
414
+ boundingVolume: PointCloudBoundingVolume,
415
+ viewport: Viewport
416
+ ): ProjectedBounds | null {
417
+ const corners = this.getBoundingVolumeCorners(boundingVolume);
418
+
419
+ let minX = Number.POSITIVE_INFINITY;
420
+ let minY = Number.POSITIVE_INFINITY;
421
+ let maxX = Number.NEGATIVE_INFINITY;
422
+ let maxY = Number.NEGATIVE_INFINITY;
423
+
424
+ try {
425
+ for (const corner of corners) {
426
+ const [x, y] = viewport.project(corner);
427
+ if (!Number.isFinite(x) || !Number.isFinite(y)) {
428
+ continue;
429
+ }
430
+ minX = Math.min(minX, x);
431
+ minY = Math.min(minY, y);
432
+ maxX = Math.max(maxX, x);
433
+ maxY = Math.max(maxY, y);
434
+ }
435
+ } catch {
436
+ return null;
437
+ }
438
+
439
+ if (
440
+ !Number.isFinite(minX) ||
441
+ !Number.isFinite(minY) ||
442
+ !Number.isFinite(maxX) ||
443
+ !Number.isFinite(maxY)
444
+ ) {
445
+ return null;
446
+ }
447
+
448
+ return {minX, minY, maxX, maxY};
449
+ }
450
+
451
+ /**
452
+ * Project a tile to a screen-space center/radius footprint.
453
+ */
454
+ private getProjectedFootprint(
455
+ boundingVolume: PointCloudBoundingVolume,
456
+ viewport: Viewport
457
+ ): ProjectedFootprint | null {
458
+ let centerX = Number.NaN;
459
+ let centerY = Number.NaN;
460
+
461
+ try {
462
+ [centerX, centerY] = viewport.project(boundingVolume.center);
463
+ } catch {
464
+ return null;
465
+ }
466
+
467
+ if (!Number.isFinite(centerX) || !Number.isFinite(centerY)) {
468
+ return null;
469
+ }
470
+
471
+ const corners = this.getBoundingVolumeCorners(boundingVolume);
472
+ let projectedRadius = 0;
473
+
474
+ try {
475
+ for (const corner of corners) {
476
+ const [x, y] = viewport.project(corner);
477
+ if (!Number.isFinite(x) || !Number.isFinite(y)) {
478
+ continue;
479
+ }
480
+
481
+ projectedRadius = Math.max(projectedRadius, Math.hypot(x - centerX, y - centerY));
482
+ }
483
+ } catch {
484
+ return null;
485
+ }
486
+
487
+ if (projectedRadius === 0) {
488
+ const fallbackBounds = this.projectBounds(boundingVolume, viewport);
489
+ if (fallbackBounds) {
490
+ projectedRadius =
491
+ Math.max(
492
+ fallbackBounds.maxX - fallbackBounds.minX,
493
+ fallbackBounds.maxY - fallbackBounds.minY
494
+ ) / 2;
495
+ }
496
+ }
497
+
498
+ return {x: centerX, y: centerY, radius: projectedRadius};
499
+ }
500
+
501
+ /**
502
+ * Returns visible children ordered by their projected importance.
503
+ */
504
+ private async getVisibleChildren(
505
+ tile: PointCloudTile,
506
+ traversalContext: TraversalContext
507
+ ): Promise<TraversalCandidate[]> {
508
+ if (tile.level >= this.options.maxDepth) {
509
+ return [];
510
+ }
511
+
512
+ const children = await this.getChildren(tile);
513
+
514
+ return children
515
+ .map(child => ({
516
+ tile: child,
517
+ weight: this.estimateScreenRadius(child, traversalContext.viewport)
518
+ }))
519
+ .filter(candidate => this.isVisible(candidate.tile, traversalContext));
520
+ }
521
+
522
+ private async getChildren(tile: PointCloudTile): Promise<PointCloudTile[]> {
523
+ if (tile.childrenLoaded) {
524
+ return tile.children;
525
+ }
526
+
527
+ if (!tile.childrenPromise) {
528
+ const childrenPromise = this.dataSource
529
+ .getChildren(tile.header)
530
+ .then(headers => headers.map(header => this.getOrCreateTile(header, tile)))
531
+ .then(children => {
532
+ tile.children = children;
533
+ tile.childrenLoaded = true;
534
+ return children;
535
+ });
536
+
537
+ tile.childrenPromise = childrenPromise.finally(() => {
538
+ if (tile.childrenPromise === childrenPromise) {
539
+ tile.childrenPromise = null;
540
+ }
541
+ });
542
+ }
543
+
544
+ return tile.childrenPromise;
545
+ }
546
+
547
+ private getOrCreateTile(
548
+ header: PointCloudTileHeader,
549
+ parent: PointCloudTile | null
550
+ ): PointCloudTile {
551
+ const existingTile = this.tilesMap.get(header.id);
552
+ if (existingTile) {
553
+ existingTile.header = header;
554
+ if (parent && existingTile.parent !== parent) {
555
+ existingTile.parent = parent;
556
+ }
557
+ return existingTile;
558
+ }
559
+
560
+ const tile = new PointCloudTile(header, parent);
561
+ this.tilesMap.set(header.id, tile);
562
+ if (parent && !parent.children.find(child => child.id === tile.id)) {
563
+ parent.children.push(tile);
564
+ }
565
+ return tile;
566
+ }
567
+
568
+ private async loadTile(tile: PointCloudTile): Promise<void> {
569
+ if (tile.contentAvailable || tile.isContentLoading || tile.contentFailed) {
570
+ return;
571
+ }
572
+
573
+ tile.isContentLoading = true;
574
+ this.pendingCount++;
575
+
576
+ try {
577
+ const content = await this.dataSource.loadTileContent(tile.header);
578
+ tile.content = content;
579
+ tile.contentAvailable = true;
580
+ if (content) {
581
+ this.options.onTileLoad(tile);
582
+ }
583
+ } catch (error) {
584
+ tile.contentFailed = true;
585
+ this.options.onTileError(tile, error as Error);
586
+ } finally {
587
+ tile.isContentLoading = false;
588
+ this.pendingCount--;
589
+ }
590
+ }
591
+
592
+ private estimateZoom(boundingVolume: PointCloudBoundingVolume | null): number {
593
+ if (!boundingVolume) {
594
+ return 1;
595
+ }
596
+
597
+ const [minBounds, maxBounds] = boundingVolume.cartographicBounds;
598
+ const longitudeSpan = Math.max(Math.abs(maxBounds[0] - minBounds[0]), 0.000001);
599
+ return Math.max(1, Math.round(Math.log2(360 / longitudeSpan)));
600
+ }
601
+
602
+ private haveSameIds(idsA: Set<string>, idsB: Set<string>): boolean {
603
+ if (idsA.size !== idsB.size) {
604
+ return false;
605
+ }
606
+
607
+ for (const id of idsA) {
608
+ if (!idsB.has(id)) {
609
+ return false;
610
+ }
611
+ }
612
+
613
+ return true;
614
+ }
615
+
616
+ /**
617
+ * Convert a tile bounding box into its eight corners.
618
+ */
619
+ private getBoundingVolumeCorners(boundingVolume: PointCloudBoundingVolume): number[][] {
620
+ const [minBounds, maxBounds] = boundingVolume.cartographicBounds;
621
+ return [
622
+ [minBounds[0], minBounds[1], minBounds[2] || 0],
623
+ [minBounds[0], minBounds[1], maxBounds[2] || 0],
624
+ [minBounds[0], maxBounds[1], minBounds[2] || 0],
625
+ [minBounds[0], maxBounds[1], maxBounds[2] || 0],
626
+ [maxBounds[0], minBounds[1], minBounds[2] || 0],
627
+ [maxBounds[0], minBounds[1], maxBounds[2] || 0],
628
+ [maxBounds[0], maxBounds[1], minBounds[2] || 0],
629
+ [maxBounds[0], maxBounds[1], maxBounds[2] || 0]
630
+ ];
631
+ }
632
+
633
+ /**
634
+ * Build a culling volume directly from the viewport frustum in common space.
635
+ */
636
+ private getCullingVolume(viewport: PointCloudViewport): CullingVolume | null {
637
+ const getFrustumPlanes = viewport.getFrustumPlanes?.bind(viewport);
638
+ if (!getFrustumPlanes) {
639
+ return null;
640
+ }
641
+
642
+ const frustumPlanes = getFrustumPlanes();
643
+ const directions = ['left', 'right', 'bottom', 'top', 'near', 'far'];
644
+ const planes: Plane[] = [];
645
+
646
+ for (const direction of directions) {
647
+ const plane = frustumPlanes[direction];
648
+ if (!plane) {
649
+ return null;
650
+ }
651
+
652
+ planes.push(new Plane(plane.normal, -plane.distance));
653
+ }
654
+
655
+ return new CullingVolume(planes);
656
+ }
657
+
658
+ /**
659
+ * Approximate a tile AABB by a common-space bounding sphere for frustum culling.
660
+ */
661
+ private getCommonSpaceBoundingSphere(
662
+ boundingVolume: PointCloudBoundingVolume,
663
+ viewport: PointCloudViewport
664
+ ): BoundingSphere | null {
665
+ const projectPosition = viewport.projectPosition?.bind(viewport);
666
+ if (!projectPosition) {
667
+ return null;
668
+ }
669
+
670
+ const corners = this.getBoundingVolumeCorners(boundingVolume);
671
+ let minX = Number.POSITIVE_INFINITY;
672
+ let minY = Number.POSITIVE_INFINITY;
673
+ let minZ = Number.POSITIVE_INFINITY;
674
+ let maxX = Number.NEGATIVE_INFINITY;
675
+ let maxY = Number.NEGATIVE_INFINITY;
676
+ let maxZ = Number.NEGATIVE_INFINITY;
677
+
678
+ try {
679
+ for (const corner of corners) {
680
+ const [x, y, z = 0] = projectPosition(corner);
681
+ if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) {
682
+ continue;
683
+ }
684
+
685
+ minX = Math.min(minX, x);
686
+ minY = Math.min(minY, y);
687
+ minZ = Math.min(minZ, z);
688
+ maxX = Math.max(maxX, x);
689
+ maxY = Math.max(maxY, y);
690
+ maxZ = Math.max(maxZ, z);
691
+ }
692
+ } catch {
693
+ return null;
694
+ }
695
+
696
+ if (
697
+ !Number.isFinite(minX) ||
698
+ !Number.isFinite(minY) ||
699
+ !Number.isFinite(minZ) ||
700
+ !Number.isFinite(maxX) ||
701
+ !Number.isFinite(maxY) ||
702
+ !Number.isFinite(maxZ)
703
+ ) {
704
+ return null;
705
+ }
706
+
707
+ return new BoundingSphere().fromCornerPoints([minX, minY, minZ], [maxX, maxY, maxZ]);
708
+ }
709
+ }