1/**
2 * JS side of the meshio bridge (adapted from mesh-converter). Loads Pyodide
3 * from the script tag in index.html, installs meshio via micropip, and runs
4 * bridge.py to turn an uploaded mesh file into the app's surface-mesh
5 * representation — all client-side.
6 */
7import bridgeCode from './bridge.py?raw'
8import type { MeshFormat } from './formats'
9import type { SurfaceMeshData } from './surfacemesh'
11const MESHIO_SPEC = 'meshio==5.3.5'
13const OUT_MSH = '/work/out.msh'
14const POSITIONS_F32 = '/work/positions.f32'
15const CELLS_U32 = '/work/cells.u32'
17interface Pyodide {
18 runPython(code: string): unknown
19 loadPackage(names: string[]): Promise<unknown>
20 pyimport(name: string): { install(spec: string): Promise<void> }
21 FS: {
22 writeFile(path: string, data: Uint8Array): void
23 readFile(path: string): Uint8Array<ArrayBuffer>
24 unlink(path: string): void
25 mkdirTree(path: string): void
26 }
27}
29declare global {
30 // provided by the pyodide.js script tag in index.html
31 function loadPyodide(options?: { indexURL?: string }): Promise<Pyodide>
32}
34export interface ParseResult {
35 mesh: SurfaceMeshData
36 numVertices: number
37 numCells: number
38 warnings: string[]
39}
41let initPromise: Promise<Pyodide> | null = null
43async function doInit(onProgress: (message: string) => void): Promise<Pyodide> {
44 if (typeof loadPyodide !== 'function') {
45 throw new Error('Pyodide script failed to load (offline? blocked CDN?)')
46 }
47 onProgress('Loading Python runtime (Pyodide)…')
48 const pyodide = await loadPyodide()
49 onProgress('Installing meshio…')
50 await pyodide.loadPackage(['micropip'])
51 await pyodide.pyimport('micropip').install(MESHIO_SPEC)
52 pyodide.runPython(bridgeCode)
53 return pyodide
54}
56/** Kick off (or join) the one-time Pyodide + meshio setup. */
57export function initMeshio(onProgress: (message: string) => void = () => {}): Promise<Pyodide> {
58 if (!initPromise) initPromise = doInit(onProgress)
59 return initPromise
60}
62/** Last line of a Python traceback, without the exception class name. */
63function pythonErrorMessage(err: unknown): string {
64 const raw = err instanceof Error ? err.message : String(err)
65 const lines = raw
66 .trim()
67 .split('\n')
68 .filter((l) => l.trim())
69 const last = lines[lines.length - 1] ?? raw
70 return last.replace(/^[\w.]+(?:Error|Exception|Exit)\s*:\s*/, '')
71}
73export async function parseMeshFile(bytes: Uint8Array, format: MeshFormat): Promise<ParseResult> {
74 const pyodide = await initMeshio()
75 const inputPath = '/work/input' + format.extension
76 pyodide.FS.mkdirTree('/work')
77 pyodide.FS.writeFile(inputPath, bytes)
78 let infoJson: string
79 try {
80 infoJson = String(
81 pyodide.runPython(
82 `parse_mesh(${JSON.stringify(inputPath)}, ${JSON.stringify(format.id)})`,
83 ),
84 )
85 } catch (err) {
86 throw new Error(pythonErrorMessage(err))
87 } finally {
88 try {
89 pyodide.FS.unlink(inputPath)
90 } catch {
91 /* not created */
92 }
93 }
94 const info = JSON.parse(infoJson) as {
95 numVertices: number
96 numCells: number
97 cellSize: 3 | 4
98 warnings: string[]
99 }
100 const posBytes = pyodide.FS.readFile(POSITIONS_F32)
101 const cellBytes = pyodide.FS.readFile(CELLS_U32)
102 const mesh: SurfaceMeshData = {
103 positions: new Float32Array(posBytes.buffer, posBytes.byteOffset, posBytes.byteLength / 4),
104 cells: new Uint32Array(cellBytes.buffer, cellBytes.byteOffset, cellBytes.byteLength / 4),
105 cellSize: info.cellSize,
106 mshBytes: pyodide.FS.readFile(OUT_MSH),
107 }
108 const { numVertices, numCells, warnings } = info
109 return { mesh, numVertices, numCells, warnings }
110}