Files
aibis-dream/Assets/Tests/FrameAnimation/EditMode/FrameAnimationImportTests.cs
T

251 lines
10 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using AibisDream.FrameAnimation.Editor;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
namespace AibisDream.FrameAnimation.Tests.EditMode
{
/// <summary>
/// Aseprite 到 FrameAnimationGraph 的边界契约。
/// 只覆盖解析语义与一次完整刷新,不枚举编辑器操作步骤。
/// </summary>
public sealed class FrameAnimationImportTests
{
private string testRoot;
private string TexturePath => testRoot + "/Atlas.png";
private string JsonPath => testRoot + "/Atlas.json";
private string GraphPath => testRoot + "/ImportGraph.asset";
[SetUp]
public void SetUp()
{
testRoot =
$"Assets/Tests/FrameAnimation/GeneratedImportTests_{Guid.NewGuid():N}";
EnsureFolder(testRoot);
}
[TearDown]
public void TearDown()
{
if (string.IsNullOrEmpty(testRoot))
return;
if (AssetDatabase.IsValidFolder(testRoot))
AssetDatabase.DeleteAsset(testRoot);
var projectRoot = Directory.GetParent(Application.dataPath).FullName;
var absoluteRoot = Path.GetFullPath(Path.Combine(projectRoot, testRoot));
if (Directory.Exists(absoluteRoot))
FileUtil.DeleteFileOrDirectory(absoluteRoot);
if (File.Exists(absoluteRoot + ".meta"))
FileUtil.DeleteFileOrDirectory(absoluteRoot + ".meta");
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
Assert.That(Directory.Exists(absoluteRoot), Is.False,
$"Test asset directory was not removed: {testRoot}");
Assert.That(File.Exists(absoluteRoot + ".meta"), Is.False,
$"Test asset meta was not removed: {testRoot}.meta");
}
[Test]
public void Parser_PreservesUnicodeOrderAndSharedSpriteSlots()
{
Assert.That(
AsepriteJsonParser.TryParse(
ObjectJson(),
"source",
out var ordered,
out var orderedIssue),
Is.True,
orderedIssue?.Message);
Assert.That(
ordered.Frames.Select(frame => frame.FrameName),
Is.EqualTo(new[] { "第二帧.png", "第一帧.png" }));
Assert.That(
ordered.Tags.Select(tag => tag.Name),
Is.EqualTo(new[] { "待机", "眨眼" }));
Assert.That(
AsepriteJsonParser.TryParse(
SharedRectJson(),
"source",
out var shared,
out var sharedIssue),
Is.True,
sharedIssue?.Message);
Assert.That(shared.Frames.Count, Is.EqualTo(3));
Assert.That(shared.SpriteSlots.Count, Is.EqualTo(2));
Assert.That(
shared.SpriteSlotBySourceIndex[0],
Is.SameAs(shared.SpriteSlotBySourceIndex[1]));
}
[Test]
public void TagExpansion_SupportsEveryAsepriteDirection()
{
var cases = new Dictionary<string, int[]>
{
["forward"] = new[] { 0, 1, 2, 3 },
["reverse"] = new[] { 3, 2, 1, 0 },
["pingpong"] = new[] { 0, 1, 2, 3, 2, 1 },
["pingpong_reverse"] = new[] { 3, 2, 1, 0, 1, 2 }
};
foreach (var pair in cases)
{
var tag = new AsepriteSourceTag("测试", 0, 3, pair.Key);
Assert.That(
AsepriteJsonParser.TryExpandTag(
tag,
4,
"source",
out var actual,
out var issue),
Is.True,
issue?.Message);
Assert.That(actual, Is.EqualTo(pair.Value), pair.Key);
}
}
[Test]
public void WritableRefresh_UpdatesFramesWithoutReplacingOwnedClip()
{
var graph = CreateWritableGraph(ObjectJson());
var firstPreview = FrameAnimationImportService.PreviewAll(graph);
Assert.That(firstPreview.HasErrors, Is.False, JoinIssues(firstPreview));
Assert.That(
FrameAnimationImportService.Apply(
firstPreview,
true,
out var firstError),
Is.True,
firstError);
var idle = graph.Clips.Single(
clip => clip.ImportInfo.SourceTagName == "待机");
Assert.That(AssetDatabase.IsSubAsset(idle), Is.True);
Assert.That(
AssetDatabase.TryGetGUIDAndLocalFileIdentifier(
idle,
out _,
out long firstLocalId),
Is.True);
WriteJson(ObjectJson(duration: 180));
var refreshPreview = FrameAnimationImportService.PreviewAll(graph);
Assert.That(
FrameAnimationImportService.Apply(
refreshPreview,
true,
out var refreshError),
Is.True,
refreshError);
Assert.That(idle.Frames[0].DurationMs, Is.EqualTo(180));
Assert.That(
AssetDatabase.TryGetGUIDAndLocalFileIdentifier(
idle,
out _,
out long refreshedLocalId),
Is.True);
Assert.That(refreshedLocalId, Is.EqualTo(firstLocalId));
}
private FrameAnimationGraph CreateWritableGraph(string json)
{
CreateTexture();
WriteJson(json);
var graph = ScriptableObject.CreateInstance<FrameAnimationGraph>();
graph.Configure(
"ImportGraph",
"Import Graph",
Array.Empty<FrameClip>(),
Array.Empty<AnimationNode>(),
Array.Empty<AnimationEdge>(),
Array.Empty<AnimationFlow>(),
string.Empty);
graph.AddImportSource(new FrameAnimationImportSource(
"中文来源",
AssetDatabase.LoadAssetAtPath<Texture2D>(TexturePath),
AssetDatabase.LoadAssetAtPath<TextAsset>(JsonPath),
new Vector2(0.5f, 0.5f),
true));
AssetDatabase.CreateAsset(graph, GraphPath);
AssetDatabase.SaveAssets();
return graph;
}
private void CreateTexture()
{
var texture = new Texture2D(4, 2, TextureFormat.RGBA32, false);
texture.SetPixels(Enumerable.Range(0, 8)
.Select(index => index < 4 ? Color.red : Color.green)
.ToArray());
texture.Apply();
File.WriteAllBytes(TexturePath, texture.EncodeToPNG());
UnityEngine.Object.DestroyImmediate(texture);
AssetDatabase.ImportAsset(
TexturePath,
ImportAssetOptions.ForceSynchronousImport);
}
private void WriteJson(string json)
{
File.WriteAllText(JsonPath, json, new UTF8Encoding(false));
AssetDatabase.ImportAsset(
JsonPath,
ImportAssetOptions.ForceSynchronousImport);
}
private static string ObjectJson(int duration = 120)
{
return "{" +
"\"frames\":{" +
$"\"第二帧.png\":{{\"frame\":{{\"x\":0,\"y\":0,\"w\":2,\"h\":2}},\"rotated\":false,\"trimmed\":false,\"duration\":{duration},\"spriteSourceSize\":{{\"x\":0,\"y\":0,\"w\":2,\"h\":2}},\"sourceSize\":{{\"w\":2,\"h\":2}}}}," +
"\"第一帧.png\":{\"frame\":{\"x\":2,\"y\":0,\"w\":2,\"h\":2},\"rotated\":false,\"trimmed\":false,\"duration\":80,\"spriteSourceSize\":{\"x\":0,\"y\":0,\"w\":2,\"h\":2},\"sourceSize\":{\"w\":2,\"h\":2}}}," +
"\"meta\":{\"image\":\"Atlas.png\",\"size\":{\"w\":4,\"h\":2},\"frameTags\":[" +
"{\"name\":\"待机\",\"from\":0,\"to\":0,\"direction\":\"forward\"}," +
"{\"name\":\"眨眼\",\"from\":0,\"to\":1,\"direction\":\"pingpong\"}]}}";
}
private static string SharedRectJson()
{
return "{" +
"\"frames\":{" +
"\"FrameA\":{\"frame\":{\"x\":0,\"y\":0,\"w\":2,\"h\":2},\"rotated\":false,\"trimmed\":false,\"duration\":100,\"spriteSourceSize\":{\"x\":0,\"y\":0,\"w\":2,\"h\":2},\"sourceSize\":{\"w\":2,\"h\":2}}," +
"\"FrameB\":{\"frame\":{\"x\":0,\"y\":0,\"w\":2,\"h\":2},\"rotated\":false,\"trimmed\":false,\"duration\":300,\"spriteSourceSize\":{\"x\":0,\"y\":0,\"w\":2,\"h\":2},\"sourceSize\":{\"w\":2,\"h\":2}}," +
"\"FrameC\":{\"frame\":{\"x\":2,\"y\":0,\"w\":2,\"h\":2},\"rotated\":false,\"trimmed\":false,\"duration\":80,\"spriteSourceSize\":{\"x\":0,\"y\":0,\"w\":2,\"h\":2},\"sourceSize\":{\"w\":2,\"h\":2}}}," +
"\"meta\":{\"image\":\"Atlas.png\",\"size\":{\"w\":4,\"h\":2},\"frameTags\":[" +
"{\"name\":\"Shared\",\"from\":0,\"to\":2,\"direction\":\"forward\"}]}}";
}
private static string JoinIssues(FrameAnimationImportPreview preview)
{
return string.Join(
"\n",
preview.Sources
.SelectMany(source => source.Issues)
.Select(issue => issue.Message));
}
private static void EnsureFolder(string path)
{
var segments = path.Split('/');
var current = segments[0];
for (var index = 1; index < segments.Length; index++)
{
var next = current + "/" + segments[index];
if (!AssetDatabase.IsValidFolder(next))
AssetDatabase.CreateFolder(current, segments[index]);
current = next;
}
}
}
}