Merge branch 'main' into codex/fix-seek-layer-state

This commit is contained in:
Alex O'Neill authored and GitHub committed 2026-10-26 13:06:49 +00:00
commit 01c35a2c11
102 files changed
+2281 -531

No files matched your search

-2
View File
@@ -1,2 +0,0 @@
// Compatibility entry; source contracts are owned by sources/.
export { createSourceSlot } from '../sources/sourceSlot.js';
-24
View File
@@ -1,24 +0,0 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { createSourceSlot } from './sourceSlot.js';
test('replacing a source rejects its late result and old cleanup cannot clear the new source', async () => {
let finish;
const slot = createSourceSlot({ read: async () => 'default' }, ['read']);
const old = slot.configure({ read: () => new Promise(resolve => { finish = resolve; }) });
const pending = slot.source.read();
const stop = slot.configure({ read: async () => 'replacement', attribution: { name: 'Replacement' } });
old();
finish('stale');
await assert.rejects(pending, { name: 'AbortError' });
assert.equal(await slot.source.read(), 'replacement');
assert.equal(slot.source.attribution.name, 'Replacement');
stop();
assert.throws(() => slot.source.read(), /not configured/);
});
test('an invalid replacement leaves the working source intact', async () => {
const slot = createSourceSlot({ read: async () => 42 }, ['read']);
assert.throws(() => slot.configure({}), TypeError);
assert.equal(await slot.source.read(), 42);
});
+43
View File
@@ -876,3 +876,46 @@ test('route numbers follow the page language and never break from their unit', (
const list = directionsStepList({ route });
assert.equal(list.items[1].lead, '240\u00a0м');
});
test('ramp and lane wording stays localized after choosing the maneuver form', (t) => {
t.after(() => setLocale('en'));
const cases = [
[
'ru',
'Въезжайте по съезду плавно налево на I-95',
'Сверните на съезд резко направо',
'Займите левую полосу',
],
[
'es',
'Toma la rampa ligeramente a la izquierda hacia I-95',
'Toma la salida bruscamente a la derecha',
'Toma el carril izquierdo',
],
[
'zh-CN',
'向左前方驶入匝道,进入 I-95',
'从出口向右后方驶出',
'使用左转车道',
],
];
for (const [locale, ramp, exit, lane] of cases) {
setLocale(locale);
assert.equal(
directionsInstruction({
type: 'on ramp',
modifier: 'slight left',
name: 'I-95',
}),
ramp,
);
assert.equal(
directionsInstruction({ type: 'off ramp', modifier: 'sharp right' }),
exit,
);
assert.equal(
directionsInstruction({ type: 'use lane', modifier: 'sharp left' }),
lane,
);
}
});
-6
View File
@@ -1,6 +0,0 @@
import { createFlowTileSource } from '../layers/traffic/flowSource.js';
export { tilesForBounds } from './tomtomTiles.js';
export { decodeFlowTile } from '../layers/traffic/flowDecode.js';
const source = createFlowTileSource();
export const { fetchFlowForBounds, getFlowSessionStats, resetFlowTileCache } =
source;
-162
View File
@@ -1,162 +0,0 @@
// src/data/flowTiles.test.mjs
import { test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
decodeFlowTile,
fetchFlowForBounds,
tilesForBounds,
getFlowSessionStats,
resetFlowTileCache,
} from './flowTiles.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Real TomTom flow tile, downtown Austin z12 x935 y1686 (probed live 2026-07-16).
const FIXTURE = path.join(__dirname, 'fixtures', 'tomtom-flow-austin-12-935-1686.pbf');
const FIXTURE_TILE = { z: 12, x: 935, y: 1686 };
function loadFixture() {
return fs.readFileSync(FIXTURE);
}
// ── decodeFlowTile against the real fixture ─────────────────
test('fixture decode: more than 50 flow segments', () => {
const segments = decodeFlowTile(loadFixture(), FIXTURE_TILE.z, FIXTURE_TILE.x, FIXTURE_TILE.y);
assert.ok(segments.length > 50, `got ${segments.length}`);
});
test('fixture decode: every trafficLevel is within [0, 1]', () => {
const segments = decodeFlowTile(loadFixture(), FIXTURE_TILE.z, FIXTURE_TILE.x, FIXTURE_TILE.y);
for (const s of segments) {
assert.ok(Number.isFinite(s.trafficLevel), `non-finite level: ${s.trafficLevel}`);
assert.ok(s.trafficLevel >= 0 && s.trafficLevel <= 1, `level out of range: ${s.trafficLevel}`);
}
});
test('fixture decode: all coordinates land in downtown Austin', () => {
const segments = decodeFlowTile(loadFixture(), FIXTURE_TILE.z, FIXTURE_TILE.x, FIXTURE_TILE.y);
for (const s of segments) {
assert.ok(Array.isArray(s.coords) && s.coords.length >= 2, 'polyline too short');
for (const [lon, lat] of s.coords) {
assert.ok(lon >= -98.0 && lon <= -97.5, `lon out of Austin range: ${lon}`);
assert.ok(lat >= 30.0 && lat <= 30.5, `lat out of Austin range: ${lat}`);
}
}
});
test('fixture decode: congestion exists (at least one trafficLevel < 1)', () => {
const segments = decodeFlowTile(loadFixture(), FIXTURE_TILE.z, FIXTURE_TILE.x, FIXTURE_TILE.y);
assert.ok(segments.some((s) => s.trafficLevel < 1), 'no congested segment found');
});
test('fixture decode: segment shape is {coords, trafficLevel, roadType, closure}', () => {
const segments = decodeFlowTile(loadFixture(), FIXTURE_TILE.z, FIXTURE_TILE.x, FIXTURE_TILE.y);
for (const s of segments) {
assert.equal(typeof s.roadType, 'string');
assert.equal(typeof s.closure, 'boolean');
}
// The fixture carries real closures — closure decoding is exercised, not vacuous.
assert.ok(segments.some((s) => s.closure === true), 'expected at least one closure in fixture');
});
test('decode of a non-MVT buffer returns [] (defensive)', () => {
assert.deepEqual(decodeFlowTile(Buffer.from('not a protobuf tile'), 12, 935, 1686), []);
});
// ── tilesForBounds (re-exported slippy math) ────────────────
test('tilesForBounds: 30.2672,-97.7431 @ z12 -> covers x935 y1686', () => {
const tiles = tilesForBounds({
south: 30.2672 - 0.001, north: 30.2672 + 0.001,
west: -97.7431 - 0.001, east: -97.7431 + 0.001,
}, 12);
assert.ok(
tiles.some((t) => t.z === 12 && t.x === 935 && t.y === 1686),
`fixture tile missing: ${JSON.stringify(tiles)}`
);
});
// ── fetchFlowForBounds (stubbed fetch: cache + abort) ───────
/** Bounds fully inside the fixture tile. */
const FIXTURE_BOUNDS = { south: 30.24, north: 30.26, west: -97.76, east: -97.74 };
function stubFetch(impl) {
const original = globalThis.fetch;
globalThis.fetch = impl;
return () => { globalThis.fetch = original; };
}
test('fetchFlowForBounds: fetches covering tiles via /api/tomtom and decodes', async () => {
resetFlowTileCache();
const calls = [];
const restore = stubFetch(async (url) => {
calls.push(String(url));
return new Response(loadFixture(), {
status: 200,
headers: { 'Content-Type': 'application/x-protobuf' },
});
});
try {
const segments = await fetchFlowForBounds(FIXTURE_BOUNDS);
assert.equal(calls.length, 1);
assert.match(calls[0], /^\/api\/tomtom\/flow\/12\/935\/1686\.pbf$/);
assert.ok(segments.length > 50);
assert.ok(getFlowSessionStats().tilesFetched >= 1);
} finally {
restore();
}
});
test('fetchFlowForBounds: decode cache serves repeat calls within TTL (no refetch)', async () => {
resetFlowTileCache();
let calls = 0;
const restore = stubFetch(async () => {
calls += 1;
return new Response(loadFixture(), { status: 200 });
});
try {
const first = await fetchFlowForBounds(FIXTURE_BOUNDS);
const second = await fetchFlowForBounds(FIXTURE_BOUNDS);
assert.equal(calls, 1, 'second call must be served from the decode cache');
assert.equal(second.length, first.length);
} finally {
restore();
}
});
test('fetchFlowForBounds: aborted signal rejects (AbortSignal-aware)', async () => {
resetFlowTileCache();
const restore = stubFetch(async (url, opts) => {
// Mimic real fetch abort semantics.
if (opts?.signal?.aborted) {
const err = new Error('aborted');
err.name = 'AbortError';
throw err;
}
return new Response(loadFixture(), { status: 200 });
});
try {
const controller = new AbortController();
controller.abort();
await assert.rejects(
fetchFlowForBounds(FIXTURE_BOUNDS, { signal: controller.signal }),
(err) => err.name === 'AbortError'
);
} finally {
restore();
}
});
test('fetchFlowForBounds: non-OK tile responses reject when nothing succeeds', async () => {
resetFlowTileCache();
const restore = stubFetch(async () => new Response(JSON.stringify({ error: 'no_key' }), { status: 503 }));
try {
await assert.rejects(fetchFlowForBounds(FIXTURE_BOUNDS));
} finally {
restore();
}
});
-46
View File
@@ -1,46 +0,0 @@
// src/data/issPass.js
/**
* Next-ISS-pass prediction. Kept as a thin wrapper over satellitePass.js so the
* original findNextIssPass signature and defaults still work.
*/
import { findNextSatellitePass, lookAnglesAt } from './satellitePass.js';
export { lookAnglesAt };
/**
* Predict the next ISS pass over an observer location.
* @param {Object} options
* @param {Object} options.satrec SGP4 satellite record
* @param {number} options.latDeg Observer latitude in degrees [-90, 90]
* @param {number} options.lonDeg Observer longitude in degrees [-180, 180]
* @param {number} options.fromMs UTC start time in milliseconds
* @param {number} [options.minElevDeg=10] Elevation that defines rise and set, in degrees
* @param {number} [options.horizonHours=24] Maximum search window in hours
* @param {number} [options.coarseStepSec=30] Coarse search step in seconds
* @param {number} [options.fineStepSec=5] Fine transit step in seconds for peak tracking and visibility
* @param {boolean} [options.requireVisible=false] Require a naked-eye-visible pass
* @returns {{ riseMs: number, setMs: number, maxElevDeg: number, maxElevMs: number, riseAzDeg: number, visible: boolean, sunlit: boolean, observerDark: boolean } | null}
*/
export function findNextIssPass({
satrec,
latDeg,
lonDeg,
fromMs,
minElevDeg = 10,
horizonHours = 24,
coarseStepSec = 30,
fineStepSec = 5,
requireVisible = false,
}) {
return findNextSatellitePass({
satrec,
latDeg,
lonDeg,
fromMs,
minElevDeg,
horizonHours,
coarseStepSec,
fineStepSec,
requireVisible,
});
}
-42
View File
@@ -1,42 +0,0 @@
// src/data/issPass.test.mjs
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { twoline2satrec } from 'satellite.js';
import { findNextIssPass, lookAnglesAt } from './issPass.js';
import { findNextSatellitePass } from './satellitePass.js';
// Canonical archived ISS TLE (valid checksums; epoch 2008-09-20 ~12:25 UTC).
const L1 = '1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927';
const L2 = '2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.72125391563537';
const AUSTIN = { latDeg: 30.2672, lonDeg: -97.7431 };
const FROM_MS = Date.UTC(2008, 8, 20, 12, 30, 0); // just after the TLE epoch
test('finds a structurally-consistent ISS pass within 24h of the TLE epoch', () => {
const satrec = twoline2satrec(L1, L2);
const pass = findNextIssPass({ satrec, ...AUSTIN, fromMs: FROM_MS, minElevDeg: 10 });
assert.ok(pass, 'expected a pass within 24h at 30°N for a 51.6° inclination orbit');
assert.ok(pass.riseMs > FROM_MS);
assert.ok(pass.riseMs < pass.maxElevMs && pass.maxElevMs < pass.setMs);
assert.ok(pass.maxElevDeg >= 10);
assert.ok(pass.riseAzDeg >= 0 && pass.riseAzDeg < 360);
// Independent re-check: elevation at the reported peak really is the max-ish
// and ≥ threshold; one minute before rise it is below threshold.
const peak = lookAnglesAt(satrec, pass.maxElevMs, AUSTIN.latDeg, AUSTIN.lonDeg);
assert.ok(peak && Math.abs(peak.elevDeg - pass.maxElevDeg) < 1.5);
const before = lookAnglesAt(satrec, pass.riseMs - 60_000, AUSTIN.latDeg, AUSTIN.lonDeg);
assert.ok(before && before.elevDeg < 10);
});
test('forwards its original 30 s coarse step and fineStepSec to the pass engine', () => {
const satrec = twoline2satrec(L1, L2);
const opts = { satrec, ...AUSTIN, fromMs: FROM_MS, minElevDeg: 10, fineStepSec: 30 };
assert.deepEqual(findNextIssPass(opts), findNextSatellitePass({ ...opts, coarseStepSec: 30 }));
assert.notDeepEqual(findNextIssPass(opts), findNextIssPass({ ...opts, fineStepSec: 5 }));
});
test('returns null when no pass clears an absurd threshold', () => {
const satrec = twoline2satrec(L1, L2);
const pass = findNextIssPass({ satrec, ...AUSTIN, fromMs: FROM_MS, minElevDeg: 89.9, horizonHours: 2 });
assert.equal(pass, null);
});
+12 -1
View File
@@ -2,7 +2,18 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
import { createLocalGeoJsonLayer, mapAnalystRecord } from './localGeojson.js';
import { localGeoJsonServices } from '../app/localGeojsonServices.js';
import {
createLocalGeoJsonLayer as createLayer,
mapAnalystRecord,
} from './localGeojsonCore.js';
/** The catalog's services, with the overlay host each test supplies. */
const createLocalGeoJsonLayer = (options) =>
createLayer(options, {
...localGeoJsonServices,
overlayHost: options.overlayHost,
});
const DC_RAW = {
id: '1176042553',
-29
View File
@@ -1,29 +0,0 @@
import { localGeoJsonServices } from '../app/localGeojsonServices.js';
import {
createLocalGeoJsonLayer as createLayer,
createLocalInfrastructureOverlayPublisher as createPublisher,
} from './localGeojsonCore.js';
export * from './localGeojsonCore.js';
/** Create a layer using the standalone app's operations; retain overlay overrides. */
export function createLocalGeoJsonLayer(options) {
return createLayer(options, {
...localGeoJsonServices,
overlayHost:
options.overlayHost === undefined
? localGeoJsonServices.overlayHost
: options.overlayHost,
});
}
/** Create an overlay publisher using the standalone app's host by default. */
export function createLocalInfrastructureOverlayPublisher(options) {
return createPublisher({
...options,
host:
options.host === undefined
? localGeoJsonServices.overlayHost
: options.host,
});
}
+10 -13
View File
@@ -6,13 +6,14 @@ import {
GROUND_SAMPLE_MAX_ARMED_RETRIES,
LOCAL_OVERLAY_COHORT_LIMIT,
LOCAL_STEM_TIP_EPSILON_M,
createLocalGeoJsonLayer,
createLocalGeoJsonLayer as createLayer,
createLocalInfrastructureOverlayEntry,
createLocalInfrastructureOverlayPublisher,
localDatasetError,
localInfrastructureOverlayCopy,
selectLocalInfrastructureOverlayCohort,
} from './localGeojson.js';
} from './localGeojsonCore.js';
import { localGeoJsonServices } from '../app/localGeojsonServices.js';
import { layerFeedState } from './manager.js';
import { INFRA_LOD_ACTIVE_MIN, INFRA_LOD_ACTIVE_MAX } from './localGeojsonLod.js';
import {
@@ -21,6 +22,13 @@ import {
_resetRenderGovernorForTest,
} from '../renderGovernor.js';
/** The catalog's services, with the overlay host each test supplies. */
const createLocalGeoJsonLayer = (options) =>
createLayer(options, {
...localGeoJsonServices,
overlayHost: options.overlayHost ?? localGeoJsonServices.overlayHost,
});
class MockLayerEvent {
constructor() {
this.listeners = new Set();
@@ -1337,14 +1345,3 @@ test('globe-LOD re-selects during continuous motion, without ever seeing a moveE
assert.deepEqual(env.layer.getLodDiagnostics(), parked, 'a parked camera never re-selects');
});
test('standalone publisher retains its default host for an undefined override', () => {
const publisher = createLocalInfrastructureOverlayPublisher({
sourceId: 'local-default-host-test', host: undefined,
});
assert.doesNotThrow(() => {
publisher.show();
publisher.hide();
publisher.destroy();
});
});
+9 -1
View File
@@ -1,9 +1,17 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
import { createLocalGeoJsonLayer } from './localGeojson.js';
import { localGeoJsonServices } from '../app/localGeojsonServices.js';
import { createLocalGeoJsonLayer as createLayer } from './localGeojsonCore.js';
import { getContextStore } from './contextStore.js';
/** The catalog's services, with the overlay host each test supplies. */
const createLocalGeoJsonLayer = (options) =>
createLayer(options, {
...localGeoJsonServices,
overlayHost: options.overlayHost,
});
const dataset = JSON.stringify({
type: 'Feature', id: 'dam', properties: { name: 'Test dam' },
geometry: { type: 'Polygon', coordinates: [[[0, 0], [0.01, 0], [0, 0.01], [0, 0]]] },
+4 -4
View File
@@ -5,7 +5,7 @@
* Pure selection policy for the bundled local-infrastructure layers
* (`local-datacenters`, `local-dams`, and any future `createLocalGeoJsonLayer`
* dataset). Cesium-specific projection, occlusion, and stem geometry stay in
* `localGeojson.js`; this module turns the resulting in-view records into a
* `localGeojsonCore.js`; this module turns the resulting in-view records into a
* bounded "active" set — the only records that layer should spend per-frame
* stem-geometry and ground-sample work on.
*
@@ -80,7 +80,7 @@ export const INFRA_LOD_INCUMBENT_BONUS = 250;
/**
* Distance at or beyond which the proximity penalty is fully applied (m).
* Matches LOCAL_OVERLAY_MAX_DISTANCE_M's order of magnitude in localGeojson.js.
* Matches LOCAL_OVERLAY_MAX_DISTANCE_M's order of magnitude in infrastructureOverlayEntry.js.
*/
export const INFRA_LOD_FAR_M = 12_000_000;
@@ -97,7 +97,7 @@ export const INFRA_LOD_MAX_DISTANCE_PENALTY = 200;
* (callers only compare it), but every input is normalized so the output is
* always a finite number.
*
* @param {number} priority Existing label-priority score (localGeojson.js).
* @param {number} priority Existing label-priority score (localGeojsonCore.js).
* @param {number} distanceM Camera→feature distance in metres.
* @param {boolean} isIncumbent Record currently holds an active stem.
* @param {object} [options]
@@ -297,7 +297,7 @@ export function applyInfraEvictionGrace({
* ------------------------------------------------------------------ */
/**
* Probe window for the motion fallback. `localGeojson.js` marks its selection
* Probe window for the motion fallback. `localGeojsonCore.js` marks its selection
* dirty on `moveEnd`, which covers drags, one-shot `setView`, and voice
* `flyTo` — but a tracked-entity follow camera, a Cockpit view, a route
* flight, or a continuous orbit never emits one. Without a fallback the
+25 -8
View File
@@ -54,6 +54,21 @@ function direction(modifier) {
return DIRECTION_WORDS[key] || key;
}
/**
* "on the left" only fits a bare side. "slight left" is the adverb
* "slightly left", and "on the slightly left" is not an instruction.
* @param {string} prefix "Take the ramp" or "Take the exit".
* @param {string} modifier OSRM maneuver modifier.
* @param {string} onto " onto …", or "".
*/
function directedWay(prefix, modifier, onto) {
const dir = direction(modifier);
if (dir === 'left' || dir === 'right')
return `${prefix} on the ${dir}${onto}`;
if (dir && dir !== 'straight') return `${prefix} ${dir}${onto}`;
return `${prefix}${onto}`;
}
function ordinal(n) {
if (!Number.isInteger(n) || n < 1) return null;
const suffix =
@@ -103,13 +118,9 @@ export function instructionFor(step) {
case 'merge':
return dir ? `Merge ${dir}${onto}` : `Merge${onto}`;
case 'on ramp':
return dir
? `Take the ramp on the ${dir}${onto}`
: `Take the ramp${onto}`;
return directedWay('Take the ramp', modifier, onto);
case 'off ramp':
return dir
? `Take the exit on the ${dir}${onto}`
: `Take the exit${onto}`;
return directedWay('Take the exit', modifier, onto);
case 'roundabout':
case 'rotary': {
const exit = ordinal(step?.exit);
@@ -124,8 +135,14 @@ export function instructionFor(step) {
case 'exit roundabout':
case 'exit rotary':
return `Exit the roundabout${onto}`;
case 'use lane':
return dir ? `Use the ${dir} lane${onto}` : `Continue${onto}`;
case 'use lane': {
const lane = modifier.includes('left')
? 'left'
: modifier.includes('right')
? 'right'
: '';
return lane ? `Use the ${lane} lane${onto}` : `Continue${onto}`;
}
case 'notification':
default:
return `Continue${onto}`;
+20
View File
@@ -77,6 +77,26 @@ test('instructions read like a navigator, one sentence per OSRM maneuver', () =>
instructionFor({ type: 'on ramp', modifier: 'left' }),
'Take the ramp on the left',
);
assert.equal(
instructionFor({ type: 'on ramp', modifier: 'slight left', name: 'I-95' }),
'Take the ramp slightly left onto I-95',
);
assert.equal(
instructionFor({ type: 'off ramp', modifier: 'sharp right', ref: 'I-280' }),
'Take the exit sharply right onto I-280',
);
assert.equal(
instructionFor({ type: 'on ramp', modifier: 'straight', name: 'US 101' }),
'Take the ramp onto US 101',
);
assert.equal(
instructionFor({ type: 'use lane', modifier: 'slight left' }),
'Use the left lane',
);
assert.equal(
instructionFor({ type: 'use lane', modifier: 'right', name: 'Main St' }),
'Use the right lane onto Main St',
);
assert.equal(
instructionFor({
type: 'merge',
+1 -1
View File
@@ -2,7 +2,7 @@
* @file Pure tile math + budget accounting for the TomTom traffic-flow proxy.
*
* Shared by the `/api/tomtom` vite plugin (server-side: coordinate validation,
* daily budget governor) and `src/data/flowTiles.js` (client-side: which tiles
* daily budget governor) and `src/layers/traffic/flowSource.js` (client-side: which tiles
* cover the current traffic fetch bounds). Zero dependencies, Cesium-free, so
* both sides can unit-test against it with node:test.
*
+1
View File
@@ -75,4 +75,5 @@ test('the Docker install path keeps its loopback-only, launcher-owned contract',
assert.match(compose, /^\s+- GEV_KEY_SETUP_TRUSTED_PEERS$/m, 'the operator policy reaches the launcher without a hard-coded address');
assert.match(compose, /^\s+- GEV_ALLOWED_HOSTS$/m, 'the host allowlist reaches Vite');
assert.doesNotMatch(compose, /network_mode/, 'the container keeps its own network namespace');
assert.match(compose, /^\s+- \.:\/app:z$/m, 'SELinux hosts can read the bind-mounted checkout');
});
+27 -26
View File
@@ -282,28 +282,28 @@ export class IntelHUD {
<div class="hud-sonar" aria-hidden="true"></div>
<div class="hud-top-bar">
<span class="hud-top-bar-left">TOP SECRET // SI-TK // NOFORN</span>
<span class="hud-top-bar-center">${this._missionId}</span>
<span class="hud-top-bar-right">PAGE 1/1</span>
<span class="hud-readout"><span class="hud-top-bar-left">TOP SECRET // SI-TK // NOFORN</span></span>
<span class="hud-readout"><span class="hud-top-bar-center">${this._missionId}</span></span>
<span class="hud-readout"><span class="hud-top-bar-right">PAGE 1/1</span></span>
</div>
<div class="hud-corner hud-top-left">
<div class="hud-bracket">┌</div>
<div class="hud-content">
<div class="hud-classification">TOP SECRET // SI-TK // NOFORN</div>
<div class="hud-system">${this._missionId} ${this._sensorId}</div>
<div class="hud-mode" id="hud-mode">${t(MODE_LABELS.normal)}</div>
<div class="hud-readout"><div class="hud-classification">TOP SECRET // SI-TK // NOFORN</div></div>
<div class="hud-readout"><div class="hud-system">${this._missionId} ${this._sensorId}</div></div>
<div class="hud-readout"><div class="hud-mode" id="hud-mode">${t(MODE_LABELS.normal)}</div></div>
<div class="hud-summary-wrap">
<div class="hud-summary-label">${t('SUMMARY')}</div>
<div class="hud-summary" id="hud-summary">${t('Awaiting telemetry...')}</div>
<div class="hud-readout"><div class="hud-summary-label">${t('SUMMARY')}</div></div>
<div class="hud-readout"><div class="hud-summary" id="hud-summary">${t('Awaiting telemetry...')}</div></div>
</div>
</div>
</div>
<div class="hud-corner hud-top-right">
<div class="hud-content" style="text-align:right">
<div class="hud-rec"><span id="hud-rec-dot">●</span> ${t('REC')} <span id="hud-timestamp">2026-01-01 00:00:00Z</span></div>
<div class="hud-orbital">ORB: ${this._orbitNum} PASS: DESC-${this._passNum}</div>
<div class="hud-readout"><div class="hud-rec"><span id="hud-rec-dot">●</span> ${t('REC')} <span id="hud-timestamp">2026-01-01 00:00:00Z</span></div></div>
<div class="hud-readout"><div class="hud-orbital">ORB: ${this._orbitNum} PASS: DESC-${this._passNum}</div></div>
</div>
<div class="hud-bracket">┐</div>
</div>
@@ -311,33 +311,33 @@ export class IntelHUD {
<div class="hud-corner hud-bottom-left">
<div class="hud-bracket">└</div>
<div class="hud-content">
<div id="hud-mgrs">MGRS: ---</div>
<div id="hud-latlon">--°--'--"N ---°--'--"W</div>
<div class="hud-readout"><div id="hud-mgrs">MGRS: ---</div></div>
<div class="hud-readout"><div id="hud-latlon">--°--'--"N ---°--'--"W</div></div>
</div>
</div>
<div class="hud-corner hud-bottom-right">
<div class="hud-content" style="text-align:right">
<div id="hud-gsd">GSD: --m NIIRS: --</div>
<div id="hud-alt">ALT: --m SUN: --° EL</div>
<div id="hud-ais-vessel" class="hud-ais-vessel">${t('AIS: --')}</div>
<div class="hud-readout"><div id="hud-gsd">GSD: --m NIIRS: --</div></div>
<div class="hud-readout"><div id="hud-alt">ALT: --m SUN: --° EL</div></div>
<div class="hud-readout"><div id="hud-ais-vessel" class="hud-ais-vessel">${t('AIS: --')}</div></div>
</div>
<div class="hud-bracket">┘</div>
</div>
<div class="hud-edge hud-left-edge">
<div id="hud-coll">COLL: --:--:--Z</div>
<div id="hud-ona">ONA: --°</div>
<div class="hud-readout"><div id="hud-coll">COLL: --:--:--Z</div></div>
<div class="hud-readout"><div id="hud-ona">ONA: --°</div></div>
</div>
<div class="hud-edge hud-right-edge">
<div>BAND: PAN</div>
<div>BITS: 11</div>
<div>LVL: 1A</div>
<div class="hud-readout"><div>BAND: PAN</div></div>
<div class="hud-readout"><div>BITS: 11</div></div>
<div class="hud-readout"><div>LVL: 1A</div></div>
</div>
<div class="hud-bottom-bar">
<span id="hud-bottom-line">LAT: -- LON: -- MGRS: ---</span>
<span class="hud-readout"><span id="hud-bottom-line">LAT: -- LON: -- MGRS: ---</span></span>
</div>
`;
this._el.dataset.variant = this._variant;
@@ -574,11 +574,12 @@ export class IntelHUD {
* @returns {string} Formatted DMS string, e.g. `"38°53'23.10"N"`.
*/
_toDMS(decimal, type) {
const abs = Math.abs(decimal);
const deg = Math.floor(abs);
const minFloat = (abs - deg) * 60;
const min = Math.floor(minFloat);
const sec = ((minFloat - min) * 60).toFixed(2);
// Round once, in hundredths of a second, so a value that rounds up to
// 60.00" carries into the minutes and degrees instead of printing 60.
const hundredths = Math.round(Math.abs(decimal) * 360000);
const deg = Math.floor(hundredths / 360000);
const min = Math.floor(hundredths / 6000) % 60;
const sec = ((hundredths % 6000) / 100).toFixed(2);
let dir;
if (type === 'lat') dir = decimal >= 0 ? 'N' : 'S';
+106
View File
@@ -0,0 +1,106 @@
// The HUD's LAT/LON readout in degrees-minutes-seconds. Seconds are printed to
// two decimals, so a value within 0.005" of the next whole minute rounds up.
// That carry must reach the minutes (and degrees): `122°20'60.00"W` is not a
// coordinate. Ordinary decimal inputs land there through binary floating
// point: 122.35 - 122 is 0.34999999999999432, so -122.35 read 20'60.00".
//
// hud.js imports `mgrs`, a CommonJS package whose named exports Node's ESM
// loader cannot see, so this installs the same one-specifier stub as
// hudAltitudeDatum.test.mjs before importing it.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { registerHooks } from 'node:module';
const MGRS_STUB_URL = 'gev-test-stub:mgrs';
registerHooks({
resolve(specifier, context, next) {
if (specifier === 'mgrs') return { url: MGRS_STUB_URL, shortCircuit: true };
return next(specifier, context);
},
load(url, context, next) {
if (url === MGRS_STUB_URL) {
return {
format: 'module',
shortCircuit: true,
source:
'export function forward() { return "10SEG55776339"; }\nexport default { forward };\n',
};
}
return next(url, context);
},
});
const { IntelHUD } = await import('./hud.js');
const toDMS = (decimal, type) => IntelHUD.prototype._toDMS(decimal, type);
test('a rounded-up second carries into the minutes', () => {
assert.equal(toDMS(-122.35, 'lon'), `122°21'00.00"W`);
assert.equal(toDMS(139.7, 'lon'), `139°42'00.00"E`);
assert.equal(toDMS(37.0166666, 'lat'), `37°01'00.00"N`);
});
test('a carry through 59 minutes reaches the degrees', () => {
assert.equal(toDMS(38.999999, 'lat'), `39°00'00.00"N`);
assert.equal(toDMS(-0.999999, 'lat'), `01°00'00.00"S`);
assert.equal(toDMS(179.999999, 'lon'), `180°00'00.00"E`);
});
test('ordinary values keep their existing format', () => {
assert.equal(toDMS(38.889722, 'lat'), `38°53'23.00"N`);
assert.equal(toDMS(-77.035278, 'lon'), `077°02'07.00"W`);
assert.equal(toDMS(51.5, 'lat'), `51°30'00.00"N`);
assert.equal(toDMS(-33.8568, 'lat'), `33°51'24.48"S`);
assert.equal(toDMS(0, 'lon'), `000°00'00.00"E`);
assert.equal(toDMS(90, 'lat'), `90°00'00.00"N`);
});
test('the telemetry tick paints the carried value into both readouts', () => {
const elements = new Map(
[
'hud-alt',
'hud-summary',
'hud-mgrs',
'hud-latlon',
'hud-bottom-line',
'hud-gsd',
'hud-coll',
'hud-ona',
'hud-mode',
].map((id) => [id, { textContent: '' }]),
);
const previousDocument = globalThis.document;
globalThis.document = {
getElementById: (id) => elements.get(id) ?? null,
querySelectorAll: () => [],
addEventListener() {},
removeEventListener() {},
};
const viewer = {
camera: {
pitch: -Math.PI / 2,
positionCartographic: {
latitude: (37.75 * Math.PI) / 180,
longitude: (-122.35 * Math.PI) / 180,
height: 1500,
},
computeViewRectangle: () => undefined,
moveStart: { addEventListener() {}, removeEventListener() {} },
moveEnd: { addEventListener() {}, removeEventListener() {} },
},
};
let hud;
try {
hud = new IntelHUD(viewer);
hud._updateCameraData();
const latlon = elements.get('hud-latlon').textContent;
const bottom = elements.get('hud-bottom-line').textContent;
assert.equal(latlon, `37°45'00.00"N 122°21'00.00"W`);
assert.match(bottom, /LON: 122°21'00\.00"W/);
assert.doesNotMatch(`${latlon} ${bottom}`, /60\.00"/);
} finally {
hud?.destroy();
if (previousDocument === undefined) delete globalThis.document;
else globalThis.document = previousDocument;
}
});
+4
View File
@@ -1867,11 +1867,15 @@
"maneuver|Merge": "Incorpórate",
"Merge onto {road}": "Incorpórate a {road}",
"Take the ramp on the {direction}": "Toma la rampa {direction}",
"Take the ramp {direction}": "Toma la rampa {direction}",
"Take the ramp on the {direction} onto {road}": "Toma la rampa {direction} hacia {road}",
"Take the ramp {direction} onto {road}": "Toma la rampa {direction} hacia {road}",
"Take the ramp": "Toma la rampa",
"Take the ramp onto {road}": "Toma la rampa hacia {road}",
"Take the exit on the {direction}": "Toma la salida {direction}",
"Take the exit {direction}": "Toma la salida {direction}",
"Take the exit on the {direction} onto {road}": "Toma la salida {direction} hacia {road}",
"Take the exit {direction} onto {road}": "Toma la salida {direction} hacia {road}",
"Take the exit": "Toma la salida",
"Take the exit onto {road}": "Toma la salida hacia {road}",
"At the roundabout, take the {exit} exit": "En la rotonda, toma la {exit} salida",
+4
View File
@@ -1899,11 +1899,15 @@
"maneuver|Merge": "Перестройтесь",
"Merge onto {road}": "Въезжайте на {road}",
"Take the ramp on the {direction}": "Въезжайте на съезд {direction}",
"Take the ramp {direction}": "Въезжайте на съезд {direction}",
"Take the ramp on the {direction} onto {road}": "Въезжайте по съезду {direction} на {road}",
"Take the ramp {direction} onto {road}": "Въезжайте по съезду {direction} на {road}",
"Take the ramp": "Въезжайте на съезд",
"Take the ramp onto {road}": "Въезжайте по съезду на {road}",
"Take the exit on the {direction}": "Сверните на съезд {direction}",
"Take the exit {direction}": "Сверните на съезд {direction}",
"Take the exit on the {direction} onto {road}": "Сверните на съезд {direction} на {road}",
"Take the exit {direction} onto {road}": "Сверните на съезд {direction} на {road}",
"Take the exit": "Сверните на съезд",
"Take the exit onto {road}": "Сверните на съезд на {road}",
"At the roundabout, take the {exit} exit": "На кольце сверните на {exit} съезд",
+4
View File
@@ -626,11 +626,15 @@
"Merge onto {road}": "并入 {road}",
"maneuver|Merge": "并道",
"Take the ramp on the {direction} onto {road}": "{direction}驶入匝道,进入 {road}",
"Take the ramp {direction} onto {road}": "{direction}驶入匝道,进入 {road}",
"Take the ramp on the {direction}": "{direction}驶入匝道",
"Take the ramp {direction}": "{direction}驶入匝道",
"Take the ramp onto {road}": "驶入匝道,进入 {road}",
"Take the ramp": "驶入匝道",
"Take the exit on the {direction} onto {road}": "从出口{direction}驶出,进入 {road}",
"Take the exit {direction} onto {road}": "从出口{direction}驶出,进入 {road}",
"Take the exit on the {direction}": "从出口{direction}驶出",
"Take the exit {direction}": "从出口{direction}驶出",
"Take the exit onto {road}": "从出口驶出,进入 {road}",
"Take the exit": "从出口驶出",
"At the roundabout, take the {exit} exit onto {road}": "在环岛从{exit}出口驶出,进入 {road}",
+20 -5
View File
@@ -206,12 +206,27 @@ export function photonSearchUrl(
url.searchParams.set('limit', String(limit));
const corners = String(bias ?? '').split('|');
if (corners.length === 2) {
const [swLat, swLng] = corners[0].split(',').map(Number);
const [neLat, neLng] = corners[1].split(',').map(Number);
if ([swLat, swLng, neLat, neLng].every(Number.isFinite)) {
const tokens = corners.flatMap((corner) => corner.split(','));
if (
corners.length === 2 &&
corners.every((corner) => corner.split(',').length === 2) &&
tokens.every((token) => token.trim() !== '')
) {
const [swLat, swLng, neLat, neLng] = tokens.map(Number);
if (
[swLat, swLng, neLat, neLng].every(Number.isFinite) &&
Math.abs(swLat) <= 90 &&
Math.abs(neLat) <= 90 &&
swLat <= neLat &&
Math.abs(swLng) <= 180 &&
Math.abs(neLng) <= 180
) {
const midpoint = (swLng + neLng + (swLng > neLng ? 360 : 0)) / 2;
url.searchParams.set('lat', String((swLat + neLat) / 2));
url.searchParams.set('lon', String((swLng + neLng) / 2));
url.searchParams.set(
'lon',
String(midpoint > 180 ? midpoint - 360 : midpoint),
);
}
}
return url.toString();
+11
View File
@@ -452,3 +452,14 @@ function readGeocodingConsumer(file) {
? [file, 'annotations/resolver.js'] : [file];
return files.map((name) => fs.readFileSync(path.join(ROOT, 'src', name), 'utf8')).join('\n');
}
test('wrapped view bias stays near the antimeridian and malformed coordinates are omitted', () => {
for (const [bias, expected] of [['-20,170|-10,-171', 179.5], ['50,175|60,-165', -175], ['0,180|10,-180', 180], ['0,-180|10,180', 0]]) {
const params = new URL(photonSearchUrl('island', { bias })).searchParams;
assert.equal(Number(params.get('lon')), expected);
}
for (const bias of ['-91,0|0,1', '0,181|1,0', '10,0|0,1', ',|,', '0,1,2|3,4', '0,1|Infinity,2']) {
assert.equal(new URL(photonSearchUrl('island', { bias })).searchParams.has('lon'), false);
}
});
+10
View File
@@ -0,0 +1,10 @@
/**
* Format a compass direction for the awareness overlay.
* @param {number} degrees
* @returns {string}
*/
export function formatAwarenessDirection(degrees) {
if (!Number.isFinite(degrees)) return '—';
const normalized = ((degrees % 360) + 360) % 360;
return String(Math.round(normalized) % 360).padStart(3, '0');
}
@@ -0,0 +1,11 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { formatAwarenessDirection } from './presentation.js';
test('awareness bearings that round through north read 000, not 360', () => {
assert.equal(formatAwarenessDirection(359.4), '359');
assert.equal(formatAwarenessDirection(359.6), '000');
assert.equal(formatAwarenessDirection(0), '000');
assert.equal(formatAwarenessDirection(-0.4), '000');
assert.equal(formatAwarenessDirection(Number.NaN), '—');
});
+3 -2
View File
@@ -11,6 +11,7 @@ import {
} from '../../data/militaryAwarenessEngine.js';
import { bearingBetweenCoordinates } from '../../cockpitMath.js';
import { AWARENESS_PAGE_SIZE, CONTEXT_RIM_HEIGHT_M } from './policy.js';
import { formatAwarenessDirection } from './presentation.js';
export function createRendering({
state: layerState,
@@ -225,12 +226,12 @@ export function createRendering({
: null;
const bearingText = Number.isFinite(bearing)
? t('BRG {value}°', {
value: String(Math.round(bearing)).padStart(3, '0'),
value: formatAwarenessDirection(bearing),
})
: t('BRG —');
const courseText = Number.isFinite(item.track)
? ` · ${t('CRS {value}°', {
value: String(Math.round(item.track)).padStart(3, '0'),
value: formatAwarenessDirection(item.track),
})}`
: '';
marker._label.textContent = `${label} · ${formatAwarenessDistance(item.distanceM)}\n${bearingText}${courseText}`;
+19 -5
View File
@@ -308,16 +308,24 @@ export function directionsInstruction(step) {
? t('Merge onto {road}', values)
: t('Merge', { context: 'maneuver' });
case 'on ramp':
if (direction)
if (modifier === 'left' || modifier === 'right')
return road
? t('Take the ramp on the {direction} onto {road}', values)
: t('Take the ramp on the {direction}', values);
if (direction && modifier !== 'straight')
return road
? t('Take the ramp {direction} onto {road}', values)
: t('Take the ramp {direction}', values);
return road ? t('Take the ramp onto {road}', values) : t('Take the ramp');
case 'off ramp':
if (direction)
if (modifier === 'left' || modifier === 'right')
return road
? t('Take the exit on the {direction} onto {road}', values)
: t('Take the exit on the {direction}', values);
if (direction && modifier !== 'straight')
return road
? t('Take the exit {direction} onto {road}', values)
: t('Take the exit {direction}', values);
return road ? t('Take the exit onto {road}', values) : t('Take the exit');
case 'roundabout':
case 'rotary':
@@ -346,9 +354,14 @@ export function directionsInstruction(step) {
return road
? t('Exit the roundabout onto {road}', values)
: t('Exit the roundabout');
case 'use lane':
if (direction) {
const lane = { road, direction: laneWord(modifier) };
case 'use lane': {
const side = modifier.includes('left')
? 'left'
: modifier.includes('right')
? 'right'
: '';
if (side) {
const lane = { road, direction: laneWord(side) };
return road
? t('Use the {direction} lane onto {road}', lane)
: t('Use the {direction} lane', lane);
@@ -356,6 +369,7 @@ export function directionsInstruction(step) {
return road
? t('Continue onto {road}', values)
: t('Continue', { context: 'maneuver' });
}
default:
return road
? t('Continue onto {road}', values)
+1 -1
View File
@@ -152,7 +152,7 @@ export function localAdsbCardModel(
t('ALT {altitude} FT · GS {speed} KT · TRK {track}', {
altitude: grouped(altitude),
speed: speed === null ? DASH : speed,
track: track === null ? DASH : `${track}°`,
track: track === null ? DASH : `${track % 360}°`,
}),
vertical === null
? t('V/S {rate}', { rate: DASH })
+19
View File
@@ -0,0 +1,19 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { setLocale } from '../../i18n/index.js';
import { localAdsbCardModel } from './card.js';
test('a track of 359.7° reads TRK 0°, not TRK 360°', () => {
setLocale('en');
const { details } = localAdsbCardModel(
{
icao: 'abc123',
altitudeFt: 12500,
groundSpeedKt: 240,
trackDeg: 359.7,
lastPositionAt: 1000,
},
5000,
);
assert.equal(details[1], 'ALT 12,500 FT · GS 240 KT · TRK 0°');
});
+1 -1
View File
@@ -19,7 +19,7 @@ import { VectorTile } from '@mapbox/vector-tile';
* tree-shakeable; decoding happens client-side so the proxy stays a dumb
* binary cache.
*
* @module data/flowTiles
* @module layers/traffic/flowDecode
*/
const FLOW_LAYER_NAME = 'Traffic flow';
+19 -2
View File
@@ -14,6 +14,8 @@ const GLOBAL_TILE_MAXIMUM_LEVEL = 3;
// Six decoded 2048×1024 canvases (8 MiB each) bound the cache below 50 MiB.
const MAX_MOSAICS = 6;
const MAX_PREFETCH_TILES = 8;
// How often a staged frame waiting out its quiet interval rechecks readiness.
export const READY_WAKE_MS = 150;
/** Globe imagery: own at most a displayed and a staging frame. Use native Cesium
* tile scheduling, projection and texture disposal; the application clock is never touched. */
@@ -126,6 +128,7 @@ function createGlobeRendering({
frame.offRender?.();
frame.offCamera?.();
clearTimeout(frame.timeout);
clearTimeout(frame.wake);
for (const request of frame.requests) request.cancel?.();
frame.requests.clear();
frame.retries.clear();
@@ -277,6 +280,8 @@ function createGlobeRendering({
frame.offCamera?.();
frame.offCamera = null;
clearTimeout(frame.timeout);
clearTimeout(frame.wake);
frame.wake = null;
frame.offAbort?.();
if (ok) {
lastError = null;
@@ -450,8 +455,20 @@ function createGlobeRendering({
if (++settled >= 2) finish(true);
else viewer.scene.requestRender();
} else settled = 0;
if (incoming === frame && frame.pending === 0 && frame.loaded > 0)
viewer.scene.requestRender();
// Waiting out the quiet interval needs another readiness check
// later, not a render every frame: wake on a timer so the render
// governor's frame cap still holds while a frame is staged.
if (
incoming === frame &&
frame.pending === 0 &&
frame.loaded > 0 &&
!frame.wake
)
frame.wake = setTimeout(() => {
frame.wake = null;
if (!frame.closed && incoming === frame)
viewer.scene.requestRender();
}, READY_WAKE_MS);
});
viewer.scene.requestRender();
onChange();
+45 -1
View File
@@ -1,6 +1,6 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { createWeatherRendering } from './rendering.js';
import { createWeatherRendering, READY_WAKE_MS } from './rendering.js';
import { createWeatherLayer } from './index.js';
import { createWeatherClock } from './clock.js';
import * as Cesium from 'cesium';
@@ -242,6 +242,50 @@ test('a ready weather frame does not wait for unrelated terrain, but requires su
h.rendering.clear();
});
test('a staged frame waiting out its quiet interval wakes on a timer, not every render', async (t) => {
t.mock.timers.enable({ apis: ['setTimeout'] });
let now = 0;
const h = renderingHarness({ now: () => now });
h.viewer.scene.globe.tilesLoaded = false;
const loaded = h.rendering.setFrame(snapshot, times[0]);
const tile = deferred();
h.providers[0].response = tile;
const request = h.providers[0].requestImage(0, 0, 0, {});
tile.resolve({});
await request;
const before = h.renders();
for (let i = 0; i < 10; i++) h.postRender.emit();
assert.equal(
h.renders(),
before,
'renders while waiting do not request further renders',
);
t.mock.timers.tick(READY_WAKE_MS - 1);
assert.equal(h.renders(), before);
t.mock.timers.tick(1);
assert.equal(h.renders(), before + 1, 'one wake per interval');
now = 200;
h.postRender.emit();
h.postRender.emit();
assert.equal(await loaded, true);
const settledRenders = h.renders();
t.mock.timers.tick(READY_WAKE_MS * 4);
assert.equal(h.renders(), settledRenders, 'commit clears the wake timer');
const abandoned = h.rendering.setFrame(snapshot, times[1]);
const second = deferred();
h.providers.at(-1).response = second;
const secondRequest = h.providers.at(-1).requestImage(0, 0, 0, {});
second.resolve({});
await secondRequest;
h.postRender.emit();
h.rendering.clear();
assert.equal(await abandoned, false);
const clearedRenders = h.renders();
t.mock.timers.tick(READY_WAKE_MS * 4);
assert.equal(h.renders(), clearedRenders, 'clear cancels the wake timer');
});
test('camera movement drops abandoned deferred tiles but still waits for admitted requests', async () => {
let now = 0;
const h = renderingHarness({ now: () => now });
+1
View File
@@ -38,6 +38,7 @@ export function createGoogleTokenSource({
headers: { Accept: 'application/json' },
cache: 'no-store',
});
if (response.status === 204) return null;
if (!response.ok) {
await response.body?.cancel();
return null;
+9
View File
@@ -79,3 +79,12 @@ test('a server that does not offer tokens yields none', async () => {
assert.equal(await createGoogleTokenSource({ fetchImpl }).token(), null);
}
});
test('an empty 204 response explicitly means no Google token is available', async () => {
const { calls, fetchImpl } = server([
new Response(null, { status: 204, headers: { 'Cache-Control': 'no-store' } }),
]);
const tokens = createGoogleTokenSource({ fetchImpl });
assert.equal(await tokens.token(), null);
assert.deepEqual(calls, [GOOGLE_TOKEN_PATH]);
});
+3 -1
View File
@@ -166,6 +166,8 @@ export function nominatimViewboxFromBounds(bounds) {
const neLat = nominatimLatitude(match[3]);
const neLng = nominatimLongitude(match[4]);
if ([swLat, swLng, neLat, neLng].some((value) => value === null)) return null;
if (swLat >= neLat) return null;
// One Nominatim viewbox cannot express a wrapped interval. Omit this soft
// preference rather than biasing the search toward the opposite hemisphere.
if (swLat >= neLat || swLng > neLng) return null;
return `${swLng},${neLat},${neLng},${swLat}`;
}
+9
View File
@@ -173,3 +173,12 @@ test('a viewport hint is sent only when every edge is a real coordinate', () =>
`${JSON.stringify(bounds)} must not become a viewbox`,
);
});
test('wrapped viewports omit the Nominatim bias without rejecting the search', () => {
assert.equal(nominatimViewboxFromBounds('-20,170|-10,-171'), null);
assert.equal(nominatimViewboxFromBounds('50,175|60,-165'), null);
assert.equal(
nominatimViewboxFromBounds('-20,170|-10,180'),
'170,-10,180,-20',
);
});
+1 -1
View File
@@ -2,7 +2,7 @@ import test from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { LOCAL_OVERLAY_COHORT_LIMIT } from '../data/localGeojson.js';
import { LOCAL_OVERLAY_COHORT_LIMIT } from '../data/localGeojsonCore.js';
import { FIRMS_AMBIENT_COHORT_LIMIT } from '../data/firmsLabels.js';
import { vesselOverlayCohortLimit } from '../data/vesselLabels.js';
import { CCTV_AMBIENT_CARD_MAX } from '../data/cctvLod.js';
+3
View File
@@ -113,6 +113,9 @@ test('Pinokio menu resolves the nested install marker and exposes each lifecycle
runtime.url = url;
const items = await menu(kernel, info);
assert.equal(items.filter((item) => item.default).length, 1);
const ready = installed && running === 'start.js' && Boolean(url);
assert.equal(items[0].popout, ready ? true : undefined);
assert.ok(items.slice(1).every((item) => item.popout === undefined));
return items.map(({ text, href, default: isDefault = false }) => ({ text, href, default: isDefault }));
};
+31 -18
View File
@@ -11,9 +11,8 @@ import { expandApplicationHtml } from '../build/application-html.js';
// actually decides fresh-boot layer state — including the early return that
// makes each layer's own initializer the operative default.
// 2. Scope feather. Owner: "I like to hide feather" set it to 0% on
// 2026-08-22; the owner revised that to 8% on 2026-08-23 and locked 11%
// on 2026-08-24, a soft
// edge. The hard crop is still one drag away, and that is pinned too.
// 2026-08-22; later revisions were 8%, then 11%, then 100% on
// 2026-10-25. Scope stays ON on a fresh boot. The hard crop remains available.
// 3. Detection ON (Dense @ 75%) for EVERY style, Normal included.
// Owner: "detect should also be on by default."
// 4. Detection OUTSIDE opacity 1% (owner final lock, 2026-08-24; 3% on 08-23, 5% before), with
@@ -38,6 +37,7 @@ import { AIRCRAFT_BRACKET_FLOOR_ANCHOR } from './data/detectionPolicy.js';
import {
SCOPE_FEATHER_RATIO_DEFAULT,
getScopeMaskFeather,
isScopeMaskEnabled,
scopeMaskGeometry,
setScopeMaskFeather,
} from './scopeMask.js';
@@ -75,32 +75,40 @@ function managerForHash(hash) {
}
// ---------------------------------------------------------------------------
// 2. Scope feather — a subtle soft edge on a first run
// 2. Scope ON with 100% feather on a first run
// ---------------------------------------------------------------------------
test('first run opens with a subtle scope feather, at every surface that decides it', () => {
assert.equal(SCOPE_FEATHER_RATIO_DEFAULT, 0.11,
'owner final lock 2026-08-24, superseding the 08-22 hard-crop and 08-23 8% rulings');
assert.equal(getScopeMaskFeather(), 0.11,
test('first run opens with Scope ON and 100% feather at every default surface', () => {
assert.equal(isScopeMaskEnabled(), true, 'the live mask starts enabled');
assert.equal(SCOPE_FEATHER_RATIO_DEFAULT, 1, 'owner preference 2026-10-25');
assert.equal(getScopeMaskFeather(), 1,
'and the live module starts there, not merely documents it');
// The slider and its readout are the same default rendered as markup — a
// fresh boot applies no restore, so a stale value here would show one number
// over a mask drawn at another.
assert.match(indexHtml, /id="scope-feather-slider"[^>]*\svalue="11"/,
'index.html: the feather slider ships at 11');
assert.match(indexHtml, /id="scope-feather-value"[^>]*>11%</,
assert.match(indexHtml, /id="scope-feather-slider"[^>]*\svalue="100"/,
'index.html: the feather slider ships at 100');
assert.match(indexHtml, /id="scope-feather-value"[^>]*>100%</,
'index.html: and its readout agrees with the handle');
assert.match(indexHtml, /<input type="checkbox" id="scope-toggle"[^>]*\schecked\s*\/>/,
'the native Scope checkbox starts checked');
// The link this session generates must describe the mask this session draws,
// for the window before the first _syncShareState.
assert.match(shareSource, /this\._scopeFeatherPct = 11;/,
assert.match(shareSource, /this\._scopeFeatherPct = 100;/,
'sharelink.js: the generator starts from the same value the mask starts at');
const params = managerForHash('')._buildHashParams();
assert.equal(params.get('sc'), '1');
assert.equal(params.get('scf'), '100');
});
test('an explicit feather still wins over the subtle default', () => {
test('an explicit feather still wins over the 100% default', () => {
// A link is authored state. The new default governs a session that said
// nothing; it must never overwrite one that said something.
assert.equal(managerForHash('#lat=10&lon=20&scf=11').parseInitialHash().scopeFeatherPct, 11,
'links carrying the previous default preserve their authored feather');
assert.equal(managerForHash('#lat=10&lon=20&scf=35').parseInitialHash().scopeFeatherPct, 35);
assert.equal(managerForHash('#lat=10&lon=20&scf=64').parseInitialHash().scopeFeatherPct, 64);
assert.equal(managerForHash('#lat=10&lon=20&scf=0').parseInitialHash().scopeFeatherPct, 0,
@@ -113,23 +121,28 @@ test('an explicit feather still wins over the subtle default', () => {
'a pre-scf link restores the author\'s view, not the new default');
});
test('the subtle default did not weaken the feather control, and 0 is still reachable', () => {
test('share links preserve explicit Scope OFF and feather choices over the defaults', () => {
const shared = managerForHash('#lat=10&lon=20&sc=0&scf=11').parseInitialHash();
assert.equal(shared.scopeEnabled, false);
assert.equal(shared.scopeFeatherPct, 11);
});
test('the 100% default retains the expanded feather range and hard edge', () => {
// The cheap way to move a default would be to nerf the control. Prove the
// slider still spans its full range and the geometry is still DERIVED from
// the ratio — a check that would pass vacuously if it only ever saw one value.
assert.match(indexHtml, /id="scope-feather-slider"[^>]*\smin="0"[^>]*\smax="100"/,
assert.match(indexHtml, /id="scope-feather-slider"[^>]*\smin="0"[^>]*\smax="200"/,
'the slider still offers the whole range');
const previous = getScopeMaskFeather();
try {
for (const ratio of [0.35, 0.7, 1]) {
for (const ratio of [0.35, 0.7, 1, 1.75, 2]) {
setScopeMaskFeather(ratio);
const geo = scopeMaskGeometry(1200, 900);
const keyholeR = 900 * 0.5 * KEYHOLE_OUTER_RADIUS;
assert.ok(Math.abs((geo.outerR - geo.innerR) - keyholeR * ratio) < 1e-9,
`feather ${ratio} must still widen the band to that fraction of the keyhole`);
}
// The new default is a real, narrow band — not the hard crop, and nowhere
// near the retired 35 % halo.
// The first-run value draws a band derived from the same keyhole radius.
setScopeMaskFeather(SCOPE_FEATHER_RATIO_DEFAULT);
const soft = scopeMaskGeometry(1200, 900);
const keyholeR = 900 * 0.5 * KEYHOLE_OUTER_RADIUS;
+25 -21
View File
@@ -1,4 +1,5 @@
import { getKeyholeGeometry } from './celestialRing.js';
import { SCOPE_FEATHER_RATIO_MAX } from './scopeMaskPolicy.js';
/**
* Scope mask — the app's signature circular viewport treatment, made real.
@@ -41,25 +42,10 @@ import { getKeyholeGeometry } from './celestialRing.js';
/** Matches the page background the emergent scope faded into. */
const SCOPE_OUTSIDE_COLOR = { r: 5, g: 5, b: 8 };
/**
* Default edge feather as a fraction of the keyhole radius.
*
* 0.11 since 2026-08-24 (owner final lock; 0.08 on 08-23, hard-crop 0 on
* 08-22 — this supersedes both), REVISING the 2026-08-22 ruling that
* set it to zero: a subtle soft edge rather than either the hard crop or the
* retired 35 % halo. The slider is untouched and still spans 0..100; this is
* only where it STARTS. `setScopeMaskFeather` is unchanged, so any feather a
* share link carries, or the operator dials in, overrides this immediately, and
* the hard-crop path at 0 is still reachable from the handle.
*
* Keep in lockstep with `#scope-feather-slider`'s markup value AND readout in
* index.html and `_scopeFeatherPct` in sharelink.js — a fresh boot applies no
* restore, so those literals ARE the first-run state. NOT the `scf` PARSE
* fallback, which stays at 35 on purpose: a link predating that field was
* authored when 35 was what its author saw, and a link from the feather-0 era
* carries `scf=0` explicitly because the generator always writes the field.
* Pinned in reasonableDefaults.test.mjs.
* First-run feather ratio; the Display slider and share defaults use 100%.
* The historical missing-scf parse fallback remains 35%.
*/
export const SCOPE_FEATHER_RATIO_DEFAULT = 0.11;
export const SCOPE_FEATHER_RATIO_DEFAULT = 1;
/**
* Terminus opacity at/above SCOPE_TERMINUS_FAR_M — slightly translucent so
* faint stars survive in the corners at globe scale.
@@ -344,7 +330,10 @@ function teardownDevicePixelRatioWatch() {
export function scopeMaskGeometry(width, height, featherRatio = _featherRatio) {
const keyhole = getKeyholeGeometry(width, height);
if (!(keyhole.radius > 0)) return null;
const feather = Math.max(0, Math.min(1, Number(featherRatio) || 0));
const feather = Math.max(
0,
Math.min(SCOPE_FEATHER_RATIO_MAX, Number(featherRatio) || 0),
);
// Feather straddles the keyhole edge so the visible radius stays anchored
// to the shared geometry every keyhole consumer (labels, cards) fades on.
const half = keyhole.radius * feather * 0.5;
@@ -423,6 +412,14 @@ function draw() {
_painted = true;
}
/** Expose only the OFF state; an absent attribute preserves the original HUD. */
function syncScopeMaskAppearance(enabled = _enabled) {
const dataset = globalThis.document?.documentElement?.dataset;
if (!dataset) return;
if (enabled) delete dataset.scopeMask;
else dataset.scopeMask = 'off';
}
/**
* Install the scope mask into the viewer container. Idempotent.
* @param {Cesium.Viewer} viewer
@@ -430,6 +427,7 @@ function draw() {
*/
export function installScopeMask(viewer) {
if (_canvas || !viewer?.container) return;
syncScopeMaskAppearance();
_container = viewer.container;
_viewer = viewer;
_canvas = document.createElement('canvas');
@@ -505,6 +503,7 @@ export function setScopeMaskEnabled(enabled) {
const next = Boolean(enabled);
const reEnabled = next && !_enabled;
_enabled = next;
syncScopeMaskAppearance();
if (reEnabled) {
// The camera moved freely while the scope was off and nothing sampled it,
// so the painted terminus can be a whole altitude band stale. Re-sync once
@@ -522,11 +521,14 @@ export function isScopeMaskEnabled() {
}
/**
* @param {number} ratio - Edge feather as a fraction of keyhole radius [0,1].
* @param {number} ratio - Edge feather as a fraction of keyhole radius [0,2].
* @returns {void}
*/
export function setScopeMaskFeather(ratio) {
_featherRatio = Math.max(0, Math.min(1, Number(ratio) || 0));
_featherRatio = Math.max(
0,
Math.min(SCOPE_FEATHER_RATIO_MAX, Number(ratio) || 0),
);
draw();
}
@@ -540,6 +542,7 @@ export function getScopeMaskFeather() {
* @returns {void}
*/
export function destroyScopeMask() {
syncScopeMaskAppearance(true);
_resizeObserver?.disconnect();
_resizeObserver = null;
teardownDevicePixelRatioWatch();
@@ -553,6 +556,7 @@ export function destroyScopeMask() {
/** Test seam. */
export function _resetScopeMaskForTest() {
syncScopeMaskAppearance(true);
_resizeObserver?.disconnect();
_resizeObserver = null;
teardownDevicePixelRatioWatch();
+50 -5
View File
@@ -18,6 +18,7 @@ import {
SCOPE_TERMINUS_MIN_PCT,
SCOPE_TERMINUS_MAX_PCT,
SCOPE_FEATHER_RATIO_DEFAULT,
getScopeMaskFeather,
SCOPE_OUTSIDE_ALPHA,
SCOPE_TERMINUS_ALPHA_NEAR,
SCOPE_TERMINUS_FAR_M,
@@ -26,6 +27,7 @@ import {
_resetScopeMaskForTest,
} from './scopeMask.js';
import { KEYHOLE_OUTER_RADIUS } from './celestialRing.js';
import { SCOPE_FEATHER_RATIO_MAX, clampScopeFeatherPct } from './scopeMaskPolicy.js';
beforeEach(() => _resetScopeMaskForTest());
@@ -47,16 +49,34 @@ test('zero feather produces a hard edge exactly at the keyhole radius', () => {
assert.equal(geo.outerR, keyholeR);
});
test('feather ratio is clamped to [0,1] and bad input falls back to 0', () => {
test('feather ratio reaches the center at 200% and clamps larger input', () => {
const wide = scopeMaskGeometry(1000, 800, 7);
const one = scopeMaskGeometry(1000, 800, 1);
assert.equal(wide.innerR, one.innerR);
assert.equal(wide.outerR, one.outerR);
const maximum = scopeMaskGeometry(1000, 800, SCOPE_FEATHER_RATIO_MAX);
const keyholeR = 800 * 0.5 * KEYHOLE_OUTER_RADIUS;
assert.equal(maximum.innerR, 0);
assert.equal(maximum.outerR, keyholeR * 2);
assert.equal(wide.innerR, maximum.innerR);
assert.equal(wide.outerR, maximum.outerR);
for (const ratio of [1, 1.5, 2]) {
setScopeMaskFeather(ratio);
assert.equal(getScopeMaskFeather(), ratio);
const geometry = scopeMaskGeometry(1000, 800);
assert.equal((geometry.innerR + geometry.outerR) / 2, keyholeR);
assert.equal(geometry.outerR - geometry.innerR, keyholeR * ratio);
}
setScopeMaskFeather(7);
assert.equal(getScopeMaskFeather(), SCOPE_FEATHER_RATIO_MAX);
const nan = scopeMaskGeometry(1000, 800, Number.NaN);
const zero = scopeMaskGeometry(1000, 800, 0);
assert.equal(nan.innerR, zero.innerR);
});
test('UI and share feather percentages share one bounded integer range', () => {
for (const [value, expected] of [[0, 0], [11, 11], [175.6, 176], [200, 200], [350, 200], [-4, 0], [Number.NaN, 0]]) {
assert.equal(clampScopeFeatherPct(value), expected);
}
});
test('degenerate viewport yields null instead of a broken gradient', () => {
assert.equal(scopeMaskGeometry(0, 0), null);
assert.equal(scopeMaskGeometry(-5, 100), null);
@@ -136,12 +156,14 @@ function stubScopeMaskDom({ width = 1000, height = 800, dpr = 1 } = {}) {
removeEventListener: (type, fn) => listeners.delete(fn),
}),
};
globalThis.document = { createElement: () => canvas };
const documentElement = { dataset: {} };
globalThis.document = { documentElement, createElement: () => canvas };
globalThis.ResizeObserver = class { observe() {} disconnect() {} };
const container = { clientWidth: width, clientHeight: height, appendChild() {} };
return {
canvas,
container,
documentElement,
/** Canvas work counters: backing-store resizes, clears, fills. */
ops,
/** Every fillStyle string assigned since install (hard-crop path). */
@@ -160,6 +182,29 @@ function stubScopeMaskDom({ width = 1000, height = 800, dpr = 1 } = {}) {
};
}
test('scope appearance follows direct state changes and install/destroy lifecycles', () => {
const dom = stubScopeMaskDom();
try {
setScopeMaskEnabled(false);
assert.equal(dom.documentElement.dataset.scopeMask, 'off');
delete dom.documentElement.dataset.scopeMask;
installScopeMask({ container: dom.container });
assert.equal(dom.documentElement.dataset.scopeMask, 'off', 'install publishes retained OFF state');
destroyScopeMask();
assert.equal(dom.documentElement.dataset.scopeMask, undefined, 'destroy clears the document marker');
installScopeMask({ container: dom.container });
assert.equal(dom.documentElement.dataset.scopeMask, 'off', 'reinstall retains the preference');
setScopeMaskEnabled(true);
assert.equal(dom.documentElement.dataset.scopeMask, undefined, 'ON uses the original presentation');
setScopeMaskEnabled(false);
_resetScopeMaskForTest();
assert.equal(dom.documentElement.dataset.scopeMask, undefined, 'test reset clears the marker');
} finally {
destroyScopeMask();
dom.restore();
}
});
test('a DPR change with no resize still repaints the backing store', () => {
const dom = stubScopeMaskDom({ width: 1000, height: 800, dpr: 1 });
try {
+11
View File
@@ -0,0 +1,11 @@
/** A 200% feather reaches the center while keeping its midpoint on the keyhole. */
export const SCOPE_FEATHER_RATIO_MAX = 2;
const SCOPE_FEATHER_MAX_PCT = SCOPE_FEATHER_RATIO_MAX * 100;
/** Normalize a UI/share-link feather percentage to the supported integer range. */
export function clampScopeFeatherPct(value) {
return Math.max(
0,
Math.min(SCOPE_FEATHER_MAX_PCT, Math.round(Number(value) || 0)),
);
}
+12 -2
View File
@@ -307,10 +307,20 @@ export function createGroundSnap({ groundFloor }) {
}
const now = Date.now();
const entry = entries.get(icao);
if (entry && now < entry.nextRetryMs) return heldSnapM(entry, surfacePos); // backoff in force
// Backoff in force, unless it ends further off than any backoff can:
// a retry stamped before the clock stepped back would otherwise wait for
// the clock to catch up.
if (
entry &&
now < entry.nextRetryMs &&
entry.nextRetryMs - now <= RETRY_MAX_MS
)
return heldSnapM(entry, surfacePos);
// Per-window budget: deny WITHOUT a retry stamp so the overflow simply
// tries again next tick instead of waiting out a backoff it didn't earn.
if (now - windowStartMs > SAMPLE_WINDOW_MS) {
// A clock that stepped back also opens a new window, or none would open
// until it caught up.
if (now < windowStartMs || now - windowStartMs > SAMPLE_WINDOW_MS) {
windowStartMs = now;
windowCount = 0;
}
+62
View File
@@ -0,0 +1,62 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import * as Cesium from 'cesium';
import { createGroundSnap } from './groundSnap.js';
const groundFloor = {
cachedMeshFloor: () => null,
meshFloorPreferred: () => false,
reportValidatedMeshFloorCell: () => {},
};
/** A viewer whose ground is a loaded 3D tileset, counting height samples. */
function makeViewer() {
const calls = { samples: 0 };
const tileset = { show: true, tilesLoaded: true };
return {
calls,
scene: {
primitives: { length: 1, get: () => tileset },
sampleHeight() {
calls.samples += 1;
return 1650;
},
globe: { getHeight: () => 1600 },
},
};
}
const atDenverAirport = Cesium.Cartesian3.fromDegrees(-104.6737, 39.8561, 0);
test('a clock that steps back opens a new sample window instead of denying samples until it catches up', (t) => {
t.mock.timers.enable({ apis: ['Date'], now: 1_000_000 });
const viewer = makeViewer();
const snap = createGroundSnap({ groundFloor });
const icaos = ['a00001', 'a00002', 'a00003', 'a00004', 'a00005', 'a00006'];
for (const icao of icaos) snap.heightFor(viewer, icao, atDenverAirport);
assert.equal(viewer.calls.samples, 4, 'four samples per 250 ms window');
t.mock.timers.setTime(1_000_000 - 60_000);
for (const icao of icaos) snap.heightFor(viewer, icao, atDenverAirport);
assert.equal(
viewer.calls.samples,
6,
'the two denied aircraft sample in the new window',
);
});
test('a retry backoff stamped before the clock stepped back does not wait for it to catch up', (t) => {
t.mock.timers.enable({ apis: ['Date'], now: 10_000_000 });
const viewer = makeViewer();
const tileset = viewer.scene.primitives.get(0);
const snap = createGroundSnap({ groundFloor });
// Tiles still streaming: a miss, backed off for 2 s.
tileset.tilesLoaded = false;
assert.equal(snap.heightFor(viewer, 'a00001', atDenverAirport), null);
tileset.tilesLoaded = true;
assert.equal(snap.heightFor(viewer, 'a00001', atDenverAirport), null);
assert.equal(viewer.calls.samples, 0, 'the backoff holds');
// The clock steps back an hour: the stamp is an hour off, past any backoff.
t.mock.timers.setTime(10_000_000 - 3_600_000);
assert.equal(snap.heightFor(viewer, 'a00001', atDenverAirport), 1650);
assert.equal(viewer.calls.samples, 1);
});
+18 -2
View File
@@ -99,6 +99,22 @@ test('Nepal locator token is valid in v2 share links', () => {
assert.equal(parsed.layerStateInvalid, false);
});
test('scope feather above 100% survives share parsing and serialization', () => {
const manager = makeManager('#v=2&lat=10&lon=20&sc=0&scf=200');
const parsed = manager.parseInitialHash();
assert.equal(parsed.scopeEnabled, false);
assert.equal(parsed.scopeFeatherPct, 200);
assert.equal(makeManager('#lat=10&lon=20&scf=175').parseInitialHash().scopeFeatherPct, 175);
assert.equal(makeManager('#lat=10&lon=20&scf=999').parseInitialHash().scopeFeatherPct, 200);
manager.onToggleChange(false, true, { scopeEnabled: false, scopeFeatherPct: 200 });
const params = manager._buildHashParams();
assert.equal(params.get('sc'), '0');
assert.equal(params.get('scf'), '200');
manager.onToggleChange(false, true, { scopeFeatherPct: 999 });
assert.equal(manager._buildHashParams().get('scf'), '200');
clearTimeout(manager._debounceTimer);
});
test('share-link serialization emits the current celestial state', () => {
const manager = makeManager();
manager.onToggleChange(false, false, { celestialRingEnabled: false });
@@ -498,8 +514,8 @@ test('every explicit visual UI gesture claims restore authority before it mutate
['cycleDetection: () => {', 'toggleCctv: () =>', 'cycleDetectionMode()', 'detection hotkey'],
['toggleBloom:', 'setBloomIntensity:', 'this._setBloomEnabled(', 'toggleBloom control'],
['setBloomIntensity:', 'toggleSharpen:', 'this._setBloomIntensity(', 'setBloomIntensity control'],
['toggleSharpen:', 'toggleScope:', 'this._setSharpenEnabled(', 'toggleSharpen control'],
['toggleScope:', 'setScopeFeather:', 'setScopeMaskEnabled(', 'toggleScope control'],
['toggleSharpen:', 'setScopeEnabled:', 'this._setSharpenEnabled(', 'toggleSharpen control'],
['setScopeEnabled:', 'setScopeFeather:', 'setScopeMaskEnabled(', 'setScopeEnabled control'],
['setScopeFeather:', 'setSharpenIntensity:', 'setScopeMaskFeather(', 'setScopeFeather control'],
['setSharpenIntensity:', 'setHudLayout:', 'this._applySharpenIntensity(', 'setSharpenIntensity control'],
['setHudLayout:', 'toggleCleanView:', 'this._setHudVariant(', 'setHudLayout control'],
+4 -12
View File
@@ -5,6 +5,7 @@ import {
normalizeAllocationStrategy,
} from './data/detectionPolicy.js';
import { clampScopeTerminusPct } from './scopeMask.js';
import { clampScopeFeatherPct } from './scopeMaskPolicy.js';
import {
decodeLayerStateParams,
encodeLayerStateParams,
@@ -112,10 +113,7 @@ export class ShareLinkManager {
this._detectionOutsideOpacityPct = 1;
this._celestialRingEnabled = false;
this._scopeEnabled = true;
// Feather opens on a soft 11% scope-mask edge (owner final lock 2026-08-24,
// superseding the 08-22 hard-crop and 08-23 8% rulings) — mirrors
// SCOPE_FEATHER_RATIO_DEFAULT in scopeMask.js and the slider's markup value.
this._scopeFeatherPct = 11;
this._scopeFeatherPct = 100;
// null = the altitude-adaptive terminus (the default). A number pins the
// outside-fill opacity as a percent, 94..100. (`sce`, 2026-08-17)
this._scopeTerminusPct = null;
@@ -227,10 +225,7 @@ export class ShareLinkManager {
// (`_scopeFeatherPct` in the constructor tracks the default: that one
// mirrors live state for the link this session generates, so it must match
// the mask, not the archive.)
scopeFeatherPct: Math.max(
0,
Math.min(100, Math.round(parseOr(params.get('scf'), 35))),
),
scopeFeatherPct: clampScopeFeatherPct(parseOr(params.get('scf'), 35)),
// Absent (or non-numeric) `sce` = adaptive (null), the default behavior;
// a value pins the terminus opacity percent, clamped into the SUPPORTED
// 94..100 band. `sce=0` used to survive as a sub-94 terminus — a hole in
@@ -494,10 +489,7 @@ export class ShareLinkManager {
if (typeof extras.scopeEnabled === 'boolean')
this._scopeEnabled = extras.scopeEnabled;
if (typeof extras.scopeFeatherPct === 'number') {
this._scopeFeatherPct = Math.max(
0,
Math.min(100, Math.round(extras.scopeFeatherPct)),
);
this._scopeFeatherPct = clampScopeFeatherPct(extras.scopeFeatherPct);
}
if (extras.scopeTerminusPct === null) this._scopeTerminusPct = null;
else if (typeof extras.scopeTerminusPct === 'number') {
+17 -3
View File
@@ -98,6 +98,13 @@ function dockCapacity(raw) {
return capacity - geofenced;
}
/** Read a numeric coordinate without treating blank text or other JSON types as zero. */
function coordinateNumber(value) {
if (typeof value !== 'number' && (typeof value !== 'string' || !value.trim()))
return NaN;
return Number(value);
}
/**
* Parse a GBFS station_information payload into a Map of station metadata.
* Handles field-name variations across different GBFS providers
@@ -113,9 +120,16 @@ export function parseStationInformation(payload) {
for (const raw of stations) {
// Accept both GBFS (station_id) and alternate (id) field names
const stationId = String(raw?.station_id ?? raw?.id ?? '').trim();
const lat = Number(raw?.lat ?? raw?.latitude);
const lon = Number(raw?.lon ?? raw?.longitude);
if (!stationId || !Number.isFinite(lat) || !Number.isFinite(lon)) continue;
const lat = coordinateNumber(raw?.lat ?? raw?.latitude);
const lon = coordinateNumber(raw?.lon ?? raw?.longitude);
if (
!stationId ||
!Number.isFinite(lat) ||
!Number.isFinite(lon) ||
Math.abs(lat) > 90 ||
Math.abs(lon) > 180
)
continue;
stationMap.set(stationId, {
stationId,
+67
View File
@@ -19,6 +19,73 @@ const parseCapacity = (fields) =>
parseStationInformation({ data: { stations: [station(fields)] } }).get('s1')
.capacity;
test('station information skips missing, blank and malformed coordinates', () => {
for (const field of ['lat', 'lon', 'latitude', 'longitude']) {
for (const value of [
undefined,
null,
'',
' \t ',
false,
true,
[],
[0],
{},
'bad',
NaN,
Infinity,
]) {
const raw = { station_id: 'bad', lat: 0, lon: 0 };
if (field === 'latitude') delete raw.lat;
if (field === 'longitude') delete raw.lon;
raw[field] = value;
const parsed = parseStationInformation({
data: { stations: [raw, station({ station_id: 'good' })] },
});
assert.deepEqual(
[...parsed.keys()],
['good'],
`${field} = ${JSON.stringify(value)}`,
);
}
}
});
test('station information skips coordinates outside latitude and longitude bounds', () => {
for (const fields of [
{ lat: 90.001 },
{ lat: -90.001 },
{ lon: 180.001 },
{ lon: -180.001 },
{ lat: undefined, latitude: '91' },
{ lon: undefined, longitude: '-181' },
]) {
const parsed = parseStationInformation({
data: { stations: [station(fields)] },
});
assert.equal(parsed.size, 0, JSON.stringify(fields));
}
});
test('station information preserves zero, boundary coordinates and numeric aliases', () => {
for (const [fields, expected] of [
[{ lat: 0, lon: 0 }, [0, 0]],
[{ lat: ' 0 ', lon: '0' }, [0, 0]],
[{ lat: 90, lon: 180 }, [90, 180]],
[{ lat: '-90', lon: '-180' }, [-90, -180]],
[
{ lat: null, lon: undefined, latitude: '30.2681', longitude: '-97.7469' },
[30.2681, -97.7469],
],
]) {
const parsed = parseStationInformation({
data: { stations: [station(fields)] },
}).get('s1');
assert.ok(parsed, JSON.stringify(fields));
assert.deepEqual([parsed.lat, parsed.lon], expected);
}
});
test('capacity counts docks, not the geofenced area PBSC adds to it', () => {
// Bilbao Bizi, BiciMAD and Dbizi as published on 2026-10-17.
assert.equal(parseCapacity({ capacity: 839, geofenced_capacity: 800 }), 39);
@@ -0,0 +1,94 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { createServer } from 'node:http';
import { once } from 'node:events';
import { createGoogleTokenSource } from '../maps/googleTokens.js';
import { googleTokenUnavailablePlugin } from '../../server/standalone/google-token-unavailable.js';
async function withServer({ beforeFallback } = {}, check) {
const middlewares = [];
const app = {
middlewares: {
use: (handler) => middlewares.push(handler),
},
};
if (beforeFallback) app.middlewares.use(beforeFallback);
googleTokenUnavailablePlugin().configureServer(app);
app.middlewares.use((_req, res) => {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Unknown API route' }));
});
const server = createServer((req, res) => {
let index = 0;
const next = () => middlewares[index++]?.(req, res, next);
next();
});
server.listen(0, '127.0.0.1');
await once(server, 'listening');
const address = server.address();
const url = 'http://127.0.0.1:' + address.port;
try {
await check(url);
} finally {
server.close();
await once(server, 'close');
}
}
test('keyless GET and HEAD return uncached 204 instead of 404', async () => {
await withServer({}, async (url) => {
for (const method of ['GET', 'HEAD']) {
const response = await fetch(url + '/api/google/tiles-token?probe=1', {
method,
});
assert.equal(response.status, 204);
assert.equal(response.headers.get('cache-control'), 'no-store');
assert.equal(await response.text(), '');
}
});
});
test('the existing client treats unavailable tokens as null', async () => {
await withServer({}, async (url) => {
const source = createGoogleTokenSource({
fetchImpl: (path, options) => fetch(new URL(path, url), options),
});
assert.equal(await source.token(), null);
});
});
test('unrelated API paths and unsupported methods remain 404', async () => {
await withServer({}, async (url) => {
for (const [path, method] of [
['/api/google/tiles-token/extra', 'GET'],
['/api/not-a-token', 'GET'],
['/api/google/tiles-token', 'POST'],
]) {
const response = await fetch(new URL(path, url), { method });
assert.equal(response.status, 404);
}
});
});
test('a real token provider registered first still wins', async () => {
await withServer(
{
beforeFallback: (req, res, next) => {
if (req.url !== '/api/google/tiles-token') return next();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(
JSON.stringify({
accessToken: 'server-token',
expiresAt: 10000000000000,
}),
);
},
},
async (url) => {
const response = await fetch(url + '/api/google/tiles-token');
assert.equal(response.status, 200);
assert.equal((await response.json()).accessToken, 'server-token');
},
);
});
+25 -5
View File
@@ -1,3 +1,4 @@
import { fileURLToPath } from 'node:url';
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
@@ -5,7 +6,11 @@ import { rm, writeFile } from 'node:fs/promises';
import { request } from 'node:http';
import path from 'node:path';
import { build, createServer, preview } from 'vite';
import { BROWSER_CSP, createBrowserViteConfig } from '../../build/vite.js';
import {
BROWSER_CSP,
createBrowserViteConfig,
escapeFsDenyPath,
} from '../../build/vite.js';
import standaloneConfig, * as compatibility from '../../vite.config.js';
import * as providers from '../../server/providers/local.js';
import { apiNotFoundPlugin } from '../../server/standalone/api-not-found.js';
@@ -30,6 +35,20 @@ test('explicit build inputs preserve browser-only defines, plugin order and loop
assert.deepEqual(config.server.allowedHosts, ['localhost', '127.0.0.1']);
assert.ok(config.server.fs.deny.includes('**/ENVIRONMENT'));
assert.ok(config.server.fs.deny.includes('.env.*'));
assert.ok(config.server.fs.deny.includes('**/.gev-logs/**'));
assert.ok(config.server.fs.deny.includes('**/.gev-cache/**'));
const root = fileURLToPath(new URL('../../', import.meta.url))
.replace(/\\/g, '/')
.replace(/\/$/, '');
assert.deepEqual(
config.server.fs.deny.filter((pattern) => pattern.endsWith('/dist/**')),
[`${escapeFsDenyPath(root)}/dist/**`],
'only project build output is denied, not dependency dist directories',
);
assert.equal(
escapeFsDenyPath('C:/Users/a/project (1) [test]{copy}'),
'C:/Users/a/project \\(1\\) \\[test\\]\\{copy\\}',
);
assert.equal(config.server.headers['X-Frame-Options'], 'DENY');
assert.equal(config.server.headers['X-Content-Type-Options'], 'nosniff');
const csp = config.server.headers['Content-Security-Policy'];
@@ -98,7 +117,7 @@ test('root config retains existing named exports and standalone provider order',
assert.equal(compatibility[name], value, name);
const config = standaloneConfig({ mode: 'test' });
const standaloneProviders = config.plugins
.slice(4, -5)
.slice(4, -6)
.map((plugin) => plugin.name);
assert.equal(
standaloneProviders.filter((name) => name === 'gemini-live-proxy').length,
@@ -112,9 +131,10 @@ test('root config retains existing named exports and standalone provider order',
standaloneProviders.filter((name) => name !== 'gemini-live-proxy'),
providers.localProviderPlugins().map((plugin) => plugin.name),
);
assert.equal(config.plugins.at(-6).name, 'gev-key-setup');
// The local MCP route follows every provider and precedes the API fallback.
assert.equal(config.plugins.at(-5).name, 'local-mcp');
assert.equal(config.plugins.at(-7).name, 'gev-key-setup');
// Token fallback follows real providers and local MCP before the API 404.
assert.equal(config.plugins.at(-6).name, 'local-mcp');
assert.equal(config.plugins.at(-5).name, 'google-token-unavailable');
assert.equal(config.plugins.at(-4).name, 'api-not-found');
// Config-only, so it sits after the provider slice rather than in it.
assert.equal(config.plugins.at(-3).name, 'gev-unwatched-runtime-dirs');
+19 -2
View File
@@ -5,6 +5,24 @@ import { defineTool, ToolError } from '../catalog.js';
import { AREA_SCHEMA, areaContains, resolveArea } from '../area.js';
import { LIMIT_SCHEMA, capRows, countNoun, isoTime } from '../results.js';
function acquiredMsUtc(acqDate, acqTime) {
if (typeof acqDate !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(acqDate))
return NaN;
const timeText = String(acqTime ?? '').trim();
if (!/^\d{1,4}$/.test(timeText)) return NaN;
const hhmm = timeText.padStart(4, '0');
const hours = Number(hhmm.slice(0, 2));
const minutes = Number(hhmm.slice(2, 4));
if (hours > 23 || minutes > 59) return NaN;
const timestamp = Date.parse(
`${acqDate}T${hhmm.slice(0, 2)}:${hhmm.slice(2, 4)}:00.000Z`,
);
return Number.isFinite(timestamp) &&
new Date(timestamp).toISOString().slice(0, 10) === acqDate
? timestamp
: NaN;
}
export const getEarthquakes = defineTool({
name: 'get_earthquakes',
title: 'Recent earthquakes',
@@ -114,8 +132,7 @@ export const getActiveFires = defineTool({
lon: fire.lon,
frp_mw: fire.frp ?? null,
confidence: fire.confidence ?? null,
acquired:
[fire.acqDate, fire.acqTime].filter(Boolean).join(' ') || null,
acquired: isoTime(acquiredMsUtc(fire.acqDate, fire.acqTime)),
satellite: fire.satellite ?? null,
day_night: fire.daynight ?? null,
})),
+46
View File
@@ -0,0 +1,46 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { composeCatalog } from '../catalog.js';
import { getActiveFires } from './hazards.js';
const catalogWith = (fires) =>
composeCatalog({
tools: [getActiveFires],
services: { fires: { getSnapshot: async () => ({ fires }) } },
});
test('a fire acquired at 00:45 UTC reads as 00:45, not "45"', async () => {
const result = await catalogWith([
{ lat: 24, lon: 121, frp: 5, acqDate: '2026-01-01', acqTime: '45' },
]).call('get_active_fires', { area: { bbox: [120, 23, 122, 25] } });
assert.equal(result.data.rows[0].acquired, '2026-01-01T00:45:00.000Z');
});
test('fire acquisition timestamps reject impossible calendar dates', async () => {
for (const acqDate of [
'2026-02-29',
'2026-02-30',
'2026-04-31',
'1900-02-29',
'2026-13-01',
'2026-01-00',
]) {
const result = await catalogWith([
{ lat: 24, lon: 121, acqDate, acqTime: '45' },
]).call('get_active_fires', { area: { bbox: [120, 23, 122, 25] } });
assert.equal(result.data.rows[0].acquired, null, acqDate);
}
});
test('fire acquisition timestamps preserve valid leap days and UTC times', async () => {
for (const [acqDate, acqTime, expected] of [
['2024-02-29', '45', '2024-02-29T00:45:00.000Z'],
['2000-02-29', 0, '2000-02-29T00:00:00.000Z'],
['2026-04-30', '2359', '2026-04-30T23:59:00.000Z'],
]) {
const result = await catalogWith([
{ lat: 24, lon: 121, acqDate, acqTime },
]).call('get_active_fires', { area: { bbox: [120, 23, 122, 25] } });
assert.equal(result.data.rows[0].acquired, expected);
}
});
+40
View File
@@ -171,6 +171,46 @@ test('bike share reports uncovered areas and failed systems honestly', async ()
);
});
test('bike share excludes blank coordinates without losing stations on the prime meridian', async () => {
const stations = [
{ station_id: 'blank', lat: 51.5, lon: ' ' },
{ station_id: 'zero', lat: 51.5, lon: 0 },
];
const catalog = composeCatalog({
tools: coreTools,
services: {
bikeshare: {
systems: [system('fixture', 'Fixture Bikes', 51.5, 0)],
async getStations(url) {
return {
data: {
stations: url.endsWith('station_information.json')
? stations
: stations.map(({ station_id }) => ({
station_id,
num_bikes_available: 4,
})),
},
};
},
},
},
});
const result = await catalog.call('get_bike_share', {
area: { lat: 51.5, lon: 0, radius_km: 2 },
});
assert.deepEqual(
result.data.rows.map((row) => row.station_id),
['zero'],
);
assert.equal(result.data.total, 1);
assert.equal(result.data.rows[0].lon, 0);
assert.equal(
result.summary,
'1 bike-share station in 2 km around 51.500, 0.000 with 4 bikes available.',
);
});
const feed = {
id: 'capmetro',
name: 'CapMetro',
+1 -1
View File
@@ -125,7 +125,7 @@ test('fires need an area and report a missing key as unavailable', async () => {
result.data.rows.map((row) => row.frp_mw),
[40, 12],
);
assert.equal(result.data.rows[1].acquired, '2026-01-01 0130');
assert.equal(result.data.rows[1].acquired, '2026-01-01T01:30:00.000Z');
const keyless = composeCatalog({
tools: coreTools,
-1
View File
@@ -70,7 +70,6 @@ export class AircraftDisplay {
// a screen reader without this. It matters more now that the button ships
// ACTIVE from markup (default-on, 2026-08-22): the very first thing assistive
// tech reported was an unpressed-looking control over an armed layer.
// Mirrors #scope-toggle, which has always carried aria-pressed.
this._models3dBtn?.setAttribute(
'aria-pressed',
String(this._models3dEnabled),
+30
View File
@@ -1,3 +1,4 @@
import { getScopeMaskFeather, isScopeMaskEnabled, setScopeMaskEnabled, setScopeMaskFeather } from '../scopeMask.js';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
@@ -271,6 +272,35 @@ test('Display and voice HUD routes preserve explicit Cyber entry and exit author
assert.equal(voice.owner.activeStyle, 'snow');
});
test('scope choice and expanded feather survive every HUD layout and both restore paths', async (t) => {
const { owner } = styleOwner(t, 'tactical');
const previous = { enabled: isScopeMaskEnabled(), feather: getScopeMaskFeather() };
t.after(() => { setScopeMaskEnabled(previous.enabled); setScopeMaskFeather(previous.feather); });
Object.assign(owner.services, { setScopeMaskEnabled, setScopeMaskFeather });
owner._scopeBtn = { checked: true };
owner._scopeFeatherSlider = { value: '11', disabled: false };
owner._scopeFeatherValue = {};
await owner.restoreShareState({ scopeEnabled: false, scopeFeatherPct: 200 });
assert.equal(owner._scopeBtn.checked, false);
assert.equal(owner._scopeFeatherSlider.disabled, true);
assert.equal(owner._scopeFeatherSlider.value, '200');
for (const layout of ['minimal', 'operator', 'cyber', 'tactical']) {
owner._setHudVariant(layout, { applyVisualDefaults: true });
assert.equal(isScopeMaskEnabled(), false, layout);
assert.equal(getScopeMaskFeather(), 2, layout);
assert.equal(owner._scopeBtn.checked, false, layout);
}
await owner.applyVisualState({ scope: { enabled: true, featherPct: 175 } });
assert.equal(isScopeMaskEnabled(), true);
assert.equal(getScopeMaskFeather(), 1.75);
assert.equal(owner._scopeBtn.checked, true);
assert.equal(owner._scopeFeatherSlider.disabled, false);
assert.equal(owner._scopeFeatherValue.textContent, '175%');
await owner.applyVisualState({ scope: { featherPct: 500 } });
assert.equal(getScopeMaskFeather(), 2);
assert.equal(owner._scopeFeatherSlider.value, '200');
});
test('scene and share HUD transitions do not restore a local pre-Cyber preset', async (t) => {
const { owner, hud } = styleOwner(t, 'tactical');
owner.setStyle('noir');
+12 -8
View File
@@ -1,6 +1,7 @@
import { clampScopeFeatherPct } from '../scopeMaskPolicy.js';
import { createFrameRateMonitor } from './frameRateMonitor.js';
import { bindApplicationShortcuts } from './visualInput.js';
import { bindDisplayControls } from './displayControls.js';
import { bindDisplayControls, syncScopeControls } from './displayControls.js';
import { canonicalizeDensity } from '../data/detectionPolicy.js';
/** Own keyboard/display event subscriptions; settings remain with their state owners. */
@@ -92,6 +93,11 @@ export class DisplayBindings {
},
});
syncScopeControls(
this._scopeBtn,
this._scopeFeatherSlider,
isScopeMaskEnabled(),
);
this._displayControls?.destroy();
this._displayControls = bindDisplayControls({
elements: {
@@ -100,7 +106,7 @@ export class DisplayBindings {
bloomSlider: this._bloomSlider,
sharpenButton: this._sharpenBtn,
sharpenSlider: this._sharpenSlider,
scopeButton: this._scopeBtn,
scopeToggle: this._scopeBtn,
scopeFeatherSlider: this._scopeFeatherSlider,
hudLayout: this._hudLayoutSelect,
hudButton: this._hudBtn,
@@ -134,17 +140,15 @@ export class DisplayBindings {
this.shareLinkManager?.claimRestoreLane?.('visual');
this._setSharpenEnabled(!this.sharpenEnabled);
},
toggleScope: () => {
setScopeEnabled: (enabled) => {
this.shareLinkManager?.claimRestoreLane?.('visual');
const next = !isScopeMaskEnabled();
setScopeMaskEnabled(next);
this._scopeBtn.classList.toggle('active', next);
this._scopeBtn.setAttribute('aria-pressed', String(next));
setScopeMaskEnabled(enabled);
syncScopeControls(this._scopeBtn, this._scopeFeatherSlider, enabled);
this._syncShareState();
},
setScopeFeather: (value) => {
this.shareLinkManager?.claimRestoreLane?.('visual');
const pct = Math.max(0, Math.min(100, value || 0));
const pct = clampScopeFeatherPct(value);
if (this._scopeFeatherValue)
this._scopeFeatherValue.textContent = `${pct}%`;
setScopeMaskFeather(pct / 100);
+7 -1
View File
@@ -18,7 +18,6 @@ export function bindDisplayControls({ elements, actions }) {
for (const [name, action] of [
['bloomButton', 'toggleBloom'],
['sharpenButton', 'toggleSharpen'],
['scopeButton', 'toggleScope'],
['cleanViewButton', 'toggleCleanView'],
['cleanViewExitButton', 'exitCleanView'],
['celestialButton', 'toggleCelestial'],
@@ -34,6 +33,7 @@ export function bindDisplayControls({ elements, actions }) {
['scopeFeatherSlider', 'setScopeFeather'],
])
listen(elements[name], 'input', action, integer);
listen(elements.scopeToggle, 'change', 'setScopeEnabled', (el) => el.checked);
listen(elements.densitySlider, 'input', 'setDensity', (el) => el.value);
listen(elements.hudLayout, 'change', 'setHudLayout', (el) => el.value);
for (const [name, action] of [
@@ -72,3 +72,9 @@ export function bindDisplayControls({ elements, actions }) {
},
};
}
/** Keep the native Scope checkbox and feather availability in step with the mask. */
export function syncScopeControls(toggle, featherSlider, enabled) {
if (toggle) toggle.checked = enabled;
if (featherSlider) featherSlider.disabled = !enabled;
}
+26
View File
@@ -158,3 +158,29 @@ test('optional controls are absent safely and subscriptions stay instance-owned'
assert.deepEqual(calls, ['b']);
two.destroy();
});
test('the native Scope checkbox reads checked state on change and disposes its listener', () => {
const scopeToggle = element();
scopeToggle.checked = true;
const scopeFeatherSlider = element('200');
const calls = [];
const controls = bindDisplayControls({
elements: { scopeToggle, scopeFeatherSlider },
actions: {
setScopeEnabled: (value) => calls.push(['enabled', value]),
setScopeFeather: (value) => calls.push(['feather', value]),
},
});
scopeToggle.checked = false;
scopeToggle.dispatchEvent(new Event('change'));
scopeToggle.checked = true;
scopeToggle.dispatchEvent(new Event('change'));
scopeFeatherSlider.dispatchEvent(new Event('input'));
controls.destroy();
scopeToggle.dispatchEvent(new Event('change'));
assert.deepEqual(calls, [
['enabled', false],
['enabled', true],
['feather', 200],
]);
});
+1 -1
View File
@@ -6,7 +6,7 @@ export function createFrameRateMonitor({ viewer, documentRef = document }) {
if (!host || !frameEvent) return { destroy() {} };
const readout = documentRef.createElement('div');
readout.className = 'frame-rate-readout';
readout.className = 'frame-rate-readout scene-text-holdout';
readout.hidden = true;
// i18n-ignore: FPS is a unit code, the same in every language
readout.textContent = 'FPS —';
+6
View File
@@ -478,6 +478,12 @@ body:not(.ui-clean-view):not(.recording-mode):has(
bottom: calc(2vh + 5.4rem);
}
:root[data-scope-mask='off']
#intel-hud[data-variant='minimal']
.hud-bottom-left {
bottom: calc(2vh + 6.5rem);
}
#intel-hud[data-variant='minimal'] .hud-bottom-bar {
bottom: calc(2vh + 9rem);
}
+21
View File
@@ -501,6 +501,27 @@ html:not([lang='en']) .panel-collapse-btn {
color: var(--accent);
}
.pp-scope-toggle input {
width: 14px;
height: 14px;
margin: 0 0 0 auto;
accent-color: var(--accent);
cursor: pointer;
}
.pp-scope-toggle:has(input:checked) .pp-label {
color: var(--accent);
}
.pp-scope-toggle:has(input:focus-visible) {
outline: 1px solid var(--accent);
outline-offset: 2px;
}
#scope-slider-row:has(input:disabled) {
opacity: 0.45;
}
/* ── Bloom Slider (inline with toggle) ───────── */
.pp-toggle-group {
display: flex;
+131
View File
@@ -371,3 +371,134 @@
#intel-hud[data-variant='minimal'] .hud-summary-wrap {
max-width: 280px;
}
/* Scope OFF adds text-sized backing; Scope ON retains the original HUD. */
.hud-readout {
display: contents;
}
:root[data-scope-mask='off']
#intel-hud:is(
[data-variant='minimal'],
[data-variant='operator'],
[data-variant='tactical']
)
.hud-readout,
:root[data-scope-mask='off']:not([data-ui-theme='cyber']) .scene-text-holdout {
background: rgba(0, 0, 0, 0.7);
box-shadow: none;
border-radius: 2px;
padding-block: 0;
padding-inline: 6px;
margin-block: 0;
margin-inline: -6px;
width: fit-content;
max-width: 100%;
box-sizing: content-box;
white-space: nowrap;
}
:root[data-scope-mask='off']
#intel-hud:is(
[data-variant='minimal'],
[data-variant='operator'],
[data-variant='tactical']
)
.hud-readout {
display: block;
width: max-content;
min-width: 0;
box-sizing: content-box;
white-space: nowrap;
}
:root[data-scope-mask='off']
#intel-hud:is(
[data-variant='minimal'],
[data-variant='operator'],
[data-variant='tactical']
)
:is(.hud-top-right, .hud-bottom-right)
.hud-readout,
:root[data-scope-mask='off']:not([data-ui-theme='cyber'])
#style-indicator
.scene-text-holdout {
margin-inline-start: auto;
}
/* Preserve glyph opacity without dimming the vertical backplates. */
:root[data-scope-mask='off']
#intel-hud:is([data-variant='operator'], [data-variant='tactical'])
.hud-edge {
opacity: 1;
}
:root[data-scope-mask='off']
#intel-hud:is([data-variant='operator'], [data-variant='tactical'])
.hud-edge
.hud-readout
> * {
opacity: 0.5;
}
:root[data-scope-mask='off'] #intel-hud[data-variant='operator'] .hud-corner,
:root[data-scope-mask='off']
#intel-hud:is([data-variant='minimal'], [data-variant='operator'])
:is(.hud-top-bar, .hud-bottom-bar) {
background: transparent;
}
:root[data-scope-mask='off']
#intel-hud:is([data-variant='minimal'], [data-variant='operator'])
:is(.hud-top-bar, .hud-bottom-bar) {
width: fit-content;
max-width: calc(100vw - 120px);
gap: 24px;
}
:root[data-scope-mask='off']
#intel-hud:is([data-variant='minimal'], [data-variant='operator'])
:is(.hud-top-bar, .hud-bottom-bar)
.hud-readout {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
:root[data-scope-mask='off']:not([data-ui-theme='cyber'])
#title-bar
h1::before {
content: '';
position: absolute;
inset: -0.12em -6px;
z-index: -1;
border-radius: 2px;
background: rgba(0, 0, 0, 0.7);
}
:root[data-scope-mask='off']:not([data-ui-theme='cyber'])
:is(#title-bar .subtitle, #style-indicator .indicator-label) {
color: var(--text-secondary);
}
:root[data-scope-mask='off']:not([data-ui-theme='cyber']) #title-bar .subtitle {
margin-top: 8px;
}
:root[data-scope-mask='off']:not([data-ui-theme='cyber'])
#style-indicator
.indicator-value {
margin-top: 4px;
}
:root[data-scope-mask='off']
#intel-hud[data-variant='minimal']
.hud-readout:has(> :is(.hud-classification, .hud-system)) {
display: none;
}
:root[data-scope-mask='off']:not([data-ui-theme='cyber'])
#title-bar
.frame-rate-readout {
margin-top: 6px;
}
+5 -9
View File
@@ -106,19 +106,15 @@
</div>
</div>
<div class="pp-toggle-group">
<button class="pp-toggle-btn active" id="scope-toggle" title="Scope — the circular viewport mask" aria-pressed="true" data-i18n-title>
<label class="pp-toggle-btn pp-scope-toggle" title="Scope — the circular viewport mask" data-i18n-title>
<span class="pp-icon material-symbols-outlined" aria-hidden="true">adjust</span>
<span class="pp-label" data-i18n>Scope</span>
</button>
<input type="checkbox" id="scope-toggle" checked />
</label>
<div class="pp-slider-row visible" id="scope-slider-row">
<span class="pp-slider-mini-label" data-i18n>Feather</span>
<!-- Feather starts at an 11% soft scope-mask edge (owner final lock
2026-08-24, superseding the 08-22 hard-crop and 08-23 8% rulings);
mirrors SCOPE_FEATHER_RATIO_DEFAULT in src/scopeMask.js. Feather
softens the black mask edge; label fading is the separate Fade
slider. -->
<input type="range" class="gev-quantitative-slider pp-slider" id="scope-feather-slider" min="0" max="100" value="11" aria-label="Scope edge feather" title="Scope edge feather as a percentage of the keyhole radius" data-i18n-aria-label data-i18n-title />
<span class="pp-slider-value" id="scope-feather-value">11%</span>
<input type="range" class="gev-quantitative-slider pp-slider" id="scope-feather-slider" min="0" max="200" value="100" aria-label="Scope edge feather" title="Scope edge feather as a percentage of the keyhole radius" data-i18n-aria-label data-i18n-title />
<span class="pp-slider-value" id="scope-feather-value">100%</span>
</div>
</div>
<div class="pp-toggle-group">
+3 -3
View File
@@ -11,13 +11,13 @@
<div id="title-bar">
<div class="title-glow"></div>
<h1><span class="title-logo brand-logo" data-logo-gaze data-logo-src="/logo.svg" aria-hidden="true"><img src="/logo.svg" alt="" /></span> <span translate="no">GOD'S EYE <span class="title-accent">VIEW</span></span></h1>
<p class="subtitle" data-i18n>NO PLACE LEFT BEHIND</p>
<p class="subtitle scene-text-holdout" data-i18n>NO PLACE LEFT BEHIND</p>
</div>
<!-- Style indicator -->
<div id="style-indicator">
<span class="indicator-label" data-i18n>ACTIVE STYLE</span>
<span class="indicator-value" id="active-style-name" data-i18n>NORMAL</span>
<span class="indicator-label scene-text-holdout" data-i18n>ACTIVE STYLE</span>
<span class="indicator-value scene-text-holdout" id="active-style-name" data-i18n>NORMAL</span>
</div>
<!-- Top-center camera and share actions -->
+13 -1
View File
@@ -159,18 +159,30 @@ export class VisualEffects {
this.requestRender('bloom');
}
/**
* Sharpen runs only when it is on and its slider is above 0, as the style
* stages do: the pass costs the same at any amount (~1.5 ms GPU per frame
* on the Arc, 2026-10-24), and `0.1 + value * 2` still sharpened at 0.
*/
syncSharpenEnabled() {
if (this.stopped || !this.sharpenStage) return;
this.sharpenStage.enabled =
this.sharpenEnabled && this.sharpenIntensity > 0.001;
}
applySharpenIntensity(value) {
if (this.stopped) return;
this.sharpenIntensity = value;
if (this.sharpenStage)
this.sharpenStage.uniforms.amount = 0.1 + value * 2.0;
this.syncSharpenEnabled();
this.requestRender('sharpen');
}
setSharpenEnabled(enabled) {
if (this.stopped) return;
this.sharpenEnabled = !!enabled;
if (this.sharpenStage) this.sharpenStage.enabled = this.sharpenEnabled;
this.syncSharpenEnabled();
this.requestRender('sharpen');
}
+18
View File
@@ -224,6 +224,24 @@ test('sharpen uses the existing uniform scale and keeps values while disabled',
f.effects.destroy();
});
test('sharpen at 0 runs no pass, and comes back with the slider', () => {
const f = fixture();
f.effects.initPostProcess(0.49);
f.effects.setSharpenEnabled(true);
assert.equal(f.effects.sharpenStage.enabled, true);
f.effects.applySharpenIntensity(0);
assert.equal(
f.effects.sharpenStage.enabled,
false,
'no residual sharpen at 0',
);
f.effects.setSharpenEnabled(true);
assert.equal(f.effects.sharpenStage.enabled, false, 'on at 0 is still off');
f.effects.applySharpenIntensity(0.2);
assert.equal(f.effects.sharpenStage.enabled, true);
f.effects.destroy();
});
test('stop revokes pending work immediately but retains stages until final destruction', () => {
const f = fixture();
f.effects.initStyles();
+10 -6
View File
@@ -1,3 +1,5 @@
import { clampScopeFeatherPct } from '../scopeMaskPolicy.js';
import { syncScopeControls } from './displayControls.js';
import { STYLE_STATUS_LABELS } from './visualPresets.js';
import { syncSegmentedControl } from './segmentedControl.js';
import { t } from '../i18n/index.js';
@@ -136,11 +138,10 @@ export class VisualSettings {
}
if (typeof scopeEnabled === 'boolean') {
setScopeMaskEnabled(scopeEnabled);
this._scopeBtn?.classList.toggle('active', scopeEnabled);
this._scopeBtn?.setAttribute('aria-pressed', String(scopeEnabled));
syncScopeControls(this._scopeBtn, this._scopeFeatherSlider, scopeEnabled);
}
if (typeof scopeFeatherPct === 'number' && this._scopeFeatherSlider) {
const pct = Math.max(0, Math.min(100, Math.round(scopeFeatherPct)));
const pct = clampScopeFeatherPct(scopeFeatherPct);
this._scopeFeatherSlider.value = String(pct);
if (this._scopeFeatherValue)
this._scopeFeatherValue.textContent = `${pct}%`;
@@ -1302,11 +1303,14 @@ export class VisualSettings {
const scopeState = state.scope || {};
if (typeof scopeState.enabled === 'boolean') {
setScopeMaskEnabled(scopeState.enabled);
this._scopeBtn?.classList.toggle('active', scopeState.enabled);
this._scopeBtn?.setAttribute('aria-pressed', String(scopeState.enabled));
syncScopeControls(
this._scopeBtn,
this._scopeFeatherSlider,
scopeState.enabled,
);
}
if (typeof scopeState.featherPct === 'number' && this._scopeFeatherSlider) {
const pct = Math.max(0, Math.min(100, Math.round(scopeState.featherPct)));
const pct = clampScopeFeatherPct(scopeState.featherPct);
this._scopeFeatherSlider.value = String(pct);
if (this._scopeFeatherValue)
this._scopeFeatherValue.textContent = `${pct}%`;