334 lines
16 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|