mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-10 21:32:23 +00:00
Define the renderer contract, with a stub renderer and a conformance suite
The renderer contract is what the application asks of a 3D renderer, so a renderer other than Cesium can draw the same application. It is nine ports (frame, globe, draw, camera, query, input, effects, credits, quality) plus the renderer itself, one strict .d.ts per port in src/render/contract/. PORTS lists every member at run time, and a TypeScript-compiler test fails when the list and the types disagree on one member. An optional member exists only with its capability; a required one answers null or empty without it. Nothing in the application uses the contract yet. The stub renderer (src/render/stub/) implements every port in memory and draws nothing. Its camera is a port of the default camera's maths: views, lookAt, flights with their height arc and easing, rectangle framing on the geodesic midpoint, the view rectangle with its horizon and pole cases, changed thresholds, follows, and flights that finish inside the call. The conformance suite (src/render/conformance/) runs 59 checks against any renderer through the contract alone, in Node or in a browser page. The camera checks compare traces recorded from Cesium's own Camera, headless on a virtual clock (scripts/record-camera-goldens.mjs), to 1 mm, 1e-9 and 1e-3 px. The other checks cover listener rules on every subscription, the frame loop, draw sets, picks, motion clocks, heights, leases, imagery, styles, credits and quality knobs. The stub passes all of them but the model check, which needs a glTF asset. Reference scenes draw one scene per primitive family, a city and a calibration path from orbit to the street, from contract calls only and with no network. /dev/conformance/ draws them on the Cesium scene renderer (src/render/cesium/sceneRenderer.js), records references through a dev-server-only, loopback-only endpoint, and compares captures with pixelmatch's antialias detection plus a solid-pixel count. Measured on Cesium: every MSAA setting passes, and a dropped 4 px point, a dropped 2 px line and a 1 px shift of the view fail. visual.js states the limits. The package exports ./render/contract, ./render/stub and ./render/conformance. Documented in docs/RENDERER-CONTRACT.md, docs/CODE-BOUNDARIES.md and docs/CURRENT-STATE.md.
This commit is contained in:
1 parent
fb30287898
commit
2fc99e7c19
65 files changed
+12761
-9
No files matched your search
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* The conformance page: runs the logic suite in the browser, and draws the
|
||||
* reference scenes on a renderer to check them against the stored
|
||||
* references (or record new ones). Driven by hand or through the
|
||||
* chrome-devtools MCP; `window.conformance` exposes the same actions.
|
||||
*/
|
||||
import {
|
||||
DEFAULT_TOLERANCE,
|
||||
runConformance,
|
||||
} from '../../src/render/conformance/index.js';
|
||||
import {
|
||||
REFERENCE_SCENES,
|
||||
sceneSupport,
|
||||
} from '../../src/render/conformance/scenes.js';
|
||||
import {
|
||||
compareImages,
|
||||
drawScene,
|
||||
referenceName,
|
||||
} from '../../src/render/conformance/visual.js';
|
||||
import { createStubHarness } from '../../src/render/stub/harness.js';
|
||||
import { toEcef } from '../../src/render/stub/math.js';
|
||||
import { createCesiumSceneRenderer } from '../../src/render/cesium/sceneRenderer.js';
|
||||
|
||||
const REFERENCES = '/src/render/conformance/goldens/visual/';
|
||||
const SAVE = '/__conformance/references/';
|
||||
const ctx = { toEcef, assets: { model: '/models/c172.glb' } };
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const status = (text) => ($('status').textContent = text);
|
||||
|
||||
const RENDERERS = {
|
||||
cesium: () => createCesiumSceneRenderer({ container: $('stage') }),
|
||||
};
|
||||
|
||||
async function runLogic() {
|
||||
status('running the logic suite…');
|
||||
const report = await runConformance(createStubHarness('stub (browser)'));
|
||||
$('logicResults').innerHTML =
|
||||
`<p>${report.harness}: <b class="pass">${report.passed} passed</b>, <b class="fail">${report.failed} failed</b>, ${report.skipped} skipped</p>
|
||||
<table>${report.results
|
||||
.filter((r) => r.status !== 'pass')
|
||||
.map(
|
||||
(r) =>
|
||||
`<tr><td class="${r.status}">${r.status}</td><td>${r.id}</td><td>${escape(r.detail ?? '')}</td></tr>`,
|
||||
)
|
||||
.join('')}</table>`;
|
||||
status('');
|
||||
return {
|
||||
passed: report.passed,
|
||||
failed: report.failed,
|
||||
skipped: report.skipped,
|
||||
failures: report.results.filter((r) => r.status === 'fail'),
|
||||
};
|
||||
}
|
||||
|
||||
const escape = (s) =>
|
||||
s.replace(/[&<>]/g, (c) => ({ '&': '&', '<': '<', '>': '>' })[c]);
|
||||
|
||||
async function pixels(blob) {
|
||||
const bitmap = await createImageBitmap(blob);
|
||||
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
|
||||
const g = canvas.getContext('2d');
|
||||
g.drawImage(bitmap, 0, 0);
|
||||
return g.getImageData(0, 0, bitmap.width, bitmap.height);
|
||||
}
|
||||
|
||||
// Object URLs shown for the current run, revoked when the next one starts.
|
||||
let objectUrls = [];
|
||||
function objectUrl(blob) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
objectUrls.push(url);
|
||||
return url;
|
||||
}
|
||||
|
||||
async function toUrl(imageData) {
|
||||
const canvas = new OffscreenCanvas(imageData.width, imageData.height);
|
||||
canvas.getContext('2d').putImageData(imageData, 0, 0);
|
||||
return objectUrl(await canvas.convertToBlob());
|
||||
}
|
||||
|
||||
/** The views the stored manifest lists, or none. */
|
||||
async function storedViews() {
|
||||
try {
|
||||
const res = await fetch(REFERENCES + 'manifest.json', {
|
||||
cache: 'no-store',
|
||||
});
|
||||
return res.ok ? ((await res.json()).views ?? []) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function reference(name) {
|
||||
const res = await fetch(REFERENCES + name, { cache: 'no-store' });
|
||||
return res.ok && res.headers.get('content-type')?.startsWith('image/')
|
||||
? res.blob()
|
||||
: null;
|
||||
}
|
||||
|
||||
/** Draws every scene; records references or compares against them. */
|
||||
async function runScenes({ record = false, only } = {}) {
|
||||
const which = $('renderer').value;
|
||||
const renderer = await RENDERERS[which]();
|
||||
const results = [];
|
||||
$('views').innerHTML = '';
|
||||
for (const url of objectUrls) URL.revokeObjectURL(url);
|
||||
objectUrls = [];
|
||||
try {
|
||||
for (const scene of REFERENCE_SCENES) {
|
||||
if (only && !only.includes(scene.id)) continue;
|
||||
const unsupported = sceneSupport(scene, renderer, ctx);
|
||||
if (unsupported) {
|
||||
results.push({ scene: scene.id, status: 'skip', detail: unsupported });
|
||||
continue;
|
||||
}
|
||||
status(`${scene.id}…`);
|
||||
await drawScene(renderer, scene, ctx, async (view, capture, settle) => {
|
||||
const name = referenceName(scene.id, view.name);
|
||||
const figure = document.createElement('figure');
|
||||
$('views').append(figure);
|
||||
if (settle.state !== 'settled') {
|
||||
const detail = `${settle.state}: ${settle.blockers.join(', ')}`;
|
||||
results.push({
|
||||
scene: scene.id,
|
||||
view: view.name,
|
||||
status: 'fail',
|
||||
detail,
|
||||
});
|
||||
figure.innerHTML = `<img src="${objectUrl(capture)}"><figcaption class="fail">${name}: ${escape(detail)}</figcaption>`;
|
||||
return;
|
||||
}
|
||||
if (record) {
|
||||
const res = await fetch(SAVE + name, {
|
||||
method: 'PUT',
|
||||
body: capture,
|
||||
});
|
||||
results.push({
|
||||
scene: scene.id,
|
||||
view: view.name,
|
||||
status: res.ok ? 'recorded' : 'fail',
|
||||
detail: res.ok ? undefined : `save ${res.status}`,
|
||||
});
|
||||
figure.innerHTML = `<img src="${objectUrl(capture)}"><figcaption>${name}: recorded</figcaption>`;
|
||||
return;
|
||||
}
|
||||
const refBlob = await reference(name);
|
||||
if (!refBlob) {
|
||||
results.push({
|
||||
scene: scene.id,
|
||||
view: view.name,
|
||||
status: 'fail',
|
||||
detail: 'no reference',
|
||||
});
|
||||
figure.innerHTML = `<img src="${objectUrl(capture)}"><figcaption class="fail">${name}: no reference</figcaption>`;
|
||||
return;
|
||||
}
|
||||
const [actual, expected] = await Promise.all([
|
||||
pixels(capture),
|
||||
pixels(refBlob),
|
||||
]);
|
||||
const diff = new Uint8ClampedArray(actual.data.length);
|
||||
const outcome = compareImages(actual, expected, { diff });
|
||||
results.push({
|
||||
scene: scene.id,
|
||||
view: view.name,
|
||||
status: outcome.match ? 'pass' : 'fail',
|
||||
diffFraction: outcome.diffFraction,
|
||||
solidPixels: outcome.solidPixels,
|
||||
mismatchedPixels: outcome.mismatchedPixels,
|
||||
...(outcome.reason ? { detail: outcome.reason } : {}),
|
||||
});
|
||||
const diffUrl = outcome.reason
|
||||
? ''
|
||||
: await toUrl(new ImageData(diff, actual.width, actual.height));
|
||||
figure.innerHTML = `<div class="row"><img src="${objectUrl(capture)}" title="${which}"><img src="${objectUrl(refBlob)}" title="reference"><img src="${diffUrl}" title="difference"></div>
|
||||
<figcaption class="${outcome.match ? 'pass' : 'fail'}">${name}: ${outcome.solidPixels} solid, ${outcome.mismatchedPixels} unexcused, ${(outcome.diffFraction * 100).toFixed(3)}% differ ${outcome.reason ?? ''}</figcaption>`;
|
||||
});
|
||||
}
|
||||
if (record) {
|
||||
// A partial recording keeps the other scenes' entries.
|
||||
const kept = only
|
||||
? (await storedViews()).filter(
|
||||
(view) => !only.some((id) => view.startsWith(`${id}-`)),
|
||||
)
|
||||
: [];
|
||||
const recorded = results
|
||||
.filter((r) => r.status === 'recorded')
|
||||
.map((r) => referenceName(r.scene, r.view));
|
||||
await fetch(SAVE + 'manifest.json', {
|
||||
method: 'PUT',
|
||||
body:
|
||||
JSON.stringify(
|
||||
{
|
||||
renderer: which,
|
||||
size: {
|
||||
width: renderer.canvas.width,
|
||||
height: renderer.canvas.height,
|
||||
},
|
||||
views: [...kept, ...recorded],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
) + '\n',
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
renderer.destroy();
|
||||
status('');
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
$('logic').onclick = () => runLogic();
|
||||
$('check').onclick = () => runScenes();
|
||||
$('record').onclick = () => runScenes({ record: true });
|
||||
window.conformance = { runLogic, runScenes };
|
||||
Reference in new issue
Block a user