Source:
src/mesh/README.md— this page is rendered directly from the file in the repository. Edit it there.
Mesh — Triangle Geometry, Loaded and Inspected
This module owns one thing: the in-memory triangle mesh that the rest of the engine slices. Everything starts with a Mesh, and every byte that comes in from disk — STL, OBJ, 3MF — funnels through here on the way to becoming one.
All coordinates are in millimeters. Z is up. There is no other convention.
Why it exists
Three distinct concerns share one truth: a soup of triangles with a bounding box.
- The slicer wants a flat list of triangles to intersect with horizontal planes — it doesn't care how they got there.
- The scene engine (../scene/) wants to clone meshes cheaply (via
Arc<Mesh>) and bake transforms once before slicing. - The loaders want a single target type to convert into, regardless of whether the source is binary STL, ASCII STL, OBJ, or a ZIP-wrapped 3MF.
mesh::types::Mesh is that single target. Loaders fill it; analysis reads it; transforms produce new copies of it; the slicer consumes it.
The contract
- Coordinates are mm; Z is up. Loaders convert from whatever the source uses (STL
f32, OBJ unspecified) tof64in this convention. Slicing, G-code, and the scene engine all assume it. - Transforms are pure.
translate_mesh,scale_mesh, and friends return a newMesh; the input is never mutated. The new mesh's cachedaabbis cleared so it is recomputed on next access. Facecarries its own vertex copies. The triangles are denormalised for slicing speed: eachFacestores its threeVertexvalues inline, not indices intoMesh::vertices. Theverticesarray exists for AABB computation; the slicer readsfaces.- Loaders never apply transforms. A loaded mesh is in the file's native coordinates. Centering, dropping to floor, rotating — all of that is the scene engine's job (../scene/ops.rs).
Anatomy
A few things worth knowing:
aabbis a cache. It isNoneafter construction or after a transform, and gets populated lazily byanalysis::calculate_aabb.Face::normalis what the file said. STL ASCII files often record zero vectors as normals; in those cases the field isNoneand the slicer recomputes orientation from the triangle winding when it needs to.- No half-edge / connectivity structure. This is deliberate — the slicer uses Clipper2 to chain segments after intersection, so we never need to know which triangles share which edges.
Analysis functions
analysis.rs provides three categories of mesh inspection:
| Function | Returns | Notes |
|---|---|---|
calculate_aabb | AABB | Scans all vertices; panics on empty mesh |
calculate_volume | Result<f64, String> | Divergence-theorem signed sum; returns Err if no faces |
calculate_surface_area | f64 | Sums Face::area() over all triangles |
compute_coplanar_groups | Vec<u32> (one id per face) | Union-find over shared edges; see below |
Coplanar face groups
compute_coplanar_groups(mesh, angle_threshold_deg, vertex_merge_distance_mm) assigns every triangle to a coplanar group. Two triangles end up in the same group when they share an edge and their geometric normals agree within angle_threshold_deg. The algorithm runs in three phases:
Normal computation. Each face gets a unit geometric normal (cross product, then normalised). Degenerate triangles (zero-length cross product) get the zero vector and never merge with anything.
Edge adjacency. Vertex positions are quantised to a
vertex_merge_distance_mmgrid so floating-point near-duplicates collapse to the same integer key. Every directed half-edge is then hashed to a symmetric key(min_vert, max_vert). Half-edges are sorted by key, giving an O(N log N) pass to collect all faces that share each edge.Union-find merge. For every set of faces that share an edge, each pair is tested: if
dot(normalA, normalB) ≥ cos(threshold), the two faces are joined. Path-halving and union-by-rank keep the structure nearly flat.
The returned Vec<u32> is contiguous — group ids start at 0 and are assigned in the order the first face of each group is encountered. The WASM bridge exposes this as SceneHandle.getFaceGroups(id, angleThresholdDeg), which the UI uses for face-highlight in the pullToFloor gizmo mode.
Tuning knobs
| Parameter | Recommended value | Effect |
|---|---|---|
angle_threshold_deg | 1.0° | Larger = merges slightly uneven surfaces |
vertex_merge_distance_mm | 0.001 mm | Larger = tolerates worse vertex welding |
File-format catalog
| Format | Variant | Loader | Notes |
|---|---|---|---|
| STL | Binary | io::read_stl | Via stl_io; fastest path, normals usually present |
| STL | ASCII | io::read_stl | Same entry point; stl_io auto-detects |
| OBJ | Wavefront | io::read_obj | Via tobj; vertex positions only, materials ignored |
| 3MF | XML-in-ZIP | io::read_3mf | Custom parse (zip + quick-xml); first object only |
SUPPORTED_EXTENSIONS lists the recognised file extensions for CLI / WS validation. The scene loader (../scene/loader.rs) dispatches on MeshFormat rather than re-sniffing extensions.
Role in the wider system
The mesh module never knows about scenes, but the scene module relies on this contract: cheap Arc<Mesh> clones, pure transforms, and a single AABB cache slot that transforms invalidate.
Lifecycle of a single mesh
- Load.
io::read_*(orscene::loader::load_bytes) parses the file into aMeshwithaabb: None. - Inspect.
analysis::calculate_aabb,calculate_volume,calculate_surface_areapopulate / report basic geometry. The first AABB call fills the cache. - Place. The scene engine wraps the mesh in
Arc<Mesh>and tracks aTransformalongside it. The mesh itself is not mutated by scene ops. - Bake. Just before
core::process_mesh,scene::transform::apply_transformproduces a newMeshwith the transform baked into the vertices, AABB cleared. - Slice.
core::slice_meshwalks the (now world-space) faces and emitsSliceLayers.
After step 5 the original Arc<Mesh> is still alive and unchanged in SceneState — re-slicing with a different transform reuses it.
What this module deliberately does not do
- No placement logic. Centering, dropping to floor, face alignment — all in ../scene/. The scene engine is the SSOT for "where is it"; this module is the SSOT for "what is it".
- No mesh repair. Holes, non-manifold edges, flipped normals — out of scope. The slicer assumes a watertight, consistently-wound mesh and the loaders pass through whatever the file contains.
- No format conversion.
read_stlproduces aMesh; there is nowrite_stl. Outputs are G-code, not meshes. - No connectivity graph. The slicer doesn't need it; building one would cost memory we don't have on wasm32.
See also
- types.rs —
Mesh,Face,Vertex,AABB - io.rs — STL / OBJ / 3MF loaders,
SUPPORTED_EXTENSIONS - analysis.rs — AABB, volume, surface area, coplanar face groups
- transforms.rs — pure translate / scale / rotate helpers
- ../scene/README.md — how meshes are placed in a scene
- ../SLICING.md — the triangle-plane intersection algorithm