Something went wrong. Try again.
atproto Thingiverse but good
Something went wrong. Try again.
3.8 kB · 109 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110//! 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::contract::{MeshFormat, MeshLoadError, ModelMesh, Units};
/// Parse OBJ bytes into a [`ModelMesh`].////// Convex polygons are fan-triangulated; geometry and material groups exposed by/// `three-d-asset` are preserved. 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 mut model: three_d_asset::Model = io::deserialize("asset.obj", bytes.to_vec()) .map_err(|e| MeshLoadError::Decode(e.to_string()))?; crate::validate_model(&mut model)?; Ok(ModelMesh::new( model, MeshFormat::Obj, Units::unitless_assumed_mm(), ))}
#[cfg(test)]mod tests { use super::load_obj; use crate::contract::{LengthUnit, MeshFormat, MeshLoadError, Units}; use three_d_asset::{Geometry, Positions, TriMesh};
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}")) }
fn first_triangle_mesh(model: &crate::contract::ModelMesh) -> &TriMesh { model .model .geometries .iter() .find_map(|primitive| match &primitive.geometry { Geometry::Triangles(mesh) => Some(mesh), Geometry::Points(_) => None, }) .expect("model should contain triangle geometry") }
#[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), } ); assert_eq!(model.triangle_count(), 12); let mesh = first_triangle_mesh(&model); assert!(matches!(mesh.positions, Positions::F32(_))); let normals = mesh .normals .as_ref() .expect("geometry normals should be populated"); assert_eq!(normals.len(), mesh.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() { 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() { let point = b"v 5 5 5\nf 1 1 1\n"; assert!(matches!(load_obj(point), Err(MeshLoadError::Degenerate))); }}