axiolid_mesh/
triangle.rs

1//! Indexed triangle mesh representation.
2
3use axiolid_core::{Aabb, Point3, Vec3};
4
5use crate::attribute::AttributeChannel;
6use crate::MeshValidationError;
7
8/// Optional independently indexed vertex normals.
9#[derive(Debug, Clone, Default, PartialEq)]
10pub struct NormalAttribute {
11    /// Normal values.
12    pub values: Vec<Vec3>,
13    /// Optional corner indices; absent means normals align with positions.
14    pub indices: Option<Vec<u32>>,
15}
16
17/// Indexed triangle mesh in local coordinates.
18///
19/// Dirty source geometry is representable. Call [`TriMesh::validate_structure`]
20/// at trust boundaries; manifold validation is a separate, more expensive pass.
21#[derive(Debug, Clone, Default, PartialEq)]
22pub struct TriMesh {
23    /// Vertex positions.
24    pub positions: Vec<Point3>,
25    /// Triangle corner indices, three entries per triangle.
26    pub indices: Vec<u32>,
27    /// Optional normals preserved from the source.
28    pub normals: Option<NormalAttribute>,
29    /// Named per-vertex channels carried alongside positions.
30    ///
31    /// Empty by default: a mesh with no extra data pays nothing for this.
32    pub attributes: Vec<AttributeChannel>,
33}
34
35impl TriMesh {
36    /// Construct a position/index mesh without normals.
37    pub fn new(positions: Vec<Point3>, indices: Vec<u32>) -> Self {
38        Self {
39            positions,
40            indices,
41            normals: None,
42            attributes: Vec::new(),
43        }
44    }
45
46    /// Number of complete triangles.
47    pub fn triangle_count(&self) -> usize {
48        self.indices.len() / 3
49    }
50
51    /// Bounds of all positions.
52    pub fn bounds(&self) -> Aabb {
53        let mut bounds = Aabb::default();
54        for &position in &self.positions {
55            bounds.extend(position);
56        }
57        bounds
58    }
59
60    /// Cheap index-buffer and attribute-size validation.
61    pub fn validate_structure(&self) -> Result<(), MeshValidationError> {
62        if self.indices.len() % 3 != 0 {
63            return Err(MeshValidationError::IncompleteTriangle {
64                index_count: self.indices.len(),
65            });
66        }
67        if let Some(&index) = self
68            .indices
69            .iter()
70            .find(|&&index| index as usize >= self.positions.len())
71        {
72            return Err(MeshValidationError::PositionIndexOutOfRange {
73                index,
74                position_count: self.positions.len(),
75            });
76        }
77        if let Some(normals) = &self.normals {
78            if let Some(indices) = &normals.indices {
79                if indices.len() != self.indices.len() {
80                    return Err(MeshValidationError::NormalIndexCount {
81                        expected: self.indices.len(),
82                        actual: indices.len(),
83                    });
84                }
85                if let Some(&index) = indices
86                    .iter()
87                    .find(|&&index| index as usize >= normals.values.len())
88                {
89                    return Err(MeshValidationError::NormalIndexOutOfRange {
90                        index,
91                        normal_count: normals.values.len(),
92                    });
93                }
94            } else if normals.values.len() != self.positions.len() {
95                return Err(MeshValidationError::NormalCount {
96                    expected: self.positions.len(),
97                    actual: normals.values.len(),
98                });
99            }
100        }
101        for (position, channel) in self.attributes.iter().enumerate() {
102            if channel.width == 0 {
103                return Err(MeshValidationError::AttributeZeroWidth {
104                    name: channel.name.clone(),
105                });
106            }
107            // Checked before the count so a duplicate is named as such
108            // rather than as whichever length happens to disagree first.
109            if self.attributes[..position]
110                .iter()
111                .any(|earlier| earlier.name == channel.name)
112            {
113                return Err(MeshValidationError::AttributeDuplicateName {
114                    name: channel.name.clone(),
115                });
116            }
117            if let Some(corners) = &channel.corner_indices {
118                validate_corner_channel(channel, corners, self.indices.len())?;
119            } else if channel.vertex_count() != self.positions.len()
120                || channel.values.len() % channel.width != 0
121            {
122                return Err(MeshValidationError::AttributeCount {
123                    name: channel.name.clone(),
124                    expected: self.positions.len(),
125                    actual: channel.vertex_count(),
126                });
127            }
128        }
129        Ok(())
130    }
131
132    /// Whether cheap structural validation succeeds.
133    pub fn is_structurally_valid(&self) -> bool {
134        self.validate_structure().is_ok()
135    }
136
137    /// Triangles in deterministic index-buffer order.
138    pub fn triangles(&self) -> impl ExactSizeIterator<Item = [u32; 3]> + '_ {
139        self.indices
140            .chunks_exact(3)
141            .map(|triangle| [triangle[0], triangle[1], triangle[2]])
142    }
143}
144
145/// Check one corner-indexed channel against the mesh's corner count.
146///
147/// Order matters for the error a caller sees: shape first (whole tuples,
148/// one entry per corner), then each entry's range, then the per-triangle
149/// all-or-nothing rule. A malformed buffer is named as malformed rather
150/// than as whichever triangle happens to trip first.
151fn validate_corner_channel(
152    channel: &AttributeChannel,
153    corners: &[u32],
154    corner_count: usize,
155) -> Result<(), MeshValidationError> {
156    if channel.values.len() % channel.width != 0 {
157        return Err(MeshValidationError::AttributeRaggedValues {
158            name: channel.name.clone(),
159            values: channel.values.len(),
160            width: channel.width,
161        });
162    }
163    if corners.len() != corner_count {
164        return Err(MeshValidationError::AttributeCornerCount {
165            name: channel.name.clone(),
166            expected: corner_count,
167            actual: corners.len(),
168        });
169    }
170    let value_count = channel.value_count();
171    if let Some(&index) = corners
172        .iter()
173        .find(|&&index| index != AttributeChannel::UNMAPPED && index as usize >= value_count)
174    {
175        return Err(MeshValidationError::AttributeCornerIndexOutOfRange {
176            name: channel.name.clone(),
177            index,
178            value_count,
179        });
180    }
181    for (triangle, entries) in corners.chunks_exact(3).enumerate() {
182        let unmapped = entries
183            .iter()
184            .filter(|&&index| index == AttributeChannel::UNMAPPED)
185            .count();
186        if unmapped != 0 && unmapped != 3 {
187            return Err(MeshValidationError::AttributePartiallyMapped {
188                name: channel.name.clone(),
189                triangle,
190            });
191        }
192    }
193    Ok(())
194}