Check a 3D model before you print it — size, volume, holes and non-manifold edges — without uploading the file.
Live: https://meshprep.benrichardson.dev
You have an STL. Maybe you paid for it, maybe a client sent it, maybe you exported it from Fusion an hour ago. Before you commit four hours and 90 grams of filament you want three things: the real dimensions (STL has no units, so is that 40mm or 40 inches?), the material it will use, and — the one that actually ruins prints — whether the mesh is watertight. A surface with holes or non-manifold edges makes a slicer guess where the inside is, and the result is missing walls, phantom infill, or a model that slices to nothing at all.
The existing answers are to install a 400MB desktop package to ask one question, or to upload the file to an online analyser. The upload is the problem: these files are routinely paid downloads with licence terms, client geometry under NDA, or a product that has not shipped. A 3D model is the intellectual property.
Meshprep does the whole job in the browser. Drop a model, and it reports the bounding box, volume, surface area, triangle and shell counts, and a pass/fail printability verdict — with every open and non-manifold edge drawn in red on the model itself, so you can see where it is broken rather than just being told a number. It also converts between STL, OBJ, PLY and 3MF, and can rescale, centre, drop to the plate and flip Y-up to Z-up on the way out.
file bytes ──▶ sniff format ──▶ parse to triangle soup ──▶ weld vertices
│
┌─────────────────────┤
▼ ▼
edge topology volume / area
(counts + winding) (divergence theorem)
│ │
└─────────┬───────────┘
▼
verdict + red edge line list ──▶ WebGL2
Everything above runs inside one dedicated Web Worker. The main thread only ever holds a copy of the geometry to feed the GPU, so a 300MB mesh never blocks the interface.
Welding comes first, and it is the whole game. STL — the dominant 3D-printing format — has no concept of a shared corner; it stores each triangle's three corners independently. A perfectly solid cube arrives as 36 loose points, and until coincident corners are merged, every single edge looks like a hole. Welding uses a spatial hash at 1e-5 of the bounding-box diagonal (a micron on a 100mm part). The subtle part is cell boundaries: two corners a nanometre apart can still land in different cells, so a corner within a quarter-cell of a boundary also probes the neighbour on that axis. Two corners that straddle a boundary are both, by definition, near it — so they always find each other, while a corner comfortably inside its cell still costs one lookup.
Then every edge is counted. In a closed solid each edge is used by exactly two triangles. Used once → a hole. Used three or more times → non-manifold, the surface branches and there is no single answer to which side is inside. The two triangles either side of a shared edge should also traverse it in opposite directions; when they do not, the winding is inconsistent.
Volume comes from the divergence theorem, not a voxel estimate: each triangle forms a tetrahedron with the origin and the signed volumes cancel everywhere except the interior. This is correct wherever the origin happens to sit, and the sign tells you whether the surface is wound outward or the model is inside-out.
The material estimate models the walls explicitly. Volume × density × infill% is wrong for most printed parts and badly wrong for small ones, because perimeters and skins are solid regardless of infill. Meshprep uses surface area × wall thickness for the shell and applies infill only to what is left over.
- WebGL2 — a first-party renderer (~300 lines, no three.js). Flat shading is recovered from screen-space derivatives of the world position, so no normal buffer is uploaded at all; backface culling is off and lighting is two-sided, because a model with inverted normals has to stay visible — telling you the normals are inverted is part of the job.
- Web Workers — parse, analyse and export, off the main thread.
- Transferable ArrayBuffer — geometry crosses the worker boundary without copying.
- Pointer Events — orbit, pan and pinch-zoom.
- File System Access API — "Save as…" straight to disk, with an anchor-download fallback.
- Drag and Drop / File API — page-wide drop target.
- Web Share API (level 2, files) — share the converted file on mobile.
- Clipboard API — copy the report as text.
- Service Worker (vite-plugin-pwa) — fully offline after first load.
Format parsers for STL (binary + ASCII), OBJ, PLY (ASCII + binary, either endianness) and 3MF
are hand-written. That is not purism: a worker has no DOMParser, and every off-the-shelf 3MF
reader assumes one. Writing them keeps the entire pipeline in the worker where it belongs, and
keeps the bundle at 48KB of JavaScript.
Protected
- Your model file never leaves your device. There is no upload endpoint and no backend.
- Parsing, analysis and export all run locally in a Web Worker in the tab.
- No filename, dimension, or value derived from your geometry is transmitted anywhere.
- Works with the network switched off after the first load.
- No cookies, no fingerprinting, no third-party fonts, no advertising.
Not protected
- Loading the page is an ordinary web request: GitHub Pages serves it and sees your IP and user agent, as any website does.
- A file you export and then choose to send through the share sheet goes wherever you send it.
- This is not DRM and not a licence checker. It does not know what a model's terms are — it simply never copies your file anywhere.
Trust model
- The static bundle served from GitHub Pages, and the TLS chain between you and it.
- A Cloudflare Web Analytics beacon records anonymous page views — no cookies, no fingerprinting, no cross-site tracking; your files/data are never sent to it.
- Feedback you choose to send (and an email address, only if you supply one) is sent to feedback.benrichardson.dev. Nothing is sent unless you open the feedback form and press Send.
- The source is public: read it, or run it offline from your own machine.
A Content-Security-Policy meta tag restricts the page to 'self' plus the analytics beacon
and the feedback endpoint. There is nothing else it can talk to.
Meshprep diagnoses; it does not repair. There is no hole filling and no remeshing — an honest diagnosis is the product, and a bad automatic repair is worse than none. It also does not detect self-intersections (O(n²) without a spatial index, and these are large meshes), does not read glTF/GLB/FBX, and does not slice, generate supports or emit G-code.
- Vite 6 + vanilla TypeScript
fflatefor the 3MF ZIP container — the only runtime dependency- Vitest for unit tests (86 tests)
- GitHub Pages for hosting, deployed via GitHub Actions
No runtime dependencies beyond fflate. No cookies, no fingerprinting, no third-party fonts. Anonymous, cookie-less page-view counts via Cloudflare Web Analytics — no personal data, no cross-site tracking.
npm install
npm run dev # vite dev server on :5173
npm test # run vitest suite
npm run build # produce dist/ for deploy
npm run preview # serve dist/ locallyA push to main triggers .github/workflows/deploy.yml, which runs tests, builds, and deploys
dist/ to GitHub Pages. The custom domain is set via public/CNAME — point a CNAME DNS
record for meshprep.benrichardson.dev at ben-gy.github.io.
MIT — see LICENSE.