Skip to content

geo: typed Garmin IMG terrain pipeline — parser → classify → kurvenlineal → contours → ver-8 bake#91

Merged
AdaWorldAPI merged 5 commits into
mainfrom
claude/ice-iceland-dem
Jul 8, 2026
Merged

geo: typed Garmin IMG terrain pipeline — parser → classify → kurvenlineal → contours → ver-8 bake#91
AdaWorldAPI merged 5 commits into
mainfrom
claude/ice-iceland-dem

Conversation

@AdaWorldAPI

Copy link
Copy Markdown
Owner

Builds the ground-up typed geo pipeline that replaces raster colour-guessing: Garmin IMG maps carry a type code on every polygon/polyline (building / water / forest / street / path / contour) plus elevation-labelled contours, so the terrain is reconstructed from real data instead of inferred from pixels. Five stages, each dep-free pure-std and regression-locked against the validated scripts/garmin_proto.py prototype and the real banked canyon tiles.

The stages

  1. IMG parser (geo/src/garmin/{container,tre,rgn,lbl,mod}.rs) — a byte-exact Rust port of the validated prototype. Container FAT + multi-part subfile reassembly → TRE (bbox, LOD level pyramid, subdivision tree) → RGN (points/lines/polys incl. the LSB-first delta bitstream with QMapShack CShiftReg continuation semantics — the part naive decoders drop) → LBL (6-bit + 8-bit labels). Parity is proven: an FNV-1a-64 fold over every decoded coordinate matches the Python golden 0xadb368a3b063c74d across all 120,174 features of the village tile, with exact per-kind counts and a decoded road name; three more tiles hit their exact feature counts.

  2. Typed classifier (classify.rs) — (kind, type_code) → GeoKind with a clean /helix-look palette. Validated: the full-tile KIND histogram is asserted exactly (Contour 62574, Street 32154, Stream 15161, …), plus a partition check that all 120,174 features classify to exactly one KIND.

  3. Inter-family kurvenlineal (kurvenlineal.rs) — fixes the "needle field" bug at its root. The old bake seeded a fresh helix::CurveRuler per cell, so the golden-spiral residue stepped discontinuously at every cell boundary (isolated spikes). This lifts it into a continuous field: the exact stride-4-over-17 residue sampled at lattice corners, smoothstep-interpolated between them — C1-continuous across cells, not a gaussian blur (it passes through the exact residue at every corner). A test reproduces the old construction alongside and proves it: old max seam-step > 0.3, new < 0.05.

  4. Terrain reconstruction (terrain.rs) — labeled contours → a connected grid heightfield. Rasterize each contour onto the grid at its elevation, then fill the gaps by harmonic relaxation (SOR) with the contour cells held fixed — the contours stay sharp, only the interior fills, so the surface is one connected sheet (never spikes), and it is not gaussian smoothing. LOD is the TRE level pyramid for free. A kind_grid overlays natural landcover (water/rivers/forest) while excluding roads/paths so a wilderness scene reads as terrain, not a street grid. Tests: synthetic ramp interpolates linearly; real level-4 canyon reconstructs to 561–3158 m with <0.1% isolated needles (the discrete maximum principle the harmonic fill guarantees).

  5. Bake bin (garmin_bake.rs, helix-gated) — ties it together into the BSO2 ver-8 radix-grid wire the cockpit /garmin/<location> route already decodes. The rich look (hypsometric LUT, kurvenlineal brightness, Gouraud, sunset, Ice/Ocean specular) is client-derived in the shader from the stored height + kind — the ver-8 contract.

Verify

  • cargo test --lib (dep-free): 19 tests green — parser parity, classifier golden histogram, terrain reconstruction/connectivity, landcover overlay.
  • cargo test --lib --features helix: 3 kurvenlineal tests green (continuity proof).
  • End-to-end: garmin_bake on the Grand Canyon village tile → 844×1024 grid, elevation 561–3121 m (real river-to-rim envelope), 864,256 verts / 1.72M tris / 2.64 MB ver-8 soa; header inspected (BSO2 ver8, W 844, H 1024).
  • clippy + rustfmt clean on all new files. geo is its own cargo workspace (isolated from the q2 workspace), so pre-existing unformatted sibling files are left untouched.

Not in this PR (follow-up)

The shader lighting (hypsometric/kurvenlineal/sunset/specular in BodyHelix.tsx), staging a scene into the garmin_scenes manifest, and the visual-verification pass. This PR is the complete, tested Rust foundation those sit on.

