Dev.to WebDev 🛠 Dev 👁 0 📖 4 min read

What "non-manifold" actually means in an STL file (and how to count the problems yourself)

Every slicer has a moment where it pops up "this model has errors" and offers to fix them. Most of the time you click yes, the print comes out fine, and you never think about it again. But when the auto-fix guesses wrong

Every slicer has a moment where it pops up "this model has errors" and offers to fix them. Most of the time you click yes, the print comes out fine, and you never think about it again. But when the auto-fix guesses wrong (a vent sealed shut, two parts fused, a wall that just isn't there), it helps to know what the slicer was actually complaining about.

The good news: the checks behind those warnings are simple. You can reason about them with nothing more than a list of triangles.

An STL is just triangles

An STL file has no curves, no units, and no idea which triangles belong together. It's a flat list of triangles, each with three corner points and a normal. Even the corners aren't shared: if six triangles meet at one point, that point is written six times.

That last detail matters, because every check below is about how triangles connect, and the file doesn't store connections at all. You have to rebuild them.

Step 1: weld the duplicate corners

Before you can ask "which triangles share this edge?", you need to know which corners are the same point. The usual approach is to round each coordinate to a tolerance and treat matching positions as one vertex:

// positions: Float32Array, 9 numbers per triangle (3 corners × xyz)
const TOL = 1e-6;
const ids = new Map();
const key = (x, y, z) =>
  `${Math.round(x / TOL)},${Math.round(y / TOL)},${Math.round(z / TOL)}`;

const index = [];
for (let i = 0; i < positions.length; i += 3) {
  const k = key(positions[i], positions[i + 1], positions[i + 2]);
  if (!ids.has(k)) ids.set(k, ids.size);
  index.push(ids.get(k));
}
// index now has 3 vertex ids per triangle

Welding doesn't change the shape. It only recovers the topology the format threw away.

Step 2: count how many triangles use each edge

With shared vertex ids, every triangle has three edges, and an edge is just a pair of ids. Sort the pair so A–B and B–A are the same edge, then count:

const edgeUse = new Map();
for (let t = 0; t < index.length; t += 3) {
  const [a, b, c] = [index[t], index[t + 1], index[t + 2]];
  for (const [u, v] of [[a, b], [b, c], [c, a]]) {
    const e = u < v ? `${u}_${v}` : `${v}_${u}`;
    edgeUse.set(e, (edgeUse.get(e) || 0) + 1);
  }
}

Now the three classic problems fall out of that one map.

The three things your slicer is warning about

1. Holes (boundary edges). An edge used by exactly one triangle is on the rim of an opening. A closed shell has none. A model straight out of a sculpting app with an unfinished base, or a scan that missed the underside, will have a whole loop of them.

2. Non-manifold edges. An edge used by three or more triangles. Picture two boxes that touch along one edge, or an internal wall left over from a boolean. The slicer can no longer tell which side is inside, which is where missing walls and odd infill come from.

3. Degenerate triangles. Triangles with (near) zero area: two corners in the same spot, or all three in a line. They're usually harmless leftovers from exports, but they can trip up the edge analysis and some slicers' repair passes.

"Watertight", in the sense slicers mean it, is simply: every edge is used by exactly two triangles.

What a basic repair can safely do, and what it shouldn't

Some fixes are safe because they don't invent geometry:

  • Weld near-duplicate vertices (same shape, cleaner data)
  • Drop zero-area triangles
  • Recompute normals so the preview shades correctly

Filling holes is different. A fill has to make up surface that isn't in the file, and an automatic fill has no idea whether that opening was a mistake or a vent you meant to keep. That's a decision for a tool where you can see and undo the result: your slicer's repair, Blender's fill on a boundary loop, or Meshmixer.

There are also problems an edge count simply can't see: triangles that pass through each other without sharing an edge (self-intersections), and a shell whose triangles are wound inside-out. If a model passes the edge test and still slices strangely, those are the next suspects.

Trying it on your own file

If you'd rather not write the script, I put this exact check in a browser page: STL repair check. It counts holes, non-manifold edges, degenerate triangles and duplicate vertices, runs the safe basic repair above, and gives you a repaired STL. The file is parsed in the tab and never uploaded. It deliberately does not fill holes, for the reason above.

One related trap: if your file is huge because of triangle count rather than detail, slicing is slow and repair tools choke. Reducing the triangle count first, then running the check, is often the faster path. Decimation can open small gaps, so check after reducing, not before.

Quick reference

Edge used by… Meaning Usual cause
1 triangle Hole / open boundary Unfinished base, scan gap, deleted face
2 triangles Normal, watertight —
3+ triangles Non-manifold Touching parts, leftover internal walls

If you've ever had a slicer "fix" eat part of a model, I'd like to hear what the original problem turned out to be. Did the repair check agree with your slicer?

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.