Files
FreeCADWasmDev/tools/openscad-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

122 lines
7.8 KiB
JavaScript

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 { loadWasmProbe } from "./wasm-probe-runtime.mjs";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const capabilityId = "OpenSCAD:openscad.importDxf";
const supportedIds = [capabilityId, "business-format:OpenSCAD:import:.dxf"];
const ids = JSON.parse(process.env.FREECAD_BUSINESS_IDS ?? JSON.stringify(supportedIds));
const cases = [];
const timings = { operations: [] };
const results = [];
let out, kernel, failure;
const files = [];
let sequence = 0;
// Separate closed contours exercise the native edge-to-face conversion.
function dxf(contours) {
const layers = [...new Set(contours.map(contour => contour.layer))];
const lines = ["0", "SECTION", "2", "TABLES", "0", "TABLE", "2", "LAYER", "70", String(layers.length)];
for (const layer of layers) lines.push("0", "LAYER", "2", layer, "70", "0", "62", "7", "6", "CONTINUOUS");
lines.push("0", "ENDTAB", "0", "ENDSEC", "0", "SECTION", "2", "ENTITIES");
for (const { layer, x, y = 0, width, height } of contours) {
const points = [[x, y], [x + width, y], [x + width, y + height], [x, y + height]];
for (let i = 0; i < points.length; i++) {
const a = points[i], b = points[(i + 1) % points.length];
lines.push("0", "LINE", "8", layer, "10", String(a[0]), "20", String(a[1]), "30", "0",
"11", String(b[0]), "21", String(b[1]), "31", "0");
}
}
return [...lines, "0", "ENDSEC", "0", "EOF", ""].join("\n");
}
try {
if (ids.some(id => supportedIds.includes(id))) {
out = fs.mkdtempSync(path.join(os.tmpdir(), "freecad-openscad-business-"));
execFileSync(path.join(root, "web/node_modules/.bin/tsc"), [
"--target", "ES2022", "--module", "ESNext", "--moduleResolution", "Bundler", "--strict", "--skipLibCheck",
"--outDir", out, path.join(root, "web/src/openscad-format-facade.ts"),
], { cwd: root, stdio: "pipe" });
const facade = await import(pathToFileURL(path.join(out, "openscad-format-facade.js")));
const { FacadeSerialQueue } = await import(pathToFileURL(path.join(out, "facade-envelope.js")));
const started = performance.now();
({ kernel } = await loadWasmProbe(path.resolve(root, process.env.FREECAD_OPENSCAD_BUSINESS_BUILD ?? "web/public/kernel"), "FreeCADWebOpenSCADRuntimeProbe"));
timings.loadSeconds = (performance.now() - started) / 1000;
const queue = new FacadeSerialQueue();
const call = async (input, checks, sourceData = source) => {
const began = performance.now();
const actual = await facade.dispatchOpenScadImportDxf(queue,
facade.createOpenScadImportDxfRequest(`openscad-business-${++sequence}`, input), kernel);
timings.operations.push({ path: input.path, documentId: input.documentId, seconds: (performance.now() - began) / 1000 });
const assertions = checks.map(([name, check]) => ({ name, passed: Boolean(check(actual)) }));
cases.push({ input: { ...input, source: sourceData }, actual, assertions });
if (assertions.some(item => !item.passed)) {
const failedAssertions = assertions.filter(item => !item.passed).map(item => item.name);
throw new Error(`DXF business assertion failed: ${JSON.stringify({ actual, failedAssertions, input })}`);
}
return actual.result;
};
const contours = [
{ layer: "Single", x: 0, width: 5, height: 3 },
{ layer: "Double", x: 20, width: 8, height: 4 },
{ layer: "Double", x: 40, width: 6, height: 7 },
];
const source = dxf(contours);
const sourcePath = "/tmp/openscad-business-contours.dxf";
kernel.FS_createDataFile("/tmp", "openscad-business-contours.dxf", new Uint8Array(Buffer.from(source, "utf8")), true, true);
files.push(sourcePath);
const payload = (documentId, layer) => ({ schema: facade.OPENSCAD_IMPORT_DXF_SCHEMA,
module: "OpenSCAD", documentId, path: sourcePath, layer });
const success = (count, areas = count === 1 ? [15] : [32, 42]) => [
["native import creates the expected number of faces", reply => reply.kind === "result" && reply.result.faceCount === count && reply.result.objectCount === count],
["native objects have distinct names", reply => reply.kind === "result" && new Set(reply.result.objectNames).size === count],
["OCCT areas match the actual input contours", reply => reply.kind === "result" && reply.result.geometryBackend === "OCCT"
&& Array.isArray(reply.result.faceAreas) && reply.result.faceAreas.length === areas.length
&& [...reply.result.faceAreas].sort((a, b) => a - b).every((area, index) => Math.abs(area - [...areas].sort((a, b) => a - b)[index]) < 1e-7)],
];
const first = await call(payload("OpenSCADBusinessA", "Single"), [...success(1),
["requested document is created", reply => reply.result?.createdDocument === true && reply.result.documentId === "OpenSCADBusinessA" && reply.result.documentObjectCount === 1]]);
await call(payload("OpenSCADBusinessA", "Double"), [...success(2),
["second layer appends distinct faces in the same document", reply => reply.result?.createdDocument === false
&& reply.result.documentHandle === first.documentHandle && reply.result.documentObjectCount === 3 && reply.result.objectNames.every(name => !first.objectNames.includes(name))]]);
await call(payload("OpenSCADBusinessB", "Double"), [...success(2),
["another requested document is isolated", reply => reply.result?.createdDocument === true && reply.result.documentHandle !== first.documentHandle]]);
for (const input of [
{ ...payload("OpenSCADBusinessA", "Single"), path: "/tmp/openscad-business-missing.dxf" },
payload("OpenSCADBusinessRollback", "AbsentLayer"),
]) {
await call(input, [["import rejects unavailable source or layer", reply => reply.kind === "error"]]);
}
await call(payload("OpenSCADBusinessRollback", "Single"), [...success(1),
["failed new-document import is rolled back before recovery", reply => reply.result?.createdDocument === true]]);
await call(payload("OpenSCADBusinessA", "Single"), [...success(1),
["existing document survives failure and accepts another import", reply => reply.result?.createdDocument === false
&& reply.result.documentHandle === first.documentHandle && reply.result.objectNames.every(name => !first.objectNames.includes(name))]]);
await call(payload("OpenSCADAllLayers"), success(3, [15, 32, 42]));
const holeSource = dxf([{ layer: "Hole", x: 0, width: 10, height: 10 }, { layer: "Hole", x: 2, y: 2, width: 3, height: 3 }]);
const holePath = "/tmp/openscad-business-hole.dxf";
kernel.FS_createDataFile("/tmp", "openscad-business-hole.dxf", new Uint8Array(Buffer.from(holeSource)), true, true);
files.push(holePath);
await call({ ...payload("OpenSCADHole", "Hole"), path: holePath }, success(1, [91]), holeSource);
}
} catch (error) {
failure = String(error.stack ?? error);
if (error.stderr?.length) fs.writeSync(2, error.stderr);
} finally {
const began = performance.now();
for (const file of files) {
try { kernel.FS_unlink(file); } catch (error) { failure ??= String(error); }
}
if (out) fs.rmSync(out, { recursive: true, force: true });
timings.cleanupSeconds = (performance.now() - began) / 1000;
}
for (const id of ids) results.push(supportedIds.includes(id)
? { capabilityId: id, status: failure || !cases.length ? "failed" : "passed", cases, ...(failure ? { error: failure } : {}) }
: { capabilityId: id, status: "not_implemented", cases: [] });
await new Promise((resolve, reject) => process.stdout.write(JSON.stringify({ schema: "freecad-web-business-cases/v1", timings, results }) + "\n",
error => error ? reject(error) : resolve()));
process.exit(results.some(result => result.status !== "passed") ? 1 : 0);