608 lines
20 KiB
Go
608 lines
20 KiB
Go
package editor
|
|
|
|
import (
|
|
"bytes"
|
|
"image"
|
|
"image/color"
|
|
"image/png"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"testing"
|
|
"time"
|
|
|
|
"aego/asset"
|
|
"aego/asset/importers/atlas"
|
|
textureimport "aego/asset/importers/texture"
|
|
"aego/asset/loader"
|
|
"aego/asset/pipeline"
|
|
"aego/core/ids"
|
|
"aego/core/log"
|
|
"aego/core/mathx"
|
|
"aego/editor/core/artedit"
|
|
"aego/editor/core/context"
|
|
"aego/editor/core/document"
|
|
"aego/editor/core/workspace"
|
|
"aego/editor/ext"
|
|
"aego/format/aetext"
|
|
"aego/gpu/null"
|
|
"aego/platform/input"
|
|
"aego/project"
|
|
"aego/render/texture"
|
|
"aego/scene/mutate"
|
|
"aego/ui/core"
|
|
"aego/ui/widgets"
|
|
)
|
|
|
|
type imageFixture struct {
|
|
ed *context.Editor
|
|
ws *workspace.Workspace
|
|
ext *ext.Registry
|
|
albedo, normal, height ids.AssetID
|
|
frames, normalFrames [2]ids.AssetID
|
|
}
|
|
|
|
func imageAuthoringFixture(t *testing.T) *imageFixture {
|
|
t.Helper()
|
|
p, err := project.Create(t.TempDir(), "Viewer", false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f := &imageFixture{albedo: ids.NewAssetID(), normal: ids.NewAssetID(), height: ids.NewAssetID()}
|
|
for i := range f.frames {
|
|
f.frames[i], f.normalFrames[i] = ids.NewAssetID(), ids.NewAssetID()
|
|
}
|
|
imp := textureimport.New()
|
|
for _, a := range []struct {
|
|
name, role string
|
|
id ids.AssetID
|
|
}{{"surface.png", "color", f.albedo}, {"surface_n.png", "normal", f.normal}, {"surface_height.png", "height", f.height}} {
|
|
img := image.NewNRGBA(image.Rect(0, 0, 8, 8))
|
|
for y := 0; y < 8; y++ {
|
|
for x := 0; x < 8; x++ {
|
|
c := color.NRGBA{R: 200, G: 120, B: 80, A: 200}
|
|
if a.role == "normal" {
|
|
c = color.NRGBA{R: 128, G: 128, B: 255, A: 17}
|
|
}
|
|
if a.role == "height" {
|
|
c = color.NRGBA{R: byte(x * 30), G: byte(y * 30), B: 0, A: 255}
|
|
}
|
|
img.SetNRGBA(x, y, c)
|
|
}
|
|
}
|
|
var encoded bytes.Buffer
|
|
if err := png.Encode(&encoded, img); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
full := filepath.Join(p.Root, "assets", a.name)
|
|
if err := os.WriteFile(full, encoded.Bytes(), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
meta := asset.NewSourceMeta("texture", imp.Version(), imp.Defaults())
|
|
meta.ID = a.id
|
|
meta.Settings["role"] = aetext.String(a.role)
|
|
if a.role == "height" {
|
|
meta.Settings["atlas"] = aetext.String("viewer")
|
|
meta.Settings["generate_normal"] = aetext.Bool(true)
|
|
meta.Settings["height_strength"] = aetext.Float32(2)
|
|
}
|
|
if a.role != "height" {
|
|
frames := f.frames
|
|
if a.role == "normal" {
|
|
frames = f.normalFrames
|
|
}
|
|
meta.Subs = []asset.SourceFrame{{ID: frames[0], Name: "left", Rect: [4]int{0, 0, 4, 8}}, {ID: frames[1], Name: "right", Rect: [4]int{4, 0, 4, 8}}}
|
|
}
|
|
if err := os.WriteFile(full+".meta", asset.MarshalSourceMeta(meta), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
imps := pipeline.NewImporterRegistry()
|
|
if err := imps.Register(atlas.Wrap(imp)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pl, err := pipeline.Open(p, imps, log.Nop())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(pl.Close)
|
|
if _, _, err := pl.Scan(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
batch, err := pl.ImportAll()
|
|
if err != nil || len(batch.Failed) > 0 {
|
|
t.Fatalf("fixture import: %v %+v", err, batch.Failed)
|
|
}
|
|
dev := null.New()
|
|
t.Cleanup(dev.Close)
|
|
r := imageTestRenderer(t, dev)
|
|
load := loader.New(dev, texture.NewUploader(dev), log.Nop())
|
|
load.SetMaterials(r.Materials())
|
|
if err := load.Mount(pl.Index(), pl.Artifacts()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(load.Close)
|
|
f.ed = &context.Editor{Docs: document.NewManager(document.Options{Root: p.Root}), Pipeline: pl, Loader: load, Renderer: r}
|
|
f.ws = workspace.New(p.Root, nil)
|
|
f.ext = ext.New(ext.Deps{Editor: f.ed, Workspace: f.ws})
|
|
if err := f.ext.Run("render2d", imageHooks); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f.ed.Docs.Subscribe(func(ev document.Event) {
|
|
if ev.Kind == document.EvSourceSaved {
|
|
id, ok := pl.Lookup(ev.Path[:len(ev.Path)-len(".meta")])
|
|
if ok {
|
|
b, err := pl.ReloadMetadata(id)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
f.ed.ApplyBatch(b)
|
|
}
|
|
}
|
|
})
|
|
return f
|
|
}
|
|
|
|
func TestImageInspectorStartsCompactWithoutChangingMetadata(t *testing.T) {
|
|
f := imageAuthoringFixture(t)
|
|
f.ed.UI = core.New(nil)
|
|
doc, err := f.ed.DocumentForAsset(f.albedo)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
before := bytes.Clone(doc.Metadata.Code.Bytes())
|
|
ui, in := f.ed.UI, &input.State{}
|
|
in.BeginFrame()
|
|
ui.Begin(in, mathx.Vec2{X: 300, Y: 1200}, 1)
|
|
ui.Region(mathx.Rect{W: 300, H: 1200})
|
|
entry, ok := f.ed.Pipeline.Entry(f.albedo)
|
|
if !ok {
|
|
t.Fatal("missing source image")
|
|
}
|
|
f.ed.AssetViews[string(entry.Kind)](f.ed, f.albedo)
|
|
height := ui.Cursor().Y - ui.Theme.Padding
|
|
ui.PopCursor()
|
|
ui.End()
|
|
if maxHeight := 10 * (ui.Theme.RowHeight + ui.Theme.Spacing); height > maxHeight {
|
|
t.Fatalf("image inspector starts with %.0f pixels of controls, want at most %.0f before expanding sections", height, maxHeight)
|
|
}
|
|
if doc.Dirty() || !bytes.Equal(before, doc.Metadata.Code.Bytes()) {
|
|
t.Fatal("viewing the compact inspector changed metadata")
|
|
}
|
|
}
|
|
|
|
func TestImageInspectorSectionsAreInteractiveAndKeepErrorsVisible(t *testing.T) {
|
|
f := imageAuthoringFixture(t)
|
|
f.ed.UI = core.New(nil)
|
|
doc, err := f.ed.DocumentForAsset(f.albedo)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
before := bytes.Clone(doc.Metadata.Code.Bytes())
|
|
ui, in := f.ed.UI, &input.State{}
|
|
entry, _ := f.ed.Pipeline.Entry(f.albedo)
|
|
draw := func() float32 {
|
|
r := mathx.Rect{W: 800, H: 1000}
|
|
ui.Begin(in, mathx.Vec2{X: r.W, Y: r.H}, 1)
|
|
ui.PushClip(r)
|
|
ui.Region(r)
|
|
f.ed.AssetViews[string(entry.Kind)](f.ed, f.albedo)
|
|
height := ui.Cursor().Y
|
|
ui.PopCursor()
|
|
ui.PopClip()
|
|
ui.End()
|
|
return height
|
|
}
|
|
step := func(p mathx.Vec2, down bool) {
|
|
in.BeginFrame()
|
|
in.ApplyMouseMove(p, mathx.Vec2{})
|
|
if down {
|
|
in.ApplyMouseDown(p, input.MouseLeft)
|
|
} else {
|
|
in.ApplyMouseUp(p, input.MouseLeft)
|
|
}
|
|
draw()
|
|
}
|
|
click := func(p mathx.Vec2) { step(p, true); step(p, false) }
|
|
row := func(n int) mathx.Vec2 {
|
|
return mathx.Vec2{X: 40, Y: ui.Theme.Padding + float32(n)*(ui.Theme.RowHeight+ui.Theme.Spacing) + ui.Theme.RowHeight/2}
|
|
}
|
|
in.BeginFrame()
|
|
draw()
|
|
v := doc.View.(*imageState)
|
|
click(row(4)) // Surface channels
|
|
if !v.sections[1].channels || v.sections[0].channels {
|
|
t.Fatal("channels did not open only in the Inspector")
|
|
}
|
|
click(row(4))
|
|
if v.sections[1].channels {
|
|
t.Fatal("channels did not close")
|
|
}
|
|
click(row(5)) // Import settings
|
|
if !v.sections[1].settings {
|
|
t.Fatal("import settings did not open")
|
|
}
|
|
if doc.Dirty() || !bytes.Equal(before, doc.Metadata.Code.Bytes()) {
|
|
t.Fatal("expanding sections changed metadata")
|
|
}
|
|
// Change the role through the visible settings combo.
|
|
role := row(6)
|
|
role.X = 200
|
|
click(role)
|
|
in.BeginFrame()
|
|
draw() // measure popup bounds before selecting the first item
|
|
choice := mathx.Vec2{X: role.X + 10, Y: role.Y + ui.Theme.RowHeight/2 + ui.Theme.Padding + ui.Theme.RowHeight/2}
|
|
click(choice)
|
|
if !doc.Dirty() || bytes.Equal(before, doc.Metadata.Code.Bytes()) {
|
|
t.Fatal("the setting inside the expanded section could not be edited")
|
|
}
|
|
if err := f.ed.UndoAsset(doc); err != nil || !bytes.Equal(before, doc.Metadata.Code.Bytes()) {
|
|
t.Fatalf("Undo did not restore metadata: %v", err)
|
|
}
|
|
// Status/error messages stay visible even while every section is collapsed.
|
|
v.sections[1] = imageSections{}
|
|
in.BeginFrame()
|
|
compact := draw()
|
|
v.message = "Visible error"
|
|
in.BeginFrame()
|
|
if got := draw(); got <= compact {
|
|
t.Fatal("an error was hidden with the collapsed sections")
|
|
}
|
|
}
|
|
|
|
func TestSourceInspectorSwitchesSlicingAndChannelsAgainstWorkingMetadata(t *testing.T) {
|
|
f := imageAuthoringFixture(t)
|
|
f.ed.UI = core.New(nil)
|
|
in := &input.State{}
|
|
var shown *asset.SourceMeta
|
|
f.ed.DrawSpriteSlicer = func(_ ids.AssetID, m *asset.SourceMeta) { shown = m }
|
|
draw := func(id ids.AssetID) {
|
|
shown = nil
|
|
ui := f.ed.UI
|
|
r := mathx.Rect{W: 800, H: 640}
|
|
ui.Begin(in, mathx.Vec2{X: r.W, Y: r.H}, 1)
|
|
ui.PushClip(r)
|
|
ui.Region(r)
|
|
entry, _ := f.ed.Pipeline.Entry(id)
|
|
f.ed.AssetViews[string(entry.Kind)](f.ed, id)
|
|
ui.PopCursor()
|
|
ui.PopClip()
|
|
ui.End()
|
|
}
|
|
toggle := func(id ids.AssetID) {
|
|
p := mathx.Vec2{X: 789, Y: 14}
|
|
in.BeginFrame()
|
|
in.ApplyMouseDown(p, input.MouseLeft)
|
|
draw(id)
|
|
in.BeginFrame()
|
|
in.ApplyMouseUp(p, input.MouseLeft)
|
|
draw(id)
|
|
}
|
|
draw(f.albedo)
|
|
if shown == nil || len(shown.Subs) != 2 {
|
|
t.Fatal("an existing sheet did not open its slicing tools")
|
|
}
|
|
doc := f.ed.Docs.AssetDocument(f.albedo)
|
|
env, err := f.ed.TextureAuthoringEnv(f.albedo)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := document.ApplyTextureMutation(doc.Metadata, env, mutate.Tx{Ops: []mutate.Op{mutate.TextureFrameRename(f.frames[0], "unsaved")}}, false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
draw(f.albedo)
|
|
if shown == nil || shown.Subs[0].Name != "unsaved" || shown.Subs[0].ID != f.frames[0] || !doc.Dirty() {
|
|
t.Fatal("slicer read the saved pipeline index instead of the shared working metadata")
|
|
}
|
|
toggle(f.albedo)
|
|
if shown != nil {
|
|
t.Fatal("sheet cannot switch to its channels")
|
|
}
|
|
draw(f.height)
|
|
if shown != nil {
|
|
t.Fatal("an unsliced source should open its channels")
|
|
}
|
|
toggle(f.height)
|
|
if shown == nil || len(shown.Subs) != 0 {
|
|
t.Fatal("slicing is unreachable before an image has frames")
|
|
}
|
|
}
|
|
|
|
func TestImageOpeningSharesDocumentFrameHistoryAndWorkspaceTab(t *testing.T) {
|
|
f := imageAuthoringFixture(t)
|
|
d, err := f.ed.OpenAsset(f.frames[0])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
model, ok := d.Binding.Model.(*artedit.Document)
|
|
if !ok || !model.Draft || len(model.Session.Document.Info().Frames) != 2 {
|
|
t.Fatal("the sliced PNG did not open as an editable drawing")
|
|
}
|
|
// Image import settings remain an independent view over the same metadata,
|
|
// rather than the presentation selected by the default PNG open action.
|
|
inspector := &imagesEditor{ctx: f.ed, reg: f.ext, states: map[document.ID]*imageState{}}
|
|
v := inspector.state(d)
|
|
if d.Asset != f.albedo || d.Selected != f.frames[0] || d.Source != nil || d.Metadata == nil || !d.Metadata.ExplicitSave {
|
|
t.Fatal("binary source or frame identity bypassed the shared document")
|
|
}
|
|
if !f.ws.Visible(artHandlerID + "/" + d.Asset.String()) {
|
|
t.Fatal("image did not open in the Art workspace tab")
|
|
}
|
|
inspected, err := f.ed.DocumentForAsset(f.frames[0])
|
|
if err != nil || inspected != d {
|
|
t.Fatal("Inspector created a second image model")
|
|
}
|
|
second, err := f.ed.OpenAsset(f.frames[1])
|
|
if err != nil || second != d || second.View != v || second.Metadata != d.Metadata {
|
|
t.Fatal("another source frame opened another metadata/history document")
|
|
}
|
|
inspector.state(second)
|
|
if model.Session.Frame != model.Session.Document.Info().Frames[1].ID {
|
|
t.Fatal("the selected source frame did not switch the Art session")
|
|
}
|
|
if v.source.data.Width != 4 || v.source.data.Height != 8 || v.content.SourceRect[0] != 4 {
|
|
t.Fatal("frame selection did not use source pixels")
|
|
}
|
|
if v.imported.data.Width != 4 || v.imported.data.Height != 8 {
|
|
t.Fatal("imported frame uses whole atlas dimensions")
|
|
}
|
|
surface := v.surface(f.ed, d)
|
|
if surface.Normal.IsZero() || surface.Albedo.IsZero() || surface.Normal.Texture == surface.Albedo.Texture {
|
|
t.Fatal("frame normal has no independent data-atlas binding")
|
|
}
|
|
var p imagePreview
|
|
p.init()
|
|
p.controls.FlipX = true
|
|
p.controls.FlipY = true
|
|
before := bytes.Clone(d.Metadata.Code.Bytes())
|
|
_ = p.lighting()
|
|
if !bytes.Equal(before, d.Metadata.Code.Bytes()) || d.Dirty() {
|
|
t.Fatal("preview-only controls dirtied source metadata")
|
|
}
|
|
}
|
|
|
|
func TestImageHeightCandidateSaveAndOneDragUndoKeepSourcePixels(t *testing.T) {
|
|
f := imageAuthoringFixture(t)
|
|
d, err := f.ed.OpenAsset(f.height)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
v := d.View.(*imageState)
|
|
encodedBefore, err := os.ReadFile(f.ed.Docs.Abs(d.Path))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
metadataBefore := bytes.Clone(d.Metadata.Code.Bytes())
|
|
for i, value := range []float32{3, 4, 5} {
|
|
v.mutate(f.ed, d, "height_strength", widgets.Edit{Changed: true, Begin: i == 0, End: i == 2}, mutate.TextureSetting("height_strength", aetext.Float32(value)))
|
|
}
|
|
if v.message != "" {
|
|
t.Fatal(v.message)
|
|
}
|
|
if !d.Dirty() {
|
|
t.Fatal("height setting did not enter the shared journal")
|
|
}
|
|
v.refresh(f.ed, d)
|
|
if !waitImageCandidate(t, &v.candidate, v.inputRevision, 0, false) {
|
|
t.Fatal("height normal candidate failed")
|
|
}
|
|
if v.candidate.good.semantic != asset.TextureNormal {
|
|
t.Fatal("height preview has no real generated normal")
|
|
}
|
|
if current, err := os.ReadFile(f.ed.Docs.Abs(d.Metadata.Path)); err != nil || !bytes.Equal(current, metadataBefore) {
|
|
t.Fatal("private candidate silently saved import settings")
|
|
}
|
|
if err := f.ed.UndoAsset(d); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !bytes.Equal(d.Metadata.Code.Bytes(), metadataBefore) {
|
|
t.Fatal("one drag required more than one Undo")
|
|
}
|
|
if err := f.ed.RedoAsset(d); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
v.refresh(f.ed, d)
|
|
if !waitImageCandidate(t, &v.candidate, v.inputRevision, 0, false) {
|
|
t.Fatal("redo candidate failed")
|
|
}
|
|
prepared := bytes.Clone(v.candidate.good.data.Pixels)
|
|
v.saveNow(f.ed, d)
|
|
if v.message != "" || d.Dirty() {
|
|
t.Fatalf("save did not commit metadata: %s", v.message)
|
|
}
|
|
if d.Metadata.SavedEdits == 0 {
|
|
t.Fatal("explicit Save was not recorded for live Undo")
|
|
}
|
|
after, err := os.ReadFile(f.ed.Docs.Abs(d.Path))
|
|
if err != nil || !bytes.Equal(after, encodedBefore) {
|
|
t.Fatal("height generation edited source image pixels")
|
|
}
|
|
v.refresh(f.ed, d)
|
|
if v.imported == nil || !bytes.Equal(v.imported.data.Pixels, prepared) {
|
|
t.Fatal("saved deterministic artifact differs from the prepared candidate")
|
|
}
|
|
}
|
|
|
|
func TestImageChannelMutationRetainsUnknownReferencesAndRejectsDataMisuse(t *testing.T) {
|
|
f := imageAuthoringFixture(t)
|
|
d, err := f.ed.OpenAsset(f.frames[0])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
inspector := &imagesEditor{ctx: f.ed, reg: f.ext, states: map[document.ID]*imageState{}}
|
|
v := inspector.state(d)
|
|
env, err := f.ed.TextureAuthoringEnv(d.Asset)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
source, ok := env.Resolve(f.frames[0])
|
|
if !ok {
|
|
t.Fatal("source info missing")
|
|
}
|
|
normal, ok := env.Resolve(f.normalFrames[0])
|
|
if !ok || !imageChannelCompatible("normal", source, normal) {
|
|
t.Fatal("normal frame drag would be rejected")
|
|
}
|
|
albedo, _ := env.Resolve(f.frames[1])
|
|
if imageChannelCompatible("normal", source, albedo) {
|
|
t.Fatal("color texture accepted as a normal")
|
|
}
|
|
before := bytes.Clone(d.Metadata.Code.Bytes())
|
|
v.mutate(f.ed, d, "normal", oneEdit(), mutate.TextureChannel(f.frames[0], "normal", f.height))
|
|
if v.message == "" || !bytes.Equal(before, d.Metadata.Code.Bytes()) {
|
|
t.Fatal("incompatible frame dimensions committed partially")
|
|
}
|
|
missing := ids.NewAssetID()
|
|
v.mutate(f.ed, d, "normal", oneEdit(), mutate.TextureChannel(f.frames[0], "normal", missing))
|
|
if v.message != "" {
|
|
t.Fatal(v.message)
|
|
}
|
|
v.refresh(f.ed, d)
|
|
ref, explicit, err := asset.TextureChannel(v.meta, f.frames[0], "normal")
|
|
if err != nil || !explicit || ref != missing {
|
|
t.Fatal("unresolved channel ID was silently removed")
|
|
}
|
|
if !v.surface(f.ed, d).Normal.IsZero() {
|
|
t.Fatal("missing explicit normal silently inherited another frame")
|
|
}
|
|
v.mutate(f.ed, d, "normal", oneEdit(), mutate.TextureChannel(f.frames[0], "normal", ids.AssetID{}))
|
|
v.refresh(f.ed, d)
|
|
ref, explicit, _ = asset.TextureChannel(v.meta, f.frames[0], "normal")
|
|
if !explicit || !ref.IsZero() {
|
|
t.Fatal("explicit channel disable became inheritance")
|
|
}
|
|
}
|
|
|
|
func TestImagePendingCandidateRejectsChangedSourceBeforeWatcherRevision(t *testing.T) {
|
|
f := imageAuthoringFixture(t)
|
|
d, err := f.ed.OpenAsset(f.height)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
v := d.View.(*imageState)
|
|
if !waitImageCandidate(t, &v.candidate, v.inputRevision, 0, false) {
|
|
t.Fatal("initial candidate failed")
|
|
}
|
|
good := v.candidate.good
|
|
initialRevision := v.inputRevision
|
|
f.ed.AssetRev++ // unrelated import refreshes status, not the height request
|
|
v.refresh(f.ed, d)
|
|
if v.inputRevision != initialRevision || v.candidate.pending {
|
|
t.Fatal("unrelated asset revision restarted the candidate")
|
|
}
|
|
blocked := make(chan struct{})
|
|
v.candidate.start(v.inputRevision, 0, func() (*imagePixels, error) { <-blocked; return normalPreviewPixels(255, 128, 128), nil })
|
|
v.pendingSave = true
|
|
before, err := os.ReadFile(f.ed.Docs.Abs(d.Metadata.Path))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
img := image.NewNRGBA(image.Rect(0, 0, 8, 8))
|
|
for y := 0; y < 8; y++ {
|
|
for x := 0; x < 8; x++ {
|
|
img.SetNRGBA(x, y, color.NRGBA{R: 64, A: 255})
|
|
}
|
|
}
|
|
var pngBytes bytes.Buffer
|
|
if err := png.Encode(&pngBytes, img); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(f.ed.Docs.Abs(d.Path), pngBytes.Bytes(), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
close(blocked)
|
|
deadline := time.Now().Add(time.Second)
|
|
for v.candidate.pending && time.Now().Before(deadline) {
|
|
v.poll(f.ed, d)
|
|
runtime.Gosched()
|
|
}
|
|
if v.candidate.pending || v.candidate.good != good || v.candidate.err == nil || v.pendingSave {
|
|
t.Fatal("stale source candidate replaced last-good or saved metadata")
|
|
}
|
|
current, err := os.ReadFile(f.ed.Docs.Abs(d.Metadata.Path))
|
|
if err != nil || !bytes.Equal(before, current) || d.Metadata.SavedEdits != 0 {
|
|
t.Fatal("stale Save request touched disk")
|
|
}
|
|
v.refresh(f.ed, d)
|
|
if v.inputRevision == initialRevision || !waitImageCandidate(t, &v.candidate, v.inputRevision, 0, false) {
|
|
t.Fatal("new source did not prepare a new candidate")
|
|
}
|
|
if bytes.Equal(v.candidate.good.data.Pixels, good.data.Pixels) {
|
|
t.Fatal("new source still shows the old normal pixels")
|
|
}
|
|
}
|
|
|
|
func TestImageReloadDiskReplacesMetadataIdentityAndPendingCandidate(t *testing.T) {
|
|
f := imageAuthoringFixture(t)
|
|
d, err := f.ed.OpenAsset(f.height)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
v := d.View.(*imageState)
|
|
if !waitImageCandidate(t, &v.candidate, v.inputRevision, 0, false) {
|
|
t.Fatal("initial candidate failed")
|
|
}
|
|
good, previousCode, previousRevision := v.candidate.good, d.Metadata.Code, v.inputRevision
|
|
blocked := make(chan struct{})
|
|
v.candidate.start(v.inputRevision, 0, func() (*imagePixels, error) {
|
|
<-blocked
|
|
return normalPreviewPixels(255, 128, 128), nil
|
|
})
|
|
v.pendingSave = true
|
|
meta, err := asset.ParseSourceMeta(d.Metadata.Code.Bytes(), d.Metadata.Path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
meta.Settings["height_strength"] = aetext.Float32(9)
|
|
disk := asset.MarshalSourceMeta(meta)
|
|
if err := os.WriteFile(f.ed.Docs.Abs(d.Metadata.Path), disk, 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
v.reload(f.ed, d)
|
|
if v.message != "" {
|
|
t.Fatal(v.message)
|
|
}
|
|
if d.Metadata.Code == previousCode || d.Metadata.Code.Rev() != previousCode.Rev() {
|
|
t.Fatal("fixture did not replace the journal with a reused revision")
|
|
}
|
|
if v.meta.Settings.Float("height_strength", 0) != 9 || v.inputRevision == previousRevision || v.pendingSave || d.Dirty() {
|
|
t.Fatal("Reload Disk retained cached metadata or an old pending Save")
|
|
}
|
|
close(blocked)
|
|
if !waitImageCandidate(t, &v.candidate, v.inputRevision, 0, false) {
|
|
t.Fatal("reloaded metadata did not prepare its current normal")
|
|
}
|
|
if bytes.Equal(good.data.Pixels, v.candidate.good.data.Pixels) || v.candidate.good.data.Width != 8 {
|
|
t.Fatal("a candidate from the replaced journal was published")
|
|
}
|
|
v.mutate(f.ed, d, "height_strength", oneEdit(), mutate.TextureSetting("height_strength", aetext.Float32(4)))
|
|
v.refresh(f.ed, d)
|
|
meta.Settings["height_strength"] = aetext.Float32(7)
|
|
disk = asset.MarshalSourceMeta(meta)
|
|
if err := os.WriteFile(f.ed.Docs.Abs(d.Metadata.Path), disk, 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
changedAt := d.Metadata.DiskAt.Add(time.Second)
|
|
if err := os.Chtimes(f.ed.Docs.Abs(d.Metadata.Path), changedAt, changedAt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, conflicts := f.ed.Docs.CheckExternalScripts()
|
|
if len(conflicts) != 1 || !d.Metadata.Conflict {
|
|
t.Fatal("dirty external metadata change did not become a conflict")
|
|
}
|
|
v.reload(f.ed, d)
|
|
if v.message != "" || d.Metadata.Conflict || d.Dirty() || v.meta.Settings.Float("height_strength", 0) != 7 {
|
|
t.Fatal("discard did not adopt the saved metadata document", v.message)
|
|
}
|
|
if d.Metadata.SavedEdits != 0 {
|
|
t.Fatal("Reload Disk silently performed a Save")
|
|
}
|
|
if current, err := os.ReadFile(f.ed.Docs.Abs(d.Metadata.Path)); err != nil || !bytes.Equal(current, disk) {
|
|
t.Fatal("Reload Disk wrote over the external change")
|
|
}
|
|
if !waitImageCandidate(t, &v.candidate, v.inputRevision, 0, false) {
|
|
t.Fatal("discarded metadata did not produce the current private preview")
|
|
}
|
|
v.candidate.close()
|
|
}
|