import assert from "node:assert/strict"; import path from "node:path"; import fs from "node:fs"; import os from "node:os"; import { setTimeout as yieldToWorkers } from "node:timers/promises"; import { execFileSync } from "node:child_process"; import { fileURLToPath, pathToFileURL } from "node:url"; import { loadWasmProbe } from "./wasm-probe-runtime.mjs"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const capabilityId = "Mesh:mesh.createRecompute"; const persistenceId = "Mesh:mesh.saveReopen"; const ioId = "Mesh:mesh.importExport"; const threeMfIds = ["business-format:Mesh:export:.3mf", "business-format:Mesh:import:.3mf"]; const amfExportId = "business-format:Mesh:export:.amf"; const astImportId = "business-format:Mesh:import:.ast"; const objImportId = "business-format:Mesh:import:.obj"; const offImportId = "business-format:Mesh:import:.off"; const plyIds = ["business-format:Mesh:export:.ply", "business-format:Mesh:import:.ply"]; const ids = JSON.parse(process.env.FREECAD_BUSINESS_IDS ?? JSON.stringify([capabilityId])); const results = []; const out = fs.mkdtempSync(path.join(os.tmpdir(), "freecad-mesh-business-")); let exitCode = 0; const report = { schema: "freecad-web-business-cases/v1", results }; try { assert.ok(Array.isArray(ids) && ids.length > 0, "FREECAD_BUSINESS_IDS must be a nonempty array"); assert.equal(new Set(ids).size, ids.length, "FREECAD_BUSINESS_IDS must not contain duplicates"); for (const id of ids) assert.ok([capabilityId, persistenceId, ioId, ...threeMfIds, amfExportId, astImportId, objImportId, offImportId, ...plyIds].includes(id), `Unsupported Mesh business ID: ${id}`); execFileSync(path.join(root, "web/node_modules/.bin/tsc"), ["--target", "ES2022", "--module", "ESNext", "--moduleResolution", "Bundler", "--strict", "--skipLibCheck", "--outDir", out, path.join(root, "web/src/mesh-facade.ts"), path.join(root, "web/src/mesh-persistence-facade.ts"), path.join(root, "web/src/mesh-io-facade.ts")], { stdio: "pipe" }); const facade = await import(pathToFileURL(path.join(out, "mesh-facade.js"))); const { FacadeSerialQueue } = await import(pathToFileURL(path.join(out, "facade-envelope.js"))); const persistence = await import(pathToFileURL(path.join(out, "mesh-persistence-facade.js"))); const io = await import(pathToFileURL(path.join(out, "mesh-io-facade.js"))); const { kernel } = await loadWasmProbe(process.env.FREECAD_MESH_BUSINESS_BUILD ?? path.join(root, "freecad-source/build-web/configure/bin"), "FreeCADWebMeshRuntimeProbe"); const queue = new FacadeSerialQueue(); let threeMfCases; let plyCases; for (const id of ids) { const cases = []; try { if (id === astImportId || id === objImportId || id === offImportId || plyIds.includes(id)) { const format = plyIds.includes(id) ? "PLY" : id === offImportId ? "OFF" : id === objImportId ? "OBJ" : "AST"; if (format === "PLY" && plyCases) { cases.push(...plyCases); } else { for (const [index, { vertices, area }] of [ { vertices: [], area: null }, { vertices: [[0,0,0],[1,1,1],[2,2,2]], area: null }, { vertices: [[0,0,0],[2,0,0],[0,3,0]], area: 3 }, { vertices: [[4,5,6],[4,5,10],[4,7,6]], area: 4 }, ].entries()) { await yieldToWorkers(0); const input = { ...io.MESH_IO_FIXTURE, format, path: `/tmp/mesh-roundtrip-${index}.${format.toLowerCase()}`, vertices }; const entry = { input, assertions: [] }; cases.push(entry); try { const actual = JSON.parse(kernel.ccall("fc_web_dispatch_json", "string", ["string"], [JSON.stringify({ cmd: io.MESH_IO_COMMAND, args: input })])); entry.actual = actual; if (area === null) { assert.equal(actual.ok, false); assert.equal(actual.code, "invalid_argument"); assert.match(actual.error, /vert|geometry|triangle|degenerate/i); } else { assert.equal(actual.ok, true, JSON.stringify(actual)); assert.equal(actual.format, format); assert.equal(actual.path, input.path); assert.equal(typeof actual.fileBase64, "string"); const bytes = Buffer.from(actual.fileBase64, "base64"); assert.equal(actual.exportBytes, bytes.length); let artifactVertices; if (format === "PLY") { const lines = bytes.toString("utf8").trim().split(/\r?\n/).map(line => line.trim()); const end = lines.indexOf("end_header"); assert.ok(end > 0); assert.deepEqual(lines.slice(0, end).filter(line => !line.startsWith("comment ")), [ "ply", "format ascii 1.0", "element vertex 3", "property float32 x", "property float32 y", "property float32 z", "element face 1", "property list uchar int vertex_index", ]); const rows = lines.slice(end + 1).map(line => line.split(/\s+/).map(Number)); assert.equal(rows.length, 4); const points = rows.slice(0, 3); assert.ok(points.every(point => point.length === 3 && point.every(Number.isFinite))); assert.equal(rows[3].length, 4); assert.equal(rows[3][0], 3); const indices = rows[3].slice(1); assert.equal(new Set(indices).size, 3); assert.ok(indices.every(index => Number.isInteger(index) && index >= 0 && index < points.length)); artifactVertices = indices.map(index => points[index]); entry.artifact = { vertices: points, indices }; } else if (format === "OFF") { const tokens = bytes.toString("utf8").replace(/#[^\r\n]*/g, "").trim().split(/\s+/); assert.equal(tokens.shift(), "OFF"); const numbers = tokens.map(Number); assert.ok(numbers.every(Number.isFinite)); assert.deepEqual(numbers.slice(0, 3), [3, 1, 0]); assert.equal(numbers.length, 16); const points = [numbers.slice(3, 6), numbers.slice(6, 9), numbers.slice(9, 12)]; assert.equal(numbers[12], 3); const indices = numbers.slice(13); assert.equal(new Set(indices).size, 3); assert.ok(indices.every(index => Number.isInteger(index) && index >= 0 && index < points.length)); artifactVertices = indices.map(index => points[index]); entry.artifact = { vertices: points, indices }; } else if (format === "OBJ") { const lines = bytes.toString("utf8").split(/\r?\n/).map(line => line.trim().split(/\s+/)); const points = lines.filter(tokens => tokens[0] === "v").map(tokens => tokens.slice(1).map(Number)); const faces = lines.filter(tokens => tokens[0] === "f"); assert.equal(points.length, 3); assert.ok(points.every(point => point.length === 3 && point.every(Number.isFinite))); assert.equal(faces.length, 1); const indices = faces[0].slice(1).map(token => Number(token.split("/")[0])); assert.equal(indices.length, 3); assert.equal(new Set(indices).size, 3); assert.ok(indices.every(index => Number.isInteger(index) && index >= 1 && index <= points.length)); artifactVertices = indices.map(index => points[index - 1]); entry.artifact = { vertices: points, indices }; } else { assert.match(bytes.toString("utf8"), /^\s*solid\b/); assert.match(bytes.toString("utf8"), /endsolid\b/); artifactVertices = [...bytes.toString("utf8").matchAll(/\bvertex\s+(\S+)\s+(\S+)\s+(\S+)/g)].map(match => match.slice(1).map(Number)); } const preservesWinding = points => points.length === 3 && [0,1,2].some(offset => points.every((point, i) => JSON.stringify(point) === JSON.stringify(vertices[(i + offset) % 3]))); assert.ok(preservesWinding(artifactVertices), "actual ASCII artifact preserves input geometry"); assert.ok(preservesWinding(actual.vertices), `native ${format} import preserves coordinates and winding`); assert.equal(actual.importedFacetCount, 1); assert.equal(actual.importedPointCount, 3); assert.equal(actual.area, area); assert.equal(actual.resourcesReleased, true); const facadeResult = await io.handleMeshIo(input, new AbortController().signal, kernel); assert.equal(facadeResult.area, area); assert.ok(preservesWinding(facadeResult.vertices)); } entry.assertions.push({ name: area === null ? `${format} rejects invalid geometry` : `${format} file and native imported geometry preserve varying input through facade`, passed: true }); } catch (error) { entry.assertions.push({ name: `${format} import business contract`, passed: false, error: String(error) }); } finally { try { kernel.FS?.unlink(input.path); } catch {} } } if (format === "PLY") plyCases = cases.slice(); } assert.ok(cases.every(entry => entry.assertions.every(check => check.passed)), `${format} import contract failed; see actual responses`); } else if (id === amfExportId) { for (const [index, vertices] of [[], [[0,0,0],[1,1,1],[2,2,2]], [[0,0,0],[2,0,0],[0,3,0]], [[4,5,6],[4,5,10],[4,7,6]]].entries()) { await yieldToWorkers(0); const input = { ...io.MESH_IO_FIXTURE, format: "AMF", path: `/tmp/mesh-export-${index}.amf`, vertices }; const entry = { input, assertions: [] }; cases.push(entry); try { const actual = JSON.parse(kernel.ccall("fc_web_dispatch_json", "string", ["string"], [JSON.stringify({ cmd: io.MESH_IO_COMMAND, args: input })])); entry.actual = actual; if (index < 2) { assert.equal(actual.ok, false); assert.equal(actual.code, "invalid_argument"); assert.match(actual.error, /vert|geometry|triangle|degenerate/i); } else { assert.equal(actual.ok, true, JSON.stringify(actual)); assert.equal(actual.format, "AMF"); assert.equal(actual.path, input.path); assert.equal(typeof actual.fileBase64, "string"); const bytes = Buffer.from(actual.fileBase64, "base64"); assert.ok(bytes.length > 0); assert.equal(actual.exportBytes, bytes.length); // Parse the actual exported artifact, so an STL payload or echoed request cannot pass. const artifact = JSON.parse(execFileSync("python3", ["-c", `import sys, json, io, zipfile import xml.etree.ElementTree as ET data = sys.stdin.buffer.read() if zipfile.is_zipfile(io.BytesIO(data)): with zipfile.ZipFile(io.BytesIO(data)) as archive: names = archive.namelist() assert len(names) == 1 data = archive.read(names[0]) root = ET.fromstring(data) assert root.tag == 'amf' assert root.get('unit', 'millimeter') == 'millimeter' meshes = root.findall('./object/mesh') assert len(meshes) == 1 mesh = meshes[0] vertices = [[float(v.findtext('coordinates/' + axis)) for axis in ('x', 'y', 'z')] for v in mesh.findall('./vertices/vertex')] triangles = [[int(t.findtext(axis)) for axis in ('v1', 'v2', 'v3')] for t in mesh.findall('./volume/triangle')] print(json.dumps(dict(vertices=vertices, triangles=triangles))) `], { input: bytes, encoding: "utf8" })); entry.artifact = artifact; assert.equal(artifact.vertices.length, 3); assert.equal(artifact.triangles.length, 1); const triangle = artifact.triangles[0]; assert.equal(new Set(triangle).size, 3); assert.ok(triangle.every(i => Number.isInteger(i) && i >= 0 && i < 3)); const points = triangle.map(i => artifact.vertices[i]); assert.ok([0, 1, 2].some(offset => points.every((point, i) => JSON.stringify(point) === JSON.stringify(vertices[(i + offset) % 3]))), "AMF artifact preserves input coordinates and winding"); assert.equal(actual.resourcesReleased, true); } entry.assertions.push({ name: index < 2 ? "AMF rejects invalid geometry" : "AMF export artifact preserves varying coordinates and winding after invalid requests", passed: true }); } catch (error) { entry.assertions.push({ name: "AMF native export business contract", passed: false, error: String(error) }); } finally { try { kernel.FS?.stat(input.path); kernel.FS?.unlink(input.path); } catch {} } } assert.ok(cases.every(entry => entry.assertions.every(check => check.passed)), "AMF native export business contract failed; see actual responses"); } else if (threeMfIds.includes(id)) { // Both format IDs exercise the same native file roundtrip in this runtime. if (!threeMfCases) { threeMfCases = []; const inputs = [ { vertices: [], area: null }, { vertices: [[0,0,0],[1,1,1],[2,2,2]], area: null }, { vertices: [[0,0,0],[2,0,0],[0,3,0]], area: 3 }, { vertices: [[4,5,6],[4,7,6],[4,5,10]], area: 4 }, ]; for (const [index, { vertices, area }] of inputs.entries()) { await yieldToWorkers(0); const input = { ...io.MESH_IO_FIXTURE, format: "3MF", path: `/tmp/mesh-roundtrip-${index}.3mf`, vertices }; const entry = { input, assertions: [] }; threeMfCases.push(entry); try { const actual = JSON.parse(kernel.ccall("fc_web_dispatch_json", "string", ["string"], [JSON.stringify({ cmd: io.MESH_IO_COMMAND, args: input })])); entry.actual = actual; if (area === null) { // A format rejection cannot stand in for geometry validation. assert.equal(actual.ok, false); assert.equal(actual.code, "invalid_argument"); assert.match(actual.error, /vert|geometry|triangle|degenerate/i); } else { assert.equal(actual.ok, true, JSON.stringify(actual)); assert.equal(actual.format, "3MF"); assert.equal(actual.path, input.path); assert.equal(typeof actual.fileBase64, "string"); const bytes = Buffer.from(actual.fileBase64, "base64"); assert.equal(actual.exportBytes, bytes.length); const artifact = JSON.parse(execFileSync("python3", ["-c", `import sys, json, io, zipfile import xml.etree.ElementTree as ET with zipfile.ZipFile(io.BytesIO(sys.stdin.buffer.read())) as archive: root = ET.fromstring(archive.read('3D/3dmodel.model')) ns = {'m': 'http://schemas.microsoft.com/3dmanufacturing/core/2015/02'} mesh = root.find('./m:resources/m:object/m:mesh', ns) assert mesh is not None vertices = [[float(v.get(axis)) for axis in ('x', 'y', 'z')] for v in mesh.findall('./m:vertices/m:vertex', ns)] triangles = [[int(t.get(axis)) for axis in ('v1', 'v2', 'v3')] for t in mesh.findall('./m:triangles/m:triangle', ns)] print(json.dumps(dict(vertices=vertices, triangles=triangles))) `], { input: bytes, encoding: "utf8" })); entry.artifact = artifact; assert.equal(artifact.triangles.length, 1); const points = artifact.triangles[0].map(i => artifact.vertices[i]); assert.ok([0, 1, 2].some(offset => points.every((point, i) => JSON.stringify(point) === JSON.stringify(vertices[(i + offset) % 3]))), "3MF artifact preserves coordinates and winding"); assert.ok(actual.exportBytes > 0); assert.equal(actual.importedFacetCount, 1); assert.equal(actual.importedPointCount, 3); assert.equal(actual.area, area); assert.ok([0, 1, 2].some(offset => actual.vertices.every((point, i) => JSON.stringify(point) === JSON.stringify(vertices[(i + offset) % 3])))); assert.equal(actual.vertices.length, 3); assert.equal(actual.resourcesReleased, true); } entry.assertions.push({ name: area === null ? "3MF rejects invalid geometry" : "3MF file roundtrip preserves input coordinates, winding and area", passed: true }); } catch (error) { entry.assertions.push({ name: "3MF native file business contract", passed: false, error: String(error) }); } } } cases.push(...threeMfCases); assert.ok(cases.every(entry => entry.assertions.every(check => check.passed)), "3MF native file business contract failed; see actual responses"); } else if (id === persistenceId) { const response = await persistence.dispatchMeshPersistence(queue, persistence.createMeshPersistenceRequest("mesh-save-reopen"), kernel); assert.equal(response.kind, "result", JSON.stringify(response)); const actual = response.result; assert.equal(actual.format, "FCStd"); assert.equal(actual.path, "/tmp/MeshAction05.FCStd"); assert.ok(actual.size > 0); assert.equal(actual.objectType, "Mesh::Feature"); assert.equal(actual.objectCount, 1); assert.equal(actual.facetCount, 1); assert.equal(actual.pointCount, 3); assert.equal(actual.roundTrip, true); cases.push({ input: persistence.MESH_PERSISTENCE_FIXTURE, actual, assertions: [{ name: "Mesh FCStd save and reopen preserves one triangle", passed: true }] }); } else if (id === ioId) { let sequence = 0; for (const vertices of [[], [[0,0,0],[1,1,1],[2,2,2]]]) { const input = { ...io.MESH_IO_FIXTURE, vertices }; const actual = JSON.parse(kernel.ccall("fc_web_dispatch_json", "string", ["string"], [JSON.stringify({ cmd: io.MESH_IO_COMMAND, args: input })])); assert.equal(actual.ok, false); cases.push({ input, actual, assertions: [{ name: "native rejects invalid geometry", passed: true }] }); } for (const [vertices, area] of [[[[0,0,0],[1,0,0],[0,1,0]], 0.5], [[[0,0,0],[2,0,0],[0,3,0]], 3], [[[4,5,6],[4,7,6],[4,5,10]], 4]]) { // Let completed pthread workers return to the pool before another synchronous import. await yieldToWorkers(0); const input = { ...io.MESH_IO_FIXTURE, path: `/tmp/mesh-roundtrip-${++sequence}.stl`, vertices }; const response = await io.dispatchMeshIo(queue, io.createMeshIoRequest(`mesh-import-export-${sequence}`, input), kernel); assert.equal(response.kind, "result", JSON.stringify(response)); const actual = response.result; assert.equal(actual.format, "STL"); assert.equal(actual.path, input.path); assert.ok([0, 1, 2].some(offset => actual.vertices.every((point, index) => JSON.stringify(point) === JSON.stringify(vertices[(index + offset) % 3]))), "STL preserves coordinates and winding up to cyclic vertex rotation"); assert.equal(actual.area, area); assert.ok(actual.exportBytes > 0); assert.equal(actual.importedFacetCount, 1); assert.equal(actual.importedPointCount, 3); assert.equal(actual.resourcesReleased, true); cases.push({ input, actual, assertions: [{ name: "STL roundtrip preserves varying native coordinates and area after invalid requests", passed: true }] }); } } else { let sequence = 0; const call = vertices => facade.dispatchMesh(queue, facade.createMeshRequest(`mesh-${++sequence}`, { ...facade.MESH_FIXTURE, vertices }), kernel); const native = vertices => JSON.parse(kernel.ccall("fc_web_dispatch_json", "string", ["string"], [JSON.stringify({ cmd: facade.MESH_COMMAND, args: { vertices } })])); for (const vertices of [[], [[0,0,0],[1,1,1],[2,2,2]], [[0,0,0],[0,0,0],[1,0,0]], [[null,0,0],[1,0,0],[0,1,0]], [[1e20,0,0],[1,0,0],[0,1,0]]]) { const actual = native(vertices); assert.equal(actual.ok, false); assert.equal(actual.code, "invalid_argument"); const response = await call(vertices); assert.notEqual(response.kind, "result"); cases.push({ input: { vertices }, actual, assertions: [{ name: "invalid geometry rejected by native and facade", passed: true }] }); } for (const [vertices, area] of [[[[0,0,0],[1,0,0],[0,1,0]], 0.5], [[[0,0,0],[2,0,0],[0,3,0]], 3], [[[4,5,6],[4,7,6],[4,5,10]], 4]]) { const response = await call(vertices); assert.equal(response.kind, "result", JSON.stringify(response)); const actual = response.result; assert.equal(actual.objectType, "Mesh::Feature"); assert.equal(actual.facetCount, 1); assert.equal(actual.pointCount, 3); assert.equal(actual.resourcesReleased, true); assert.deepEqual(actual.vertices, vertices); assert.equal(actual.area, area); cases.push({ input: { vertices }, actual, assertions: [{ name: "recomputed native facet coordinates and area match varying inputs after invalid requests", passed: true }, { name: "native resources are released", passed: true }] }); } } results.push({ capabilityId: id, status: "passed", cases }); } catch (error) { results.push({ capabilityId: id, status: "failed", cases, error: String(error), diagnostics: error.stdout?.toString() }); exitCode = 1; } } } catch (error) { for (const id of Array.isArray(ids) ? ids : [capabilityId]) { results.push({ capabilityId: id, status: "failed", cases: [], error: String(error), diagnostics: error.stdout?.toString() }); } exitCode = 1; } finally { fs.rmSync(out, { recursive: true, force: true }); } // Flush the complete evidence before exiting the one-shot pthread runtime. process.stdout.write(JSON.stringify(report) + "\n", () => process.exit(exitCode));