250 lines
13 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|
|
}
|