🤖 Generated with Claude Code

https://claude.ai/code/session_012jEwwaT5JZ5x8qWvcnaMYC


Generated by Claude Code

claude added 5 commits July 7, 2026 19:37
…he prototype

Ports scripts/garmin_proto.py — the validated Grand Canyon decoder — into a
dep-free, pure-std `geo/src/garmin/` module. Four stages, one submodule each:

- container.rs — the IMG container: XOR de-obfuscation, the 512-byte FAT from
  0x600, and multi-part subfile reassembly (blocks concatenated in part*240+i
  order) → named `name.TYPE` byte slices.
- tre.rs — the TRE: N/E/S/W int24 bbox, the LOD level pyramid, and the
  subdivision tree (16-byte records, 14-byte on the most-detailed level).
- rgn.rs — the RGN: points / indexed-points / polylines / polygons, including
  the LSB-first delta bitstream with QMapShack CShiftReg continuation +
  sign semantics (the marker naive decoders drop, which turns fine levels into
  random-walk mush — the plan's documented level-4 bug).
- lbl.rs — the LBL: 6-bit-packed and 8-bit-latin1 label text (road names,
  elevation-labelled contours).

Public API in mod.rs: `Img::{parse,read,decode}`, `Feature`, `Kind`, `Decoded`,
plus shared bounds-checked little-endian byte readers.

Parity is proven, not asserted: an in-module test folds every decoded
coordinate through FNV-1a-64 and matches the Python golden
(0xadb368a3b063c74d) over all 120,174 features of the village tile 47505316,
with exact per-kind (line 114616 / poly 3185 / point 2373) and per-level
counts, a concrete first-feature check, and a decoded road name ("US Hwy 180").
Three more banked tiles decode to their exact prototype feature counts.

This replaces raster colour-guessing with typed lookups for the geo bake
pipeline (tasks #13-#16): every polygon/polyline carries a Garmin type code
(building / water / forest / street / path / contour) plus contour elevations.

geo is its own cargo workspace (isolated from the q2 workspace); clippy
(-D warnings) and rustfmt clean on the new files.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012jEwwaT5JZ5x8qWvcnaMYC
…he needle bug)

The prior terrain bake seeded a fresh helix::CurveRuler per lattice cell
(iceland_dem::ruler_phase → from_place(mix(floor(pos·detail)))). Adjacent cells
drew uncorrelated phases, so the golden-spiral residue STEPPED discontinuously
at every cell boundary. Harmless while it only tinted colour; pushed into
geometry it IS the "needle field" — every cell its own isolated spike, never
connected to its neighbour. That is the intra-family failure.

This lifts the residue into a shared `kurvenlineal` module that samples the
exact stride-4-over-17 CurveRuler value at the integer lattice CORNERS and
smoothstep-interpolates between them (trilinear value-noise). The field is now
C1-continuous ACROSS cell boundaries — inter-family: neighbouring cells share
corner values, so relief flows smoothly to the next spike. It is NOT a gaussian
blur — the field passes through the exact CurveRuler value at every lattice
corner; only the cell interior is blended. Deterministic (phase is convention,
not data — OGAR D-QUANTGATE).

Test `inter_family_is_continuous_where_intra_family_jumps` proves the fix by
reproducing the old per-cell construction alongside: the old residue's max
adjacent step is > 0.3 (a seam jump); the new field's is < 0.05 (continuous).
Plus determinism/range and pass-through-lattice-values tests. helix-gated (uses
the real helix::CurveRuler, the same crate the sculpt tool + anatomy body use).
clippy clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012jEwwaT5JZ5x8qWvcnaMYC
…uristics stage)

The "heuristics" stage of the bake pipeline: replaces raster colour-guessing
with a typed lookup. A Garmin polygon IS a building/water/forest and a polyline
IS a contour/street/stream, by its type code — no inference, no colour
thresholds.

GeoKind { Building, Water, Stream, Woods, Park, Street, Path, Contour, Other }
with a sweet /helix-look palette (water reads as water, vegetation as
vegetation, no muddy browns), a stable PALETTE index order for the ver-8 wire
palette block, is_area() (building/water/woods/park fill; rest are lines), and
classify(kind, type_code) over the OpenTopoMap/US-topo code family. Feature gains
geo_kind() and is_contour() (the contour lines are the elevation source for the
terrain heightfield).

