flutter3d
Showcase Changelog 38 packages API reference

STL: the source

since 0.7.0 Formats and I/O

Regions the guide quotes: source (line 18), load (line 55), write (line 62), report (line 79), check (line 111).

1/// Reading ASCII STL and writing it back as the binary form.
2///
3/// Quoted by `stl.md` and shown whole in the Source tab.
4library;
5
6import 'dart:convert';
7import 'dart:typed_data';
8
9import 'package:flutter/widgets.dart';
10import 'package:flutter3d/flutter3d.dart';
11import 'package:flutter3d_showcase/src/demo/demo.dart';
12import 'package:vector_math/vector_math.dart';
14final class StlDemo extends ShowcaseDemo {
15 late final ModelDocument _decoded;
16 late final Uint8List _binary;
18 // STL has no shared vertices: each of its four facets repeats its own
19 // three corners in full, with the facet's own normal ahead of them.
20 static const String _sourceStl = '''
21solid tetra
22 facet normal 0 0 0
23 outer loop
24 vertex 0 1 0
25 vertex 1 -1 1
26 vertex -1 -1 1
27 endloop
28 endfacet
29 facet normal 0 0 0
30 outer loop
31 vertex 0 1 0
32 vertex -1 -1 1
33 vertex 0 -1 -1.4
34 endloop
35 endfacet
36 facet normal 0 0 0
37 outer loop
38 vertex 0 1 0
39 vertex 0 -1 -1.4
40 vertex 1 -1 1
41 endloop
42 endfacet
43 facet normal 0 0 0
44 outer loop
45 vertex 1 -1 1
46 vertex 0 -1 -1.4
47 vertex -1 -1 1
48 endloop
49 endfacet
50endsolid tetra
51''';
53 @override
54 Future<void> prepare(DemoContext context) async {
55 // Every facet's own normal is the zero vector here, on purpose: a
56 // surprising number of real exporters write exactly that, and
57 // `StlNormals.fromFile`, the default, falls back to the triangle's own
58 // cross product rather than trusting a degenerate normal.
59 final bytes = Uint8List.fromList(utf8.encode(_sourceStl));
60 _decoded = await StlLoader().load(bytes);
62 _binary = StlWriter(_decoded, name: 'tetra').write();
63 }
65 @override
66 Scene build(DemoContext context) => Scene()
67 ..add(
68 MeshNode(
69 DeviceMesh.upload(context.device, _decoded.surfaces.single.mesh),
70 Material(baseColor: Vector4(0.75, 0.75, 0.8, 1.0), roughness: 0.5),
71 name: 'tetra',
72 ),
73 )
74 ..add(
75 LightNode(name: 'sun', intensity: 3.0)
76 ..setLocalForward(Vector3(-0.4, -1.0, -0.3)),
77 );
79 String _report() {
80 final headerBytes = _binary.sublist(0, 20);
81 final header = String.fromCharCodes(headerBytes.where((byte) => byte != 0));
82 final facetCount = ByteData.sublistView(
83 _binary,
84 80,
85 84,
86 ).getUint32(0, Endian.little);
87 return 'source (ASCII) bytes: ${_sourceStl.length}\n'
88 'written (binary) bytes: ${_binary.length}\n'
89 'binary header starts: "$header"\n'
90 'facet count in binary header: $facetCount';
91 }
93 @override
94 Widget? customBody(BuildContext buildContext, DemoContext context) =>
95 Container(
96 color: const Color(0xFF14161A),
97 padding: const EdgeInsets.all(24),
98 alignment: Alignment.topLeft,
99 child: DefaultTextStyle(
100 style: const TextStyle(
101 color: Color(0xFFE8E8EC),
102 fontSize: 16,
103 fontFamily: 'monospace',
104 ),
105 child: Text(_report()),
106 ),
107 );
109 @override
110 void verify(Scene scene, FrameResult frame) {
111 final mesh = _decoded.surfaces.single.mesh;
112 if (mesh.triangleCount != 4) {
113 throw StateError('the tetrahedron did not decode to four facets');
114 }
115 // A facet whose file normal was degenerate must have been recomputed,
116 // never left at zero.
117 final nx = mesh.vertices[3];
118 final ny = mesh.vertices[4];
119 final nz = mesh.vertices[5];
120 if (nx == 0.0 && ny == 0.0 && nz == 0.0) {
121 throw StateError('a degenerate normal was not recomputed');
122 }
123 if (_binary.length != 84 + 50 * 4) {
124 throw StateError('the binary file is not the size four facets makes it');
125 }
126 if (frame.drawCalls < 1) {
127 throw StateError('the tetrahedron was not drawn');
128 }
129 }