Skip to content

Commit 222d19d

Browse files
Merge pull request #91 from PaulNonatomic/main
Develop
2 parents de039f1 + bbd1399 commit 222d19d

15 files changed

Lines changed: 378 additions & 21 deletions

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/Persistence/CopiedData.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using Nonatomic.VSM2.StateGraph;
4+
using UnityEngine;
5+
6+
namespace Nonatomic.VSM2.Editor.Persistence
7+
{
8+
[Serializable]
9+
public class CopiedData
10+
{
11+
public List<StateNodeModel> SelectedNodes;
12+
public List<StateTransitionModel> SelectedTransitions;
13+
14+
public CopiedData(List<StateNodeModel> selectedNodes, List<StateTransitionModel> selectedTransitions)
15+
{
16+
SelectedNodes = selectedNodes;
17+
SelectedTransitions = selectedTransitions;
18+
}
19+
20+
public string Serialize()
21+
{
22+
return JsonUtility.ToJson(this);
23+
}
24+
}
25+
}

Editor/Persistence/CopiedData.cs.meta

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
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;
13+
14+
namespace Nonatomic.VSM2.Editor.StateGraph
15+
{
16+
public static class CopyPasteHelper
17+
{
18+
public static CopiedData LastCopy => _lastCopy;
19+
private static CopiedData _lastCopy;
20+
21+
public static void CacheCopiedData(CopiedData copy)
22+
{
23+
_lastCopy = copy;
24+
}
25+
26+
public static void ClearCopyCache()
27+
{
28+
_lastCopy = null;
29+
}
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+
}
161+
}
162+
}

Editor/StateGraph/CopyPasteHelper.cs.meta

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Editor/StateGraph/Factories/StateGraphNodeFactory.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,19 @@ public static StateNodeModel MakeStateNodeData(StateMachineModel model,
4141
{
4242
var state = ScriptableObject.CreateInstance(stateType) as State;
4343
if (!state) return null;
44-
45-
state.name = $"{stateType.Name}-{GUID.Generate()}";
44+
45+
state.name = GenerateStateName(stateType);
4646
var stateNode = new StateNodeModel(state, position);
4747
model.AddState(stateNode);
4848

4949
return stateNode;
5050
}
5151

52+
public static string GenerateStateName(Type stateType)
53+
{
54+
return $"{stateType.Name}-{GUID.Generate()}";
55+
}
56+
5257
private static Type GetViewTypeByStateType(Type stateType)
5358
{
5459
return _stateTypeToNodeViewType.TryGetValue(stateType, out var value)

Editor/StateGraph/Factories/StateGraphTransitionFactory.cs

Lines changed: 33 additions & 6 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,22 +51,39 @@ 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
{
6783
input = inputPort,
6884
output = outputPort,
69-
userData = transitionModel
85+
userData = transitionModel,
86+
name = GenerateEdgeName(inputPort, outputPort)
7087
};
7188

7289
var position = edge.GetPosition();
@@ -79,5 +96,15 @@ public static StateNodeEdge MakeTransitionView(GraphView graphView, StateTransit
7996

8097
return edge;
8198
}
99+
100+
public static string GenerateEdgeName(TransitionModel model)
101+
{
102+
return $"Edge-{model.OriginNodeId}-{model.DestinationNodeId}";
103+
}
104+
105+
public static string GenerateEdgeName(Port inputPort, Port outputPort)
106+
{
107+
return $"Edge-{inputPort.node.name}-{outputPort.node.name}";
108+
}
82109
}
83110
}

Editor/StateGraph/StateGraphContextMenu.cs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,17 @@ public class StateGraphContextMenu
1010
public event Action<Vector2> OnCreateNewStateNode;
1111
public event Action<Vector2> OnCreateNewStickyNote;
1212
public event Action<NodeView> OnDeleteStateNode;
13+
public event Action OnDeleteSelection;
14+
public event Action OnCopySelected;
15+
public event Action OnPasteSelected;
1316
public event Action<StateNodeEdge> OnDeleteEdgeContext;
17+
18+
private readonly NodeGraphView _graphView;
1419

1520
public StateGraphContextMenu(NodeGraphView graphView)
1621
{
17-
graphView.RegisterCallback<ContextualMenuPopulateEvent>(BuildContextMenu);
22+
_graphView = graphView;
23+
_graphView.RegisterCallback<ContextualMenuPopulateEvent>(BuildContextMenu);
1824
}
1925

2026
private void BuildContextMenu(ContextualMenuPopulateEvent evt)
@@ -39,6 +45,18 @@ private void BuildNodeContext(ContextualMenuPopulateEvent evt, NodeView nodeView
3945
{
4046
evt.menu.AppendAction("Delete", action
4147
=> OnDeleteStateNode?.Invoke(nodeView));
48+
49+
if (_graphView.selection.Count > 0)
50+
{
51+
evt.menu.AppendAction("Copy", action
52+
=> OnCopySelected?.Invoke());
53+
}
54+
55+
if (CopyPasteHelper.LastCopy != null)
56+
{
57+
evt.menu.AppendAction("Paste", action
58+
=> OnPasteSelected?.Invoke());
59+
}
4260
}
4361

4462
private void BuildStateEdgeContext(ContextualMenuPopulateEvent evt, StateNodeEdge edge)
@@ -54,6 +72,21 @@ private void BuildStateGraphContext(ContextualMenuPopulateEvent evt)
5472

5573
evt.menu.AppendAction("Add Sticky Note", action
5674
=> OnCreateNewStickyNote?.Invoke(action.eventInfo.mousePosition));
75+
76+
if (_graphView.selection.Count > 0)
77+
{
78+
evt.menu.AppendAction("Copy", action
79+
=> OnCopySelected?.Invoke());
80+
81+
evt.menu.AppendAction("Delete", action
82+
=> OnDeleteSelection?.Invoke());
83+
}
84+
85+
if (CopyPasteHelper.LastCopy != null)
86+
{
87+
evt.menu.AppendAction("Paste", action
88+
=> OnPasteSelected?.Invoke());
89+
}
5790
}
5891
}
5992
}

Editor/StateGraph/StateNodeGraphStateManager.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Linq;
22
using Nonatomic.VSM2.Editor.NodeGraph;
3+
using Nonatomic.VSM2.Editor.Persistence;
34
using Nonatomic.VSM2.StateGraph;
45
using UnityEditor;
56
using UnityEngine;
@@ -16,7 +17,7 @@ public StateNodeGraphStateManager(string id) : base(id)
1617
{
1718

1819
}
19-
20+
2021
public void LoadModelFromStateController()
2122
{
2223
if (string.IsNullOrEmpty(StateControllerId)) return;

0 commit comments

Comments
 (0)