using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; namespace AibisDream.FrameAnimation.Editor { internal static class FrameAnimationFlowFocusPolicy { public static AnimationFlow FindFocusedFlow(FrameAnimationGraph graph, string focusedFlowId) { return graph?.Flows.FirstOrDefault(flow => flow != null && flow.Id == focusedFlowId); } public static bool Contains( FrameAnimationGraph graph, string focusedFlowId, FrameAnimationEditorSelection selection) { var flow = FindFocusedFlow(graph, focusedFlowId); if (flow == null) { return false; } var reachable = new FrameAnimationGraphTopology(graph).GetReachable(flow.EntryNodeId); return selection.Value switch { AnimationNode node => reachable.Nodes.Contains(node), AnimationEdge edge => reachable.Edges.Contains(edge), AnimationFlow selectedFlow => selectedFlow == flow, _ => false }; } public static bool ShouldExitForSelection( FrameAnimationGraph graph, string focusedFlowId, FrameAnimationEditorSelection selection) { if (string.IsNullOrEmpty(focusedFlowId)) { return false; } return selection.Kind switch { FrameAnimationEditorSelectionKind.None => false, FrameAnimationEditorSelectionKind.Flow => false, FrameAnimationEditorSelectionKind.Node => !Contains(graph, focusedFlowId, selection), FrameAnimationEditorSelectionKind.Edge => !Contains(graph, focusedFlowId, selection), _ => true }; } public static bool ShouldExitForSelectionSet( FrameAnimationGraph graph, string focusedFlowId, IEnumerable selections) { if (string.IsNullOrEmpty(focusedFlowId)) { return false; } var items = (selections ?? Array.Empty()).ToArray(); return items.Length > 0 && items.Any(item => (item.Kind == FrameAnimationEditorSelectionKind.Node || item.Kind == FrameAnimationEditorSelectionKind.Edge) ? !Contains(graph, focusedFlowId, item) : ShouldExitForSelection(graph, focusedFlowId, item)); } } internal sealed class FrameAnimationGraphImpact { public IReadOnlyList Flows { get; } public int LostNodeCount { get; } internal FrameAnimationGraphImpact(IReadOnlyList flows, int lostNodeCount) { Flows = flows ?? Array.Empty(); LostNodeCount = lostNodeCount; } } internal static class FrameAnimationGraphImpactAnalyzer { public static FrameAnimationGraphImpact AnalyzeEdgeRemoval( FrameAnimationGraph graph, IEnumerable edges) { var removed = new HashSet((edges ?? Array.Empty()).Where(edge => edge != null)); return Analyze(graph, Array.Empty(), removed); } public static FrameAnimationGraphImpact AnalyzeNodeRemoval( FrameAnimationGraph graph, IEnumerable nodes) { var removedNodes = new HashSet((nodes ?? Array.Empty()).Where(node => node != null)); var removedIds = new HashSet(removedNodes.Select(node => node.InternalId)); var removedEdges = new HashSet(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 nodes, IEnumerable edges) { if (graph == null) { return new FrameAnimationGraphImpact(Array.Empty(), 0); } var removedNodes = new HashSet((nodes ?? Array.Empty()) .Where(node => node != null)); var removedIds = new HashSet(removedNodes.Select(node => node.InternalId)); var removedEdges = new HashSet((edges ?? Array.Empty()) .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 removedNodes, IReadOnlyCollection removedEdges) { if (graph == null) { return new FrameAnimationGraphImpact(Array.Empty(), 0); } var topology = new FrameAnimationGraphTopology(graph); var removedNodeIds = new HashSet(removedNodes.Select(node => node.InternalId)); var affected = new List(); var lost = new HashSet(); 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 ReachableWithout( FrameAnimationGraph graph, string entry, HashSet removedNodeIds, IReadOnlyCollection removedEdges) { var result = new HashSet(); 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(); 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 edges) { var items = (edges ?? Array.Empty()).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 nodes, bool deleteEntryFlows, out string error) { return RemoveNodesAndEdges( graph, nodes, Array.Empty(), deleteEntryFlows, out error); } public static bool RemoveNodesAndEdges( FrameAnimationGraph graph, IEnumerable nodes, IEnumerable additionalEdges, bool deleteEntryFlows, out string error) { error = string.Empty; if (graph == null) { error = "Graph 已失效。"; return false; } var items = (nodes ?? Array.Empty()).Where(node => node != null).Distinct().ToArray(); var extraEdges = (additionalEdges ?? Array.Empty()) .Where(edge => edge != null).Distinct().ToArray(); var ids = new HashSet(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 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 Calculate( FrameAnimationGraph graph, IEnumerable scope) { var source = scope ?? (graph != null ? graph.Nodes : (IEnumerable)Array.Empty()); 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(); } var ids = new HashSet(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(nodes.Where(node => incoming[node.InternalId] == 0) .OrderBy(node => node.InternalId, StringComparer.Ordinal)); var processed = new HashSet(); 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(); 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; } } }