flutter3d
Showcase Changelog 38 packages API reference

STL

since 0.7.0 Formats and I/O

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');
}