stepPreview.worker.ts 3.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
  1. /// <reference lib="webworker" />
  2. // STEP triangulation worker (#2976).
  3. //
  4. // OpenCascade compiled to WASM does the triangulation. It runs in a worker
  5. // for two reasons: parsing a real assembly takes seconds and would freeze
  6. // the UI thread, and — decisive — the emscripten/embind glue generates its
  7. // invoker functions with `new Function(...)`, which the app's nonce-strict
  8. // CSP rightly blocks on the document. Per CSP3 a dedicated worker is
  9. // governed by the policy delivered with this script's own response, so the
  10. // backend relaxes 'unsafe-eval' for exactly this asset and nothing else
  11. // (see security_headers_middleware in backend/app/main.py).
  12. import occtimportjs from 'occt-import-js';
  13. import type { OcctInstance } from 'occt-import-js';
  14. import wasmUrl from 'occt-import-js/dist/occt-import-js.wasm?url';
  15. export interface StepWorkerRequest {
  16. /** Correlation id echoed on the response — the worker is shared. */
  17. id: number;
  18. buffer: ArrayBuffer;
  19. }
  20. export interface StepWorkerMesh {
  21. positions: Float32Array;
  22. normals: Float32Array | null;
  23. indices: Uint32Array | null;
  24. color: [number, number, number] | null;
  25. }
  26. export type StepWorkerResponse =
  27. | { id: number; ok: true; meshes: StepWorkerMesh[] }
  28. | { id: number; ok: false; reason: 'no-meshes' | 'error' };
  29. // The ~7 MB wasm instance is expensive to initialise — created once and kept
  30. // for the worker's lifetime. A failed init is not cached so a later preview
  31. // retries from scratch.
  32. let instancePromise: Promise<OcctInstance> | null = null;
  33. function getInstance(): Promise<OcctInstance> {
  34. if (!instancePromise) {
  35. instancePromise = occtimportjs({ locateFile: () => wasmUrl }).catch((err: unknown) => {
  36. instancePromise = null;
  37. throw err;
  38. });
  39. }
  40. return instancePromise;
  41. }
  42. self.onmessage = async (event: MessageEvent<StepWorkerRequest>) => {
  43. const { id } = event.data;
  44. try {
  45. const occt = await getInstance();
  46. const result = occt.ReadStepFile(new Uint8Array(event.data.buffer), null);
  47. if (!result.success || result.meshes.length === 0) {
  48. self.postMessage({ id, ok: false, reason: 'no-meshes' } satisfies StepWorkerResponse);
  49. return;
  50. }
  51. const meshes = result.meshes.map(
  52. (mesh): StepWorkerMesh => ({
  53. positions: new Float32Array(mesh.attributes.position.array),
  54. normals: mesh.attributes.normal ? new Float32Array(mesh.attributes.normal.array) : null,
  55. indices: mesh.index ? new Uint32Array(mesh.index.array) : null,
  56. color: mesh.color ?? null,
  57. })
  58. );
  59. // Transfer the typed-array buffers instead of structured-cloning them —
  60. // large assemblies are tens of MB of vertex data.
  61. const transfers = meshes.flatMap((mesh) => {
  62. const buffers = [mesh.positions.buffer];
  63. if (mesh.normals) buffers.push(mesh.normals.buffer);
  64. if (mesh.indices) buffers.push(mesh.indices.buffer);
  65. return buffers;
  66. });
  67. self.postMessage({ id, ok: true, meshes } satisfies StepWorkerResponse, transfers);
  68. } catch {
  69. self.postMessage({ id, ok: false, reason: 'error' } satisfies StepWorkerResponse);
  70. }
  71. };