Something went wrong. Try again.
atproto Thingiverse but good
Something went wrong. Try again.
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103//! OBJ ingestion: parse in-memory bytes into a [`ModelMesh`] via `three-d-asset`.//!//! `three-d-asset` parses Wavefront OBJ through `wavefront_obj`, which//! fan-triangulates convex polygons at parse time and drops Line/Point primitives;//! concave polygons are out of scope. OBJ carries no unit, so the loader assumes//! millimetres. There is no filesystem or network access — an `.mtl` material sidecar is//! not fetched (out of scope) — so this is safe to call from a `wasm32-unknown-unknown`//! build.
use three_d_asset::io;
use crate::mesh::contract::{MeshFormat, MeshLoadError, ModelMesh, Units};
/// Parse OBJ bytes into a [`ModelMesh`].////// Convex polygons are fan-triangulated; Line and Point primitives are dropped. OBJ is/// unitless, so the result assumes millimetres. Positions are normalized to F32 by the/// shared validator (OBJ arrives as F64).pub fn load_obj(bytes: &[u8]) -> Result<ModelMesh, MeshLoadError> { if bytes.is_empty() { return Err(MeshLoadError::Empty); } let model: three_d_asset::Model = io::deserialize("asset.obj", bytes.to_vec()) .map_err(|e| MeshLoadError::Decode(e.to_string()))?; let mut trimesh = crate::mesh::flatten_model(model); crate::mesh::validate(&mut trimesh)?; Ok(ModelMesh { trimesh, format: MeshFormat::Obj, units: Units::unitless_assumed_mm(), })}
#[cfg(test)]mod tests { use super::load_obj; use crate::mesh::contract::{LengthUnit, MeshFormat, MeshLoadError, Units}; use three_d_asset::Positions;
fn fixture(name: &str) -> Vec<u8> { let path = format!("{}/public/models/{name}", env!("CARGO_MANIFEST_DIR")); std::fs::read(&path) .unwrap_or_else(|_| panic!("OBJ fixture {name} should be committed at {path}")) }
#[test] fn parses_quad_cube_fixture() { let model = load_obj(&fixture("cube.obj")).expect("cube.obj should parse"); assert_eq!(model.format, MeshFormat::Obj); assert_eq!( model.units, Units { source: LengthUnit::Unknown, assumed: Some(LengthUnit::Millimeter), } ); // 6 convex quads fan-triangulate to 12 triangles. assert_eq!(model.trimesh.triangle_count(), 12); // OBJ's F64 positions are normalized to F32. assert!(matches!(model.trimesh.positions, Positions::F32(_))); let normals = model .trimesh .normals .as_ref() .expect("geometry normals should be populated"); assert_eq!(normals.len(), model.trimesh.positions.len()); assert!( normals .iter() .all(|n| n.x.is_finite() && n.y.is_finite() && n.z.is_finite()), "normals must be finite" ); }
#[test] fn error_empty() { assert!(matches!(load_obj(&[]), Err(MeshLoadError::Empty))); }
#[test] fn error_parse_garbage() { let garbage = b"this is definitely not a valid OBJ file !@#$"; assert!(matches!(load_obj(garbage), Err(MeshLoadError::Decode(_)))); }
#[test] fn error_zero_triangles() { // Vertices but no faces: parses to an empty mesh. let verts_only = b"v 0 0 0\nv 1 0 0\nv 0 1 0\n"; assert!(matches!( load_obj(verts_only), Err(MeshLoadError::Degenerate) )); }
#[test] fn error_point_sized_aabb() { // A face collapsed to a single point has a point-sized AABB. let point = b"v 5 5 5\nf 1 1 1\n"; assert!(matches!(load_obj(point), Err(MeshLoadError::Degenerate))); }}