- generate a desktop-Blender PBR asset library and seed catalog v2 into OPFS - add React asset browser, material preview sphere and Web PBR render panel - extend shader graph/compiler to the finite 14-node set and native properties - add image colour-space editing, scene-camera still rendering and asset repair - add focused E2E/unit coverage and implementation status notes Co-Authored-By: Claude Code <noreply@anthropic.com>
122 lines
4.0 KiB
JavaScript
122 lines
4.0 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Compose the catalog-v2 manifest for the generated PBR asset library.
|
|
*
|
|
* Inputs (produced by generate-pbr-library.py + export-asset-catalog-v1.py):
|
|
* <dir>/desktop-canonical.json
|
|
* <dir>/preview-manifest.json
|
|
* <dir>/pbr-library.blend
|
|
*
|
|
* Output:
|
|
* <dir>/manifest.json
|
|
*
|
|
* Usage: node tools/web/generate-pbr-library-manifest.mjs [--dir <path>]
|
|
*/
|
|
import assert from "node:assert/strict";
|
|
import crypto from "node:crypto";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
import ts from "../../web/node_modules/typescript/lib/typescript.js";
|
|
|
|
const REPO = path.resolve(import.meta.dirname, "../..");
|
|
const dirIndex = process.argv.indexOf("--dir");
|
|
const DIR = dirIndex > 0 && process.argv[dirIndex + 1]
|
|
? path.resolve(process.argv[dirIndex + 1])
|
|
: path.join(REPO, "web/app/public/assets/pbr-library");
|
|
|
|
const LIBRARY_ID = "library:pbr-essentials-v1";
|
|
const LIBRARY_NAME = "PBR Essentials";
|
|
const LIBRARY_SOURCE = "libraries/pbr-library.blend";
|
|
|
|
const readJson = (name) => JSON.parse(fs.readFileSync(path.join(DIR, name), "utf8"));
|
|
const sha256 = (data) => crypto.createHash("sha256").update(data).digest("hex");
|
|
|
|
async function loadProtocol() {
|
|
const sourcePath = path.join(REPO, "web/protocol/asset-catalog-v2.ts");
|
|
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
|
|
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
|
|
fileName: sourcePath,
|
|
reportDiagnostics: true,
|
|
});
|
|
assert.deepEqual(transpiled.diagnostics, []);
|
|
const temporary = path.join(DIR, ".asset-catalog-v2.generated.mjs");
|
|
fs.writeFileSync(temporary, transpiled.outputText);
|
|
try {
|
|
return await import(`${pathToFileURL(temporary).href}?v=${Date.now()}`);
|
|
} finally {
|
|
fs.rmSync(temporary, { force: true });
|
|
}
|
|
}
|
|
|
|
const desktop = readJson("desktop-canonical.json");
|
|
const previewManifest = readJson("preview-manifest.json");
|
|
const libraryBytes = fs.readFileSync(path.join(DIR, "pbr-library.blend"));
|
|
const sourceSha256 = sha256(libraryBytes);
|
|
|
|
const previews = new Map();
|
|
for (const entry of previewManifest.previews) {
|
|
const key = `${entry.idType}:${entry.name}`;
|
|
if (previews.has(key)) throw new Error(`duplicate preview ${key}`);
|
|
if (!fs.existsSync(path.join(DIR, entry.file))) throw new Error(`missing preview file ${entry.file}`);
|
|
const bytes = fs.readFileSync(path.join(DIR, entry.file));
|
|
if (sha256(bytes) !== entry.sha256) throw new Error(`preview hash mismatch ${entry.file}`);
|
|
previews.set(key, {
|
|
sha256: entry.sha256,
|
|
mimeType: "image/png",
|
|
width: entry.width,
|
|
height: entry.height,
|
|
byteLength: entry.byteLength,
|
|
});
|
|
}
|
|
|
|
const protocol = await loadProtocol();
|
|
const base = await protocol.createAssetCatalogManifestV2FromDesktop(desktop, 1);
|
|
|
|
const assets = [];
|
|
for (const asset of base.assets) {
|
|
const key = `${asset.idType}:${asset.name}`;
|
|
const preview = previews.get(key);
|
|
if (!preview) throw new Error(`asset ${key} has no preview`);
|
|
const assetLibraryIdentifier = LIBRARY_ID;
|
|
assets.push({
|
|
...asset,
|
|
assetLibraryIdentifier,
|
|
assetId: await protocol.createAssetCatalogV2StableId({
|
|
assetLibraryIdentifier,
|
|
relativeAssetIdentifier: asset.relativeAssetIdentifier,
|
|
}),
|
|
sourcePath: LIBRARY_SOURCE,
|
|
sourceSha256,
|
|
preview,
|
|
});
|
|
}
|
|
|
|
const candidate = {
|
|
schemaVersion: 2,
|
|
revision: 1,
|
|
catalogs: base.catalogs,
|
|
assets,
|
|
libraries: [{
|
|
libraryId: LIBRARY_ID,
|
|
name: LIBRARY_NAME,
|
|
sourcePath: LIBRARY_SOURCE,
|
|
sourceSha256,
|
|
dependencyIds: [],
|
|
readOnly: true,
|
|
}],
|
|
};
|
|
|
|
const manifest = await protocol.parseAssetCatalogManifestV2(candidate);
|
|
fs.writeFileSync(path.join(DIR, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
|
|
const totalBytes = assets.reduce((sum, asset) => sum + (asset.preview?.byteLength ?? 0), 0);
|
|
console.log(JSON.stringify({
|
|
manifest: "manifest.json",
|
|
catalogs: manifest.catalogs.length,
|
|
assets: manifest.assets.length,
|
|
libraries: manifest.libraries.length,
|
|
previewBytes: totalBytes,
|
|
sourceSha256,
|
|
}, null, 2));
|