Skip to content

Commit 0c87027

Browse files
committed
Tidied up and simplified the copy and paste process.
1 parent c7adda8 commit 0c87027

6 files changed

Lines changed: 173 additions & 99 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Change Log
22

3+
## [0.6.6-beta] - Aug 18, 2024
4+
- Added support for copy & paste
5+
36
## [0.6.5-beta] - Aug 16, 2024
47
- Updated frame delay of DelayState transition to 0
58
- Fix for add nodes multiple times when entering runtime

Editor/NodeGraph/NodeGraphView.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ namespace Nonatomic.VSM2.Editor.NodeGraph
1313
public class NodeGraphView : GraphView
1414
{
1515
public event Action<Vector2> OnGridPositionChanged;
16-
17-
protected NodeGraphStateManager StateManager;
16+
public NodeGraphStateManager StateManager;
17+
1818
protected Vector2 MousePosition;
1919

2020
public NodeGraphView(string id)

Editor/StateGraph/CopyPasteHelper.cs

Lines changed: 143 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
1-
using Nonatomic.VSM2.Editor.Persistence;
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using Nonatomic.VSM2.Editor.NodeGraph;
5+
using Nonatomic.VSM2.Editor.Persistence;
6+
using Nonatomic.VSM2.Editor.StateGraph.Nodes;
7+
using Nonatomic.VSM2.NodeGraph;
8+
using Nonatomic.VSM2.StateGraph;
9+
using Nonatomic.VSM2.StateGraph.States;
10+
using UnityEditor;
11+
using UnityEngine;
12+
using UnityEngine.UIElements;
213

314
namespace Nonatomic.VSM2.Editor.StateGraph
415
{
@@ -16,5 +27,136 @@ public static void ClearCopyCache()
1627
{
1728
_lastCopy = null;
1829
}
30+
31+
public static void Copy(StateGraphView graphView)
32+
{
33+
var selectedNodeModels = graphView.selection
34+
.OfType<BaseStateNodeView>()
35+
.Select(node => node.NodeModel)
36+
.ToList();
37+
38+
var selectedTransitions = graphView.selection
39+
.OfType<StateNodeEdge>()
40+
.Select(edge => (StateTransitionModel) edge.userData)
41+
.ToList();
42+
43+
var copy = new CopiedData(selectedNodeModels, selectedTransitions);
44+
CacheCopiedData(copy);
45+
}
46+
47+
public static void Paste(StateGraphView graphView)
48+
{
49+
if (LastCopy == null) return;
50+
51+
var model = graphView.StateManager.Model as StateMachineModel;
52+
if (!model) return;
53+
54+
var clonedNodes = LastCopy.SelectedNodes.Select(node => node.Clone()).ToList();
55+
var clonedTransition = LastCopy.SelectedTransitions.Select(trans => trans.Clone()).ToList();
56+
57+
RenameClonedNodes(model, clonedNodes);
58+
OffsetNodes(clonedNodes, new Vector2(50, 50));
59+
60+
//Remap transition nodes since their GUIDs have changed
61+
RemapTransitionNodes(graphView, model, LastCopy, clonedTransition, clonedNodes);
62+
63+
graphView.PopulateGraph(model);
64+
65+
//Wait a frame for the nodes to be created then select them
66+
EditorApplication.delayCall += ()=> SelectNodes(graphView, clonedNodes);
67+
}
68+
69+
private static void SelectNodes(StateGraphView graphView, List<StateNodeModel> nodes)
70+
{
71+
graphView.selection.Clear();
72+
73+
foreach (var node in nodes)
74+
{
75+
var originNode = graphView.contentViewContainer.Q<NodeView>(node.Id);
76+
originNode?.Select(graphView, true);
77+
}
78+
}
79+
80+
private static void RenameClonedNodes(StateMachineModel model, List<StateNodeModel> clonedNodes)
81+
{
82+
foreach (var node in clonedNodes)
83+
{
84+
//Prevent copying entry nodes
85+
if (node.State is EntryState) continue;
86+
87+
//Provide new GUIDs for the pasted node models
88+
node.Id = node.State.name = StateGraphNodeFactory.GenerateStateName(node.State.GetType());
89+
90+
model.AddState(node);
91+
}
92+
}
93+
94+
private static void OffsetNodes(List<StateNodeModel> nodes, Vector2 offset)
95+
{
96+
foreach (var node in nodes)
97+
{
98+
node.Position += offset;
99+
}
100+
}
101+
102+
private static void RemapTransitionNodes(StateGraphView graphView, StateMachineModel model, CopiedData copy, List<StateTransitionModel> clonedTransition, List<StateNodeModel> clonedNodes)
103+
{
104+
foreach (var transition in clonedTransition)
105+
{
106+
UpdateNode(graphView, copy, transition, clonedNodes, true);
107+
UpdateNode(graphView, copy, transition, clonedNodes, false);
108+
109+
if (transition.OriginNodeId != null && transition.DestinationNodeId != null)
110+
{
111+
model.AddTransition(transition);
112+
}
113+
}
114+
}
115+
116+
private static void UpdateNode(StateGraphView graphView, CopiedData copy, TransitionModel transition, List<StateNodeModel> clonedNodes, bool updateOrigin)
117+
{
118+
// Determine the node type to update based on the parameter
119+
var nodeId = updateOrigin
120+
? transition.OriginNodeId
121+
: transition.DestinationNodeId;
122+
123+
Func<StateNodeModel, List<PortModel>> portSelector = updateOrigin
124+
? node => node.OutputPorts
125+
: node => node.InputPorts;
126+
127+
var nodeIndex = copy.SelectedNodes.FindIndex(node => node.Id.Equals(nodeId));
128+
if (nodeIndex > -1)
129+
{
130+
// Node was found in copied nodes, update from cloned nodes
131+
var clonedNode = clonedNodes[nodeIndex];
132+
if (updateOrigin)
133+
{
134+
transition.OriginNodeId = clonedNode.Id;
135+
transition.OriginPort = clonedNode.OutputPorts.FirstOrDefault(port => port.Id.Equals(transition.OriginPort.Id));
136+
}
137+
else
138+
{
139+
transition.DestinationNodeId = clonedNode.Id;
140+
transition.DestinationPort = clonedNode.InputPorts.FirstOrDefault(port => port.Id.Equals(transition.DestinationPort.Id));
141+
}
142+
}
143+
else
144+
{
145+
// Node was not found in copied nodes, find in existing nodes
146+
var existingNodeView = graphView.contentViewContainer.Q<BaseStateNodeView>(nodeId);
147+
if (existingNodeView == null) return;
148+
149+
if (updateOrigin)
150+
{
151+
transition.OriginNodeId = existingNodeView.NodeModel.Id;
152+
transition.OriginPort = existingNodeView.NodeModel.OutputPorts.FirstOrDefault(port => port.Id.Equals(transition.OriginPort.Id));
153+
}
154+
else
155+
{
156+
transition.DestinationNodeId = existingNodeView.NodeModel.Id;
157+
transition.DestinationPort = existingNodeView.NodeModel.InputPorts.FirstOrDefault(port => port.Id.Equals(transition.DestinationPort.Id));
158+
}
159+
}
160+
}
19161
}
20162
}

