edd4e73Mesh converter: upload, view, and export meshes across formatsJeremy Magland 1import { useRef, useState } from 'react'
2import { MeshView } from './MeshView'
3import type { MeshData } from './mesh/types'
4import { faceCount, vertexCount } from './mesh/types'
5import { acceptedExtensions, conversionLosses, formatForFilename, formats } from './mesh/formats'
6import { makeSampleMesh } from './mesh/sample'
7import './App.css'
9function App() {
10 const [mesh, setMesh] = useState<MeshData | null>(null)
11 const [sourceLabel, setSourceLabel] = useState<string>('')
12 const [error, setError] = useState<string | null>(null)
13 const [exportFormatId, setExportFormatId] = useState(formats[0].id)
14 const fileInputRef = useRef<HTMLInputElement>(null)
16 const exportFormat = formats.find((f) => f.id === exportFormatId) ?? formats[0]
17 const losses = mesh ? conversionLosses(mesh, exportFormat) : []
19 const handleFile = async (file: File) => {
20 setError(null)
21 const format = formatForFilename(file.name)
22 if (!format) {
23 setError(
24 `Unrecognized extension on "${file.name}". Supported: ${acceptedExtensions.join(', ')}`,
25 )
26 return
27 }
28 try {
29 const text = await file.text()
30 setMesh(format.parse(text))
31 setSourceLabel(`${file.name} (${format.label})`)
32 } catch (e) {
33 setError(e instanceof Error ? e.message : String(e))
34 }
35 }
37 const loadSample = () => {
38 setError(null)
39 setMesh(makeSampleMesh())
40 setSourceLabel('built-in sample')
41 }
43 const downloadExport = () => {
44 if (!mesh) return
45 const text = exportFormat.serialize(mesh)
46 const base = (mesh.name ?? 'mesh').replace(/[^\w-]+/g, '_').toLowerCase() || 'mesh'
47 const blob = new Blob([text], { type: 'text/plain' })
48 const url = URL.createObjectURL(blob)
49 const a = document.createElement('a')
50 a.href = url
51 a.download = base + exportFormat.extension
52 a.click()
53 URL.revokeObjectURL(url)
54 }
56 return (
57 <div className="app">
58 <div className="sidebar">
59 <h1>Mesh Converter</h1>
60 <p className="tagline">
61 Load a mesh, inspect it in 3D, export to another format. Formats differ in what they can
62 store — anything the target can't hold is dropped.
63 </p>
65 <section>
66 <h2>Load</h2>
67 <div className="button-row">
68 <button onClick={() => fileInputRef.current?.click()}>Open mesh file…</button>
69 <button onClick={loadSample}>Load sample</button>
70 </div>
71 <input
72 ref={fileInputRef}
73 type="file"
74 accept={acceptedExtensions.join(',')}
75 hidden
76 onChange={(e) => {
77 const file = e.target.files?.[0]
78 if (file) handleFile(file)
79 e.target.value = ''
80 }}
81 />
82 {error && <div className="error">{error}</div>}
83 </section>
85 {mesh && (
86 <section>
87 <h2>Loaded mesh</h2>
88 <div className="mesh-info">
89 <div className="source">{sourceLabel}</div>
90 <div>
91 <strong>{mesh.name ?? '(unnamed)'}</strong> — {vertexCount(mesh)} vertices,{' '}
92 {faceCount(mesh)} faces
93 </div>
94 <div className="chips">
95 <span className="chip on">positions</span>
96 <span className="chip on">faces</span>
97 <span className={`chip ${mesh.normals ? 'on' : ''}`}>normals</span>
98 <span className={`chip ${mesh.colors ? 'on' : ''}`}>colors</span>
99 <span className={`chip ${mesh.name ? 'on' : ''}`}>name</span>
100 </div>
101 </div>
102 </section>
103 )}
105 {mesh && (
106 <section>
107 <h2>Export</h2>
108 <select value={exportFormatId} onChange={(e) => setExportFormatId(e.target.value)}>
109 {formats.map((f) => (
110 <option key={f.id} value={f.id}>
111 {f.label} — {f.extension}
112 </option>
113 ))}
114 </select>
115 <p className="format-blurb">{exportFormat.blurb}</p>
116 {losses.length > 0 ? (
117 <div className="warning">
118 Exporting to {exportFormat.extension} will drop:{' '}
119 <strong>{losses.join(', ')}</strong>
120 </div>
121 ) : (
122 <div className="ok">Lossless — this format keeps everything in the loaded mesh.</div>
123 )}
124 <button className="primary" onClick={downloadExport}>
125 Download {exportFormat.extension}
126 </button>
127 </section>
128 )}
130 <section>
131 <h2>Formats</h2>
132 <table className="format-table">
133 <thead>
134 <tr>
135 <th>Format</th>
136 <th>normals</th>
137 <th>colors</th>
138 <th>name</th>
139 </tr>
140 </thead>
141 <tbody>
142 {formats.map((f) => (
143 <tr key={f.id}>
144 <td>
145 {f.id.toUpperCase()} <span className="ext">{f.extension}</span>
146 </td>
147 <td>{f.capabilities.normals ? '✓' : '—'}</td>
148 <td>{f.capabilities.colors ? '✓' : '—'}</td>
149 <td>{f.capabilities.name ? '✓' : '—'}</td>
150 </tr>
151 ))}
152 </tbody>
153 </table>
154 <p className="footnote">
155 These are invented text formats for illustration; real formats come later. All store
156 positions and triangle faces.
157 </p>
158 </section>
159 </div>
161 <div className="viewport">
162 {mesh ? (
163 <MeshView mesh={mesh} />
164 ) : (
165 <div className="empty-state">
166 <p>No mesh loaded.</p>
167 <p>Open a .mopf, .tricol, or .bmsh file — or load the sample.</p>
168 </div>
169 )}
170 </div>
171 </div>
172 )
173}
175export default App