@developmentseed/deck.gl-raster 0.7.0-beta.1 → 0.8.0-beta.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 (94) hide show
  1. package/dist/fp64.d.ts +18 -0
  2. package/dist/fp64.d.ts.map +1 -0
  3. package/dist/fp64.js +28 -0
  4. package/dist/fp64.js.map +1 -0
  5. package/dist/globe-grid-mesh.d.ts +30 -0
  6. package/dist/globe-grid-mesh.d.ts.map +1 -0
  7. package/dist/globe-grid-mesh.js +67 -0
  8. package/dist/globe-grid-mesh.js.map +1 -0
  9. package/dist/gpu-modules/cutline-bbox.d.ts +26 -40
  10. package/dist/gpu-modules/cutline-bbox.d.ts.map +1 -1
  11. package/dist/gpu-modules/cutline-bbox.js +24 -53
  12. package/dist/gpu-modules/cutline-bbox.js.map +1 -1
  13. package/dist/gpu-modules/index.d.ts +1 -1
  14. package/dist/gpu-modules/index.d.ts.map +1 -1
  15. package/dist/gpu-modules/index.js +1 -1
  16. package/dist/gpu-modules/index.js.map +1 -1
  17. package/dist/index.d.ts +4 -4
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +4 -2
  20. package/dist/index.js.map +1 -1
  21. package/dist/layer-utils.d.ts +2 -2
  22. package/dist/layer-utils.d.ts.map +1 -1
  23. package/dist/layer-utils.js.map +1 -1
  24. package/dist/mesh-layer/mesh-layer-fragment.glsl.d.ts +1 -1
  25. package/dist/mesh-layer/mesh-layer-fragment.glsl.js +1 -1
  26. package/dist/mesh-layer/mesh-layer-vertex.glsl.d.ts +3 -0
  27. package/dist/mesh-layer/mesh-layer-vertex.glsl.d.ts.map +1 -0
  28. package/dist/mesh-layer/mesh-layer-vertex.glsl.js +90 -0
  29. package/dist/mesh-layer/mesh-layer-vertex.glsl.js.map +1 -0
  30. package/dist/mesh-layer/mesh-layer.d.ts +31 -5
  31. package/dist/mesh-layer/mesh-layer.d.ts.map +1 -1
  32. package/dist/mesh-layer/mesh-layer.js +67 -3
  33. package/dist/mesh-layer/mesh-layer.js.map +1 -1
  34. package/dist/multi-raster-tileset/index.d.ts +2 -2
  35. package/dist/multi-raster-tileset/index.d.ts.map +1 -1
  36. package/dist/multi-raster-tileset/index.js +1 -1
  37. package/dist/multi-raster-tileset/index.js.map +1 -1
  38. package/dist/multi-raster-tileset/multi-tileset-descriptor.d.ts +21 -21
  39. package/dist/multi-raster-tileset/multi-tileset-descriptor.d.ts.map +1 -1
  40. package/dist/multi-raster-tileset/multi-tileset-descriptor.js +11 -11
  41. package/dist/multi-raster-tileset/multi-tileset-descriptor.js.map +1 -1
  42. package/dist/multi-raster-tileset/secondary-tile-resolver.d.ts +4 -4
  43. package/dist/multi-raster-tileset/secondary-tile-resolver.d.ts.map +1 -1
  44. package/dist/multi-raster-tileset/secondary-tile-resolver.js +2 -2
  45. package/dist/multi-raster-tileset/secondary-tile-resolver.js.map +1 -1
  46. package/dist/raster-layer.d.ts +39 -4
  47. package/dist/raster-layer.d.ts.map +1 -1
  48. package/dist/raster-layer.js +51 -35
  49. package/dist/raster-layer.js.map +1 -1
  50. package/dist/raster-tile-layer/raster-tile-layer.d.ts +6 -6
  51. package/dist/raster-tile-layer/raster-tile-layer.d.ts.map +1 -1
  52. package/dist/raster-tile-layer/raster-tile-layer.js +40 -31
  53. package/dist/raster-tile-layer/raster-tile-layer.js.map +1 -1
  54. package/dist/raster-tileset/affine-tileset-level.d.ts +4 -4
  55. package/dist/raster-tileset/affine-tileset-level.d.ts.map +1 -1
  56. package/dist/raster-tileset/affine-tileset-level.js +2 -2
  57. package/dist/raster-tileset/affine-tileset.d.ts +3 -3
  58. package/dist/raster-tileset/affine-tileset.d.ts.map +1 -1
  59. package/dist/raster-tileset/affine-tileset.js +1 -1
  60. package/dist/raster-tileset/bounding-volume-cache.d.ts +11 -4
  61. package/dist/raster-tileset/bounding-volume-cache.d.ts.map +1 -1
  62. package/dist/raster-tileset/bounding-volume-cache.js +13 -4
  63. package/dist/raster-tileset/bounding-volume-cache.js.map +1 -1
  64. package/dist/raster-tileset/index.d.ts +3 -2
  65. package/dist/raster-tileset/index.d.ts.map +1 -1
  66. package/dist/raster-tileset/index.js +1 -0
  67. package/dist/raster-tileset/index.js.map +1 -1
  68. package/dist/raster-tileset/raster-tile-traversal.d.ts +68 -13
  69. package/dist/raster-tileset/raster-tile-traversal.d.ts.map +1 -1
  70. package/dist/raster-tileset/raster-tile-traversal.js +240 -35
  71. package/dist/raster-tileset/raster-tile-traversal.js.map +1 -1
  72. package/dist/raster-tileset/raster-tileset-2d.d.ts +64 -6
  73. package/dist/raster-tileset/raster-tileset-2d.d.ts.map +1 -1
  74. package/dist/raster-tileset/raster-tileset-2d.js +75 -2
  75. package/dist/raster-tileset/raster-tileset-2d.js.map +1 -1
  76. package/dist/raster-tileset/sort-by-distance.d.ts +41 -0
  77. package/dist/raster-tileset/sort-by-distance.d.ts.map +1 -0
  78. package/dist/raster-tileset/sort-by-distance.js +72 -0
  79. package/dist/raster-tileset/sort-by-distance.js.map +1 -0
  80. package/dist/raster-tileset/tile-matrix-set.d.ts +4 -4
  81. package/dist/raster-tileset/tile-matrix-set.d.ts.map +1 -1
  82. package/dist/raster-tileset/tile-matrix-set.js +1 -1
  83. package/dist/raster-tileset/tile-matrix-set.js.map +1 -1
  84. package/dist/raster-tileset/tileset-interface.d.ts +3 -3
  85. package/dist/raster-tileset/tileset-interface.d.ts.map +1 -1
  86. package/dist/raster-tileset/web-mercator-clamp.d.ts +29 -0
  87. package/dist/raster-tileset/web-mercator-clamp.d.ts.map +1 -0
  88. package/dist/raster-tileset/web-mercator-clamp.js +54 -0
  89. package/dist/raster-tileset/web-mercator-clamp.js.map +1 -0
  90. package/package.json +7 -7
  91. package/dist/raster-tile-layer/constants.d.ts +0 -11
  92. package/dist/raster-tile-layer/constants.d.ts.map +0 -1
  93. package/dist/raster-tile-layer/constants.js +0 -11
  94. package/dist/raster-tile-layer/constants.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/raster-tileset/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,YAAY,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAC;AAC3E,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,YAAY,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,YAAY,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC9E,YAAY,EACV,MAAM,EACN,YAAY,EACZ,OAAO,EACP,kBAAkB,GACnB,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/raster-tileset/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,YAAY,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAC;AAC3E,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,YAAY,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,qCAAqC,EAAE,MAAM,uBAAuB,CAAC;AAC9E,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,YAAY,EACV,uBAAuB,EACvB,kBAAkB,GACnB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EACV,MAAM,EACN,YAAY,EACZ,OAAO,EACP,kBAAkB,GACnB,MAAM,YAAY,CAAC"}
