Files
aego/app/edit/png_frames_workflow_gl_test.go

247 lines
8.7 KiB
Go

//go:build aego_gltest && linux && cgo && !aego_nonative
package edit
import (
"bytes"
"image"
"image/png"
"os"
"path/filepath"
"runtime"
"testing"
"aego/art"
"aego/asset"
"aego/asset/importers/artsource"
"aego/asset/pipeline"
"aego/core/log"
"aego/editor/core/artedit"
"aego/modules/render2d"
"aego/platform/input"
"aego/platform/sdl"
"aego/scene"
"aego/scene/sceneio"
)
// The example project is copied to a temporary directory. Project/scene and
// image opening use the Go API; first Save and drawing use SDL events. This
// is an offscreen/software-runner workflow, not physical OS acceptance.
func TestPlatformerSlicedPNGCopyNative(t *testing.T) {
platformerSlicedPNGCopyNative(t, false)
}
// This extended route explicitly assigns a new controller in a disposable
// project; it never silently rewrites the original Player or its PNG assets.
func TestPlatformerSlicedPNGGameUndoExportNative(t *testing.T) {
platformerSlicedPNGCopyNative(t, true)
}
func platformerSlicedPNGCopyNative(t *testing.T, fullWorkflow bool) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
t.Setenv("SDL_AUDIODRIVER", "dummy")
p := platformerAuthoringProject(t)
if fullWorkflow {
preparePlatformerNativeExportProject(t, p)
}
s, err := Open(p, Config{Hidden: true, NoWatch: true, Width: 1280, Height: 900, Logger: log.Nop()}, platformerAuthoringModule{})
if err != nil {
t.Fatal(err)
}
defer s.Close()
if !s.Step() {
t.Fatal("editor stopped during startup")
}
ed := s.App.Context()
sceneDoc, err := ed.Docs.Open("scenes/main.aescene")
if err != nil {
t.Fatal(err)
}
if !s.Step() {
t.Fatal("editor stopped after opening the scene")
}
beforeScene := sceneio.Save(sceneDoc.Scene())
hero := sceneDoc.Scene().Find(sceneDoc.Scene().Root(), "Player")
sprite, ok := scene.Get[render2d.SpriteRenderer](sceneDoc.Scene(), hero)
if !ok {
t.Fatal("the real platformer Player has no sprite")
}
beforeSprite := sprite.Sprite.ID
paths := []string{"assets/surfaces/runner.png", "assets/surfaces/runner.png.meta", "assets/surfaces/runner_n.png", "assets/surfaces/runner_n.png.meta", "scenes/main.aescene"}
before := make(map[string][]byte, len(paths))
for _, rel := range paths {
before[rel], err = os.ReadFile(filepath.Join(p.Root, rel))
if err != nil {
t.Fatal(err)
}
}
metaBefore, err := pipeline.ParseMeta(before[paths[1]], paths[1])
if err != nil || len(metaBefore.Subs) != 8 {
t.Fatal("the original runner needs eight indexed frames", err)
}
imageDoc, err := ed.OpenAsset(beforeSprite)
if err != nil || imageDoc == nil || imageDoc.Path != "assets/surfaces/runner.png" {
t.Fatalf("Player sprite did not open its PNG source: %+v, %v", imageDoc, err)
}
model, ok := imageDoc.Binding.Model.(*artedit.Document)
if !ok || !model.Draft || imageDoc.Dirty() || len(model.Session.Document.Info().Frames) != 8 {
t.Fatal("Player sprite did not open as eight editable frames in memory")
}
for _, rel := range paths {
after, err := os.ReadFile(filepath.Join(p.Root, rel))
if err != nil || !bytes.Equal(before[rel], after) {
t.Fatalf("opening the PNG changed %s: %v", rel, err)
}
}
ws := s.App.Workspace()
panelID := "render2d.art/" + imageDoc.Asset.String()
ws.Focus(panelID)
if ws.Tree.Maximized() != panelID && !ws.ToggleMaximize(panelID) {
t.Fatal("could not maximize the Art Canvas")
}
for range 4 {
if !s.Step() {
t.Fatal("editor stopped before first Save")
}
}
capture := func(name string) {
t.Helper()
dir := os.Getenv("AEGO_PNG_FRAMES_SHOTS")
if dir == "" {
return
}
var shot *image.RGBA
var captureErr error
s.Engine.CaptureFrame(func(img *image.RGBA, err error) { shot, captureErr = img, err })
if !s.Step() || captureErr != nil || shot == nil {
t.Fatalf("capture %s: %v", name, captureErr)
}
if err := os.MkdirAll(dir, 0755); err != nil {
t.Fatal(err)
}
f, err := os.Create(filepath.Join(dir, name+".png"))
if err != nil {
t.Fatal(err)
}
err = png.Encode(f, shot)
closeErr := f.Close()
if err != nil || closeErr != nil {
t.Fatalf("write %s: %v, %v", name, err, closeErr)
}
}
capture("art-draft")
key := func(k input.Scancode, ctrl bool) {
t.Helper()
if !sdl.TestEditKey(k, true, ctrl, false) || !s.Step() || !sdl.TestEditKey(k, false, false, false) || !s.Step() {
t.Fatal("native keyboard event failed")
}
}
key(input.KeyS, true)
if !ed.UI.ModalActive() {
t.Fatal("first Save did not ask before linking the source")
}
capture("first-save")
const copyPath = "assets/surfaces/runner.aeart"
if data, err := s.Pipeline.SourceBytes(copyPath); err != nil || data != nil {
t.Fatal("the editable source appeared before confirmation", err)
}
if current, err := os.ReadFile(filepath.Join(p.Root, paths[1])); err != nil || !bytes.Equal(current, before[paths[1]]) {
t.Fatal("the PNG metadata changed before confirmation", err)
}
key(input.KeyEnter, false)
id, ok := s.Pipeline.Lookup(copyPath)
if !ok {
notice, _ := ed.LastNotice()
t.Fatalf("confirmed editable source was not imported: %+v", notice)
}
copyDoc := ed.Docs.AssetDocument(id)
if copyDoc == nil {
t.Fatal("the linked drawing was not opened")
}
if copyDoc.Binding.Model != model || model.Draft || copyDoc.Dirty() || len(model.Session.Document.Info().Frames) != 8 {
t.Fatal("first Save did not transfer the same clean eight-frame session")
}
metaAfter, ok := s.Pipeline.MetaOf(imageDoc.Asset)
link, err := artsource.TextureLinkOf(&metaAfter)
if !ok || err != nil || link == nil || link.Source != id || len(link.Frames) != len(metaBefore.Subs) || len(metaAfter.Subs) != len(metaBefore.Subs) {
t.Fatal("first Save did not create the guarded PNG frame link", err)
}
for i, original := range metaBefore.Subs {
current := metaAfter.Subs[i]
if original.ID != current.ID || original.Name != current.Name || original.Rect != current.Rect || original.Pivot != current.Pivot || original.HasPivot != current.HasPivot || link.Frames[i].Asset != original.ID {
t.Fatalf("frame %d lost its source identity, slicing or pivot", i)
}
for _, channel := range []string{"normal", "height", "emission"} {
oldID, oldExplicit, oldErr := asset.TextureChannel(metaBefore, original.ID, channel)
newID, newExplicit, newErr := asset.TextureChannel(&metaAfter, current.ID, channel)
if oldErr != nil || newErr != nil || oldID != newID || oldExplicit != newExplicit {
t.Fatalf("frame %d lost its %s association: %v, %v", i, channel, oldErr, newErr)
}
}
if _, present := s.Pipeline.Entry(original.ID); !present {
t.Fatalf("the original runtime frame %s disappeared", original.ID)
}
}
// Subsequent checks compare against the explicitly confirmed sidecar, while
// all PNG bytes, surface sources and scene bytes remain unchanged.
before[paths[1]], err = os.ReadFile(filepath.Join(p.Root, paths[1]))
if err != nil {
t.Fatal(err)
}
artPanel := "render2d.art/" + id.String()
if current := ws.Tree.Maximized(); current != "" && current != artPanel {
ws.ToggleMaximize(current)
}
ws.Focus(artPanel)
if ws.Tree.Maximized() != artPanel && !ws.ToggleMaximize(artPanel) {
t.Fatal("could not maximize Art Studio")
}
for range 3 {
s.Step()
}
first := model.Session.Document.Info().Frames[0].ID
if err := model.Session.Select("", first, art.Albedo); err != nil {
t.Fatal(err)
}
original, _, err := model.Capture(first, art.Albedo)
if err != nil {
t.Fatal(err)
}
for i := range 3 {
if !sdl.TestPointerEvent(500, 400, i) || !s.Step() {
t.Fatal("could not paint the imported frame")
}
}
painted, _, err := model.Capture(first, art.Albedo)
if err != nil || bytes.Equal(painted.Pix, original.Pix) || model.History.Cursor() != 1 {
t.Fatalf("native pointer stroke did not edit the imported frame: %v", err)
}
capture("painted")
key(input.KeyZ, true)
undone, _, err := model.Capture(first, art.Albedo)
if err != nil || !bytes.Equal(undone.Pix, original.Pix) {
t.Fatalf("native Undo did not restore the frame: %v", err)
}
for _, rel := range paths {
after, err := os.ReadFile(filepath.Join(p.Root, rel))
if err != nil || !bytes.Equal(before[rel], after) {
t.Fatalf("conversion changed original %s: %v", rel, err)
}
}
if sceneDoc.Dirty() || !bytes.Equal(sceneio.Save(sceneDoc.Scene()), beforeScene) || sprite.Sprite.ID != beforeSprite {
t.Fatal("conversion changed the real Player scene or its sprite reference")
}
if fullWorkflow {
platformerSlicedPNGGameUndoExport(t, p, s, sceneDoc, copyDoc, model, original.Pix, painted.Pix, key)
// The test explicitly edits the copied scene's Animator, but the PNG,
// its sidecar and normal channel must remain byte-identical throughout.
for _, rel := range paths[:len(paths)-1] {
after, err := os.ReadFile(filepath.Join(p.Root, rel))
if err != nil || !bytes.Equal(before[rel], after) {
t.Fatalf("Player game/export workflow changed original %s: %v", rel, err)
}
}
}
}