feat(frameAnima): 动画系统前四个阶段
This commit is contained in:
@@ -0,0 +1,622 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Experimental.GraphView;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace AibisDream.FrameAnimation.Editor
|
||||
{
|
||||
internal sealed class FrameAnimationClipNodeView : Node
|
||||
{
|
||||
private readonly Label subtitle;
|
||||
private readonly Label behavior;
|
||||
private readonly Label flowBadge;
|
||||
private readonly Label issueBadge;
|
||||
private readonly Action<FrameAnimationClipNodeView> selectedCallback;
|
||||
|
||||
public AnimationNode Data { get; }
|
||||
public Port InputPort { get; }
|
||||
public Port OutputPort { get; }
|
||||
|
||||
internal FrameAnimationClipNodeView(
|
||||
AnimationNode data,
|
||||
Action<FrameAnimationClipNodeView> selectedCallback)
|
||||
{
|
||||
Data = data;
|
||||
this.selectedCallback = selectedCallback;
|
||||
viewDataKey = data.InternalId;
|
||||
style.minWidth = 245f;
|
||||
style.maxWidth = 285f;
|
||||
|
||||
InputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Input, Port.Capacity.Multi,
|
||||
typeof(AnimationNode));
|
||||
InputPort.portName = "In";
|
||||
inputContainer.Add(InputPort);
|
||||
OutputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Output, Port.Capacity.Single,
|
||||
typeof(AnimationNode));
|
||||
OutputPort.portName = "Next";
|
||||
outputContainer.Add(OutputPort);
|
||||
|
||||
subtitle = new Label();
|
||||
behavior = new Label();
|
||||
flowBadge = new Label();
|
||||
issueBadge = new Label();
|
||||
issueBadge.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
extensionContainer.Add(subtitle);
|
||||
extensionContainer.Add(behavior);
|
||||
extensionContainer.Add(flowBadge);
|
||||
extensionContainer.Add(issueBadge);
|
||||
extensionContainer.Add(new Label("Preview · Phase 5")
|
||||
{
|
||||
style =
|
||||
{
|
||||
height = 72f,
|
||||
unityTextAlign = TextAnchor.MiddleCenter,
|
||||
backgroundColor = new StyleColor(new Color(0.12f, 0.12f, 0.12f, 0.8f)),
|
||||
marginTop = 5f
|
||||
}
|
||||
});
|
||||
RefreshExpandedState();
|
||||
}
|
||||
|
||||
public override void OnSelected()
|
||||
{
|
||||
base.OnSelected();
|
||||
selectedCallback?.Invoke(this);
|
||||
}
|
||||
|
||||
internal void Refresh(
|
||||
FrameAnimationGraph graph,
|
||||
IReadOnlyList<FrameAnimationEditorIssue> issues,
|
||||
IReadOnlyList<AnimationFlow> flows,
|
||||
IReadOnlyCollection<string> entryFlowIds)
|
||||
{
|
||||
var clip = graph.Clips.FirstOrDefault(item => item != null && item.Id == Data.ClipId);
|
||||
title = string.IsNullOrWhiteSpace(Data.DisplayName) ? Data.ClipId : Data.DisplayName;
|
||||
subtitle.text = clip != null ? $"Clip: {Data.ClipId}" : $"Clip: <Missing {Data.ClipId}>";
|
||||
var speed = Data.SpeedOverride.HasValue
|
||||
? $"Speed {Data.SpeedOverride.Value:0.###} (Node)"
|
||||
: clip != null ? $"Speed {clip.Speed:0.###} (Clip)" : "Speed ?";
|
||||
var end = Data.EndBehaviorOverride?.ToString() ?? "Continue / terminal fallback";
|
||||
behavior.text = $"{speed} · {end}";
|
||||
var used = flows.Select(flow => flow.Id).ToArray();
|
||||
var entries = used.Where(entryFlowIds.Contains).ToArray();
|
||||
flowBadge.text = used.Length == 0
|
||||
? "Unused"
|
||||
: "Flows: " + string.Join(", ", used.Take(3)) + (used.Length > 3 ? $" +{used.Length - 3}" : string.Empty) +
|
||||
(entries.Length > 0 ? " [ENTRY]" : string.Empty);
|
||||
var matching = issues.Where(issue => issue.Selection.Kind == FrameAnimationEditorSelectionKind.Node &&
|
||||
ReferenceEquals(issue.Selection.Value, Data)).ToArray();
|
||||
issueBadge.text = matching.Any(issue => issue.Severity == FrameAnimationValidationSeverity.Error)
|
||||
? "ERROR"
|
||||
: matching.Length > 0 ? "WARNING" : string.Empty;
|
||||
issueBadge.style.color = matching.Any(issue => issue.Severity == FrameAnimationValidationSeverity.Error)
|
||||
? new StyleColor(new Color(1f, 0.35f, 0.3f))
|
||||
: new StyleColor(new Color(1f, 0.72f, 0.25f));
|
||||
OutputPort.SetEnabled(!Data.EndBehaviorOverride.HasValue);
|
||||
OutputPort.tooltip = Data.EndBehaviorOverride.HasValue
|
||||
? "该节点已设置终点结束行为,请先清除覆盖。"
|
||||
: "创建唯一的顺序后继";
|
||||
}
|
||||
|
||||
internal void SetDimmed(bool dimmed)
|
||||
{
|
||||
style.opacity = dimmed ? 0.22f : 1f;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FrameAnimationEdgeView : Edge
|
||||
{
|
||||
private readonly Action<FrameAnimationEdgeView> selectedCallback;
|
||||
public AnimationEdge Data { get; }
|
||||
|
||||
internal FrameAnimationEdgeView(AnimationEdge data, Action<FrameAnimationEdgeView> selectedCallback)
|
||||
{
|
||||
Data = data;
|
||||
this.selectedCallback = selectedCallback;
|
||||
viewDataKey = data.InternalId;
|
||||
userData = data;
|
||||
}
|
||||
|
||||
public override void OnSelected()
|
||||
{
|
||||
base.OnSelected();
|
||||
selectedCallback?.Invoke(this);
|
||||
}
|
||||
|
||||
internal void SetDimmed(bool dimmed)
|
||||
{
|
||||
style.opacity = dimmed ? 0.16f : 1f;
|
||||
}
|
||||
|
||||
internal void Refresh(IReadOnlyList<FrameAnimationEditorIssue> issues)
|
||||
{
|
||||
var matching = (issues ?? Array.Empty<FrameAnimationEditorIssue>()).Where(issue =>
|
||||
issue.Selection.Kind == FrameAnimationEditorSelectionKind.Edge &&
|
||||
ReferenceEquals(issue.Selection.Value, Data)).ToArray();
|
||||
var color = matching.Any(issue => issue.Severity == FrameAnimationValidationSeverity.Error)
|
||||
? new Color(1f, 0.3f, 0.25f)
|
||||
: matching.Length > 0 ? new Color(1f, 0.72f, 0.2f) : Color.white;
|
||||
edgeControl.inputColor = color;
|
||||
edgeControl.outputColor = color;
|
||||
tooltip = matching.Length == 0
|
||||
? $"{Data.ExitName} / {Data.Condition}"
|
||||
: string.Join("\n", matching.Select(issue => $"[{issue.Code}] {issue.Message}"));
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FrameAnimationClipSearchProvider : ScriptableObject, ISearchWindowProvider
|
||||
{
|
||||
private FrameAnimationGraph graph;
|
||||
private Action<FrameClip> selected;
|
||||
|
||||
internal void Configure(FrameAnimationGraph value, Action<FrameClip> callback)
|
||||
{
|
||||
graph = value;
|
||||
selected = callback;
|
||||
}
|
||||
|
||||
public List<SearchTreeEntry> CreateSearchTree(SearchWindowContext context)
|
||||
{
|
||||
var entries = new List<SearchTreeEntry>
|
||||
{
|
||||
new SearchTreeGroupEntry(new GUIContent("Create Clip Node"), 0)
|
||||
};
|
||||
foreach (var clip in graph.Clips.Where(clip => clip != null)
|
||||
.OrderBy(clip => clip.DisplayName, StringComparer.Ordinal)
|
||||
.ThenBy(clip => clip.Id, StringComparer.Ordinal))
|
||||
{
|
||||
entries.Add(new SearchTreeEntry(new GUIContent($"{clip.DisplayName} ({clip.Id})"))
|
||||
{
|
||||
level = 1,
|
||||
userData = clip
|
||||
});
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
public bool OnSelectEntry(SearchTreeEntry entry, SearchWindowContext context)
|
||||
{
|
||||
if (entry.userData is FrameClip clip)
|
||||
{
|
||||
selected?.Invoke(clip);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FrameAnimationGraphView : GraphView
|
||||
{
|
||||
internal const string ClipDragKey = "AibisDream.FrameAnimation.ClipDrag";
|
||||
|
||||
private readonly Dictionary<string, FrameAnimationClipNodeView> nodeViews =
|
||||
new Dictionary<string, FrameAnimationClipNodeView>();
|
||||
private readonly Dictionary<string, FrameAnimationEdgeView> edgeViews =
|
||||
new Dictionary<string, FrameAnimationEdgeView>();
|
||||
private FrameAnimationGraph graph;
|
||||
private IReadOnlyList<FrameAnimationEditorIssue> issues = Array.Empty<FrameAnimationEditorIssue>();
|
||||
private bool rebuilding;
|
||||
private Vector2 contextPosition;
|
||||
|
||||
internal event Action<FrameAnimationEditorSelection> SelectionRequested;
|
||||
internal event Action<IReadOnlyList<AnimationNode>> MultiSelectionChanged;
|
||||
internal event Action<AnimationNode> CreateFlowRequested;
|
||||
internal event Action GraphChanged;
|
||||
internal event Action<string> NotificationRequested;
|
||||
|
||||
internal FrameAnimationGraphView()
|
||||
{
|
||||
style.flexGrow = 1f;
|
||||
SetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale);
|
||||
this.AddManipulator(new ContentDragger());
|
||||
this.AddManipulator(new SelectionDragger());
|
||||
this.AddManipulator(new RectangleSelector());
|
||||
var grid = new GridBackground();
|
||||
Insert(0, grid);
|
||||
grid.StretchToParentSize();
|
||||
graphViewChanged = OnGraphViewChanged;
|
||||
RegisterCallback<MouseUpEvent>(_ => NotifySelectionSummary());
|
||||
RegisterCallback<DragUpdatedEvent>(OnDragUpdated);
|
||||
RegisterCallback<DragPerformEvent>(OnDragPerform);
|
||||
}
|
||||
|
||||
internal void Bind(
|
||||
FrameAnimationGraph value,
|
||||
IReadOnlyList<FrameAnimationEditorIssue> validationIssues,
|
||||
string focusedFlowId)
|
||||
{
|
||||
graph = value;
|
||||
issues = validationIssues ?? Array.Empty<FrameAnimationEditorIssue>();
|
||||
var selectedIds = selection.OfType<FrameAnimationClipNodeView>()
|
||||
.Select(view => view.Data.InternalId).ToArray();
|
||||
var selectedEdgeIds = selection.OfType<FrameAnimationEdgeView>()
|
||||
.Select(view => view.Data.InternalId).ToArray();
|
||||
rebuilding = true;
|
||||
DeleteElements(graphElements.ToList());
|
||||
nodeViews.Clear();
|
||||
edgeViews.Clear();
|
||||
if (graph != null)
|
||||
{
|
||||
var topology = new FrameAnimationGraphTopology(graph);
|
||||
var entries = new HashSet<string>(graph.Flows.Where(flow => flow != null)
|
||||
.Select(flow => flow.EntryNodeId));
|
||||
foreach (var node in graph.Nodes.Where(node => node != null))
|
||||
{
|
||||
var view = new FrameAnimationClipNodeView(node, OnNodeSelected);
|
||||
view.SetPosition(new Rect(FrameAnimationGraphMutationService.GetNodePosition(graph, node),
|
||||
new Vector2(255f, 150f)));
|
||||
view.Refresh(graph, issues, topology.FindFlowsUsingNode(node.InternalId), entries);
|
||||
nodeViews[node.InternalId] = view;
|
||||
AddElement(view);
|
||||
}
|
||||
foreach (var edge in graph.Edges.Where(edge => edge != null))
|
||||
{
|
||||
if (!nodeViews.TryGetValue(edge.FromNodeId ?? string.Empty, out var from) ||
|
||||
!nodeViews.TryGetValue(edge.ToNodeId ?? string.Empty, out var to))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var view = new FrameAnimationEdgeView(edge, OnEdgeSelected)
|
||||
{
|
||||
output = from.OutputPort,
|
||||
input = to.InputPort
|
||||
};
|
||||
view.output.Connect(view);
|
||||
view.input.Connect(view);
|
||||
view.Refresh(issues);
|
||||
edgeViews[edge.InternalId] = view;
|
||||
AddElement(view);
|
||||
}
|
||||
foreach (var id in selectedIds.Where(nodeViews.ContainsKey))
|
||||
{
|
||||
AddToSelection(nodeViews[id]);
|
||||
}
|
||||
foreach (var id in selectedEdgeIds.Where(edgeViews.ContainsKey))
|
||||
{
|
||||
AddToSelection(edgeViews[id]);
|
||||
}
|
||||
}
|
||||
rebuilding = false;
|
||||
SetFocusedFlow(focusedFlowId);
|
||||
}
|
||||
|
||||
public override void BuildContextualMenu(ContextualMenuPopulateEvent evt)
|
||||
{
|
||||
if (evt.target == this || evt.target == contentViewContainer)
|
||||
{
|
||||
contextPosition = contentViewContainer.WorldToLocal(evt.mousePosition);
|
||||
evt.menu.AppendAction("Create Clip Node", _ => OpenClipSearch(evt.mousePosition),
|
||||
graph != null && graph.Clips.Any(clip => clip != null)
|
||||
? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Disabled);
|
||||
}
|
||||
if (selection.Count == 1 && selection[0] is FrameAnimationClipNodeView nodeView)
|
||||
{
|
||||
evt.menu.AppendAction("Create Flow From Node", _ => CreateFlowRequested?.Invoke(nodeView.Data));
|
||||
}
|
||||
base.BuildContextualMenu(evt);
|
||||
}
|
||||
|
||||
public override List<Port> GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter)
|
||||
{
|
||||
var result = new List<Port>();
|
||||
if (graph == null || !(startPort.node is FrameAnimationClipNodeView startNode))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
ports.ForEach(port =>
|
||||
{
|
||||
if (!(port.node is FrameAnimationClipNodeView targetNode) || port.direction == startPort.direction ||
|
||||
targetNode == startNode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var from = startPort.direction == Direction.Output ? startNode.Data : targetNode.Data;
|
||||
var to = startPort.direction == Direction.Output ? targetNode.Data : startNode.Data;
|
||||
if (FrameAnimationGraphMutationService.CanConnect(graph, from, to, out _))
|
||||
{
|
||||
result.Add(port);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
internal void SetFocusedFlow(string flowId)
|
||||
{
|
||||
if (graph == null || string.IsNullOrEmpty(flowId))
|
||||
{
|
||||
foreach (var view in nodeViews.Values) view.SetDimmed(false);
|
||||
foreach (var view in edgeViews.Values) view.SetDimmed(false);
|
||||
return;
|
||||
}
|
||||
var flow = graph.Flows.FirstOrDefault(item => item != null && item.Id == flowId);
|
||||
var topology = new FrameAnimationGraphTopology(graph);
|
||||
var reachable = flow != null
|
||||
? topology.GetReachable(flow.EntryNodeId)
|
||||
: new FrameAnimationReachability(Array.Empty<AnimationNode>(), Array.Empty<AnimationEdge>());
|
||||
var nodeIds = new HashSet<string>(reachable.Nodes.Select(node => node.InternalId));
|
||||
var edgeIds = new HashSet<string>(reachable.Edges.Select(edge => edge.InternalId));
|
||||
foreach (var pair in nodeViews) pair.Value.SetDimmed(!nodeIds.Contains(pair.Key));
|
||||
foreach (var pair in edgeViews) pair.Value.SetDimmed(!edgeIds.Contains(pair.Key));
|
||||
}
|
||||
|
||||
internal void SelectAndFrame(FrameAnimationEditorSelection target)
|
||||
{
|
||||
GraphElement element = target.Value switch
|
||||
{
|
||||
AnimationNode node when nodeViews.TryGetValue(node.InternalId, out var nodeView) => nodeView,
|
||||
AnimationEdge edge when edgeViews.TryGetValue(edge.InternalId, out var edgeView) => edgeView,
|
||||
_ => null
|
||||
};
|
||||
if (element == null) return;
|
||||
ClearSelection();
|
||||
AddToSelection(element);
|
||||
FrameSelection();
|
||||
}
|
||||
|
||||
internal IReadOnlyList<AnimationNode> SelectedNodes =>
|
||||
selection.OfType<FrameAnimationClipNodeView>().Select(view => view.Data).ToArray();
|
||||
|
||||
internal void RequestDeleteNodes(IEnumerable<AnimationNode> nodes)
|
||||
{
|
||||
var items = (nodes ?? Array.Empty<AnimationNode>()).Where(node => node != null).Distinct().ToArray();
|
||||
if (graph == null || items.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var ids = new HashSet<string>(items.Select(node => node.InternalId));
|
||||
var entryFlows = graph.Flows.Where(flow => flow != null && ids.Contains(flow.EntryNodeId)).ToArray();
|
||||
var impact = FrameAnimationGraphImpactAnalyzer.AnalyzeNodeRemoval(graph, items);
|
||||
if (!EditorUtility.DisplayDialog(
|
||||
"Delete Frame Animation Nodes",
|
||||
BuildRemovalMessage("Node", items.Length, impact, entryFlows),
|
||||
entryFlows.Length > 0 ? "Delete Nodes + Flows" : "Delete",
|
||||
"Cancel"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!FrameAnimationGraphMutationService.RemoveNodes(graph, items, entryFlows.Length > 0, out var error))
|
||||
{
|
||||
NotificationRequested?.Invoke(error);
|
||||
return;
|
||||
}
|
||||
GraphChanged?.Invoke();
|
||||
}
|
||||
|
||||
internal void RequestDeleteEdges(IEnumerable<AnimationEdge> edges)
|
||||
{
|
||||
var items = (edges ?? Array.Empty<AnimationEdge>()).Where(edge => edge != null).Distinct().ToArray();
|
||||
if (graph == null || items.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var impact = FrameAnimationGraphImpactAnalyzer.AnalyzeEdgeRemoval(graph, items);
|
||||
if (impact.Flows.Count > 0 && !EditorUtility.DisplayDialog(
|
||||
"Disconnect Frame Animation Nodes",
|
||||
BuildRemovalMessage("Edge", items.Length, impact, Array.Empty<AnimationFlow>()),
|
||||
"Disconnect",
|
||||
"Cancel"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
FrameAnimationGraphMutationService.RemoveEdges(graph, items);
|
||||
GraphChanged?.Invoke();
|
||||
}
|
||||
|
||||
internal void FrameCurrentFlow(string flowId)
|
||||
{
|
||||
if (graph == null || string.IsNullOrEmpty(flowId)) return;
|
||||
var flow = graph.Flows.FirstOrDefault(item => item != null && item.Id == flowId);
|
||||
if (flow == null) return;
|
||||
var ids = new HashSet<string>(new FrameAnimationGraphTopology(graph)
|
||||
.GetReachable(flow.EntryNodeId).Nodes.Select(node => node.InternalId));
|
||||
ClearSelection();
|
||||
foreach (var pair in nodeViews.Where(pair => ids.Contains(pair.Key))) AddToSelection(pair.Value);
|
||||
FrameSelection();
|
||||
}
|
||||
|
||||
internal void ApplyCalculatedPositions(IReadOnlyDictionary<AnimationNode, Vector2> positions)
|
||||
{
|
||||
foreach (var pair in positions)
|
||||
{
|
||||
if (nodeViews.TryGetValue(pair.Key.InternalId, out var view))
|
||||
{
|
||||
var rect = view.GetPosition();
|
||||
rect.position = pair.Value;
|
||||
view.SetPosition(rect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenClipSearch(Vector2 panelPosition)
|
||||
{
|
||||
var provider = ScriptableObject.CreateInstance<FrameAnimationClipSearchProvider>();
|
||||
provider.Configure(graph, clip => CreateNode(clip, contextPosition));
|
||||
SearchWindow.Open(new SearchWindowContext(GUIUtility.GUIToScreenPoint(panelPosition)), provider);
|
||||
}
|
||||
|
||||
private void CreateNode(FrameClip clip, Vector2 position)
|
||||
{
|
||||
if (FrameAnimationGraphMutationService.CreateNode(graph, clip, position, out var node, out var error))
|
||||
{
|
||||
GraphChanged?.Invoke();
|
||||
SelectionRequested?.Invoke(new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Node, node));
|
||||
}
|
||||
else
|
||||
{
|
||||
NotificationRequested?.Invoke(error);
|
||||
}
|
||||
}
|
||||
|
||||
private GraphViewChange OnGraphViewChanged(GraphViewChange change)
|
||||
{
|
||||
if (rebuilding || graph == null)
|
||||
{
|
||||
return change;
|
||||
}
|
||||
if (change.edgesToCreate != null)
|
||||
{
|
||||
foreach (var visual in change.edgesToCreate.ToArray())
|
||||
{
|
||||
var error = string.Empty;
|
||||
AnimationEdge data = null;
|
||||
var from = visual.output?.node as FrameAnimationClipNodeView;
|
||||
var to = visual.input?.node as FrameAnimationClipNodeView;
|
||||
if (from == null || to == null ||
|
||||
!FrameAnimationGraphMutationService.TryConnect(
|
||||
graph, from.Data, to.Data, out data, out error))
|
||||
{
|
||||
change.edgesToCreate.Remove(visual);
|
||||
NotificationRequested?.Invoke(string.IsNullOrEmpty(error) ? "无法创建连接。" : error);
|
||||
continue;
|
||||
}
|
||||
visual.userData = data;
|
||||
}
|
||||
GraphChanged?.Invoke();
|
||||
}
|
||||
if (change.elementsToRemove != null)
|
||||
{
|
||||
ProcessRemovals(change);
|
||||
}
|
||||
if (change.movedElements != null)
|
||||
{
|
||||
var positions = change.movedElements.OfType<FrameAnimationClipNodeView>()
|
||||
.ToDictionary(view => view.Data, view => view.GetPosition().position);
|
||||
if (positions.Count > 0)
|
||||
{
|
||||
FrameAnimationGraphMutationService.SetNodePositions(graph, positions,
|
||||
"Move Frame Animation Nodes");
|
||||
GraphChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
return change;
|
||||
}
|
||||
|
||||
private void ProcessRemovals(GraphViewChange change)
|
||||
{
|
||||
var nodeViewsToRemove = change.elementsToRemove.OfType<FrameAnimationClipNodeView>().ToArray();
|
||||
var removedNodeIds = new HashSet<string>(nodeViewsToRemove.Select(view => view.Data.InternalId));
|
||||
if (nodeViewsToRemove.Length > 0)
|
||||
{
|
||||
var nodes = nodeViewsToRemove.Select(view => view.Data).ToArray();
|
||||
var explicitlySelectedEdges = change.elementsToRemove.OfType<Edge>()
|
||||
.Select(edge => edge.userData as AnimationEdge)
|
||||
.Where(edge => edge != null).Distinct().ToArray();
|
||||
var entryFlows = graph.Flows.Where(flow => flow != null && removedNodeIds.Contains(flow.EntryNodeId)).ToArray();
|
||||
var impact = FrameAnimationGraphImpactAnalyzer.AnalyzeRemoval(
|
||||
graph, nodes, explicitlySelectedEdges);
|
||||
var message = BuildRemovalMessage("Node", nodes.Length, impact, entryFlows);
|
||||
if (explicitlySelectedEdges.Length > 0)
|
||||
{
|
||||
message += $"\n\n同时删除选中的 Edge:{explicitlySelectedEdges.Length}";
|
||||
}
|
||||
var confirmed = EditorUtility.DisplayDialog("Delete Frame Animation Nodes", message,
|
||||
entryFlows.Length > 0 ? "Delete Nodes + Flows" : "Delete", "Cancel");
|
||||
var removed = false;
|
||||
var error = string.Empty;
|
||||
if (confirmed)
|
||||
{
|
||||
removed = FrameAnimationGraphMutationService.RemoveNodesAndEdges(
|
||||
graph, nodes, explicitlySelectedEdges, entryFlows.Length > 0, out error);
|
||||
}
|
||||
if (!removed)
|
||||
{
|
||||
change.elementsToRemove.Clear();
|
||||
if (!string.IsNullOrEmpty(error)) NotificationRequested?.Invoke(error);
|
||||
}
|
||||
GraphChanged?.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
var remainingEdges = change.elementsToRemove.OfType<Edge>()
|
||||
.Where(edge => !IsIncidentTo(edge, removedNodeIds))
|
||||
.Select(edge => edge.userData as AnimationEdge).Where(edge => edge != null).Distinct().ToArray();
|
||||
if (remainingEdges.Length > 0)
|
||||
{
|
||||
var impact = FrameAnimationGraphImpactAnalyzer.AnalyzeEdgeRemoval(graph, remainingEdges);
|
||||
if (impact.Flows.Count > 0 && !EditorUtility.DisplayDialog("Disconnect Frame Animation Nodes",
|
||||
BuildRemovalMessage("Edge", remainingEdges.Length, impact, Array.Empty<AnimationFlow>()),
|
||||
"Disconnect", "Cancel"))
|
||||
{
|
||||
foreach (var edgeView in change.elementsToRemove.OfType<Edge>()
|
||||
.Where(view => remainingEdges.Contains(view.userData as AnimationEdge)).ToArray())
|
||||
{
|
||||
change.elementsToRemove.Remove(edgeView);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
FrameAnimationGraphMutationService.RemoveEdges(graph, remainingEdges);
|
||||
}
|
||||
}
|
||||
GraphChanged?.Invoke();
|
||||
}
|
||||
|
||||
private static bool IsIncidentTo(Edge edge, IReadOnlyCollection<string> nodeIds)
|
||||
{
|
||||
var from = edge?.output?.node as FrameAnimationClipNodeView;
|
||||
var to = edge?.input?.node as FrameAnimationClipNodeView;
|
||||
return from != null && nodeIds.Contains(from.Data.InternalId) ||
|
||||
to != null && nodeIds.Contains(to.Data.InternalId);
|
||||
}
|
||||
|
||||
private static string BuildRemovalMessage(
|
||||
string kind,
|
||||
int count,
|
||||
FrameAnimationGraphImpact impact,
|
||||
IReadOnlyCollection<AnimationFlow> entryFlows)
|
||||
{
|
||||
var message = $"将删除 {count} 个 {kind}。";
|
||||
if (impact.Flows.Count > 0)
|
||||
{
|
||||
message += $"\n\n受影响 Flow:{string.Join(", ", impact.Flows.Select(flow => flow.Id))}" +
|
||||
$"\n将失去可达关系的 Node:{impact.LostNodeCount}";
|
||||
}
|
||||
if (entryFlows.Count > 0)
|
||||
{
|
||||
message += $"\n\n入口 Flow 将同时删除:{string.Join(", ", entryFlows.Select(flow => flow.Id))}";
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
private void OnNodeSelected(FrameAnimationClipNodeView view)
|
||||
{
|
||||
SelectionRequested?.Invoke(new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Node, view.Data));
|
||||
NotifySelectionSummary();
|
||||
}
|
||||
|
||||
private void OnEdgeSelected(FrameAnimationEdgeView view)
|
||||
{
|
||||
SelectionRequested?.Invoke(new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Edge, view.Data));
|
||||
NotifySelectionSummary();
|
||||
}
|
||||
|
||||
private void NotifySelectionSummary()
|
||||
{
|
||||
MultiSelectionChanged?.Invoke(SelectedNodes);
|
||||
}
|
||||
|
||||
private void OnDragUpdated(DragUpdatedEvent evt)
|
||||
{
|
||||
if (DragAndDrop.GetGenericData(ClipDragKey) is FrameClip clip && graph?.Clips.Contains(clip) == true)
|
||||
{
|
||||
DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
|
||||
evt.StopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDragPerform(DragPerformEvent evt)
|
||||
{
|
||||
if (!(DragAndDrop.GetGenericData(ClipDragKey) is FrameClip clip) || graph?.Clips.Contains(clip) != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
DragAndDrop.AcceptDrag();
|
||||
CreateNode(clip, contentViewContainer.WorldToLocal(evt.mousePosition));
|
||||
DragAndDrop.SetGenericData(ClipDragKey, null);
|
||||
evt.StopPropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user