@@ -1,5 +1,6 @@
1
1
  export { AffineTileset } from "./affine-tileset.js";
2
2
  export { AffineTilesetLevel } from "./affine-tileset-level.js";
3
3
  export { RasterTileset2D } from "./raster-tileset-2d.js";
4
+ export { sortItemsByDistanceFromViewportCenter } from "./sort-by-distance.js";
4
5
  export { TileMatrixSetAdaptor } from "./tile-matrix-set.js";
5
6
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/raster-tileset/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAE/D,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/raster-tileset/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAE/D,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,qCAAqC,EAAE,MAAM,uBAAuB,CAAC;AAC9E,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC"}
@@ -13,14 +13,13 @@
13
13
  * The result is a set of tiles at varying zoom levels that efficiently
14
14
  * cover the visible area with appropriate detail.
15
15
  *
16
- * The traversal is driven by a {@link TilesetDescriptor}, which abstracts over
16
+ * The traversal is driven by a {@link RasterTilesetDescriptor}, which abstracts over
17
17
  * both OGC TileMatrixSet grids and Zarr multiscale pyramids.
18
18
  */
19
19
  import type { Viewport } from "@deck.gl/core";
20
- import type { OrientedBoundingBox } from "@math.gl/culling";
21
- import { CullingVolume } from "@math.gl/culling";
20
+ import { CullingVolume, OrientedBoundingBox } from "@math.gl/culling";
22
21
  import { BoundingVolumeCache } from "./bounding-volume-cache.js";
23
- import type { TilesetDescriptor, TilesetLevel } from "./tileset-interface.js";
22
+ import type { RasterTilesetDescriptor, RasterTilesetLevel } from "./tileset-interface.js";
24
23
  import type { Bounds, TileIndex, ZRange } from "./types.js";
25
24
  /**
26
25
  * Raster Tile Node - represents a single tile in a tileset pyramid.
@@ -29,8 +28,8 @@ import type { Bounds, TileIndex, ZRange } from "./types.js";
29
28
  *
30
29
  * This node class uses the following coordinate system:
31
30
  *
32
- * - x: tile column (0 to TilesetLevel.matrixWidth, left to right)
33
- * - y: tile row (0 to TilesetLevel.matrixHeight, top to bottom)
31
+ * - x: tile column (0 to RasterTilesetLevel.matrixWidth, left to right)
32
+ * - y: tile row (0 to RasterTilesetLevel.matrixHeight, top to bottom)
34
33
  * - z: overview level. This assumes ordering where: 0 = coarsest, higher = finer
35
34
  */
