/// /** * CAD Engine Web Worker * * Loads opencascade.js WASM via replicad-opencascadejs, then uses replicad * to execute user-written CAD code and return triangle mesh data. */ import type { ExportPayload, ImportPayload, MeshPayload, WorkerRequest, WorkerResponse, } from "./types"; // These will be dynamically imported after WASM init let replicadModule: typeof import("replicad") | null = null; let isInitialized = false; // Keep reference to last computed shape for export // biome-ignore lint/suspicious/noExplicitAny: replicad Shape type is complex let lastShape: any = null; function respond(response: WorkerResponse) { self.postMessage(response); } function respondWithTransfer( response: WorkerResponse, transfer: Transferable[], ) { self.postMessage(response, transfer); } async function initializeEngine(id: string): Promise { try { respond({ id, type: "progress", payload: { stage: "Loading OpenCascade WASM...", percent: 10 }, }); // Dynamic import to avoid bundling issues with WASM const [opencascadeModule, replicad] = await Promise.all([ import("replicad-opencascadejs/src/replicad_single.js"), import("replicad"), ]); respond({ id, type: "progress", payload: { stage: "Initializing kernel...", percent: 40 }, }); const opencascade = opencascadeModule.default; const OC = await opencascade(); respond({ id, type: "progress", payload: { stage: "Configuring replicad...", percent: 80 }, }); replicad.setOC(OC); replicadModule = replicad; isInitialized = true; respond({ id, type: "ready", payload: null }); } catch (error) { const message = error instanceof Error ? error.message : "Failed to initialize engine"; const stack = error instanceof Error ? error.stack : undefined; respond({ id, type: "error", payload: { message, stack } }); } } function executeCode(id: string, code: string): void { if (!isInitialized || !replicadModule) { respond({ id, type: "error", payload: { message: "Engine not initialized" }, }); return; } try { respond({ id, type: "progress", payload: { stage: "Executing code...", percent: 30 }, }); // Build a function with replicad imports available in scope const r = replicadModule; const scopeEntries = { draw: r.draw, drawCircle: r.drawCircle, drawEllipse: r.drawEllipse, drawPolysides: r.drawPolysides, drawRoundedRectangle: r.drawRoundedRectangle, drawSingleCircle: r.drawSingleCircle, drawSingleEllipse: r.drawSingleEllipse, makeBaseBox: r.makeBaseBox, makeCylinder: r.makeCylinder, makeSphere: r.makeSphere, makeSolid: r.makeSolid, assembleWire: r.assembleWire, compoundShapes: r.compoundShapes, loft: r.loft, cast: r.cast, }; const scopeKeys = Object.keys(scopeEntries); const scopeValues = Object.values(scopeEntries); // Strip import statements — user code shouldn't need them since we inject the scope const cleanCode = code .replace(/^\s*import\s+.*from\s+['"]replicad['"].*$/gm, "") .trim(); // Wrap code so the last expression or `return` gives us the shape const wrappedCode = cleanCode.includes("return") ? cleanCode : `${cleanCode}\n// Auto-return last variable if no explicit return\nreturn shape ?? result ?? model;`; const fn = new Function(...scopeKeys, wrappedCode); const shape = fn(...scopeValues); if (!shape || typeof shape.mesh !== "function") { respond({ id, type: "error", payload: { message: `Code must return a replicad shape (e.g. \`return shape;\`). Got: ${shape === undefined ? "undefined" : typeof shape}`, }, }); return; } // Keep reference for export lastShape = shape; respond({ id, type: "progress", payload: { stage: "Tessellating...", percent: 70 }, }); // Get face mesh const faceMesh = shape.mesh({ tolerance: 0.1 }); const faceVertices = new Float32Array(faceMesh.vertices); const faceNormals = new Float32Array(faceMesh.normals); const faceTriangles = new Uint32Array(faceMesh.triangles); // Get edge mesh const edgeMesh = shape.meshEdges({ tolerance: 0.1 }); const edgeVertices = new Float32Array(edgeMesh.lines); const payload: MeshPayload = { faces: { vertices: faceVertices, normals: faceNormals, triangles: faceTriangles, }, edges: { vertices: edgeVertices, }, }; respondWithTransfer({ id, type: "mesh", payload }, [ faceVertices.buffer, faceNormals.buffer, faceTriangles.buffer, edgeVertices.buffer, ]); } catch (error) { const message = error instanceof Error ? error.message : "Code execution failed"; const stack = error instanceof Error ? error.stack : undefined; respond({ id, type: "error", payload: { message, stack } }); } } function exportModel(id: string, payload: ExportPayload): void { if (!isInitialized) { respond({ id, type: "error", payload: { message: "Engine not initialized" }, }); return; } if (!lastShape) { respond({ id, type: "error", payload: { message: "No model to export. Run code first." }, }); return; } try { respond({ id, type: "progress", payload: { stage: `Exporting ${payload.format.toUpperCase()}...`, percent: 50, }, }); let blob: Blob; let mimeType: string; switch (payload.format) { case "step": { blob = lastShape.blobSTEP(); mimeType = "application/step"; break; } case "stl": { blob = lastShape.blobSTL({ binary: true }); mimeType = "application/sla"; break; } default: respond({ id, type: "error", payload: { message: `Unsupported format: ${payload.format}` }, }); return; } // Convert blob to ArrayBuffer for transfer blob.arrayBuffer().then((data) => { respondWithTransfer( { id, type: "result", payload: { data, format: payload.format, filename: payload.filename, mimeType, }, }, [data], ); }); } catch (error) { const message = error instanceof Error ? error.message : "Export failed"; const stack = error instanceof Error ? error.stack : undefined; respond({ id, type: "error", payload: { message, stack } }); } } async function importModel(id: string, payload: ImportPayload): Promise { if (!isInitialized || !replicadModule) { respond({ id, type: "error", payload: { message: "Engine not initialized" }, }); return; } try { respond({ id, type: "progress", payload: { stage: `Importing ${payload.format.toUpperCase()}...`, percent: 30, }, }); const r = replicadModule; if (payload.format === "step") { // Use replicad's importSTEP to load the STEP file const blob = new Blob([payload.data], { type: "application/step" }); const shape = await r.importSTEP(blob); if (!shape || typeof shape.mesh !== "function") { respond({ id, type: "error", payload: { message: "Failed to parse STEP file — no valid geometry found", }, }); return; } lastShape = shape; respond({ id, type: "progress", payload: { stage: "Tessellating imported model...", percent: 70 }, }); const faceMesh = shape.mesh({ tolerance: 0.1 }); const faceVertices = new Float32Array(faceMesh.vertices); const faceNormals = new Float32Array(faceMesh.normals); const faceTriangles = new Uint32Array(faceMesh.triangles); const edgeMesh = shape.meshEdges({ tolerance: 0.1 }); const edgeVertices = new Float32Array(edgeMesh.lines); const meshPayload: MeshPayload = { faces: { vertices: faceVertices, normals: faceNormals, triangles: faceTriangles, }, edges: { vertices: edgeVertices }, }; respondWithTransfer({ id, type: "mesh", payload: meshPayload }, [ faceVertices.buffer, faceNormals.buffer, faceTriangles.buffer, edgeVertices.buffer, ]); } else { respond({ id, type: "error", payload: { message: `Import not yet supported for format: ${payload.format}`, }, }); } } catch (error) { const message = error instanceof Error ? error.message : "Import failed"; const stack = error instanceof Error ? error.stack : undefined; respond({ id, type: "error", payload: { message, stack } }); } } self.onmessage = (event: MessageEvent) => { const { id, type, payload } = event.data; switch (type) { case "init": initializeEngine(id); break; case "execute": executeCode(id, (payload as { code: string }).code); break; case "export": exportModel(id, payload as ExportPayload); break; case "import": importModel(id, payload as ImportPayload); break; } };