Files
FreeCADWasmDev/tools/meshpart-business-cases.mjs
wangdequan 9a7c4f0335 Save FreeCAD Web workspace sources and user validation evidence
IFC native binding remains incomplete; preserve accurate test results.
2026-09-15 22:01:58 +08:00

110 lines
6.1 KiB
JavaScript

import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { execFileSync } from "node:child_process";
import { fileURLToPath, pathToFileURL } from "node:url";
import { setTimeout as yieldToWorkers } from "node:timers/promises";
import { loadWasmProbe } from "./wasm-probe-runtime.mjs";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const capabilityId = "MeshPart:meshpart.createRecompute";
const cases = [];
const report = { schema: "freecad-web-business-cases/v1", results: [] };
const out = fs.mkdtempSync(path.join(os.tmpdir(), "freecad-meshpart-business-"));
let kernel;
let facade;
let queue;
let exitCode = 0;
let retainedDocument = false;
const documentName = "MeshPartBusinessInput";
const stage = (name) => process.stderr.write(`[MeshPart ${performance.now().toFixed(1)}ms] ${name}\n`);
function check(entry, name, actual, expected) {
const passed = typeof expected === "function" ? expected(actual) : actual === expected;
entry.assertions.push({ name, passed, actual, ...(typeof expected === "function" ? {} : { expected }) });
assert.ok(passed, `${name}${entry.actual?.error ? `: ${JSON.stringify(entry.actual.error)}` : ""}`);
}
async function execute(size, options = {}) {
const input = { ...facade.MESHPART_FIXTURE, size, documentName, ...options };
const entry = { input, assertions: [] };
cases.push(entry);
stage(`entered case ${cases.length}`);
await yieldToWorkers(0);
const response = await facade.dispatchMeshPart(queue, facade.createMeshPartRequest(`meshpart-${cases.length}`, input), kernel);
entry.actual = response;
if (response.kind === "result") retainedDocument = !response.result.resourcesReleased;
stage(`completed case ${cases.length}`);
return { entry, response };
}
function verifyMesh(entry, mesh, size, released) {
check(entry, "actual facet count", mesh.facetCount, n => Number.isInteger(n) && n >= 12);
check(entry, "actual point count", mesh.pointCount, n => Number.isInteger(n) && n >= 8);
check(entry, "two document objects after create/update", mesh.objectCount, 2);
check(entry, "document ownership", mesh.documentName, documentName);
check(entry, "document lifecycle", mesh.resourcesReleased, released);
check(entry, "finite mesh coordinates", mesh.vertices, vs => vs.every(v => v.length === 3 && v.every(Number.isFinite)));
for (let axis = 0; axis < 3; axis++) {
const coordinates = mesh.vertices.map(v => v[axis]);
check(entry, `mesh minimum axis ${axis}`, Math.min(...coordinates), n => Math.abs(n) < 1e-6);
check(entry, `mesh maximum axis ${axis}`, Math.max(...coordinates), n => Math.abs(n - size[axis]) < 1e-6);
}
const expectedArea = 2 * (size[0] * size[1] + size[1] * size[2] + size[0] * size[2]);
check(entry, "native mesh area", mesh.area, n => Math.abs(n - expectedArea) < 1e-5);
let area = 0;
for (const indices of mesh.facets) {
assert.ok(indices.length === 3 && indices.every(i => Number.isInteger(i) && i >= 0 && i < mesh.vertices.length), "valid triangle indices");
const [a, b, c] = indices.map(i => mesh.vertices[i]);
const u = b.map((v, i) => v - a[i]);
const v = c.map((value, i) => value - a[i]);
area += Math.hypot(u[1]*v[2]-u[2]*v[1], u[2]*v[0]-u[0]*v[2], u[0]*v[1]-u[1]*v[0]) / 2;
}
check(entry, "area independently measured from returned triangles", area, n => Math.abs(n - expectedArea) < 1e-5);
}
try {
const ids = JSON.parse(process.env.FREECAD_BUSINESS_IDS ?? JSON.stringify([capabilityId]));
assert.deepEqual(ids, [capabilityId], "Only the declared MeshPart business ID is supported");
stage("compile existing facade");
execFileSync(path.join(root, "web/node_modules/.bin/tsc"), ["--target", "ES2022", "--module", "ESNext", "--moduleResolution", "Bundler", "--strict", "--skipLibCheck", "--outDir", out, path.join(root, "web/src/meshpart-facade.ts")], { stdio: "pipe" });
facade = await import(pathToFileURL(path.join(out, "meshpart-facade.js")));
const { FacadeSerialQueue } = await import(pathToFileURL(path.join(out, "facade-envelope.js")));
queue = new FacadeSerialQueue();
stage("entered WASM load");
({ kernel } = await loadWasmProbe(path.join(root, "web/public/kernel"), "FreeCADWebMeshPartRuntimeProbe", { printErr: text => process.stderr.write(`${text}\n`) }));
stage("completed WASM load");
for (const size of [[2, 3, 4], [0.5, 7, 1.25]]) {
const { entry, response } = await execute(size, { keepDocument: true });
check(entry, "facade success", response.kind, "result");
verifyMesh(entry, response.result, size, false);
}
for (const size of [[0, 3, 4], [-1, 2, 3], [null, 2, 3]]) {
const { entry, response } = await execute(size, { keepDocument: true });
check(entry, "reject invalid dimensions", response.kind, "error");
check(entry, "native validation code", response.error?.code, "invalid_argument");
}
const size = [4, 1.5, 2];
const { entry, response } = await execute(size, { keepDocument: true, closeAfterRecompute: true });
check(entry, "recovery after invalid input", response.kind, "result");
verifyMesh(entry, response.result, size, true);
retainedDocument = false;
report.results.push({ capabilityId, status: "passed", cases });
} catch (error) {
exitCode = 1;
report.results.push({ capabilityId, status: "failed", cases, error: String(error.stack ?? error) });
} finally {
stage("entered cleanup");
if (retainedDocument && kernel) {
try {
const result = JSON.parse(kernel.ccall("fc_web_dispatch_json", "string", ["string"], [JSON.stringify({ cmd: "meshpart.createRecompute", args: { ...facade.MESHPART_FIXTURE, documentName, keepDocument: true, closeAfterRecompute: true } })]));
assert.ok(result.ok && result.resourcesReleased, "cleanup closes owned document");
} catch (error) {
exitCode = 1;
report.results[0].status = "failed";
report.results[0].cleanupError = String(error);
}
}
kernel?.PThread?.terminateAllThreads?.();
fs.rmSync(out, { recursive: true, force: true });
stage("completed cleanup");
process.stdout.write(`${JSON.stringify(report)}\n`, () => process.exit(exitCode));
}