Read QA drawing evidence through the globe contract

Port the layer probes in twenty-six browser scripts to engine primitive
lists, drawn rows and layer diagnostics. Preserve Cesium as the reference
and keep the supported Navara paths on the same observations. This
changes QA observation only, with no application behaviour change.

Review the interrupted infrastructure group and retain its readiness,
drape, selection, label and floor checks. Match both installation outline
buffers. Frame each CCTV city before reading its visible coverage. Read
Radio colours and selected identities from actual mover writes. Retain
transit's GPU upload budgets using engine-owned line primitives rather
than substituting rebuild counts. Keep native inspection where a check
specifically measures GPU uploads, loaded meshes or ground readiness.

The preceding combined B/C commit owns all shared manifests, docs,
locale catalogs and the coupling baseline; this commit changes only its
own twenty-six QA scripts. Coupling remains unchanged at 344 Cesium
renderer lines, 1294 Navara renderer lines, 9 Cesium import files, 1026
viewer lines, 2 Navara kind checks, 0 engine sniff calls, 9 extension
reads, 1 ground pass, 3 SDK loads, 4 screen-handler files and 42 engine-name
lines.

The full unit suite and the boundary, format and coupling checks pass.
Browser validation remains to be run.

Browser checks to run
GOOGLE_MAPS_API_KEY= CESIUM_ION_TOKEN= npm run dev -- --port 4291
node scripts/qa-alpr-journey.mjs http://localhost:4291
node scripts/qa-alpr-journey.mjs 'http://localhost:4291/?engine=navara'
node scripts/qa-cables-overlay.mjs --url http://localhost:4291
node scripts/qa-cctv-v2.mjs --url http://localhost:4291 --fixture --terrain-globe
node scripts/qa-context-arrivals.mjs http://localhost:4291
node scripts/qa-directions.mjs --url http://localhost:4291
QA_BASE_URL=http://localhost:4291 node scripts/qa-draw-tool.mjs
QA_BASE_URL=http://localhost:4291 node scripts/qa-floor-hold.mjs
node scripts/qa-ghostmaps.mjs --url http://localhost:4291
node scripts/qa-gnss.mjs --url http://localhost:4291
node scripts/qa-height-datum.mjs --url http://localhost:4291
node scripts/qa-infra-lod.mjs --url http://localhost:4291
node scripts/qa-installation-navigation.mjs http://localhost:4291
node scripts/qa-military-names.mjs http://localhost:4291 --no-video
node scripts/qa-overpass-offload.mjs --url http://localhost:4291
node scripts/qa-radio.mjs --url http://localhost:4291
node scripts/qa-traffic-motion.mjs http://localhost:4291 --mode=osm
QA_BASE_URL=http://localhost:4291 node scripts/qa-transit-recovery.mjs
QA_BASE_URL=http://localhost:4291 node scripts/qa-transit.mjs
node scripts/qa-weather-teardown.mjs --url http://localhost:4291 --headless
for engine in cesium navara; do
  node scripts/qa-enrich-ambient.mjs --url http://localhost:4291 --engine=$engine
  QA_BASE_URL=http://localhost:4291 node scripts/qa-floor-verify.mjs --engine=$engine
  node scripts/qa-heading-b3.mjs --url http://localhost:4291 --engine=$engine
  node scripts/qa-overlay-baseline.mjs --url http://localhost:4291 --engine=$engine
  node scripts/qa-sprites-b5.mjs --url http://localhost:4291 --engine=$engine
  node scripts/qa-vessel-datum.mjs --url 'http://localhost:4291/?qaFeed=vessels:fixture' --engine $engine
done

The military-names driver exercises installation-polish; the transit
driver exercises transit-scenes. Use only the keyless server. Cases that
require photoreal tiles or unavailable providers remain unverified.
This commit is contained in:
German Khokhlov committed 2026-11-07 12:45:21 +04:00
1 parent 97c4dc76ce
commit 3907b7084e
26 files changed
+1429 -1075

No files matched your search

