@loaders.gl/tiles 4.4.5 → 5.0.0-alpha.2

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