//! Mesh contract types: the renderer-facing CPU model and its provenance. //! //! [`MeshFormat`], [`LengthUnit`], and [`Units`] are re-exported from //! [`polymodel_renderer_protocol`] so the app, worker, and mesh crate share a single //! definition without the mesh crate pulling `three-d-asset` into the app. pub use polymodel_renderer_protocol::{LengthUnit, MeshFormat, Units}; // These `three-d-asset` types are the owned CPU-side contract shared with the renderer // worker. They do not include GPU/runtime state. pub use three_d_asset::{AxisAlignedBoundingBox, Geometry, Model, PointCloud, TriMesh}; /// Camera framing hint derived from a mesh's axis-aligned bounding box: the centre the /// camera should orbit and a bounding-radius estimate to frame it. #[derive(Debug, Clone, Copy, PartialEq)] pub struct CameraFit { /// AABB centre. pub center: [f32; 3], /// Half the largest AABB extent. pub radius: f32, } /// A Polymodel model: owned CPU geometry/material data plus format provenance and units. /// /// glTF/OBJ keep the per-primitive geometry/material structure exposed by /// `three-d-asset`; STL is represented as a one-part triangle model. Triangle normals are /// geometry-derived during validation. Point-cloud geometry is preserved even though the /// current worker renders triangles first. #[derive(Debug)] pub struct ModelMesh { /// The owned CPU model, including primitive boundaries, transforms, materials, and /// decoded textures. pub model: Model, /// The format this mesh was loaded from. pub format: MeshFormat, /// Unit provenance and assumption. pub units: Units, } impl ModelMesh { /// Construct a model contract from an owned CPU model. pub fn new(model: Model, format: MeshFormat, units: Units) -> Self { Self { model, format, units, } } /// Number of CPU primitives/parts preserved from the source model. pub fn part_count(&self) -> usize { self.model.geometries.len() } /// The axis-aligned bounding box over all preserved primitive positions in world space. pub fn aabb(&self) -> AxisAlignedBoundingBox { let mut aabb = AxisAlignedBoundingBox::EMPTY; for primitive in &self.model.geometries { match &primitive.geometry { Geometry::Triangles(mesh) => { aabb.expand_with_aabb(AxisAlignedBoundingBox::new_with_transformed_positions( &mesh.positions.to_f32(), primitive.transformation, )); } Geometry::Points(points) => { aabb.expand_with_aabb(AxisAlignedBoundingBox::new_with_transformed_positions( &points.positions.to_f32(), primitive.transformation, )); } } } aabb } /// Camera framing derived from [`Self::aabb`]: `center` is the box centre and /// `radius` is half the largest box extent. pub fn camera_fit(&self) -> CameraFit { let aabb = self.aabb(); let center = aabb.center(); let size = aabb.size(); CameraFit { center: [center.x, center.y, center.z], radius: size.x.max(size.y).max(size.z) / 2.0, } } } /// Errors that can occur loading a mesh. #[derive(Debug, thiserror::Error)] pub enum MeshLoadError { /// The input was empty (zero bytes). #[error("mesh input is empty")] Empty, /// The bytes could not be parsed as a valid mesh of the expected format. #[error("failed to parse STL: {0}")] Parse(#[from] std::io::Error), /// A format parsed by `three-d-asset` (OBJ/glTF) failed to decode, including a /// missing or undecodable companion resource (buffer/texture). #[error("mesh decode failed: {0}")] Decode(String), /// A glTF companion URI is unsafe to fetch or load. #[error("unsafe companion URI: {0}")] UnsafeCompanionUri(String), /// The mesh parsed but is degenerate: no triangles, an out-of-range index, a /// non-finite position, or a point-sized bounding box. #[error("mesh is degenerate")] Degenerate, /// The format is not supported by the active parser. Returned by the /// [`RemoteMeshParser`](crate::parser::RemoteMeshParser) seam when no conversion is /// wired in (reserved for future server-side formats like STEP). #[error("unsupported mesh format")] UnsupportedFormat, } /// Lightweight stats for reporting back to the main thread. impl ModelMesh { /// Number of source positions across all preserved parts. /// /// This is a sum of part-local triangle vertices and point-cloud points, not a global /// deduplicated vertex count. pub fn vertex_count(&self) -> u32 { self.model .geometries .iter() .map(|primitive| match &primitive.geometry { Geometry::Triangles(mesh) => mesh.positions.len() as u32, Geometry::Points(points) => points.positions.len() as u32, }) .sum() } /// Number of triangles across triangle parts. Point-cloud parts contribute zero. pub fn triangle_count(&self) -> u32 { self.model .geometries .iter() .map(|primitive| match &primitive.geometry { Geometry::Triangles(mesh) => mesh.triangle_count() as u32, Geometry::Points(_) => 0, }) .sum() } }