#!/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));