Skip to content

Commit 987e3d5

Browse files
committed
cockpit: north-up fix for all grid scenes + Havel white balance + /havel route
Three fixes from the operator's alignment check (Havel vs Google Maps): 1. NORTH-UP FIX (all ver-8/9 grid scenes — iceland, grand-canyon, havel — no rebake). The bake stores z = (lat−lat0)·M (north = +z) while the default aerial camera sits on the +z side (screen-up = −z), so every grid scene rendered N–S MIRRORED against a real map. Caught on the Havel: the Kölpinsee chain appeared SOUTH of the Müritz. The demgrid ground truth verified correct — display-side only. Fix in decodeGrid: negate zrow at read (positions + normals derive from the one table), emit the triangle winding flipped ((a,d2,b)/(b,d2,e)) so faces keep pointing +y, negate the concept-centroid z. Wire bytes + HHTL keys untouched. 2. WHITE BALANCE (operator: "the lighting is rather dull"). The ESRI imagery over Mecklenburg is hazy — measured white point p98 = 171/166/145. Bake-time per-channel p2→p98 stretch (→ 10..250) + saturation ×1.18 on the demgrid rgb, then rebake: the corrected colour is sunk into the grid once (the Diaprojektor model), not shader-guessed per frame. Havel asset replaced (72.7 MB gz). 3. /havel FIRST-CLASS ROUTE (like /ice): main.tsx Route + pathScene alias → garmin:havel + scene-menu alias so the dropdown shows it selected. First attempt missed the top-level router (the path fell through to the AIWAR cockpit) — caught by the headless shot. Headless-verified on a 1.2M proxy: Müritz + Kölpinsee at NW (north-up ✓), haze gone, /havel renders the terrain. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_012jEwwaT5JZ5x8qWvcnaMYC
1 parent 3ce5898 commit 987e3d5

4 files changed

Lines changed: 32 additions & 4 deletions

File tree

claude-notes/plans/2026-07-10-garmin-dem-satellite-skin.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,22 @@ the render on mobile; the full grid is the desktop birdview.
5353
- Scene = `/garmin/havel` via one manifest entry; the dropdown auto-populates from
5454
`garmin_scenes` — zero cockpit code.
5555

56+
## North-up fix + white balance + /havel route (2026-07-10, operator alignment check)
57+
- **N–S mirror found by the operator** (Havel vs Google: Kölpinsee rendered SOUTH of
58+
the Müritz). Root cause: bake stores z = (lat−lat0)·M (north = +z) while the default
59+
aerial camera sits on the +z side (screen-up = −z) → every ver-8/9 grid scene rendered
60+
N–S mirrored. Ground truth (demgrid) verified correct — display-side bug only.
61+
- **Fix in `decodeGrid` (no rebake, all grid scenes at once):** negate `zrow` at read
62+
(positions + normals derive), emit winding flipped ((a,d2,b)/(b,d2,e)) so faces stay
63+
+y, negate concept-centroid z. Wire + HHTL keys untouched.
64+
- **White balance (operator: "rather dull"):** ESRI over Mecklenburg is hazy — measured
65+
p98 white point only 171/166/145. Bake-time per-channel p2→p98 stretch (→10..250) +
66+
sat ×1.18 on the demgrid rgb, then rebake — corrected colour sunk once (Diaprojektor),
67+
not shader-corrected per frame.
68+
- **`/havel` first-class route** (like /ice): main.tsx Route + pathScene alias →
69+
`garmin:havel`. First attempt missed the top-level router (path fell through to the
70+
AIWAR cockpit) — caught by the headless shot.
71+
5672
## Follow-ups
5773
- **66 MB from a q2 Release, on-demand** (operator preference) — host the full-tile /
5874
higher-zoom asset in a GitHub Release; scene fetches it on demand instead of git.

cockpit/public/havel.v8grid.soa.gz

3.36 MB
Binary file not shown.

