251 lines
10 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|
|
}
|