3MF#
.3mf read + write eager
Summary of the specification#
3MF - the 3D Manufacturing Format of the 3MF Consortium - is the file a slicer takes in place of an STL. It is a ZIP package in the Open Packaging Conventions layout: [Content_Types].xml names the part types, _rels/.rels points at the model part, and the model part - 3D/3dmodel.model by convention - is one XML document. Its <resources> hold <object> elements, each either a <mesh> of <vertex x y z> and <triangle v1 v2 v3> entries or a <components> assembly of other objects placed under a transform; <basematerials> names the materials and their display colours. The <build> says which objects are printed and where, one <item objectid transform> each. The model carries a unit - millimetres unless it says otherwise - and a handful of named <metadata> entries. Every slicer reads it, Windows and macOS preview it, and Cura, PrusaSlicer and Bambu Studio save their projects in it.
Specification at a glance#
container |
ZIP; |
model part |
|
geometry |
|
assemblies |
|
transform |
twelve numbers, a 4x3 affine matrix in row-vector order; the last three are the translation |
materials |
|
build |
|
units |
|
Reading#
import polyxios as px
mesh = px.read("part.3mf")
mesh.element_tags # one group per built object
mesh.element_attrs["colors"] # RGBA per triangle, NaN where none
mesh.global_attrs["unit"] # "millimeter"
The build is read the way a printer would see it: every item’s object, a mesh outright or an assembly through its components, with each transform composed on the way down and applied to the vertices. A transform that mirrors turns each triangle inside out, so the winding of a mirrored placement is reversed to keep the surface facing outward. Each mesh object’s triangles become an element tag group named by the object’s name, or object_<id> when it has none; an object placed twice - two components of one part - contributes twice under the one name. An object the build never places is not read.
A triangle’s material is its colour: a pid/p1 naming a <basematerials> entry or a materials-extension <colorgroup> entry reads into element_attrs["colors"] as RGBA in 0..1, the object’s pid/pindex standing in for a triangle that names none, NaN on the triangles with no colour at all. A texture, composite or multi-property is not a colour and reads as NaN. The model’s unit and every <metadata> entry land in global_attrs under their own names. lazy=True raises LazyReadError: a ZIP member has to be inflated before it can be parsed.
The model part is found through _rels/.rels; a package without one is read through the conventional 3D/3dmodel.model. A file that is not a ZIP, a package with no model part, a triangle naming a vertex the object does not hold, an item naming an object the model does not hold, an object that contains itself, and a transform that is not twelve numbers each raise CodecError naming the fault.
Writing#
px.write(mesh, "out.3mf")
px.write(mesh, "out.3mf", unit="inch")
3MF holds triangles, so the writer works in them: a triangle goes out as it is, a quad, pixel, polygon or triangle strip is split into triangles, and a quadratic surface element keeps its corners. Lines, vertices and volume elements have no triangle to be written as and are dropped with a warning; a mesh with elements and no surface at all is refused. Every element tag group goes out as an <object> of its name, in the order of the groups, and the triangles no group claims as one last unnamed object. A 3MF object owns its triangles, so an element in two groups stays with the first and the second is told so; each object also carries its own vertex list, so a vertex two objects share is written to both. element_attrs["colors"] - three or four columns, 0..1 as floats or 0..255 as integers - becomes one <basematerials> with a <base> per distinct colour, each triangle naming its own; a NaN row names none. The unit is the unit= argument, else global_attrs["unit"], else millimetres; a mesh unit the format lacks - mm from another reader, say - is written as millimetres with a warning, while a unit= the format lacks is refused. A global attribute named as the specification names its metadata - Title, Designer, Description, Copyright, LicenseTerms, Rating, CreationDate, ModificationDate, Application - is written as <metadata>. Every other attribute has no place in the format and is not written. Two writes of one mesh are byte-identical: the ZIP entries carry a fixed timestamp.
Format-specific options#
Option |
Default |
Effect |
|---|---|---|
|
|
On write: the model’s unit of length, one of |
Quirks worth knowing#
Objects come back as tags. A mesh written with no tags reads back with one group,
object_1, because the file holds an object and the reader reports what the file holds. A mesh whose tags partition it goes out under those names and comes back under them.Transforms are applied, not kept. A part placed three times reads as three copies of its triangles; the placements are not recoverable from what comes back, and a rewrite holds three objects’ worth of triangles in one.
One colour per triangle. 3MF lets a triangle name a property per corner (
p1,p2,p3); onlyp1is read, since a colour per corner has no element to land on. The material’s name is not kept, only its colour.Coordinates are written in full. Each coordinate is written as the shortest decimal that reads back to the same double, so a round trip through the file changes nothing.
Buffers. A file object is read and written like a path: the whole package is read into memory, and a
.gzname is compressed on the way out and read on the way in like any other format.The model part is parsed as a stream. It is fed to the parser as it inflates, and each
<vertex>and<triangle>goes straight into a numeric buffer rather than into the tree, so a mesh of ten million vertices - two gigabytes of XML - reads in the memory its numbers take, not thirty times that. Inflated whole, a part past a gigabyte is more than the XML parser will take in one buffer.The materials and production extensions are read only as far as
<colorgroup>; a beam lattice, a slice stack or a texture is not geometry the reader returns.
See also
Supported formats - the full format table.