diff --git a/packages/raster-reproject/package.json b/packages/raster-reproject/package.json index 000bd04..94079d3 100644 --- a/packages/raster-reproject/package.json +++ b/packages/raster-reproject/package.json @@ -42,9 +42,11 @@ "url": "git+https://github.com/developmentseed/deck.gl-raster.git" }, "devDependencies": { + "@types/delaunator": "^5.0.3", "@types/node": "^25.0.1", "@typescript-eslint/eslint-plugin": "^8.16.0", "@typescript-eslint/parser": "^8.16.0", + "delaunator": "^5.0.1", "eslint": "^9.15.0", "jsdom": "^27.2.0", "prettier": "^3.3.3", diff --git a/packages/raster-reproject/src/delatin.ts b/packages/raster-reproject/src/delatin.ts index c9f6ad3..4c3dd40 100644 --- a/packages/raster-reproject/src/delatin.ts +++ b/packages/raster-reproject/src/delatin.ts @@ -12,6 +12,32 @@ * license, then subject to further modifications. */ +/** + * A type hint for how to initialize the RasterReprojector from an existing + * mesh. This is explicitly designed to match the Delaunator interface. + */ +export type ExistingDelaunayTriangulation = { + /** + * Triangle vertex indices (each group of three numbers forms a triangle). + * + * All triangles should be directed counterclockwise. + */ + triangles: Uint32Array; + + /** + * Triangle half-edge indices that allows you to traverse the triangulation. + * + * - `i`-th half-edge in the array corresponds to vertex `triangles[i]` the half-edge is coming from. + * - `halfedges[i]` is the index of a twin half-edge in an adjacent triangle (or -1 for outer half-edges on the convex hull). + */ + halfedges: Int32Array; + + /** + * Input coordinates in the form `[x0, y0, x1, y1, ....]`. + */ + coords: ArrayLike; +}; + /** * Barycentric sample points in uv space for where to sample reprojection * errors. @@ -53,6 +79,7 @@ export interface ReprojectionFns { inverseReproject(x: number, y: number): [number, number]; } +// TODO: document that height and width here are in terms of input pixels. export class RasterReprojector { reprojectors: ReprojectionFns; width: number; @@ -93,10 +120,12 @@ export class RasterReprojector { private _pending: number[]; private _pendingLen: number; - constructor( + // Make constructor private so that all instances are created via static + // methods + private constructor( reprojectors: ReprojectionFns, - width: number, - height: number = width, + height: number, + width: number = height, ) { this.reprojectors = reprojectors; this.width = width; @@ -115,20 +144,66 @@ export class RasterReprojector { this._errors = []; this._pending = []; // triangles pending addition to queue this._pendingLen = 0; + } + + // TODO: create a name for "initialize with two diagonal triangles covering + // the whole uv space" + public static fromRectangle( + reprojectors: ReprojectionFns, + height: number, + width: number = height, + ): RasterReprojector { + const reprojector = new RasterReprojector(reprojectors, height, width); // The two initial triangles cover the entire input texture in UV space, so // they range from [0, 0] to [1, 1] in u and v. const u1 = 1; const v1 = 1; - const p0 = this._addPoint(0, 0); - const p1 = this._addPoint(u1, 0); - const p2 = this._addPoint(0, v1); - const p3 = this._addPoint(u1, v1); + const p0 = reprojector._addPoint(0, 0); + const p1 = reprojector._addPoint(u1, 0); + const p2 = reprojector._addPoint(0, v1); + const p3 = reprojector._addPoint(u1, v1); // add initial two triangles - const t0 = this._addTriangle(p3, p0, p2, -1, -1, -1); - this._addTriangle(p0, p3, p1, t0, -1, -1); - this._flush(); + const t0 = reprojector._addTriangle(p3, p0, p2, -1, -1, -1); + reprojector._addTriangle(p0, p3, p1, t0, -1, -1); + reprojector._flush(); + + return reprojector; + } + + public static fromExistingTriangulation( + delaunay: ExistingDelaunayTriangulation, + reprojectors: ReprojectionFns, + height: number, + width: number = height, + ): RasterReprojector { + const reprojector = new RasterReprojector(reprojectors, height, width); + + // Add points for each value in delaunay.coords + reprojector.uvs = Array.from(delaunay.coords); + reprojector.triangles = Array.from(delaunay.triangles); + reprojector._halfedges = Array.from(delaunay.halfedges); + + // Initialize exactOutputPositions by reprojection + const numCoords = delaunay.coords.length / 2; + for (let i = 0; i < numCoords; i++) { + const u = delaunay.coords[i * 2]!; + const v = delaunay.coords[i * 2 + 1]!; + const exactOutputPosition = reprojector._computeOutputPosition(u, v); + reprojector.exactOutputPositions.push( + exactOutputPosition[0]!, + exactOutputPosition[1]!, + ); + } + + // TODO: Also need to init triangle metadata + // Set _candidatesUV and _queueIndices + // Set `_pending` + + reprojector._flush(); + + return reprojector; } // refine the mesh until its maximum error gets below the given one @@ -440,13 +515,7 @@ export class RasterReprojector { this.uvs.push(u, v); // compute and store exact output position via reprojection - const pixelX = u * (this.width - 1); - const pixelY = v * (this.height - 1); - const inputPosition = this.reprojectors.pixelToInputCRS(pixelX, pixelY); - const exactOutputPosition = this.reprojectors.forwardReproject( - inputPosition[0], - inputPosition[1], - ); + const exactOutputPosition = this._computeOutputPosition(u, v); this.exactOutputPositions.push( exactOutputPosition[0]!, exactOutputPosition[1]!, @@ -455,6 +524,16 @@ export class RasterReprojector { return i; } + private _computeOutputPosition(u: number, v: number): [number, number] { + const pixelX = u * (this.width - 1); + const pixelY = v * (this.height - 1); + const inputPosition = this.reprojectors.pixelToInputCRS(pixelX, pixelY); + return this.reprojectors.forwardReproject( + inputPosition[0], + inputPosition[1], + ); + } + // add or update a triangle in the mesh _addTriangle( a: number, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3556245..f211acb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -245,6 +245,9 @@ importers: packages/raster-reproject: devDependencies: + '@types/delaunator': + specifier: ^5.0.3 + version: 5.0.3 '@types/node': specifier: ^25.0.1 version: 25.0.1 @@ -254,6 +257,9 @@ importers: '@typescript-eslint/parser': specifier: ^8.16.0 version: 8.49.0(eslint@9.39.1)(typescript@5.9.3) + delaunator: + specifier: ^5.0.1 + version: 5.0.1 eslint: specifier: ^9.15.0 version: 9.39.1 @@ -1319,6 +1325,9 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/delaunator@5.0.3': + resolution: {integrity: sha512-6tTLP8NX0OwtB/fmW9bXp4EWPptawTSsrSGjboWRuzqkxNEEJGyzRPHbr8wnV2DBWfAZ+EPTOvW3B/KysJrl2g==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -1724,6 +1733,9 @@ packages: resolution: {integrity: sha512-3daSWyvZ/zwJvuMGlzG1O+Ow0YSadGfb3jsh9xoCutv2tWyB9dA4YvR9L9/fSdDZa2dByYQe+TqapSGUrjnkoA==} engines: {node: '>=0.10.0'} + delaunator@5.0.1: + resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -2452,6 +2464,9 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + robust-predicates@3.0.2: + resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} + rollup@4.53.3: resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -3877,6 +3892,8 @@ snapshots: '@types/deep-eql@4.0.2': {} + '@types/delaunator@5.0.3': {} + '@types/estree@1.0.8': {} '@types/geojson-vt@3.2.5': @@ -4326,6 +4343,10 @@ snapshots: dependencies: core-assert: 0.2.1 + delaunator@5.0.1: + dependencies: + robust-predicates: 3.0.2 + dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -5075,6 +5096,8 @@ snapshots: reusify@1.1.0: {} + robust-predicates@3.0.2: {} + rollup@4.53.3: dependencies: '@types/estree': 1.0.8