@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,311 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import type {Tile3D} from '../common/tile-3d';
6
+ import {Vector3} from '@math.gl/core';
7
+ import {CullingVolume, Plane} from '@math.gl/culling';
8
+ import {Ellipsoid} from '@math.gl/geospatial';
9
+ import {GeospatialViewport, Viewport} from '../../types';
10
+
11
+ export type FrameState = {
12
+ camera: {
13
+ /** Camera position in WGS84 Cartesian meters. */
14
+ position: number[];
15
+ /** Normalized camera direction in WGS84 Cartesian coordinates. */
16
+ direction: number[];
17
+ /** Normalized camera up direction in WGS84 Cartesian coordinates. */
18
+ up: number[];
19
+ /** Camera position as `[longitude, latitude, height]`, with height in meters. */
20
+ cartographicPosition: [number, number, number];
21
+ /** Perspective vertical field of view in radians. */
22
+ verticalFieldOfView: number;
23
+ /** Elapsed seconds since camera position or direction last changed. */
24
+ timeSinceMovement: number;
25
+ };
26
+ viewport: GeospatialViewport;
27
+ topDownViewport: GeospatialViewport; // Use it to calculate projected radius for a tile
28
+ height: number;
29
+ cullingVolume: CullingVolume;
30
+ frameNumber: number; // TODO: This can be the same between updates, what number is unique for between updates?
31
+ sseDenominator: number; // Assumes fovy = 60 degrees
32
+ /** View-dependent density used by perspective dynamic screen-space error for this traversal. */
33
+ dynamicScreenSpaceErrorDensity: number;
34
+ };
35
+
36
+ /** Optional camera and projection measurements supplied when constructing a frame state. */
37
+ export type GetFrameStateOptions = {
38
+ /** Elapsed seconds since camera position or direction last changed. */
39
+ timeSinceCameraMovement?: number;
40
+ };
41
+
42
+ /** Camera pose and timestamp retained between traversal frames. */
43
+ export type CameraMotionState = {
44
+ /** Last observed Earth-centered, Earth-fixed camera position. */
45
+ position: number[];
46
+ /** Last observed normalized Earth-centered, Earth-fixed camera direction. */
47
+ direction: number[];
48
+ /** Timestamp in milliseconds of the last observed pose change. */
49
+ lastMovementTime: number;
50
+ };
51
+
52
+ /** Updated camera state and elapsed stationary time for a traversal frame. */
53
+ export type CameraMotionUpdate = {
54
+ /** Camera pose to retain for the next traversal. */
55
+ state: CameraMotionState;
56
+ /** Elapsed seconds since the camera pose last changed. */
57
+ timeSinceMovement: number;
58
+ };
59
+
60
+ const scratchVector = new Vector3();
61
+ const scratchPosition = new Vector3();
62
+ const CAMERA_POSITION_EPSILON = 1e-5;
63
+ const CAMERA_DIRECTION_EPSILON = 1e-7;
64
+ const cullingVolume = new CullingVolume([
65
+ new Plane(),
66
+ new Plane(),
67
+ new Plane(),
68
+ new Plane(),
69
+ new Plane(),
70
+ new Plane()
71
+ ]);
72
+
73
+ /**
74
+ * Extracts a frame state appropriate for tile culling from a structural deck.gl viewport.
75
+ *
76
+ * `options.timeSinceCameraMovement` can be supplied by a caller that retains camera pose across
77
+ * calls. It defaults to a stationary camera so standalone callers do not defer requests.
78
+ *
79
+ * @param viewport - Geospatial viewport supplying camera and projection measurements.
80
+ * @param frameNumber - Current tileset traversal frame number.
81
+ * @param options - Additional camera and projection measurements for traversal.
82
+ * @returns Frame state used for visibility, SSE, and request-priority calculations.
83
+ */
84
+ export function getFrameState(
85
+ viewport: GeospatialViewport,
86
+ frameNumber: number,
87
+ options: GetFrameStateOptions = {}
88
+ ): FrameState {
89
+ // Traverse and and request. Update _selectedTiles so that we know what to render.
90
+ // Traverse and and request. Update _selectedTiles so that we know what to render.
91
+ const {cameraUp, height} = viewport;
92
+ const cameraDirection = viewport.cameraDirection as number[] | undefined;
93
+ const {metersPerUnit} = viewport.distanceScales;
94
+
95
+ // TODO - Ellipsoid.eastNorthUpToFixedFrame() breaks on raw array, create a Vector.
96
+ // TODO - Ellipsoid.eastNorthUpToFixedFrame() takes a cartesian, is that intuitive?
97
+ const viewportCenterCartesian = worldToCartesian(viewport, viewport.center);
98
+ const enuToFixedTransform = Ellipsoid.WGS84.eastNorthUpToFixedFrame(viewportCenterCartesian);
99
+
100
+ const cameraPositionCartographic = viewport.unprojectPosition(viewport.cameraPosition);
101
+ const cameraPositionCartesian = Ellipsoid.WGS84.cartographicToCartesian(
102
+ cameraPositionCartographic,
103
+ new Vector3()
104
+ );
105
+
106
+ // These should still be normalized as the transform has scale 1 (goes from meters to meters)
107
+ let cameraDirectionCartesian: Vector3;
108
+ if (cameraDirection) {
109
+ cameraDirectionCartesian = new Vector3(
110
+ // @ts-ignore
111
+ enuToFixedTransform.transformAsVector(new Vector3(cameraDirection).scale(metersPerUnit))
112
+ ).normalize();
113
+ } else {
114
+ // Concrete deck.gl viewports do not expose the legacy structural cameraDirection field.
115
+ // The near-plane center is on the view axis, so its world-space direction is equivalent and
116
+ // works for perspective foveation without coupling loaders.gl to deck.gl internals.
117
+ // getFrustumPlanes is required by traversal but omitted from the minimal structural Viewport
118
+ // type for compatibility with existing application-side declarations.
119
+ // @ts-expect-error Runtime geospatial viewports provide getFrustumPlanes.
120
+ const nearPlane = viewport.getFrustumPlanes().near;
121
+ const nearCenterCommon = closestPointOnPlane(nearPlane, viewport.cameraPosition);
122
+ const nearCenterCartesian = worldToCartesian(viewport, nearCenterCommon);
123
+ cameraDirectionCartesian = new Vector3(nearCenterCartesian)
124
+ .subtract(cameraPositionCartesian)
125
+ .normalize();
126
+ }
127
+ const cameraUpCartesian = new Vector3(
128
+ // @ts-ignore
129
+ enuToFixedTransform.transformAsVector(new Vector3(cameraUp).scale(metersPerUnit))
130
+ ).normalize();
131
+
132
+ commonSpacePlanesToWGS84(viewport);
133
+
134
+ const ViewportClass = viewport.constructor;
135
+ const {longitude, latitude, width, bearing, zoom} = viewport;
136
+ // @ts-ignore
137
+ const topDownViewport = new ViewportClass({
138
+ longitude,
139
+ latitude,
140
+ height,
141
+ width,
142
+ bearing,
143
+ zoom,
144
+ pitch: 0
145
+ });
146
+
147
+ // TODO: make a file/class for frameState and document what needs to be attached to this so that traversal can function
148
+ return {
149
+ camera: {
150
+ position: cameraPositionCartesian,
151
+ direction: cameraDirectionCartesian,
152
+ up: cameraUpCartesian,
153
+ cartographicPosition: cameraPositionCartographic,
154
+ verticalFieldOfView:
155
+ Number.isFinite(viewport.fovy) && Number(viewport.fovy) > 0
156
+ ? (Number(viewport.fovy) * Math.PI) / 180
157
+ : Math.PI / 3,
158
+ timeSinceMovement: options.timeSinceCameraMovement ?? Number.POSITIVE_INFINITY
159
+ },
160
+ viewport,
161
+ topDownViewport,
162
+ height,
163
+ cullingVolume,
164
+ frameNumber, // TODO: This can be the same between updates, what number is unique for between updates?
165
+ sseDenominator: 1.15, // Assumes fovy = 60 degrees
166
+ // Tileset3D fills this immediately before traversal because it depends on the root volume.
167
+ dynamicScreenSpaceErrorDensity: 0
168
+ };
169
+ }
170
+
171
+ /**
172
+ * Updates retained camera motion state and calculates how long the current pose has been stable.
173
+ *
174
+ * The first observation is treated as stationary so initial tileset loading is never delayed.
175
+ * Position and direction are both compared because orbiting in place changes request relevance
176
+ * without necessarily changing the viewport's camera position.
177
+ *
178
+ * @param previousState - Pose retained from the preceding traversal for this viewport.
179
+ * @param position - Current Earth-centered, Earth-fixed camera position.
180
+ * @param direction - Current normalized Earth-centered, Earth-fixed camera direction.
181
+ * @param currentTime - Current monotonic-enough wall-clock time in milliseconds.
182
+ * @returns Updated pose and elapsed stationary time.
183
+ */
184
+ export function updateCameraMotionState(
185
+ previousState: CameraMotionState | undefined,
186
+ position: number[] | Vector3,
187
+ direction: number[] | Vector3,
188
+ currentTime: number
189
+ ): CameraMotionUpdate {
190
+ const positionArray = Array.from(position);
191
+ const directionArray = Array.from(direction);
192
+ const positionChanged = previousState
193
+ ? positionArray.length !== previousState.position.length ||
194
+ positionArray.some(
195
+ (value, index) => Math.abs(value - previousState.position[index]) > CAMERA_POSITION_EPSILON
196
+ )
197
+ : false;
198
+ const directionChanged = previousState
199
+ ? directionArray.length !== previousState.direction.length ||
200
+ directionArray.some(
201
+ (value, index) =>
202
+ Math.abs(value - previousState.direction[index]) > CAMERA_DIRECTION_EPSILON
203
+ )
204
+ : false;
205
+ const lastMovementTime =
206
+ positionChanged || directionChanged
207
+ ? currentTime
208
+ : (previousState?.lastMovementTime ?? Number.NEGATIVE_INFINITY);
209
+
210
+ return {
211
+ state: {
212
+ position: positionArray,
213
+ direction: directionArray,
214
+ lastMovementTime
215
+ },
216
+ timeSinceMovement: Math.max((currentTime - lastMovementTime) / 1000, 0)
217
+ };
218
+ }
219
+
220
+ /**
221
+ * Limit `tiles` array length with `maximumTilesSelected` number.
222
+ * The criteria for this filtering is distance of a tile center
223
+ * to the `frameState.viewport`'s longitude and latitude
224
+ * @param tiles - tiles array to filter
225
+ * @param frameState - frameState to calculate distances
226
+ * @param maximumTilesSelected - maximal amount of tiles in the output array
227
+ * @returns new tiles array
228
+ */
229
+ export function limitSelectedTiles(
230
+ tiles: Tile3D[],
231
+ frameState: FrameState,
232
+ maximumTilesSelected: number
233
+ ): [Tile3D[], Tile3D[]] {
234
+ if (maximumTilesSelected === 0 || tiles.length <= maximumTilesSelected) {
235
+ return [tiles, []];
236
+ }
237
+ // Accumulate distances in couples array: [tileIndex: number, distanceToViewport: number]
238
+ const tuples: [number, number][] = [];
239
+ const {longitude: viewportLongitude, latitude: viewportLatitude} = frameState.viewport;
240
+ for (const [index, tile] of tiles.entries()) {
241
+ const [longitude, latitude] = tile.header.mbs;
242
+ const deltaLon = Math.abs(viewportLongitude - longitude);
243
+ const deltaLat = Math.abs(viewportLatitude - latitude);
244
+ const distance = Math.sqrt(deltaLat * deltaLat + deltaLon * deltaLon);
245
+ tuples.push([index, distance]);
246
+ }
247
+ const tuplesSorted = tuples.sort((a, b) => a[1] - b[1]);
248
+ const selectedTiles: Tile3D[] = [];
249
+ for (let i = 0; i < maximumTilesSelected; i++) {
250
+ selectedTiles.push(tiles[tuplesSorted[i][0]]);
251
+ }
252
+ const unselectedTiles: Tile3D[] = [];
253
+ for (let i = maximumTilesSelected; i < tuplesSorted.length; i++) {
254
+ unselectedTiles.push(tiles[tuplesSorted[i][0]]);
255
+ }
256
+
257
+ return [selectedTiles, unselectedTiles];
258
+ }
259
+
260
+ function commonSpacePlanesToWGS84(viewport) {
261
+ // Extract frustum planes based on current view.
262
+ const frustumPlanes = viewport.getFrustumPlanes();
263
+
264
+ // Get the near/far plane centers
265
+ const nearCenterCommon = closestPointOnPlane(frustumPlanes.near, viewport.cameraPosition);
266
+ const nearCenterCartesian = worldToCartesian(viewport, nearCenterCommon);
267
+ const cameraCartesian = worldToCartesian(viewport, viewport.cameraPosition, scratchPosition);
268
+
269
+ let i = 0;
270
+ cullingVolume.planes[i++].fromPointNormal(
271
+ nearCenterCartesian,
272
+ scratchVector.copy(nearCenterCartesian).subtract(cameraCartesian)
273
+ );
274
+
275
+ for (const dir in frustumPlanes) {
276
+ if (dir === 'near') {
277
+ continue; // eslint-disable-line no-continue
278
+ }
279
+ const plane = frustumPlanes[dir];
280
+ const posCommon = closestPointOnPlane(plane, nearCenterCommon, scratchPosition);
281
+ const cartesianPos = worldToCartesian(viewport, posCommon, scratchPosition);
282
+
283
+ cullingVolume.planes[i++].fromPointNormal(
284
+ cartesianPos,
285
+ // Want the normal to point into the frustum since that's what culling expects
286
+ scratchVector.copy(nearCenterCartesian).subtract(cartesianPos)
287
+ );
288
+ }
289
+ }
290
+
291
+ function closestPointOnPlane(
292
+ plane: {distance: number; normal: Vector3},
293
+ refPoint: number[] | Vector3,
294
+ out: Vector3 = new Vector3()
295
+ ): Vector3 {
296
+ const distanceToRef = plane.normal.dot(refPoint);
297
+ out
298
+ .copy(plane.normal)
299
+ .scale(plane.distance - distanceToRef)
300
+ .add(refPoint);
301
+ return out;
302
+ }
303
+
304
+ function worldToCartesian(
305
+ viewport: Viewport,
306
+ point: number[] | Vector3,
307
+ out: Vector3 = new Vector3()
308
+ ): Vector3 {
309
+ const cartographicPos = viewport.unprojectPosition(point);
310
+ return Ellipsoid.WGS84.cartographicToCartesian(cartographicPos, out);
311
+ }
@@ -4,7 +4,7 @@
4
4
 
