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}