🎥 PodcastsDesigned in California Makes Its Official Debut(03.09.2026 um 17:59 Uhr)
🔧 AI Nachrichten Major AI platforms go down in unprecedented simultaneous outage(03.09.2026 um 17:34 Uhr)
🍏 iOS / Mac OSWill Siri AI Speak Hindi? What Apple Has Published for India(11.09.2026 um 05:15 Uhr)
🍏 iOS / Mac OSiPhone Duo Apps Could Make or Break Apple’s Foldable iPhone(11.09.2026 um 05:16 Uhr)
🎥 PodcastsDesigned in California Makes Its Official Debut(03.09.2026 um 17:59 Uhr)
🔧 AI Nachrichten Major AI platforms go down in unprecedented simultaneous outage(03.09.2026 um 17:34 Uhr)
🍏 iOS / Mac OSWill Siri AI Speak Hindi? What Apple Has Published for India(11.09.2026 um 05:15 Uhr)
🍏 iOS / Mac OSiPhone Duo Apps Could Make or Break Apple’s Foldable iPhone(11.09.2026 um 05:16 Uhr)

🔧 Programmierung 🕛 vor 1 Monat 8 Min Lesezeit
0

Diagnosing a Broken STL File: Union-Find, Boundary Loops, and SDF Voxel Rebuilds

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht

A lot of 3D files are quietly broken. Not "won't open" broken — they load fine in a viewer, look like a normal model, and then a slicer either rejects them outright or silently produces garbage: missing walls, phantom internal cavities, infill leaking out through a gap you can't see. This happens constantly with AI-generated meshes and phone LiDAR scans, and it's rare enough with hand-modeled CAD files that most tutorials don't cover it well.



We've spent the last few months building .






What "broken" means to a slicer



A mesh is a soup of triangles. A slicer needs it to describe a closed volume — for every point in space, "inside" or "outside" has to be unambiguous. Three properties matter:





  • Watertight: no boundary edges. Every edge of every triangle should be shared by exactly one other triangle.


  • Manifold: no edge shared by more than two triangles. If three or more faces meet at an edge, "inside vs outside" stops being well-defined there.


  • Consistent winding / single shell where expected: disconnected fragments (common in AI output — dozens of tiny floating shards) confuse a slicer's part detection.



You get all of this from one structure: an edge → face-count map.




CODE
function buildEdgeMap(indexArr: ArrayLike<number>) {
const edgeMap = new Map<string, { count: number; faces: number[] }>();
const faceCount = indexArr.length / 3;
for (let f = 0; f < faceCount; f++) {
const [a, b, c] = [indexArr[f * 3], indexArr[f * 3 + 1], indexArr[f * 3 + 2]];
for (const [v1, v2] of [[a, b], [b, c], [c, a]] as const) {
const key = v1 < v2 ? `${v1}_${v2}` : `${v2}_${v1}`;
const info = edgeMap.get(key) ?? { count: 0, faces: [] };
info.count++;
info.faces.push(f);
edgeMap.set(key, info);
}
}
return edgeMap;
}






Count == 1 → boundary edge (a hole). Count > 2 → non-manifold. Count == 2 → fine. Run a union-find over faces connected by manifold edges and you get shell count for free — which doubles as an AI-slop detector: a model with 40+ disconnected components is almost never a real 40-part assembly, it's floating debris from generation noise.



One catch that eats a surprising amount of debugging time: STL doesn't share vertices. Every triangle stores three fresh corner points, so two triangles that meet at an edge in space have geometrically-identical-but-distinct vertex indices. Run edge analysis on raw STL data and everything looks like a boundary edge, because nothing is technically shared. You have to weld coincident vertices first, within a tolerance — and the tolerance has to scale with the model, not be a fixed epsilon:




CODE
const WELD_TOLERANCE_RATIO = 2e-5; // relative to bbox diagonal
const tolerance = Math.max(diag * WELD_TOLERANCE_RATIO, 1e-6);






A fixed epsilon either welds legitimate close-but-distinct vertices on a tiny model, or fails to weld numerically-fuzzy duplicates on a large one. Scaling to the bounding-box diagonal fixes both.






Tier 1: light repair, entirely in the browser



MeshRefinery's free tool runs this in-browser with three.js, no upload required — the file never leaves the client. The pipeline, in order:





  1. Weld coincident vertices (above).


  2. Drop degenerate faces — zero-area triangles, detected via the cross-product area test. Harmless but they pollute every downstream step.


  3. Remove floating fragments — union-find shell detection, drop any shell under ~0.5% of total face count. This is the AI-slop cleanup: keep the model, discard the noise.

  4. Fan-fill small holes.



Hole filling is the fiddly part. You can't just connect arbitrary boundary vertices — you need the loop, walked in the winding order the surrounding surface already uses, so the new triangles come out facing the right way. The trick: build a directed boundary-edge map instead of an undirected one. Each boundary edge belongs to exactly one face, so record it in that face's winding direction; the resulting map is a set of v1 -> v2 pointers that chain into a loop automatically:




CODE
function traceLoops(nextMap: Map<number, number>, maxLoopSize: number) {
const visited = new Set<number>();
const loops: number[][] = [];
for (const start of nextMap.keys()) {
if (visited.has(start)) continue;
const loop = [start];
visited.add(start);
let cur = nextMap.get(start);
while (cur !== undefined && cur !== start) {
loop.push(cur);
visited.add(cur);
cur = nextMap.get(cur);
}
if (cur === start && loop.length >= 3 && loop.length <= maxLoopSize) loops.push(loop);
}
return loops;
}






Each closed loop under the size cap gets a centroid vertex and a triangle fan. Cheap, and it's honestly all you need for the vast majority of "AI mesh has a few small gaps" cases.



It is explicitly not a general watertight guarantee. Fan-filling from a centroid works for small, roughly-planar loops. It does nothing useful for a hole with saddle topology, or one large enough that a flat fan would self-intersect, or a mesh that's non-manifold in ways no amount of hole-filling touches. Being honest about that boundary in the product copy — "this cleans up the common stuff, it will not rescue a genuinely broken mesh" — mattered more to user trust than we expected going in.






Tier 2: when fan-filling isn't enough



For anything harder, the paid path throws away the fan-fill approach entirely and rebuilds the surface from a signed distance field:





  1. Generalized winding number (via libigl) to classify points as inside/outside — this is the part that makes the whole approach robust to non-manifold and disconnected input; unlike ray casting, it degrades gracefully instead of returning garbage on bad topology.

  2. Evaluate that at every point on a voxel grid, turn it into a signed distance field (Euclidean distance transform, outside − inside), Gaussian-smooth it.


  3. Marching cubes over the SDF's zero-isosurface produces a brand-new, watertight, manifold mesh — by construction, not by patching.

  4. Drop any voxel-noise fragments under the same 0.5% face-count threshold as before, fix normals, and run a few iterations of Taubin smoothing to knock down the voxel staircasing without shrinking the volume the way naive Laplacian smoothing would.



The tradeoff is real: you lose the original vertex positions and any UV/vertex-color data, because you have an entirely new mesh. So the last step is transferring color back — for every new vertex, find the nearest point on the original surface, get its barycentric coordinates within that triangle, and interpolate the original triangle's vertex colors:




CODE
closest, dist, tri_id = trimesh.proximity.closest_point(src, dst.vertices)
bary = trimesh.triangles.points_to_barycentric(src.triangles[tri_id], closest)
colors = (src_colors[src.faces[tri_id]] * bary[:, :, None]).sum(axis=1)






And because "trust me, it's fine" isn't a great answer for something as lossy-sounding as a full SDF rebuild, the API also reports fidelity: sample points on the new surface, measure distance back to the original, express it as a percentage of the model's bounding-box diagonal. Mean error is typically well under 1% of diagonal — small enough not to matter for FDM printing, and now a number instead of a vibe.






A repair-adjacent problem: color survives the trip too



This one isn't repair, it's format translation, but it hits the same "the naive version is wrong" pattern. GLB/glTF store color as a PBR base-color texture, not per-vertex color — so a plain geometry load silently drops all color. To carry it into a vertex-colored format, bake the texture: sample the base-color image at each vertex's UV coordinate.




CODE
const px = Math.min(w - 1, Math.max(0, Math.floor(u * w)));
const py = Math.min(h - 1, Math.max(0, Math.floor(v * h)));
const p = (py * w + px) * 4;
colors[i * 3] = data[p] / 255; // R
colors[i * 3 + 1] = data[p + 1] / 255; // G
colors[i * 3 + 2] = data[p + 2] / 255; // B






One color-space gotcha buried in there: baseColorTexture is sRGB-encoded and 3MF colors are sRGB hex, so the raw getImageData bytes map straight through with zero conversion — but three.js's flat material.color is stored linear, so that path needs an explicit convertLinearToSRGB() or flat-colored meshes come out visibly wrong while textured ones look fine. Easy to miss because it only breaks one of the two code paths.



3MF itself is worth a mention: it's just a zip (an OPC package, the same container format DOCX/XLSX use), holding three tiny XML parts — a content-types manifest, a relationships file, and the actual model. Colors go in an <m:colorgroup>, deduped into a palette and referenced per-vertex-per-triangle — that's the exact structure Bambu Studio, PrusaSlicer, and Cura read to drive full-color/AMS printing.






Where this leaves things



Two tiers, two very different engineering budgets: a fast, honest, client-side pass that fixes the common cases for free with zero upload, and a heavier server-side SDF rebuild for the cases that actually need a real watertight guarantee — with a measured fidelity number instead of a promise. If you're dealing with broken meshes yourself, both are free to try.

Vollständiger Original-Bericht
Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
1 Quelle
Samsung Taps Mistral AI for On-Premises Chip Manufacturing
1 Quelle
CISA’s ChatGPT Incident Exposes a Bigger AI Governance Problem
1 Quelle
Beware — these new phishing attacks use a convincing fake Adobe Reader pages to trick victims into installing malware
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Diagnosing a Broken STL File: Union-Find, Boundary Loops, and SDF Voxel Rebuilds

Thematisch verwandte Begriffe: Diagnosing, Broken, File, UnionFind · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...