5
5
  import {Matrix4, Vector3} from '@math.gl/core';
6
6
  import {Ellipsoid} from '@math.gl/geospatial';
7
- import {Tile3D} from '../tile-3d';
7
+ import {Tile3D} from '../common/tile-3d';
8
8
  import {FrameState} from './frame-state';
9
9
 
10
10
  const cameraPositionCartesian = new Vector3();
@@ -25,7 +25,7 @@ const cartesianToEnuMatrix = new Matrix4();
25
25
  * @returns
26
26
  */
27
27
  export function getLodStatus(tile: Tile3D, frameState: FrameState): 'DIG' | 'OUT' | 'DRAW' {
28
- if (tile.lodMetricValue === 0 || isNaN(tile.lodMetricValue)) {
28
+ if (tile.lodMetricValue === 0 || Number.isNaN(tile.lodMetricValue)) {
29
29
  return 'DIG';
30
30
  }
31
31
  const screenSize = 2 * getProjectedRadius(tile, frameState);
@@ -0,0 +1,370 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ // This file is derived from the Cesium code base under Apache 2 license
6
+ // See LICENSE.md and https://github.com/AnalyticalGraphicsInc/cesium/blob/master/LICENSE.md
7
+
8
+ import {Matrix4, Quaternion, Vector3, clamp} from '@math.gl/core';
9
+ import {Ellipsoid} from '@math.gl/geospatial';
10
+ import type {Tile3D} from '../common/tile-3d';
11
+ import type {FrameState} from './frame-state';
12
+
13
+ /** Options that control the perspective dynamic screen-space error optimization. */
14
+ export type DynamicScreenSpaceErrorOptions = {
15
+ /** Base fog density used to reduce distant, horizon-facing screen-space error. */
16
+ dynamicScreenSpaceErrorDensity: number;
17
+ /** Maximum number of logical pixels subtracted from perspective screen-space error. */
18
+ dynamicScreenSpaceErrorFactor: number;
19
+ /** Fraction of the tileset height at which the optimization begins to fade. */
20
+ dynamicScreenSpaceErrorHeightFalloff: number;
21
+ };
22
+
23
+ type TileBoundingVolumeHeader = {
24
+ box?: number[];
25
+ region?: number[];
26
+ sphere?: number[];
27
+ };
28
+
29
+ type TilesetHeightRange = {
30
+ cameraHeight: number;
31
+ minimumHeight: number;
32
+ maximumHeight: number;
33
+ cameraDirection: Vector3;
34
+ upDirection: Vector3;
35
+ };
36
+
37
+ const scratchPositionNormal = new Vector3();
38
+ const scratchCartographic = new Vector3();
39
+ const scratchCenter = new Vector3();
40
+ const scratchPosition = new Vector3();
41
+ const scratchDirection = new Vector3();
42
+ const scratchBoxXAxis = new Vector3();
43
+ const scratchBoxYAxis = new Vector3();
44
+ const scratchBoxZAxis = new Vector3();
45
+ const scratchBoxQuaternion = new Quaternion();
46
+ const unitZDirection = new Vector3(0, 0, 1);
47
+
48
+ /**
49
+ * Refreshes the root transform before calculating transform-dependent dynamic SSE density.
50
+ *
51
+ * Traversal normally updates transforms while visiting tile visibility, but density is calculated
52
+ * immediately before traversal. Performing the root update at that earlier boundary keeps local
53
+ * box and sphere height falloff synchronized with an animated tileset model matrix in the same
54
+ * frame. {@link Tile3D._updateTransform} also recalculates world-space geometric error from its raw
55
+ * source value, so repeated calls do not compound scale.
56
+ *
57
+ * @param root - Root tile whose computed transform feeds dynamic SSE height calculations.
58
+ * @param modelMatrix - Current tileset-to-world transform for this frame.
59
+ */
60
+ export function updateRootTransformForDynamicScreenSpaceError(
61
+ root: Tile3D,
62
+ modelMatrix: Matrix4
63
+ ): void {
64
+ root._updateTransform(modelMatrix);
65
+ }
66
+
67
+ /**
68
+ * Calculates the effective dynamic screen-space error density for one viewport and frame.
69
+ *
70
+ * Dynamic SSE is a perspective-only optimization for street-level views. It reduces refinement
71
+ * for distant tiles near the horizon, where loading every geometric detail is expensive and has
72
+ * limited visual benefit. Density fades to zero as the camera rises above the tileset or points
73
+ * vertically. The returned value belongs to the supplied frame state; callers must not reuse it
74
+ * for another viewport because camera height and direction are view dependent.
75
+ *
76
+ * @param root - Root tile whose transform and source bounding volume define the tileset space.
77
+ * @param frameState - Current camera measurements in WGS84 and logical viewport space.
78
+ * @param options - Dynamic SSE density and height-falloff controls.
79
+ * @returns Effective fog density for this viewport, or zero when it cannot be calculated.
80
+ */
81
+ export function calculateDynamicScreenSpaceErrorDensity(
82
+ root: Tile3D,
83
+ frameState: FrameState,
84
+ options: DynamicScreenSpaceErrorOptions
85
+ ): number {
86
+ if (
87
+ !Number.isFinite(options.dynamicScreenSpaceErrorDensity) ||
88
+ options.dynamicScreenSpaceErrorDensity <= 0
89
+ ) {
90
+ return 0;
91
+ }
92
+
93
+ const boundingVolumeHeader = getContentBoundingVolumeHeader(root);
94
+ const heightRange = boundingVolumeHeader.region
95
+ ? getRegionHeightRange(boundingVolumeHeader.region, frameState)
96
+ : getCartesianHeightRange(root, boundingVolumeHeader, frameState);
97
+
98
+ if (!heightRange) {
99
+ return 0;
100
+ }
101
+
102
+ const heightFalloff = clamp(options.dynamicScreenSpaceErrorHeightFalloff, 0, 1);
103
+ const heightClose =
104
+ heightRange.minimumHeight +
105
+ (heightRange.maximumHeight - heightRange.minimumHeight) * heightFalloff;
106
+ const heightSpan = heightRange.maximumHeight - heightClose;
107
+ const heightPercentage =
108
+ heightSpan > 0
109
+ ? clamp((heightRange.cameraHeight - heightClose) / heightSpan, 0, 1)
110
+ : heightRange.cameraHeight >= heightRange.maximumHeight
111
+ ? 1
112
+ : 0;
113
+
114
+ // A horizontal view has a dot product near zero and receives the strongest optimization.
115
+ // A vertical view has a dot product near one and keeps the unmodified perspective SSE.
116
+ const verticalAlignment = clamp(
117
+ Math.abs(heightRange.cameraDirection.dot(heightRange.upDirection)),
118
+ 0,
119
+ 1
120
+ );
121
+ const horizonFactor = (1 - verticalAlignment) * (1 - heightPercentage);
122
+
123
+ return options.dynamicScreenSpaceErrorDensity * horizonFactor;
124
+ }
125
+
126
+ /**
127
+ * Calculates exponential fog strength for a camera distance and density.
128
+ * @param distanceToCamera - Distance from the camera to the tile bounding volume in meters.
129
+ * @param density - Effective dynamic SSE density in inverse meters.
130
+ * @returns Unitless strength in the inclusive range from zero to one.
131
+ */
132
+ export function getDynamicScreenSpaceErrorFog(distanceToCamera: number, density: number): number {
133
+ const scalar = distanceToCamera * density;
134
+ return 1 - Math.exp(-(scalar * scalar));
135
+ }
136
+
137
+ /**
138
+ * Calculates the perspective SSE reduction for a tile.
139
+ * @param distanceToCamera - Distance from the camera to the tile bounding volume in meters.
140
+ * @param density - View-dependent fog density calculated for the current frame.
141
+ * @param factor - Maximum reduction in logical/CSS pixels.
142
+ * @returns Number of logical pixels to subtract from perspective SSE.
143
+ */
144
+ export function getDynamicScreenSpaceError(
145
+ distanceToCamera: number,
146
+ density: number,
147
+ factor: number
148
+ ): number {
149
+ if (density <= 0 || factor <= 0 || !Number.isFinite(density) || !Number.isFinite(factor)) {
150
+ return 0;
151
+ }
152
+
153
+ return getDynamicScreenSpaceErrorFog(distanceToCamera, density) * factor;
154
+ }
155
+
156
+ /**
157
+ * Calculates 3D Tiles screen-space error (SSE) for the current projection.
158
+ *
159
+ * Perspective SSE projects the tile's world-space geometric error through the existing viewport
160
+ * height and frustum denominator. Dynamic SSE may then reduce distant horizon refinement using
161
+ * the density calculated specifically for this frame and viewport. Orthographic projection has
162
+ * no perspective distance falloff, so its error is divided directly by the viewport's world-space
163
+ * meters per logical pixel and is never adjusted by perspective dynamic SSE.
164
+ *
165
+ * @param tile - Tile containing the transform-scaled geometric error and camera distance.
166
+ * @param frameState - Current camera and viewport measurements.
167
+ * @param useParentLodMetric - Whether request prioritization should use the parent's error.
168
+ * @param viewportHeightFraction - Fraction of viewport height represented by the priority pass.
169
+ * @returns Estimated error in logical/CSS pixels.
170
+ */
171
+ export function getTiles3DScreenSpaceError(
172
+ tile: Tile3D,
173
+ frameState: FrameState,
174
+ useParentLodMetric: boolean,
175
+ viewportHeightFraction = 1
176
+ ): number {
177
+ const tileset = tile.tileset;
178
+ const parentLodMetricValue = (tile.parent && tile.parent.lodMetricValue) || tile.lodMetricValue;
179
+ const lodMetricValue = useParentLodMetric ? parentLodMetricValue : tile.lodMetricValue;
180
+
181
+ // Leaf tiles do not have any error so save the computation.
182
+ if (lodMetricValue === 0) {
183
+ return 0;
184
+ }
185
+
186
+ const {viewDistanceScale} = tileset.options;
187
+ const lodScale = viewDistanceScale || 1;
188
+ const heightFraction = Number.isFinite(viewportHeightFraction)
189
+ ? Math.max(viewportHeightFraction, 0)
190
+ : 1;
191
+ const orthographicError = getOrthographicScreenSpaceError(
192
+ lodMetricValue,
193
+ frameState,
194
+ lodScale,
195
+ heightFraction
196
+ );
197
+ if (orthographicError !== null) {
198
+ return orthographicError;
199
+ }
200
+
201
+ // Avoid divide by zero when viewer is inside the tile.
202
+ const distanceToCamera = Math.max(tile._distanceToCamera, 1e-7);
203
+ const {height, sseDenominator} = frameState;
204
+ let screenSpaceError =
205
+ (lodMetricValue * height * heightFraction * lodScale) / (distanceToCamera * sseDenominator);
206
+
207
+ if (tileset.options.dynamicScreenSpaceError && !frameState.viewport.orthographic) {
208
+ screenSpaceError -= getDynamicScreenSpaceError(
209
+ distanceToCamera,
210
+ frameState.dynamicScreenSpaceErrorDensity,
211
+ tileset.options.dynamicScreenSpaceErrorFactor
212
+ );
213
+ }
214
+
215
+ return screenSpaceError;
216
+ }
217
+
218
+ /**
219
+ * Calculates orthographic SSE when the viewport provides a valid logical-pixel scale.
220
+ *
221
+ * deck.gl viewport dimensions and `metersPerPixel` use logical/CSS pixels. Applying the browser's
222
+ * device pixel ratio again would therefore double-correct the result and select different LODs on
223
+ * otherwise identical displays. If a structural third-party viewport does not expose a usable
224
+ * scale, `null` tells the caller to retain the established perspective-compatible fallback.
225
+ *
226
+ * @param lodMetricValue - Transform-scaled geometric error in world-space meters.
227
+ * @param frameState - Current frame state containing the viewport.
228
+ * @param lodScale - Application refinement scale from `viewDistanceScale`.
229
+ * @param viewportHeightFraction - Fraction of logical viewport height represented by the pass.
230
+ * @returns SSE in logical pixels, or `null` when orthographic SSE cannot be calculated.
231
+ */
232
+ function getOrthographicScreenSpaceError(
233
+ lodMetricValue: number,
234
+ frameState: FrameState,
235
+ lodScale: number,
236
+ viewportHeightFraction: number
237
+ ): number | null {
238
+ const viewport = frameState.viewport;
239
+ if (!viewport?.orthographic) {
240
+ return null;
241
+ }
242
+
243
+ const metersPerPixel = viewport.metersPerPixel;
244
+ if (!Number.isFinite(metersPerPixel) || !metersPerPixel || metersPerPixel <= 0) {
245
+ return null;
246
+ }
247
+
248
+ return (lodMetricValue * lodScale * viewportHeightFraction) / metersPerPixel;
249
+ }
250
+
251
+ /** Returns the content bounding volume header, falling back to the traversal volume. */
252
+ function getContentBoundingVolumeHeader(root: Tile3D): TileBoundingVolumeHeader {
253
+ return root.header.content?.boundingVolume || root.header.boundingVolume;
254
+ }
255
+
256
+ /** Calculates camera and tileset heights for a geodetic region bounding volume. */
257
+ function getRegionHeightRange(region: number[], frameState: FrameState): TilesetHeightRange {
258
+ const upDirection = scratchPositionNormal.copy(frameState.camera.position).normalize();
259
+ const cameraDirection = scratchDirection.copy(frameState.camera.direction).normalize();
260
+
261
+ return {
262
+ cameraHeight: frameState.camera.cartographicPosition[2],
263
+ minimumHeight: region[4],
264
+ maximumHeight: region[5],
265
+ cameraDirection,
266
+ upDirection
267
+ };
268
+ }
269
+
270
+ /**
271
+ * Calculates camera and tileset heights for box and sphere volumes.
272
+ *
273
+ * The root transform is inverted so that both camera height and source volume dimensions use the
274
+ * same coordinate system. Small coordinates are treated as local z-up. Earth-scale coordinates
275
+ * are treated as WGS84 and use cartographic height, matching Cesium's conservative approximation.
276
+ */
277
+ function getCartesianHeightRange(
278
+ root: Tile3D,
279
+ boundingVolumeHeader: TileBoundingVolumeHeader,
280
+ frameState: FrameState
281
+ ): TilesetHeightRange | null {
282
+ const halfHeight = getBoundingVolumeCenterAndHalfHeight(boundingVolumeHeader, scratchCenter);
283
+ if (halfHeight === null) {
284
+ return null;
285
+ }
286
+
287
+ const localTransform = new Matrix4(root.computedTransform).invert();
288
+ const ellipsoid = root.tileset.ellipsoid as Ellipsoid;
289
+ const centerMagnitude = Math.hypot(scratchCenter[0], scratchCenter[1], scratchCenter[2]);
290
+
291
+ if (centerMagnitude > ellipsoid.minimumRadius) {
292
+ const centerCartographic = ellipsoid.cartesianToCartographic(
293
+ scratchCenter,
294
+ scratchCartographic
295
+ );
296
+ const upDirection = scratchPositionNormal.copy(frameState.camera.position).normalize();
297
+ const cameraDirection = scratchDirection.copy(frameState.camera.direction).normalize();
298
+
299
+ return {
300
+ cameraHeight: frameState.camera.cartographicPosition[2],
301
+ minimumHeight: 0,
302
+ maximumHeight: centerCartographic[2] * 2,
303
+ cameraDirection,
304
+ upDirection
305
+ };
306
+ }
307
+
308
+ localTransform.transformAsPoint(frameState.camera.position, scratchPosition);
309
+ localTransform.transformAsVector(frameState.camera.direction, scratchDirection);
310
+ scratchDirection.normalize();
311
+
312
+ return {
313
+ cameraHeight: scratchPosition[2],
314
+ minimumHeight: scratchCenter[2] - halfHeight,
315
+ maximumHeight: scratchCenter[2] + halfHeight,
316
+ cameraDirection: scratchDirection,
317
+ upDirection: unitZDirection
318
+ };
319
+ }
320
+
321
+ /**
322
+ * Copies the source volume center and returns its conservative z-up half-height.
323
+ * @param boundingVolumeHeader - Root content or traversal volume from tileset JSON.
324
+ * @param result - Scratch vector that receives the source-space center.
325
+ * @returns Half-height in source-space meters, or `null` for an unsupported volume.
326
+ */
327
+ function getBoundingVolumeCenterAndHalfHeight(
328
+ boundingVolumeHeader: TileBoundingVolumeHeader,
329
+ result: Vector3
330
+ ): number | null {
331
+ if (boundingVolumeHeader.box) {
332
+ const box = boundingVolumeHeader.box;
333
+ result.set(box[0], box[1], box[2]);
334
+ return getBoxHalfHeight(box);
335
+ }
336
+
337
+ if (boundingVolumeHeader.sphere) {
338
+ const sphere = boundingVolumeHeader.sphere;
339
+ result.set(sphere[0], sphere[1], sphere[2]);
340
+ return sphere[3];
341
+ }
342
+
343
+ return null;
344
+ }
345
+
346
+ /**
347
+ * Returns the conservative source-z extent of an oriented box.
348
+ *
349
+ * A 12-component 3D Tiles box stores three half-axis vectors, so every vector can contribute to
350
+ * vertical height after rotation. The alternate 10-component representation stores half sizes and
351
+ * a quaternion; rotating its three scaled basis vectors produces the same sum of absolute z
352
+ * projections. Using only the nominal z axis would underestimate tilted volumes and fade dynamic
353
+ * SSE too early while the camera is still within the tileset's height range.
354
+ *
355
+ * @param box - Source-space oriented box in half-axis or half-size/quaternion form.
356
+ * @returns Conservative source-z half-height in the box coordinate system's distance units.
357
+ */
358
+ function getBoxHalfHeight(box: number[]): number {
359
+ if (box.length === 10) {
360
+ scratchBoxQuaternion.fromArray(box, 6);
361
+ scratchBoxXAxis.set(box[3], 0, 0).transformByQuaternion(scratchBoxQuaternion);
362
+ scratchBoxYAxis.set(0, box[4], 0).transformByQuaternion(scratchBoxQuaternion);
363
+ scratchBoxZAxis.set(0, 0, box[5]).transformByQuaternion(scratchBoxQuaternion);
364
+ return (
365
+ Math.abs(scratchBoxXAxis[2]) + Math.abs(scratchBoxYAxis[2]) + Math.abs(scratchBoxZAxis[2])
366
+ );
367
+ }
368
+
369
+ return Math.abs(box[5]) + Math.abs(box[8]) + Math.abs(box[11]);
370
+ }