cockpit/src/GeoHelix.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ function pathScene(): string | null {
3737
const p = window.location.pathname;
3838
if (p === '/geo') return 'osm';
3939
if (p === '/ice') return 'iceland';
40+
if (p === '/havel') return 'garmin:havel'; // the canoe map gets a first-class URL (like /ice)
4041
// Mod-rewrite style: /garmin/<location> → scene "garmin:<location>". The slug is
4142
// resolved SERVER-side (/api/garmin/:location → manifest garmin_scenes → dist file),
4243
// so a new scene is a manifest entry + a bake — no client change.
@@ -178,6 +179,13 @@ function decodeGrid(buf: ArrayBuffer, stride = 1): Decoded {
178179
const x0 = dv.getFloat32(o, true), dx = dv.getFloat32(o + 4, true), yscale = dv.getFloat32(o + 8, true);
179180
o += 12;
180181
const zrow = new Float32Array(buf.slice(o, o + 4 * Hf)); o += 4 * Hf;
182+
// NORTH-UP FIX: the bake stores z = (lat − lat0)·M (north = +z), but the default
183+
// aerial camera sits on the +z side (screen-up = −z), which rendered every grid
184+
// scene N–S MIRRORED against a real map (caught on the Havel bake: Kölpinsee
185+
// appeared SOUTH of the Müritz). Negate z at decode — positions, normals and the
186+
// row table all derive from this one table, and the winding below is emitted in
187+
// the flipped order so faces keep pointing +y. Wire + HHTL keys are untouched.
188+
for (let i = 0; i < Hf; i++) zrow[i] = -zrow[i];
181189
const nK = dv.getUint8(o); o += 1;
182190
const pal = new Uint8Array(buf.slice(o, o + 3 * nK)); o += 3 * nK;
183191
const hfFull = new Uint16Array(buf.slice(o, o + 2 * nVf)); o += 2 * nVf;
@@ -287,16 +295,18 @@ function decodeGrid(buf: ArrayBuffer, stride = 1): Decoded {
287295
for (let r = 0; r < H - 1; r++) {
288296
for (let c = 0; c < W - 1; c++) {
289297
const a = r * W + c, b = a + 1, d2 = a + W, e = d2 + 1;
290-
index[wI++] = a; index[wI++] = b; index[wI++] = d2;
291-
index[wI++] = b; index[wI++] = e; index[wI++] = d2;
298+
// Winding for the NEGATED-z frame (north-up fix above): row r+1 now has
299+
// GREATER z, so emit (a,d2,b)/(b,d2,e) to keep the face normal +y (up).
300+
index[wI++] = a; index[wI++] = d2; index[wI++] = b;
301+
index[wI++] = b; index[wI++] = d2; index[wI++] = e;
292302
}
293303
}
294304
const labelIdx = new Uint32Array(buf.slice(labelOff, labelOff + 4 * nC));
295305
const cen = new Float32Array(buf.slice(cenOff, cenOff + 12 * nC));
296306
const conceptList: ConceptMeta[] = [];
297307
for (let c = 0; c < nC; c++) {
298308
conceptList.push({ row: c, name: names[labelIdx[c]] ?? `concept ${c}`, layer: cLayer[c] || 8,
299-
cx: -cen[c * 3], cy: cen[c * 3 + 2], cz: cen[c * 3 + 1] }); // source → display (-x,z,y)
309+
cx: -cen[c * 3], cy: cen[c * 3 + 2], cz: -cen[c * 3 + 1] }); // source → display (-x,-z,y): z negated by the north-up fix
300310
}
301311
return { nVerts: nV, nTris: nT, positions, index, colors, normals, layer, vrow: rowArr, concepts: nC, conceptList, isGrid: true, stride, skin: !!skin };
302312
}
@@ -1298,7 +1308,8 @@ export default function GeoHelix() {
12981308
{ label: 'iceland (/ice)', path: '/ice' },
12991309
...garminScenes.map((s) => ({ label: `garmin: ${s}`, path: `/garmin/${s}` })),
13001310
];
1301-
const herePath = window.location.pathname;
1311+
// /havel is a first-class alias of /garmin/havel — the menu should show it selected.
1312+
const herePath = window.location.pathname === '/havel' ? '/garmin/havel' : window.location.pathname;
13021313

13031314
return (
13041315
<div style={{ position: 'fixed', inset: 0, background: `#${PAGE_BG.toString(16).padStart(6, '0')}` }}>

cockpit/src/main.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ createRoot(document.getElementById('root')!).render(
119119
GeoHelix parses the slug from the path (pathScene). */}
120120
<Route path="/geo" element={<GeoHelix />} />
121121
<Route path="/ice" element={<GeoHelix />} />
122+
<Route path="/havel" element={<GeoHelix />} />
122123
<Route path="/garmin/:location" element={<GeoHelix />} />
123124
{/* /cpic — CPIC pharmacogenomics cockpit (gene-first): {gene, diplotype, drug}
124125
→ phenotype → recommendation, 2-hop NARS deduction over the real CPIC tables

0 commit comments

Comments
 (0)