Files
aibis-dream/Assets/Editor/FrameAnimation/FrameClipImportService.cs
T

334 lines
16 KiB
C#

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<FrameAnimationImportIssue> issues = new List<FrameAnimationImportIssue>();
private readonly List<FrameAnimationSpriteDiff> spriteDiffs = new List<FrameAnimationSpriteDiff>();
public FrameClip Clip { get; }
public AsepriteSourceDocument Document { get; internal set; }
public FrameAnimationSpritePlan SpritePlan { get; internal set; }
public IReadOnlyList<int> SourceIndices { get; internal set; } = Array.Empty<int>();
public string SourceTagName { get; internal set; } = string.Empty;
public string CurrentHash { get; internal set; } = string.Empty;
public bool HasFrameChanges { get; internal set; }
public IReadOnlyList<FrameAnimationImportIssue> Issues => issues;
public IReadOnlyList<FrameAnimationSpriteDiff> 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<string>();
foreach (var guid in AssetDatabase.FindAssets("t:FrameAnimationGraph"))
{
var graph = AssetDatabase.LoadAssetAtPath<FrameAnimationGraph>(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<FrameClip>(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);
}
}
}