diff --git a/packages/deck.gl-raster/src/raster-tile-layer/raster-tile-layer.ts b/packages/deck.gl-raster/src/raster-tile-layer/raster-tile-layer.ts index c3474b76..e3942fdc 100644 --- a/packages/deck.gl-raster/src/raster-tile-layer/raster-tile-layer.ts +++ b/packages/deck.gl-raster/src/raster-tile-layer/raster-tile-layer.ts @@ -380,12 +380,9 @@ export class RasterTileLayer< return debugLayers; } - const { x, y, z } = tile.index; - const level = descriptor.levels[z]; - if (!level) { - return debugLayers; - } - const { forwardTransform, inverseTransform } = level.tileTransform(x, y); + // Access forwardTransform/inverseTransform from tile metadata so that + // reference equality holds across renders. + const { forwardTransform, inverseTransform } = tile; const tileResult = renderTile(props.data); if (!tileResult) { return debugLayers; diff --git a/packages/deck.gl-raster/src/raster-tileset/raster-tileset-2d.ts b/packages/deck.gl-raster/src/raster-tileset/raster-tileset-2d.ts index 8e7d1827..1d55ecf9 100644 --- a/packages/deck.gl-raster/src/raster-tileset/raster-tileset-2d.ts +++ b/packages/deck.gl-raster/src/raster-tileset/raster-tileset-2d.ts @@ -21,6 +21,7 @@ import type { Bounds, Corners, ProjectedBoundingBox, + ProjectionFunction, TileIndex, ZRange, } from "./types.js"; @@ -55,6 +56,24 @@ export type TileMetadata = { * Tile height in pixels. */ tileHeight: number; + + /** + * Forward (tile-local pixel → CRS) transform for this tile. + * + * Stable across the tile's lifetime; computed once at tile creation. Stored + * on the tile so downstream layers (e.g. `RasterTileLayer._renderSubLayers`) + * receive a reference-stable function across renders, which is what + * `RasterLayer`'s `reprojectionFnsChanged` check needs to avoid spurious mesh + * regeneration. + */ + forwardTransform: ProjectionFunction; + + /** + * Inverse (CRS → tile-local pixel) transform. + * + * Same stability guarantees as {@link TileMetadata.forwardTransform}. + */ + inverseTransform: ProjectionFunction; }; /** @@ -253,6 +272,9 @@ export class RasterTileset2D extends Tileset2D { ...projectedBounds, ); + const { forwardTransform, inverseTransform } = + levelDescriptor.tileTransform(x, y); + return { bbox: { west, @@ -269,6 +291,8 @@ export class RasterTileset2D extends Tileset2D { projectedCorners, tileWidth, tileHeight, + forwardTransform, + inverseTransform, }; } } diff --git a/packages/deck.gl-raster/tests/raster-tileset/raster-tileset-2d-tile-transform.test.ts b/packages/deck.gl-raster/tests/raster-tileset/raster-tileset-2d-tile-transform.test.ts new file mode 100644 index 00000000..004bdd76 --- /dev/null +++ b/packages/deck.gl-raster/tests/raster-tileset/raster-tileset-2d-tile-transform.test.ts @@ -0,0 +1,53 @@ +import type { _Tileset2DProps as Tileset2DProps } from "@deck.gl/geo-layers"; +import { compose, scale, translation } from "@developmentseed/affine"; +import { describe, expect, it } from "vitest"; +import { AffineTileset } from "../../src/raster-tileset/affine-tileset.js"; +import { AffineTilesetLevel } from "../../src/raster-tileset/affine-tileset-level.js"; +import { RasterTileset2D } from "../../src/raster-tileset/raster-tileset-2d.js"; + +const identity = (x: number, y: number): [number, number] => [x, y]; + +const PROJECTIONS = { + projectTo3857: identity, + projectFrom3857: identity, + projectTo4326: identity, + projectFrom4326: identity, +}; + +function tilesetProps(): Tileset2DProps { + return { getTileData: () => new Promise(() => {}) } as Tileset2DProps; +} + +describe("RasterTileset2D.getTileMetadata", () => { + it("attaches per-tile forwardTransform/inverseTransform to TileMetadata", () => { + const level = new AffineTilesetLevel({ + affine: compose(translation(100, 200), scale(10, -10)), + arrayWidth: 8, + arrayHeight: 8, + tileWidth: 4, + tileHeight: 4, + mpu: 1, + }); + const descriptor = new AffineTileset({ + levels: [level], + ...PROJECTIONS, + }); + const tileset = new RasterTileset2D(tilesetProps(), descriptor); + + const metadata = tileset.getTileMetadata({ x: 1, y: 1, z: 0 }); + + expect(typeof metadata.forwardTransform).toBe("function"); + expect(typeof metadata.inverseTransform).toBe("function"); + + // Tile (1,1) at pixel (0,0) should map to the CRS origin of that tile. + // Tile is 4x4 pixels at 10 CRS units/pixel from origin (100, 200), Y flipped. + const [x, y] = metadata.forwardTransform(0, 0); + expect(x).toBeCloseTo(140, 10); + expect(y).toBeCloseTo(160, 10); + + // Round-trip via inverseTransform. + const [px, py] = metadata.inverseTransform(x, y); + expect(px).toBeCloseTo(0, 10); + expect(py).toBeCloseTo(0, 10); + }); +});