Files
workinf_Blender_Wasm/tools/web/check-re-pbr-library.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

131 lines
6.8 KiB
JavaScript

#!/usr/bin/env node
/** Validate the derived RE 5000PBR asset library and its preview/manifest identity. */
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
const REPO = path.resolve(import.meta.dirname, "../..");
const args = process.argv.slice(2);
const flag = (name, fallback) => {
const index = args.indexOf(`--${name}`);
return index >= 0 && args[index + 1] ? args[index + 1] : fallback;
};
const HQ = args.includes("--hq");
const DIR = path.resolve(args.find((value) => !value.startsWith("--") && value !== "hq") ?? flag("dir", path.join(REPO, HQ ? "web/app/public/assets/re-pbr-hq" : "web/app/public/assets/re-pbr")));
const BLENDER = path.join(REPO, "build_blender_native/bin/blender");
const LIBRARY_ID = flag("library-id", HQ ? "library:re-pbr-hq-v1" : "library:re-pbr-v1");
const LIBRARY_NAME = flag("library-name", HQ ? "RE 5000PBR HQ" : "RE 5000PBR");
const LIBRARY_SOURCE = flag("source", HQ ? "libraries/re-pbr-hq-library.blend" : "libraries/re-pbr-library.blend");
const EXPECTED_MATERIALS = 6;
const MAX_LIBRARY_BYTES = Number(flag("max-library-bytes", HQ ? 16 * 1024 * 1024 : 5 * 1024 * 1024));
const MAX_PREVIEW_BYTES = 128 * 1024;
const MAX_TEXTURE_SIZE = Number(flag("max-texture-size", HQ ? 2048 : 1024));
const SUFFIX = flag("suffix", HQ ? "_HQ" : "");
const sha256 = (data) => crypto.createHash("sha256").update(data).digest("hex");
const readJson = (name) => JSON.parse(fs.readFileSync(path.join(DIR, name), "utf8"));
const assertAscii = (name, value) => assert.match(value, /^[\x20-\x7e]+$/, `${name} must be ASCII: ${value}`);
const manifest = readJson("manifest.json");
const report = readJson("build-report.json");
const previewManifest = readJson("preview-manifest.json");
assert.equal(manifest.schemaVersion, 2);
assert.equal(manifest.libraries.length, 1);
assert.equal(manifest.libraries[0].libraryId, LIBRARY_ID);
assert.equal(manifest.libraries[0].name, LIBRARY_NAME);
assert.equal(manifest.libraries[0].sourcePath, LIBRARY_SOURCE);
assert.equal(manifest.libraries[0].readOnly, true);
assert.equal(manifest.assets.length, EXPECTED_MATERIALS);
assert.ok(manifest.assets.every((asset) => asset.idType === "MATERIAL"), "all assets must be MATERIAL");
assert.ok(manifest.assets.every((asset) => asset.assetLibraryIdentifier === LIBRARY_ID));
assert.ok(manifest.assets.every((asset) => asset.preview), "every asset needs a preview");
const libraryPath = path.join(DIR, "re-pbr-library.blend");
const libraryBytes = fs.readFileSync(libraryPath);
const librarySha = sha256(libraryBytes);
assert.equal(librarySha, manifest.libraries[0].sourceSha256, "manifest library SHA mismatch");
assert.equal(librarySha, report.library.sha256, "build report library SHA mismatch");
assert.ok(libraryBytes.byteLength <= MAX_LIBRARY_BYTES, `library exceeds ${MAX_LIBRARY_BYTES} bytes`);
assert.equal(report.materials.length, EXPECTED_MATERIALS);
assert.equal(previewManifest.previews.length, EXPECTED_MATERIALS);
for (const name of ["manifest.json", "build-report.json", "preview-manifest.json", "blender_assets.cats.txt", "re-pbr-library.blend"]) {
assertAscii("file", name);
}
for (const asset of manifest.assets) {
assertAscii("asset name", asset.name);
assert.ok(asset.name !== "Dots Stroke", "Dots Stroke must not be exported");
if (SUFFIX) assert.ok(asset.name.endsWith(SUFFIX), `asset name misses suffix ${SUFFIX}: ${asset.name}`);
assert.ok(asset.preview.byteLength <= MAX_PREVIEW_BYTES, `preview too large: ${asset.name}`);
const previewPath = path.join(DIR, "previews", `${asset.preview.sha256}.png`);
const previewBytes = fs.readFileSync(previewPath);
assert.equal(sha256(previewBytes), asset.preview.sha256, `preview SHA mismatch: ${asset.name}`);
assert.equal(previewBytes.byteLength, asset.preview.byteLength, `preview size mismatch: ${asset.name}`);
assert.equal(asset.preview.width, 256);
assert.equal(asset.preview.height, 256);
}
const blenderExpr = `
import bpy, json
result = {"materials": {}, "errors": []}
expected = {"FRAME", "REROUTE", "DISPLACEMENT", "MAPPING", "VALUE", "BSDF_PRINCIPLED", "NORMAL_MAP"}
for material in bpy.data.materials:
if material.name == "Dots Stroke" or not material.use_nodes or material.node_tree is None:
continue
types = {node.type for node in material.node_tree.nodes}
images = []
for node in material.node_tree.nodes:
if node.type == "TEX_IMAGE" and node.image is not None:
image = node.image
images.append({
"name": image.name,
"size": list(image.size[:]),
"colorspace": image.colorspace_settings.name,
"packed": bool(image.packed_file),
"packedBytes": image.packed_file.size if image.packed_file else 0,
"fileFormat": image.file_format,
})
result["materials"][material.name] = {"types": sorted(types), "images": images}
print("CHECK_JSON:" + json.dumps(result))
`;
const output = execFileSync(BLENDER, ["-b", libraryPath, "--python-expr", blenderExpr], {
encoding: "utf8",
timeout: 120_000,
maxBuffer: 8 * 1024 * 1024,
});
const line = output.split(/\r?\n/).find((value) => value.startsWith("CHECK_JSON:"));
assert.ok(line, "Blender inspection produced no CHECK_JSON line");
const inspection = JSON.parse(line.slice("CHECK_JSON:".length));
assert.equal(Object.keys(inspection.materials).length, EXPECTED_MATERIALS);
for (const [name, info] of Object.entries(inspection.materials)) {
const types = new Set(info.types);
for (const expectedType of ["FRAME", "REROUTE", "DISPLACEMENT", "MAPPING", "VALUE", "BSDF_PRINCIPLED", "NORMAL_MAP"]) {
assert.ok(types.has(expectedType), `${name} lost node ${expectedType}`);
}
assert.equal(info.images.length, 4, `${name} must keep four image textures`);
for (const image of info.images) {
assert.ok(image.packed, `${image.name} is not packed`);
assert.ok(image.size[0] > 0 && image.size[1] > 0, `${image.name} has no pixels`);
assert.ok(image.size[0] <= MAX_TEXTURE_SIZE && image.size[1] <= MAX_TEXTURE_SIZE, `${image.name} exceeds ${MAX_TEXTURE_SIZE}px`);
assert.equal(image.fileFormat, "JPEG", `${image.name} is not JPEG`);
assert.ok(image.colorspace === "sRGB" || image.colorspace === "Non-Color", `${image.name} colorspace ${image.colorspace}`);
if (image.name.toLowerCase().includes("base_color")) assert.equal(image.colorspace, "sRGB");
else assert.equal(image.colorspace, "Non-Color");
}
}
const summary = {
status: "PASS",
libraryId: LIBRARY_ID,
librarySha256: librarySha,
libraryBytes: libraryBytes.byteLength,
materials: manifest.assets.length,
previews: manifest.assets.length,
previewBytes: manifest.assets.reduce((sum, asset) => sum + asset.preview.byteLength, 0),
textureBytes: report.textures.reduce((sum, item) => sum + item.packedBytes, 0),
};
console.log(JSON.stringify(summary, null, 2));