Files
sanspie 17cb5e4291 Stage 2-5: assets, scene/ECS, editor, 3D, and Aego Script
Stage 2: render pipeline and UI toolkit over gpu/platform.
Stage 3: schema registry, asset pipeline + runtime loader, scene graph and ECS.
Stage 4: editor — docking, inspector, scene/game views, play mode, asset browser, console, profiler; 4.5 adds the 3D foundation (mixed 2D/3D spaces, meshes, depth).
Stage 5: Aego Script — compiler (lexer/parser/types/resolver/checker/asm/compiler), bytecode with verifier and register VM, instance runtime, stdlib, native bindings, and editor/CLI integration (aego run/edit/check/test/dump).
2026-10-08 13:17:28 +03:00

137 lines
3.9 KiB
Go

package mesh
import (
"testing"
"aego/core/mathx"
"aego/gpu"
"aego/gpu/null"
"aego/render/sprite"
"aego/render/texture"
)
func outward(t *testing.T, name string, d *Data) {
t.Helper()
for i := 0; i < len(d.Indices); i += 3 {
a := d.Vertices[d.Indices[i]].Pos
b := d.Vertices[d.Indices[i+1]].Pos
c := d.Vertices[d.Indices[i+2]].Pos
n := b.Sub(a).Cross(c.Sub(a))
if n.LenSq() < 1e-12 {
continue
}
centroid := a.Add(b).Add(c).Scale(1.0 / 3)
if n.Dot(centroid) <= 0 {
t.Fatalf("%s: triangle %d faces inward", name, i/3)
}
}
}
func TestPrimitives(t *testing.T) {
cube := Cube(2)
if len(cube.Vertices) != 24 || len(cube.Indices) != 36 {
t.Fatalf("cube = %d/%d", len(cube.Vertices), len(cube.Indices))
}
if cube.Bounds != (mathx.AABB3{Min: mathx.Vec3{X: -1, Y: -1, Z: -1}, Max: mathx.Vec3{X: 1, Y: 1, Z: 1}}) {
t.Fatalf("cube bounds = %+v", cube.Bounds)
}
outward(t, "cube", cube)
outward(t, "sphere", Sphere(1, 12, 16))
outward(t, "cylinder", Cylinder(0.5, 2, 16))
p := Plane(4, 2, 3)
for i := 0; i < len(p.Indices); i += 3 {
a, b, c := p.Vertices[p.Indices[i]].Pos, p.Vertices[p.Indices[i+1]].Pos, p.Vertices[p.Indices[i+2]].Pos
if b.Sub(a).Cross(c.Sub(a)).Y <= 0 {
t.Fatal("plane must face +Y")
}
}
for _, d := range []*Data{cube, Sphere(1, 8, 8), Cylinder(1, 1, 8), p} {
if err := d.Validate(); err != nil {
t.Fatal(err)
}
}
}
func TestAEMSRoundTrip(t *testing.T) {
src := Sphere(1.5, 6, 8)
back, err := Decode(Encode(src))
if err != nil {
t.Fatal(err)
}
if back.Bounds != src.Bounds || len(back.Vertices) != len(src.Vertices) || len(back.Indices) != len(src.Indices) {
t.Fatal("header lost")
}
for i := range src.Vertices {
if back.Vertices[i] != src.Vertices[i] {
t.Fatalf("vertex %d differs", i)
}
}
bad := Encode(Cube(1))
bad[len(bad)-4] = 200
if _, err := Decode(bad); err == nil {
t.Fatal("out of range index accepted")
}
if _, err := Decode([]byte("AEMS")); err == nil {
t.Fatal("truncated artifact accepted")
}
if _, err := Decode(Encode(Cube(1))[:60]); err == nil {
t.Fatal("short body accepted")
}
}
func TestUploadAndDestroy(t *testing.T) {
dev := null.New()
m, err := Upload(dev, Cube(1), "cube")
if err != nil {
t.Fatal(err)
}
if m.Count != 36 || m.VB.IsZero() || m.IB.IsZero() {
t.Fatalf("mesh = %+v", m)
}
m.Destroy(dev)
if !m.VB.IsZero() {
t.Fatal("destroy did not reset the handles")
}
if _, err := Upload(dev, &Data{}, "empty"); err == nil {
t.Fatal("empty mesh uploaded")
}
}
func TestBatcherOrdering(t *testing.T) {
m := &Mesh{Count: 36, Bounds: Cube(1).Bounds}
white := texture.Whole(gpu.TextureHandle{Index: 1, Gen: 1}, 1, 1)
b := NewBatcher(16)
cam := mathx.Vec3{Z: 10}
push := func(z float32, blend sprite.BlendMode, ds bool) {
d := Draw{Mesh: m, World: mathx.Mat4Translate(mathx.Vec3{Z: z}), Blend: blend, DoubleSided: ds}
if !b.Push(&d, cam, white, gpu.SamplerHandle{}) {
t.Fatal("push failed")
}
}
push(0, sprite.BlendOpaque, false)
push(5, sprite.BlendOpaque, false)
push(8, sprite.BlendOpaque, true)
push(-5, sprite.BlendOpaque, false)
push(0, sprite.BlendPremultiplied, false)
push(5, sprite.BlendPremultiplied, false)
push(-5, sprite.BlendAdditive, false)
b.Sort()
op := b.Opaque()
if len(op) != 4 || op[0].World.Translation().Z != 5 || op[1].World.Translation().Z != 0 || op[2].World.Translation().Z != -5 || !op[3].DoubleSided {
t.Fatal("opaque must be front to back with double sided last")
}
tr := b.Transparent()
if len(tr) != 3 || tr[0].World.Translation().Z != -5 || tr[2].World.Translation().Z != 5 {
t.Fatal("transparent must be back to front")
}
if op[0].Texture != white {
t.Fatal("missing texture must default to white")
}
small := NewBatcher(1)
d := Draw{Mesh: m, World: mathx.Mat4Identity()}
small.Push(&d, cam, white, gpu.SamplerHandle{})
if small.Push(&d, cam, white, gpu.SamplerHandle{}) || small.Dropped() != 1 {
t.Fatal("budget overflow not counted")
}
}