* Add craft-mesh: neutral mesh model with STL, OBJ/MTL and PLY * craft-mesh: glTF 2.0 and GLB * craft-mesh: README, crate list entry, a test for the UV set bound
craft-mesh
3D mesh file formats behind one neutral model. A reader turns bytes into a Scene; a writer turns
a Scene into bytes. One Cargo feature per format, all on by default.
| Feature | Format | Read | Write | Dependency |
|---|---|---|---|---|
stl |
STL, binary and ASCII | yes | binary and ASCII | none |
obj |
Wavefront OBJ with MTL | yes | OBJ and MTL | none |
ply |
PLY, ASCII and both binary byte orders | yes | no (no app writes PLY) | none |
gltf |
glTF 2.0 (.gltf) and GLB |
yes | GLB | serde_json |
FBX, USD, Alembic, COLLADA and 3MF are not here yet.
The neutral model
Scene:meshes,materials,images,nodeswithroots,skins,animations,cameras,lights, plusexternal(every other file the file names) andwarnings(what was left out or repaired).Mesh: a name, attribute pools (positionsandnormalsasf64,uv_sets, per-positioncolors,jointsandweights) andparts.Part: the faces of a mesh that share a group name, a material and a smoothing group. A face is a polygon of three or moreCorners; each corner indexes a position and optionally a normal and a UV. Formats with separate indices (OBJ) are kept exactly, and polygons stay whole.Part::triangles()fans them;Mesh::unified()gives one vertex per distinct corner as a triangle list, which is what a renderer wants.Material: metallic-roughness factors, alpha mode, double-sided, unlit, and five texture slots. ATextureRefnames anImage, a UV set and wrap modes. AnImageis a URI the caller resolves or the encoded bytes the file embedded; nothing here decodes images.Node: a name, a column-major 4×4transform(and the same asTrswhen the file gave it in parts), optional mesh, skin, camera and light, and children. Every reader fillsnodes: a format without a hierarchy gets one root node per mesh.Scene::instances()walks the hierarchy and returns every placed mesh with its world transform.
Conventions, whatever the file:
- Positions are in the file's own axes and units. Nothing is turned or scaled (STL and PLY files are usually Z-up, glTF is Y-up in metres).
- UVs have their origin at the top left, v down (glTF). The OBJ and PLY readers and the OBJ writer flip v.
- Material colours are linear. MTL and STL colours are display colours and are converted. Vertex colours are the stored values scaled to 0..1.
- Missing normals stay missing. STL facets get the normal of their winding.
Coverage
STL. Binary and ASCII, told apart by the exact binary size first, then by solid with a
facet or endsolid. Binary headers that begin with solid, trailing bytes, a facet count left
at 0, several ASCII solids, ASCII loops of more than three vertices, the Magics (COLOR= header)
and VisCAM/SolidView per-facet colours (a material and a part per colour). ASCII coordinates keep
f64 precision. Facets with a non-finite coordinate are left out with a warning. Writers apply
the node transforms and write one solid; colours are not written.
OBJ and MTL. v with optional r g b, vt, vn, f with all four reference forms and
negative references, polygons of any size, o (one mesh each, holding only its own vertices in
file order), g, usemtl, s, mtllib, \ continuations, # comments. A vertex or face line
that cannot be read is left out and counted in one warning; later vertices keep their numbers.
MTL: Kd, d, Tr, Ke, Ns, Pr, Pm, map_Kd, map_Ke, map_Bump/bump/norm. The
writer writes objects, vertex colours, the first UV set, normals, groups, materials, smoothing
groups and whole polygons, and an MTL library with unique material names. Lines, points and
free-form geometry are passed over.
PLY. ASCII, little- and big-endian binary; \n, \r\n and \r header lines; elements in
any order; scalar and list properties of every type. From the vertex element: x y z,
nx ny nz, s t / u v / texture_u texture_v, red green blue alpha (also r g b a and
diffuse_*), integer colours scaled by their type. From the face element: vertex_indices or
vertex_index, polygons kept whole. A file without faces is a point cloud (positions, no parts).
ply::parse_header and ply::Rows are public for callers that need every property as a column,
edge elements or thinning.
glTF and GLB. The default scene's node tree (matrix or TRS); TRIANGLES, TRIANGLE_STRIP and
TRIANGLE_FAN, indexed or not; POSITION, NORMAL, every TEXCOORD_n (up to 8), COLOR_0, JOINTS_0,
WEIGHTS_0 in any component type the specification or KHR_mesh_quantization allows; strided
buffer views; accessors without a buffer view (zeros); materials with their texture slots, UV set
and sampler wrap modes, KHR_materials_emissive_strength, KHR_materials_unlit; images embedded
in a buffer view or a data: URI, or named; skins; translation, rotation and scale animations;
cameras; KHR_lights_punctual. Primitives of a mesh that name the same accessors share their
vertices. write_glb writes nodes, meshes (positions, normals, UV sets, colours), materials and
images.
Not read: sparse accessors and required geometry extensions (Draco, meshopt) are
Error::Unsupported; points, lines, morph targets, tangents and other extensions are passed over.
Not written: skins, animations, cameras, lights, and .gltf with separate files.
Hostile input
Every reader takes Limits and returns Error::TooLarge past one. A count is checked against the
bytes that are there before anything is allocated from it. Nothing indexes with a value from the
file unchecked. Each limit has a test, and every reader is run over all truncations and two
mutations of every byte of good files.
| Limit | Default | Applies to |
|---|---|---|
max_vertices |
20 000 000 | entries per attribute pool (OBJ: each of v, vt, vn, counting unused ones; glTF: vertices over all meshes) |
max_triangles |
20 000 000 | triangles after fanning polygons, strips and fans |
max_face_corners |
4096 | corners of one face; PLY list lengths |
max_meshes |
100 000 | OBJ objects, glTF meshes |
max_parts |
100 000 | groups, material runs, glTF primitives |
max_materials |
100 000 | materials; images; OBJ material libraries |
max_nodes |
100 000 | nodes, and node visits when the hierarchy is walked (ends cycles) |
max_depth |
64 | node nesting |
max_data_bytes |
1 GiB | buffers and material libraries taken from the resolver; embedded image bytes; skin and animation values; the binary chunk write_glb builds |
max_extras |
10 000 | skins, animations, cameras, lights, buffers (each); channels of one animation |
Fixed bounds: a PLY header of 1 MiB, 64 elements and 256 properties per element; 2^24 components for glTF accessors without a buffer view, over the whole file.
External files. No reader opens a file. A reader that needs another file to parse (an OBJ
material library, a glTF buffer) asks the caller's Resolver for it by the name the file writes;
the resolver may decline (None). Images are never asked for: they are named in Image::uri.
Every name is listed in Scene::external. Names come from the file, so a resolver must not trust
them as paths: they may be absolute or contain ...
What stays in the apps
Mapping a Scene onto the app's mesh, body or document types; computing normals, tangents and
UVs; welding; unit scaling and up-axis turns; flattening the hierarchy into world space (use
Scene::instances()); decoding images; choosing where external files come from; sampling
animations and skinning; file-size limits before reading.
Origin
All code is from Craft apps (MIT OR Apache-2.0), written from the public format descriptions. No sample files are shipped: every test generates its input.
| Module | Base | Ported in |
|---|---|---|
stl reader |
EffectCraft crates/model/src/stl.rs (detection, colour extensions, degenerate facets) |
SolidCraft read_stl: a facet count left at 0, non-UTF-8 ASCII names. TextureCraft mesh_import/stl.rs: same detection, nothing extra. The Z-up to Y-up turn EffectCraft did stays in EffectCraft. |
stl writers |
SolidCraft stl_binary, stl_ascii |
Node transforms, a header that never starts with solid |
obj reader |
EffectCraft crates/model/src/obj.rs (own parser, MTL, continuations, library name candidates) |
SolidCraft obj.rs: checked references, objects holding only their vertices, bad lines counted not fatal, smoothing groups kept, pool caps. TextureCraft mesh_import/obj.rs: vertex colours, whole polygons, group names. MaterialCraft mesh.rs: bounds on unused v/vt/vn definitions. |
obj writer |
TextureCraft mesh_export.rs (obj_text, unique material names) |
SolidCraft obj_files: several objects with running indices, control characters in names |
ply |
SolidCraft ply.rs (header bounds, three line endings, size checks before reading) |
TextureCraft mesh_import/ply.rs: normals, UVs, alpha, whole polygons, faces before vertices |
gltf reader |
EffectCraft crates/model/src/gltf.rs (own reader over serde_json; skins, animations, cameras, lights) |
SolidCraft gltf_in.rs: stricter GLB chunk checks, the required-extension policy, hierarchy caps. TextureCraft mesh_import/gltf.rs: COLOR_0, sparse accessors refused instead of read wrong. MaterialCraft mesh.rs: a bad index drops its triangle and later ones keep their vertices. |
gltf writer |
SolidCraft glb.rs (glb_container, accessor layout) |
TextureCraft glb_bytes: UV sets, embedded images and texture slots |
TextureCraft, MaterialCraft and SolidCraft read OBJ and glTF through the tobj and gltf crates;
this crate does not depend on either.
Migration
No app uses this crate yet.
| App | Replace | With | Keep in the app |
|---|---|---|---|
| EffectCraft | crates/model/src/{stl,obj,gltf}.rs |
stl::read, obj::read, gltf::read |
Scene to Model; the STL Y-up turn; computed normals per smoothing group and tangents; image decoding and the sibling-name lookup for textures; animation sampling. FBX stays. |
| TextureCraft | mesh_import/{stl,obj,ply,gltf}.rs, obj_text, the geometry part of glb_bytes |
readers, obj::write, gltf::write_glb |
Scene to RawMesh (world space through instances()), unwrap, tangents, cameras to orbit cameras, texture baking. GLB clearcoat and sheen extensions are not in the model yet, so glb_bytes keeps them until then. Drops tobj and gltf. |
| MaterialCraft | crates/formats/src/mesh.rs obj and gltf |
obj::read, gltf::read with its 4M-triangle Limits |
Non-finite normal and UV rejection, box UVs, normalising to the unit sphere. Drops tobj and gltf. |
| SolidCraft | crates/io/src/{obj,ply}.rs, read_stl, stl_binary, stl_ascii, gltf_in.rs parsing, glb.rs container |
readers, ply::parse_header/Rows under ply_complex.rs, STL writers, gltf::write_glb |
Millimetre scaling, crease normals, mesh bodies, component plans from the node tree, point-cloud thinning and columns, looks. An OBJ without o now gives one mesh with a part per g; SolidCraft splits by part to keep one body per group. Drops gltf. |
| DeckCraft | nothing merged; open PRs #1380 and #743 each add a crates/mesh |
this crate | Rasterising |