623 lines
27 KiB
C#
623 lines
27 KiB
C#
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();
|
||
}
|
||
}
|
||
}
|