58 lines
3.9 KiB
JavaScript
58 lines
3.9 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { createHash } from "node:crypto";
|
|
import { readFile, writeFile } from "node:fs/promises";
|
|
import { NodeIO } from "@gltf-transform/core";
|
|
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
|
|
|
|
const [beforeFile, afterFile, reportFile] = process.argv.slice(2);
|
|
assert.ok(beforeFile && afterFile && reportFile, "Usage: verify-native-uv-source-layout.mjs <before-canonical.glb> <candidate-canonical.glb> <proof.json>");
|
|
const digest = bytes => createHash("sha256").update(bytes).digest("hex");
|
|
const io = new NodeIO().registerExtensions(ALL_EXTENSIONS);
|
|
function layout(document) {
|
|
const root = document.getRoot();
|
|
const graph = root.listNodes().map(node => ({ name: node.getName(), matrix: node.getMatrix(),
|
|
children: node.listChildren().map(child => child.getName()).sort() })).sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
|
|
const triangles = [];
|
|
for (const node of root.listNodes()) for (const primitive of node.getMesh()?.listPrimitives() ?? []) {
|
|
assert.equal(primitive.getMode(), 4, "Source UV audit requires static triangle primitives.");
|
|
assert.equal(primitive.listTargets().length, 0, "Morphed source geometry requires its own layout proof.");
|
|
// UV1 is the explicitly adapted presentation stream. Every other source
|
|
// attribute and ordered triangle corner is compared independent of grouping.
|
|
const semantics = primitive.listSemantics().filter(name => name !== "TEXCOORD_1").sort();
|
|
const position = primitive.getAttribute("POSITION");
|
|
assert.ok(position);
|
|
const indices = primitive.getIndices();
|
|
const count = indices?.getCount() ?? position.getCount();
|
|
for (let offset = 0; offset < count; offset += 3) {
|
|
const corners = [];
|
|
for (let corner = 0; corner < 3; corner++) {
|
|
const index = indices ? indices.getScalar(offset + corner) : offset + corner;
|
|
corners.push(semantics.map(name => primitive.getAttribute(name).getElement(index, [])));
|
|
}
|
|
triangles.push(JSON.stringify([node.getName(), semantics, corners]));
|
|
}
|
|
}
|
|
triangles.sort();
|
|
const textures = root.listTextures().map(texture => ({ name: texture.getName(), sha256: digest(texture.getImage()) }));
|
|
return { graph, triangles, textures };
|
|
}
|
|
const before = layout(await io.read(beforeFile));
|
|
const after = layout(await io.read(afterFile));
|
|
assert.deepEqual(after.graph, before.graph, "Source hierarchy and node transforms changed.");
|
|
assert.equal(after.triangles.length, before.triangles.length, "Source triangle count changed.");
|
|
for (let index = 0; index < before.triangles.length; index++) {
|
|
assert.equal(after.triangles[index], before.triangles[index], `Decoded source triangle ${index} changed.`);
|
|
}
|
|
for (const texture of before.textures) {
|
|
assert.ok(after.textures.some(candidate => candidate.name === texture.name && candidate.sha256 === texture.sha256),
|
|
`Original embedded source image ${texture.name} changed or was removed.`);
|
|
}
|
|
const report = { beforeFile, beforeSha256: digest(await readFile(beforeFile)), afterFile, afterSha256: digest(await readFile(afterFile)),
|
|
hierarchyAndTransformsPreserved: true, orderedSourceTriangleCornersPreserved: true,
|
|
allSourceAttributesExceptAdaptedUv1Preserved: true, originalSourceImagesPreserved: true,
|
|
triangleCount: before.triangles.length, originalSourceImageCount: before.textures.length,
|
|
addedSourceRegionImages: after.textures.filter(texture => !before.textures.some(original => original.name === texture.name)),
|
|
caveat: "Presentation UV1, primitive/material grouping and BIN/index layout can change; decoded geometry, normals, UV0 and all other source attributes remain identical." };
|
|
await writeFile(reportFile, `${JSON.stringify(report, null, 2)}\n`);
|
|
console.log(JSON.stringify({ triangles: report.triangleCount, originalSourceImages: report.originalSourceImageCount, addedSourceRegionImages: report.addedSourceRegionImages.length, reportFile }));
|