- 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
180 lines
5.1 KiB
Go
180 lines
5.1 KiB
Go
package mesh
|
|
|
|
import "math"
|
|
|
|
// All primitives are centered at the origin with +Y up and get outward
|
|
// (counter-clockwise) winding; ensureOutward fixes the global
|
|
// orientation via the signed volume as a safety net.
|
|
|
|
func ensureOutward(m *Mesh) *Mesh {
|
|
if m.SignedVolume() < 0 {
|
|
m.FlipWinding()
|
|
}
|
|
return m
|
|
}
|
|
|
|
// Box builds an axis-aligned box of the given size.
|
|
func Box(size Vec3) *Mesh {
|
|
x, y, z := size.X/2, size.Y/2, size.Z/2
|
|
m := &Mesh{
|
|
Name: "box",
|
|
Verts: []Vec3{
|
|
{-x, -y, -z}, {x, -y, -z}, {x, y, -z}, {-x, y, -z}, // back (z-)
|
|
{-x, -y, z}, {x, -y, z}, {x, y, z}, {-x, y, z}, // front (z+)
|
|
},
|
|
}
|
|
quads := [][4]int{
|
|
{0, 3, 2, 1}, // back
|
|
{4, 5, 6, 7}, // front
|
|
{0, 1, 5, 4}, // bottom
|
|
{2, 3, 7, 6}, // top
|
|
{1, 2, 6, 5}, // right
|
|
{0, 4, 7, 3}, // left
|
|
}
|
|
for _, q := range quads {
|
|
m.Tris = append(m.Tris, Triangle{q[0], q[1], q[2]}, Triangle{q[0], q[2], q[3]})
|
|
}
|
|
return ensureOutward(m)
|
|
}
|
|
|
|
// Plane builds a flat rectangle in the XZ plane (an open mesh).
|
|
func Plane(w, d float64) *Mesh {
|
|
x, z := w/2, d/2
|
|
return &Mesh{
|
|
Name: "plane",
|
|
Verts: []Vec3{{-x, 0, -z}, {x, 0, -z}, {x, 0, z}, {-x, 0, z}},
|
|
Tris: []Triangle{{0, 2, 1}, {0, 3, 2}}, // +Y facing
|
|
}
|
|
}
|
|
|
|
// Sphere builds a UV sphere. segments = around the equator (>= 3),
|
|
// rings = from pole to pole (>= 2).
|
|
func Sphere(r float64, segments, rings int) *Mesh {
|
|
if segments < 3 {
|
|
segments = 3
|
|
}
|
|
if rings < 2 {
|
|
rings = 2
|
|
}
|
|
m := &Mesh{Name: "sphere"}
|
|
top := len(m.Verts)
|
|
m.Verts = append(m.Verts, Vec3{0, r, 0})
|
|
// interior rings, top to bottom
|
|
ringStart := make([]int, rings)
|
|
for i := 1; i < rings; i++ {
|
|
theta := math.Pi * float64(i) / float64(rings)
|
|
y := r * math.Cos(theta)
|
|
rad := r * math.Sin(theta)
|
|
ringStart[i] = len(m.Verts)
|
|
for j := 0; j < segments; j++ {
|
|
phi := 2 * math.Pi * float64(j) / float64(segments)
|
|
m.Verts = append(m.Verts, Vec3{rad * math.Cos(phi), y, rad * math.Sin(phi)})
|
|
}
|
|
}
|
|
bottom := len(m.Verts)
|
|
m.Verts = append(m.Verts, Vec3{0, -r, 0})
|
|
|
|
at := func(ring, seg int) int { return ringStart[ring] + seg%segments }
|
|
for j := 0; j < segments; j++ {
|
|
m.Tris = append(m.Tris, Triangle{top, at(1, j), at(1, j+1)}) // top cap
|
|
m.Tris = append(m.Tris, Triangle{bottom, at(rings-1, j+1), at(rings-1, j)})
|
|
}
|
|
for i := 1; i < rings-1; i++ {
|
|
for j := 0; j < segments; j++ {
|
|
a, b := at(i, j), at(i, j+1)
|
|
c, d := at(i+1, j+1), at(i+1, j)
|
|
m.Tris = append(m.Tris, Triangle{a, b, c}, Triangle{a, c, d})
|
|
}
|
|
}
|
|
return ensureOutward(m)
|
|
}
|
|
|
|
// Cylinder builds a closed cylinder of height h around the Y axis.
|
|
func Cylinder(r, h float64, segments int) *Mesh {
|
|
if segments < 3 {
|
|
segments = 3
|
|
}
|
|
m := &Mesh{Name: "cylinder"}
|
|
y := h / 2
|
|
topC := len(m.Verts)
|
|
m.Verts = append(m.Verts, Vec3{0, y, 0})
|
|
botC := len(m.Verts)
|
|
m.Verts = append(m.Verts, Vec3{0, -y, 0})
|
|
topStart := len(m.Verts)
|
|
for j := 0; j < segments; j++ {
|
|
phi := 2 * math.Pi * float64(j) / float64(segments)
|
|
m.Verts = append(m.Verts, Vec3{r * math.Cos(phi), y, r * math.Sin(phi)})
|
|
}
|
|
botStart := len(m.Verts)
|
|
for j := 0; j < segments; j++ {
|
|
phi := 2 * math.Pi * float64(j) / float64(segments)
|
|
m.Verts = append(m.Verts, Vec3{r * math.Cos(phi), -y, r * math.Sin(phi)})
|
|
}
|
|
t := func(j int) int { return topStart + j%segments }
|
|
b := func(j int) int { return botStart + j%segments }
|
|
for j := 0; j < segments; j++ {
|
|
m.Tris = append(m.Tris,
|
|
Triangle{topC, t(j + 1), t(j)}, // top cap
|
|
Triangle{botC, b(j), b(j + 1)}, // bottom cap
|
|
Triangle{t(j), t(j + 1), b(j + 1)}, // side
|
|
Triangle{t(j), b(j + 1), b(j)}, // side
|
|
)
|
|
}
|
|
return ensureOutward(m)
|
|
}
|
|
|
|
// Cone builds a closed cone with its base at -h/2 and apex at +h/2.
|
|
func Cone(r, h float64, segments int) *Mesh {
|
|
if segments < 3 {
|
|
segments = 3
|
|
}
|
|
m := &Mesh{Name: "cone"}
|
|
apex := len(m.Verts)
|
|
m.Verts = append(m.Verts, Vec3{0, h / 2, 0})
|
|
baseC := len(m.Verts)
|
|
m.Verts = append(m.Verts, Vec3{0, -h / 2, 0})
|
|
start := len(m.Verts)
|
|
for j := 0; j < segments; j++ {
|
|
phi := 2 * math.Pi * float64(j) / float64(segments)
|
|
m.Verts = append(m.Verts, Vec3{r * math.Cos(phi), -h / 2, r * math.Sin(phi)})
|
|
}
|
|
at := func(j int) int { return start + j%segments }
|
|
for j := 0; j < segments; j++ {
|
|
m.Tris = append(m.Tris,
|
|
Triangle{apex, at(j + 1), at(j)},
|
|
Triangle{baseC, at(j), at(j + 1)},
|
|
)
|
|
}
|
|
return ensureOutward(m)
|
|
}
|
|
|
|
// Torus builds a torus around the Y axis: ring radius R (center of tube
|
|
// to center of torus) and tube radius r.
|
|
func Torus(R, r float64, segments, rings int) *Mesh {
|
|
if segments < 3 {
|
|
segments = 3
|
|
}
|
|
if rings < 3 {
|
|
rings = 3
|
|
}
|
|
m := &Mesh{Name: "torus"}
|
|
for i := 0; i < segments; i++ { // around the main ring
|
|
phi := 2 * math.Pi * float64(i) / float64(segments)
|
|
cx, cz := math.Cos(phi), math.Sin(phi)
|
|
for j := 0; j < rings; j++ { // around the tube
|
|
theta := 2 * math.Pi * float64(j) / float64(rings)
|
|
rad := R + r*math.Cos(theta)
|
|
m.Verts = append(m.Verts, Vec3{rad * cx, r * math.Sin(theta), rad * cz})
|
|
}
|
|
}
|
|
at := func(i, j int) int { return (i%segments)*rings + j%rings }
|
|
for i := 0; i < segments; i++ {
|
|
for j := 0; j < rings; j++ {
|
|
a, b := at(i, j), at(i+1, j)
|
|
c, d := at(i+1, j+1), at(i, j+1)
|
|
m.Tris = append(m.Tris, Triangle{a, b, c}, Triangle{a, c, d})
|
|
}
|
|
}
|
|
return ensureOutward(m)
|
|
}
|