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:
179
mesh-tool/mesh/primitives.go
Normal file
179
mesh-tool/mesh/primitives.go
Normal file
@@ -0,0 +1,179 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user