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

250 lines
13 KiB
C#

using System;
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
{
public sealed class FrameAnimationImportTests
{
private const string TestRoot = "Assets/Tests/FrameAnimation/GeneratedImportTests";
private const string TexturePath = TestRoot + "/Atlas.png";
private const string JsonPath = TestRoot + "/Atlas.json";
private const string GraphPath = TestRoot + "/ImportGraph.asset";
[SetUp]
public void SetUp()
{
AssetDatabase.DeleteAsset(TestRoot);
EnsureFolder(TestRoot);
}
[TearDown]
public void TearDown()
{
AssetDatabase.DeleteAsset(TestRoot);
AssetDatabase.Refresh();
}
[Test]
public void Parser_PreservesObjectAndArrayOrderAndUnicode()
{
Assert.That(AsepriteJsonParser.TryParse(ObjectJson(), "source", out var objectDocument, out var objectIssue),
Is.True, objectIssue?.Message);
Assert.That(AsepriteJsonParser.TryParse(ArrayJson(), "source", out var arrayDocument, out var arrayIssue),
Is.True, arrayIssue?.Message);
CollectionAssert.AreEqual(
new[] { "第二帧.png", "第一帧.png" },
objectDocument.Frames.Select(frame => frame.FrameName));
CollectionAssert.AreEqual(
objectDocument.Frames.Select(frame => frame.FrameName),
arrayDocument.Frames.Select(frame => frame.FrameName));
CollectionAssert.AreEqual(new[] { "待机", "眨眼" }, objectDocument.Tags.Select(tag => tag.Name));
}
[TestCase("forward", new[] { 0, 1, 2, 3 })]
[TestCase("reverse", new[] { 3, 2, 1, 0 })]
[TestCase("pingpong", new[] { 0, 1, 2, 3, 2, 1 })]
[TestCase("pingpong_reverse", new[] { 3, 2, 1, 0, 1, 2 })]
public void TagExpansion_IsDeterministic(string direction, int[] expected)
{
var tag = new AsepriteSourceTag("测试", 0, 3, direction);
Assert.That(AsepriteJsonParser.TryExpandTag(tag, 4, "source", out var actual, out var issue),
Is.True, issue?.Message);
CollectionAssert.AreEqual(expected, actual);
}
[TestCase("pingpong")]
[TestCase("pingpong_reverse")]
public void TagExpansion_SingleFrameDoesNotDuplicateEndpoint(string direction)
{
var tag = new AsepriteSourceTag("单帧", 1, 1, direction);
Assert.That(AsepriteJsonParser.TryExpandTag(tag, 3, "source", out var actual, out _), Is.True);
CollectionAssert.AreEqual(new[] { 1 }, actual);
}
[Test]
public void WritableRefresh_CreatesAndUpdatesStableSubAssetsAndMissingState()
{
var graph = CreateWritableGraph(ObjectJson());
var source = graph.ImportSources.Single();
var firstPreview = FrameAnimationImportService.PreviewAll(graph);
Assert.That(firstPreview.HasErrors, Is.False, JoinIssues(firstPreview));
Assert.That(firstPreview.Sources.Single().SpriteDiffs.Count(diff =>
diff.Kind == FrameAnimationSpriteChangeKind.Added), Is.EqualTo(2));
Assert.That(FrameAnimationImportService.Apply(firstPreview, true, out var firstError), Is.True, firstError);
Assert.That(graph.Clips.Count, Is.EqualTo(2));
var idle = graph.Clips.Single(clip => clip.ImportInfo.SourceTagName == "待机");
Assert.That(AssetDatabase.IsSubAsset(idle), Is.True);
Assert.That(AssetDatabase.GetAssetPath(idle), Is.EqualTo(GraphPath));
Assert.That(idle.Frames[0].FrameName, Is.EqualTo("第二帧.png"));
Assert.That(source.LastSourceHash, Is.Not.Empty);
Assert.That(AssetDatabase.TryGetGUIDAndLocalFileIdentifier(idle, out _, out long firstLocalId), Is.True);
var idleSerialized = new SerializedObject(idle);
idleSerialized.FindProperty("id").stringValue = "用户重命名待机";
idleSerialized.FindProperty("displayName").stringValue = "自定义显示名";
idleSerialized.FindProperty("speed").floatValue = 1.75f;
idleSerialized.ApplyModifiedPropertiesWithoutUndo();
WriteJson(ObjectJson(duration: 180));
var updatePreview = FrameAnimationImportService.PreviewAll(graph);
Assert.That(updatePreview.HasErrors, Is.False, JoinIssues(updatePreview));
Assert.That(FrameAnimationImportService.Apply(updatePreview, true, out var updateError), Is.True, updateError);
Assert.That(AssetDatabase.TryGetGUIDAndLocalFileIdentifier(idle, out _, out long updatedLocalId), Is.True);
Assert.That(updatedLocalId, Is.EqualTo(firstLocalId));
Assert.That(idle.Id, Is.EqualTo("用户重命名待机"));
Assert.That(idle.DisplayName, Is.EqualTo("自定义显示名"));
Assert.That(idle.Speed, Is.EqualTo(1.75f));
Assert.That(idle.Frames[0].DurationMs, Is.EqualTo(180));
WriteJson(ObjectJson(includeIdle: false));
var missingPreview = FrameAnimationImportService.PreviewAll(graph);
Assert.That(FrameAnimationImportService.Apply(missingPreview, true, out var missingError), Is.True, missingError);
Assert.That(idle.ImportInfo.IsMissingFromSource, Is.True);
WriteJson(ObjectJson(duration: 220));
var restorePreview = FrameAnimationImportService.PreviewAll(graph);
Assert.That(FrameAnimationImportService.Apply(restorePreview, true, out var restoreError), Is.True, restoreError);
Assert.That(idle.ImportInfo.IsMissingFromSource, Is.False);
Assert.That(idle.Frames[0].DurationMs, Is.EqualTo(220));
Assert.That(AssetDatabase.TryGetGUIDAndLocalFileIdentifier(idle, out _, out long restoredLocalId), Is.True);
Assert.That(restoredLocalId, Is.EqualTo(firstLocalId));
}
[Test]
public void ReadOnlyPreview_MatchesExistingSpritesWithoutChangingImporter()
{
var writableGraph = CreateWritableGraph(ObjectJson());
var writablePreview = FrameAnimationImportService.PreviewAll(writableGraph);
Assert.That(FrameAnimationImportService.Apply(writablePreview, true, out var applyError), Is.True, applyError);
var importer = (TextureImporter)AssetImporter.GetAtPath(TexturePath);
var before = EditorJsonUtility.ToJson(importer);
var readOnlyGraph = ScriptableObject.CreateInstance<FrameAnimationGraph>();
readOnlyGraph.Configure("ReadOnly", "Read Only", Array.Empty<FrameClip>(),
Array.Empty<AnimationNode>(), Array.Empty<AnimationEdge>(), Array.Empty<AnimationFlow>(), string.Empty);
readOnlyGraph.AddImportSource(new FrameAnimationImportSource(
"Read Only", AssetDatabase.LoadAssetAtPath<Texture2D>(TexturePath),
AssetDatabase.LoadAssetAtPath<TextAsset>(JsonPath), new Vector2(0.5f, 0.5f), false));
AssetDatabase.CreateAsset(readOnlyGraph, TestRoot + "/ReadOnlyGraph.asset");
AssetDatabase.SaveAssets();
var preview = FrameAnimationImportService.PreviewAll(readOnlyGraph);
Assert.That(preview.HasErrors, Is.False, JoinIssues(preview));
Assert.That(preview.HasSpriteChanges, Is.False);
Assert.That(EditorJsonUtility.ToJson(importer), Is.EqualTo(before));
}
[Test]
public void Preview_BlocksMultipleWritableOwnersWithoutMutation()
{
var graph = CreateWritableGraph(ObjectJson());
var source = graph.ImportSources.Single();
graph.AddImportSource(new FrameAnimationImportSource(
"Second Owner", source.Texture, source.AsepriteJson, source.Pivot, true));
EditorUtility.SetDirty(graph);
AssetDatabase.SaveAssets();
var importer = (TextureImporter)AssetImporter.GetAtPath(TexturePath);
var before = EditorJsonUtility.ToJson(importer);
var preview = FrameAnimationImportService.PreviewAll(graph);
Assert.That(preview.HasErrors, Is.True);
Assert.That(preview.Sources.SelectMany(item => item.Issues).Any(issue =>
issue.Code == FrameAnimationImportIssueCode.TextureOwnershipConflict), Is.True);
Assert.That(FrameAnimationImportService.Apply(preview, true, out _), Is.False);
Assert.That(EditorJsonUtility.ToJson(importer), Is.EqualTo(before));
Assert.That(graph.Clips, Is.Empty);
}
private static 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 static void CreateTexture()
{
var texture = new Texture2D(4, 2, TextureFormat.RGBA32, false);
var pixels = Enumerable.Range(0, 8)
.Select(index => index < 4 ? Color.red : Color.green)
.ToArray();
texture.SetPixels(pixels);
texture.Apply();
File.WriteAllBytes(TexturePath, texture.EncodeToPNG());
UnityEngine.Object.DestroyImmediate(texture);
AssetDatabase.ImportAsset(TexturePath, ImportAssetOptions.ForceSynchronousImport);
}
private static void WriteJson(string json)
{
File.WriteAllText(JsonPath, json, new UTF8Encoding(false));
AssetDatabase.ImportAsset(JsonPath, ImportAssetOptions.ForceSynchronousImport);
}
private static string ObjectJson(int duration = 120, bool includeIdle = true)
{
var tags = includeIdle
? "{\"name\":\"待机\",\"from\":0,\"to\":0,\"direction\":\"forward\"}," +
"{\"name\":\"眨眼\",\"from\":0,\"to\":1,\"direction\":\"pingpong\"}"
: "{\"name\":\"眨眼\",\"from\":0,\"to\":1,\"direction\":\"pingpong\"}";
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\":[{tags}]}}" +
"}";
}
private static string ArrayJson()
{
return "{" +
"\"frames\":[" +
"{\"filename\":\"第二帧.png\",\"frame\":{\"x\":0,\"y\":0,\"w\":2,\"h\":2},\"rotated\":false,\"trimmed\":false,\"duration\":120,\"spriteSourceSize\":{\"x\":0,\"y\":0,\"w\":2,\"h\":2},\"sourceSize\":{\"w\":2,\"h\":2}}," +
"{\"filename\":\"第一帧.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 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;
}
}
}
}