Validated against the real village tile: the full-tile KIND histogram is asserted
exactly (Contour 62574, Street 32154, Stream 15161, Path 3600, Other 3664, Water
2255, Park 766; Building/Woods 0 — wilderness tile), plus a partition check that
every one of the 120174 features classifies to exactly one KIND. Dep-free,
clippy clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012jEwwaT5JZ5x8qWvcnaMYC
The "terrain" stage: labeled contour polylines → a connected grid heightfield.
Method B — rasterize each contour polyline onto the W×H grid at its elevation,
then fill the gaps by harmonic relaxation (SOR) with the contour cells held
FIXED (Dirichlet BC). The contours stay razor-sharp; only the interior fills, so
the surface is one connected sheet (grid neighbours = structural connectivity),
never a field of isolated spikes. This is NOT gaussian smoothing — a blur would
smear the contours; here they are immovable and relaxation only interpolates the
unconstrained cells.

The banked canyon data is ideal: 100% of contours are labeled (62573/62574),
uniform 40-ft interval, distributed across TRE levels 2/3/4 (3690/8068/50816) —
so LOD is the level pyramid for free: heightfield_for_level bakes one surface per
level, the renderer swaps tier by camera distance.

API: HeightField, grid_from_contours (domain-agnostic gridder),
contours_for_level + heightfield_for_level (Garmin adapter: is_contour filter +
LBL elevation, feet→m). Dep-free, clippy clean.

Tests: (1) label parse feet/metres; (2) synthetic two-contour ramp interpolates
linearly (mid ≈ 50, monotone); (3) real village-tile level-4 reconstruction —
50816 contours → a connected canyon: elevation stays in the real envelope
(560..3158 m), >1500 m relief, and isolated needle spikes are <0.1% of cells
(the discrete maximum principle the harmonic fill guarantees — the exact
property the old per-cell needle bake violated). All finite, no NaN.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012jEwwaT5JZ5x8qWvcnaMYC
… pipeline)

Ties the typed pipeline together into a bakeable scene:
  IMG → decode → contours → heightfield (terrain::heightfield_for_level)
      → landcover KIND overlay → ver-8 radix-grid wire.

terrain.rs additions:
- kind_grid(dec, bbox, w, h, overlay): rasterize selected features' GeoKind onto
  the grid (areas first, lines on top); bare terrain stays Other for the shader's
  hypsometric tint. LANDCOVER = [Water, Stream, Woods, Park] — roads/paths/
  buildings are excluded so a wilderness scene reads as TERRAIN, not a street grid
  (the "cluttered lines" look, avoided by construction). project() made pub.

garmin_bake bin (helix-gated, mirrors iceland_dem's projection/concept/encode):
level (TRE LOD tier, default 4), aspect-correct grid, equirectangular metric
projection about the tile centre, true-scale elevation, HHTL-keyed row concepts,
GeoKind palette, encode_grid_bso2. The rich look (hypsometric LUT, inter-family
kurvenlineal brightness, Gouraud, sunset, Ice/Ocean specular) is client-derived
in the shader from the stored height+kind — the ver-8 contract.

Verified end-to-end: baking the Grand Canyon village tile (47505316) →
844x1024 grid, elevation 561..3121 m (real river-to-rim envelope), 864256 verts /
1.72M tris / 2.64 MB ver-8 soa. Header inspected: BSO2 ver8, nC 1024, W 844,
H 1024. New kind_grid test asserts streams overlay while roads/paths do not and
terrain stays the majority. clippy + fmt clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012jEwwaT5JZ5x8qWvcnaMYC
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@AdaWorldAPI, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 53 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 53ceb4d5-e999-4f8c-a11c-147189782b7a

📥 Commits

Reviewing files that changed from the base of the PR and between 1c65a92 and 69ceda7.

📒 Files selected for processing (12)
  • claude-notes/plans/2026-07-07-garmin-img-typed-rebuild.md
  • geo/Cargo.toml
  • geo/src/bin/garmin_bake.rs
  • geo/src/garmin/classify.rs
  • geo/src/garmin/container.rs
  • geo/src/garmin/lbl.rs
  • geo/src/garmin/mod.rs
  • geo/src/garmin/rgn.rs
  • geo/src/garmin/terrain.rs
  • geo/src/garmin/tre.rs
  • geo/src/kurvenlineal.rs
  • geo/src/lib.rs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cursor

cursor Bot commented Jul 8, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_7402a70b-26bd-4fec-892c-6b26741f8b65)

@AdaWorldAPI AdaWorldAPI merged commit 07d597c into main Jul 8, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants