import { useEffect, useRef, useState } from 'react' import { MeshView } from './MeshView' import { VIEW_MODES } from './viewModes' import type { ViewMode } from './viewModes' import type { MeshData } from './mesh/types' import { faceCount, vertexCount } from './mesh/types' import { acceptedExtensions, conversionLosses, formatForFilename, formats } from './mesh/formats' import { estimateExportSize, getMeshioVersion, initMeshio, parseMeshFile, serializeMesh, } from './mesh/meshio' import { makeSampleMesh } from './mesh/sample' import { buildShareUrl, MAX_SHARE_URL_CHARS, parseShareHash } from './share' import './App.css' type EngineState = 'loading' | 'ready' | 'error' /** * What a share link would carry: the original uploaded file (so the recipient * gets byte-identical data in the original format), or a marker for the * generated sample mesh. */ type ShareSource = | { kind: 'file'; formatId: string; bytes: Uint8Array } | { kind: 'sample' } type ShareStatus = | { kind: 'copied'; chars: number } | { kind: 'manual'; url: string } // clipboard unavailable — show the link for hand-copying | { kind: 'too-large'; chars: number } | { kind: 'error'; message: string } function formatBytes(n: number): string { if (n < 1024) return `${n} B` if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB` return `${(n / 1024 / 1024).toFixed(1)} MB` } function App() { const [mesh, setMesh] = useState(null) const [sourceLabel, setSourceLabel] = useState('') const [baseName, setBaseName] = useState('mesh') const [parseWarnings, setParseWarnings] = useState([]) const [error, setError] = useState(null) const [exportFormatId, setExportFormatId] = useState(formats[0].id) const [engine, setEngine] = useState<{ state: EngineState; message: string }>({ state: 'loading', message: 'Loading mesh engine…', }) const [busy, setBusy] = useState<'parsing' | 'exporting' | 'sizing' | null>(null) const [exportSizes, setExportSizes] = useState | null>(null) const [viewMode, setViewMode] = useState('both') const [shareSource, setShareSource] = useState(null) const [shareStatus, setShareStatus] = useState(null) const fileInputRef = useRef(null) const shareLoadAttempted = useRef(false) const exportFormat = formats.find((f) => f.id === exportFormatId) ?? formats[0] const losses = mesh ? conversionLosses(mesh, exportFormat) : [] const engineReady = engine.state === 'ready' useEffect(() => { let cancelled = false initMeshio((message) => { if (!cancelled) setEngine({ state: 'loading', message }) }) .then(() => getMeshioVersion()) .then((version) => { if (!cancelled) setEngine({ state: 'ready', message: `meshio ${version} ready` }) }) .catch((e) => { if (!cancelled) { setEngine({ state: 'error', message: `Mesh engine failed to load: ${e instanceof Error ? e.message : String(e)}`, }) } }) return () => { cancelled = true } }, []) // Load a mesh from a #share=… link on startup (parseMeshFile waits for the // engine init kicked off above). useEffect(() => { if (shareLoadAttempted.current) return shareLoadAttempted.current = true ;(async () => { let payload try { payload = await parseShareHash(window.location.hash) } catch (e) { setError(`Could not read the share link: ${e instanceof Error ? e.message : String(e)}`) return } if (!payload) return if (formats.some((f) => f.id === payload.exportFormatId)) { setExportFormatId(payload.exportFormatId) } if (VIEW_MODES.some((m) => m.id === payload.viewMode)) { setViewMode(payload.viewMode as ViewMode) } if (payload.formatId === null) { setMesh(makeSampleMesh()) setShareSource({ kind: 'sample' }) setParseWarnings([]) setSourceLabel('built-in sample (from share link)') setBaseName(payload.name || 'rainbow_torus') return } const format = formats.find((f) => f.id === payload.formatId) if (!format) { setError(`Could not read the share link: unknown mesh format "${payload.formatId}"`) return } setBusy('parsing') try { const { mesh: parsed, info } = await parseMeshFile(payload.bytes, format) setMesh(parsed) setShareSource({ kind: 'file', formatId: format.id, bytes: payload.bytes }) setParseWarnings(info.warnings) setSourceLabel(`${payload.name}${format.extension} (${format.label}, from share link)`) setBaseName(payload.name || 'mesh') } catch (e) { setError( `Could not load the shared mesh: ${e instanceof Error ? e.message : String(e)}`, ) } finally { setBusy(null) } })() }, []) // A share link only describes the mesh it was created for — drop it from // the address bar once a different mesh is loaded. const clearShareHash = () => { if (window.location.hash) { history.replaceState(null, '', window.location.pathname + window.location.search) } } const handleFile = async (file: File) => { setError(null) const format = formatForFilename(file.name) if (!format) { setError( `Unrecognized extension on "${file.name}". Supported: ${acceptedExtensions.join(', ')}`, ) return } setBusy('parsing') try { const bytes = new Uint8Array(await file.arrayBuffer()) const { mesh: parsed, info } = await parseMeshFile(bytes, format) setMesh(parsed) setExportSizes(null) setParseWarnings(info.warnings) setSourceLabel(`${file.name} (${format.label})`) setBaseName(file.name.replace(/\.[^.]+$/, '')) setShareSource({ kind: 'file', formatId: format.id, bytes }) setShareStatus(null) clearShareHash() } catch (e) { setError(e instanceof Error ? e.message : String(e)) } finally { setBusy(null) } } const loadSample = () => { setError(null) setMesh(makeSampleMesh()) setExportSizes(null) setParseWarnings([]) setSourceLabel('built-in sample') setBaseName('rainbow_torus') setShareSource({ kind: 'sample' }) setShareStatus(null) clearShareHash() } const shareMesh = async () => { if (!shareSource) return setShareStatus(null) try { const url = await buildShareUrl({ name: baseName, formatId: shareSource.kind === 'file' ? shareSource.formatId : null, exportFormatId, viewMode, bytes: shareSource.kind === 'file' ? shareSource.bytes : new Uint8Array(0), }) if (url.length > MAX_SHARE_URL_CHARS) { setShareStatus({ kind: 'too-large', chars: url.length }) return } try { await navigator.clipboard.writeText(url) setShareStatus({ kind: 'copied', chars: url.length }) } catch { setShareStatus({ kind: 'manual', url }) } } catch (e) { setShareStatus({ kind: 'error', message: e instanceof Error ? e.message : String(e) }) } } const estimateSizes = async () => { if (!mesh) return setError(null) setBusy('sizing') setExportSizes({}) try { // pure-Python formats first, so a first-time h5py download doesn't // hold up the quick results const ordered = [...formats].sort( (a, b) => (a.pyodidePackages?.length ?? 0) - (b.pyodidePackages?.length ?? 0), ) for (const format of ordered) { const size = await estimateExportSize(mesh, format) setExportSizes((prev) => ({ ...(prev ?? {}), [format.id]: size })) } } catch (e) { setError(e instanceof Error ? e.message : String(e)) } finally { setBusy(null) } } const downloadExport = async () => { if (!mesh) return setError(null) setBusy('exporting') try { const bytes = await serializeMesh(mesh, exportFormat) const base = baseName.replace(/[^\w-]+/g, '_').toLowerCase() || 'mesh' const blob = new Blob([bytes], { type: 'application/octet-stream' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = base + exportFormat.extension a.click() URL.revokeObjectURL(url) } catch (e) { setError(e instanceof Error ? e.message : String(e)) } finally { setBusy(null) } } return (

Mesh Converter

Load a mesh, inspect it in 3D, export to another format. Conversion runs entirely in your browser via meshio on Pyodide.

{engine.message}

Load

{ const file = e.target.files?.[0] if (file) handleFile(file) e.target.value = '' }} /> {error &&
{error}
}
{mesh && (

Loaded mesh

{sourceLabel}
{vertexCount(mesh)} vertices, {faceCount(mesh)} faces
positions faces normals colors
{parseWarnings.length > 0 && (

{parseWarnings.join('; ')}

)}
)} {mesh && (

Export

{exportFormat.blurb}

{losses.length > 0 ? (
Exporting to {exportFormat.extension} will drop:{' '} {losses.join(', ')}
) : (
Lossless — this format keeps everything in the loaded mesh.
)}
)} {mesh && shareSource && (

Share

{shareStatus?.kind === 'copied' && (
Link copied to clipboard ({shareStatus.chars.toLocaleString()} characters).
)} {shareStatus?.kind === 'manual' && (
Couldn’t write to the clipboard — copy the link below by hand. e.target.select()} />
)} {shareStatus?.kind === 'too-large' && (
This mesh is too large to share by URL: the link would be{' '} {shareStatus.chars.toLocaleString()} characters, beyond the{' '} {MAX_SHARE_URL_CHARS.toLocaleString()} that links can reliably carry. Download the file and share it directly instead.
)} {shareStatus?.kind === 'error' &&
{shareStatus.message}
}

The link embeds the compressed mesh (the original file) plus the export and view settings in the URL itself — nothing is uploaded anywhere. Whoever opens it can view the mesh and download it in any format.

)}

Formats

{exportSizes && } {formats.map((f) => ( setExportFormatId(f.id)} title={`Export as ${f.label}`} > {exportSizes && ( )} ))}
Format normals colorssize
{f.id.toUpperCase()} {f.extension} {f.capabilities.normals ? '✓' : '—'} {f.capabilities.colors ? '✓' : '—'} {exportSizes[f.id] != null ? formatBytes(exportSizes[f.id]) : '…'}
{mesh && ( )}

All formats store positions and triangle faces; ✓ marks the extra attributes this app preserves on export. Quads and polygons are triangulated on import. Click a row to choose the export format.

{mesh ? ( ) : (

No mesh loaded.

Open a {acceptedExtensions.join(', ')} file — or load the sample.

)}
) } export default App