Export#
Turning a solved solid into buffers a renderer can upload, or a file a slicer can read.
import { toRenderMesh, toBinarySTL } from 'forma-dsl';
Both take a Solid - the concrete field of a
rendered part - and both return plain typed arrays that outlive the
context, so they are what to copy out before disposing.
toRenderMesh#
toRenderMesh(solid)#
Converts a solid into flat arrays for a renderer.
| Parameter | solid: Solid |
| Returns | RenderMesh |
| Field | Type | Description |
|---|---|---|
positions |
Float32Array |
Three vertices per triangle, expanded - triangleCount * 9 floats |
normals |
Float32Array |
One face normal, repeated for all three corners |
triangleCount |
number |
Triangles |
vertexCount |
number |
Vertices before expansion, as the kernel counted them |
Why vertices are expanded#
The kernel returns indexed triangles sharing vertices, which is what a viewer wants for upload size. But a shared vertex across a hard edge gets one averaged normal and is smooth-shaded, so a cube reads as a sphere-ish blob. Normals are therefore computed per face and the vertices expanded, which is what keeps the edge hard.
The cost is that positions.length / 3 is triangleCount * 3 rather than vertexCount. Use
vertexCount only as a measure of the underlying mesh.
Caveats#
- A degenerate triangle keeps a zero normal rather than dividing by zero and poisoning the buffer with NaN.
renderalready calls this for every part -part.meshis the result. Call it yourself only for a solid you built directly.
toBinarySTL#
toBinarySTL(solid, header?)#
Encodes a solid as binary STL.
solid |
Solid |
header |
string, default 'forma-dsl'. Free text for the 80-byte header, truncated to fit |
| Returns | Uint8Array - the file |
Binary rather than ASCII: a model of any size is several times smaller and every slicer reads it.
Caveats#
- STL has no units. Every consumer treats the numbers as millimetres, which is what the language's lengths are usually meant to be, but nothing records that.
- STL has no colour and no parts. A
part's colour and opacity do not survive. Export each part to its own file if they need to stay distinct. - The header is truncated to 79 bytes so it cannot run into the triangle count that follows.
- Float32 throughout, as the format requires. A model with features far from the origin loses precision.
Usage#
Exporting one part#
import { render, toBinarySTL } from 'forma-dsl';
import { writeFile } from 'node:fs/promises';
const result = await render(source);
await writeFile('part.stl', toBinarySTL(result.parts[0].concrete));
result.context.dispose();
Exporting every part separately#
Colours do not survive STL, so one file per part is how an assembly stays an assembly:
const result = await render(source);
for (const part of result.parts) {
await writeFile(`${part.name}.stl`, toBinarySTL(part.concrete, `${result.name}/${part.name}`));
}
result.context.dispose();
The header is a reasonable place to record where the file came from.
Downloading from a browser#
const stl = toBinarySTL(part.concrete, part.name);
const url = URL.createObjectURL(new Blob([stl], { type: 'model/stl' }));
const a = document.createElement('a');
a.href = url;
a.download = `${part.name}.stl`;
a.click();
URL.revokeObjectURL(url);
Uploading to a renderer#
render has already produced the mesh, so three.js needs no conversion:
import { BufferGeometry, BufferAttribute, Mesh, MeshStandardMaterial } from 'three';
function toThree(part) {
const geometry = new BufferGeometry();
geometry.setAttribute('position', new BufferAttribute(part.mesh.positions, 3));
geometry.setAttribute('normal', new BufferAttribute(part.mesh.normals, 3));
return new Mesh(geometry, new MeshStandardMaterial({
color: part.color,
transparent: part.opacity < 1,
opacity: part.opacity,
}));
}
Keeping geometry past the context#
Typed arrays are plain memory, so they survive disposal - unlike part.concrete:
const result = await render(source);
const saved = result.parts.map((part) => ({
name: part.name,
color: part.color,
stl: toBinarySTL(part.concrete), // copied out…
}));
result.context.dispose(); // …so this is safe
Troubleshooting#
EvaluationContext has been disposed#
The solid was freed before it was encoded. Export first, dispose after - or copy the arrays out, which is what the example above does.
The STL is empty, or has zero triangles#
The part solved to empty geometry. Usual causes: a difference that removed everything, an
extrude whose height reached 0, a revolve whose angle reached 0. All of these are
silent by design, so a parameter at the end of its range does not break a model.
My part came in at the wrong scale#
STL carries no units. The slicer is assuming millimetres; if the model was written in centimetres, scale it in the document.
Colours are missing from the exported file#
STL has none. Export parts separately and colour them in the slicer, or use the render mesh directly if you control the viewer.
Hard edges look smooth in my renderer#
The mesh is already expanded with per-face normals, so this is the renderer recomputing
them. Upload mesh.normals as given and do not call anything like
computeVertexNormals().