745 lines
31 KiB
C#
745 lines
31 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using UnityEditor;
|
||
using UnityEngine;
|
||
|
||
namespace AibisDream.FrameAnimation.Editor
|
||
{
|
||
internal enum FrameAnimationEditorSelectionKind
|
||
{
|
||
None,
|
||
Graph,
|
||
Clip,
|
||
Flow,
|
||
Source,
|
||
Node,
|
||
Edge
|
||
}
|
||
|
||
internal readonly struct FrameAnimationEditorSelection
|
||
{
|
||
public FrameAnimationEditorSelectionKind Kind { get; }
|
||
public object Value { get; }
|
||
|
||
public FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind kind, object value)
|
||
{
|
||
Kind = kind;
|
||
Value = value;
|
||
}
|
||
}
|
||
|
||
internal enum FrameAnimationClipFilter
|
||
{
|
||
All,
|
||
Manual,
|
||
Imported,
|
||
Normal,
|
||
Missing,
|
||
Referenced,
|
||
Unused
|
||
}
|
||
|
||
internal enum FrameAnimationClipSort
|
||
{
|
||
Name,
|
||
Source,
|
||
FrameCount,
|
||
Duration,
|
||
Missing,
|
||
ReferenceCount
|
||
}
|
||
|
||
internal static class FrameAnimationAssetReferenceIndex
|
||
{
|
||
public static IReadOnlyList<FrameAnimationGraph> FindGraphsReferencing(FrameClip clip)
|
||
{
|
||
if (clip == null)
|
||
{
|
||
return Array.Empty<FrameAnimationGraph>();
|
||
}
|
||
|
||
return AssetDatabase.FindAssets("t:FrameAnimationGraph")
|
||
.Select(guid => AssetDatabase.LoadAssetAtPath<FrameAnimationGraph>(AssetDatabase.GUIDToAssetPath(guid)))
|
||
.Where(graph => graph != null && graph.Clips.Any(item => item == clip))
|
||
.Distinct()
|
||
.OrderBy(graph => AssetDatabase.GetAssetPath(graph), StringComparer.Ordinal)
|
||
.ToArray();
|
||
}
|
||
}
|
||
|
||
internal static class FrameAnimationResourceQuery
|
||
{
|
||
public static IReadOnlyList<FrameClip> QueryClips(
|
||
FrameAnimationGraph graph,
|
||
string search,
|
||
FrameAnimationClipFilter filter,
|
||
FrameAnimationClipSort sort)
|
||
{
|
||
if (graph == null)
|
||
{
|
||
return Array.Empty<FrameClip>();
|
||
}
|
||
|
||
var sourceNames = graph.ImportSources.Where(source => source != null)
|
||
.GroupBy(source => source.InternalId ?? string.Empty)
|
||
.ToDictionary(group => group.Key, group => group.First().DisplayName ?? string.Empty);
|
||
var query = graph.Clips.Where(clip => clip != null && MatchesSearch(clip, sourceNames, search));
|
||
query = filter switch
|
||
{
|
||
FrameAnimationClipFilter.Manual => query.Where(clip => !clip.IsImported),
|
||
FrameAnimationClipFilter.Imported => query.Where(clip => clip.IsImported),
|
||
FrameAnimationClipFilter.Normal => query.Where(clip => !clip.IsImported || !clip.ImportInfo.IsMissingFromSource),
|
||
FrameAnimationClipFilter.Missing => query.Where(clip => clip.IsImported && clip.ImportInfo.IsMissingFromSource),
|
||
FrameAnimationClipFilter.Referenced => query.Where(clip => ReferenceCount(graph, clip) > 0),
|
||
FrameAnimationClipFilter.Unused => query.Where(clip => ReferenceCount(graph, clip) == 0),
|
||
_ => query
|
||
};
|
||
|
||
IOrderedEnumerable<FrameClip> ordered = sort switch
|
||
{
|
||
FrameAnimationClipSort.Source => query.OrderBy(clip => SourceName(clip, sourceNames), StringComparer.Ordinal),
|
||
FrameAnimationClipSort.FrameCount => query.OrderBy(clip => clip.FrameCount),
|
||
FrameAnimationClipSort.Duration => query.OrderBy(clip => clip.TotalDurationMs),
|
||
FrameAnimationClipSort.Missing => query.OrderBy(clip => clip.IsImported && clip.ImportInfo.IsMissingFromSource ? 1 : 0),
|
||
FrameAnimationClipSort.ReferenceCount => query.OrderBy(clip => ReferenceCount(graph, clip)),
|
||
_ => query.OrderBy(clip => clip.DisplayName ?? clip.Id, StringComparer.Ordinal)
|
||
};
|
||
return ordered.ThenBy(clip => clip.Id, StringComparer.Ordinal)
|
||
.ThenBy(clip => AssetDatabase.GetAssetPath(clip), StringComparer.Ordinal)
|
||
.ToArray();
|
||
}
|
||
|
||
public static int ReferenceCount(FrameAnimationGraph graph, FrameClip clip)
|
||
{
|
||
return graph?.Nodes.Count(node => node != null && node.ClipId == clip?.Id) ?? 0;
|
||
}
|
||
|
||
public static string SourceName(
|
||
FrameClip clip,
|
||
IReadOnlyDictionary<string, string> sourceNames = null)
|
||
{
|
||
if (clip == null || !clip.IsImported)
|
||
{
|
||
return clip != null && clip.HasStandaloneImportSource ? "Standalone Source" : "Manual";
|
||
}
|
||
var sourceId = clip.ImportInfo?.ImportSourceId;
|
||
if (sourceNames != null && !string.IsNullOrEmpty(sourceId) &&
|
||
sourceNames.TryGetValue(sourceId, out var name))
|
||
{
|
||
return name;
|
||
}
|
||
return sourceId ?? string.Empty;
|
||
}
|
||
|
||
private static bool MatchesSearch(
|
||
FrameClip clip,
|
||
IReadOnlyDictionary<string, string> sourceNames,
|
||
string search)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(search))
|
||
{
|
||
return true;
|
||
}
|
||
return Contains(clip.Id, search) || Contains(clip.DisplayName, search) ||
|
||
Contains(clip.ImportInfo?.SourceTagName, search) ||
|
||
Contains(SourceName(clip, sourceNames), search);
|
||
}
|
||
|
||
private static bool Contains(string value, string search)
|
||
{
|
||
return !string.IsNullOrEmpty(value) && value.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0;
|
||
}
|
||
}
|
||
|
||
internal static class FrameAnimationAssetOperations
|
||
{
|
||
public static bool IsPlayableIdAvailable(
|
||
FrameAnimationGraph graph,
|
||
string id,
|
||
FrameClip exceptClip = null,
|
||
AnimationFlow exceptFlow = null)
|
||
{
|
||
return graph != null && !string.IsNullOrWhiteSpace(id) &&
|
||
graph.Clips.All(clip => clip == null || clip == exceptClip || clip.Id != id) &&
|
||
graph.Flows.All(flow => flow == null || flow == exceptFlow || flow.Id != id);
|
||
}
|
||
|
||
public static bool CreateManualClip(
|
||
FrameAnimationGraph graph,
|
||
string id,
|
||
string displayName,
|
||
string externalPath,
|
||
out FrameClip clip,
|
||
out string error)
|
||
{
|
||
clip = null;
|
||
error = string.Empty;
|
||
if (!IsPlayableIdAvailable(graph, id))
|
||
{
|
||
error = "Clip id 为空或与现有 Clip / Flow 冲突。";
|
||
return false;
|
||
}
|
||
|
||
Undo.IncrementCurrentGroup();
|
||
var group = Undo.GetCurrentGroup();
|
||
Undo.SetCurrentGroupName("Create Manual Frame Clip");
|
||
try
|
||
{
|
||
Undo.RecordObject(graph, "Add Manual Frame Clip");
|
||
clip = ScriptableObject.CreateInstance<FrameClip>();
|
||
clip.name = id;
|
||
clip.Configure(id, string.IsNullOrWhiteSpace(displayName) ? id : displayName,
|
||
Array.Empty<FrameAnimationFrame>(), 1f,
|
||
graph.Settings.NewManualClipDefaultEndBehavior);
|
||
if (string.IsNullOrWhiteSpace(externalPath))
|
||
{
|
||
AssetDatabase.AddObjectToAsset(clip, graph);
|
||
}
|
||
else
|
||
{
|
||
AssetDatabase.CreateAsset(clip, externalPath);
|
||
}
|
||
Undo.RegisterCreatedObjectUndo(clip, "Create Manual Frame Clip");
|
||
graph.AddClip(clip);
|
||
EditorUtility.SetDirty(graph);
|
||
EditorUtility.SetDirty(clip);
|
||
AssetDatabase.SaveAssets();
|
||
Undo.CollapseUndoOperations(group);
|
||
return true;
|
||
}
|
||
catch (Exception exception)
|
||
{
|
||
Undo.RevertAllDownToGroup(group);
|
||
clip = null;
|
||
error = exception.Message;
|
||
return false;
|
||
}
|
||
}
|
||
|
||
public static bool AddExistingManualClip(
|
||
FrameAnimationGraph graph,
|
||
FrameClip clip,
|
||
out string error)
|
||
{
|
||
error = string.Empty;
|
||
if (graph == null || clip == null)
|
||
{
|
||
error = "Graph 或 Clip 为空。";
|
||
return false;
|
||
}
|
||
if (clip.IsImported || AssetDatabase.IsSubAsset(clip) ||
|
||
string.IsNullOrEmpty(AssetDatabase.GetAssetPath(clip)))
|
||
{
|
||
error = "只能添加独立 .asset 形式、且不由 Graph ImportSource 管理的 Clip。";
|
||
return false;
|
||
}
|
||
if (graph.Clips.Contains(clip))
|
||
{
|
||
error = "当前 Graph 已引用该 Clip。";
|
||
return false;
|
||
}
|
||
if (!IsPlayableIdAvailable(graph, clip.Id))
|
||
{
|
||
error = $"playable id '{clip.Id}' 与当前 Graph 中的 Clip / Flow 冲突。";
|
||
return false;
|
||
}
|
||
|
||
Undo.RecordObject(graph, "Add Existing Manual Frame Clip");
|
||
graph.AddClip(clip);
|
||
EditorUtility.SetDirty(graph);
|
||
return true;
|
||
}
|
||
|
||
public static bool CopyToManual(
|
||
FrameAnimationGraph graph,
|
||
FrameClip source,
|
||
string id,
|
||
string displayName,
|
||
string externalPath,
|
||
out FrameClip copy,
|
||
out string error)
|
||
{
|
||
copy = null;
|
||
error = string.Empty;
|
||
if (source == null || !IsPlayableIdAvailable(graph, id))
|
||
{
|
||
error = "来源为空,或新 id 与现有 Clip / Flow 冲突。";
|
||
return false;
|
||
}
|
||
|
||
Undo.IncrementCurrentGroup();
|
||
var group = Undo.GetCurrentGroup();
|
||
Undo.SetCurrentGroupName("Copy Frame Clip As Manual");
|
||
try
|
||
{
|
||
Undo.RecordObject(graph, "Add Manual Frame Clip Copy");
|
||
copy = ScriptableObject.CreateInstance<FrameClip>();
|
||
copy.name = id;
|
||
copy.Configure(id,
|
||
string.IsNullOrWhiteSpace(displayName) ? source.DisplayName : displayName,
|
||
source.Frames.Select(frame => frame == null
|
||
? null
|
||
: new FrameAnimationFrame(frame.Sprite, frame.DurationMs, frame.FrameName, frame.SourceIndex)),
|
||
source.Speed,
|
||
source.DefaultEndBehavior);
|
||
if (string.IsNullOrWhiteSpace(externalPath))
|
||
{
|
||
AssetDatabase.AddObjectToAsset(copy, graph);
|
||
}
|
||
else
|
||
{
|
||
AssetDatabase.CreateAsset(copy, externalPath);
|
||
}
|
||
Undo.RegisterCreatedObjectUndo(copy, "Create Manual Frame Clip Copy");
|
||
graph.AddClip(copy);
|
||
EditorUtility.SetDirty(graph);
|
||
EditorUtility.SetDirty(copy);
|
||
AssetDatabase.SaveAssets();
|
||
Undo.CollapseUndoOperations(group);
|
||
return true;
|
||
}
|
||
catch (Exception exception)
|
||
{
|
||
Undo.RevertAllDownToGroup(group);
|
||
copy = null;
|
||
error = exception.Message;
|
||
return false;
|
||
}
|
||
}
|
||
|
||
public static bool RenameClip(
|
||
FrameAnimationGraph graph,
|
||
FrameClip clip,
|
||
string newId,
|
||
out string error)
|
||
{
|
||
error = string.Empty;
|
||
if (graph == null || clip == null || !graph.Clips.Contains(clip))
|
||
{
|
||
error = "Clip 不属于当前 Graph。";
|
||
return false;
|
||
}
|
||
var owners = FrameAnimationAssetReferenceIndex.FindGraphsReferencing(clip);
|
||
if (!AssetDatabase.IsSubAsset(clip) && owners.Count > 1)
|
||
{
|
||
error = "外部 Manual Clip 被多个 Graph 共享,禁止重命名:" +
|
||
string.Join(", ", owners.Select(owner => owner.name));
|
||
return false;
|
||
}
|
||
if (!IsPlayableIdAvailable(graph, newId, clip))
|
||
{
|
||
error = "新 id 为空或与现有 Clip / Flow 冲突。";
|
||
return false;
|
||
}
|
||
|
||
var oldId = clip.Id;
|
||
Undo.IncrementCurrentGroup();
|
||
var group = Undo.GetCurrentGroup();
|
||
Undo.SetCurrentGroupName("Rename Frame Clip");
|
||
Undo.RecordObject(graph, "Update Frame Clip References");
|
||
Undo.RecordObject(clip, "Rename Frame Clip");
|
||
clip.SetId(newId);
|
||
foreach (var node in graph.Nodes.Where(node => node != null && node.ClipId == oldId))
|
||
{
|
||
node.SetClipId(newId);
|
||
}
|
||
if (graph.Settings.DefaultPlayableId == oldId)
|
||
{
|
||
graph.Settings.SetDefaultPlayableId(newId);
|
||
}
|
||
EditorUtility.SetDirty(graph);
|
||
EditorUtility.SetDirty(clip);
|
||
Undo.CollapseUndoOperations(group);
|
||
return true;
|
||
}
|
||
|
||
public static bool RenameFlow(
|
||
FrameAnimationGraph graph,
|
||
AnimationFlow flow,
|
||
string newId,
|
||
out string error)
|
||
{
|
||
error = string.Empty;
|
||
if (graph == null || flow == null || !graph.Flows.Contains(flow) ||
|
||
!IsPlayableIdAvailable(graph, newId, exceptFlow: flow))
|
||
{
|
||
error = "Flow 不属于当前 Graph,或新 id 与现有 Clip / Flow 冲突。";
|
||
return false;
|
||
}
|
||
|
||
var oldId = flow.Id;
|
||
Undo.RecordObject(graph, "Rename Animation Flow");
|
||
flow.SetId(newId);
|
||
if (graph.Settings.DefaultPlayableId == oldId)
|
||
{
|
||
graph.Settings.SetDefaultPlayableId(newId);
|
||
}
|
||
foreach (var data in graph.EditorData.FlowEditorData.Where(data => data != null && data.FlowId == oldId))
|
||
{
|
||
data.SetFlowId(newId);
|
||
}
|
||
EditorUtility.SetDirty(graph);
|
||
return true;
|
||
}
|
||
|
||
public static bool RenameGraph(FrameAnimationGraph graph, string newId, out string error)
|
||
{
|
||
error = string.Empty;
|
||
if (graph == null || string.IsNullOrWhiteSpace(newId))
|
||
{
|
||
error = "Graph id 不能为空。";
|
||
return false;
|
||
}
|
||
Undo.RecordObject(graph, "Rename Frame Animation Graph");
|
||
graph.SetId(newId);
|
||
EditorUtility.SetDirty(graph);
|
||
return true;
|
||
}
|
||
|
||
public static bool RemoveClip(FrameAnimationGraph graph, FrameClip clip, out string error)
|
||
{
|
||
if (!CanRemoveClip(graph, clip, out error))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
Undo.IncrementCurrentGroup();
|
||
var group = Undo.GetCurrentGroup();
|
||
Undo.SetCurrentGroupName("Remove Frame Clip");
|
||
Undo.RecordObject(graph, "Remove Frame Clip Reference");
|
||
graph.RemoveClip(clip);
|
||
if (AssetDatabase.IsSubAsset(clip) && AssetDatabase.GetAssetPath(clip) == AssetDatabase.GetAssetPath(graph))
|
||
{
|
||
Undo.DestroyObjectImmediate(clip);
|
||
}
|
||
EditorUtility.SetDirty(graph);
|
||
AssetDatabase.SaveAssets();
|
||
Undo.CollapseUndoOperations(group);
|
||
return true;
|
||
}
|
||
|
||
public static bool CanRemoveClip(FrameAnimationGraph graph, FrameClip clip, out string error)
|
||
{
|
||
error = string.Empty;
|
||
if (graph == null || clip == null || !graph.Clips.Contains(clip))
|
||
{
|
||
error = "Clip 不属于当前 Graph。";
|
||
return false;
|
||
}
|
||
var nodeCount = graph.Nodes.Count(node => node != null && node.ClipId == clip.Id);
|
||
if (nodeCount > 0)
|
||
{
|
||
error = $"Clip 仍被 {nodeCount} 个 AnimationNode 引用。";
|
||
return false;
|
||
}
|
||
if (graph.Settings.DefaultPlayableId == clip.Id)
|
||
{
|
||
error = "Clip 仍被 defaultPlayableId 引用。";
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
public static bool RemoveFlow(FrameAnimationGraph graph, AnimationFlow flow, out string error)
|
||
{
|
||
if (!CanRemoveFlow(graph, flow, out error))
|
||
{
|
||
return false;
|
||
}
|
||
Undo.RecordObject(graph, "Remove Animation Flow");
|
||
graph.RemoveFlow(flow);
|
||
graph.EditorData.RemoveFlowData(flow.Id);
|
||
EditorUtility.SetDirty(graph);
|
||
return true;
|
||
}
|
||
|
||
public static bool CanRemoveFlow(FrameAnimationGraph graph, AnimationFlow flow, out string error)
|
||
{
|
||
error = string.Empty;
|
||
if (graph == null || flow == null || !graph.Flows.Contains(flow))
|
||
{
|
||
error = "Flow 不属于当前 Graph。";
|
||
return false;
|
||
}
|
||
if (graph.Settings.DefaultPlayableId == flow.Id)
|
||
{
|
||
error = "Flow 仍被 defaultPlayableId 引用。";
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
public static bool RemoveSource(
|
||
FrameAnimationGraph graph,
|
||
FrameAnimationImportSource source,
|
||
out string error)
|
||
{
|
||
error = string.Empty;
|
||
if (graph == null || source == null || !graph.ImportSources.Contains(source))
|
||
{
|
||
error = "ImportSource 不属于当前 Graph。";
|
||
return false;
|
||
}
|
||
var clips = graph.Clips.Where(clip => clip != null &&
|
||
clip.ImportInfo?.ImportSourceId == source.InternalId).ToArray();
|
||
if (clips.Length > 0)
|
||
{
|
||
error = "ImportSource 仍关联 Imported Clip:" + string.Join(", ", clips.Select(clip => clip.Id));
|
||
return false;
|
||
}
|
||
Undo.RecordObject(graph, "Remove Frame Animation ImportSource");
|
||
graph.RemoveImportSource(source);
|
||
EditorUtility.SetDirty(graph);
|
||
return true;
|
||
}
|
||
}
|
||
|
||
internal sealed class FrameAnimationEditorIssue
|
||
{
|
||
public FrameAnimationValidationSeverity Severity { get; }
|
||
public string Code { get; }
|
||
public FrameAnimationEditorSelection Selection { get; }
|
||
public string Message { get; }
|
||
public string Suggestion { get; }
|
||
|
||
public FrameAnimationEditorIssue(
|
||
FrameAnimationValidationSeverity severity,
|
||
string code,
|
||
FrameAnimationEditorSelection selection,
|
||
string message,
|
||
string suggestion)
|
||
{
|
||
Severity = severity;
|
||
Code = code ?? string.Empty;
|
||
Selection = selection;
|
||
Message = message ?? string.Empty;
|
||
Suggestion = suggestion ?? string.Empty;
|
||
}
|
||
}
|
||
|
||
internal static class FrameAnimationEditorValidationService
|
||
{
|
||
public static IReadOnlyList<FrameAnimationEditorIssue> Validate(
|
||
FrameAnimationGraph graph,
|
||
bool includeImports,
|
||
out FrameAnimationImportPreview importPreview)
|
||
{
|
||
importPreview = null;
|
||
if (graph == null)
|
||
{
|
||
return Array.Empty<FrameAnimationEditorIssue>();
|
||
}
|
||
|
||
var issues = FrameAnimationGraphValidator.Validate(graph).Issues
|
||
.Select(issue => new FrameAnimationEditorIssue(
|
||
issue.Severity,
|
||
issue.Code.ToString(),
|
||
Locate(graph, issue.TargetType, issue.TargetId),
|
||
issue.Message,
|
||
Suggestion(issue.Code)))
|
||
.ToList();
|
||
|
||
foreach (var duplicate in graph.Clips.Where(clip => clip != null)
|
||
.GroupBy(clip => clip).Where(group => group.Count() > 1))
|
||
{
|
||
issues.Add(new FrameAnimationEditorIssue(
|
||
FrameAnimationValidationSeverity.Error,
|
||
"DuplicateClipReference",
|
||
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Clip, duplicate.Key),
|
||
$"Graph 重复引用 Clip '{duplicate.Key.Id}'。",
|
||
"移除重复的 Clip 引用。"));
|
||
}
|
||
|
||
var graphPath = AssetDatabase.GetAssetPath(graph);
|
||
foreach (var clip in graph.Clips.Where(clip => clip != null))
|
||
{
|
||
if (AssetDatabase.IsSubAsset(clip) && AssetDatabase.GetAssetPath(clip) != graphPath)
|
||
{
|
||
issues.Add(new FrameAnimationEditorIssue(
|
||
FrameAnimationValidationSeverity.Error,
|
||
"ForeignSubAssetClip",
|
||
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Clip, clip),
|
||
$"Clip '{clip.Id}' 是其他资产的 sub-asset。",
|
||
"移除该引用,并复制为当前 Graph 的 Manual Clip。"));
|
||
}
|
||
if (clip.IsImported && (!AssetDatabase.IsSubAsset(clip) || AssetDatabase.GetAssetPath(clip) != graphPath))
|
||
{
|
||
issues.Add(new FrameAnimationEditorIssue(
|
||
FrameAnimationValidationSeverity.Error,
|
||
"ImportedClipOwnership",
|
||
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Clip, clip),
|
||
$"Imported Clip '{clip.Id}' 不是当前 Graph 的 sub-asset。",
|
||
"删除非法引用并从对应 ImportSource 重新刷新。"));
|
||
}
|
||
}
|
||
|
||
foreach (var node in graph.Nodes.Where(node => node != null))
|
||
{
|
||
var clip = graph.Clips.SingleOrDefault(item => item != null && item.Id == node.ClipId);
|
||
if (clip?.ImportInfo?.IsMissingFromSource == true)
|
||
{
|
||
issues.Add(new FrameAnimationEditorIssue(
|
||
FrameAnimationValidationSeverity.Error,
|
||
"NodeReferencesMissingClip",
|
||
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Node, node),
|
||
$"Node '{node.InternalId}' 引用了 Missing Clip '{clip.Id}'。",
|
||
"恢复源 Tag、修改节点引用或删除该节点。"));
|
||
}
|
||
}
|
||
|
||
var nodeIds = new HashSet<string>(graph.Nodes.Where(node => node != null)
|
||
.Select(node => node.InternalId));
|
||
foreach (var node in graph.Nodes.Where(node => node != null &&
|
||
graph.EditorData.NodeEditorData.All(data => data == null || data.NodeId != node.InternalId)))
|
||
{
|
||
issues.Add(new FrameAnimationEditorIssue(
|
||
FrameAnimationValidationSeverity.Warning,
|
||
"EditorDataMissing",
|
||
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Node, node),
|
||
$"Node '{node.InternalId}' 缺少画布位置数据。",
|
||
"打开 Graph Editor 初始化位置,或执行自动布局。"));
|
||
}
|
||
foreach (var data in graph.EditorData.NodeEditorData.Where(data => data != null &&
|
||
!nodeIds.Contains(data.NodeId)))
|
||
{
|
||
issues.Add(new FrameAnimationEditorIssue(
|
||
FrameAnimationValidationSeverity.Warning,
|
||
"EditorDataOrphan",
|
||
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Graph, graph),
|
||
$"存在孤立的 NodeEditorData '{data.NodeId}'。",
|
||
"该数据不会影响运行时;确认无引用后可由后续维护工具清理。"));
|
||
}
|
||
foreach (var duplicate in graph.EditorData.NodeEditorData.Where(data => data != null)
|
||
.GroupBy(data => data.NodeId).Where(group => group.Count() > 1))
|
||
{
|
||
issues.Add(new FrameAnimationEditorIssue(
|
||
FrameAnimationValidationSeverity.Error,
|
||
"EditorDataDuplicate",
|
||
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Graph, graph),
|
||
$"NodeEditorData '{duplicate.Key}' 重复。",
|
||
"保留一条对应位置数据并移除重复项。"));
|
||
}
|
||
|
||
var flowIds = new HashSet<string>(graph.Flows.Where(flow => flow != null).Select(flow => flow.Id));
|
||
foreach (var flow in graph.Flows.Where(flow => flow != null &&
|
||
graph.EditorData.FlowEditorData.All(data => data == null || data.FlowId != flow.Id)))
|
||
{
|
||
issues.Add(new FrameAnimationEditorIssue(
|
||
FrameAnimationValidationSeverity.Warning,
|
||
"FlowEditorDataMissing",
|
||
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Flow, flow),
|
||
$"Flow '{flow.Id}' 缺少画布颜色数据。",
|
||
"打开 Graph Editor 初始化 Flow 颜色。"));
|
||
}
|
||
foreach (var data in graph.EditorData.FlowEditorData.Where(data => data != null &&
|
||
!flowIds.Contains(data.FlowId)))
|
||
{
|
||
issues.Add(new FrameAnimationEditorIssue(
|
||
FrameAnimationValidationSeverity.Warning,
|
||
"FlowEditorDataOrphan",
|
||
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Graph, graph),
|
||
$"存在孤立的 FlowEditorData '{data.FlowId}'。",
|
||
"确认 Flow 已删除后可由后续维护工具清理。"));
|
||
}
|
||
foreach (var duplicate in graph.EditorData.FlowEditorData.Where(data => data != null)
|
||
.GroupBy(data => data.FlowId).Where(group => group.Count() > 1))
|
||
{
|
||
issues.Add(new FrameAnimationEditorIssue(
|
||
FrameAnimationValidationSeverity.Error,
|
||
"FlowEditorDataDuplicate",
|
||
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Graph, graph),
|
||
$"FlowEditorData '{duplicate.Key}' 重复。",
|
||
"保留一条对应颜色数据并移除重复项。"));
|
||
}
|
||
|
||
if (!includeImports)
|
||
{
|
||
return issues;
|
||
}
|
||
|
||
importPreview = FrameAnimationImportService.PreviewAll(graph);
|
||
foreach (var sourcePreview in importPreview.Sources)
|
||
{
|
||
foreach (var importIssue in sourcePreview.Issues)
|
||
{
|
||
issues.Add(new FrameAnimationEditorIssue(
|
||
importIssue.Severity,
|
||
importIssue.Code.ToString(),
|
||
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Source, sourcePreview.Source),
|
||
importIssue.Message,
|
||
"打开对应 ImportSource,修复来源设置后重新 Preview。"));
|
||
}
|
||
}
|
||
return issues;
|
||
}
|
||
|
||
private static FrameAnimationEditorSelection Locate(
|
||
FrameAnimationGraph graph,
|
||
FrameAnimationValidationTargetType type,
|
||
string id)
|
||
{
|
||
return type switch
|
||
{
|
||
FrameAnimationValidationTargetType.Clip => new FrameAnimationEditorSelection(
|
||
FrameAnimationEditorSelectionKind.Clip, graph.Clips.FirstOrDefault(clip => clip != null && clip.Id == id)),
|
||
FrameAnimationValidationTargetType.Flow => new FrameAnimationEditorSelection(
|
||
FrameAnimationEditorSelectionKind.Flow, graph.Flows.FirstOrDefault(flow => flow != null && flow.Id == id)),
|
||
FrameAnimationValidationTargetType.Node => new FrameAnimationEditorSelection(
|
||
FrameAnimationEditorSelectionKind.Node, graph.Nodes.FirstOrDefault(node => node != null && node.InternalId == id)),
|
||
FrameAnimationValidationTargetType.Edge => new FrameAnimationEditorSelection(
|
||
FrameAnimationEditorSelectionKind.Edge, graph.Edges.FirstOrDefault(edge => edge != null && edge.InternalId == id)),
|
||
FrameAnimationValidationTargetType.ImportSource => new FrameAnimationEditorSelection(
|
||
FrameAnimationEditorSelectionKind.Source, graph.ImportSources.FirstOrDefault(source => source != null && source.InternalId == id)),
|
||
_ => new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Graph, graph)
|
||
};
|
||
}
|
||
|
||
private static string Suggestion(FrameAnimationValidationCode code)
|
||
{
|
||
return code switch
|
||
{
|
||
FrameAnimationValidationCode.PlayableIdEmpty => "使用正式 Rename 命令设置唯一 playable id。",
|
||
FrameAnimationValidationCode.PlayableIdDuplicate => "重命名冲突的 Clip 或 Flow。",
|
||
FrameAnimationValidationCode.ClipFramesEmpty => "为 Manual Clip 添加帧,或刷新 Imported Clip 来源。",
|
||
FrameAnimationValidationCode.FrameDurationInvalid => "将 durationMs 修改为大于 0 的整数。",
|
||
FrameAnimationValidationCode.DefaultPlayableInvalid => "在 Graph 属性中选择有效的默认 Clip 或 Flow。",
|
||
FrameAnimationValidationCode.ImportedClipMissingFromSource => "恢复源 Tag,或确认没有引用后删除 Missing Clip。",
|
||
_ => "打开问题对象并修复对应字段或引用。"
|
||
};
|
||
}
|
||
}
|
||
|
||
internal static class FrameAnimationWorkspaceState
|
||
{
|
||
private const string Prefix = "AibisDream.FrameAnimation.Workspace.";
|
||
|
||
public static string Key(FrameAnimationGraph graph, string name)
|
||
{
|
||
var graphPath = graph != null ? AssetDatabase.GetAssetPath(graph) : string.Empty;
|
||
var graphGuid = string.IsNullOrEmpty(graphPath) ? "none" : AssetDatabase.AssetPathToGUID(graphPath);
|
||
var project = Hash128.Compute(Application.dataPath).ToString();
|
||
return Prefix + project + "." + graphGuid + "." + name;
|
||
}
|
||
|
||
public static float GetFloat(FrameAnimationGraph graph, string name, float fallback) =>
|
||
EditorPrefs.GetFloat(Key(graph, name), fallback);
|
||
|
||
public static void SetFloat(FrameAnimationGraph graph, string name, float value) =>
|
||
EditorPrefs.SetFloat(Key(graph, name), value);
|
||
|
||
public static string GetString(FrameAnimationGraph graph, string name, string fallback) =>
|
||
EditorPrefs.GetString(Key(graph, name), fallback);
|
||
|
||
public static void SetString(FrameAnimationGraph graph, string name, string value) =>
|
||
EditorPrefs.SetString(Key(graph, name), value ?? string.Empty);
|
||
|
||
public static bool GetBool(FrameAnimationGraph graph, string name, bool fallback) =>
|
||
EditorPrefs.GetBool(Key(graph, name), fallback);
|
||
|
||
public static void SetBool(FrameAnimationGraph graph, string name, bool value) =>
|
||
EditorPrefs.SetBool(Key(graph, name), value);
|
||
}
|
||
}
|