Editor/StateGraph/Factories/StateGraphTransitionFactory.cs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public static void MakeTransition(GraphView graphView,
3434
destinationNodeId,
3535
destinationPortModel);
3636

37-
var transitionView = StateGraphTransitionFactory.MakeTransitionView(graphView, transitionData);
37+
StateGraphTransitionFactory.MakeTransitionView(graphView, transitionData);
3838
}
3939

4040
public static StateTransitionModel MakeTransitionData(Edge edge)
@@ -51,16 +51,32 @@ public static StateTransitionModel MakeTransitionData(Edge edge)
5151
public static StateNodeEdge MakeTransitionView(GraphView graphView, StateTransitionModel transitionModel)
5252
{
5353
var originNode = graphView.contentViewContainer.Q<NodeView>(transitionModel.OriginNodeId);
54-
if(originNode == null) throw new Exception("Failed to create edge because of missing origin node");
54+
if (originNode == null)
55+
{
56+
Debug.LogWarning($"Failed to create transition because of missing origin node with id:{transitionModel.OriginNodeId}");
57+
return null;
58+
}
5559

5660
var destinationNode = graphView.contentViewContainer.Q<NodeView>(transitionModel.DestinationNodeId);
57-
if(destinationNode == null) throw new Exception("Failed to create edge because of missing destination node");
61+
if (destinationNode == null)
62+
{
63+
Debug.LogWarning($"Failed to create transition because of missing destination node with id:{transitionModel.DestinationNodeId}");
64+
return null;
65+
}
5866

5967
var inputPort = destinationNode.Q<Port>(transitionModel.DestinationPort.Id, "port", "input");
60-
if(inputPort == null) throw new Exception("Failed to create edge because of missing input port");
68+
if (inputPort == null)
69+
{
70+
Debug.LogWarning($"Failed to create transition because of missing input port with id:{transitionModel.DestinationPort.Id}");
71+
return null;
72+
}
6173

6274
var outputPort = originNode.Q<Port>(transitionModel.OriginPort.Id, "port", "output");
63-
if(outputPort == null) throw new Exception("Failed to create edge because of missing output port");
75+
if (outputPort == null)
76+
{
77+
Debug.LogWarning($"Failed to create transition because of missing output port with id:{transitionModel.OriginPort.Id}");
78+
return null;
79+
}
6480

6581
var edge = new StateNodeEdge()
6682
{

Editor/StateGraph/VisualElements/StateGraphView.cs

Lines changed: 3 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -242,101 +242,14 @@ private void HandleGridPositionChanged(Vector2 position)
242242

243243
private void HandleCopySelected()
244244
{
245-
var selectedNodeModels = selection
246-
.OfType<BaseStateNodeView>()
247-
.Select(node => node.NodeModel)
248-
.ToList();
249-
250-
var selectedTransitions = selection
251-
.OfType<StateNodeEdge>()
252-
.Select(edge => (StateTransitionModel) edge.userData)
253-
.ToList();
254-
255-
var copy = new CopiedData(selectedNodeModels, selectedTransitions);
256-
CopyPasteHelper.CacheCopiedData(copy);
245+
if (GuardUtils.GuardAgainstRuntimeOperation()) return;
246+
CopyPasteHelper.Copy(this);
257247
}
258248

259249
private void HandlePasteSelected()
260250
{
261251
if (GuardUtils.GuardAgainstRuntimeOperation()) return;
262-
if (CopyPasteHelper.LastCopy == null) return;
263-
264-
var model = StateManager.Model as StateMachineModel;
265-
if (!model) return;
266-
267-
var copy = CopyPasteHelper.LastCopy;
268-
var clonedNodes = copy.SelectedNodes.Select(node => node.Clone()).ToList();
269-
var clonedTransition = copy.SelectedTransitions.Select(trans => trans.Clone()).ToList();
270-
271-
//rename node states
272-
foreach (var node in clonedNodes)
273-
{
274-
//prevent copying entry nodes
275-
if (node.State is EntryState) continue;
276-
277-
//Provide new GUIDs for the pasted node models
278-
node.Id = node.State.name = StateGraphNodeFactory.GenerateStateName(node.State.GetType());
279-
280-
//Offset node positions so the pasted nodes don't overlap
281-
node.Position.x += 50;
282-
node.Position.y += 50;
283-
284-
//Save nodes
285-
model.AddState(node);
286-
}
287-
288-
//remap transitions
289-
foreach (var transition in clonedTransition)
290-
{
291-
var originNodeIndex = copy.SelectedNodes.FindIndex(node => node.Id.Equals(transition.OriginNodeId));
292-
if (originNodeIndex > -1)
293-
{
294-
var clonedOriginNode = clonedNodes[originNodeIndex];
295-
transition.OriginNodeId = clonedOriginNode.Id;
296-
transition.OriginPort = clonedOriginNode.OutputPorts.FirstOrDefault(port => port.Id.Equals(transition.OriginPort.Id));
297-
}
298-
else
299-
{
300-
var existingOriginNode = contentViewContainer.Q<BaseStateNodeView>(transition.OriginNodeId);
301-
if (existingOriginNode == null) continue;
302-
303-
transition.OriginNodeId = existingOriginNode.NodeModel.Id;
304-
transition.OriginPort = existingOriginNode.NodeModel.OutputPorts.FirstOrDefault(port => port.Id.Equals(transition.OriginPort.Id));
305-
}
306-
307-
var destinationNodeIndex = copy.SelectedNodes.FindIndex(node => node.Id.Equals(transition.DestinationNodeId));
308-
if (destinationNodeIndex > -1)
309-
{
310-
var clonedDestinationNode = clonedNodes[destinationNodeIndex];
311-
transition.DestinationNodeId = clonedDestinationNode.Id;
312-
transition.DestinationPort = clonedDestinationNode.InputPorts.FirstOrDefault(port => port.Id.Equals(transition.DestinationPort.Id));
313-
}
314-
else
315-
{
316-
var existingDestinationNode = contentViewContainer.Q<BaseStateNodeView>(transition.DestinationNodeId);
317-
if (existingDestinationNode == null) continue;
318-
319-
transition.OriginNodeId = existingDestinationNode.NodeModel.Id;
320-
transition.OriginPort = existingDestinationNode.NodeModel.OutputPorts.FirstOrDefault(port => port.Id.Equals(transition.DestinationPort.Id));
321-
}
322-
323-
//Save transition
324-
model.AddTransition(transition);
325-
}
326-
327-
PopulateGraph(model);
328-
329-
//Select the pasted nodes
330-
EditorApplication.delayCall += () =>
331-
{
332-
selection.Clear();
333-
334-
foreach (var node in clonedNodes)
335-
{
336-
var originNode = contentViewContainer.Q<NodeView>(node.Id);
337-
originNode?.Select(this, true);
338-
}
339-
};
252+
CopyPasteHelper.Paste(this);
340253
}
341254

342255
private void HandleDeleteSelection()

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "com.nonatomic.visualstatemachinev2",
3-
"version": "0.6.4-beta",
3+
"version": "0.6.5-beta",
44
"displayName": "Visual State Machine V2",
55
"description": "Visual State Machine V2",
66
"unity": "2022.3",

0 commit comments

Comments
 (0)