540 lines
22 KiB
C#
540 lines
22 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
|
|
namespace AibisDream.FrameAnimation.Editor
|
|
{
|
|
internal sealed class FrameAnimationGraphImpact
|
|
{
|
|
public IReadOnlyList<AnimationFlow> Flows { get; }
|
|
public int LostNodeCount { get; }
|
|
|
|
internal FrameAnimationGraphImpact(IReadOnlyList<AnimationFlow> flows, int lostNodeCount)
|
|
{
|
|
Flows = flows ?? Array.Empty<AnimationFlow>();
|
|
LostNodeCount = lostNodeCount;
|
|
}
|
|
}
|
|
|
|
internal static class FrameAnimationGraphImpactAnalyzer
|
|
{
|
|
public static FrameAnimationGraphImpact AnalyzeEdgeRemoval(
|
|
FrameAnimationGraph graph,
|
|
IEnumerable<AnimationEdge> edges)
|
|
{
|
|
var removed = new HashSet<AnimationEdge>((edges ?? Array.Empty<AnimationEdge>()).Where(edge => edge != null));
|
|
return Analyze(graph, Array.Empty<AnimationNode>(), removed);
|
|
}
|
|
|
|
public static FrameAnimationGraphImpact AnalyzeNodeRemoval(
|
|
FrameAnimationGraph graph,
|
|
IEnumerable<AnimationNode> nodes)
|
|
{
|
|
var removedNodes = new HashSet<AnimationNode>((nodes ?? Array.Empty<AnimationNode>()).Where(node => node != null));
|
|
var removedIds = new HashSet<string>(removedNodes.Select(node => node.InternalId));
|
|
var removedEdges = new HashSet<AnimationEdge>(graph.Edges.Where(edge => edge != null &&
|
|
(removedIds.Contains(edge.FromNodeId) || removedIds.Contains(edge.ToNodeId))));
|
|
return Analyze(graph, removedNodes, removedEdges);
|
|
}
|
|
|
|
public static FrameAnimationGraphImpact AnalyzeRemoval(
|
|
FrameAnimationGraph graph,
|
|
IEnumerable<AnimationNode> nodes,
|
|
IEnumerable<AnimationEdge> edges)
|
|
{
|
|
if (graph == null)
|
|
{
|
|
return new FrameAnimationGraphImpact(Array.Empty<AnimationFlow>(), 0);
|
|
}
|
|
var removedNodes = new HashSet<AnimationNode>((nodes ?? Array.Empty<AnimationNode>())
|
|
.Where(node => node != null));
|
|
var removedIds = new HashSet<string>(removedNodes.Select(node => node.InternalId));
|
|
var removedEdges = new HashSet<AnimationEdge>((edges ?? Array.Empty<AnimationEdge>())
|
|
.Where(edge => edge != null));
|
|
removedEdges.UnionWith(graph.Edges.Where(edge => edge != null &&
|
|
(removedIds.Contains(edge.FromNodeId) || removedIds.Contains(edge.ToNodeId))));
|
|
return Analyze(graph, removedNodes, removedEdges);
|
|
}
|
|
|
|
private static FrameAnimationGraphImpact Analyze(
|
|
FrameAnimationGraph graph,
|
|
IReadOnlyCollection<AnimationNode> removedNodes,
|
|
IReadOnlyCollection<AnimationEdge> removedEdges)
|
|
{
|
|
if (graph == null)
|
|
{
|
|
return new FrameAnimationGraphImpact(Array.Empty<AnimationFlow>(), 0);
|
|
}
|
|
var topology = new FrameAnimationGraphTopology(graph);
|
|
var removedNodeIds = new HashSet<string>(removedNodes.Select(node => node.InternalId));
|
|
var affected = new List<AnimationFlow>();
|
|
var lost = new HashSet<string>();
|
|
foreach (var flow in graph.Flows.Where(flow => flow != null))
|
|
{
|
|
var before = topology.GetReachable(flow.EntryNodeId).Nodes
|
|
.Where(node => node != null).Select(node => node.InternalId).ToHashSet();
|
|
var after = ReachableWithout(graph, flow.EntryNodeId, removedNodeIds, removedEdges);
|
|
var flowLost = before.Where(nodeId => !after.Contains(nodeId)).ToArray();
|
|
if (flowLost.Length > 0)
|
|
{
|
|
affected.Add(flow);
|
|
lost.UnionWith(flowLost);
|
|
}
|
|
}
|
|
return new FrameAnimationGraphImpact(
|
|
affected.OrderBy(flow => flow.Id, StringComparer.Ordinal).ToArray(),
|
|
lost.Count);
|
|
}
|
|
|
|
private static HashSet<string> ReachableWithout(
|
|
FrameAnimationGraph graph,
|
|
string entry,
|
|
HashSet<string> removedNodeIds,
|
|
IReadOnlyCollection<AnimationEdge> removedEdges)
|
|
{
|
|
var result = new HashSet<string>();
|
|
if (removedNodeIds.Contains(entry ?? string.Empty))
|
|
{
|
|
return result;
|
|
}
|
|
var outgoing = graph.Edges.Where(edge => edge != null && !removedEdges.Contains(edge))
|
|
.GroupBy(edge => edge.FromNodeId ?? string.Empty)
|
|
.ToDictionary(group => group.Key, group => group.ToArray());
|
|
var queue = new Queue<string>();
|
|
queue.Enqueue(entry ?? string.Empty);
|
|
while (queue.Count > 0)
|
|
{
|
|
var id = queue.Dequeue();
|
|
if (removedNodeIds.Contains(id) || !result.Add(id))
|
|
{
|
|
continue;
|
|
}
|
|
if (outgoing.TryGetValue(id, out var edges))
|
|
{
|
|
foreach (var edge in edges)
|
|
{
|
|
queue.Enqueue(edge.ToNodeId ?? string.Empty);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
internal static class FrameAnimationGraphMutationService
|
|
{
|
|
private static readonly Color[] FlowColors =
|
|
{
|
|
new Color(0.24f, 0.65f, 1f), new Color(0.42f, 0.8f, 0.42f),
|
|
new Color(1f, 0.62f, 0.24f), new Color(0.82f, 0.42f, 0.9f),
|
|
new Color(1f, 0.38f, 0.52f), new Color(0.25f, 0.82f, 0.78f)
|
|
};
|
|
|
|
public static bool CreateNode(
|
|
FrameAnimationGraph graph,
|
|
FrameClip clip,
|
|
Vector2 position,
|
|
out AnimationNode node,
|
|
out string error)
|
|
{
|
|
node = null;
|
|
error = string.Empty;
|
|
if (graph == null || clip == null || !graph.Clips.Contains(clip))
|
|
{
|
|
error = "只能为当前 Graph 中的 FrameClip 创建节点。";
|
|
return false;
|
|
}
|
|
Undo.RecordObject(graph, "Create Frame Animation Node");
|
|
node = new AnimationNode(clip.Id,
|
|
string.IsNullOrWhiteSpace(clip.DisplayName) ? clip.Id : clip.DisplayName);
|
|
graph.AddNode(node);
|
|
graph.EditorData.GetOrCreateNodeData(node.InternalId, position).SetPosition(position);
|
|
Dirty(graph);
|
|
return true;
|
|
}
|
|
|
|
public static bool TryConnect(
|
|
FrameAnimationGraph graph,
|
|
AnimationNode from,
|
|
AnimationNode to,
|
|
out AnimationEdge edge,
|
|
out string error)
|
|
{
|
|
edge = null;
|
|
if (!CanConnect(graph, from, to, out error))
|
|
{
|
|
return false;
|
|
}
|
|
Undo.RecordObject(graph, "Connect Frame Animation Nodes");
|
|
edge = new AnimationEdge(from.InternalId, to.InternalId);
|
|
graph.AddEdge(edge);
|
|
Dirty(graph);
|
|
return true;
|
|
}
|
|
|
|
public static bool CanConnect(
|
|
FrameAnimationGraph graph,
|
|
AnimationNode from,
|
|
AnimationNode to,
|
|
out string error)
|
|
{
|
|
error = string.Empty;
|
|
if (graph == null || from == null || to == null ||
|
|
graph.Nodes.Count(node => node == from) != 1 || graph.Nodes.Count(node => node == to) != 1)
|
|
{
|
|
error = "连接两端必须唯一属于当前 Graph。";
|
|
return false;
|
|
}
|
|
if (from == to || from.InternalId == to.InternalId)
|
|
{
|
|
error = "节点不允许连接到自身。";
|
|
return false;
|
|
}
|
|
var topology = new FrameAnimationGraphTopology(graph);
|
|
if (topology.GetOutgoing(from.InternalId).Count > 0)
|
|
{
|
|
error = "第一版每个节点最多只能有一个后继。";
|
|
return false;
|
|
}
|
|
if (from.EndBehaviorOverride.HasValue)
|
|
{
|
|
error = "该节点已设置终点结束行为,请先清除覆盖。";
|
|
return false;
|
|
}
|
|
if (graph.Edges.Any(existing => existing != null &&
|
|
existing.FromNodeId == from.InternalId && existing.ToNodeId == to.InternalId))
|
|
{
|
|
error = "该连接已经存在。";
|
|
return false;
|
|
}
|
|
if (topology.WouldCreateCycle(from.InternalId, to.InternalId))
|
|
{
|
|
error = "该连接会形成自连接或多节点环路。";
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public static void SetNodeDisplayName(FrameAnimationGraph graph, AnimationNode node, string value)
|
|
{
|
|
Undo.RecordObject(graph, "Rename Frame Animation Node");
|
|
node.SetDisplayName(value);
|
|
Dirty(graph);
|
|
}
|
|
|
|
public static void SetNodeClip(FrameAnimationGraph graph, AnimationNode node, FrameClip clip)
|
|
{
|
|
Undo.RecordObject(graph, "Set Frame Animation Node Clip");
|
|
node.SetClipId(clip != null ? clip.Id : string.Empty);
|
|
Dirty(graph);
|
|
}
|
|
|
|
public static void SetNodeSpeed(FrameAnimationGraph graph, AnimationNode node, float? value)
|
|
{
|
|
Undo.RecordObject(graph, "Set Frame Animation Node Speed");
|
|
node.SetSpeedOverride(value);
|
|
Dirty(graph);
|
|
}
|
|
|
|
public static bool SetNodeEndBehavior(
|
|
FrameAnimationGraph graph,
|
|
AnimationNode node,
|
|
FrameClipEndBehavior? value,
|
|
bool removeOutgoing,
|
|
out string error)
|
|
{
|
|
error = string.Empty;
|
|
var outgoing = graph.Edges.Where(edge => edge != null && edge.FromNodeId == node.InternalId).ToArray();
|
|
if (value.HasValue && outgoing.Length > 0 && !removeOutgoing)
|
|
{
|
|
error = "节点仍有后继 Edge。";
|
|
return false;
|
|
}
|
|
Undo.RecordObject(graph, "Set Frame Animation Node End Behavior");
|
|
if (value.HasValue)
|
|
{
|
|
foreach (var edge in outgoing)
|
|
{
|
|
graph.RemoveEdge(edge);
|
|
}
|
|
}
|
|
node.SetEndBehaviorOverride(value);
|
|
Dirty(graph);
|
|
return true;
|
|
}
|
|
|
|
public static void RemoveEdges(FrameAnimationGraph graph, IEnumerable<AnimationEdge> edges)
|
|
{
|
|
var items = (edges ?? Array.Empty<AnimationEdge>()).Where(edge => edge != null).Distinct().ToArray();
|
|
if (graph == null || items.Length == 0)
|
|
{
|
|
return;
|
|
}
|
|
Undo.RecordObject(graph, "Disconnect Frame Animation Nodes");
|
|
foreach (var edge in items)
|
|
{
|
|
graph.RemoveEdge(edge);
|
|
}
|
|
Dirty(graph);
|
|
}
|
|
|
|
public static bool RemoveNodes(
|
|
FrameAnimationGraph graph,
|
|
IEnumerable<AnimationNode> nodes,
|
|
bool deleteEntryFlows,
|
|
out string error)
|
|
{
|
|
return RemoveNodesAndEdges(
|
|
graph, nodes, Array.Empty<AnimationEdge>(), deleteEntryFlows, out error);
|
|
}
|
|
|
|
public static bool RemoveNodesAndEdges(
|
|
FrameAnimationGraph graph,
|
|
IEnumerable<AnimationNode> nodes,
|
|
IEnumerable<AnimationEdge> additionalEdges,
|
|
bool deleteEntryFlows,
|
|
out string error)
|
|
{
|
|
error = string.Empty;
|
|
if (graph == null)
|
|
{
|
|
error = "Graph 已失效。";
|
|
return false;
|
|
}
|
|
var items = (nodes ?? Array.Empty<AnimationNode>()).Where(node => node != null).Distinct().ToArray();
|
|
var extraEdges = (additionalEdges ?? Array.Empty<AnimationEdge>())
|
|
.Where(edge => edge != null).Distinct().ToArray();
|
|
var ids = new HashSet<string>(items.Select(node => node.InternalId));
|
|
var entryFlows = graph.Flows.Where(flow => flow != null && ids.Contains(flow.EntryNodeId)).ToArray();
|
|
if (entryFlows.Length > 0 && !deleteEntryFlows)
|
|
{
|
|
error = "删除项包含 Flow 入口节点。";
|
|
return false;
|
|
}
|
|
Undo.RecordObject(graph, "Delete Frame Animation Graph Elements");
|
|
foreach (var edge in graph.Edges.Where(edge => edge != null &&
|
|
(ids.Contains(edge.FromNodeId) || ids.Contains(edge.ToNodeId) || extraEdges.Contains(edge))).ToArray())
|
|
{
|
|
graph.RemoveEdge(edge);
|
|
}
|
|
foreach (var flow in entryFlows)
|
|
{
|
|
graph.RemoveFlow(flow);
|
|
graph.EditorData.RemoveFlowData(flow.Id);
|
|
if (graph.Settings.DefaultPlayableId == flow.Id)
|
|
{
|
|
graph.Settings.SetDefaultPlayableId(string.Empty);
|
|
}
|
|
}
|
|
foreach (var node in items)
|
|
{
|
|
graph.RemoveNode(node);
|
|
graph.EditorData.RemoveNodeData(node.InternalId);
|
|
}
|
|
Dirty(graph);
|
|
return true;
|
|
}
|
|
|
|
public static string MakeUniqueFlowId(FrameAnimationGraph graph, AnimationNode node)
|
|
{
|
|
var root = (string.IsNullOrWhiteSpace(node?.DisplayName) ? "Animation" : node.DisplayName) + "Flow";
|
|
var candidate = root;
|
|
var suffix = 2;
|
|
while (!FrameAnimationAssetOperations.IsPlayableIdAvailable(graph, candidate))
|
|
{
|
|
candidate = root + suffix++;
|
|
}
|
|
return candidate;
|
|
}
|
|
|
|
public static bool CreateFlow(
|
|
FrameAnimationGraph graph,
|
|
AnimationNode entry,
|
|
string id,
|
|
string displayName,
|
|
out AnimationFlow flow,
|
|
out string error)
|
|
{
|
|
flow = null;
|
|
error = string.Empty;
|
|
if (graph == null || entry == null || !graph.Nodes.Contains(entry))
|
|
{
|
|
error = "Flow 入口必须属于当前 Graph。";
|
|
return false;
|
|
}
|
|
var owner = graph.Flows.FirstOrDefault(existing => existing != null && existing.EntryNodeId == entry.InternalId);
|
|
if (owner != null)
|
|
{
|
|
error = $"该节点已是 Flow '{owner.Id}' 的入口。";
|
|
return false;
|
|
}
|
|
if (!FrameAnimationAssetOperations.IsPlayableIdAvailable(graph, id))
|
|
{
|
|
error = "Flow id 为空或与 Clip / Flow 冲突。";
|
|
return false;
|
|
}
|
|
Undo.RecordObject(graph, "Create Animation Flow");
|
|
flow = new AnimationFlow(id, string.IsNullOrWhiteSpace(displayName) ? id : displayName, entry.InternalId);
|
|
graph.AddFlow(flow);
|
|
graph.EditorData.GetOrCreateFlowData(flow.Id, FlowColors[(graph.Flows.Count - 1) % FlowColors.Length]);
|
|
Dirty(graph);
|
|
return true;
|
|
}
|
|
|
|
public static bool SetFlowEntry(
|
|
FrameAnimationGraph graph,
|
|
AnimationFlow flow,
|
|
AnimationNode entry,
|
|
out string error)
|
|
{
|
|
error = string.Empty;
|
|
if (graph == null || flow == null || entry == null ||
|
|
!graph.Flows.Contains(flow) || !graph.Nodes.Contains(entry))
|
|
{
|
|
error = "Flow 或入口节点不属于当前 Graph。";
|
|
return false;
|
|
}
|
|
var owner = graph.Flows.FirstOrDefault(existing => existing != null && existing != flow &&
|
|
existing.EntryNodeId == entry.InternalId);
|
|
if (owner != null)
|
|
{
|
|
error = $"该节点已是 Flow '{owner.Id}' 的入口。";
|
|
return false;
|
|
}
|
|
Undo.RecordObject(graph, "Set Animation Flow Entry");
|
|
flow.SetEntryNodeId(entry.InternalId);
|
|
Dirty(graph);
|
|
return true;
|
|
}
|
|
|
|
public static void SetFlowColor(FrameAnimationGraph graph, AnimationFlow flow, Color value)
|
|
{
|
|
if (graph == null || flow == null)
|
|
{
|
|
return;
|
|
}
|
|
Undo.RecordObject(graph, "Set Animation Flow Color");
|
|
graph.EditorData.GetOrCreateFlowData(flow.Id, value).SetColor(value);
|
|
Dirty(graph);
|
|
}
|
|
|
|
public static bool EnsureEditorData(FrameAnimationGraph graph)
|
|
{
|
|
if (graph == null)
|
|
{
|
|
return false;
|
|
}
|
|
var missingNodes = graph.Nodes.Where(node => node != null &&
|
|
graph.EditorData.NodeEditorData.All(data => data == null || data.NodeId != node.InternalId)).ToArray();
|
|
var missingFlows = graph.Flows.Where(flow => flow != null &&
|
|
graph.EditorData.FlowEditorData.All(data => data == null || data.FlowId != flow.Id)).ToArray();
|
|
if (missingNodes.Length == 0 && missingFlows.Length == 0)
|
|
{
|
|
return false;
|
|
}
|
|
Undo.RecordObject(graph, "Initialize Frame Animation Graph Layout");
|
|
for (var index = 0; index < missingNodes.Length; index++)
|
|
{
|
|
var position = new Vector2((index % 4) * 300f, (index / 4) * 180f);
|
|
graph.EditorData.GetOrCreateNodeData(missingNodes[index].InternalId, position);
|
|
}
|
|
for (var index = 0; index < missingFlows.Length; index++)
|
|
{
|
|
graph.EditorData.GetOrCreateFlowData(missingFlows[index].Id,
|
|
FlowColors[(graph.Flows.ToList().IndexOf(missingFlows[index]) + FlowColors.Length) % FlowColors.Length]);
|
|
}
|
|
Dirty(graph);
|
|
return true;
|
|
}
|
|
|
|
public static Vector2 GetNodePosition(FrameAnimationGraph graph, AnimationNode node)
|
|
{
|
|
return graph?.EditorData.NodeEditorData.FirstOrDefault(data =>
|
|
data != null && data.NodeId == node?.InternalId)?.Position ?? Vector2.zero;
|
|
}
|
|
|
|
public static void SetNodePositions(
|
|
FrameAnimationGraph graph,
|
|
IReadOnlyDictionary<AnimationNode, Vector2> positions,
|
|
string undoName)
|
|
{
|
|
if (graph == null || positions == null || positions.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
Undo.RecordObject(graph, undoName);
|
|
foreach (var pair in positions)
|
|
{
|
|
graph.EditorData.GetOrCreateNodeData(pair.Key.InternalId, pair.Value).SetPosition(pair.Value);
|
|
}
|
|
Dirty(graph);
|
|
}
|
|
|
|
private static void Dirty(FrameAnimationGraph graph)
|
|
{
|
|
EditorUtility.SetDirty(graph);
|
|
}
|
|
}
|
|
|
|
internal static class FrameAnimationGraphLayoutService
|
|
{
|
|
public static IReadOnlyDictionary<AnimationNode, Vector2> Calculate(
|
|
FrameAnimationGraph graph,
|
|
IEnumerable<AnimationNode> scope)
|
|
{
|
|
var source = scope ?? (graph != null
|
|
? graph.Nodes
|
|
: (IEnumerable<AnimationNode>)Array.Empty<AnimationNode>());
|
|
var nodes = source.Where(node => node != null).Distinct()
|
|
.GroupBy(node => node.InternalId ?? string.Empty)
|
|
.Select(group => group.First()).ToArray();
|
|
if (graph == null || nodes.Length == 0)
|
|
{
|
|
return new Dictionary<AnimationNode, Vector2>();
|
|
}
|
|
var ids = new HashSet<string>(nodes.Select(node => node.InternalId));
|
|
var edges = graph.Edges.Where(edge => edge != null && ids.Contains(edge.FromNodeId) && ids.Contains(edge.ToNodeId)).ToArray();
|
|
var incoming = nodes.ToDictionary(node => node.InternalId, _ => 0);
|
|
foreach (var edge in edges)
|
|
{
|
|
incoming[edge.ToNodeId]++;
|
|
}
|
|
var ranks = nodes.ToDictionary(node => node.InternalId, _ => 0);
|
|
var queue = new Queue<AnimationNode>(nodes.Where(node => incoming[node.InternalId] == 0)
|
|
.OrderBy(node => node.InternalId, StringComparer.Ordinal));
|
|
var processed = new HashSet<string>();
|
|
while (queue.Count > 0)
|
|
{
|
|
var node = queue.Dequeue();
|
|
if (!processed.Add(node.InternalId)) continue;
|
|
foreach (var edge in edges.Where(edge => edge.FromNodeId == node.InternalId))
|
|
{
|
|
ranks[edge.ToNodeId] = Math.Max(ranks[edge.ToNodeId], ranks[node.InternalId] + 1);
|
|
incoming[edge.ToNodeId]--;
|
|
if (incoming[edge.ToNodeId] == 0)
|
|
{
|
|
queue.Enqueue(nodes.First(item => item.InternalId == edge.ToNodeId));
|
|
}
|
|
}
|
|
}
|
|
var oldPositions = nodes.ToDictionary(node => node,
|
|
node => FrameAnimationGraphMutationService.GetNodePosition(graph, node));
|
|
var originX = oldPositions.Values.Min(position => position.x);
|
|
var originY = oldPositions.Values.Min(position => position.y);
|
|
var result = new Dictionary<AnimationNode, Vector2>();
|
|
foreach (var group in nodes.GroupBy(node => ranks[node.InternalId]).OrderBy(group => group.Key))
|
|
{
|
|
var ordered = group.OrderBy(node => oldPositions[node].y)
|
|
.ThenBy(node => node.InternalId, StringComparer.Ordinal).ToArray();
|
|
for (var index = 0; index < ordered.Length; index++)
|
|
{
|
|
result[ordered[index]] = new Vector2(originX + group.Key * 300f, originY + index * 180f);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
}
|