diff --git a/packages/editor/src/index.tsx b/packages/editor/src/index.tsx index 0d105fdc1..c2cb09461 100644 --- a/packages/editor/src/index.tsx +++ b/packages/editor/src/index.tsx @@ -515,6 +515,8 @@ export { editorHostPanelRegistry, registerEditorHostPanel, } from './lib/plugin-panels' +export { configureManifoldRuntime } from './lib/print-shell-compiler-manifold-worker' +export type { ManifoldRuntimeOptions } from './lib/print-shell-compiler-protocol' export { createQuickMeasurementPointerScheduler, quickMeasurementContext, diff --git a/packages/editor/src/lib/print-shell-compiler-manifold-core.ts b/packages/editor/src/lib/print-shell-compiler-manifold-core.ts index 082dcb835..013cd2e26 100644 --- a/packages/editor/src/lib/print-shell-compiler-manifold-core.ts +++ b/packages/editor/src/lib/print-shell-compiler-manifold-core.ts @@ -1,6 +1,10 @@ -import ManifoldModule, { type Manifold as ManifoldSolid, type ManifoldToplevel } from 'manifold-3d' +import type { Manifold as ManifoldSolid, ManifoldToplevel } from 'manifold-3d' import type { PrintShellCompileDiagnostic } from './print-shell-compiler-baseline' -import type { ManifoldCompileOutput, ManifoldMeshData } from './print-shell-compiler-protocol' +import type { + ManifoldCompileOutput, + ManifoldMeshData, + ManifoldRuntimeOptions, +} from './print-shell-compiler-protocol' let modulePromise: Promise | null = null const MANIFOLD_OUTPUT_WELD_EPSILON_METERS = 2e-5 @@ -8,14 +12,59 @@ const COLLINEAR_SEAM_CROSS_LENGTH_SQ = 1e-20 type Triangle = [number, number, number] -async function getManifoldModule(wasmUrl?: string): Promise { - modulePromise ??= ManifoldModule(wasmUrl ? { locateFile: () => wasmUrl } : undefined).then( - (module) => { +type ManifoldFactory = (config?: { + locateFile?: (path: string) => string +}) => Promise + +// manifold-3d's emscripten glue awaits import('node:module') behind a Node +// check. The branch never executes in a browser, but webpack refuses to +// *build* a graph that can reach it, so a static specifier here poisons every +// external bundler that compiles this package's source (#715). The factory is +// therefore loaded through an import() no bundler can trace: the bare +// specifier resolves wherever node-style resolution exists at runtime (bun +// tests, dev servers, bundlers that inline it anyway), and the version-pinned +// CDN copy covers bundled browser builds that left the specifier unresolved — +// emscripten then locates manifold.wasm relative to the glue's own URL. Hosts +// that can't reach the CDN (offline, CSP) pass their own URLs through +// configureManifoldRuntime. +const MANIFOLD_VERSION = '3.5.1' +const FALLBACK_MODULE_URL = `https://cdn.jsdelivr.net/npm/manifold-3d@${MANIFOLD_VERSION}/manifold.js` + +function importUntraced(specifier: string): Promise<{ default: ManifoldFactory }> { + return import(/* webpackIgnore: true */ /* @vite-ignore */ specifier) +} + +async function loadManifoldFactory(moduleUrl?: string): Promise { + const specifiers = moduleUrl ? [moduleUrl] : ['manifold-3d', FALLBACK_MODULE_URL] + let lastError: unknown + for (const specifier of specifiers) { + try { + return (await importUntraced(specifier)).default + } catch (error) { + lastError = error + } + } + throw lastError instanceof Error ? lastError : new Error('Failed to load manifold-3d.') +} + +async function getManifoldModule(runtime?: ManifoldRuntimeOptions): Promise { + modulePromise ??= loadManifoldFactory(runtime?.moduleUrl) + .then((factory) => { + const wasmUrl = runtime?.wasmUrl + return factory(wasmUrl ? { locateFile: () => wasmUrl } : undefined) + }) + .then((module) => { module.setup() return module - }, - ) - return modulePromise + }) + try { + return await modulePromise + } catch (error) { + // A transient load failure (offline, blocked CDN) must not poison every + // later compile attempt with the cached rejection. + modulePromise = null + throw error + } } function manifoldMesh( @@ -211,7 +260,7 @@ function elapsed(startedAt: number): number { export async function compileManifoldMeshData( meshes: ManifoldMeshData[], - wasmUrl?: string, + runtime?: ManifoldRuntimeOptions, ): Promise { const startedAt = performance.now() const sourceNodeIds = Array.from(new Set(meshes.map((mesh) => mesh.nodeId))).sort() @@ -238,7 +287,7 @@ export async function compileManifoldMeshData( } try { - const module = await getManifoldModule(wasmUrl) + const module = await getManifoldModule(runtime) for (const mesh of meshes) { try { solids.push(new module.Manifold(manifoldMesh(module, mesh))) diff --git a/packages/editor/src/lib/print-shell-compiler-manifold-worker.ts b/packages/editor/src/lib/print-shell-compiler-manifold-worker.ts index f4ddb4c85..d274f3a60 100644 --- a/packages/editor/src/lib/print-shell-compiler-manifold-worker.ts +++ b/packages/editor/src/lib/print-shell-compiler-manifold-worker.ts @@ -16,12 +16,25 @@ import { import type { ManifoldCompileOutput, ManifoldMeshData, + ManifoldRuntimeOptions, ManifoldWorkerRequest, ManifoldWorkerResponse, } from './print-shell-compiler-protocol' const WORKER_TIMEOUT_MS = 60_000 +let manifoldRuntime: ManifoldRuntimeOptions | undefined + +/** + * Overrides where the print-export worker loads the manifold-3d module and + * wasm from. See the loader in print-shell-compiler-manifold-core.ts for the + * default resolution order; hosts with restrictive networks should call this + * with self-hosted asset URLs before the first print export. + */ +export function configureManifoldRuntime(options: ManifoldRuntimeOptions | undefined): void { + manifoldRuntime = options +} + export type ManifoldCompileRunner = (meshes: ManifoldMeshData[]) => Promise export type SemanticManifoldCompileOptions = SemanticPrintCompileOptions & { @@ -76,7 +89,7 @@ export const runManifoldWorker: ManifoldCompileRunner = (meshes) => { const activeWorker = getWorker() const id = nextRequestId nextRequestId += 1 - const request: ManifoldWorkerRequest = { id, meshes } + const request: ManifoldWorkerRequest = { id, meshes, runtime: manifoldRuntime } const transfer = meshes.flatMap((mesh) => [ mesh.positions.buffer as ArrayBuffer, mesh.indices.buffer as ArrayBuffer, diff --git a/packages/editor/src/lib/print-shell-compiler-manifold.worker.ts b/packages/editor/src/lib/print-shell-compiler-manifold.worker.ts index 49b273969..20f0118cc 100644 --- a/packages/editor/src/lib/print-shell-compiler-manifold.worker.ts +++ b/packages/editor/src/lib/print-shell-compiler-manifold.worker.ts @@ -10,7 +10,7 @@ const workerScope = self as unknown as { } workerScope.addEventListener('message', async (event) => { - const output = await compileManifoldMeshData(event.data.meshes) + const output = await compileManifoldMeshData(event.data.meshes, event.data.runtime) const response: ManifoldWorkerResponse = { id: event.data.id, ...output } const transfer: Transferable[] = [] if (response.status === 'compiled') { diff --git a/packages/editor/src/lib/print-shell-compiler-protocol.ts b/packages/editor/src/lib/print-shell-compiler-protocol.ts index 8554f1e2e..6907ba62b 100644 --- a/packages/editor/src/lib/print-shell-compiler-protocol.ts +++ b/packages/editor/src/lib/print-shell-compiler-protocol.ts @@ -22,9 +22,17 @@ export type ManifoldCompileOutput = durationMs: number } +export type ManifoldRuntimeOptions = { + /** URL of the manifold-3d emscripten glue module. Defaults to the pinned CDN copy. */ + moduleUrl?: string + /** URL of manifold.wasm. Defaults to resolving relative to the glue module. */ + wasmUrl?: string +} + export type ManifoldWorkerRequest = { id: number meshes: ManifoldMeshData[] + runtime?: ManifoldRuntimeOptions } export type ManifoldWorkerResponse = ManifoldCompileOutput & { id: number }