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

745 lines
31 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}