mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
fix(transit): keep the real silhouettes under sensor presets, make the selected trail visible, put Transit in Movement (#596)
## Changes Follow-up to #587 after the owner's first look at the merged transit layer on Google 3D tiles. - **Sensor presets keep the real silhouettes.** Under NVG, thermal (white-hot, black-hot, Ironbow) and noir, vehicles are the same silhouettes as in normal view, filled opaque white with a 2 px dark halo, at the CRT display sizes (26 px fleet, 39 px selected). The solid rounded "signature" bodies are gone. - **The selected vehicle's trail is visible.** The retained history is now draped onto 3D tiles and terrain (`GroundPolylinePrimitive`, `ClassificationType.BOTH`, the way the traffic heat-lines are drawn), 3 px core over a 5 px dark backing, per-segment age colour, a short draped head clipped to the sprite. Two defects kept it off screen before: Cesium's per-instance attribute getters return copies, so the renderer's show/colour edits were discarded, and a marker did not recover after the camera jumped to it, which hid the trail with it. Trail geometry is now prepared on selection independent of floor resolution and marker visibility; sparse long intervals (rail) keep a bounded marker corridor instead of dropping the interval; classification survives map-source changes. - **Transit sits in the Movement panel group** between Street Traffic and Bike Share (the panel orders by `PANEL_GROUPS`, not by catalog order; the catalog is moved too). ## How it was verified - `npm run format:check` (854 files), `npm run check:boundaries`, `npm test` (4,062 passing), `npm run build` clean; tracking regression 109/109 in a real browser on Google 3D tiles. - Browser harness (`scripts/qa-transit.mjs`), keyed on Google 3D tiles and keyless: keyed 194 pass, with the trail pixel check passing at 300, 600 and 900 m; the remaining keyed failures are the per-frame allocation gate already recorded as a follow-up from #587 (CPU and frame time within budget), four readability lines at threshold edges, and the card-anchor check now counting frames where the selected marker is legitimately off screen; keyless 193 pass with the remaining misses being threshold edges on the thin tram silhouette (centre 0.83–0.88 against 0.85) and one bus ring 0.28 against 0.25 on a pure-white roof. - New harness checks: `Google 3D selected trail visible at 6 of 8 projected samples` (repeated off/on framebuffer captures at 300/600/900 m, samples outside the sprite footprint, a background-change negative control; fails on main, passes here), a camera-arrival check (select a moving vehicle, `setView` onto it, marker shown and trail body drawn within 2 s), `PANEL_ORDER` pinned, raster tests comparing every sensor raster's mask and aspect ratio to the normal silhouette. - Direct look on a keyed build: a live CapMetro bus at 30 m/s selected through the layer API draws its draped trail along the highway lane (harness capture `keyed-600m-trail-visible.jpg`). - Adversarial review by Codex Astra before the last two fix passes (stack-switch classification, marker corridor under the vertex cap, pixel-gate loophole), all three fixed with regressions. Docs: `docs/CURRENT-STATE.md` and `CHANGELOG.md` updated in this PR. No data-source changes.
This commit is contained in:
1 parent
cd22ca3559
commit
0bb61f53c6
24 files changed
+1945
-205
No files matched your search
+128
-24
@@ -32,10 +32,10 @@ test('a haloed glyph draws the dark ring under the shipped white body', () => {
|
||||
assert.notEqual(plain, haloed);
|
||||
assert.match(
|
||||
haloed,
|
||||
/stroke="#000000" stroke-opacity="1" stroke-width="19\.20"/,
|
||||
/stroke="#05080C" stroke-opacity="1" stroke-width="14\.77"/,
|
||||
);
|
||||
assert.ok(
|
||||
haloed.indexOf('#000000') < haloed.indexOf('fill="white"'),
|
||||
haloed.indexOf('#05080C') < haloed.indexOf('fill="white"'),
|
||||
'ring first, body on top',
|
||||
);
|
||||
assert.match(haloed, /viewBox="-\d+ -\d+ \d+ \d+"/, 'padded frame');
|
||||
@@ -97,40 +97,144 @@ test('all 36 variants retain the specified final halo at both display sizes', as
|
||||
assert.equal(transitIconCacheSize(), 36);
|
||||
});
|
||||
|
||||
test('sensor bodies cover sixty percent of the padded display with opaque white', async () => {
|
||||
test('every mono raster preserves the normal silhouette mask and aspect at CRT display size', async () => {
|
||||
const sharp = (await import('sharp')).default;
|
||||
const { presetSpriteScale, presetSpriteOutlinePx } =
|
||||
await import('./transitPresetStyle.js');
|
||||
const rasterBody = async (svg) => {
|
||||
// Compare fills in the same frame; the separately tested external halo is excluded.
|
||||
const d =
|
||||
[...svg.matchAll(/<path d="([^"]+)" fill="white"/g)][0]?.[1] ??
|
||||
svg.match(/<path d="([^"]+)"\s+fill="white"/)[1];
|
||||
const { data, info } = await sharp(
|
||||
Buffer.from(
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" viewBox="-48 -48 96 96"><path d="${d}" fill="white"/></svg>`,
|
||||
),
|
||||
)
|
||||
.ensureAlpha()
|
||||
.raw()
|
||||
.toBuffer({ resolveWithObject: true });
|
||||
const mask = [];
|
||||
let minX = info.width,
|
||||
minY = info.height,
|
||||
maxX = 0,
|
||||
maxY = 0;
|
||||
for (let y = 0; y < info.height; y++)
|
||||
for (let x = 0; x < info.width; x++) {
|
||||
const alpha = data[(y * info.width + x) * 4 + 3];
|
||||
mask.push(alpha);
|
||||
if (alpha > 127) {
|
||||
minX = Math.min(minX, x);
|
||||
maxX = Math.max(maxX, x);
|
||||
minY = Math.min(minY, y);
|
||||
maxY = Math.max(maxY, y);
|
||||
}
|
||||
}
|
||||
return { mask, aspect: (maxX - minX + 1) / (maxY - minY + 1) };
|
||||
};
|
||||
for (const selected of [false, true])
|
||||
for (const kind of TRANSIT_ICON_KINDS) {
|
||||
const normal = await rasterBody(
|
||||
decode(transitIcon(kind, selected ? 96 : 48, { style: 'normal' })),
|
||||
);
|
||||
for (const style of ['surveillance', 'thermal', 'noir', 'nvg']) {
|
||||
const svg = decode(transitIcon(kind, selected ? 96 : 48, { style }));
|
||||
const mono = await rasterBody(svg);
|
||||
assert.deepEqual(
|
||||
mono.mask,
|
||||
normal.mask,
|
||||
`${kind}/${style}: no envelope`,
|
||||
);
|
||||
assert.ok(
|
||||
Math.abs(mono.aspect / normal.aspect - 1) <= 0.05,
|
||||
`${kind}/${style}: aspect`,
|
||||
);
|
||||
const display =
|
||||
(selected ? 30 : 20) * presetSpriteScale(style, selected);
|
||||
const crt = (selected ? 30 : 20) * presetSpriteScale('retro', selected);
|
||||
assert.equal(display, crt);
|
||||
const frame = haloFrame(
|
||||
presetSpriteOutlinePx(style, selected),
|
||||
display,
|
||||
);
|
||||
const crtFrame = haloFrame(
|
||||
presetSpriteOutlinePx('retro', selected),
|
||||
crt,
|
||||
);
|
||||
assert.equal(
|
||||
(display * frame.ratio) / frame.ratio,
|
||||
(crt * crtFrame.ratio) / crtFrame.ratio,
|
||||
'unpadded silhouette matches CRT; only external halo padding grows',
|
||||
);
|
||||
assert.equal(
|
||||
(svg.match(/<path /g) || []).length,
|
||||
2,
|
||||
'only outer halo and opaque silhouette',
|
||||
);
|
||||
assert.match(svg, /fill="white" \/>/);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('sensor raster halo stays dark across its screen-space band on a white roof', async () => {
|
||||
const sharp = (await import('sharp')).default;
|
||||
const { presetSpriteScale, presetSpriteOutlinePx } =
|
||||
await import('./transitPresetStyle.js');
|
||||
const { reduceSensorContrast } =
|
||||
await import('../layers/transit/qaMetrics.js');
|
||||
const sides = {
|
||||
bus: 17,
|
||||
tram: 10,
|
||||
subway: 11.5,
|
||||
rail: 10.5,
|
||||
ferry: 12.5,
|
||||
unknown: 10,
|
||||
};
|
||||
for (const selected of [false, true]) {
|
||||
const pixels = [];
|
||||
for (const kind of TRANSIT_ICON_KINDS) {
|
||||
const display =
|
||||
(selected ? 30 : 20) * presetSpriteScale('thermal', selected);
|
||||
assert.equal(display, selected ? 36 : 30);
|
||||
assert.equal(presetSpriteOutlinePx('thermal', selected), 3);
|
||||
const frame = haloFrame(
|
||||
presetSpriteOutlinePx('thermal', selected),
|
||||
display,
|
||||
);
|
||||
const size = Math.round(display * frame.ratio * 8);
|
||||
const svg = decode(
|
||||
transitIcon(kind, selected ? 96 : 48, { style: 'thermal' }),
|
||||
);
|
||||
const frame = haloFrame(3, display);
|
||||
const width = Math.round(display * frame.ratio);
|
||||
const { data, info } = await sharp(Buffer.from(svg))
|
||||
.resize(width, width)
|
||||
.ensureAlpha()
|
||||
.resize(size, size)
|
||||
.flatten({ background: '#ffffff' })
|
||||
.raw()
|
||||
.toBuffer({ resolveWithObject: true });
|
||||
let white = 0;
|
||||
for (let i = 0; i < data.length; i += 4)
|
||||
if (
|
||||
data[i] >= 240 &&
|
||||
data[i + 1] >= 240 &&
|
||||
data[i + 2] >= 240 &&
|
||||
data[i + 3] >= 240
|
||||
)
|
||||
white++;
|
||||
assert.ok(
|
||||
white / (info.width * info.height) >= 0.6,
|
||||
`${kind}/${display}: white ${white / (info.width * info.height)}`,
|
||||
);
|
||||
const c = (Math.floor(width / 2) * width + Math.floor(width / 2)) * 4;
|
||||
assert.deepEqual([...data.subarray(c, c + 4)], [255, 255, 255, 255]);
|
||||
const sample = (offsetPx) => {
|
||||
const x = Math.floor(
|
||||
size / 2 + (offsetPx * size) / (display * frame.ratio),
|
||||
);
|
||||
const i = (Math.floor(size / 2) * info.width + x) * info.channels;
|
||||
return (
|
||||
(0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2]) / 255
|
||||
);
|
||||
};
|
||||
// The body stays at CRT size; the ring must cover this fixed external band.
|
||||
const ringMin = sample((sides[kind] * display) / 96 + 1.25);
|
||||
pixels.push({
|
||||
key: kind,
|
||||
verified: true,
|
||||
centre: sample(0),
|
||||
ringMin,
|
||||
ringMax: 1 - ringMin,
|
||||
background: 1,
|
||||
});
|
||||
}
|
||||
const white = reduceSensorContrast(pixels, 'white', 'thermal');
|
||||
assert.ok(white.pass, JSON.stringify({ selected, pixels, white }));
|
||||
const black = reduceSensorContrast(
|
||||
pixels.map((p) => ({ ...p, centre: 1 - p.centre })),
|
||||
'black',
|
||||
'thermal',
|
||||
);
|
||||
assert.ok(black.pass, JSON.stringify({ selected, black }));
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user