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 SpriteNamesBySourceIndex { get; } public IReadOnlyDictionary ReadOnlySpritesBySourceIndex { get; } public FrameAnimationSpritePlan( string texturePath, IReadOnlyList existingRects, IReadOnlyList plannedRects, IReadOnlyDictionary spriteNamesBySourceIndex, IReadOnlyDictionary readOnlySpritesBySourceIndex) { TexturePath = texturePath ?? string.Empty; ExistingRects = existingRects ?? Array.Empty(); PlannedRects = plannedRects ?? Array.Empty(); SpriteNamesBySourceIndex = spriteNamesBySourceIndex ?? new Dictionary(); ReadOnlySpritesBySourceIndex = readOnlySpritesBySourceIndex ?? new Dictionary(); } public bool MatchesSprite(int sourceIndex, Sprite sprite) { if (sprite == null) { return false; } if (ReadOnlySpritesBySourceIndex.TryGetValue(sourceIndex, out var readOnlySprite)) { return readOnlySprite == sprite; } return SpriteNamesBySourceIndex.TryGetValue(sourceIndex, out var spriteName) && sprite.name == spriteName; } public bool TryResolveSprite( int sourceIndex, IReadOnlyDictionary writableSprites, out Sprite sprite) { if (ReadOnlySpritesBySourceIndex.TryGetValue(sourceIndex, out sprite)) { return sprite != null; } if (SpriteNamesBySourceIndex.TryGetValue(sourceIndex, out var spriteName) && writableSprites != null && writableSprites.TryGetValue(spriteName, out sprite)) { return sprite != null; } sprite = null; return false; } } 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, null); } var existingRects = source.ManageSpriteSlicing && importer.spriteImportMode != SpriteImportMode.Multiple ? Array.Empty() : 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:v2"); 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 originalRects = planned.ToArray(); var assigned = new HashSet(); var spriteNamesBySourceIndex = new Dictionary(); foreach (var slot in document.SpriteSlots) { var unityRect = ToUnityRect(slot.Rect, source.Texture.height); SpriteRect matched = null; var slotHasError = false; foreach (var aliasName in slot.FrameNames) { var nameMatches = planned.Where(rect => rect.name == aliasName).ToArray(); if (nameMatches.Length > 1) { preview.AddIssue(Error(source, FrameAnimationImportIssueCode.SpriteMatchFailed, $"Texture 中存在多个名为 '{aliasName}' 的 SpriteRect。")); preview.AddSpriteDiff(new FrameAnimationSpriteDiff( FrameAnimationSpriteChangeKind.Error, slot.CanonicalFrameName, unityRect, "SpriteRect 名称重复", slot.FrameNames)); slotHasError = true; break; } if (nameMatches.Length == 1 && !assigned.Contains(nameMatches[0])) { matched = nameMatches[0]; break; } } if (slotHasError) { continue; } if (matched == null) { var rectMatches = planned.Where(rect => !assigned.Contains(rect) && RectEquals(rect.rect, unityRect)) .ToArray(); if (rectMatches.Length > 1) { preview.AddIssue(Error(source, FrameAnimationImportIssueCode.SpriteMatchFailed, $"共享帧 '{slot.CanonicalFrameName}' 的 rect 对应多个现有 SpriteRect。")); preview.AddSpriteDiff(new FrameAnimationSpriteDiff( FrameAnimationSpriteChangeKind.Error, slot.CanonicalFrameName, unityRect, "SpriteRect rect 匹配不唯一", slot.FrameNames)); continue; } matched = rectMatches.SingleOrDefault(); } if (matched == null) { matched = new SpriteRect { name = slot.CanonicalFrameName, rect = unityRect, pivot = source.Pivot, alignment = SpriteAlignment.Custom, spriteID = GUID.Generate() }; planned.Add(matched); assigned.Add(matched); MapSlot(slot, matched.name, spriteNamesBySourceIndex); preview.AddSpriteDiff(new FrameAnimationSpriteDiff( FrameAnimationSpriteChangeKind.Added, matched.name, unityRect, BuildSlotSummary("新增 SpriteRect", slot), slot.FrameNames)); continue; } var changed = !RectEquals(matched.rect, unityRect) || matched.pivot != source.Pivot || matched.alignment != SpriteAlignment.Custom; matched.rect = unityRect; matched.pivot = source.Pivot; matched.alignment = SpriteAlignment.Custom; assigned.Add(matched); MapSlot(slot, matched.name, spriteNamesBySourceIndex); preview.AddSpriteDiff(new FrameAnimationSpriteDiff( changed ? FrameAnimationSpriteChangeKind.Updated : FrameAnimationSpriteChangeKind.Unchanged, matched.name, unityRect, BuildSlotSummary(changed ? "更新 rect/pivot,保留 spriteID" : "无变化", slot), slot.FrameNames)); } foreach (var retained in originalRects.Where(rect => !assigned.Contains(rect))) { preview.AddSpriteDiff(new FrameAnimationSpriteDiff( FrameAnimationSpriteChangeKind.Retained, retained.name, retained.rect, "源中已消失或不再作为共享帧主 Sprite,保留现有 SpriteRect")); } return new FrameAnimationSpritePlan( texturePath, existingRects, planned, spriteNamesBySourceIndex, 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 slot in document.SpriteSlots) { var unityRect = ToUnityRect(slot.Rect, source.Texture.height); var rectMatches = sprites.Where(sprite => RectEquals(sprite.rect, unityRect)).ToArray(); if (rectMatches.Length != 1) { preview.AddIssue(Error(source, FrameAnimationImportIssueCode.SpriteMatchFailed, $"共享帧 '{slot.CanonicalFrameName}' 无法按 rect 唯一匹配 Sprite,候选数:{rectMatches.Length}。")); preview.AddSpriteDiff(new FrameAnimationSpriteDiff( FrameAnimationSpriteChangeKind.Error, slot.CanonicalFrameName, unityRect, "只读 Sprite 匹配失败", slot.FrameNames)); continue; } foreach (var sourceIndex in slot.SourceIndices) { matches[sourceIndex] = rectMatches[0]; } preview.AddSpriteDiff(new FrameAnimationSpriteDiff( FrameAnimationSpriteChangeKind.Unchanged, rectMatches[0].name, unityRect, BuildSlotSummary($"匹配 Sprite '{rectMatches[0].name}'", slot), slot.FrameNames)); } return new FrameAnimationSpritePlan(texturePath, existingRects, existingRects, null, matches); } private static void MapSlot( AsepriteSpriteSlot slot, string spriteName, IDictionary spriteNamesBySourceIndex) { foreach (var sourceIndex in slot.SourceIndices) { spriteNamesBySourceIndex[sourceIndex] = spriteName; } } private static string BuildSlotSummary(string prefix, AsepriteSpriteSlot slot) { return slot.SourceIndices.Count > 1 ? $"{prefix};{slot.SourceIndices.Count} 个 SourceFrame 共享" : prefix; } 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); } } }