import { useCallback, useEffect, useState } from 'react' import { ACCEPT, formatForFilename } from './mesh/formats' import { initMeshio, parseMeshFile } from './mesh/meshio' import { edgeClassification, type SurfaceMeshData } from './mesh/surfacemesh' import { buildSolveScript, prewarm, solve, type SolutionData } from './engine/engine' import { PDES, SLOW_CELLS, 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: SurfaceMeshData numVertices: number numCells: number closed: boolean nonManifold: boolean warnings: string[] } const SAMPLES = [ { label: 'Sphere (quads)', file: 'sphere.msh' }, { label: 'Sphere (triangles)', file: 'sphere-tri.msh' }, { label: 'Torus', file: 'torus.msh' }, ] // Filename the converted mesh is staged and downloaded under — embedded in // the generated MATLAB script, so sanitized to stay a plain quotable token. const mshFileName = (name: string) => (name.replace(/\.[^.]*$/, '').replace(/[^\w.-]+/g, '_') || 'mesh') + '.msh' const download = (filename: string, content: BlobPart) => { const a = document.createElement('a') a.href = URL.createObjectURL(new Blob([content], { type: 'application/octet-stream' })) a.download = filename a.click() URL.revokeObjectURL(a.href) } 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) const cls = edgeClassification(result.mesh.cells, result.mesh.cellSize) setMesh({ name, data: result.mesh, numVertices: result.numVertices, numCells: result.numCells, 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], ) // Everything the MATLAB script template needs, from the current UI state. const solveParams = useCallback( () => mesh && { pde: pde.id, f: fExpr.trim(), c: cExpr.trim(), p: order, closed: mesh.closed, meshFile: mshFileName(mesh.name), }, [mesh, pde, fExpr, cExpr, order], ) const onSolve = useCallback(async () => { const params = solveParams() if (!mesh || !params) return setSolveError(null) setSolving(true) setSolveStatus('') setSolveSeconds(null) const t0 = performance.now() try { const result = await solve(mesh.data.mshBytes, params, { 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, solveParams, appendConsole]) const onDownloadMsh = useCallback(() => { if (mesh) download(mshFileName(mesh.name), mesh.data.mshBytes as BlobPart) }, [mesh]) const onDownloadScript = useCallback(() => { const params = solveParams() if (params) download('solve_pde.m', buildSolveScript(params)) }, [solveParams]) 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 // points per patch: (p+1)^2 on quads, (p+1)(p+2)/2 on triangles const dof = mesh ? mesh.data.cellSize === 3 ? (mesh.numCells * (order + 1) * (order + 2)) / 2 : mesh.numCells * (order + 1) * (order + 1) : 0 // Empty expressions would leave a syntactically broken generated script. const paramsOk = !!mesh && fExpr.trim() !== '' && (!pde.cPresets || cExpr.trim() !== '') const canSolve = paramsOk && engineReady && !solving return (

Mesh PDE Solver

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

) }