From 395cbd6bfea2fb56c0fa21b6eddd5b91ee5968a0 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Fri, 26 Dec 2025 22:09:23 -0800 Subject: [PATCH 1/3] wip: Create reprojector mesh initial conditions from delaunator --- packages/raster-reproject/package.json | 1 + packages/raster-reproject/src/delatin.ts | 67 ++++++++++++++++++------ pnpm-lock.yaml | 8 +++ 3 files changed, 60 insertions(+), 16 deletions(-) diff --git a/packages/raster-reproject/package.json b/packages/raster-reproject/package.json index 000bd04a..c2f79b97 100644 --- a/packages/raster-reproject/package.json +++ b/packages/raster-reproject/package.json @@ -42,6 +42,7 @@ "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", diff --git a/packages/raster-reproject/src/delatin.ts b/packages/raster-reproject/src/delatin.ts index c9f6ad36..fe7e6a1c 100644 --- a/packages/raster-reproject/src/delatin.ts +++ b/packages/raster-reproject/src/delatin.ts @@ -12,6 +12,8 @@ * license, then subject to further modifications. */ +import type Delaunator from "delaunator"; + /** * Barycentric sample points in uv space for where to sample reprojection * errors. @@ -55,8 +57,8 @@ export interface ReprojectionFns { export class RasterReprojector { reprojectors: ReprojectionFns; - width: number; - height: number; + width: number | null; + height: number | null; /** * UV vertex coordinates (x, y), i.e. @@ -93,14 +95,10 @@ export class RasterReprojector { private _pending: number[]; private _pendingLen: number; - constructor( - reprojectors: ReprojectionFns, - width: number, - height: number = width, - ) { + constructor(reprojectors: ReprojectionFns) { this.reprojectors = reprojectors; - this.width = width; - this.height = height; + this.width = null; + this.height = null; this.uvs = []; // vertex coordinates (x, y) this.exactOutputPositions = []; @@ -115,20 +113,57 @@ export class RasterReprojector { this._errors = []; this._pending = []; // triangles pending addition to queue this._pendingLen = 0; + } + + static fromHeightAndWidth( + reprojectors: ReprojectionFns, + height: number, + width: number = height, + ): RasterReprojector { + const reprojector = new RasterReprojector(reprojectors); + + // Set width and height + reprojector.width = width; + reprojector.height = height; // 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; + } + + static fromDelaunator>( + delaunay: Delaunator, + reprojectors: ReprojectionFns, + ): RasterReprojector { + const reprojector = new RasterReprojector(reprojectors); + + // Add points for each value in delaunay.coords + // delaunay.coords; + + // + reprojector.triangles = Array.from(delaunay.triangles); + + reprojector._halfedges = Array.from(delaunay.halfedges); + + // 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 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 35562453..2f1311e0 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 @@ -1319,6 +1322,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==} @@ -3877,6 +3883,8 @@ snapshots: '@types/deep-eql@4.0.2': {} + '@types/delaunator@5.0.3': {} + '@types/estree@1.0.8': {} '@types/geojson-vt@3.2.5': From b27cd0c95345348183da8fd9753df13fad525564 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Fri, 26 Dec 2025 22:44:06 -0800 Subject: [PATCH 2/3] Make generic interface for delaunator input --- packages/raster-reproject/package.json | 1 + packages/raster-reproject/src/delatin.ts | 30 +++++++++++++++++++++--- pnpm-lock.yaml | 15 ++++++++++++ 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/packages/raster-reproject/package.json b/packages/raster-reproject/package.json index c2f79b97..94079d3b 100644 --- a/packages/raster-reproject/package.json +++ b/packages/raster-reproject/package.json @@ -46,6 +46,7 @@ "@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 fe7e6a1c..c173d3e4 100644 --- a/packages/raster-reproject/src/delatin.ts +++ b/packages/raster-reproject/src/delatin.ts @@ -12,7 +12,31 @@ * license, then subject to further modifications. */ -import type Delaunator from "delaunator"; +/** + * 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 @@ -143,8 +167,8 @@ export class RasterReprojector { return reprojector; } - static fromDelaunator>( - delaunay: Delaunator, + static fromExistingTriangulation( + delaunay: ExistingDelaunayTriangulation, reprojectors: ReprojectionFns, ): RasterReprojector { const reprojector = new RasterReprojector(reprojectors); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2f1311e0..f211acb1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -257,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 @@ -1730,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'} @@ -2458,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'} @@ -4334,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 @@ -5083,6 +5096,8 @@ snapshots: reusify@1.1.0: {} + robust-predicates@3.0.2: {} + rollup@4.53.3: dependencies: '@types/estree': 1.0.8 From 8513e0c882d81f7b2aa390d61ae3ab0951e74fdd Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Fri, 26 Dec 2025 22:56:00 -0800 Subject: [PATCH 3/3] set exactOutputPositions --- packages/raster-reproject/src/delatin.ts | 70 +++++++++++++++--------- 1 file changed, 45 insertions(+), 25 deletions(-) diff --git a/packages/raster-reproject/src/delatin.ts b/packages/raster-reproject/src/delatin.ts index c173d3e4..4c3dd401 100644 --- a/packages/raster-reproject/src/delatin.ts +++ b/packages/raster-reproject/src/delatin.ts @@ -79,10 +79,11 @@ 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 | null; - height: number | null; + width: number; + height: number; /** * UV vertex coordinates (x, y), i.e. @@ -119,10 +120,16 @@ export class RasterReprojector { private _pending: number[]; private _pendingLen: number; - constructor(reprojectors: ReprojectionFns) { + // Make constructor private so that all instances are created via static + // methods + private constructor( + reprojectors: ReprojectionFns, + height: number, + width: number = height, + ) { this.reprojectors = reprojectors; - this.width = null; - this.height = null; + this.width = width; + this.height = height; this.uvs = []; // vertex coordinates (x, y) this.exactOutputPositions = []; @@ -139,16 +146,14 @@ export class RasterReprojector { this._pendingLen = 0; } - static fromHeightAndWidth( + // 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); - - // Set width and height - reprojector.width = width; - reprojector.height = height; + 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. @@ -167,21 +172,32 @@ export class RasterReprojector { return reprojector; } - static fromExistingTriangulation( + public static fromExistingTriangulation( delaunay: ExistingDelaunayTriangulation, reprojectors: ReprojectionFns, + height: number, + width: number = height, ): RasterReprojector { - const reprojector = new RasterReprojector(reprojectors); + const reprojector = new RasterReprojector(reprojectors, height, width); // Add points for each value in delaunay.coords - // delaunay.coords; - - // + reprojector.uvs = Array.from(delaunay.coords); reprojector.triangles = Array.from(delaunay.triangles); - reprojector._halfedges = Array.from(delaunay.halfedges); - // Also need to init triangle metadata + // 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` @@ -499,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]!, @@ -514,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,