mesh-tool: mesht - create/inspect/edit OBJ+STL with ASCII multi-view rendering
- OBJ read/write (multi-object, ngon fan-triangulation, negative indices), STL binary+ascii with auto-detect and vertex welding - info: bbox/size/area/volume + watertightness via edge manifold stats - view: z-buffered orthographic ASCII renders (front/side/top/iso/back) - transform: center/mirror/scale/rotate/translate/fit, per-object filter, winding auto-flip on negative determinant - create: box/sphere/cylinder/cone/plane/torus; merge; convert - go tests: primitive volumes vs analytic values, roundtrips, topology Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmdG9GqfSWCzts7AkDwRDh
This commit is contained in:
129
mesh-tool/mesh/mesh.go
Normal file
129
mesh-tool/mesh/mesh.go
Normal file
@@ -0,0 +1,129 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user