openbim_ifcx_geometry/
mesh.rs

1//! Indexed triangle meshes from `usd::usdgeom::mesh`.
2//!
3//! In `ifcx_alpha` files the attribute is an object:
4//!
5//! ```json
6//! "usd::usdgeom::mesh": {
7//!     "points": [[0, 0, 0], [1, 0, 0], [1, 1, 0]],
8//!     "faceVertexIndices": [0, 1, 2]
9//! }
10//! ```
11//!
12//! Every face is a triangle: none of the 1 925 meshes in the upstream examples
13//! (`1a63082`) has `faceVertexCounts`, and upstream's viewer reads the indices
14//! three at a time. If `faceVertexCounts` is present, every entry must be 3;
15//! polygons are rejected rather than triangulated, since a fan would draw
16//! concave faces wrongly. Other fields (USD `orientation`, normals, primvars)
17//! are ignored; winding is taken as USD's default right-handed,
18//! counter-clockwise front face.
19
20use serde_json::Value;
21
22use crate::error::DecodeError;
23use crate::json;
24use crate::math::{cross, normalize_or_zero, sub, Vec3};
25
26/// Attribute id that carries a mesh.
27pub const ATTRIBUTE: &str = "usd::usdgeom::mesh";
28
29/// An indexed triangle mesh in the node's local coordinates.
30///
31/// Positions are `f64`; see [`crate::math`] for why.
32#[derive(Debug, Clone, PartialEq)]
33pub struct TriangleMesh {
34    /// Vertex positions, from `points`.
35    pub positions: Vec<Vec3>,
36    /// Three indices into `positions` per triangle, from
37    /// `faceVertexIndices`. Every index is in range.
38    pub indices: Vec<u32>,
39    /// One unit normal per triangle, `(b - a) × (c - a)` normalised, so
40    /// `face_normals.len() == indices.len() / 3`. Degenerate triangles get
41    /// a zero vector. Renderers that want flat shading duplicate vertices
42    /// per face and use these.
43    pub face_normals: Vec<Vec3>,
44}
45
46impl TriangleMesh {
47    /// Decodes the value of a [`ATTRIBUTE`] (`usd::usdgeom::mesh`)
48    /// attribute.
49    ///
50    /// ```
51    /// use openbim_ifcx_geometry::mesh::TriangleMesh;
52    ///
53    /// let value = serde_json::json!({
54    ///     "points": [[0, 0, 0], [1, 0, 0], [0, 1, 0]],
55    ///     "faceVertexIndices": [0, 1, 2]
56    /// });
57    /// let mesh = TriangleMesh::from_attribute(&value)?;
58    /// assert_eq!(mesh.triangle_count(), 1);
59    /// assert_eq!(mesh.face_normals[0], [0.0, 0.0, 1.0]);
60    /// # Ok::<(), openbim_ifcx_geometry::DecodeError>(())
61    /// ```
62    pub fn from_attribute(value: &Value) -> Result<Self, DecodeError> {
63        let object = json::object(value, ATTRIBUTE)?;
64        let positions = json::points3(json::field(object, "points")?, "points")?;
65        let indices = json::indices(
66            json::field(object, "faceVertexIndices")?,
67            "faceVertexIndices",
68        )?;
69        if let Some(counts) = object.get("faceVertexCounts") {
70            let counts = json::counts(counts, "faceVertexCounts")?;
71            if let Some((face, &n)) = counts.iter().enumerate().find(|(_, &n)| n != 3) {
72                return Err(DecodeError::NonTriangleFace {
73                    face,
74                    vertex_count: n,
75                });
76            }
77            let counted = counts.len() as u64 * 3;
78            if counted != indices.len() as u64 {
79                return Err(DecodeError::CountMismatch {
80                    field: "faceVertexCounts",
81                    counted,
82                    available: indices.len(),
83                });
84            }
85        }
86        Self::new(positions, indices)
87    }
88
89    /// Builds a mesh from positions and triangle indices, checking the
90    /// indices and computing face normals.
91    pub fn new(positions: Vec<Vec3>, indices: Vec<u32>) -> Result<Self, DecodeError> {
92        if indices.len() % 3 != 0 {
93            return Err(DecodeError::NotTriangles {
94                index_count: indices.len(),
95            });
96        }
97        if let Some((i, &index)) = indices
98            .iter()
99            .enumerate()
100            .find(|(_, &index)| index as usize >= positions.len())
101        {
102            return Err(DecodeError::IndexOutOfRange {
103                at: format!("faceVertexIndices[{i}]"),
104                index,
105                vertex_count: positions.len(),
106            });
107        }
108        let face_normals = indices
109            .chunks_exact(3)
110            .map(|t| {
111                let [a, b, c] = [0, 1, 2].map(|k| positions[t[k] as usize]);
112                normalize_or_zero(cross(sub(b, a), sub(c, a)))
113            })
114            .collect();
115        Ok(Self {
116            positions,
117            indices,
118            face_normals,
119        })
120    }
121
122    /// Number of triangles.
123    pub fn triangle_count(&self) -> usize {
124        self.indices.len() / 3
125    }
126
127    /// Triangles as index triples.
128    pub fn triangles(&self) -> impl Iterator<Item = [u32; 3]> + '_ {
129        self.indices.chunks_exact(3).map(|t| [t[0], t[1], t[2]])
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use serde_json::json;
137
138    /// Unit cube, 8 corners, 12 outward-facing counter-clockwise triangles.
139    fn cube() -> Value {
140        json!({
141            "points": [
142                [0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0],
143                [0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1]
144            ],
145            "faceVertexIndices": [
146                0, 2, 1, 0, 3, 2,
147                4, 5, 6, 4, 6, 7,
148                0, 1, 5, 0, 5, 4,
149                1, 2, 6, 1, 6, 5,
150                2, 3, 7, 2, 7, 6,
151                3, 0, 4, 3, 4, 7
152            ]
153        })
154    }
155
156    #[test]
157    fn cube_has_8_positions_and_36_indices() {
158        let mesh = TriangleMesh::from_attribute(&cube()).unwrap();
159        assert_eq!(mesh.positions.len(), 8);
160        assert_eq!(mesh.indices.len(), 36);
161        assert_eq!(mesh.triangle_count(), 12);
162        assert_eq!(mesh.face_normals.len(), 12);
163        let expected = [
164            [0.0, 0.0, -1.0],
165            [0.0, 0.0, 1.0],
166            [0.0, -1.0, 0.0],
167            [1.0, 0.0, 0.0],
168            [0.0, 1.0, 0.0],
169            [-1.0, 0.0, 0.0],
170        ];
171        for (i, n) in mesh.face_normals.iter().enumerate() {
172            assert_eq!(*n, expected[i / 2], "triangle {i}");
173        }
174        assert_eq!(mesh.triangles().next(), Some([0, 2, 1]));
175    }
176
177    #[test]
178    fn accepts_integer_and_float_coordinates() {
179        let mesh = TriangleMesh::from_attribute(&json!({
180            "points": [[0, 0.5, 0], [2, 0, 0], [0, 0, 1e3]],
181            "faceVertexIndices": [0, 1, 2]
182        }))
183        .unwrap();
184        assert_eq!(mesh.positions[2], [0.0, 0.0, 1000.0]);
185    }
186
187    #[test]
188    fn degenerate_triangle_has_zero_normal() {
189        let mesh = TriangleMesh::new(vec![[0.0; 3], [1.0, 0.0, 0.0]], vec![0, 1, 1]).unwrap();
190        assert_eq!(mesh.face_normals, vec![[0.0; 3]]);
191    }
192
193    #[test]
194    fn out_of_range_index_is_an_error() {
195        let e = TriangleMesh::from_attribute(&json!({
196            "points": [[0, 0, 0], [1, 0, 0], [0, 1, 0]],
197            "faceVertexIndices": [0, 1, 2, 2, 1, 3]
198        }))
199        .unwrap_err();
200        assert_eq!(
201            e,
202            DecodeError::IndexOutOfRange {
203                at: "faceVertexIndices[5]".into(),
204                index: 3,
205                vertex_count: 3
206            }
207        );
208    }
209
210    #[test]
211    fn non_triangle_index_list_is_an_error() {
212        let e = TriangleMesh::from_attribute(&json!({
213            "points": [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]],
214            "faceVertexIndices": [0, 1, 2, 3]
215        }))
216        .unwrap_err();
217        assert_eq!(e, DecodeError::NotTriangles { index_count: 4 });
218    }
219
220    #[test]
221    fn polygon_face_counts_are_rejected() {
222        let e = TriangleMesh::from_attribute(&json!({
223            "points": [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]],
224            "faceVertexIndices": [0, 1, 2, 3, 0, 2],
225            "faceVertexCounts": [4, 2]
226        }))
227        .unwrap_err();
228        assert_eq!(
229            e,
230            DecodeError::NonTriangleFace {
231                face: 0,
232                vertex_count: 4
233            }
234        );
235    }
236
237    #[test]
238    fn triangle_face_counts_must_cover_indices() {
239        let ok = TriangleMesh::from_attribute(&json!({
240            "points": [[0, 0, 0], [1, 0, 0], [0, 1, 0]],
241            "faceVertexIndices": [0, 1, 2],
242            "faceVertexCounts": [3]
243        }));
244        assert!(ok.is_ok());
245        let e = TriangleMesh::from_attribute(&json!({
246            "points": [[0, 0, 0], [1, 0, 0], [0, 1, 0]],
247            "faceVertexIndices": [0, 1, 2, 0, 1, 2],
248            "faceVertexCounts": [3]
249        }))
250        .unwrap_err();
251        assert_eq!(
252            e,
253            DecodeError::CountMismatch {
254                field: "faceVertexCounts",
255                counted: 3,
256                available: 6
257            }
258        );
259    }
260
261    #[test]
262    fn wrong_shapes_are_errors() {
263        let err = |v: Value| TriangleMesh::from_attribute(&v).unwrap_err();
264        assert_eq!(
265            err(json!({"faceVertexIndices": []})),
266            DecodeError::MissingField { field: "points" }
267        );
268        assert_eq!(
269            err(json!({"points": []})),
270            DecodeError::MissingField {
271                field: "faceVertexIndices"
272            }
273        );
274        assert_eq!(
275            err(json!({"points": [[0, 0]], "faceVertexIndices": []})),
276            DecodeError::WrongShape {
277                at: "points[0]".into(),
278                expected: "array of 3 numbers"
279            }
280        );
281        assert_eq!(
282            err(json!({"points": [[0, 0, 0]], "faceVertexIndices": [0, -1, 0]})),
283            DecodeError::WrongShape {
284                at: "faceVertexIndices[1]".into(),
285                expected: "integer from 0 to 4294967295"
286            }
287        );
288        assert_eq!(
289            err(json!({"points": [[0, 0, 0]], "faceVertexIndices": [0, 0.5, 0]})),
290            DecodeError::WrongShape {
291                at: "faceVertexIndices[1]".into(),
292                expected: "integer from 0 to 4294967295"
293            }
294        );
295    }
296
297    #[test]
298    fn empty_mesh_is_valid() {
299        let mesh =
300            TriangleMesh::from_attribute(&json!({"points": [], "faceVertexIndices": []})).unwrap();
301        assert_eq!(mesh.triangle_count(), 0);
302    }
303}