Skip to main content

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