88 lines
5.0 KiB
TypeScript
88 lines
5.0 KiB
TypeScript
import crypto from "node:crypto";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { spawnSync } from "node:child_process";
|
|
import { expect, test, type Page } from "@playwright/test";
|
|
|
|
const root = path.resolve(import.meta.dirname, "../../..");
|
|
const fixtureRoot = path.join(root, "tests/files/web/m12_obj_multi_v1");
|
|
const sourceObj = fs.readFileSync(path.join(fixtureRoot, "multi-object.obj"));
|
|
const sourceMtl = fs.readFileSync(path.join(fixtureRoot, "multi-object.mtl"));
|
|
const texture = fs.readFileSync(path.join(fixtureRoot, "m12_obj_texture.png"));
|
|
const reportPath = path.join(root, "tests/golden/M12-07C/web-roundtrip-report.json");
|
|
const sha256 = (bytes: Uint8Array | Buffer | string) => crypto.createHash("sha256").update(bytes).digest("hex");
|
|
|
|
async function runBrowserRoundtrip(page: Page, textureAssets: string[]) {
|
|
return page.evaluate(async (input: { obj: number[]; mtl: number[]; textureAssets: string[] }) => new Promise<any>((resolve, reject) => {
|
|
const worker = new Worker("/src/workers/obj-roundtrip-test.worker.ts", { type: "module" });
|
|
worker.onmessage = (event: MessageEvent<any>) => { worker.terminate(); resolve(event.data); };
|
|
worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); };
|
|
const obj = Uint8Array.from(input.obj).buffer;
|
|
const mtl = Uint8Array.from(input.mtl).buffer;
|
|
worker.postMessage({ obj, mtl, textureAssets: input.textureAssets }, [obj, mtl]);
|
|
}), { obj: Array.from(sourceObj), mtl: Array.from(sourceMtl), textureAssets });
|
|
}
|
|
|
|
test("M12-07C round-trips a Web OBJ through desktop Blender and reports texture loss", async ({ page }) => {
|
|
await page.goto("/");
|
|
const bound = await runBrowserRoundtrip(page, ["m12_obj_texture.png"]);
|
|
const missing = await runBrowserRoundtrip(page, []);
|
|
expect(bound.ok).toBe(true);
|
|
expect(bound.lossReport).toEqual({ schemaVersion: 1, operation: "OBJ_EXPORT_LOSS_REPORT", canRoundTrip: true, warningCount: 0, warnings: [] });
|
|
expect(missing.lossReport.warnings.map((warning: { code: string }) => warning.code)).toEqual(["OBJ_TEXTURE_ORIGIN_UNRESOLVED", "OBJ_TEXTURE_ORIGIN_UNRESOLVED"]);
|
|
|
|
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m12-07c-web-obj-"));
|
|
try {
|
|
const webObjPath = path.join(temporary, "web-output.obj");
|
|
const webMtlPath = path.join(temporary, "single-mesh.mtl");
|
|
fs.writeFileSync(webObjPath, bound.obj, "utf8");
|
|
fs.writeFileSync(webMtlPath, bound.mtl, "utf8");
|
|
fs.writeFileSync(path.join(temporary, "m12_obj_texture.png"), texture);
|
|
const desktopReportPath = path.join(temporary, "desktop-report.json");
|
|
const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender");
|
|
const result = spawnSync(blender, ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-obj-web-roundtrip.py"), "--", webObjPath, desktopReportPath], { cwd: root, encoding: "utf8", maxBuffer: 20 * 1024 * 1024 });
|
|
expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0);
|
|
const desktop = JSON.parse(fs.readFileSync(desktopReportPath, "utf8"));
|
|
const report = {
|
|
schemaVersion: 1,
|
|
task: "M12-07C",
|
|
operation: "WEB_OBJ_TO_DESKTOP_ROUNDTRIP",
|
|
source: { objSha256: sha256(sourceObj), mtlSha256: sha256(sourceMtl), textureSha256: sha256(texture) },
|
|
browser: {
|
|
imported: {
|
|
schemaVersion: bound.imported.schemaVersion,
|
|
objectCount: bound.imported.objects.length,
|
|
positionCount: bound.imported.positions.length,
|
|
texcoordCount: bound.imported.texcoords.length,
|
|
normalCount: bound.imported.normals.length,
|
|
faceCount: bound.imported.faces.length,
|
|
materialCount: bound.imported.materials.length,
|
|
},
|
|
outputObjSha256: sha256(Buffer.from(bound.obj)),
|
|
outputMtlSha256: sha256(Buffer.from(bound.mtl)),
|
|
lossReport: bound.lossReport,
|
|
missingTextureLoss: missing.lossReport,
|
|
},
|
|
desktop,
|
|
comparisons: {
|
|
objectCountExact: desktop.objectCount === bound.imported.objects.length,
|
|
triangleCountExact: desktop.objects.reduce((sum: number, object: { triangleCount: number }) => sum + object.triangleCount, 0) === bound.imported.faces.length,
|
|
uvLayerPresent: desktop.objects.every((object: { uvLayers: string[] }) => object.uvLayers.includes("UVMap")),
|
|
materialPresent: desktop.objects.every((object: { materials: string[] }) => object.materials.length === 1),
|
|
},
|
|
nextTask: "M12-07D",
|
|
};
|
|
if (process.env.UPDATE_OBJ_ROUNDTRIP === "1") {
|
|
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
|
|
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n");
|
|
}
|
|
const expected = JSON.parse(fs.readFileSync(reportPath, "utf8"));
|
|
expect(report).toEqual(expected);
|
|
expect(report.comparisons).toEqual({ objectCountExact: true, triangleCountExact: true, uvLayerPresent: true, materialPresent: true });
|
|
}
|
|
finally {
|
|
fs.rmSync(temporary, { recursive: true, force: true });
|
|
}
|
|
});
|