A decoder of your own: the source
Open the live demo · Read the guide · View on GitHub
Regions the guide quotes: decoder (line 13), request (line 74), decode (line 82), check (line 108).
1/// A decoder of your own, tried before every format this engine ships.
2///
3/// Quoted by `custom_decoder.md` and shown whole in the Source tab.
4library;
6import 'dart:convert';
7import 'dart:typed_data';
9import 'package:flutter3d/flutter3d.dart';
10import 'package:flutter3d_showcase/src/demo/demo.dart';
11import 'package:vector_math/vector_math.dart';
13/// A made-up format this engine has never heard of: one line, three
14/// numbers, a box of that size. `ModelDecoder` is the whole plugin
15/// boundary — a project's own studio format arrives exactly this way.
16final class _BoxTextDecoder implements ModelDecoder {
17 const _BoxTextDecoder();
20 bool handles(String fileName, Uint8List bytes) =>
21 fileName.toLowerCase().endsWith('.box');
24 Future<ModelDocument> decode(
25 Uint8List bytes,
26 ModelLoadRequest request,
27 AssetUriResolver resolveUri,
28 ) async {
29 final parts = utf8.decode(bytes).trim().split(RegExp(r'\s+'));
30 if (parts.length != 3) {
31 throw const FormatException('a .box file is three numbers: w h d');
32 }
33 final size = Vector3(
34 double.parse(parts[0]),
35 double.parse(parts[1]),
36 double.parse(parts[2]),
37 );
38 return PlainModelDocument(
39 surfaces: <ModelSurface>[
40 ModelSurface(mesh: CuboidShape(size: size).build()),
41 ],
42 nodes: <ModelNode>[
43 ModelNode(surfaces: <int>[0]),
44 ],
45 );
46 }
47}
49final class _BytesSource extends AssetSource {
50 const _BytesSource(this._bytes, this._fileName);
52 final Uint8List _bytes;
53 final String _fileName;
56 String get key => 'bytes:$_fileName';
59 String get fileName => _fileName;
62 Future<Uint8List> read() async => _bytes;
65 AssetUriResolver get resolveUri =>
66 (request) async => throw StateError('a .box file has no siblings');
67}
69final class CustomDecoderDemo extends ShowcaseDemo {
70 late final ModelDocument _decoded;
73 Future<void> prepare(DemoContext context) async {
74 // A byte-identical file this engine's own decoders would refuse:
75 // three plain numbers, no glTF, no OBJ header, nothing recognisable.
76 final bytes = Uint8List.fromList(utf8.encode('1.4 0.6 2.0'));
77 final request = ModelLoadRequest(
78 source: _BytesSource(bytes, 'crate.box'),
79 decoders: const <ModelDecoder>[_BoxTextDecoder()],
80 );
82 // An application's own decoders are tried before this package's
83 // built-in ones, by file name and by magic — so a project can also
84 // replace a built-in reader, not only add to it.
85 _decoded = await decodeModelBytes(
86 request,
87 bytes,
88 request.source.resolveUri,
89 );
90 }
93 Scene build(DemoContext context) => Scene()
94 ..add(
95 MeshNode(
96 DeviceMesh.upload(context.device, _decoded.surfaces.single.mesh),
97 Material(baseColor: Vector4(0.7, 0.55, 0.35, 1.0), roughness: 0.7),
98 name: 'crate',
99 ),
100 )
101 ..add(
102 LightNode(name: 'sun', intensity: 3.0)
103 ..setLocalForward(Vector3(-0.4, -1.0, -0.3)),
104 );
107 void verify(Scene scene, FrameResult frame) {
108 if (_decoded.surfaces.length != 1) {
109 throw StateError('the custom decoder did not produce one surface');
110 }
111 final bounds = _decoded.computeBounds();
112 final width = bounds.max.x - bounds.min.x;
113 if ((width - 1.4).abs() > 1e-6) {
114 throw StateError('the box did not come out the size the file named');
115 }
116 if (frame.drawCalls < 1) {
117 throw StateError('the crate was not drawn');
118 }
119 }
120}