Export, read back, compare
Open the live demo · Read the source · View on GitHub
A writer that draws a nice picture in a viewer proves nothing about
whether it round-trips. exportChecked writes a document, reads the
result straight back through the matching loader, and reports what
changed. This page runs it against three formats at once.
Step 1: Write, read back, and compare #
exportToGlb, exportToObj and exportToStl are all the same function,
exportChecked, with a writer already chosen. Each answers an
ExportReport: the files it wrote, what the writer already knew it could
not carry, and what compareModelDocuments found reading the file back.
// `exportChecked` writes, reads the result back through the same writer
// and compares. A binary format like GLB is held to a tolerance of
// `0.0`; OBJ, a decimal text format, rounds on the way out by design and
// is checked against `ObjWriter.decimals` own precision instead.
_glb = await exportToGlb(_document);
_obj = await exportToObj(_document);
_stl = await exportToStl(_document);
Note. A clean report with warnings in it is a writer working as documented, dropping only what it already said it would. Warnings with more differences than they explain is a writer with a real bug.
Step 2: Check the file's own bookkeeping #
Reading a file back proves the geometry survived, but a glTF accessor also
declares its own min/max, and a reader trusts that declaration rather
than recomputing it from the data. validateGltfExport checks the one
thing every reader silently relies on a writer to get right.
// `compareModelDocuments` proves the geometry round-tripped; it says
// nothing about whether the file's own declared bounds match the data,
// which is a writer's own bookkeeping and not part of a document at
// all. `validateGltfExport` checks that separately, straight off the
// bytes.
_boundsProblems = await validateGltfExport(_glb.files.values.first);
Step 3: Put the numbers where they can be read #
Three formats, three reports. The page prints each one as a short line: clean or not, how many warnings, how many differences.
String _line(String name, ExportReport report) =>
'$name: ${report.isClean ? 'clean' : 'not clean'}, '
'${report.writerWarnings.length} warning(s), '
'${report.differences.length} difference(s)';
String _report() =>
'${_line('glb', _glb)}\n'
'${_line('obj', _obj)}\n'
'${_line('stl', _stl)}\n'
'accessor bounds: ${_boundsProblems.isEmpty ? 'match the data' : _boundsProblems.join('\n')}';
Step 4: Check the claim this page makes #
A cube is about as simple as a document gets, so this page's own claim is strict: the GLB round trip has to be clean, and its declared bounds have to match its data.
if (!_glb.isClean) {
throw StateError('the GLB round trip was not clean: $_glb');
}
if (_boundsProblems.isNotEmpty) {
throw StateError('the GLB declared the wrong bounds: $_boundsProblems');
}
if (frame.drawCalls < 1) {
throw StateError('the cube was not drawn');
}