36
35
  export declare class RasterTileNode {
@@ -57,17 +56,17 @@ export declare class RasterTileNode {
57
56
  /** A cache of the children of this node. */
58
57
  private _children?;
59
58
  constructor(x: number, y: number, z: number, { descriptor }: {
60
- descriptor: TilesetDescriptor;
59
+ descriptor: RasterTilesetDescriptor;
61
60
  });
62
61
  /** Get the level info for this tile's z index. */
63
- get level(): TilesetLevel;
62
+ get level(): RasterTilesetLevel;
64
63
  /** Get the children of this node.
65
64
  *
66
65
  * Find all tiles at level this.z + 1 whose spatial extent overlaps this tile.
67
66
  *
68
67
  * A tileset pyramid is not guaranteed to be a quadtree — it is a stack of
69
68
  * independent grids. We find children by mapping the parent tile's CRS bounds
70
- * into the child grid using {@link TilesetLevel.crsBoundsToTileRange}.
69
+ * into the child grid using {@link RasterTilesetLevel.crsBoundsToTileRange}.
71
70
  */
72
71
  get children(): RasterTileNode[] | null;
73
72
  /**
@@ -106,6 +105,14 @@ export declare class RasterTileNode {
106
105
  * comparison would. See `dev-docs/lod-and-pixel-matching.md` § (A).
107
106
  */
108
107
  pixelRatio: number;
108
+ /**
109
+ * Number of world copies to shift this tile's bounding volume by along
110
+ * common-space X for frustum testing. Default `0` (primary world).
111
+ * Non-zero passes are additive — they may set `selected = true` but
112
+ * never override a previous `true` to `false`. See
113
+ * `dev-docs/world-copies.md`.
114
+ */
115
+ worldOffset?: number;
109
116
  /**
110
117
  * Bounding-volume cache shared by every node in this traversal. Populated
111
118
  * lazily as tiles are visited; reused across `getTileIndices` calls (so
@@ -138,8 +145,23 @@ export declare class RasterTileNode {
138
145
  * volume depends only on `(z, x, y, zRange)` for a given descriptor, so on a
139
146
  * cache hit it is returned without rerunning {@link computeBoundingVolume}'s
140
147
  * proj4 reprojections + oriented-bounding-box fit.
148
+ *
149
+ * For non-zero `worldOffset`, returns a translated copy (center shifted by
150
+ * `worldOffset * TILE_SIZE` along common-space X) without polluting the
151
+ * cache — the cache always stores the offset-0 volume. See
152
+ * `dev-docs/world-copies.md`.
153
+ *
154
+ * @param zRange Elevation `[min, max]` in common-space units.
155
+ * @param project Projection function for Globe view, or `null`
156
+ * for Web Mercator common space.
157
+ * @param boundingVolumeCache Cache keyed by `z/x/y`. Stores the offset-0
158
+ * volume only.
159
+ * @param worldOffset Number of world copies to translate the result
160
+ * by along common-space X. `0` returns the
161
+ * cached offset-0 volume directly. Non-zero
162
+ * values return a fresh translated copy.
141
163
  */
142
- getBoundingVolume(zRange: ZRange, project: ((xyz: number[]) => number[]) | null, boundingVolumeCache: BoundingVolumeCache): {
164
+ getBoundingVolume(zRange: ZRange, project: ((xyz: number[]) => number[]) | null, boundingVolumeCache: BoundingVolumeCache, worldOffset?: number): {
143
165
  boundingVolume: OrientedBoundingBox;
144
166
  commonSpaceBounds: Bounds;
145
167
  };
@@ -157,7 +179,40 @@ export declare class RasterTileNode {
157
179
  *
158
180
  */
159
181
  private _getGenericBoundingVolume;
182
+ /**
183
+ * Globe-view bounding volume: reproject the tile's reference points to WGS84,
184
+ * project them onto the globe sphere (`project` = `viewport.projectPosition`)
185
+ * to build the oriented bounding box used for frustum culling, and separately
186
+ * compute a Web-Mercator-world AABB for the `bounds` pre-filter in
187
+ * {@link update} (which compares against `wgs84Bounds` in mercator world).
188
+ *
189
+ * NOTE: elevation is not modeled on globe yet — reference points are sampled
190
+ * at the surface (z = 0). Flat rasters only. See
191
+ * `dev-docs/specs/2026-05-21-globe-view-design.md`.
192
+ */
193
+ private _getGlobeBoundingVolume;
160
194
  }
195
+ /**
196
+ * Rescale positions from EPSG:3857 into deck.gl's common space
197
+ *
198
+ * Similar to the upstream code here:
199
+ * https://github.com/visgl/deck.gl/blob/b0134f025148b52b91320d16768ab5d14a745328/modules/geo-layers/src/tileset-2d/tile-2d-traversal.ts#L172-L177
200
+ */
201
+ export declare function rescaleEPSG3857ToCommonSpace([x, y]: [number, number]): [
202
+ number,
203
+ number
204
+ ];
205
+ /**
206
+ * Inverse of {@link rescaleEPSG3857ToCommonSpace}: rescale a deck.gl
207
+ * common-space position back into EPSG:3857 meters.
208
+ *
209
+ * Common-space inputs are in-range by construction, so (unlike the forward
210
+ * direction) no latitude clamp is applied.
211
+ */
212
+ export declare function rescaleCommonSpaceToEPSG3857([x, y]: [number, number]): [
213
+ number,
214
+ number
215
+ ];
161
216
  /**
162
217
  * Build the list of root (z=0) `RasterTileNode`s for the traversal.
163
218
  *
@@ -176,19 +231,19 @@ export declare class RasterTileNode {
176
231
  * Exported for unit testing.
177
232
  */
178
233
  export declare function createRootTiles(opts: {
179
- descriptor: TilesetDescriptor;
234
+ descriptor: RasterTilesetDescriptor;
180
235
  viewport: Pick<Viewport, "getBounds">;
181
236
  datasetWgs84Bounds: Bounds;
182
237
  }): RasterTileNode[];
183
238
  /**
184
239
  * Get tile indices visible in viewport.
185
240
  *
186
- * Uses frustum culling driven by a {@link TilesetDescriptor}, which abstracts
241
+ * Uses frustum culling driven by a {@link RasterTilesetDescriptor}, which abstracts
187
242
  * over OGC TileMatrixSet grids and Zarr multiscale pyramids.
188
243
  *
189
244
  * Overview levels follow the descriptor ordering: index 0 = coarsest, higher = finer.
190
245
  */
191
- export declare function getTileIndices(descriptor: TilesetDescriptor, opts: {
246
+ export declare function getTileIndices(descriptor: RasterTilesetDescriptor, opts: {
192
247
  viewport: Viewport;
193
248
  maxZ: number;
194
249
  zRange: ZRange | null;
@@ -1 +1 @@
1
- {"version":3,"file":"raster-tile-traversal.d.ts","sourceRoot":"","sources":["../../src/raster-tileset/raster-tile-traversal.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"raster-tile-traversal.d.ts","sourceRoot":"","sources":["../../src/raster-tileset/raster-tile-traversal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAI9C,OAAO,EACL,aAAa,EAEb,mBAAmB,EAEpB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,KAAK,EACV,uBAAuB,EACvB,kBAAkB,EACnB,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EACV,MAAM,EAIN,SAAS,EACT,MAAM,EACP,MAAM,YAAY,CAAC;AAqFpB;;;;;;;;;;GAUG;AACH,qBAAa,cAAc;IACzB,yBAAyB;IACzB,CAAC,EAAE,MAAM,CAAC;IAEV,0BAA0B;IAC1B,CAAC,EAAE,MAAM,CAAC;IAEV,uDAAuD;IACvD,CAAC,EAAE,MAAM,CAAC;IAEV,OAAO,CAAC,UAAU,CAA0B;IAE5C;;;;;OAKG;IACH,OAAO,CAAC,YAAY,CAAC,CAAU;IAE/B;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,CAAU;IAE3B,4CAA4C;IAC5C,OAAO,CAAC,SAAS,CAAC,CAA0B;gBAG1C,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,EAAE,UAAU,EAAE,EAAE;QAAE,UAAU,EAAE,uBAAuB,CAAA;KAAE;IAQzD,kDAAkD;IAClD,IAAI,KAAK,IAAI,kBAAkB,CAE9B;IAED;;;;;;;OAOG;IACH,IAAI,QAAQ,IAAI,cAAc,EAAE,GAAG,IAAI,CA+BtC;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,MAAM,CAAC,MAAM,EAAE;QACb,QAAQ,EAAE,QAAQ,CAAC;QAEnB,OAAO,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC;QAE9C,aAAa,EAAE,aAAa,CAAC;QAE7B,eAAe,EAAE,MAAM,CAAC;QACxB,wCAAwC;QACxC,IAAI,EAAE,MAAM,CAAC;QACb,sCAAsC;QACtC,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,wCAAwC;QACxC,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB;;;;;WAKG;QACH,UAAU,EAAE,MAAM,CAAC;QACnB;;;;;;WAMG;QACH,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB;;;;;WAKG;QACH,mBAAmB,EAAE,mBAAmB,CAAC;KAC1C,GAAG,OAAO;IAqHX;;;;;;OAMG;IACH,WAAW,CAAC,MAAM,GAAE,cAAc,EAAO,GAAG,cAAc,EAAE;IAY5D;;;;;;OAMG;IACH,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,GAAG,OAAO;IAUhE;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,iBAAiB,CACf,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,MAAM,EAAE,CAAC,GAAG,IAAI,EAC7C,mBAAmB,EAAE,mBAAmB,EACxC,WAAW,SAAI,GACd;QAAE,cAAc,EAAE,mBAAmB,CAAC;QAAC,iBAAiB,EAAE,MAAM,CAAA;KAAE;IAyBrE;;;;;;OAMG;IACH,OAAO,CAAC,qBAAqB;IAsB7B;;;;OAIG;IACH,OAAO,CAAC,yBAAyB;IA0DjC;;;;;;;;;;OAUG;IACH,OAAO,CAAC,uBAAuB;CAyChC;AAoHD;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG;IACtE,MAAM;IACN,MAAM;CACP,CAWA;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG;IACtE,MAAM;IACN,MAAM;CACP,CAKA;AAYD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE;IACpC,UAAU,EAAE,uBAAuB,CAAC;IACpC,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;IACtC,kBAAkB,EAAE,MAAM,CAAC;CAC5B,GAAG,cAAc,EAAE,CA4CnB;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,UAAU,EAAE,uBAAuB,EACnC,IAAI,EAAE;IACJ,QAAQ,EAAE,QAAQ,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;CAC3C,GACA,SAAS,EAAE,CAoHb"}
@@ -13,12 +13,13 @@
13
13
  * The result is a set of tiles at varying zoom levels that efficiently
14
14
  * cover the visible area with appropriate detail.
15
15
  *
16
- * The traversal is driven by a {@link TilesetDescriptor}, which abstracts over
16
+ * The traversal is driven by a {@link RasterTilesetDescriptor}, which abstracts over
17
17
  * both OGC TileMatrixSet grids and Zarr multiscale pyramids.
18
18
  */
19
- import { _GlobeViewport, assert } from "@deck.gl/core";
19
+ import { _GlobeViewport as GlobeViewport } from "@deck.gl/core";
20
20
  import { transformBounds } from "@developmentseed/proj";
21
- import { CullingVolume, makeOrientedBoundingBoxFromPoints, Plane, } from "@math.gl/culling";
21
+ import { Vector3 } from "@math.gl/core";
22
+ import { CullingVolume, makeOrientedBoundingBoxFromPoints, OrientedBoundingBox, Plane, } from "@math.gl/culling";
22
23
  import { lngLatToWorld, worldToLngLat } from "@math.gl/web-mercator";
23
24
  import { BoundingVolumeCache } from "./bounding-volume-cache.js";
24
25
  /**
@@ -33,6 +34,12 @@ import { BoundingVolumeCache } from "./bounding-volume-cache.js";
33
34
  * bottom-right.
34
35
  */
35
36
  const TILE_SIZE = 512;
37
+ /**
38
+ * Maximum number of world copies to test on each side of the primary world
39
+ * during multi-world tile traversal. Matches upstream
40
+ * `@deck.gl/geo-layers/tile-2d-traversal.ts`.
41
+ */
42
+ const MAX_MAPS = 3;
36
43
  // Reference points used to sample tile boundaries for bounding volume
37
44
  // calculation.
38
45
  //
@@ -64,6 +71,18 @@ const REF_POINTS_9 = REF_POINTS_5.concat([
64
71
  [1, 0.5], // right edge
65
72
  [0.5, 1], // bottom edge
66
73
  ]);
74
+ // For the globe bounding volume: REF_POINTS_9 plus two more points on the
75
+ // horizontal centerline (11 points total). The sphere surface bulges most
76
+ // between samples along the widest span of a tile, so denser sampling there
77
+ // keeps the oriented bounding box from under-enclosing the tile (which would
78
+ // false-cull it). This matches upstream deck.gl's densest reference set, used
79
+ // there only for the coarsest (whole-world) zoom. We use it for every globe
80
+ // tile: a tile never spans more than the whole world, so 11 points always
81
+ // suffice, and per-tile cost is paid once thanks to the bounding-volume cache.
82
+ const REF_POINTS_11 = REF_POINTS_9.concat([
83
+ [0.25, 0.5],
84
+ [0.75, 0.5],
85
+ ]);
67
86
  /** semi-major axis of the WGS84 ellipsoid
68
87
  *
69
88
  * EPSG:3857 also uses the WGS84 datum, so this is used for conversions from
@@ -84,8 +103,8 @@ const MAX_WEB_MERCATOR_LAT = 85.05112877980659;
84
103
  *
85
104
  * This node class uses the following coordinate system:
86
105
  *
87
- * - x: tile column (0 to TilesetLevel.matrixWidth, left to right)
88
- * - y: tile row (0 to TilesetLevel.matrixHeight, top to bottom)
106
+ * - x: tile column (0 to RasterTilesetLevel.matrixWidth, left to right)
107
+ * - y: tile row (0 to RasterTilesetLevel.matrixHeight, top to bottom)
89
108
  * - z: overview level. This assumes ordering where: 0 = coarsest, higher = finer
90
109
  */
91
110
  export class RasterTileNode {
@@ -127,7 +146,7 @@ export class RasterTileNode {
127
146
  *
128
147
  * A tileset pyramid is not guaranteed to be a quadtree — it is a stack of
129
148
  * independent grids. We find children by mapping the parent tile's CRS bounds
130
- * into the child grid using {@link TilesetLevel.crsBoundsToTileRange}.
149
+ * into the child grid using {@link RasterTilesetLevel.crsBoundsToTileRange}.
131
150
  */
132
151
  get children() {
133
152
  if (!this._children) {
@@ -174,16 +193,29 @@ export class RasterTileNode {
174
193
  * @returns true if this tile or any descendant is visible, false otherwise
175
194
  */
176
195
  update(params) {
177
- // Reset state
178
- this.childVisible = false;
179
- this.selected = false;
180
- const { viewport, cullingVolume, elevationBounds, minZ, maxZ = this.descriptor.levels.length - 1, project, bounds, pixelRatio, boundingVolumeCache, } = params;
181
- // Get bounding volume for this tile
182
- const { boundingVolume, commonSpaceBounds } = this.getBoundingVolume(elevationBounds, project, boundingVolumeCache);
196
+ const { viewport, cullingVolume, elevationBounds, minZ, maxZ = this.descriptor.levels.length - 1, project, bounds, pixelRatio, worldOffset = 0, boundingVolumeCache, } = params;
197
+ // Reset per-frame state on the primary pass only. Non-zero worldOffset
198
+ // passes are additive — they can flip selected/childVisible from
199
+ // false true but never the reverse. See dev-docs/world-copies.md.
200
+ if (worldOffset === 0) {
201
+ this.childVisible = false;
202
+ this.selected = false;
203
+ }
204
+ // Get bounding volume for this tile (translated for frustum culling at
205
+ // non-zero worldOffset). `commonSpaceBounds` is the Web-Mercator-world AABB
206
+ // used for the LOD latitude (a worldOffset only shifts X, so latitude is
207
+ // unaffected).
208
+ const { boundingVolume, commonSpaceBounds } = this.getBoundingVolume(elevationBounds, project, boundingVolumeCache, worldOffset);
183
209
  // Step 1: Bounds checking
184
- // If geographic bounds are specified, reject tiles outside those bounds
185
- if (bounds && !this.insideBounds(bounds, commonSpaceBounds)) {
186
- return false;
210
+ // If geographic bounds are specified, reject tiles outside those bounds.
211
+ // The dataset's `bounds` live in primary-world common space, and a tile
212
+ // at `(x, y, z)` represents the same data regardless of which world copy
213
+ // it's drawn in — so always compare against the offset-0 AABB.
214
+ if (bounds) {
215
+ const primaryWorldVolume = this.getBoundingVolume(elevationBounds, project, boundingVolumeCache, 0);
216
+ if (!this.insideBounds(bounds, primaryWorldVolume.commonSpaceBounds)) {
217
+ return false;
218
+ }
187
219
  }
188
220
  // Frustum culling
189
221
  // Test if tile's bounding volume intersects the camera frustum
@@ -197,7 +229,7 @@ export class RasterTileNode {
197
229
  // Only select this tile if no child is visible (prevents overlapping tiles)
198
230
  // "When pitch is low, force selection at maxZ."
199
231
  if (!this.childVisible && this.z >= minZ) {
200
- const metersPerCSSPixel = getMetersPerPixelAtBoundingVolume(boundingVolume, viewport.zoom);
232
+ const metersPerCSSPixel = getMetersPerPixelAtCommonSpaceBounds(commonSpaceBounds, viewport.zoom);
201
233
  const tileMetersPerPixel = this.level.metersPerPixel;
202
234
  // On-screen size of one source pixel, measured in device pixels.
203
235
  // ≤ 1 means the source can fully resolve the rendered framebuffer.
@@ -215,14 +247,22 @@ export class RasterTileNode {
215
247
  // Note that if `this.children` is `null`, then there are no children
216
248
  // available because we're already at the finest tile resolution available
217
249
  if (children && children.length > 0) {
218
- this.selected = false;
250
+ if (worldOffset === 0) {
251
+ this.selected = false;
252
+ }
219
253
  let anyChildVisible = false;
220
254
  for (const child of children) {
221
255
  if (child.update(params)) {
222
256
  anyChildVisible = true;
223
257
  }
224
258
  }
225
- this.childVisible = anyChildVisible;
259
+ // Only set childVisible to true; never override a previous true to
260
+ // false on a subsequent pass. Offset-0 already starts with
261
+ // childVisible=false (reset above), so this preserves the
262
+ // "any pass that finds a visible child wins" semantics.
263
+ if (anyChildVisible) {
264
+ this.childVisible = true;
265
+ }
226
266
  return anyChildVisible;
227
267
  }
228
268
  return true;
@@ -266,15 +306,41 @@ export class RasterTileNode {
266
306
  * volume depends only on `(z, x, y, zRange)` for a given descriptor, so on a
267
307
  * cache hit it is returned without rerunning {@link computeBoundingVolume}'s
268
308
  * proj4 reprojections + oriented-bounding-box fit.
309
+ *
310
+ * For non-zero `worldOffset`, returns a translated copy (center shifted by
311
+ * `worldOffset * TILE_SIZE` along common-space X) without polluting the
312
+ * cache — the cache always stores the offset-0 volume. See
313
+ * `dev-docs/world-copies.md`.
314
+ *
315
+ * @param zRange Elevation `[min, max]` in common-space units.
316
+ * @param project Projection function for Globe view, or `null`
317
+ * for Web Mercator common space.
318
+ * @param boundingVolumeCache Cache keyed by `z/x/y`. Stores the offset-0
319
+ * volume only.
320
+ * @param worldOffset Number of world copies to translate the result
321
+ * by along common-space X. `0` returns the
322
+ * cached offset-0 volume directly. Non-zero
323
+ * values return a fresh translated copy.
269
324
  */
270
- getBoundingVolume(zRange, project, boundingVolumeCache) {
271
- const hit = boundingVolumeCache.get(this.z, this.x, this.y);
272
- if (hit && hit.zRange[0] === zRange[0] && hit.zRange[1] === zRange[1]) {
273
- return hit;
325
+ getBoundingVolume(zRange, project, boundingVolumeCache, worldOffset = 0) {
326
+ const cacheHit = boundingVolumeCache.get(this.z, this.x, this.y);
327
+ // `base` is the tile's volume in the primary world (offset 0). The cache
328
+ // only ever stores the primary-world volume; it is returned as-is for
329
+ // worldOffset 0, or translated below for a non-zero offset.
330
+ let base;
331
+ if (cacheHit &&
332
+ cacheHit.zRange[0] === zRange[0] &&
333
+ cacheHit.zRange[1] === zRange[1]) {
334
+ base = cacheHit;
274
335
  }
275
- const result = this.computeBoundingVolume(zRange, project);
276
- boundingVolumeCache.set(this.z, this.x, this.y, { zRange, ...result });
277
- return result;
336
+ else {
337
+ base = this.computeBoundingVolume(zRange, project);
338
+ boundingVolumeCache.set(this.z, this.x, this.y, { zRange, ...base });
339
+ }
340
+ if (worldOffset === 0) {
341
+ return base;
342
+ }
343
+ return translateBoundingVolume(base, worldOffset * TILE_SIZE);
278
344
  }
279
345
  /**
280
346
  * Compute (without caching) the 3D bounding volume for this tile in deck.gl's
@@ -284,12 +350,10 @@ export class RasterTileNode {
284
350
  * tiling is already in EPSG:3857.
285
351
  */
286
352
  computeBoundingVolume(zRange, project) {
287
- // Case 1: Globe view - need to construct an oriented bounding box from
288
- // reprojected sample points, but also using the `project` param
353
+ // Case 1: Globe view reproject sample points to WGS84 and project them
354
+ // onto the globe sphere with the viewport's `project` function.
289
355
  if (project) {
290
- assert(false, "TODO: implement getBoundingVolume in Globe view");
291
- // Reproject positions to wgs84 instead, then pass them into `project`
292
- // return makeOrientedBoundingBoxFromPoints(refPointPositions);
356
+ return this._getGlobeBoundingVolume(project);
293
357
  }
294
358
  // (Future) Case 2: Web Mercator input image, can directly compute AABB in
295
359
  // common space
@@ -344,6 +408,47 @@ export class RasterTileNode {
344
408
  commonSpaceBounds,
345
409
  };
346
410
  }
411
+ /**
412
+ * Globe-view bounding volume: reproject the tile's reference points to WGS84,
413
+ * project them onto the globe sphere (`project` = `viewport.projectPosition`)
414
+ * to build the oriented bounding box used for frustum culling, and separately
415
+ * compute a Web-Mercator-world AABB for the `bounds` pre-filter in
416
+ * {@link update} (which compares against `wgs84Bounds` in mercator world).
417
+ *
418
+ * NOTE: elevation is not modeled on globe yet — reference points are sampled
419
+ * at the surface (z = 0). Flat rasters only. See
420
+ * `dev-docs/specs/2026-05-21-globe-view-design.md`.
421
+ */
422
+ _getGlobeBoundingVolume(project) {
423
+ const tileCorners = this.level.projectedTileCorners(this.x, this.y);
424
+ const refPointsWgs84 = sampleReferencePointsInWGS84(REF_POINTS_11, tileCorners, this.descriptor.projectTo4326);
425
+ const refPointPositions = [];
426
+ let minX = Number.POSITIVE_INFINITY;
427
+ let minY = Number.POSITIVE_INFINITY;
428
+ let maxX = Number.NEGATIVE_INFINITY;
429
+ let maxY = Number.NEGATIVE_INFINITY;
430
+ for (const [lng, lat] of refPointsWgs84) {
431
+ const projected = project([lng, lat, 0]);
432
+ refPointPositions.push([projected[0], projected[1], projected[2]]);
433
+ const [worldX, worldY] = lngLatToWorld([lng, lat]);
434
+ if (worldX < minX) {
435
+ minX = worldX;
436
+ }
437
+ if (worldY < minY) {
438
+ minY = worldY;
439
+ }
440
+ if (worldX > maxX) {
441
+ maxX = worldX;
442
+ }
443
+ if (worldY > maxY) {
444
+ maxY = worldY;
445
+ }
446
+ }
447
+ return {
448
+ boundingVolume: makeOrientedBoundingBoxFromPoints(refPointPositions),
449
+ commonSpaceBounds: [minX, minY, maxX, maxY],
450
+ };
451
+ }
347
452
  }
348
453
  /**
349
454
  * Wrap a forward projection to EPSG:3857 so that it never returns NaN.
@@ -403,13 +508,31 @@ function sampleReferencePointsInEPSG3857(refPoints, tileCorners, projectTo3857,
403
508
  }
404
509
  return refPointPositions;
405
510
  }
511
+ /**
512
+ * Sample the selected reference points in WGS84 lng/lat.
513
+ *
514
+ * Like {@link sampleReferencePointsInEPSG3857}, reference points are `[relX,
515
+ * relY]` fractions in `[0, 1]` bilinearly interpolated across the tile's four
516
+ * CRS corners, then reprojected to WGS84. Used by the GlobeView bounding-volume
517
+ * path, which projects lng/lat onto the sphere rather than rescaling 3857
518
+ * meters into common space.
519
+ */
520
+ function sampleReferencePointsInWGS84(refPoints, tileCorners, projectTo4326) {
521
+ const { topLeft, topRight, bottomLeft, bottomRight } = tileCorners;
522
+ const refPointPositions = [];
523
+ for (const [relX, relY] of refPoints) {
524
+ const [geoX, geoY] = bilerpPoint(topLeft, topRight, bottomLeft, bottomRight, relX, relY);
525
+ refPointPositions.push(projectTo4326(geoX, geoY));
526
+ }
527
+ return refPointPositions;
528
+ }
406
529
  /**
407
530
  * Rescale positions from EPSG:3857 into deck.gl's common space
408
531
  *
409
532
  * Similar to the upstream code here:
410
533
  * https://github.com/visgl/deck.gl/blob/b0134f025148b52b91320d16768ab5d14a745328/modules/geo-layers/src/tileset-2d/tile-2d-traversal.ts#L172-L177
411
534
  */
412
- function rescaleEPSG3857ToCommonSpace([x, y]) {
535
+ export function rescaleEPSG3857ToCommonSpace([x, y]) {
413
536
  // Clamp Y to Web Mercator bounds
414
537
  const clampedY = Math.max(-EPSG_3857_HALF_CIRCUMFERENCE, Math.min(EPSG_3857_HALF_CIRCUMFERENCE, y));
415
538
  return [
@@ -417,6 +540,19 @@ function rescaleEPSG3857ToCommonSpace([x, y]) {
417
540
  (clampedY / EPSG_3857_CIRCUMFERENCE + 0.5) * TILE_SIZE,
418
541
  ];
419
542
  }
543
+ /**
544
+ * Inverse of {@link rescaleEPSG3857ToCommonSpace}: rescale a deck.gl
545
+ * common-space position back into EPSG:3857 meters.
546
+ *
547
+ * Common-space inputs are in-range by construction, so (unlike the forward
548
+ * direction) no latitude clamp is applied.
549
+ */
550
+ export function rescaleCommonSpaceToEPSG3857([x, y]) {
551
+ return [
552
+ (x / TILE_SIZE - 0.5) * EPSG_3857_CIRCUMFERENCE,
553
+ (y / TILE_SIZE - 0.5) * EPSG_3857_CIRCUMFERENCE,
554
+ ];
555
+ }
420
556
  /**
421
557
  * Above this root-tile count, `createRootTiles` culls to the viewport
422
558
  * before instantiation. Below it, every root tile is created and downstream
@@ -482,7 +618,7 @@ export function createRootTiles(opts) {
482
618
  /**
483
619
  * Get tile indices visible in viewport.
484
620
  *
485
- * Uses frustum culling driven by a {@link TilesetDescriptor}, which abstracts
621
+ * Uses frustum culling driven by a {@link RasterTilesetDescriptor}, which abstracts
486
622
  * over OGC TileMatrixSet grids and Zarr multiscale pyramids.
487
623
  *
488
624
  * Overview levels follow the descriptor ordering: index 0 = coarsest, higher = finer.
@@ -497,7 +633,7 @@ export function getTileIndices(descriptor, opts) {
497
633
  // so this frame can never evict an entry it will need again this frame.
498
634
  boundingVolumeCache.sweep();
499
635
  // Only define `project` function for Globe viewports, same as upstream
500
- const project = viewport instanceof _GlobeViewport && viewport.resolution
636
+ const project = viewport instanceof GlobeViewport && viewport.resolution
501
637
  ? viewport.projectPosition
502
638
  : null;
503
639
  // Get the culling volume of the current camera
@@ -562,6 +698,24 @@ export function getTileIndices(descriptor, opts) {
562
698
  for (const root of roots) {
563
699
  root.update(traversalParams);
564
700
  }
701
+ // World-copy passes: when the viewport spans multiple world copies (e.g.
702
+ // WebMercatorViewport with repeat: true panned across the antimeridian),
703
+ // re-run the traversal with the tile bounding volumes shifted by ±1, ±2…
704
+ // world copies along common-space X. A tile is selected if any pass selects
705
+ // it. See dev-docs/world-copies.md.
706
+ const subViewportCount = viewport.subViewports?.length ?? 0;
707
+ if (subViewportCount > 1) {
708
+ for (let offset = -1; offset >= -MAX_MAPS; offset--) {
709
+ if (!runOffsetPass(roots, traversalParams, offset)) {
710
+ break;
711
+ }
712
+ }
713
+ for (let offset = 1; offset <= MAX_MAPS; offset++) {
714
+ if (!runOffsetPass(roots, traversalParams, offset)) {
715
+ break;
716
+ }
717
+ }
718
+ }
565
719
  // Collect selected tiles
566
720
  const selectedNodes = [];
567
721
  for (const root of roots) {
@@ -569,6 +723,23 @@ export function getTileIndices(descriptor, opts) {
569
723
  }
570
724
  return selectedNodes;
571
725
  }
726
+ /**
727
+ * Run a non-zero world-offset traversal pass over each root.
728
+ *
729
+ * Returns `true` if any root tile was visible at this offset, signaling the
730
+ * caller to walk further from the primary world. Returns `false` when no
731
+ * tiles were visible — the offset has gone past the visible range and the
732
+ * caller stops walking that side.
733
+ */
734
+ function runOffsetPass(roots, baseParams, worldOffset) {
735
+ let anyVisible = false;
736
+ for (const root of roots) {
737
+ if (root.update({ ...baseParams, worldOffset })) {
738
+ anyVisible = true;
739
+ }
740
+ }
741
+ return anyVisible;
742
+ }
572
743
  /**
573
744
  * Compute the meters per pixel at a given latitude and zoom level.
574
745
  *
@@ -583,10 +754,44 @@ function getMetersPerPixel(latitude, zoom) {
583
754
  return ((earthCircumference * Math.cos((latitude * Math.PI) / 180)) /
584
755
  2 ** (zoom + 8));
585
756
  }
586
- function getMetersPerPixelAtBoundingVolume(boundingVolume, zoom) {
587
- const [_lng, lat] = worldToLngLat(boundingVolume.center);
757
+ function getMetersPerPixelAtCommonSpaceBounds(commonSpaceBounds, zoom) {
758
+ const [minX, minY, maxX, maxY] = commonSpaceBounds;
759
+ // `commonSpaceBounds` is in Web Mercator world space ([0, 512]) in BOTH the
760
+ // mercator and globe paths (the globe path builds it via `lngLatToWorld`), so
761
+ // its center maps back to a real latitude. The 3D oriented-bounding-box
762
+ // center, by contrast, is in globe common space on a globe and would
763
+ // `worldToLngLat` to a garbage latitude (~-89°, near the Mercator
764
+ // singularity), making meters-per-pixel far too small so the LOD always
765
+ // recursed to the finest level.
766
+ const [, lat] = worldToLngLat([(minX + maxX) / 2, (minY + maxY) / 2]);
588
767
  return getMetersPerPixel(lat, zoom);
589
768
  }
769
+ /**
770
+ * Translate a tile's bounding volume by `dx` units along common-space X.
771
+ *
772
+ * Returns a fresh OBB and AABB; does not mutate the input. Used by the
773
+ * world-copy traversal to test the same tile at multiple shifted positions
774
+ * without recomputing the underlying geometry.
775
+ */
776
+ function translateBoundingVolume(base, dx) {
777
+ const { boundingVolume, commonSpaceBounds } = base;
778
+ const center = boundingVolume.center;
779
+ const translatedCenter = new Vector3((center[0] ?? 0) + dx, center[1] ?? 0, center[2] ?? 0);
780
+ const translated = new OrientedBoundingBox(translatedCenter, boundingVolume.halfAxes);
781
+ // `update()`'s bounds check always re-reads the offset-0 `commonSpaceBounds`,
782
+ // so this translated AABB isn't consumed in production — it's kept for API
783
+ // symmetry with `boundingVolume` and is asserted directly by unit tests.
784
+ const translatedBounds = [
785
+ commonSpaceBounds[0] + dx,
786
+ commonSpaceBounds[1],
787
+ commonSpaceBounds[2] + dx,
788
+ commonSpaceBounds[3],
789
+ ];
790
+ return {
791
+ boundingVolume: translated,
792
+ commonSpaceBounds: translatedBounds,
793
+ };
794
+ }
590
795
  /**
591
796
  * Compute the axis-aligned bounding box of a rotated tile rectangle.
592
797
  */