using System; using System.Collections.Generic; using System.IO; using System.Linq; using UnityEditor; using UnityEngine; namespace AibisDream.FrameAnimation.Editor { internal sealed class FrameClipImportPreview : IFrameAnimationImportPreviewSink { private readonly List issues = new List(); private readonly List spriteDiffs = new List(); public FrameClip Clip { get; } public AsepriteSourceDocument Document { get; internal set; } public FrameAnimationSpritePlan SpritePlan { get; internal set; } public IReadOnlyList SourceIndices { get; internal set; } = Array.Empty(); public string SourceTagName { get; internal set; } = string.Empty; public string CurrentHash { get; internal set; } = string.Empty; public bool HasFrameChanges { get; internal set; } public IReadOnlyList Issues => issues; public IReadOnlyList SpriteDiffs => spriteDiffs; public bool HasErrors => issues.Any(issue => issue.Severity == FrameAnimationValidationSeverity.Error); public bool HasSpriteChanges => spriteDiffs.Any(diff => diff.Kind == FrameAnimationSpriteChangeKind.Added || diff.Kind == FrameAnimationSpriteChangeKind.Updated); public FrameClipImportPreview(FrameClip clip) { Clip = clip; } public void AddIssue(FrameAnimationImportIssue issue) { if (issue != null) issues.Add(issue); } public void AddSpriteDiff(FrameAnimationSpriteDiff diff) { if (diff != null) spriteDiffs.Add(diff); } } internal static class FrameClipImportService { public static bool EnableSource(FrameClip clip, out string error) { error = string.Empty; if (!IsExternalClip(clip)) { error = "独立来源只能添加到外部 .asset FrameClip。"; return false; } if (clip.IsImported) { error = "Graph Imported Clip 必须继续由所属 Graph 的 ImportSource 管理。"; return false; } if (clip.HasStandaloneImportSource) return true; Undo.RecordObject(clip, "Enable Standalone Frame Clip Source"); clip.GetOrCreateStandaloneImportSource(); EditorUtility.SetDirty(clip); return true; } public static void DetachSource(FrameClip clip) { if (clip == null || !clip.HasStandaloneImportSource) return; Undo.RecordObject(clip, "Detach Standalone Frame Clip Source"); clip.ClearStandaloneImportSource(); EditorUtility.SetDirty(clip); } public static FrameClipImportPreview Preview(FrameClip clip) { var preview = new FrameClipImportPreview(clip); if (!IsExternalClip(clip)) { preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.ImportedClipOwnershipInvalid, "独立来源只能用于外部 .asset FrameClip。")); return preview; } var source = clip.StandaloneImportSource; if (source == null) { preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.ImportSourceReferenceInvalid, "FrameClip 尚未启用独立来源。")); return preview; } if (source.Texture == null) { preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.TextureMissing, "独立来源缺少 Texture。")); return preview; } if (source.AsepriteJson == null) { preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.JsonMissing, "独立来源缺少 Aseprite JSON。")); return preview; } var sourceId = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(clip)); if (!AsepriteJsonParser.TryParse(source.AsepriteJson.text, sourceId, out var document, out var parseIssue)) { preview.AddIssue(parseIssue); return preview; } preview.Document = document; ValidateFrames(clip, source, document, preview); if (preview.HasErrors) return preview; if (document.Tags.Count > 0) { var first = document.Tags[0]; preview.SourceTagName = first.Name; if (string.IsNullOrWhiteSpace(first.Name)) { preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.TagNameInvalid, "第一个 Tag 的名称不能为空。")); return preview; } if (!AsepriteJsonParser.TryExpandTag(first, document.Frames.Count, sourceId, out var indices, out var tagIssue)) { preview.AddIssue(tagIssue); return preview; } preview.SourceIndices = indices; } else { preview.SourceIndices = Enumerable.Range(0, document.Frames.Count).ToArray(); } ValidateTextureOwnership(clip, source, preview); if (preview.HasErrors) return preview; var context = Context(clip, source); preview.SpritePlan = FrameAnimationSpriteUtility.BuildPlan( context, document, preview, preview.SourceIndices.Distinct().ToArray()); preview.CurrentHash = FrameAnimationSpriteUtility.ComputeSourceHash(context); preview.HasFrameChanges = !FramesEqual(clip, preview); if (!string.IsNullOrEmpty(source.LastSourceHash) && source.LastSourceHash != preview.CurrentHash) { preview.AddIssue(new FrameAnimationImportIssue( FrameAnimationValidationSeverity.Warning, FrameAnimationImportIssueCode.SourceChanged, sourceId, clip.Id, "来源内容或切图设置自上次刷新后已发生变化。")); } return preview; } public static bool Apply(FrameClipImportPreview preview, bool allowSpriteChanges, out string error) { error = string.Empty; if (preview?.Clip == null || preview.Document == null || preview.SpritePlan == null) { error = "独立 Clip 导入预览无效。"; return false; } if (preview.HasErrors) { error = "导入预览包含阻断错误,未修改任何资产。"; return false; } if (preview.HasSpriteChanges && !allowSpriteChanges) { error = "导入需要修改 TextureImporter,但尚未获得确认。"; return false; } var clip = preview.Clip; var source = clip.StandaloneImportSource; FrameAnimationTextureSnapshot snapshot = null; Undo.IncrementCurrentGroup(); var group = Undo.GetCurrentGroup(); Undo.SetCurrentGroupName("Refresh Standalone Frame Clip Source"); try { if (source.ManageSpriteSlicing && preview.HasSpriteChanges) { snapshot = FrameAnimationSpriteUtility.CaptureSnapshot(preview.SpritePlan); FrameAnimationSpriteUtility.ApplyPlan(preview.SpritePlan); } var sprites = source.ManageSpriteSlicing ? FrameAnimationSpriteUtility.LoadSpritesByName(preview.SpritePlan.TexturePath) : null; var frames = preview.SourceIndices.Select(sourceIndex => { var sourceFrame = preview.Document.Frames[sourceIndex]; if (!preview.SpritePlan.TryResolveSprite(sourceIndex, sprites, out var sprite)) throw new InvalidOperationException($"刷新后找不到 SourceFrame '{sourceFrame.FrameName}' 对应的 Sprite。"); return new FrameAnimationFrame(sprite, sourceFrame.DurationMs, sourceFrame.FrameName, sourceIndex); }).ToArray(); Undo.RecordObject(clip, "Refresh Standalone Frame Clip"); clip.SetFrames(frames); if (string.IsNullOrWhiteSpace(clip.Id)) { var initialId = !string.IsNullOrWhiteSpace(preview.SourceTagName) ? preview.SourceTagName : Path.GetFileNameWithoutExtension(AssetDatabase.GetAssetPath(clip)); clip.SetId(initialId); if (string.IsNullOrWhiteSpace(clip.DisplayName)) clip.SetDisplayName(initialId); } source.SetLastImport( FrameAnimationSpriteUtility.ComputeSourceHash(Context(clip, source)), preview.SourceTagName); EditorUtility.SetDirty(clip); AssetDatabase.SaveAssets(); Undo.CollapseUndoOperations(group); return true; } catch (Exception exception) { Undo.RevertAllDownToGroup(group); if (snapshot != null) FrameAnimationSpriteUtility.RestoreSnapshot(snapshot); AssetDatabase.SaveAssets(); error = $"独立 Clip 导入失败,已尝试回滚:{exception.Message}"; Debug.LogException(exception); return false; } } private static void ValidateFrames( FrameClip clip, FrameClipImportSource source, AsepriteSourceDocument document, FrameClipImportPreview preview) { if (document.Frames.Count == 0) preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.FramesMissing, "Aseprite JSON 的 frames 为空。")); foreach (var duplicate in document.Frames.GroupBy(frame => frame.FrameName).Where(group => group.Count() > 1)) preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.FrameNameDuplicate, $"frameName '{duplicate.Key}' 在来源内重复。")); for (var index = 0; index < document.Frames.Count; index++) { var frame = document.Frames[index]; if (string.IsNullOrWhiteSpace(frame.FrameName)) preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.FrameNameInvalid, $"SourceFrame[{index}] 的 frameName 为空。")); if (frame.DurationMs <= 0) preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.FrameDurationInvalid, $"SourceFrame '{frame.FrameName}' 的 duration 必须大于 0。")); if (frame.Rect.width <= 0 || frame.Rect.height <= 0 || frame.Rect.x < 0 || frame.Rect.y < 0 || frame.Rect.xMax > source.Texture.width || frame.Rect.yMax > source.Texture.height) preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.FrameRectInvalid, $"SourceFrame '{frame.FrameName}' 的 rect 越出 Texture 范围。")); if (frame.Trimmed) preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.TrimmedUnsupported, $"SourceFrame '{frame.FrameName}' 使用了不支持的 trimmed。")); if (frame.Rotated) preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.RotatedUnsupported, $"SourceFrame '{frame.FrameName}' 使用了不支持的 rotated。")); } if (document.TextureSize.x != source.Texture.width || document.TextureSize.y != source.Texture.height) preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.TextureSizeMismatch, $"meta.size {document.TextureSize.x}x{document.TextureSize.y} 与 Texture {source.Texture.width}x{source.Texture.height} 不一致。")); } private static void ValidateTextureOwnership( FrameClip clip, FrameClipImportSource source, FrameClipImportPreview preview) { if (!source.ManageSpriteSlicing) return; var owners = new List(); foreach (var guid in AssetDatabase.FindAssets("t:FrameAnimationGraph")) { var graph = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guid)); if (graph == null) continue; owners.AddRange(graph.ImportSources.Where(item => item != null && item.IsEnabled && item.ManageSpriteSlicing && item.Texture == source.Texture) .Select(item => $"Graph '{graph.name}' / {item.DisplayName}")); } foreach (var guid in AssetDatabase.FindAssets("t:FrameClip")) { var other = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guid)); var otherSource = other?.StandaloneImportSource; if (other == null || other == clip || otherSource == null || !otherSource.ManageSpriteSlicing || otherSource.Texture != source.Texture) continue; owners.Add($"FrameClip '{other.name}'"); } if (owners.Count > 0) preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.TextureOwnershipConflict, $"Texture '{source.Texture.name}' 已有其他可写来源:{string.Join(", ", owners)}")); } private static bool FramesEqual(FrameClip clip, FrameClipImportPreview preview) { if (clip.FrameCount != preview.SourceIndices.Count) return false; for (var index = 0; index < preview.SourceIndices.Count; index++) { var sourceIndex = preview.SourceIndices[index]; var sourceFrame = preview.Document.Frames[sourceIndex]; var frame = clip.Frames[index]; if (frame == null || frame.SourceIndex != sourceIndex || frame.FrameName != sourceFrame.FrameName || frame.DurationMs != sourceFrame.DurationMs || !preview.SpritePlan.MatchesSprite(sourceIndex, frame.Sprite)) return false; } return true; } private static FrameAnimationSpriteSourceContext Context(FrameClip clip, FrameClipImportSource source) { return new FrameAnimationSpriteSourceContext( AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(clip)), source.Texture, source.AsepriteJson, source.Pivot, source.ManageSpriteSlicing); } private static bool IsExternalClip(FrameClip clip) { return clip != null && !AssetDatabase.IsSubAsset(clip) && !string.IsNullOrEmpty(AssetDatabase.GetAssetPath(clip)); } private static FrameAnimationImportIssue Error( FrameClip clip, FrameAnimationImportIssueCode code, string message) { var path = clip != null ? AssetDatabase.GetAssetPath(clip) : string.Empty; return new FrameAnimationImportIssue( FrameAnimationValidationSeverity.Error, code, AssetDatabase.AssetPathToGUID(path), clip != null ? clip.Id : string.Empty, message); } } }