Skip to content

fix(render): shade depth areas from the mariner's contours, not the baked token#20

Merged
beetlebugorg merged 2 commits into
mainfrom
fix/seabed-live-shading
Jul 13, 2026
Merged

fix(render): shade depth areas from the mariner's contours, not the baked token#20
beetlebugorg merged 2 commits into
mainfrom
fix/seabed-live-shading

Conversation

@beetlebugorg

Copy link
Copy Markdown
Owner

The bug

Depth areas were shaded against the bake context, not the mariner's. Deep water
never appeared, and the shallow/safety/deep contours and the two/four-shade toggle
had no effect on a rendered chart.

Why

The S-101 rules run once per cell with the fixed bake context (portray.Context
defaults: shallow 2 m, safety and deep both 30 m), because a baked tile has to
serve every mariner. So a DEPARE's ColorFill token encodes the bake's bands, not
the viewer's — which is exactly why scene.fillArea also hands every depth area its
DRVAL1/DRVAL2, and why the MapLibre client re-runs SEABED01 live
(mariner.seabedTokenExpr).

The resolving surfaces never ran the second half. All three dropped the range on the
floor (_ = depth) and painted the baked token:

  • DEPDW essentially never appeared. The baked deep contour is 30 m, so any cell
    whose depths run shallower than that showed no deep water at all.
  • DEPMD never appeared either. The bake's safety and deep contours are both
    30 m, which collapses the medium-deep band to nothing.
  • The mariner's contours did nothing. Moving shallow/safety/deep, or switching
    between two and four shades, changed no pixels.

Telling detail: the other two live-mariner depth swaps in these same surfaces were
already wired — soundings against safety_depth (SNDFRM04), and DANGER01/02 against
safety_contour. The seabed fill was the one that got missed.

The fix

Add resolve.seabedToken — SEABED01, mirroring mariner.seabedTokenExpr band for
band so the two styling paths can't silently drift — and swap the fill colour live in
the pixel, vector and ascii surfaces whenever a fill carries a depth range.

  • The baked fill transparency is preserved: the swap is of the colour, not the
    opacity.
  • A non-depth area (no DRVAL range) still takes its baked token, untouched.
  • Straddling areas take the shallower shade (>= on DRVAL1, > on DRVAL2), per
    spec.

Tests

  • resolve: the four bands + the drying fallthrough, the straddle rule, and the
    two-shade case, pinned against the style expression they mirror.
  • pixel: end-to-end through a real surface — the same 12–20 m area paints DEPMD
    under the default contours and DEPDW once the mariner pulls the deep contour in to
    10 m, which is the shade that could not previously appear. A non-depth fill is
    asserted unchanged.

Full suite green.


Also on this branch: perf(chart) — the S-52 colour tables are now parsed once per
process instead of once per render. They are a pure function of the embedded profile
and read-only once parsed, but every render path re-parsed the XML into three hash
maps. Measured on the explore path (one thumbnail per feature): 239 parses before,
1 after
across the 239 DEPARE features in US5MD1MC.

@beetlebugorg beetlebugorg force-pushed the fix/seabed-live-shading branch from 60aae85 to 3cf809b Compare July 13, 2026 09:18
…aked token

The S-101 rules are run ONCE per cell with the fixed bake context (portray.Context
defaults: shallow 2 m, safety/deep 30 m), because a baked tile must serve every
mariner. So a DEPARE's ColorFill token is the BAKE's shading, not this mariner's —
which is why scene.fillArea also hands every depth area its DRVAL1/DRVAL2, and why
the MapLibre style re-runs SEABED01 client-side (mariner.seabedTokenExpr).

The resolving surfaces never did. All three dropped the range on the floor
(`_ = depth`), painted the baked token, and so:

  * DEPDW essentially never appeared — the baked deep contour is 30 m, so any
    coastal cell whose depths run under that showed no deep water at all;
  * DEPMD never appeared — the bake's safety and deep contours are BOTH 30 m,
    which collapses the medium-deep band to nothing;
  * the mariner's shallow/safety/deep contours and the two/four-shade toggle
    changed nothing on screen.

Add resolve.seabedToken (SEABED01, mirroring mariner.seabedTokenExpr band for
band) and swap the fill colour live in the pixel, vector and ascii surfaces when
a fill carries a depth range. The baked fill transparency is preserved: the swap
is of the colour, not the opacity. Non-depth areas keep their baked token.

The other two live-mariner depth swaps in these surfaces (SNDFRM04 soundings vs
safety_depth, DANGER01/02 vs safety_contour) were already wired; this was the
gap.
…render

The S-52 colour tables are a pure function of the embedded colour profile — the
same bytes for every chart, and read-only once parsed — but every render path
re-parsed the whole XML into three hash maps: the per-handle cache only helped
the tile path, and a host that opens and purges chart handles as its quilt walks
cells (a large chart set makes that constant) paid the parse on every open. The
one-shot render paths (PNG/PDF/ascii/compose, and the per-feature explore
thumbnails) paid it on every call.

Share one lazily-parsed copy behind a CAS one-shot (Zig 0.16 puts mutexes behind
an Io the engine deliberately doesn't take; contention is possible only on the
very first render, and steady state is one acquire load).

Measured on the explore path, which renders one thumbnail per feature —
239 DEPARE features in US5MD1MC: 239 profile parses before, 1 after.
@beetlebugorg beetlebugorg force-pushed the fix/seabed-live-shading branch from 3cf809b to e54b464 Compare July 13, 2026 09:27
@beetlebugorg beetlebugorg merged commit 92593c9 into main Jul 13, 2026
5 checks passed
@beetlebugorg beetlebugorg deleted the fix/seabed-live-shading branch July 13, 2026 09:33
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.

1 participant