schematic_mesher/mesher/
geometry.rs

1//! Mesh geometry types.
2
3/// A vertex in the output mesh.
4#[derive(Debug, Clone, Copy, PartialEq)]
5pub struct Vertex {
6    /// Position in 3D space.
7    pub position: [f32; 3],
8    /// Normal vector.
9    pub normal: [f32; 3],
10    /// Texture coordinates.
11    pub uv: [f32; 2],
12    /// Vertex color (RGBA).
13    pub color: [f32; 4],
14}
15
16impl Vertex {
17    pub fn new(position: [f32; 3], normal: [f32; 3], uv: [f32; 2]) -> Self {
18        Self {
19            position,
20            normal,
21            uv,
22            color: [1.0, 1.0, 1.0, 1.0], // White by default
23        }
24    }
25
26    pub fn with_color(mut self, color: [f32; 4]) -> Self {
27        self.color = color;
28        self
29    }
30}
31
32impl Default for Vertex {
33    fn default() -> Self {
34        Self {
35            position: [0.0, 0.0, 0.0],
36            normal: [0.0, 1.0, 0.0],
37            uv: [0.0, 0.0],
38            color: [1.0, 1.0, 1.0, 1.0],
39        }
40    }
41}
42
43/// A triangle mesh.
44#[derive(Debug, Clone, Default)]
45pub struct Mesh {
46    /// Vertex data.
47    pub vertices: Vec<Vertex>,
48    /// Triangle indices (3 per triangle).
49    pub indices: Vec<u32>,
50}
51
52impl Mesh {
53    pub fn new() -> Self {
54        Self::default()
55    }
56
57    /// Add a vertex and return its index.
58    pub fn add_vertex(&mut self, vertex: Vertex) -> u32 {
59        let index = self.vertices.len() as u32;
60        self.vertices.push(vertex);
61        index
62    }
63
64    /// Add a triangle by vertex indices.
65    pub fn add_triangle(&mut self, i0: u32, i1: u32, i2: u32) {
66        self.indices.push(i0);
67        self.indices.push(i1);
68        self.indices.push(i2);
69    }
70
71    /// Add a quad (two triangles) by vertex indices.
72    /// Vertices are provided in order around the quad. Triangles are wound CCW for front-facing.
73    pub fn add_quad(&mut self, i0: u32, i1: u32, i2: u32, i3: u32) {
74        // Reverse winding for CCW front faces (glTF standard)
75        // First triangle: 0, 2, 1
76        self.add_triangle(i0, i2, i1);
77        // Second triangle: 0, 3, 2
78        self.add_triangle(i0, i3, i2);
79    }
80
81    /// Add a quad with AO-aware triangulation to fix anisotropy.
82    /// Uses the diagonal that minimizes interpolation artifacts.
83    pub fn add_quad_ao(&mut self, i0: u32, i1: u32, i2: u32, i3: u32, ao: [u8; 4]) {
84        // Compare diagonal sums to choose triangulation
85        // See: https://0fps.net/2013/07/03/ambient-occlusion-for-minecraft-like-worlds/
86        if ao[0] as u16 + ao[2] as u16 > ao[1] as u16 + ao[3] as u16 {
87            // Flip: use 1-3 diagonal instead of 0-2
88            // Triangles: (1, 3, 0) and (1, 2, 3) with CCW winding
89            self.add_triangle(i1, i0, i3);
90            self.add_triangle(i1, i3, i2);
91        } else {
92            // Normal: use 0-2 diagonal
93            self.add_triangle(i0, i2, i1);
94            self.add_triangle(i0, i3, i2);
95        }
96    }
97
98    /// Get the number of triangles.
99    pub fn triangle_count(&self) -> usize {
100        self.indices.len() / 3
101    }
102
103    /// Get the number of vertices.
104    pub fn vertex_count(&self) -> usize {
105        self.vertices.len()
106    }
107
108    /// Check if the mesh is empty.
109    pub fn is_empty(&self) -> bool {
110        self.vertices.is_empty()
111    }
112
113    /// Merge another mesh into this one.
114    pub fn merge(&mut self, other: &Mesh) {
115        let offset = self.vertices.len() as u32;
116
117        self.vertices.extend_from_slice(&other.vertices);
118
119        for index in &other.indices {
120            self.indices.push(index + offset);
121        }
122    }
123
124    /// Translate all vertices by an offset.
125    pub fn translate(&mut self, offset: [f32; 3]) {
126        for vertex in &mut self.vertices {
127            vertex.position[0] += offset[0];
128            vertex.position[1] += offset[1];
129            vertex.position[2] += offset[2];
130        }
131    }
132
133    /// Get positions as a flat array (for glTF export).
134    pub fn positions_flat(&self) -> Vec<f32> {
135        self.vertices
136            .iter()
137            .flat_map(|v| v.position)
138            .collect()
139    }
140
141    /// Get normals as a flat array (for glTF export).
142    pub fn normals_flat(&self) -> Vec<f32> {
143        self.vertices
144            .iter()
145            .flat_map(|v| v.normal)
146            .collect()
147    }
148
149    /// Get UVs as a flat array (for glTF export).
150    pub fn uvs_flat(&self) -> Vec<f32> {
151        self.vertices
152            .iter()
153            .flat_map(|v| v.uv)
154            .collect()
155    }
156
157    /// Get colors as a flat array (for glTF export).
158    pub fn colors_flat(&self) -> Vec<f32> {
159        self.vertices
160            .iter()
161            .flat_map(|v| v.color)
162            .collect()
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn test_mesh_creation() {
172        let mut mesh = Mesh::new();
173        assert!(mesh.is_empty());
174
175        let v0 = mesh.add_vertex(Vertex::new([0.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0]));
176        let v1 = mesh.add_vertex(Vertex::new([1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 0.0]));
177        let v2 = mesh.add_vertex(Vertex::new([1.0, 0.0, 1.0], [0.0, 1.0, 0.0], [1.0, 1.0]));
178
179        mesh.add_triangle(v0, v1, v2);
180
181        assert_eq!(mesh.vertex_count(), 3);
182        assert_eq!(mesh.triangle_count(), 1);
183    }
184
185    #[test]
186    fn test_mesh_quad() {
187        let mut mesh = Mesh::new();
188
189        let v0 = mesh.add_vertex(Vertex::new([0.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0]));
190        let v1 = mesh.add_vertex(Vertex::new([1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 0.0]));
191        let v2 = mesh.add_vertex(Vertex::new([1.0, 0.0, 1.0], [0.0, 1.0, 0.0], [1.0, 1.0]));
192        let v3 = mesh.add_vertex(Vertex::new([0.0, 0.0, 1.0], [0.0, 1.0, 0.0], [0.0, 1.0]));
193
194        mesh.add_quad(v0, v1, v2, v3);
195
196        assert_eq!(mesh.vertex_count(), 4);
197        assert_eq!(mesh.triangle_count(), 2);
198        // CCW winding: (0,2,1) and (0,3,2)
199        assert_eq!(mesh.indices, vec![0, 2, 1, 0, 3, 2]);
200    }
201
202    #[test]
203    fn test_mesh_merge() {
204        let mut mesh1 = Mesh::new();
205        let v0 = mesh1.add_vertex(Vertex::new([0.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0]));
206        let v1 = mesh1.add_vertex(Vertex::new([1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 0.0]));
207        let v2 = mesh1.add_vertex(Vertex::new([0.0, 0.0, 1.0], [0.0, 1.0, 0.0], [0.0, 1.0]));
208        mesh1.add_triangle(v0, v1, v2);
209
210        let mut mesh2 = Mesh::new();
211        let v0 = mesh2.add_vertex(Vertex::new([2.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0]));
212        let v1 = mesh2.add_vertex(Vertex::new([3.0, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 0.0]));
213        let v2 = mesh2.add_vertex(Vertex::new([2.0, 0.0, 1.0], [0.0, 1.0, 0.0], [0.0, 1.0]));
214        mesh2.add_triangle(v0, v1, v2);
215
216        mesh1.merge(&mesh2);
217
218        assert_eq!(mesh1.vertex_count(), 6);
219        assert_eq!(mesh1.triangle_count(), 2);
220        // Second triangle indices should be offset by 3
221        assert_eq!(mesh1.indices, vec![0, 1, 2, 3, 4, 5]);
222    }
223}