mesh.rs
6.4 kB · rust · 235 lines
1use super::vec::Vec3;23/// The names of the four built-in solids.4pub const SOLIDS: [&str; 4] = ["cube", "tetra", "octa", "icosa"];56const S3: f32 = 0.57735027;7const PHI: f32 = 1.618034;89/// A triangulated solid with one outward unit normal per face.10#[derive(Clone, Debug)]11pub struct Mesh {12 /// The vertex positions.13 pub verts: Vec<Vec3>,14 /// The faces as triples of vertex indices.15 pub faces: Vec<[usize; 3]>,16 /// The outward unit normal of each face.17 pub normals: Vec<Vec3>,18}1920impl Mesh {21 /// Returns the vertex pairs where faces of different normals meet, skipping coplanar seams.22 pub fn edges(&self) -> Vec<[usize; 2]> {23 let mut seen: std::collections::HashMap<(usize, usize), Vec3> =24 std::collections::HashMap::new();25 let mut out = Vec::new();26 for (i, f) in self.faces.iter().enumerate() {27 let n = self.normals[i];28 for (a, b) in [(f[0], f[1]), (f[1], f[2]), (f[0], f[2])] {29 let key = (a.min(b), a.max(b));30 match seen.get(&key) {31 None => {32 seen.insert(key, n);33 }34 Some(prev) => {35 if prev.dot(n) < 0.9999 {36 out.push([key.0, key.1]);37 }38 }39 }40 }41 }42 out43 }44}4546fn build(verts: Vec<Vec3>, faces: Vec<[usize; 3]>, k: f32, scale: f32) -> Mesh {47 let normals = faces48 .iter()49 .map(|f| {50 let (a, b, c) = (verts[f[0]], verts[f[1]], verts[f[2]]);51 let n = (b - a).cross(c - a).scale(k);52 let center = a + b + c;53 if n.dot(center) < 0.0 {54 n.scale(-1.0)55 } else {56 n57 }58 })59 .collect();60 Mesh {61 verts: verts.into_iter().map(|v| v.scale(scale)).collect(),62 faces,63 normals,64 }65}6667/// Builds the named solid, falling back to the cube for unknown names.68///69/// ```70/// let mesh = mrlymath::space::solid("icosa");71/// assert_eq!((mesh.verts.len(), mesh.faces.len()), (12, 20));72/// ```73pub fn solid(name: &str) -> Mesh {74 match name {75 "tetra" => tetra(),76 "octa" => octa(),77 "icosa" => icosa(),78 _ => cube(),79 }80}8182/// Builds the cube, scaled to the unit ball.83pub fn cube() -> Mesh {84 let mut verts = Vec::new();85 for x in [-1.0f32, 1.0] {86 for y in [-1.0f32, 1.0] {87 for z in [-1.0f32, 1.0] {88 verts.push(Vec3::new(x, y, z));89 }90 }91 }92 let quads = [93 [0, 1, 3, 2],94 [4, 5, 7, 6],95 [0, 1, 5, 4],96 [2, 3, 7, 6],97 [0, 2, 6, 4],98 [1, 3, 7, 5],99 ];100 let mut faces = Vec::new();101 for q in quads {102 faces.push([q[0], q[1], q[2]]);103 faces.push([q[0], q[2], q[3]]);104 }105 build(verts, faces, 0.25, S3)106}107108/// Builds the tetrahedron, scaled to the unit ball.109pub fn tetra() -> Mesh {110 let verts = vec![111 Vec3::new(1.0, 1.0, 1.0),112 Vec3::new(1.0, -1.0, -1.0),113 Vec3::new(-1.0, 1.0, -1.0),114 Vec3::new(-1.0, -1.0, 1.0),115 ];116 let faces = vec![[0, 1, 2], [0, 1, 3], [0, 2, 3], [1, 2, 3]];117 build(verts, faces, 0.14433757, S3)118}119120/// Builds the octahedron, scaled to the unit ball.121pub fn octa() -> Mesh {122 let verts = vec![123 Vec3::new(1.0, 0.0, 0.0),124 Vec3::new(-1.0, 0.0, 0.0),125 Vec3::new(0.0, 1.0, 0.0),126 Vec3::new(0.0, -1.0, 0.0),127 Vec3::new(0.0, 0.0, 1.0),128 Vec3::new(0.0, 0.0, -1.0),129 ];130 let mut faces = Vec::new();131 for x in [0, 1] {132 for y in [2, 3] {133 for z in [4, 5] {134 faces.push([x, y, z]);135 }136 }137 }138 build(verts, faces, S3, 1.0)139}140141/// Builds the icosahedron, scaled to the unit ball.142pub fn icosa() -> Mesh {143 let verts = vec![144 Vec3::new(-1.0, PHI, 0.0),145 Vec3::new(1.0, PHI, 0.0),146 Vec3::new(-1.0, -PHI, 0.0),147 Vec3::new(1.0, -PHI, 0.0),148 Vec3::new(0.0, -1.0, PHI),149 Vec3::new(0.0, 1.0, PHI),150 Vec3::new(0.0, -1.0, -PHI),151 Vec3::new(0.0, 1.0, -PHI),152 Vec3::new(PHI, 0.0, -1.0),153 Vec3::new(PHI, 0.0, 1.0),154 Vec3::new(-PHI, 0.0, -1.0),155 Vec3::new(-PHI, 0.0, 1.0),156 ];157 let faces = vec![158 [0, 11, 5],159 [0, 5, 1],160 [0, 1, 7],161 [0, 7, 10],162 [0, 10, 11],163 [1, 5, 9],164 [5, 11, 4],165 [11, 10, 2],166 [10, 7, 6],167 [7, 1, 8],168 [3, 9, 4],169 [3, 4, 2],170 [3, 2, 6],171 [3, 6, 8],172 [3, 8, 9],173 [4, 9, 5],174 [2, 4, 11],175 [6, 2, 10],176 [8, 6, 7],177 [9, 8, 1],178 ];179 build(verts, faces, 0.28867513, 0.525_731_1)180}181182#[cfg(test)]183mod tests {184 use super::*;185 use std::collections::HashSet;186187 fn all() -> Vec<(&'static str, Mesh)> {188 SOLIDS.iter().map(|&n| (n, solid(n))).collect()189 }190191 #[test]192 fn normals_are_unit_and_outward() {193 for (name, mesh) in all() {194 for (i, f) in mesh.faces.iter().enumerate() {195 let n = mesh.normals[i];196 assert!((n.dot(n) - 1.0).abs() < 1e-4, "{name} face {i} |n|");197 let center = mesh.verts[f[0]] + mesh.verts[f[1]] + mesh.verts[f[2]];198 assert!(n.dot(center) > 0.0, "{name} face {i} inward");199 }200 }201 }202 #[test]203 fn solids_close() {204 for (name, mesh) in all() {205 let mut edges = HashSet::new();206 for f in &mesh.faces {207 for (a, b) in [(f[0], f[1]), (f[1], f[2]), (f[0], f[2])] {208 edges.insert((a.min(b), a.max(b)));209 }210 }211 let v = mesh.verts.len() as i64;212 let e = edges.len() as i64;213 let f = mesh.faces.len() as i64;214 assert_eq!(v - e + f, 2, "{name} euler");215 }216 }217 #[test]218 fn solids_fit_the_unit_ball() {219 for (name, mesh) in all() {220 for v in &mesh.verts {221 assert!(v.dot(*v) < 1.001, "{name} vert outside");222 }223 }224 }225 #[test]226 fn unknown_names_fall_back_to_cube() {227 assert_eq!(solid("soup").faces.len(), solid("cube").faces.len());228 }229 #[test]230 fn edges_skip_coplanar_diagonals() {231 for (name, count) in [("cube", 12), ("tetra", 6), ("octa", 12), ("icosa", 30)] {232 assert_eq!(solid(name).edges().len(), count, "{name}");233 }234 }235}