243 lines
8.5 KiB
Go
243 lines
8.5 KiB
Go
package editor
|
|
|
|
import (
|
|
"bytes"
|
|
stdctx "context"
|
|
"image"
|
|
"image/color"
|
|
"image/png"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"aego/art"
|
|
"aego/asset/importers/artsource"
|
|
"aego/asset/pipeline"
|
|
"aego/core/ids"
|
|
"aego/editor/core/artedit"
|
|
"aego/editor/core/assetops"
|
|
"aego/editor/core/context"
|
|
"aego/engine/i18n"
|
|
)
|
|
|
|
func linkedArtEditor(t *testing.T) (*context.Editor, ids.AssetID, [2]ids.AssetID, []byte) {
|
|
t.Helper()
|
|
ed, _, _ := importTestEditor(t)
|
|
pixels := image.NewNRGBA(image.Rect(0, 0, 4, 2))
|
|
for y := 0; y < 2; y++ {
|
|
for x := 0; x < 4; x++ {
|
|
pixels.SetNRGBA(x, y, color.NRGBA{R: uint8(20 + x*40), G: 100, B: 200, A: 255})
|
|
}
|
|
}
|
|
var data bytes.Buffer
|
|
if err := png.Encode(&data, pixels); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
id, err := assetops.CreateFile(ed, "assets/linked.png", data.Bytes())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
before, err := ed.Pipeline.SourceBytes("assets/linked.png.meta")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
meta, err := pipeline.ParseMeta(before, "assets/linked.png.meta")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
frames := [2]ids.AssetID{ids.NewAssetID(), ids.NewAssetID()}
|
|
meta.Subs = []pipeline.SubAsset{
|
|
{ID: frames[0], Name: "idle", Rect: [4]int{0, 0, 2, 2}},
|
|
{ID: frames[1], Name: "run", Rect: [4]int{2, 0, 2, 2}},
|
|
}
|
|
batch, err := ed.Pipeline.ApplySources([]pipeline.SourceEdit{{Path: "assets/linked.png.meta", Before: before, After: pipeline.WriteMeta(meta)}}, false)
|
|
if err != nil || batch == nil || len(batch.Failed) != 0 || len(batch.Retry) != 0 {
|
|
t.Fatalf("cannot prepare fixture: batch=%+v error=%v", batch, err)
|
|
}
|
|
ed.ApplyBatch(batch)
|
|
return ed, id, frames, bytes.Clone(data.Bytes())
|
|
}
|
|
|
|
func linkedSourcePrompt(t *testing.T, ed *context.Editor) context.Question {
|
|
t.Helper()
|
|
qs := ed.TakeQuestions()
|
|
if len(qs) != 1 || qs[0].Then == nil || !strings.Contains(qs[0].Message, "metadata") || !strings.Contains(qs[0].Message, "shared asset") {
|
|
t.Fatalf("missing visible confirmation of metadata and shared asset changes: %+v", qs)
|
|
}
|
|
qs[0].Then(0)
|
|
qs = ed.TakeQuestions()
|
|
if len(qs) != 1 || qs[0].Submit == nil {
|
|
t.Fatalf("missing source path prompt: %+v", qs)
|
|
}
|
|
return qs[0]
|
|
}
|
|
|
|
// Keep exercising the earlier explicit linker independently of the default
|
|
// PNG-open route, which now creates only an in-memory draft until Save.
|
|
func askLegacyLinkedSource(t *testing.T, ed *context.Editor, id ids.AssetID) {
|
|
t.Helper()
|
|
doc, err := ed.DocumentForAsset(id)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
meta, err := pipeline.ParseMeta(doc.Metadata.Code.Bytes(), doc.Metadata.Path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := askLinkedArtSource(ed, doc, meta); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestLinkedSourceConfirmationAndDirtyDocument(t *testing.T) {
|
|
ed, source, frames, original := linkedArtEditor(t)
|
|
askLegacyLinkedSource(t, ed, frames[1])
|
|
qs := ed.TakeQuestions()
|
|
if len(qs) != 1 || qs[0].Then == nil {
|
|
t.Fatalf("missing confirmation: %+v", qs)
|
|
}
|
|
qs[0].Then(1)
|
|
if _, ok := ed.Pipeline.Lookup("assets/linked.aeart"); ok || len(ed.TakeQuestions()) != 0 {
|
|
t.Fatal("cancel created a source or opened a path prompt")
|
|
}
|
|
askLegacyLinkedSource(t, ed, frames[1])
|
|
linkedSourcePrompt(t, ed).Submit([]string{"assets/linked.aeart"})
|
|
if notice, ok := ed.LastNotice(); ok && notice.Level == context.LevelError {
|
|
t.Fatal(notice.Text)
|
|
}
|
|
artID, ok := ed.Pipeline.Lookup("assets/linked.aeart")
|
|
if !ok || artID == source {
|
|
t.Fatal("linked source was not created independently")
|
|
}
|
|
doc := ed.Docs.AssetDocument(artID)
|
|
if doc == nil {
|
|
t.Fatal("linked drawing did not open")
|
|
}
|
|
model, ok := doc.Binding.Model.(*artedit.Document)
|
|
if !ok {
|
|
t.Fatal("linked drawing has no Art Studio model")
|
|
}
|
|
meta, ok := ed.Pipeline.MetaOf(source)
|
|
if !ok {
|
|
t.Fatal("original image metadata disappeared")
|
|
}
|
|
link, err := artsource.TextureLinkOf(&meta)
|
|
if err != nil || link == nil || len(link.Frames) != 2 || model.Session.Frame != link.Frames[1].Frame {
|
|
t.Fatalf("selected source frame was not opened: %v", err)
|
|
}
|
|
if _, err := model.Execute(stdctx.Background(), art.Operation{Kind: "stroke", Points: []image.Point{{X: 1, Y: 1}}, Brush: art.Brush{Size: 1, Paint: art.Paint{Color: art.Color{R: 255, B: 255, A: 255}}}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := ed.EditSourceArt(frames[0]); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if ed.Docs.AssetDocument(artID) != doc || !doc.Dirty() || model.Session.Frame != link.Frames[0].Frame {
|
|
t.Fatal("switching frames lost the canonical dirty document")
|
|
}
|
|
if err := ed.EditSourceArt(frames[1]); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
painted, _, err := model.Capture(model.Session.Frame, art.Albedo)
|
|
if err != nil || painted.NRGBAAt(1, 1) != (color.NRGBA{R: 255, B: 255, A: 255}) {
|
|
t.Fatalf("dirty pixels did not survive frame switching: %v", err)
|
|
}
|
|
if err := ed.UndoAsset(doc); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := ed.RedoAsset(doc); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := ed.SaveAsset(doc); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
batch, err := ed.Pipeline.ImportAll()
|
|
if err != nil || batch == nil || len(batch.Failed) != 0 || len(batch.Retry) != 0 {
|
|
t.Fatalf("saved drawing import failed: batch=%+v error=%v", batch, err)
|
|
}
|
|
if got, err := ed.Pipeline.SourceBytes("assets/linked.png"); err != nil || !bytes.Equal(got, original) {
|
|
t.Fatalf("original PNG was changed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLinkedSourceCreationGuardsConcurrentWork(t *testing.T) {
|
|
for _, scenario := range []string{"dirty metadata", "read-only metadata", "external PNG", "external sidecar", "target appeared", "closed document"} {
|
|
t.Run(scenario, func(t *testing.T) {
|
|
ed, source, frames, _ := linkedArtEditor(t)
|
|
askLegacyLinkedSource(t, ed, frames[1])
|
|
prompt := linkedSourcePrompt(t, ed)
|
|
doc := ed.Docs.AssetDocument(source)
|
|
target := filepath.Join(ed.Project.Root, "assets/linked.aeart")
|
|
switch scenario {
|
|
case "dirty metadata":
|
|
doc.Metadata.Code.Replace(0, 0, "// owner work\n")
|
|
case "read-only metadata":
|
|
doc.Metadata.ReadOnly = true
|
|
case "external PNG":
|
|
if err := os.WriteFile(filepath.Join(ed.Project.Root, "assets/linked.png"), []byte("external image"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
case "external sidecar":
|
|
if err := os.WriteFile(filepath.Join(ed.Project.Root, "assets/linked.png.meta"), []byte("external metadata"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
case "target appeared":
|
|
if err := os.WriteFile(target, []byte("another drawing"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
case "closed document":
|
|
if err := ed.Docs.CloseAsset(source, false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
prompt.Submit([]string{"assets/linked.aeart"})
|
|
if notice, ok := ed.LastNotice(); !ok || notice.Level != context.LevelError {
|
|
t.Fatalf("unsafe source creation was not rejected: %+v", notice)
|
|
}
|
|
if scenario == "target appeared" {
|
|
if data, err := os.ReadFile(target); err != nil || string(data) != "another drawing" {
|
|
t.Fatal("another drawing was overwritten", err)
|
|
}
|
|
} else if _, err := os.Lstat(target); !os.IsNotExist(err) {
|
|
t.Fatalf("unsafe source creation left a target: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOpeningLegacyLinkedSourceWarnsWithoutChangingFiles(t *testing.T) {
|
|
ed, source, frames, original := linkedArtEditor(t)
|
|
askLegacyLinkedSource(t, ed, frames[1])
|
|
linkedSourcePrompt(t, ed).Submit([]string{"assets/linked.aeart"})
|
|
meta, ok := ed.Pipeline.MetaOf(source)
|
|
if !ok {
|
|
t.Fatal("linked PNG metadata missing")
|
|
}
|
|
link, err := artsource.TextureLinkOf(&meta)
|
|
if err != nil || link == nil || link.LayoutSHA256 == "" {
|
|
t.Fatalf("new link lacks its geometry guard: %+v, %v", link, err)
|
|
}
|
|
before, err := ed.Pipeline.SourceBytes("assets/linked.png.meta")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Exercise the compatibility branch without changing the test project's
|
|
// metadata: only the parsed link passed to the opener is made legacy-like.
|
|
link.LayoutSHA256 = ""
|
|
if err := openLinkedArtFrame(ed, link, frames[1]); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
warning := i18n.Tr("editor.art.legacy_link_geometry", "This older PNG link cannot detect slicing changes. Keep its source rectangles unchanged and review them before importing.")
|
|
if notice, ok := ed.LastNotice(); !ok || notice.Level != context.LevelWarn || notice.Text != warning {
|
|
t.Fatalf("legacy geometry limitation was not surfaced: %+v", notice)
|
|
}
|
|
after, err := ed.Pipeline.SourceBytes("assets/linked.png.meta")
|
|
if err != nil || !bytes.Equal(before, after) {
|
|
t.Fatalf("opening a legacy link changed metadata: %v", err)
|
|
}
|
|
png, err := ed.Pipeline.SourceBytes("assets/linked.png")
|
|
if err != nil || !bytes.Equal(original, png) {
|
|
t.Fatalf("opening a legacy link changed PNG bytes: %v", err)
|
|
}
|
|
}
|