#!/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):
*
/desktop-canonical.json
* /preview-manifest.json
* /pbr-library.blend
*
* Output:
* /manifest.json
*
* Usage: node tools/web/generate-pbr-library-manifest.mjs [--dir ]
* [--library-id ] [--library-name ] [--source ] [--blend ]
*/
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 flag = (name, fallback) => {
const index = process.argv.indexOf(`--${name}`);
return index > 0 && process.argv[index + 1] ? process.argv[index + 1] : fallback;
};
const DIR = path.resolve(flag("dir", path.join(REPO, "web/app/public/assets/pbr-library")));
const LIBRARY_ID = flag("library-id", "library:pbr-essentials-v1");
const LIBRARY_NAME = flag("library-name", "PBR Essentials");
const LIBRARY_SOURCE = flag("source", "libraries/pbr-library.blend");
const BLEND_FILE = flag("blend", "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, BLEND_FILE));
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));