203 lines
5.8 KiB
Go
203 lines
5.8 KiB
Go
package sceneview
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"aego/asset"
|
|
"aego/core/ids"
|
|
"aego/core/mathx"
|
|
"aego/editor/core/gizmo"
|
|
"aego/editor/core/props"
|
|
"aego/editor/core/shape"
|
|
"aego/scene"
|
|
)
|
|
|
|
func spatialScene(t *testing.T) (*scene.Scene, asset.Resolver) {
|
|
t.Helper()
|
|
return scene.New(testTypes(t), nil, scene.Options{DefaultSpace: scene.Space3D}), nil
|
|
}
|
|
|
|
func cube(s *scene.Scene, at mathx.Vec3) scene.NodeHandle {
|
|
h := s.Create3D(s.Root(), "Cube")
|
|
*scene.Add[Block](s, h) = Block{Half: 0.5}
|
|
s.SetPosition3D(h, at)
|
|
return h
|
|
}
|
|
|
|
type fakeHost struct {
|
|
s *scene.Scene
|
|
res asset.Resolver
|
|
sh shape.Set
|
|
sel []ids.NodeID
|
|
op gizmo.Op3
|
|
commits [][]props.Transform3DEdit
|
|
}
|
|
|
|
func (h *fakeHost) Scene() *scene.Scene { return h.s }
|
|
func (h *fakeHost) Shapes() shape.Set { return h.sh }
|
|
func (h *fakeHost) Selected() []ids.NodeID { return h.sel }
|
|
func (h *fakeHost) Tool3() gizmo.Op3 { return h.op }
|
|
func (h *fakeHost) ShowGizmo() bool { return true }
|
|
func (h *fakeHost) LocalAxes() bool { return false }
|
|
func (h *fakeHost) Snap3() gizmo.Snap3 { return gizmo.Snap3{} }
|
|
func (h *fakeHost) Select(n []ids.NodeID, add bool) {
|
|
if add {
|
|
h.sel = append(h.sel, n...)
|
|
return
|
|
}
|
|
h.sel = append([]ids.NodeID(nil), n...)
|
|
}
|
|
func (h *fakeHost) Commit3D(_ string, e []props.Transform3DEdit) {
|
|
h.commits = append(h.commits, e)
|
|
for _, x := range e {
|
|
h.s.SetLocal3D(h.s.Handle(x.Node), x.After)
|
|
}
|
|
}
|
|
|
|
func frontController() *Controller3 {
|
|
c := NewController3()
|
|
c.Cam = Camera3D{Distance: 10, FOV: 60}
|
|
c.SetViewport(800, 600)
|
|
return c
|
|
}
|
|
|
|
func screenOf(t *testing.T, c *Controller3, p mathx.Vec3) mathx.Vec2 {
|
|
t.Helper()
|
|
q, ok := c.View().WorldToScreen3(p)
|
|
if !ok {
|
|
t.Fatalf("%v is not on screen", p)
|
|
}
|
|
return q
|
|
}
|
|
|
|
func TestEyeLooksAtTarget(t *testing.T) {
|
|
c := DefaultCamera3D()
|
|
c.Target = mathx.Vec3{X: 1, Y: 2, Z: 3}
|
|
eye := c.Eye()
|
|
fwd := eye.TransformDir(mathx.Vec3{Z: -1}).Normalized()
|
|
to := c.Target.Sub(eye.Translation()).Normalized()
|
|
if !fwd.NearlyEqual(to, 1e-5) {
|
|
t.Fatalf("forward %v, to target %v", fwd, to)
|
|
}
|
|
c.Orbit(0, -1e6)
|
|
if c.Pitch > maxPitch {
|
|
t.Fatal("pitch not clamped")
|
|
}
|
|
}
|
|
|
|
func TestPanAndDolly(t *testing.T) {
|
|
c := DefaultCamera3D()
|
|
before := c
|
|
c.Pan(30, -20, 600)
|
|
if d := c.Target.Sub(before.Target); mathx.Abs(d.Dot(c.Forward())) > 1e-5 || d.Len() == 0 {
|
|
t.Fatalf("pan moved along forward: %v", d)
|
|
}
|
|
c.Dolly(1e6)
|
|
if c.Distance != minDistance {
|
|
t.Fatalf("dolly not clamped: %v", c.Distance)
|
|
}
|
|
}
|
|
|
|
func TestFrameFitsBox(t *testing.T) {
|
|
c := frontController()
|
|
b := mathx.AABB3{Min: mathx.Vec3{X: 9, Y: -1, Z: -1}, Max: mathx.Vec3{X: 11, Y: 1, Z: 1}}
|
|
c.Cam.Frame(b)
|
|
v := c.View()
|
|
for _, x := range []float32{b.Min.X, b.Max.X} {
|
|
for _, y := range []float32{b.Min.Y, b.Max.Y} {
|
|
for _, z := range []float32{b.Min.Z, b.Max.Z} {
|
|
p, ok := v.WorldToScreen3(mathx.Vec3{X: x, Y: y, Z: z})
|
|
if !ok || p.X < 0 || p.X > 800 || p.Y < 0 || p.Y > 600 {
|
|
t.Fatalf("corner off screen: %v %v", p, ok)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPick3DNearestAndDisabled(t *testing.T) {
|
|
s, res := spatialScene(t)
|
|
a := cube(s, mathx.Vec3{})
|
|
b := cube(s, mathx.Vec3{Z: -5})
|
|
s.Flush()
|
|
ray := mathx.Ray3{Origin: mathx.Vec3{Z: 10}, Dir: mathx.Vec3{Z: -1}}
|
|
h, tt, ok := Pick3D(s, testShapes(t, s, res), ray, ^uint64(0))
|
|
if !ok || h != a || mathx.Abs(tt-9.5) > 1e-4 {
|
|
t.Fatalf("pick = %v %v %v", h, tt, ok)
|
|
}
|
|
s.SetEnabled(a, false)
|
|
s.UpdateTransforms()
|
|
if h, _, _ := Pick3D(s, testShapes(t, s, res), ray, ^uint64(0)); h != b {
|
|
t.Fatal("disabled cube was picked")
|
|
}
|
|
}
|
|
|
|
func TestPickPointAndBoxSelect(t *testing.T) {
|
|
s, _ := spatialScene(t)
|
|
e := s.Create3D(s.Root(), "Empty")
|
|
s.SetPosition3D(e, mathx.Vec3{X: 3})
|
|
behind := s.Create3D(s.Root(), "Behind")
|
|
s.SetPosition3D(behind, mathx.Vec3{Z: 20})
|
|
s.Flush()
|
|
o := mathx.Vec3{Z: 10}
|
|
ray := mathx.Ray3{Origin: o, Dir: mathx.Vec3{X: 3}.Sub(o).Normalized()}
|
|
h, _, ok := PickPoint3D(s, testShapes(t, s, nil), ray, ^uint64(0), func(mathx.Vec3) float32 { return 0.1 })
|
|
if !ok || h != e {
|
|
t.Fatal("empty node not picked")
|
|
}
|
|
c := frontController()
|
|
got := BoxSelect3D(s, shape.Set{}, c.View(), mathx.Vec2{}, mathx.Vec2{X: 800, Y: 600}, ^uint64(0))
|
|
if len(got) != 1 || got[0] != s.ID(e) {
|
|
t.Fatalf("box select = %v", got)
|
|
}
|
|
}
|
|
|
|
func TestGizmoDragCommitsOnce(t *testing.T) {
|
|
s, res := spatialScene(t)
|
|
n := s.Create3D(s.Root(), "N")
|
|
s.Flush()
|
|
h := &fakeHost{s: s, res: res, sh: testShapes(t, s, res), sel: []ids.NodeID{s.ID(n)}}
|
|
c := frontController()
|
|
l := gizmo.PxToWorld(c.View(), mathx.Vec3{}, gizmo.DefaultSize3)
|
|
grab := screenOf(t, c, mathx.Vec3{X: l * 0.7})
|
|
c.Update(h, Input3{Mouse: grab, Hovered: true})
|
|
if c.Hot() != gizmo.Part3X {
|
|
t.Fatalf("hot = %d", c.Hot())
|
|
}
|
|
c.Update(h, Input3{Mouse: grab, Hovered: true, Down: [3]bool{true}, Pressed: [3]bool{true}})
|
|
to := screenOf(t, c, mathx.Vec3{X: l*0.7 + 1})
|
|
c.Update(h, Input3{Mouse: to, Hovered: true, Down: [3]bool{true}})
|
|
if got := s.Local3D(n).Position.X; mathx.Abs(got-1) > 1e-2 {
|
|
t.Fatalf("preview x = %v", got)
|
|
}
|
|
c.Update(h, Input3{Mouse: to, Hovered: true})
|
|
if len(h.commits) != 1 || len(h.commits[0]) != 1 {
|
|
t.Fatalf("commits = %+v", h.commits)
|
|
}
|
|
e := h.commits[0][0]
|
|
if e.Before.Position != (mathx.Vec3{}) || !e.After.Position.NearlyEqual(mathx.Vec3{X: 1}, 1e-2) {
|
|
t.Fatalf("edit = %+v", e)
|
|
}
|
|
}
|
|
|
|
func TestClickSelectsAndClears(t *testing.T) {
|
|
s, res := spatialScene(t)
|
|
a := cube(s, mathx.Vec3{})
|
|
cube(s, mathx.Vec3{Z: -5})
|
|
s.Flush()
|
|
h := &fakeHost{s: s, res: res, sh: testShapes(t, s, res)}
|
|
c := frontController()
|
|
click := func(p mathx.Vec2) {
|
|
c.Update(h, Input3{Mouse: p, Hovered: true, Down: [3]bool{true}, Pressed: [3]bool{true}})
|
|
c.Update(h, Input3{Mouse: p, Hovered: true})
|
|
}
|
|
click(screenOf(t, c, mathx.Vec3{}))
|
|
if len(h.sel) != 1 || h.sel[0] != s.ID(a) {
|
|
t.Fatalf("selection = %v", h.sel)
|
|
}
|
|
click(mathx.Vec2{X: 5, Y: 5})
|
|
if len(h.sel) != 0 {
|
|
t.Fatal("click on empty space did not clear")
|
|
}
|
|
}
|