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 { /// /// Aseprite 到 FrameAnimationGraph 的边界契约。 /// 只覆盖解析语义与一次完整刷新,不枚举编辑器操作步骤。 /// 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 { ["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(); graph.Configure( "ImportGraph", "Import Graph", Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), string.Empty); graph.AddImportSource(new FrameAnimationImportSource( "中文来源", AssetDatabase.LoadAssetAtPath(TexturePath), AssetDatabase.LoadAssetAtPath(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; } } } }