Files
workinf_Blender_Wasm/tools/web/check-re-pbr-lesson.mjs
mes123456 d5dd647881
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
feat(web): finish RE 5000PBR lesson video and fix non-ASCII GLB import
- Add 11:07 Chinese-narrated lesson video, VTT, poster and 13 chapter screenshots.
- Regenerate 20-render WebGL2 matrix and 15 operation captures on RTX 4060.
- Fix WASM staging for non-ASCII import filenames (猴头.glb); rebuild web_engine.
- Harden lesson video recording pipeline; fix stale lesson-link E2E selector.
- Add RE PBR derived libraries, lesson page, evidence and status report.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-11-12 21:49:24 +08:00

107 lines
5.7 KiB
JavaScript

#!/usr/bin/env node
/** Validate the RE 5000PBR render matrix, lesson assets and static HTML. */
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
const REPO = path.resolve(import.meta.dirname, "../..");
const OUT = path.join(REPO, "tests/golden/RE-PBR-RENDER-001");
const LESSON = path.join(REPO, "web/app/public/tutorial/pbr");
const GLB_SOURCE = path.join(REPO, "re/猴头.glb");
const GLB_FIXTURE = path.join(REPO, "tests/files/web/pbr-render-lesson/monkey-head.glb");
const MATERIALS = [
"Black_Anti_Slip_grip",
"Blackish_Brick_Wall_016",
"black_randome_tile",
"black_shinning_tile",
"black_tiles_wall",
"black_worn_wall_brick",
];
const sha256 = (data) => crypto.createHash("sha256").update(data).digest("hex");
const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
const pngSize = (file) => {
const bytes = fs.readFileSync(file);
assert.equal(bytes.subarray(0, 8).toString("hex"), "89504e470d0a1a0a", `not a PNG: ${file}`);
return { width: bytes.readUInt32BE(16), height: bytes.readUInt32BE(20) };
};
const report = readJson(path.join(OUT, "report.json"));
assert.equal(report.status, "PASS");
assert.equal(report.renderer, "WEB_WEBGL2");
assert.equal(report.headed, true);
assert.equal(report.renders.length, 20, "expected 20 renders");
assert.equal(report.steps.length, 15, "expected 15 lesson steps");
assert.equal(report.errors.length, 0);
assert.equal(sha256(fs.readFileSync(GLB_SOURCE)), sha256(fs.readFileSync(GLB_FIXTURE)), "GLB fixture SHA mismatch");
assert.equal(report.glbSha256, sha256(fs.readFileSync(GLB_FIXTURE)));
const materialScene = new Map();
for (const render of report.renders) {
const file = path.join(REPO, render.path);
assert.ok(fs.existsSync(file), `missing render: ${render.path}`);
assert.equal(sha256(fs.readFileSync(file)), render.sha256, `render SHA mismatch: ${render.path}`);
assert.equal(fs.statSync(file).size, render.byteLength, `render byte length mismatch: ${render.path}`);
const size = pngSize(file);
assert.equal(size.width, render.resolution, `render width mismatch: ${render.path}`);
assert.equal(size.height, render.resolution, `render height mismatch: ${render.path}`);
assert.ok(render.mean > 1 && render.mean < 254, `render appears blank: ${render.path} mean=${render.mean}`);
assert.ok(render.textureStatus?.startsWith("ready:"), `texture status not ready: ${render.path} ${render.textureStatus}`);
assert.ok(render.textureAssets > 0, `no texture assets: ${render.path}`);
if (render.camera === "SCENE" && render.resolution === 1024 && render.material && !render.name.includes("reopen")) {
materialScene.set(render.material, render.sha256);
}
assert.ok(fs.existsSync(path.join(REPO, render.jpg)), `missing lesson JPG: ${render.jpg}`);
}
assert.equal(materialScene.size, 6, "missing 1024 scene renders for six materials");
assert.ok(report.renders.every((render) => !String(render.compileStatus ?? "").includes("BLOCKED")), "a material compile was BLOCKED");
assert.ok(report.renders.filter((render) => String(render.compileStatus ?? "").includes("COMPILED")).length >= 6, "six RE materials must compile");
assert.equal(new Set(materialScene.values()).size, 6, "six material renders are not distinct");
const baseline = report.renders.find((render) => render.name === "baseline-unassigned-1024-scene");
assert.ok(baseline, "baseline render missing");
assert.ok(![...materialScene.values()].includes(baseline.sha256), "baseline identical to a material render");
assert.ok(report.renders.some((render) => render.name.endsWith("-reopen-1024-scene")), "reopen render missing");
const htmlPath = path.join(LESSON, "render-lesson.html");
const cssPath = path.join(LESSON, "render-lesson.css");
assert.ok(fs.existsSync(htmlPath), "render-lesson.html missing");
assert.ok(fs.existsSync(cssPath), "render-lesson.css missing");
const html = fs.readFileSync(htmlPath, "utf8");
assert.ok(!/<script\b/i.test(html), "lesson must not contain inline scripts");
assert.ok(!/<style\b/i.test(html), "lesson must not contain inline style blocks");
assert.ok(!/\son[a-z]+\s*=/i.test(html), "lesson must not contain inline on* handlers");
assert.ok(!/https?:\/\//i.test(html), "lesson must not reference remote URLs");
assert.ok(html.includes("20 张 Web/WebGL2 成片"), "lesson must state the 20-render result");
const sources = [...html.matchAll(/(?:src|href)="([^"]+)"/g)].map((match) => match[1]);
for (const source of sources) {
if (source.startsWith("#") || source.startsWith("/?") || source.startsWith("mailto:")) continue;
assert.ok(source.startsWith("/"), `relative lesson URL is not supported: ${source}`);
const file = path.join(REPO, "web/app/public", source.slice(1));
assert.ok(fs.existsSync(file), `lesson asset missing: ${source}`);
}
for (const match of html.matchAll(/poster="([^"]+)"/g)) {
const source = match[1];
assert.ok(source.startsWith("/"), `relative poster is not supported: ${source}`);
assert.ok(fs.existsSync(path.join(REPO, "web/app/public", source.slice(1))), `lesson poster missing: ${source}`);
}
for (const material of MATERIALS) {
assert.ok(html.includes(`material-${material}-1024-scene.jpg`), `lesson misses ${material} render`);
assert.ok(html.includes(material), `lesson misses ${material} name`);
}
for (const step of report.steps) {
assert.ok(fs.existsSync(path.join(REPO, step.path)), `missing step image: ${step.path}`);
}
assert.ok(fs.existsSync(path.join(LESSON, "lesson/landing.jpg")), "landing image missing");
console.log(JSON.stringify({
status: "PASS",
renders: report.renders.length,
steps: report.steps.length,
materials: materialScene.size,
baselineMean: baseline.mean,
lesson: path.relative(REPO, htmlPath),
}, null, 2));