test(3d): npm run 3d:review — triptychs/heatmaps for ALL pairs, not just changed

compare-dirs gains --artifacts changed|always: 'always' also writes the
side-by-side triptych + heatmap for pairs that PASS the floor (blue-grey
UNCHANGED caption), so a human can eyeball old-vs-new without a failing
gate. The gating scripts keep the default (empty diff dir = green);
3d:review writes the full 47-pair set to output/diff/parity-review/.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Istvan Matejcsok 2026-07-03 15:25:51 +02:00
commit 2aa7657c64
3 changed files with 22 additions and 3 deletions

View file

@ -53,6 +53,7 @@
"3d:check": "tsx tools/screenshots/compare-dirs.ts --old 3d-regression/baseline --new 3d-regression/output/native --out 3d-regression/output/diff/native-self --floors 3d-regression/floors.json --level native-self --label 3d-native --fail-on-change", "3d:check": "tsx tools/screenshots/compare-dirs.ts --old 3d-regression/baseline --new 3d-regression/output/native --out 3d-regression/output/diff/native-self --floors 3d-regression/floors.json --level native-self --label 3d-native --fail-on-change",
"3d:check:webgl": "tsx tools/screenshots/compare-dirs.ts --old 3d-regression/baseline-webgl --new 3d-regression/output/webgl --out 3d-regression/output/diff/webgl-self --floors 3d-regression/floors.json --level webgl-self --label 3d-webgl --fail-on-change", "3d:check:webgl": "tsx tools/screenshots/compare-dirs.ts --old 3d-regression/baseline-webgl --new 3d-regression/output/webgl --out 3d-regression/output/diff/webgl-self --floors 3d-regression/floors.json --level webgl-self --label 3d-webgl --fail-on-change",
"3d:check:parity": "tsx tools/screenshots/compare-dirs.ts --old 3d-regression/baseline --new 3d-regression/output/webgl --out 3d-regression/output/diff/parity --floors 3d-regression/floors.json --level webgl-vs-native --label 3d-parity", "3d:check:parity": "tsx tools/screenshots/compare-dirs.ts --old 3d-regression/baseline --new 3d-regression/output/webgl --out 3d-regression/output/diff/parity --floors 3d-regression/floors.json --level webgl-vs-native --label 3d-parity",
"3d:review": "tsx tools/screenshots/compare-dirs.ts --old 3d-regression/baseline --new 3d-regression/output/webgl --out 3d-regression/output/diff/parity-review --floors 3d-regression/floors.json --level webgl-vs-native --label 3d-parity --artifacts always",
"3d:test:webgl": "playwright test e2e/3d-webgl.spec.ts" "3d:test:webgl": "playwright test e2e/3d-webgl.spec.ts"
}, },
"devDependencies": { "devDependencies": {

View file

@ -60,6 +60,7 @@ function parseArgs(argv: string[]): Record<string, string | boolean> {
else if (a === '--floor') out.floor = argv[++i]; else if (a === '--floor') out.floor = argv[++i];
else if (a === '--label') out.label = argv[++i]; else if (a === '--label') out.label = argv[++i];
else if (a === '--fail-on-change') out.failOnChange = true; else if (a === '--fail-on-change') out.failOnChange = true;
else if (a === '--artifacts') out.artifacts = argv[++i];
else { else {
console.error(`[compare-dirs] unknown argument: ${a}`); console.error(`[compare-dirs] unknown argument: ${a}`);
process.exit(2); process.exit(2);
@ -72,7 +73,8 @@ function usageError(msg: string): never {
console.error(`[compare-dirs] ${msg}`); console.error(`[compare-dirs] ${msg}`);
console.error( console.error(
'[compare-dirs] usage: --old <dir> --new <dir> --out <dir> ' + '[compare-dirs] usage: --old <dir> --new <dir> --out <dir> ' +
'[--floors <json> --level <level>] [--floor <ratio>] [--fail-on-change] [--label <text>]' '[--floors <json> --level <level>] [--floor <ratio>] [--fail-on-change] [--label <text>] ' +
'[--artifacts changed|always]'
); );
process.exit(2); process.exit(2);
} }
@ -131,7 +133,12 @@ function main(): void {
unchangedCount: 0, unchangedCount: 0,
}; };
const caption = (status: 'added' | 'removed' | 'changed', name: string) => labelText(status, name, label); const artifacts = (args.artifacts as string) || 'changed';
if (artifacts !== 'changed' && artifacts !== 'always')
usageError(`--artifacts must be "changed" or "always", got "${artifacts}"`);
const caption = (status: 'added' | 'removed' | 'changed' | 'unchanged', name: string) =>
labelText(status, name, label);
for (const name of oldNames) { for (const name of oldNames) {
if (!newNames.has(name)) { if (!newNames.has(name)) {
@ -155,6 +162,16 @@ function main(): void {
const { result, heatmap, triptych } = pair; const { result, heatmap, triptych } = pair;
if (result.verdict === 'unchanged') { if (result.verdict === 'unchanged') {
report.unchangedCount++; report.unchangedCount++;
// Review mode: keep the triptych/heatmap for passing pairs too, so a
// human can eyeball old-vs-new side by side without a failing gate.
if (artifacts === 'always') {
const triptychPath = path.join(outDir, `${name}.triptych.png`);
savePng(triptychPath,
withBottomLabel(triptych, caption('unchanged', name), LABEL.colors.unchanged));
savePng(path.join(outDir, `${name}.heatmap.png`), heatmap);
}
continue; continue;
} }
const triptychPath = path.join(outDir, `${name}.triptych.png`); const triptychPath = path.join(outDir, `${name}.triptych.png`);

View file

@ -103,10 +103,11 @@ export const LABEL = {
added: [46, 125, 50] as [number, number, number], // green added: [46, 125, 50] as [number, number, number], // green
removed: [198, 40, 40] as [number, number, number], // red removed: [198, 40, 40] as [number, number, number], // red
changed: [239, 108, 0] as [number, number, number], // orange changed: [239, 108, 0] as [number, number, number], // orange
unchanged: [69, 90, 100] as [number, number, number], // blue-grey (review-only artifacts)
}, },
}; };
export type LabelStatus = 'added' | 'removed' | 'changed'; export type LabelStatus = 'added' | 'removed' | 'changed' | 'unchanged';
/** Caption text: `CHANGED name.png · kicad/pcbnew.spec.ts` (spec omitted if unknown). */ /** Caption text: `CHANGED name.png · kicad/pcbnew.spec.ts` (spec omitted if unknown). */
export function labelText(status: LabelStatus, name: string, spec: string | null): string { export function labelText(status: LabelStatus, name: string, spec: string | null): string {