///
// STEP triangulation worker (#2976).
//
// OpenCascade compiled to WASM does the triangulation. It runs in a worker
// for two reasons: parsing a real assembly takes seconds and would freeze
// the UI thread, and — decisive — the emscripten/embind glue generates its
// invoker functions with `new Function(...)`, which the app's nonce-strict
// CSP rightly blocks on the document. Per CSP3 a dedicated worker is
// governed by the policy delivered with this script's own response, so the
// backend relaxes 'unsafe-eval' for exactly this asset and nothing else
// (see security_headers_middleware in backend/app/main.py).
import occtimportjs from 'occt-import-js';
import type { OcctInstance } from 'occt-import-js';
import wasmUrl from 'occt-import-js/dist/occt-import-js.wasm?url';
export interface StepWorkerRequest {
/** Correlation id echoed on the response — the worker is shared. */
id: number;
buffer: ArrayBuffer;
}
export interface StepWorkerMesh {
positions: Float32Array;
normals: Float32Array | null;
indices: Uint32Array | null;
color: [number, number, number] | null;
}
export type StepWorkerResponse =
| { id: number; ok: true; meshes: StepWorkerMesh[] }
| { id: number; ok: false; reason: 'no-meshes' | 'error' };
// The ~7 MB wasm instance is expensive to initialise — created once and kept
// for the worker's lifetime. A failed init is not cached so a later preview
// retries from scratch.
let instancePromise: Promise | null = null;
function getInstance(): Promise {
if (!instancePromise) {
instancePromise = occtimportjs({ locateFile: () => wasmUrl }).catch((err: unknown) => {
instancePromise = null;
throw err;
});
}
return instancePromise;
}
self.onmessage = async (event: MessageEvent) => {
const { id } = event.data;
try {
const occt = await getInstance();
const result = occt.ReadStepFile(new Uint8Array(event.data.buffer), null);
if (!result.success || result.meshes.length === 0) {
self.postMessage({ id, ok: false, reason: 'no-meshes' } satisfies StepWorkerResponse);
return;
}
const meshes = result.meshes.map(
(mesh): StepWorkerMesh => ({
positions: new Float32Array(mesh.attributes.position.array),
normals: mesh.attributes.normal ? new Float32Array(mesh.attributes.normal.array) : null,
indices: mesh.index ? new Uint32Array(mesh.index.array) : null,
color: mesh.color ?? null,
})
);
// Transfer the typed-array buffers instead of structured-cloning them —
// large assemblies are tens of MB of vertex data.
const transfers = meshes.flatMap((mesh) => {
const buffers = [mesh.positions.buffer];
if (mesh.normals) buffers.push(mesh.normals.buffer);
if (mesh.indices) buffers.push(mesh.indices.buffer);
return buffers;
});
self.postMessage({ id, ok: true, meshes } satisfies StepWorkerResponse, transfers);
} catch {
self.postMessage({ id, ok: false, reason: 'error' } satisfies StepWorkerResponse);
}
};