- 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
123 lines
2.6 KiB
Go
123 lines
2.6 KiB
Go
package mesh
|
|
|
|
import "math"
|
|
|
|
// Vec3 is a point or direction in 3D space.
|
|
type Vec3 struct{ X, Y, Z float64 }
|
|
|
|
func (a Vec3) Add(b Vec3) Vec3 { return Vec3{a.X + b.X, a.Y + b.Y, a.Z + b.Z} }
|
|
func (a Vec3) Sub(b Vec3) Vec3 { return Vec3{a.X - b.X, a.Y - b.Y, a.Z - b.Z} }
|
|
func (a Vec3) Mul(s float64) Vec3 { return Vec3{a.X * s, a.Y * s, a.Z * s} }
|
|
func (a Vec3) Dot(b Vec3) float64 { return a.X*b.X + a.Y*b.Y + a.Z*b.Z }
|
|
func (a Vec3) Len() float64 { return math.Sqrt(a.Dot(a)) }
|
|
func (a Vec3) Cross(b Vec3) Vec3 {
|
|
return Vec3{
|
|
a.Y*b.Z - a.Z*b.Y,
|
|
a.Z*b.X - a.X*b.Z,
|
|
a.X*b.Y - a.Y*b.X,
|
|
}
|
|
}
|
|
|
|
// Norm returns the unit vector, or the zero vector for zero-length input.
|
|
func (a Vec3) Norm() Vec3 {
|
|
l := a.Len()
|
|
if l == 0 {
|
|
return Vec3{}
|
|
}
|
|
return a.Mul(1 / l)
|
|
}
|
|
|
|
// Min/Max return the component-wise minimum/maximum.
|
|
func (a Vec3) Min(b Vec3) Vec3 {
|
|
return Vec3{math.Min(a.X, b.X), math.Min(a.Y, b.Y), math.Min(a.Z, b.Z)}
|
|
}
|
|
func (a Vec3) Max(b Vec3) Vec3 {
|
|
return Vec3{math.Max(a.X, b.X), math.Max(a.Y, b.Y), math.Max(a.Z, b.Z)}
|
|
}
|
|
|
|
// Mat4 is a row-major 4x4 transform matrix.
|
|
type Mat4 [16]float64
|
|
|
|
func Identity() Mat4 {
|
|
return Mat4{
|
|
1, 0, 0, 0,
|
|
0, 1, 0, 0,
|
|
0, 0, 1, 0,
|
|
0, 0, 0, 1,
|
|
}
|
|
}
|
|
|
|
// Mul returns m * n (n is applied first when transforming points).
|
|
func (m Mat4) Mul(n Mat4) Mat4 {
|
|
var r Mat4
|
|
for row := 0; row < 4; row++ {
|
|
for col := 0; col < 4; col++ {
|
|
sum := 0.0
|
|
for k := 0; k < 4; k++ {
|
|
sum += m[row*4+k] * n[k*4+col]
|
|
}
|
|
r[row*4+col] = sum
|
|
}
|
|
}
|
|
return r
|
|
}
|
|
|
|
// Apply transforms a point (w = 1).
|
|
func (m Mat4) Apply(v Vec3) Vec3 {
|
|
return Vec3{
|
|
m[0]*v.X + m[1]*v.Y + m[2]*v.Z + m[3],
|
|
m[4]*v.X + m[5]*v.Y + m[6]*v.Z + m[7],
|
|
m[8]*v.X + m[9]*v.Y + m[10]*v.Z + m[11],
|
|
}
|
|
}
|
|
|
|
// Det3 is the determinant of the upper-left 3x3. Negative means the
|
|
// transform mirrors space, which flips triangle winding.
|
|
func (m Mat4) Det3() float64 {
|
|
return m[0]*(m[5]*m[10]-m[6]*m[9]) -
|
|
m[1]*(m[4]*m[10]-m[6]*m[8]) +
|
|
m[2]*(m[4]*m[9]-m[5]*m[8])
|
|
}
|
|
|
|
func Translate(t Vec3) Mat4 {
|
|
m := Identity()
|
|
m[3], m[7], m[11] = t.X, t.Y, t.Z
|
|
return m
|
|
}
|
|
|
|
func ScaleXYZ(s Vec3) Mat4 {
|
|
m := Identity()
|
|
m[0], m[5], m[10] = s.X, s.Y, s.Z
|
|
return m
|
|
}
|
|
|
|
func RotateX(deg float64) Mat4 {
|
|
s, c := math.Sincos(deg * math.Pi / 180)
|
|
return Mat4{
|
|
1, 0, 0, 0,
|
|
0, c, -s, 0,
|
|
0, s, c, 0,
|
|
0, 0, 0, 1,
|
|
}
|
|
}
|
|
|
|
func RotateY(deg float64) Mat4 {
|
|
s, c := math.Sincos(deg * math.Pi / 180)
|
|
return Mat4{
|
|
c, 0, s, 0,
|
|
0, 1, 0, 0,
|
|
-s, 0, c, 0,
|
|
0, 0, 0, 1,
|
|
}
|
|
}
|
|
|
|
func RotateZ(deg float64) Mat4 {
|
|
s, c := math.Sincos(deg * math.Pi / 180)
|
|
return Mat4{
|
|
c, -s, 0, 0,
|
|
s, c, 0, 0,
|
|
0, 0, 1, 0,
|
|
0, 0, 0, 1,
|
|
}
|
|
}
|