package mesh import "fmt" // Triangle indexes three vertices, counter-clockwise seen from outside. type Triangle [3]int // Mesh is one named object: a triangle soup over a shared vertex list. type Mesh struct { Name string Verts []Vec3 Tris []Triangle } // Scene is an ordered list of meshes, matching OBJ objects. STL files // load as a single-mesh scene. type Scene struct { Meshes []*Mesh } func (s *Scene) TotalVerts() int { n := 0 for _, m := range s.Meshes { n += len(m.Verts) } return n } func (s *Scene) TotalTris() int { n := 0 for _, m := range s.Meshes { n += len(m.Tris) } return n } // Mesh returns the named mesh, or nil. func (s *Scene) Mesh(name string) *Mesh { for _, m := range s.Meshes { if m.Name == name { return m } } return nil } // Merged flattens the scene into a single mesh (copies data). func (s *Scene) Merged() *Mesh { out := &Mesh{Name: "merged"} for _, m := range s.Meshes { off := len(out.Verts) out.Verts = append(out.Verts, m.Verts...) for _, t := range m.Tris { out.Tris = append(out.Tris, Triangle{t[0] + off, t[1] + off, t[2] + off}) } } if len(s.Meshes) == 1 { out.Name = s.Meshes[0].Name } return out } // Append adds meshes from another scene, de-duplicating names by // appending _2, _3, ... func (s *Scene) Append(other *Scene) { taken := map[string]bool{} for _, m := range s.Meshes { taken[m.Name] = true } for _, m := range other.Meshes { name := m.Name for i := 2; taken[name]; i++ { name = fmt.Sprintf("%s_%d", m.Name, i) } m.Name = name taken[name] = true s.Meshes = append(s.Meshes, m) } } // FaceNormal returns the (unit) normal of triangle i. func (m *Mesh) FaceNormal(i int) Vec3 { t := m.Tris[i] a, b, c := m.Verts[t[0]], m.Verts[t[1]], m.Verts[t[2]] return b.Sub(a).Cross(c.Sub(a)).Norm() } // FlipWinding reverses the orientation of every triangle. func (m *Mesh) FlipWinding() { for i, t := range m.Tris { m.Tris[i] = Triangle{t[0], t[2], t[1]} } } type edge struct{ a, b int } func normEdge(a, b int) edge { if a > b { a, b = b, a } return edge{a, b} } // EdgeStats classifies the mesh topology: boundary edges belong to one // triangle, manifold edges to two, anything more is non-manifold. A // closed (watertight) mesh has zero boundary and zero non-manifold edges. func (m *Mesh) EdgeStats() (boundary, nonManifold int) { count := map[edge]int{} for _, t := range m.Tris { count[normEdge(t[0], t[1])]++ count[normEdge(t[1], t[2])]++ count[normEdge(t[2], t[0])]++ } for _, n := range count { switch { case n == 1: boundary++ case n > 2: nonManifold++ } } return } // Closed reports whether the mesh is watertight. func (m *Mesh) Closed() bool { b, nm := m.EdgeStats() return b == 0 && nm == 0 }