using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Security.Cryptography; using System.Text; using UnityEditor; using UnityEditor.U2D.Sprites; using UnityEngine; namespace AibisDream.FrameAnimation.Editor { internal sealed class FrameAnimationSpritePlan { public string TexturePath { get; } public IReadOnlyList ExistingRects { get; } public IReadOnlyList PlannedRects { get; } public IReadOnlyDictionary ReadOnlySprites { get; } public FrameAnimationSpritePlan( string texturePath, IReadOnlyList existingRects, IReadOnlyList plannedRects, IReadOnlyDictionary readOnlySprites) { TexturePath = texturePath ?? string.Empty; ExistingRects = existingRects ?? Array.Empty(); PlannedRects = plannedRects ?? Array.Empty(); ReadOnlySprites = readOnlySprites ?? new Dictionary(); } } internal sealed class FrameAnimationTextureSnapshot { public string TexturePath { get; } public TextureImporterType TextureType { get; } public SpriteImportMode SpriteImportMode { get; } public IReadOnlyList SpriteRects { get; } public FrameAnimationTextureSnapshot( string texturePath, TextureImporterType textureType, SpriteImportMode spriteImportMode, IReadOnlyList spriteRects) { TexturePath = texturePath; TextureType = textureType; SpriteImportMode = spriteImportMode; SpriteRects = spriteRects; } } internal static class FrameAnimationSpriteUtility { public static FrameAnimationSpritePlan BuildPlan( FrameAnimationImportSource source, AsepriteSourceDocument document, FrameAnimationImportSourcePreview preview) { var texturePath = AssetDatabase.GetAssetPath(source.Texture); var importer = AssetImporter.GetAtPath(texturePath) as TextureImporter; if (importer == null) { preview.AddIssue(Error(source, FrameAnimationImportIssueCode.TextureImporterInvalid, $"无法读取 TextureImporter:{texturePath}")); return new FrameAnimationSpritePlan(texturePath, null, null, null); } var existingRects = ReadSpriteRects(importer); return source.ManageSpriteSlicing ? BuildWritablePlan(source, document, preview, texturePath, existingRects) : BuildReadOnlyPlan(source, document, preview, texturePath, existingRects); } public static Rect ToUnityRect(RectInt asepriteRect, int textureHeight) { return new Rect( asepriteRect.x, textureHeight - asepriteRect.y - asepriteRect.height, asepriteRect.width, asepriteRect.height); } public static FrameAnimationTextureSnapshot CaptureSnapshot(FrameAnimationSpritePlan plan) { var importer = AssetImporter.GetAtPath(plan.TexturePath) as TextureImporter; if (importer == null) { throw new InvalidOperationException($"无法读取 TextureImporter:{plan.TexturePath}"); } return new FrameAnimationTextureSnapshot( plan.TexturePath, importer.textureType, importer.spriteImportMode, CloneRects(ReadSpriteRects(importer))); } public static void ApplyPlan(FrameAnimationSpritePlan plan) { var importer = AssetImporter.GetAtPath(plan.TexturePath) as TextureImporter; if (importer == null) { throw new InvalidOperationException($"无法读取 TextureImporter:{plan.TexturePath}"); } Undo.RecordObject(importer, "Refresh Frame Animation Sprite Slicing"); importer.textureType = TextureImporterType.Sprite; importer.spriteImportMode = SpriteImportMode.Multiple; WriteSpriteRects(importer, plan.PlannedRects); importer.SaveAndReimport(); } public static void RestoreSnapshot(FrameAnimationTextureSnapshot snapshot) { var importer = AssetImporter.GetAtPath(snapshot.TexturePath) as TextureImporter; if (importer == null) { return; } importer.textureType = snapshot.TextureType; importer.spriteImportMode = snapshot.SpriteImportMode; WriteSpriteRects(importer, snapshot.SpriteRects); importer.SaveAndReimport(); } public static IReadOnlyDictionary LoadSpritesByName(string texturePath) { return AssetDatabase.LoadAllAssetsAtPath(texturePath) .OfType() .GroupBy(sprite => sprite.name) .Where(group => group.Count() == 1) .ToDictionary(group => group.Key, group => group.Single()); } public static string ComputeSourceHash( FrameAnimationImportSource source, IReadOnlyList spriteRects = null) { if (source == null || source.Texture == null || source.AsepriteJson == null) { return string.Empty; } var texturePath = AssetDatabase.GetAssetPath(source.Texture); var builder = new StringBuilder(); builder.AppendLine("FrameAnimationImportHash:v1"); builder.AppendLine(source.AsepriteJson.text ?? string.Empty); builder.AppendLine(AssetDatabase.AssetPathToGUID(texturePath)); builder.AppendLine(AssetDatabase.GetAssetDependencyHash(texturePath).ToString()); builder.AppendLine(source.Pivot.x.ToString("R", CultureInfo.InvariantCulture)); builder.AppendLine(source.Pivot.y.ToString("R", CultureInfo.InvariantCulture)); builder.AppendLine(source.ManageSpriteSlicing ? "write" : "read"); var rects = spriteRects; if (rects == null && AssetImporter.GetAtPath(texturePath) is TextureImporter importer) { rects = ReadSpriteRects(importer); } foreach (var rect in (rects ?? Array.Empty()).OrderBy(item => item.name)) { builder.Append(rect.name).Append('|') .Append(rect.spriteID).Append('|') .Append(rect.rect.x.ToString("R", CultureInfo.InvariantCulture)).Append(',') .Append(rect.rect.y.ToString("R", CultureInfo.InvariantCulture)).Append(',') .Append(rect.rect.width.ToString("R", CultureInfo.InvariantCulture)).Append(',') .Append(rect.rect.height.ToString("R", CultureInfo.InvariantCulture)).Append('|') .Append(rect.pivot.x.ToString("R", CultureInfo.InvariantCulture)).Append(',') .Append(rect.pivot.y.ToString("R", CultureInfo.InvariantCulture)).AppendLine(); } using var sha = SHA256.Create(); var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(builder.ToString())); return BitConverter.ToString(bytes).Replace("-", string.Empty).ToLowerInvariant(); } private static FrameAnimationSpritePlan BuildWritablePlan( FrameAnimationImportSource source, AsepriteSourceDocument document, FrameAnimationImportSourcePreview preview, string texturePath, IReadOnlyList existingRects) { var planned = CloneRects(existingRects).ToList(); var byName = planned.GroupBy(rect => rect.name).ToDictionary(group => group.Key, group => group.ToList()); foreach (var frame in document.Frames) { var unityRect = ToUnityRect(frame.Rect, source.Texture.height); if (byName.TryGetValue(frame.FrameName, out var matches) && matches.Count > 1) { preview.AddIssue(Error(source, FrameAnimationImportIssueCode.SpriteMatchFailed, $"Texture 中存在多个名为 '{frame.FrameName}' 的 SpriteRect。")); preview.AddSpriteDiff(new FrameAnimationSpriteDiff( FrameAnimationSpriteChangeKind.Error, frame.FrameName, unityRect, "SpriteRect 名称重复")); continue; } if (matches != null && matches.Count == 1) { var rect = matches[0]; var changed = !RectEquals(rect.rect, unityRect) || rect.pivot != source.Pivot; rect.rect = unityRect; rect.pivot = source.Pivot; rect.alignment = SpriteAlignment.Custom; preview.AddSpriteDiff(new FrameAnimationSpriteDiff( changed ? FrameAnimationSpriteChangeKind.Updated : FrameAnimationSpriteChangeKind.Unchanged, frame.FrameName, unityRect, changed ? "更新 rect/pivot,保留 spriteID" : "无变化")); continue; } var newRect = new SpriteRect { name = frame.FrameName, rect = unityRect, pivot = source.Pivot, alignment = SpriteAlignment.Custom, spriteID = GUID.Generate() }; planned.Add(newRect); byName[frame.FrameName] = new List { newRect }; preview.AddSpriteDiff(new FrameAnimationSpriteDiff( FrameAnimationSpriteChangeKind.Added, frame.FrameName, unityRect, "新增 SpriteRect")); } var sourceNames = new HashSet(document.Frames.Select(frame => frame.FrameName)); foreach (var retained in existingRects.Where(rect => !sourceNames.Contains(rect.name))) { preview.AddSpriteDiff(new FrameAnimationSpriteDiff( FrameAnimationSpriteChangeKind.Retained, retained.name, retained.rect, "源中已消失,保留现有 SpriteRect")); } return new FrameAnimationSpritePlan(texturePath, existingRects, planned, null); } private static FrameAnimationSpritePlan BuildReadOnlyPlan( FrameAnimationImportSource source, AsepriteSourceDocument document, FrameAnimationImportSourcePreview preview, string texturePath, IReadOnlyList existingRects) { var sprites = AssetDatabase.LoadAllAssetsAtPath(texturePath).OfType().ToArray(); var matches = new Dictionary(); foreach (var frame in document.Frames) { var unityRect = ToUnityRect(frame.Rect, source.Texture.height); var named = sprites.Where(sprite => sprite.name == frame.FrameName).ToArray(); var rectMatches = sprites.Where(sprite => RectEquals(sprite.rect, unityRect)).ToArray(); Sprite match = null; if (named.Length == 1 && RectEquals(named[0].rect, unityRect)) { match = named[0]; } else if (named.Length == 0 && rectMatches.Length == 1) { match = rectMatches[0]; } if (match == null || (named.Length > 0 && rectMatches.Length > 0 && !named.Intersect(rectMatches).Any())) { preview.AddIssue(Error(source, FrameAnimationImportIssueCode.SpriteMatchFailed, $"SourceFrame '{frame.FrameName}' 无法按名称和 rect 唯一匹配 Sprite。")); preview.AddSpriteDiff(new FrameAnimationSpriteDiff( FrameAnimationSpriteChangeKind.Error, frame.FrameName, unityRect, "只读 Sprite 匹配失败")); continue; } matches[frame.FrameName] = match; preview.AddSpriteDiff(new FrameAnimationSpriteDiff( FrameAnimationSpriteChangeKind.Unchanged, frame.FrameName, unityRect, $"匹配 Sprite '{match.name}'")); } return new FrameAnimationSpritePlan(texturePath, existingRects, existingRects, matches); } private static IReadOnlyList ReadSpriteRects(TextureImporter importer) { var factories = new SpriteDataProviderFactories(); factories.Init(); var provider = factories.GetSpriteEditorDataProviderFromObject(importer); if (provider == null) { return Array.Empty(); } provider.InitSpriteEditorDataProvider(); return CloneRects(provider.GetSpriteRects()); } private static void WriteSpriteRects(TextureImporter importer, IReadOnlyList rects) { var factories = new SpriteDataProviderFactories(); factories.Init(); var provider = factories.GetSpriteEditorDataProviderFromObject(importer); if (provider == null) { throw new InvalidOperationException($"无法获取 Sprite Data Provider:{importer.assetPath}"); } provider.InitSpriteEditorDataProvider(); provider.SetSpriteRects(CloneRects(rects).ToArray()); provider.Apply(); } private static IReadOnlyList CloneRects(IEnumerable rects) { return (rects ?? Array.Empty()).Select(rect => new SpriteRect { name = rect.name, rect = rect.rect, pivot = rect.pivot, alignment = rect.alignment, border = rect.border, spriteID = rect.spriteID }).ToArray(); } private static bool RectEquals(Rect left, Rect right) { return Mathf.Approximately(left.x, right.x) && Mathf.Approximately(left.y, right.y) && Mathf.Approximately(left.width, right.width) && Mathf.Approximately(left.height, right.height); } private static FrameAnimationImportIssue Error( FrameAnimationImportSource source, FrameAnimationImportIssueCode code, string message) { return new FrameAnimationImportIssue( FrameAnimationValidationSeverity.Error, code, source?.InternalId, source?.InternalId, message); } } }