STL
Open the live demo · Read the source · View on GitHub
STL is the format a 3D printer's slicer reads: a flat list of triangles, no shared vertices, no materials, no hierarchy. This page reads a small ASCII file and writes the binary form back out.
Step 1: A file whose facets do not trust their own normals #
Every facet in STL carries its own normal, ahead of its three vertices. This one sets every normal to zero on purpose, which is what a surprising number of real exporters actually write.
// STL has no shared vertices: each of its four facets repeats its own
// three corners in full, with the facet's own normal ahead of them.
static const String _sourceStl = '''
solid tetra
facet normal 0 0 0
outer loop
vertex 0 1 0
vertex 1 -1 1
vertex -1 -1 1
endloop
endfacet
facet normal 0 0 0
outer loop
vertex 0 1 0
vertex -1 -1 1
vertex 0 -1 -1.4
endloop
endfacet
facet normal 0 0 0
outer loop
vertex 0 1 0
vertex 0 -1 -1.4
vertex 1 -1 1
endloop
endfacet
facet normal 0 0 0
outer loop
vertex 1 -1 1
vertex 0 -1 -1.4
vertex -1 -1 1
endloop
endfacet
endsolid tetra
''';
Step 2: Decode it #
StlLoader reads both dialects: it tells binary from ASCII by the file's
own size arithmetic, not by whether the text starts with solid, because
a binary file's free-text header often starts with that word too.
StlNormals.fromFile, the default, falls back to the triangle's own cross
product whenever the file's normal is the zero vector.
// Every facet's own normal is the zero vector here, on purpose: a
// surprising number of real exporters write exactly that, and
// `StlNormals.fromFile`, the default, falls back to the triangle's own
// cross product rather than trusting a degenerate normal.
final bytes = Uint8List.fromList(utf8.encode(_sourceStl));
_decoded = await StlLoader().load(bytes);
Step 3: Write it back as binary #
StlWriter bakes every surface's transform into its positions, since STL
has no per-object placement to carry it in, and writes one flat facet
list either as compact binary or as text.
_binary = StlWriter(_decoded, name: 'tetra').write();
Step 4: Compare the two files #
The ASCII source and the binary output describe the same four triangles in very different numbers of bytes.
String _report() {
final headerBytes = _binary.sublist(0, 20);
final header = String.fromCharCodes(headerBytes.where((byte) => byte != 0));
final facetCount = ByteData.sublistView(
_binary,
80,
84,
).getUint32(0, Endian.little);
return 'source (ASCII) bytes: ${_sourceStl.length}\n'
'written (binary) bytes: ${_binary.length}\n'
'binary header starts: "$header"\n'
'facet count in binary header: $facetCount';
}
Step 5: Check the claim #
Four facets in, four facets out, every normal actually pointing somewhere, and a binary file exactly the size its own header formula says four facets should be.
final mesh = _decoded.surfaces.single.mesh;
if (mesh.triangleCount != 4) {
throw StateError('the tetrahedron did not decode to four facets');
}
// A facet whose file normal was degenerate must have been recomputed,
// never left at zero.
final nx = mesh.vertices[3];
final ny = mesh.vertices[4];
final nz = mesh.vertices[5];
if (nx == 0.0 && ny == 0.0 && nz == 0.0) {
throw StateError('a degenerate normal was not recomputed');
}
if (_binary.length != 84 + 50 * 4) {
throw StateError('the binary file is not the size four facets makes it');
}
if (frame.drawCalls < 1) {
throw StateError('the tetrahedron was not drawn');
}