Files

147 lines
4.4 KiB
Go

package world
import (
"slices"
"testing"
)
func collideGeoms(a *geom, xa xform, b *geom, xb xform) Manifold {
var m Manifold
ga, gb := a.c, b.c
collideConvex(&ga, xa, &gb, xb, 0.02, &m)
return m
}
func TestCircleCircle(t *testing.T) {
c := gm(t, Circle{R: 1}, Transform{})
for i := 0; i < 20; i++ {
a := float32(i) * 0.31
n := makeRot(a).apply(Vec2{X: 1})
m := collideGeoms(&c, xfAt(0, 0, 0), &c, xfAt(1.5*n.X, 1.5*n.Y, a))
if m.Count != 1 || !near(m.Points[0].Separation, -0.5, 1e-5) || !near2(m.Normal, n, 1e-5) || !near2(m.Points[0].Point, n.Scale(0.75), 1e-5) {
t.Fatalf("pose %d: %+v", i, m)
}
}
}
func TestBoxBox(t *testing.T) {
box := gm(t, Box{Half: Vec2{X: 1, Y: 1}}, Transform{})
for i := 0; i < 20; i++ {
th := float32(i) * 0.3
x := -0.5 + float32(i)*0.05
q := makeRot(th)
pb := q.apply(Vec2{X: x, Y: 1.9})
xa, xb := xfAt(0, 0, th), xfAt(pb.X, pb.Y, th)
m := collideGeoms(&box, xa, &box, xb)
if m.Count != 2 || !near2(m.Normal, q.apply(Vec2{Y: 1}), 1e-4) {
t.Fatalf("pose %d: %+v", i, m)
}
want := []float32{max(-1, x-1), min(1, x+1)}
for k := 0; k < 2; k++ {
p := xa.invApply(m.Points[k].Point)
if !near(m.Points[k].Separation, -0.1, 1e-4) || !near(p.Y, 0.95, 1e-3) {
t.Fatalf("pose %d point %d: %+v local %v", i, k, m.Points[k], p)
}
if !near(p.X, want[0], 1e-3) && !near(p.X, want[1], 1e-3) {
t.Fatalf("pose %d point %d x %v, want one of %v", i, k, p.X, want)
}
}
}
}
func TestCircleBoxRegions(t *testing.T) {
box := gm(t, Box{Half: Vec2{X: 1, Y: 1}}, Transform{})
circle := gm(t, Circle{R: 0.5}, Transform{})
m := collideGeoms(&box, identityXf(), &circle, xfAt(0.3, 1.4, 0))
if m.Count != 1 || !near(m.Points[0].Separation, -0.1, 1e-5) || !near2(m.Normal, Vec2{Y: 1}, 1e-5) {
t.Fatalf("face: %+v", m)
}
m = collideGeoms(&box, identityXf(), &circle, xfAt(1.3, 1.3, 0))
d := Vec2{X: 0.3, Y: 0.3}
if m.Count != 1 || !near(m.Points[0].Separation, d.Len()-0.5, 1e-5) || !near2(m.Normal, d.Normalized(), 1e-5) {
t.Fatalf("vertex: %+v", m)
}
m = collideGeoms(&circle, xfAt(0.3, 1.4, 0), &box, identityXf())
if m.Count != 1 || !near2(m.Normal, Vec2{Y: -1}, 1e-5) {
t.Fatalf("flipped: %+v", m)
}
}
func TestCapsuleBox(t *testing.T) {
box := gm(t, Box{Half: Vec2{X: 1, Y: 1}}, Transform{})
cap := gm(t, Capsule{HalfLen: 1, R: 0.25}, Transform{})
m := collideGeoms(&box, identityXf(), &cap, xfAt(0, 1.2, 0))
if m.Count != 2 || !near2(m.Normal, Vec2{Y: 1}, 1e-4) {
t.Fatalf("%+v", m)
}
for k := 0; k < 2; k++ {
if !near(m.Points[k].Separation, -0.05, 1e-4) {
t.Fatalf("point %d: %+v", k, m.Points[k])
}
}
}
func TestFeatureIDsAreStable(t *testing.T) {
box := gm(t, Box{Half: Vec2{X: 1, Y: 1}}, Transform{})
ids := func(x float32) []uint16 {
m := collideGeoms(&box, identityXf(), &box, xfAt(x, 1.95, 0.01))
out := make([]uint16, m.Count)
for i := range out {
out[i] = m.Points[i].ID
}
slices.Sort(out)
return out
}
if a, b := ids(0.1), ids(0.1005); !slices.Equal(a, b) || len(a) != 2 {
t.Fatalf("ids changed: %v %v", a, b)
}
}
func TestChainGhostsOnJunction(t *testing.T) {
chain := gm(t, Chain{Pts: []Vec2{{X: -4}, {}, {X: 4}}}, Transform{})
cap := gm(t, Capsule{HalfLen: 0.5, R: 0.25}, Transform{})
for x := float32(-1.5); x <= 1.5; x += 0.05 {
hits := 0
for i := 0; i < chain.children(); i++ {
seg, g0, g3 := chain.chain.segment(i)
var m Manifold
collideChainSegment(&seg, g0, g3, true, identityXf(), &cap.c, xfAt(x, -0.24, 0), 0.02, &m)
if m.Count == 0 {
continue
}
hits++
if m.Normal.Dot(Vec2{Y: -1}) < 0.999 {
t.Fatalf("x=%v segment %d: false normal %v", x, i, m.Normal)
}
}
if hits == 0 {
t.Fatalf("x=%v: capsule lost contact with the ground", x)
}
}
}
func TestOneSidedIgnoresBack(t *testing.T) {
chain := gm(t, Chain{Pts: []Vec2{{X: -4}, {}, {X: 4}}}, Transform{})
circle := gm(t, Circle{R: 0.6}, Transform{})
for _, oneSided := range []bool{true, false} {
hits := 0
for i := 0; i < chain.children(); i++ {
seg, g0, g3 := chain.chain.segment(i)
var m Manifold
collideChainSegment(&seg, g0, g3, oneSided, identityXf(), &circle.c, xfAt(0.5, 0.5, 0), 0.02, &m)
if m.Count > 0 {
hits++
if !near2(m.Normal, Vec2{Y: 1}, 1e-3) {
t.Fatalf("two-sided normal %v", m.Normal)
}
}
}
if oneSided && hits != 0 {
t.Fatal("one-sided chain caught a circle from behind")
}
if !oneSided && hits == 0 {
t.Fatal("two-sided chain missed the circle")
}
}
}