+323 -248
View File
@@ -160,6 +160,103 @@ async function main() {
return Promise.resolve();
};
});
// What Radio draws, read through the globe contract (the layer draws
// engine.movers point and sprite sets, src/layers/radio/render/display.js;
// there are no Radio Entities, data sources or Cesium clusters any more):
// set counts from engine.movers.list(), and what is drawn at a point
// from engine.picker.drillPick (each instance's pickId).
await page.evaluateOnNewDocument((fixture) => {
const SETS = ['radio-stations', 'radio-clusters', 'radio-selected', 'radio-bracket'];
const byId = new Map(fixture.map(([id, lat, lon]) => [id, { lat, lon }]));
const engine = () => window.__godsEyeView.engine;
// WGS84 geodetic → Earth-fixed metres.
const world = (lat, lon, height = 0) => {
const a = 6378137;
const e2 = 6.69437999014e-3;
const phi = (lat * Math.PI) / 180;
const lambda = (lon * Math.PI) / 180;
const n = a / Math.sqrt(1 - e2 * Math.sin(phi) ** 2);
return {
x: (n + height) * Math.cos(phi) * Math.cos(lambda),
y: (n + height) * Math.cos(phi) * Math.sin(lambda),
z: (n * (1 - e2) + height) * Math.sin(phi),
};
};
const api = {
world,
stationPosition(id, height = 2.5) {
const station = byId.get(String(id).replace(/^radio:(selected:)?/, ''));
return station ? world(station.lat, station.lon, height) : null;
},
// Instance counts of the Radio mover sets ({} while none exist).
counts() {
const counts = {};
for (const set of engine().movers.list())
if (SETS.includes(set.id)) counts[set.id] = set.count;
return counts;
},
anyDrawn() {
return Object.values(api.counts()).some((count) => count > 0);
},
// A world position's canvas point, or null off the canvas.
canvasPoint(position) {
const e = engine();
const screen = position && e.projector.worldToWindow(position);
if (!screen || !Number.isFinite(screen.x) || !Number.isFinite(screen.y)) return null;
const rect = e.host.canvas.getBoundingClientRect();
const point = { x: screen.x, y: screen.y };
if (point.x < 0 || point.y < 0 || point.x > rect.width || point.y > rect.height) return null;
return point;
},
// The picks at a canvas point: { id, owner } of what the engine drew there.
picksAt(point, limit = 16) {
if (!point) return [];
return (engine().picker.drillPick(point, limit) || []).map((picked) => ({
id: picked?.id == null ? null : String(picked.id),
owner: picked?.owner ?? null,
}));
},
// The pick id written to the selected marker, while its set is live.
selectedMarkerId() {
if (api.counts()['radio-selected'] !== 1) return null;
const item = window.__qaRadioInstances?.get('radio-selected')?.[0];
return item?.pickId ?? null;
},
// The station ids a pick finds at each fixture station's canvas
// point: { front, back } split by whether the station is in front of
// the horizon. A station culled behind the globe must not be picked.
drawnStations(limit = 4) {
const e = engine();
const front = new Set();
const back = new Set();
for (const [id] of byId) {
const position = api.stationPosition(id);
const point = api.canvasPoint(position);
if (!point) continue;
const ids = new Set(api.picksAt(point, limit).map((pick) => pick.id));
if (!ids.has(`radio:${id}`)) continue;
(e.projector.horizonVisible(position) ? front : back).add(id);
}
return { front: [...front], back: [...back] };
},
// Renders frames until the engine has drawn `count` of them.
frames(count = 2) {
const e = engine();
return new Promise((resolve) => {
let left = count;
const timeout = setTimeout(() => { remove(); resolve(); }, 3_000);
const remove = e.frames.postRender.addEventListener(() => {
if (--left > 0) return void e.render.request('qa-radio');
remove();
clearTimeout(timeout);
resolve();
});
e.render.request('qa-radio');
});
},
};
window.__qaRadioDrawn = api;
}, stations.map(({ id, lat, lon }) => [id, lat, lon]));
await page.setRequestInterception(true);
let catalogDegraded = false;
let catalogStations = stations;
@@ -290,6 +387,23 @@ async function main() {
window.__godsEyeView.styleManager.setPanelCollapsed('global-context-panel', true);
});
await page.evaluate(() => {
const movers = window.__godsEyeView.engine.movers;
const create = movers.createSet.bind(movers);
window.__qaRadioInstances = new Map();
movers.createSet = (options) => {
const set = create(options);
if (!options.id.startsWith('radio-')) return set;
const items = [];
window.__qaRadioInstances.set(options.id, items);
const write = set.set.bind(set);
set.set = (index, fields) => {
write(index, fields);
items[index] = { ...items[index], ...fields };
};
return set;
};
});
const enablePanels = await revealRadioControls(page, 'radio-enable-btn');
const enableStartedAt = Date.now();
await page.click('#radio-enable-btn');
@@ -399,7 +513,7 @@ async function main() {
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
const result = {
presentationActive: module.getUIState().presentationActive,
markerSourceVisible: gev.viewer.dataSources.getByName('Radio stations')[0]?.show,
markerSourceVisible: window.__qaRadioDrawn.anyDrawn(),
tunerHidden: document.getElementById('radio-tuner').hidden,
filterDisabled: document.getElementById('radio-filter').disabled,
prevDisabled: document.getElementById('radio-prev-btn').disabled,
@@ -493,7 +607,7 @@ async function main() {
const disablingPresentation = await page.evaluate(() => ({
manager: window.__godsEyeView.dataManager.getLayerLifecycleState('radio'),
presentationActive: window.__godsEyeView.dataManager.layers.get('radio').module.getUIState().presentationActive,
markerSourceVisible: window.__godsEyeView.viewer.dataSources.getByName('Radio stations')[0]?.show,
markerSourceVisible: window.__qaRadioDrawn.anyDrawn(),
full: document.getElementById('radio-enable-btn').textContent,
layerState: document.getElementById('radio-layer-state').textContent,
message: document.getElementById('radio-playback-state').textContent,
@@ -562,7 +676,7 @@ async function main() {
const enablingPresentation = await page.evaluate(() => ({
manager: window.__godsEyeView.dataManager.getLayerLifecycleState('radio'),
presentationActive: window.__godsEyeView.dataManager.layers.get('radio').module.getUIState().presentationActive,
markerSourceVisible: window.__godsEyeView.viewer.dataSources.getByName('Radio stations')[0]?.show,
markerSourceVisible: window.__qaRadioDrawn.anyDrawn(),
full: document.getElementById('radio-enable-btn').textContent,
layerState: document.getElementById('radio-layer-state').textContent,
message: document.getElementById('radio-playback-state').textContent,
@@ -666,10 +780,8 @@ async function main() {
const sample = await page.evaluate(async () => {
const viewer = window.__godsEyeView.viewer;
const radio = window.__godsEyeView.dataManager.layers.get('radio').module;
const dataSource = Array.from({ length: viewer.dataSources.length }, (_, index) => viewer.dataSources.get(index))
.find((item) => item.name === 'Radio stations');
const drawn = window.__qaRadioDrawn;
const { getPaintRect: getOverlayPaintRect, getDiagnostics: getWorldOverlayDiagnostics } = window.__gevWorldOverlay;
const { radioStationIdFromPick } = await import('/src/data/radio.js');
return new Promise((resolve) => {
let removePostRender = null;
const finish = () => {
@@ -678,18 +790,19 @@ async function main() {
const paintedSingletonIds = overlay.singletonIds.filter((entryId) => Boolean(getOverlayPaintRect('radio', entryId)));
const pickableSingletonIds = paintedSingletonIds.filter((entryId) => {
const rect = getOverlayPaintRect('radio', entryId);
const anchor = rect?.entry?.position
&& viewer.scene.cartesianToCanvasCoordinates(rect.entry.position);
const anchor = drawn.canvasPoint(rect?.entry?.position);
if (!anchor) return false;
return (viewer.scene.drillPick(anchor, 16) || [])
.some((picked) => radioStationIdFromPick(picked) === entryId.slice('station:'.length));
return drawn.picksAt(anchor, 16)
.some((picked) => picked.id === `radio:${entryId.slice('station:'.length)}`);
});
resolve({
overlay,
host: getWorldOverlayDiagnostics(),
paintedSingletonIds,
pickableSingletonIds,
nativeLabelCount: dataSource.entities.values.filter((entity) => Boolean(entity.label)).length,
// Radio draws no text on the globe itself: no engine line or
// label belongs to it (the old check counted native entity labels).
radioLines: window.__godsEyeView.engine.lines.list().filter((line) => /^radio/.test(line.id)).length,
});
};
const timeout = setTimeout(finish, 500);
@@ -725,17 +838,17 @@ async function main() {
}))),
);
check(
'Radio label allocation stays bounded with no native Cesium entity text across sparse and All views',
'Radio label allocation stays bounded with no globe-drawn Radio text across sparse and All views',
singletonViews.every((sample) => (
sample.overlay.entryCount <= 65
&& sample.nativeLabelCount === 0
&& sample.radioLines === 0
&& sample.host.paintedBySource?.radio > 0
)),
JSON.stringify(singletonViews.map((sample) => ({
category: sample.category,
view: sample.name,
entries: sample.overlay.entryCount,
nativeLabels: sample.nativeLabelCount,
radioLines: sample.radioLines,
painted: sample.host.paintedBySource?.radio,
}))),
);
@@ -1234,13 +1347,10 @@ async function main() {
});
await page.select('#radio-filter', 'all');
const allPalette = await page.evaluate(() => {
const source = Array.from({ length: window.__godsEyeView.viewer.dataSources.length }, (_, index) => (
window.__godsEyeView.viewer.dataSources.get(index)
)).find((item) => item.name === 'Radio stations');
const colorFor = (index) => {
const id = `radio:00000000-0000-4000-8000-${index.toString(16).padStart(12, '0')}`;
const color = source?.entities.getById(id)?.point?.color?.getValue?.();
return color ? [color.red, color.green, color.blue].map((value) => Math.round(value * 255)) : null;
const station = window.__qaRadioInstances.get('radio-stations')?.find(item => item.pickId === id);
return station?.color?.slice(0, 3).map(value => Math.round(value * 255)) ?? null;
};
return { news: colorFor(0), talk: colorFor(1), music: colorFor(7) };
});
@@ -1254,14 +1364,24 @@ async function main() {
);
const clusterBadge = await page.evaluate(async () => {
const viewer = window.__godsEyeView.viewer;
const drawn = window.__qaRadioDrawn;
const { getPaintRect: getOverlayPaintRect, getDiagnostics: getWorldOverlayDiagnostics } = window.__gevWorldOverlay;
const { distanceFade } = await import('/src/overlays/worldOverlayDraw.js');
const { radioStationIdFromPick } = await import('/src/data/radio.js');
const baselineSolveRevision = getWorldOverlayDiagnostics().solveRevision;
const source = Array.from({ length: viewer.dataSources.length }, (_, index) => viewer.dataSources.get(index))
.find((item) => item.name === 'Radio stations');
const ellipsoid = viewer.scene.globe.ellipsoid;
// Start from another view, so the clusters at the view below are a new
// cohort the host must solve (the old check forced one by switching
// Cesium's clustering off and on).
viewer.camera.cancelFlight();
viewer.camera.setView({
destination: ellipsoid.cartographicToCartesian({
longitude: 68 * Math.PI / 180,
latitude: 7 * Math.PI / 180,
height: 24_200_000,
}),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
await new Promise((resolve) => setTimeout(resolve, 500));
await drawn.frames(3);
const baselineSolveRevision = getWorldOverlayDiagnostics().solveRevision;
viewer.camera.setView({
destination: ellipsoid.cartographicToCartesian({
longitude: -98 * Math.PI / 180,
@@ -1270,73 +1390,59 @@ async function main() {
}),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
source.clustering.enabled = false;
viewer.scene.requestRender();
await new Promise((resolve) => setTimeout(resolve, 100));
return new Promise((resolve) => {
const originalPixelRange = source.clustering.pixelRange;
const timeout = setTimeout(() => {
remove();
resolve(null);
}, 2_000);
const remove = source.clustering.clusterEvent.addEventListener(async (entities, cluster) => {
clearTimeout(timeout);
remove();
const native = {
count: entities.length,
labelShow: cluster.label.show,
labelText: cluster.label.text,
pointSize: cluster.point.pixelSize,
pointMaxDistance: cluster.point.distanceDisplayCondition?.far ?? null,
};
source.clustering.pixelRange = originalPixelRange;
const radio = window.__godsEyeView.dataManager.layers.get('radio').module;
await new Promise((done) => setTimeout(done, 0));
let painted = null;
for (let attempt = 0; attempt < 20 && !painted?.rect; attempt += 1) {
painted = await new Promise((done) => {
let removePostRender = null;
const timeout = setTimeout(() => {
removePostRender?.();
done({ rect: null, host: getWorldOverlayDiagnostics() });
}, 150);
removePostRender = viewer.scene.postRender.addEventListener(() => {
clearTimeout(timeout);
removePostRender();
const host = getWorldOverlayDiagnostics();
let match = null;
for (const entryId of radio.getOverlayDiagnostics().clusterIds) {
const rect = getOverlayPaintRect('radio', entryId);
if (rect) match = { entryId, rect };
}
done({
expectedEntryId: match?.entryId || null,
rect: match ? { x: match.rect.x, y: match.rect.y, w: match.rect.w, h: match.rect.h } : null,
host,
});
});
viewer.scene.requestRender();
const radio = window.__godsEyeView.dataManager.layers.get('radio').module;
// The layer clusters on screen itself now (render/display.js,
// ../screenClusters.js) into the radio-clusters point set; the cluster
// text is a shared-host overlay entry. There is no Cesium cluster
// label or point to read: a painted cluster entry's anchor must pick
// a radio-clusters instance instead.
let painted = null;
for (let attempt = 0; attempt < 20 && !painted?.rect; attempt += 1) {
painted = await new Promise((done) => {
let removePostRender = null;
const timeout = setTimeout(() => {
removePostRender?.();
done({ rect: null, host: getWorldOverlayDiagnostics() });
}, 150);
removePostRender = viewer.scene.postRender.addEventListener(() => {
clearTimeout(timeout);
removePostRender();
const host = getWorldOverlayDiagnostics();
let match = null;
for (const entryId of radio.getOverlayDiagnostics().clusterIds) {
const rect = getOverlayPaintRect('radio', entryId);
if (rect) match = { entryId, rect };
}
done({
expectedEntryId: match?.entryId || null,
rect: match ? { x: match.rect.x, y: match.rect.y, w: match.rect.w, h: match.rect.h } : null,
position: match?.rect?.entry?.position || null,
host,
});
}
resolve({
native,
baselineSolveRevision,
expectedEntryId: painted?.expectedEntryId || null,
rect: painted?.rect || null,
source: radio.getOverlayDiagnostics(),
host: painted?.host || getWorldOverlayDiagnostics(),
});
viewer.scene.requestRender();
});
source.clustering.enabled = true;
viewer.scene.requestRender();
});
}
const position = typeof painted?.position === 'function' ? painted.position() : painted?.position;
const anchor = drawn.canvasPoint(position);
const clusterPick = drawn.picksAt(anchor, 16).find((picked) => picked.owner === 'radio-clusters') || null;
return {
drawn: drawn.counts(),
clusterPick,
radioLines: window.__godsEyeView.engine.lines.list().filter((line) => /^radio/.test(line.id)).length,
baselineSolveRevision,
expectedEntryId: painted?.expectedEntryId || null,
rect: painted?.rect || null,
source: radio.getOverlayDiagnostics(),
host: painted?.host || getWorldOverlayDiagnostics(),
};
});
check(
'Radio cluster text is native-free and painted through the bounded shared host',
clusterBadge?.native?.labelShow === false
&& clusterBadge?.native?.labelText === ''
&& clusterBadge?.native?.pointSize >= 14
&& clusterBadge?.native?.pointMaxDistance === 50_000_000
clusterBadge?.drawn?.['radio-clusters'] > 0
&& clusterBadge?.radioLines === 0
&& /^radio:/.test(clusterBadge?.clusterPick?.id || '')
&& clusterBadge?.source?.clusterTexts?.some((text) => /^\d+\s+[A-Z]/.test(text))
&& clusterBadge?.source?.entryCount <= 65
&& clusterBadge?.host?.entriesBySource?.radio === clusterBadge?.source?.entryCount
@@ -1374,32 +1480,24 @@ async function main() {
0,
cameraRadius * cameraRadius - ellipsoid.maximumRadius * ellipsoid.maximumRadius,
));
const points = Array.from({ length: viewer.dataSources.length }, (_, index) => viewer.dataSources.get(index))
.find((item) => item.name === 'Radio stations')?.clustering?._clusterPointCollection;
// The cluster points are the radio-clusters mover set; pick at each
// painted cluster entry's anchor (nearest the centre first) and accept
// only a radio-clusters instance.
const drawn = window.__qaRadioDrawn;
const { getPaintRect: getOverlayPaintRect } = window.__gevWorldOverlay;
let clusterPoint = null;
// Pool order is not visibility order after a camera change. Choose an
// on-screen point independently of the native pick result being tested.
const visiblePoints = [];
for (let index = 0; index < (points?.length || 0); index += 1) {
const point = points.get(index);
if (!point?.show || !Array.isArray(point.id) || !point.id.length) continue;
const anchor = viewer.scene.cartesianToCanvasCoordinates(point.position);
if (!anchor) continue;
if (anchor.x < 0 || anchor.y < 0 || anchor.x > viewer.canvas.clientWidth || anchor.y > viewer.canvas.clientHeight) continue;
visiblePoints.push({ point, anchor });
}
visiblePoints.sort((a, b) => (
Math.hypot(a.anchor.x - viewer.canvas.clientWidth / 2, a.anchor.y - viewer.canvas.clientHeight / 2)
- Math.hypot(b.anchor.x - viewer.canvas.clientWidth / 2, b.anchor.y - viewer.canvas.clientHeight / 2)
));
if (visiblePoints.length) {
const { point, anchor } = visiblePoints[0];
const exactPick = (viewer.scene.drillPick(anchor, 16) || []).find((picked) => (
picked?.primitive === point && picked?.id === point.id
));
const width = viewer.canvas.clientWidth;
const height = viewer.canvas.clientHeight;
const anchors = source.clusterIds
.map((entryId) => getOverlayPaintRect('radio', entryId)?.entry?.position)
.map((position) => drawn.canvasPoint(typeof position === 'function' ? position() : position))
.filter(Boolean)
.sort((a, b) => Math.hypot(a.x - width / 2, a.y - height / 2) - Math.hypot(b.x - width / 2, b.y - height / 2));
if (anchors.length) {
const pick = drawn.picksAt(anchors[0], 16).find((picked) => picked.owner === 'radio-clusters');
clusterPoint = {
maxDistance: point.distanceDisplayCondition?.far ?? null,
pickable: Boolean(exactPick),
drawn: drawn.counts()['radio-clusters'] ?? 0,
pickable: Boolean(pick),
};
}
return {
@@ -1418,7 +1516,7 @@ async function main() {
&& highGlobalClusterLabels.host.projectedCount > 0
&& highGlobalClusterLabels.host.paintedBySource?.radio > 0
&& highGlobalClusterLabels.horizonDistanceAlpha === 1
&& highGlobalClusterLabels.clusterPoint?.maxDistance === 50_000_000
&& highGlobalClusterLabels.clusterPoint?.drawn > 0
&& highGlobalClusterLabels.clusterPoint?.pickable,
JSON.stringify(highGlobalClusterLabels),
);
@@ -2411,30 +2509,29 @@ async function main() {
});
});
await sleep(700);
await settleRadioFrames(page);
const newsClusterTarget = await page.evaluate(() => {
const gev = window.__godsEyeView;
const source = Array.from({ length: gev.viewer.dataSources.length }, (_, index) => gev.viewer.dataSources.get(index))
.find((item) => item.name === 'Radio stations');
const points = source.clustering._clusterPointCollection;
for (let index = 0; index < (points?.length || 0); index += 1) {
const point = points.get(index);
if (!point.show || !Array.isArray(point.id) || !point.id.length) continue;
const canvasPoint = gev.viewer.scene.cartesianToCanvasCoordinates(point.position);
// A drawn cluster: a painted cluster entry whose anchor picks a
// radio-clusters instance. Its pickId names the cluster's first
// station, which a click on it plays.
const radio = window.__godsEyeView.dataManager.layers.get('radio').module;
const drawn = window.__qaRadioDrawn;
const { getPaintRect: getOverlayPaintRect } = window.__gevWorldOverlay;
const { clusterIds } = radio.getOverlayDiagnostics();
for (const entryId of clusterIds) {
const position = getOverlayPaintRect('radio', entryId)?.entry?.position;
const canvasPoint = drawn.canvasPoint(typeof position === 'function' ? position() : position);
if (!canvasPoint) continue;
const pick = drawn.picksAt(canvasPoint, 16).find((picked) => picked.owner === 'radio-clusters');
if (!pick) continue;
return {
x: canvasPoint.x,
y: canvasPoint.y,
firstId: String(point.id[0]?.id || '').replace(/^radio:/, ''),
pointIds: point.id.map((entity) => entity.id),
firstId: pick.id.replace(/^radio:/, ''),
pointIds: [pick.id],
};
}
return {
missing: true,
points: Array.from({ length: points?.length || 0 }, (_, index) => ({
show: points.get(index).show,
pointIds: Array.isArray(points.get(index)?.id) ? points.get(index).id.length : 0,
})),
};
return { missing: true, clusterIds, drawn: drawn.counts() };
});
if (!newsClusterTarget.missing) {
await page.mouse.click(newsClusterTarget.x, newsClusterTarget.y);
@@ -2635,30 +2732,40 @@ async function main() {
const horizon = await page.evaluate(async () => {
const viewer = window.__godsEyeView.viewer;
const ellipsoid = viewer.scene.globe.ellipsoid;
const source = Array.from({ length: viewer.dataSources.length }, (_, index) => viewer.dataSources.get(index))
.find((item) => item.name === 'Radio stations');
const visibleIds = () => source.entities.values.filter((entity) => entity.show).map((entity) => entity.id);
const drawn = window.__qaRadioDrawn;
// What is drawn, through the engine's picks at every fixture station's
// canvas point: a station behind the globe projects onto the canvas
// too (the point sets draw without a depth test), so a pick of one
// there means it was not culled.
const setView = (lon, lat = 0) => {
viewer.camera.cancelFlight();
viewer.camera.setView({
destination: ellipsoid.cartographicToCartesian({ longitude: lon * Math.PI / 180, latitude: lat * Math.PI / 180, height: 2_000_000 }),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
viewer.scene.requestRender();
};
setView(-54);
await new Promise((resolve) => setTimeout(resolve, 500));
const front = visibleIds();
await drawn.frames(3);
const front = drawn.drawnStations();
setView(180, -2);
await new Promise((resolve) => setTimeout(resolve, 500));
const back = visibleIds();
const frontSet = new Set(front);
await drawn.frames(3);
const back = drawn.drawnStations();
const frontSet = new Set(front.front);
return {
front: front.length,
back: back.length,
changed: back.some((id) => !frontSet.has(id)),
front: front.front.length,
back: back.front.length,
farSidePicked: front.back.length + back.back.length,
changed: back.front.some((id) => !frontSet.has(id)),
};
});
check('far-side markers are horizon culled', horizon.front > 0 && horizon.front < 750 && horizon.back > 0 && horizon.back < 750, `${horizon.front}/${horizon.back} visible`);
check(
'far-side markers are horizon culled',
horizon.front > 0 && horizon.front < 750 && horizon.back > 0 && horizon.back < 750 && horizon.farSidePicked === 0,
`${horizon.front}/${horizon.back} drawn in front, ${horizon.farSidePicked} picked behind the globe`,
);
check('horizon visibility follows camera hemisphere', horizon.changed);
await page.select('#radio-filter', 'all');
@@ -2683,8 +2790,7 @@ async function main() {
buttons: type === 'pointerup' || type === 'pointercancel' ? 0 : 1,
}));
const snapshot = () => {
const markerId = gev.viewer.entities.values
.find((entity) => String(entity.id).startsWith('radio:selected:'))?.id || null;
const markerId = window.__qaRadioDrawn.selectedMarkerId();
return {
slot: Number(slider.value),
markerId,
@@ -2804,8 +2910,7 @@ async function main() {
// The tuner renders from the Radio store; let Vue patch before reading.
await Promise.resolve();
const shiftedSlot = Number(slider.value);
const previewBeforeCancel = gev.viewer.entities.values
.find((entity) => String(entity.id).startsWith('radio:selected:'))?.id || null;
const previewBeforeCancel = window.__qaRadioDrawn.selectedMarkerId();
const flyToCallsBeforeCancel = flyToCalls.length;
pointer('pointercancel', targetIndex);
await Promise.resolve();
@@ -2821,10 +2926,7 @@ async function main() {
tuningActive: afterState.tuningActive,
};
const flyToCallsAfterCancel = flyToCalls.length;
const selectedEntity = () => gev.viewer.entities.values
.find((entity) => String(entity.id).startsWith('radio:selected:')) || null;
const restoredEntity = selectedEntity();
const previewId = restoredEntity?.id || null;
const previewId = window.__qaRadioDrawn.selectedMarkerId();
after.previewId = previewId;
const filterControl = document.getElementById('radio-filter');
const changedFilter = afterState.filter === 'news' ? 'all' : 'news';
@@ -2834,13 +2936,15 @@ async function main() {
filterControl.value = changedFilter;
filterControl.dispatchEvent(new Event('change', { bubbles: true }));
const changedFilterState = radio.getUIState();
const changedFilterEntity = selectedEntity();
// The marker is an instance of the radio-selected set: whether the
// layer rebuilt it is not observable through the contract (the old
// check compared Entity objects), only which station it is drawn on.
const changedFilterEntity = window.__qaRadioDrawn.selectedMarkerId();
const changedFilterCleanup = {
filter: changedFilterState.filter,
restoredStationId: changedFilterState.tuningRestoredStationId,
selectedId: changedFilterState.selected?.id || null,
entityRebuilt: Boolean(changedFilterEntity && changedFilterEntity !== restoredEntity),
entityId: changedFilterEntity?.id || null,
entityId: changedFilterEntity,
snapshotPreserved: radio.getAcceptedCatalogSnapshot() === changedFilterSnapshot,
playDelta: window.__qaRadioPlayCalls.length - changedFilterPlays,
cameraDelta: flyToCalls.length - changedFilterCameraMoves,
@@ -2848,14 +2952,13 @@ async function main() {
const sameFilterBegan = radio.beginTuning();
const sameFilterCancelled = radio.cancelTuning();
const sameFilterRestoredState = radio.getUIState();
const sameFilterRestoredEntity = selectedEntity();
const sameFilterSnapshot = radio.getAcceptedCatalogSnapshot();
const sameFilterPlays = window.__qaRadioPlayCalls.length;
const sameFilterCameraMoves = flyToCalls.length;
filterControl.value = sameFilterRestoredState.filter;
filterControl.dispatchEvent(new Event('change', { bubbles: true }));
const sameFilterState = radio.getUIState();
const sameFilterEntity = selectedEntity();
const sameFilterEntity = window.__qaRadioDrawn.selectedMarkerId();
const sameFilterCleanup = {
began: sameFilterBegan,
cancelled: sameFilterCancelled,
@@ -2863,8 +2966,7 @@ async function main() {
restoredAfter: sameFilterState.tuningRestoredStationId,
filter: sameFilterState.filter,
selectedId: sameFilterState.selected?.id || null,
entityRebuilt: Boolean(sameFilterEntity && sameFilterEntity !== sameFilterRestoredEntity),
entityId: sameFilterEntity?.id || null,
entityId: sameFilterEntity,
snapshotPreserved: radio.getAcceptedCatalogSnapshot() === sameFilterSnapshot,
playDelta: window.__qaRadioPlayCalls.length - sameFilterPlays,
cameraDelta: flyToCalls.length - sameFilterCameraMoves,
@@ -2904,11 +3006,10 @@ async function main() {
JSON.stringify(tunerCancelRestore),
);
check(
'accepted changed-filter action clears and rebuilds the cancel-restored marker without side effects',
'accepted changed-filter action clears the cancel-restored marker onto the selection without side effects',
tunerCancelRestore.changedFilterCleanup.filter === tunerCancelRestore.changedFilter
&& tunerCancelRestore.changedFilterCleanup.restoredStationId === null
&& tunerCancelRestore.changedFilterCleanup.selectedId === tunerCancelRestore.before.selectedId
&& tunerCancelRestore.changedFilterCleanup.entityRebuilt
&& tunerCancelRestore.changedFilterCleanup.entityId === `radio:selected:${tunerCancelRestore.before.selectedId}`
&& tunerCancelRestore.changedFilterCleanup.snapshotPreserved
&& tunerCancelRestore.changedFilterCleanup.playDelta === 0
@@ -2916,14 +3017,13 @@ async function main() {
JSON.stringify(tunerCancelRestore.changedFilterCleanup),
);
check(
'accepted same-filter action clears and rebuilds the cancel-restored marker without side effects',
'accepted same-filter action clears the cancel-restored marker onto the selection without side effects',
tunerCancelRestore.sameFilterCleanup.began
&& tunerCancelRestore.sameFilterCleanup.cancelled
&& tunerCancelRestore.sameFilterCleanup.restoredBefore === tunerCancelRestore.before.selectedId
&& tunerCancelRestore.sameFilterCleanup.restoredAfter === null
&& tunerCancelRestore.sameFilterCleanup.filter === tunerCancelRestore.changedFilter
&& tunerCancelRestore.sameFilterCleanup.selectedId === tunerCancelRestore.before.selectedId
&& tunerCancelRestore.sameFilterCleanup.entityRebuilt
&& tunerCancelRestore.sameFilterCleanup.entityId === `radio:selected:${tunerCancelRestore.before.selectedId}`
&& tunerCancelRestore.sameFilterCleanup.snapshotPreserved
&& tunerCancelRestore.sameFilterCleanup.playDelta === 0
@@ -3308,8 +3408,7 @@ async function main() {
cancelable: true,
key: value,
}));
const markerId = () => gev.viewer.entities.values
.find((entity) => String(entity.id).startsWith('radio:selected:'))?.id || null;
const markerId = () => window.__qaRadioDrawn.selectedMarkerId();
const startIndex = Number(slider.value);
const expectedIndex = Math.min(style._radioControls._radioTunerStations.length - 1, startIndex + 1);
const expectedId = style._radioControls._radioTunerStations[expectedIndex]?.id || null;
@@ -3409,8 +3508,7 @@ async function main() {
centerIndex,
slot: Number(slider.value),
ratio: Number(document.getElementById('radio-tuner').style.getPropertyValue('--radio-tuner-ratio')),
markerId: gev.viewer.entities.values
.find((entity) => String(entity.id).startsWith('radio:selected:'))?.id || null,
markerId: window.__qaRadioDrawn.selectedMarkerId(),
expectedId: style._radioControls._radioTunerStations[centerIndex]?.id || null,
};
slider.dispatchEvent(new PointerEvent('pointercancel', {
@@ -3535,9 +3633,10 @@ async function main() {
const markerTarget = await page.evaluate(async () => {
const viewer = window.__godsEyeView.viewer;
const radio = window.__godsEyeView.dataManager.layers.get('radio').module;
const source = Array.from({ length: viewer.dataSources.length }, (_, index) => viewer.dataSources.get(index))
.find((item) => item.name === 'Radio stations');
source.clustering.enabled = false;
const drawn = window.__qaRadioDrawn;
// Radio clusters on screen in its own display now (no Cesium
// clustering to switch off); at this 500 km view the News stations
// are hundreds of pixels apart, so none cluster.
const selectedId = radio.getUIState().selected?.id || null;
if (selectedId) {
radio.selectStation(selectedId, {
@@ -3564,9 +3663,6 @@ async function main() {
});
viewer.scene.requestRender();
});
// Cesium clears its private cluster collections during the rendered
// disabled-state update. Publish the same-filter singleton overlay only
// after that frame so this harness mutation cannot leak stale clusters.
radio.setFilter('news');
viewer.scene.requestRender();
await new Promise((resolve) => setTimeout(resolve, 500));
@@ -3592,27 +3688,26 @@ async function main() {
}
const diagnostics = radio.getOverlayDiagnostics();
const singletonIds = new Set(diagnostics.singletonIds);
let visibleEntityCount = 0;
let singletonEntityCount = 0;
// A singleton label painted on the shared host whose station point
// a pick at the label's anchor finds (the station's own pickId).
let pickableSingletonCount = 0;
let paintedSingletonCount = 0;
for (const entity of source.entities.values) {
if (!entity.show) continue;
visibleEntityCount += 1;
const id = String(entity.id).slice('radio:'.length);
const overlayId = `station:${id}`;
if (!singletonIds.has(overlayId)) continue;
singletonEntityCount += 1;
const position = entity.position?.getValue(viewer.clock.currentTime);
const canvas = position && viewer.scene.cartesianToCanvasCoordinates(position);
for (const overlayId of singletonIds) {
const id = overlayId.slice('station:'.length);
const paintRect = getOverlayPaintRect('radio', overlayId);
if (paintRect) paintedSingletonCount += 1;
if (paintRect && canvas?.x > 300 && canvas.x < 1100 && canvas.y > 100 && canvas.y < 800) {
if (!paintRect) continue;
paintedSingletonCount += 1;
const canvas = drawn.canvasPoint(paintRect.entry?.position);
const pickable = drawn.picksAt(canvas, 16)
.some((picked) => picked.owner === 'radio-stations' && picked.id === `radio:${id}`);
if (pickable) pickableSingletonCount += 1;
if (pickable && canvas.x > 300 && canvas.x < 1100 && canvas.y > 100 && canvas.y < 800) {
finish({
found: true,
id,
x: canvas.x,
y: canvas.y,
ownsNativeLabel: Boolean(entity.label),
pickable,
labelPainted: true,
singletonTexts: diagnostics.singletonTexts,
});
@@ -3622,8 +3717,8 @@ async function main() {
lastObservation = {
found: false,
filter: radio.getUIState().filter,
visibleEntityCount,
singletonEntityCount,
drawn: drawn.counts(),
pickableSingletonCount,
paintedSingletonCount,
singletonIds: diagnostics.singletonIds,
singletonTexts: diagnostics.singletonTexts,
@@ -3645,8 +3740,8 @@ async function main() {
});
check('Radio exposes a visible marker for click-tolerance QA', markerTarget?.found === true, JSON.stringify(markerTarget));
check(
'the directly pickable singleton point has painted shared-host text and no native label',
markerTarget?.labelPainted && !markerTarget?.ownsNativeLabel,
'the directly pickable singleton point has painted shared-host text',
markerTarget?.labelPainted && markerTarget?.pickable,
JSON.stringify(markerTarget),
);
check(
@@ -3681,8 +3776,6 @@ async function main() {
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
radio.stopPlayback();
Array.from({ length: gev.viewer.dataSources.length }, (_, index) => gev.viewer.dataSources.get(index))
.find((item) => item.name === 'Radio stations').clustering.enabled = true;
// Next plays only while a stream is active, so start from a playing station.
radio.selectStation('00000000-0000-4000-8000-000000000001', { autoplay: true, focus: false });
});
@@ -3738,8 +3831,6 @@ async function main() {
const playing = await page.evaluate(async () => {
const radio = window.__godsEyeView.dataManager.layers.get('radio').module;
const viewer = window.__godsEyeView.viewer;
const selectedEntity = window.__godsEyeView.viewer.entities.values
.find((entity) => String(entity.id).startsWith('radio:selected:'));
const { getDiagnostics: getWorldOverlayDiagnostics } = window.__gevWorldOverlay;
const flightTargets = window.__qaRadioFlyToCalls.map(({ destination }) => {
const position = viewer.scene.globe.ellipsoid.cartesianToCartographic(destination);
@@ -3756,11 +3847,11 @@ async function main() {
flightTargets,
cameraHeight: window.__qaRadioCameraHeight,
playLabel: document.getElementById('radio-play-btn').getAttribute('aria-label'),
regularMarkerSize: Array.from({ length: window.__godsEyeView.viewer.dataSources.length }, (_, index) => window.__godsEyeView.viewer.dataSources.get(index))
.find((item) => item.name === 'Radio stations')?.entities.values[0]?.point?.pixelSize?.getValue(),
selectedMarkerSize: selectedEntity?.point?.pixelSize?.getValue(),
selectedBracketWidth: selectedEntity?.billboard?.width?.getValue(),
selectedOwnsNativeLabel: Boolean(selectedEntity?.label),
// The point and bracket sizes are the display's set options
// (render/style.js), which the movers do not list; what is drawn is
// one selected point and one bracket sprite on the selection.
drawn: window.__qaRadioDrawn.counts(),
selectedMarkerId: window.__qaRadioDrawn.selectedMarkerId(),
overlay: radio.getOverlayDiagnostics(),
host: getWorldOverlayDiagnostics(),
expectedPrimary: window.__qaRadioExpectedPrimary,
@@ -3889,17 +3980,15 @@ async function main() {
});
});
check(
'individual Radio dots stay at least half the largest cluster point',
playing.regularMarkerSize === 13
&& playing.regularMarkerSize * 2 >= 26
&& playing.selectedMarkerSize === 14,
`${playing.regularMarkerSize}px / ${playing.selectedMarkerSize}px selected`,
'the current station carries a four-corner selection bracket',
playing.drawn['radio-selected'] === 1
&& playing.drawn['radio-bracket'] === 1
&& playing.selectedMarkerId === `radio:selected:${playing.state.selected?.id}`,
JSON.stringify({ drawn: playing.drawn, marker: playing.selectedMarkerId }),
);
check('the current station carries a 40px four-corner selection bracket', playing.selectedBracketWidth === 40, `${playing.selectedBracketWidth}px`);
check(
'selected-station text is published to the protected shared-host lane with no native entity label',
!playing.selectedOwnsNativeLabel
&& playing.overlay.selectedCount === 1
'selected-station text is published to the protected shared-host lane',
playing.overlay.selectedCount === 1
&& playing.host.entriesBySource?.radio === playing.overlay.entryCount,
JSON.stringify({ overlay: playing.overlay, host: playing.host.entriesBySource }),
);
@@ -3921,16 +4010,21 @@ async function main() {
destination,
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
const selectedEntity = viewer.entities.values
.find((entity) => String(entity.id) === `radio:selected:${selected.id}`);
// The selected point is drawn when a pick at its anchor finds its
// pickId (the old check read the Entity's show).
const drawn = window.__qaRadioDrawn;
const selectedShown = () => drawn
.picksAt(drawn.canvasPoint(drawn.stationPosition(selected.id, 5)), 8)
.some((picked) => picked.owner === 'radio-selected' && picked.id === `radio:selected:${selected.id}`);
const { getPaintRect: getOverlayPaintRect, getDiagnostics: getWorldOverlayDiagnostics } = window.__gevWorldOverlay;
const expectedEntryId = `selected:${selected.id}`;
let painted = null;
// Radio's horizon pass reveals the selected entity on its own timer
let shown = false;
// Radio's horizon pass reveals the selected point on its own timer
// after the camera jump, so a stationary view waits for both to settle.
for (
let attempt = 0;
attempt < 20 && !(painted?.rect && selectedEntity?.show === true);
attempt < 20 && !(painted?.rect && shown);
attempt += 1
) {
painted = await new Promise((resolve) => {
@@ -3950,12 +4044,13 @@ async function main() {
});
viewer.scene.requestRender();
});
shown = selectedShown();
}
return {
expectedEntryId,
rect: painted?.rect || null,
host: painted?.host || getWorldOverlayDiagnostics(),
selectedEntityShow: selectedEntity?.show,
selectedEntityShow: shown,
cameraHeight: viewer.camera.positionCartographic.height,
};
});
@@ -3980,8 +4075,7 @@ async function main() {
}),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
const selectedEntity = viewer.entities.values
.find((entity) => String(entity.id) === `radio:selected:${selected.id}`);
const drawn = window.__qaRadioDrawn;
const { getPaintRect: getOverlayPaintRect, getDiagnostics: getWorldOverlayDiagnostics } = window.__gevWorldOverlay;
const expectedEntryId = `selected:${selected.id}`;
let rect = null;
@@ -4006,50 +4100,32 @@ async function main() {
viewer.scene.requestRender();
});
}
const entityPosition = selectedEntity?.position?.getValue?.();
const entityAnchor = entityPosition
&& viewer.scene.cartesianToCanvasCoordinates(entityPosition);
// The selected point and its bracket are instances of the
// radio-selected and radio-bracket mover sets, both picked as
// `radio:selected:<id>`; the pick at the point's anchor is the check.
const pickId = `radio:selected:${selected.id}`;
const entityPosition = drawn.stationPosition(selected.id, 5);
const entityAnchor = drawn.canvasPoint(entityPosition);
let selectedNativePick = null;
let selectedPointPick = null;
let selectedBillboardPick = null;
// Retry on the STRICT anchor: waiting only for "any pick" let a
// billboard satisfy the loop while the point anchor was still missing.
// Retry on the STRICT anchor: waiting only for "any pick" let the
// bracket satisfy the loop while the point anchor was still missing.
for (let attempt = 0; entityAnchor && attempt < 8 && !selectedPointPick; attempt += 1) {
const picks = viewer.scene.drillPick(entityAnchor, 16) || [];
selectedNativePick = picks.find((picked) => picked?.id === selectedEntity);
selectedPointPick = picks.find((picked) => (
picked?.id === selectedEntity
// Public point properties survive production minification; class names do not.
&& Number.isFinite(picked?.primitive?.pixelSize)
&& Number.isFinite(picked?.primitive?.outlineWidth)
));
selectedBillboardPick = picks.find((picked) => (
picked?.id === selectedEntity
&& Number.isFinite(picked?.primitive?.width)
&& Number.isFinite(picked?.primitive?.height)
));
if (!selectedPointPick) {
await new Promise((resolve) => {
const removePostRender = viewer.scene.postRender.addEventListener(() => {
removePostRender();
resolve();
});
viewer.scene.requestRender();
});
}
const picks = drawn.picksAt(entityAnchor, 16);
selectedNativePick = picks.find((picked) => picked.id === pickId);
selectedPointPick = picks.find((picked) => picked.id === pickId && picked.owner === 'radio-selected');
selectedBillboardPick = picks.find((picked) => picked.id === pickId && picked.owner === 'radio-bracket');
if (!selectedPointPick) await drawn.frames(1);
}
const selectedPointDistance = selectedEntity?.point?.distanceDisplayCondition?.getValue?.();
const counts = drawn.counts();
return {
rect,
selectedNativePickable: Boolean(selectedNativePick),
selectedPointPickable: Boolean(selectedPointPick),
selectedBillboardPickable: Boolean(selectedBillboardPick),
selectedPointMaxDistance: selectedPointDistance?.far ?? null,
selectedEntityShow: selectedEntity?.show,
selectedBracketVisible: Boolean(
selectedEntity?.billboard
&& selectedEntity.billboard.show?.getValue?.() !== false
),
selectedEntityShow: Boolean(selectedPointPick),
selectedBracketVisible: counts['radio-bracket'] === 1,
entityAnchor: entityAnchor ? { x: entityAnchor.x, y: entityAnchor.y } : null,
entityDistance: entityPosition
? Math.hypot(
@@ -4070,15 +4146,14 @@ async function main() {
&& highGlobalSelectedText.rect?.h > 0
&& highGlobalSelectedText.selectedEntityShow === true
&& highGlobalSelectedText.selectedBracketVisible
&& highGlobalSelectedText.selectedPointMaxDistance === 50_000_000
// The POINT anchor is the invariant: it is what stays pickable out to
// the 50,000 km band. Accepting any native pick let the co-located
// billboard keep this green after the point anchor regressed.
// the 50,000 km band (the display's distance cut; the movers list no
// per-set distance, so the old 50,000 km distance-condition read is
// gone). Accepting any pick let the co-located bracket keep this
// green after the point anchor regressed.
//
// `selectedBillboardPickable` is reported for diagnostics but NOT
// asserted: measured here it is false — the selection bracket renders
// (selectedBracketVisible) without being pickable at the point anchor,
// so requiring it would pin a behavior the app does not have.
// asserted.
&& highGlobalSelectedText.selectedPointPickable
&& highGlobalSelectedText.selectedNativePickable,
JSON.stringify(highGlobalSelectedText),