import { useCallback, useEffect, useState } from 'react' import { ACCEPT, formatForFilename } from './mesh/formats' import { initMeshio, parseMeshFile } from './mesh/meshio' import { edgeClassification, type QuadMeshData } from './mesh/quadmesh' import { prewarm, solve, type SolutionData } from './engine/engine' import { PDES, MAX_QUADS, SLOW_QUADS, MIN_ORDER, MAX_ORDER, DEFAULT_ORDER, type PdeDef, } from './pde/presets' import { SurfaceView } from './render/SurfaceView' // Module-level so React StrictMode double-mounting doesn't prewarm twice // (the prewarm downloads the MATLAB packages into numbl's IndexedDB cache). let prewarmPromise: Promise | null = null interface LoadedMesh { name: string data: QuadMeshData numVertices: number numQuads: number closed: boolean nonManifold: boolean warnings: string[] } const SAMPLES = [ { label: 'Sphere (cubed)', file: 'sphere.msh' }, { label: 'Torus', file: 'torus.msh' }, ] export default function App() { const [meshioStatus, setMeshioStatus] = useState('Loading Python runtime…') const [meshioReady, setMeshioReady] = useState(false) const [engineStatus, setEngineStatus] = useState('Preparing MATLAB packages…') const [engineReady, setEngineReady] = useState(false) const [consoleLines, setConsoleLines] = useState([]) const [mesh, setMesh] = useState(null) const [meshError, setMeshError] = useState(null) const [parsing, setParsing] = useState(false) const [pde, setPde] = useState(PDES[0]) const [fExpr, setFExpr] = useState(PDES[0].fPresets[0].expr) const [cExpr, setCExpr] = useState('100*(1 - z)') const [order, setOrder] = useState(DEFAULT_ORDER) const [solving, setSolving] = useState(false) const [solveStatus, setSolveStatus] = useState('') const [solveError, setSolveError] = useState(null) const [solution, setSolution] = useState(null) const [solveSeconds, setSolveSeconds] = useState(null) const appendConsole = useCallback((text: string) => { setConsoleLines((lines) => [...lines.slice(-199), text.replace(/\n$/, '')]) }, []) useEffect(() => { initMeshio(setMeshioStatus) .then(() => { setMeshioReady(true) setMeshioStatus('') }) .catch((err) => setMeshioStatus(`Mesh reader failed: ${String(err.message ?? err)}`)) if (!prewarmPromise) { prewarmPromise = prewarm({ onProgress: setEngineStatus, onOutput: appendConsole }) } prewarmPromise // A failed prewarm isn't fatal — the solve re-attempts the downloads. .catch((err) => appendConsole(`package prewarm failed: ${String(err?.message ?? err)}`)) .finally(() => { setEngineReady(true) setEngineStatus('') }) }, [appendConsole]) const loadMesh = useCallback( async (name: string, bytes: Uint8Array) => { setMeshError(null) setSolveError(null) setSolution(null) setParsing(true) try { const format = formatForFilename(name) if (!format) throw new Error(`Unsupported file extension on "${name}"`) const result = await parseMeshFile(bytes, format) if (result.numQuads > MAX_QUADS) { throw new Error( `${result.numQuads} quads is too many for an in-browser solve ` + `(limit ${MAX_QUADS}); please upload a coarser mesh.`, ) } const cls = edgeClassification(result.mesh.quads) setMesh({ name, data: result.mesh, numVertices: result.numVertices, numQuads: result.numQuads, closed: cls.closed, nonManifold: cls.nonManifold, warnings: result.warnings, }) } catch (err) { setMesh(null) setMeshError(err instanceof Error ? err.message : String(err)) } finally { setParsing(false) } }, [], ) const onUpload = useCallback( async (e: React.ChangeEvent) => { const file = e.target.files?.[0] e.target.value = '' if (!file) return await loadMesh(file.name, new Uint8Array(await file.arrayBuffer())) }, [loadMesh], ) const onSample = useCallback( async (file: string) => { const resp = await fetch(`${import.meta.env.BASE_URL}samples/${file}`) if (!resp.ok) { setMeshError(`Failed to fetch sample: HTTP ${resp.status}`) return } await loadMesh(file, new Uint8Array(await resp.arrayBuffer())) }, [loadMesh], ) const onSolve = useCallback(async () => { if (!mesh) return setSolveError(null) setSolving(true) setSolveStatus('') setSolveSeconds(null) const t0 = performance.now() try { const result = await solve( mesh.data.mshBytes, { pde: pde.id, f: fExpr.trim(), c: cExpr.trim(), p: order, closed: mesh.closed, }, { onProgress: setSolveStatus, onOutput: appendConsole }, ) setSolution(result) setSolveSeconds((performance.now() - t0) / 1000) } catch (err) { setSolveError(err instanceof Error ? err.message : String(err)) } finally { setSolving(false) } }, [mesh, pde, fExpr, cExpr, order, appendConsole]) const onDownloadMsh = useCallback(() => { if (!mesh) return const blob = new Blob([mesh.data.mshBytes as BlobPart], { type: 'application/octet-stream' }) const a = document.createElement('a') a.href = URL.createObjectURL(blob) a.download = mesh.name.replace(/\.[^.]*$/, '') + '.msh' a.click() URL.revokeObjectURL(a.href) }, [mesh]) const onPdeChange = (id: string) => { const def = PDES.find((p) => p.id === id) ?? PDES[0] setPde(def) setFExpr(def.fPresets[0].expr) } const booting = !meshioReady || !engineReady const dof = mesh ? mesh.numQuads * (order + 1) * (order + 1) : 0 const canSolve = !!mesh && engineReady && !solving && fExpr.trim() !== '' return (

Mesh PDE Solver

Upload a quadrilateral surface mesh, pick a PDE, and solve it on the surface with{' '} surfacefun {' '} running in your browser via numbl.

) }