Skip to content

feat: reprojector initial-triangulation seed + clamp Web Mercator meshes to ±85.051°#574

Merged
kylebarron merged 13 commits into
mainfrom
kyle/antimeridian
May 27, 2026
Merged

feat: reprojector initial-triangulation seed + clamp Web Mercator meshes to ±85.051°#574
kylebarron merged 13 commits into
mainfrom
kyle/antimeridian

Conversation

@kylebarron
Copy link
Copy Markdown
Member

@kylebarron kylebarron commented May 27, 2026

Before After
Screenshot 2026-05-27 at 1 31 55 PM Screenshot 2026-05-27 at 1 32 24 PM
Screenshot 2026-05-27 at 1 32 07 PM Screenshot 2026-05-27 at 1 32 54 PM
Screenshot 2026-05-27 at 1 36 00 PM Screenshot 2026-05-27 at 1 37 17 PM

Before:

image

After:

image

Closes #351

Summary

Generalizes RasterReprojector to start from an arbitrary initial triangulation instead of always the full unit square, and uses that to clamp reprojection meshes to the valid Web Mercator latitude band (±85.051°). This fixes the degenerate near-pole triangles / non-convergence for EPSG:4326 imagery reaching ±90° (#182, #351): beyond ±85.051° makeClampedForwardTo3857 collapses every row onto the same edge Y, so the old mesh smeared the polar caps onto the map edge and the reprojector burned all 10k refinement iterations.

It's also the foundation for rendering imagery across the antimeridian (cut-in-two): the same seed will let a crossing tile be split into two normal pieces. That implementation is a follow-up PR; the design spec is included here for context.

What's here

@developmentseed/raster-reproject

  • Constructor accepts an optional initialTriangulation seed ({ uvs, triangles, halfedges } — delaunator's shape), defaulting to the existing unit square (behavior unchanged). Refinement only splits existing triangles, so a sub-domain seed confines the mesh to that region.
  • triangulateRectangle(uMin, vMin, uMax, vMax) for axis-aligned sub-rectangle seeds (no runtime triangulation dependency).
  • InitialTriangulation type; its docstring shows the delaunator one-liner. delaunator is a dev/test dependency only (validates winding compatibility) — not shipped.

@developmentseed/deck.gl-raster

  • createInitialWebMercatorTriangulation(cornerLats) — a north-up geographic tile past ±85.051° → a clamped triangulateRectangle; otherwise undefined (rotated/projected/in-bounds/fully-polar → full mesh).
  • RasterLayer gains an initialTriangulation prop (mesh regenerates on change).
  • getTileMetadata computes a per-tile _webMercatorReprojectorSeed; _renderSubLayers applies it in the Web Mercator path only (globe renders the full mesh — it shows the poles).

Docs

  • dev-docs/specs/2026-05-27-antimeridian-crossing-tile-design.md — the cut-in-two design (this PR is its foundation; antimeridian rendering follows separately).

Test plan

  • raster-reproject unit tests — default seed unchanged, delaunator-seed winding, sub-domain confinement, triangulateRectangle
  • deck.gl-raster unit tests — createInitialWebMercatorTriangulation band math + gating
  • Visual (cog-basic, EOxCloudless global ±90°) — mesh ends cleanly at ±85.051° matching the basemap; "mesh refinement did not converge" warning gone; high-latitude features (northern Norway) align with country outlines

Written by Claude on behalf of @kylebarron

kylebarron and others added 9 commits May 27, 2026 11:29
Design for rendering imagery crossing ±180° in Web Mercator (issues #171,
#366). Splits a crossing tile at the antimeridian into west/east pieces so
each reprojects as a normal non-crossing tile — avoiding the proj4-rewrap
unwrap that prior attempts (#353/#374/#269) stumbled on. Generalizes the
RasterReprojector to accept a delaunator-shaped initial-triangulation seed
(subsuming #351 uvBounds / pole clamp), splits in _renderSubLayers into two
single-mesh RasterLayers, and uses a two-box bounding volume composing with
the merged world-copy traversal (#518).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ted cuts

- Use the vendored geotiff-test-data antimeridian.tif fixture (42x42 EPSG:4326,
  crosses -180 at column 24) as the primary deterministic crossing test.
- Handle slanted (rotated-geotransform) cuts, not just vertical: any straight
  cut yields convex pieces delaunator triangulates exactly; error only on
  curved/concave cuts. createInitialConditions is therefore part of the MVP
  crossing path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…d seed)

Add the InitialTriangulation type and a tree-shakeable createInitialConditions
helper that builds a Delaunay seed from a UV point set. delaunator is confined
to its own module (initial-conditions.ts); delatin.ts does not import it and the
package is sideEffects:false, so it tree-shakes out for consumers that don't use
it. Foundation for antimeridian cut-in-two and the sub-domain capability in #351.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…hip a wrapper

Per review: a one-line delaunator wrapper isn't worth a runtime dependency.
Expose only the InitialTriangulation type and show the delaunator one-liner in
its docstring. delaunator moves to devDependencies — used by tests to validate
winding compatibility, not shipped to consumers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…esign

raster-reproject exposes only the InitialTriangulation type + documents the
delaunator one-liner; delaunator is a dev/test dep (winding validation), not
shipped. Runtime seed-building for crossing tiles is the deck.gl-raster
builder's job (follow-up plan). Mark stage 1 done.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ulation

Generalize the constructor to accept an optional initialTriangulation seed
(delaunator's data shape), defaulting to a hardcoded unit-square seed so
behavior is unchanged and the package needs no runtime triangulation dep.
Refinement only ever splits existing triangles, so a sub-domain seed confines
the mesh to that region. Tests build seeds via delaunator (the documented
pattern) to validate winding compatibility + sub-domain confinement.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Build an axis-aligned 2-triangle rectangle seed for a UV sub-rectangle (no
delaunator, runtime-safe). UNIT_SQUARE_SEED is now rectangleSeed(0,0,1,1).
Used to clamp a mesh to a UV band — e.g. the valid Web Mercator latitude band
(#182 / #351) — and reused by the antimeridian vertical-cut case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure helper that returns a rectangleSeed clamping a north-up geographic tile's
reprojection mesh to the Web Mercator latitude band, or undefined when no clamp
is needed/possible (rotated/projected tiles, fully-polar tiles). Avoids the
degenerate near-pole triangles from #182 / #351. Unit-tested with synthetic
corner latitudes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wire the reprojector seed through the render path: RasterLayer gains an
initialTriangulation prop (passed to RasterReprojector, regenerated on change);
getTileMetadata computes a per-tile _webMercatorReprojectorSeed via
webMercatorClampSeed; _renderSubLayers passes it in the Web Mercator branch
only (globe shows the poles, full mesh). Fixes the degenerate near-pole
triangles for EPSG:4326 imagery reaching ±90° (#182 / #351).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kylebarron kylebarron changed the title Kyle/antimeridian feat: reprojector initial-triangulation seed + clamp Web Mercator meshes to ±85.051° May 27, 2026
@github-actions github-actions Bot added the feat label May 27, 2026
Comment thread packages/deck.gl-raster/src/raster-tileset/raster-tileset-2d.ts Outdated
kylebarron and others added 4 commits May 27, 2026 14:20
Rename webMercatorClampSeed → webMercatorInitialTriangulation and the tile
metadata field _webMercatorReprojectorSeed → _webMercatorInitialTriangulation,
for consistency with the InitialTriangulation type and the
RasterLayer.initialTriangulation prop (drops the ad-hoc 'seed'/'ReprojectorSeed'
terms).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…Triangulation

Verb-prefixed name for the builder (was webMercatorInitialTriangulation); the
tile metadata field stays _webMercatorInitialTriangulation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Active verb name for the helper that triangulates a UV rectangle into an
InitialTriangulation; internal UNIT_SQUARE_SEED -> UNIT_SQUARE_TRIANGULATION;
test file renamed to match. Updates the deck.gl-raster clamp caller too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Completes the rename (a657eb8 only moved the test file): rectangleSeed ->
triangulateRectangle in delatin.ts + index export + the renamed test, and the
deck.gl-raster web-mercator-clamp caller. UNIT_SQUARE_SEED -> UNIT_SQUARE_TRIANGULATION.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kylebarron kylebarron marked this pull request as ready for review May 27, 2026 18:38
@kylebarron kylebarron merged commit a56f239 into main May 27, 2026
4 checks passed
@kylebarron kylebarron deleted the kyle/antimeridian branch May 27, 2026 18:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant