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 "); 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 }));