455 lines
17 KiB
C#
455 lines
17 KiB
C#
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using FlaxEditor.Scripting;
|
|
using FlaxEditor.Surface.Elements;
|
|
using FlaxEditor.Surface.Undo;
|
|
using FlaxEngine;
|
|
using FlaxEngine.Utilities;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
|
|
// ReSharper disable ClassNeverInstantiated.Local
|
|
#pragma warning disable 649
|
|
|
|
namespace FlaxEditor.Surface
|
|
{
|
|
public partial class VisjectSurface
|
|
{
|
|
private struct DataModelValue
|
|
{
|
|
public string EnumTypeName;
|
|
public object Value;
|
|
}
|
|
|
|
private class DataModelBox
|
|
{
|
|
public int ID;
|
|
public uint[] NodeIDs;
|
|
public int[] BoxIDs;
|
|
}
|
|
|
|
private class DataModelNode
|
|
{
|
|
public ushort GroupID;
|
|
public ushort TypeID;
|
|
public uint ID;
|
|
public float X;
|
|
public float Y;
|
|
public DataModelValue[] Values;
|
|
public DataModelBox[] Boxes;
|
|
}
|
|
|
|
private class DataModelComment
|
|
{
|
|
public string Title;
|
|
public Color Color;
|
|
public Rectangle Bounds;
|
|
}
|
|
|
|
private class DataModel
|
|
{
|
|
public DataModelNode[] Nodes;
|
|
public DataModelComment[] Comments;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Copies the selected items.
|
|
/// </summary>
|
|
public void Copy()
|
|
{
|
|
var selection = SelectedControls;
|
|
if (selection.Count == 0)
|
|
{
|
|
Clipboard.Text = string.Empty;
|
|
return;
|
|
}
|
|
|
|
var dataModel = new DataModel();
|
|
var dataModelNodes = new List<DataModelNode>(selection.Count);
|
|
var dataModelComments = new List<DataModelComment>();
|
|
var dataModelBoxes = new List<DataModelBox>(32);
|
|
|
|
for (int i = 0; i < selection.Count; i++)
|
|
{
|
|
var node = selection[i] as SurfaceNode;
|
|
if (node == null)
|
|
continue;
|
|
|
|
var dataModelNode = new DataModelNode
|
|
{
|
|
GroupID = node.GroupArchetype.GroupID,
|
|
TypeID = node.Archetype.TypeID,
|
|
ID = node.ID,
|
|
X = node.Location.X,
|
|
Y = node.Location.Y,
|
|
};
|
|
if (node.Values != null)
|
|
{
|
|
dataModelNode.Values = new DataModelValue[node.Values.Length];
|
|
for (int j = 0; j < node.Values.Length; j++)
|
|
{
|
|
var value = new DataModelValue
|
|
{
|
|
Value = node.Values[j],
|
|
};
|
|
if (value.Value != null && value.Value.GetType().IsEnum)
|
|
value.EnumTypeName = value.Value.GetType().FullName;
|
|
dataModelNode.Values[j] = value;
|
|
}
|
|
}
|
|
|
|
if (node.Elements != null && node.Elements.Count > 0)
|
|
{
|
|
for (int j = 0; j < node.Elements.Count; j++)
|
|
{
|
|
if (node.Elements[j] is Box box && box.Connections.Count > 0)
|
|
{
|
|
var dataModelBox = new DataModelBox
|
|
{
|
|
ID = box.ID,
|
|
NodeIDs = new uint[box.Connections.Count],
|
|
BoxIDs = new int[box.Connections.Count],
|
|
};
|
|
|
|
for (int k = 0; k < box.Connections.Count; k++)
|
|
{
|
|
var target = box.Connections[k];
|
|
dataModelBox.NodeIDs[k] = target.ParentNode.ID;
|
|
}
|
|
|
|
for (int k = 0; k < box.Connections.Count; k++)
|
|
{
|
|
var target = box.Connections[k];
|
|
dataModelBox.BoxIDs[k] = target.ID;
|
|
}
|
|
|
|
dataModelBoxes.Add(dataModelBox);
|
|
}
|
|
}
|
|
|
|
if (dataModelBoxes.Count > 0)
|
|
{
|
|
dataModelNode.Boxes = dataModelBoxes.ToArray();
|
|
dataModelBoxes.Clear();
|
|
}
|
|
}
|
|
|
|
dataModelNodes.Add(dataModelNode);
|
|
}
|
|
|
|
dataModel.Nodes = dataModelNodes.ToArray();
|
|
|
|
for (int i = 0; i < selection.Count; i++)
|
|
{
|
|
var comment = selection[i] as SurfaceComment;
|
|
if (comment == null)
|
|
continue;
|
|
|
|
var dataModelComment = new DataModelComment
|
|
{
|
|
Title = comment.Title,
|
|
Color = comment.Color,
|
|
Bounds = comment.Bounds,
|
|
};
|
|
|
|
dataModelComments.Add(dataModelComment);
|
|
}
|
|
|
|
dataModel.Comments = dataModelComments.ToArray();
|
|
|
|
Clipboard.Text = FlaxEngine.Json.JsonSerializer.Serialize(dataModel);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if can paste the nodes data from the clipboard.
|
|
/// </summary>
|
|
/// <returns>True if can paste data, otherwise false.</returns>
|
|
public bool CanPaste()
|
|
{
|
|
var data = Clipboard.Text;
|
|
if (data == null || data.Length < 2)
|
|
return false;
|
|
|
|
try
|
|
{
|
|
var model = JsonConvert.DeserializeObject<DataModel>(data);
|
|
return model?.Nodes != null && model.Nodes.Length != 0;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pastes the copied items.
|
|
/// </summary>
|
|
public void Paste()
|
|
{
|
|
if (!CanEdit)
|
|
return;
|
|
var data = Clipboard.Text;
|
|
if (data == null || data.Length < 2)
|
|
return;
|
|
|
|
try
|
|
{
|
|
// Load Mr Json
|
|
var model = FlaxEngine.Json.JsonSerializer.Deserialize<DataModel>(data);
|
|
if (model.Nodes == null)
|
|
model.Nodes = new DataModelNode[0];
|
|
|
|
// Build the nodes IDs mapping (need to generate new IDs for the pasted nodes and preserve the internal connections)
|
|
var idsMapping = new Dictionary<uint, uint>();
|
|
for (int i = 0; i < model.Nodes.Length; i++)
|
|
{
|
|
uint result = 1;
|
|
while (true)
|
|
{
|
|
bool valid = true;
|
|
if (idsMapping.ContainsValue(result))
|
|
{
|
|
result++;
|
|
valid = false;
|
|
}
|
|
else
|
|
{
|
|
for (int j = 0; j < Nodes.Count; j++)
|
|
{
|
|
if (Nodes[j].ID == result)
|
|
{
|
|
result++;
|
|
valid = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (valid)
|
|
break;
|
|
}
|
|
|
|
idsMapping.Add(model.Nodes[i].ID, result);
|
|
}
|
|
|
|
// Find controls upper left location
|
|
var upperLeft = new Float2(model.Nodes[0].X, model.Nodes[0].Y);
|
|
for (int i = 1; i < model.Nodes.Length; i++)
|
|
{
|
|
upperLeft.X = Mathf.Min(upperLeft.X, model.Nodes[i].X);
|
|
upperLeft.Y = Mathf.Min(upperLeft.Y, model.Nodes[i].Y);
|
|
}
|
|
|
|
// Create nodes
|
|
var nodes = new Dictionary<uint, SurfaceNode>();
|
|
var nodesData = new Dictionary<uint, DataModelNode>();
|
|
for (int i = 0; i < model.Nodes.Length; i++)
|
|
{
|
|
var nodeData = model.Nodes[i];
|
|
|
|
// Peek type
|
|
if (!NodeFactory.GetArchetype(NodeArchetypes, nodeData.GroupID, nodeData.TypeID, out var groupArchetype, out var nodeArchetype))
|
|
throw new InvalidOperationException("Unknown node type.");
|
|
|
|
// Validate given node type
|
|
if (!CanUseNodeType(groupArchetype, nodeArchetype))
|
|
continue;
|
|
|
|
// Create
|
|
var node = NodeFactory.CreateNode(idsMapping[nodeData.ID], Context, groupArchetype, nodeArchetype);
|
|
if (node == null)
|
|
throw new InvalidOperationException("Failed to create node.");
|
|
Nodes.Add(node);
|
|
nodes.Add(nodeData.ID, node);
|
|
nodesData.Add(nodeData.ID, nodeData);
|
|
|
|
// Initialize
|
|
if (nodeData.Values != null && node.Values.Length > 0)
|
|
{
|
|
if (node.Values != null && node.Values.Length == nodeData.Values.Length)
|
|
{
|
|
// Copy and fix values (Json deserializes may output them in a different format)
|
|
for (int l = 0; l < node.Values.Length; l++)
|
|
{
|
|
var src = nodeData.Values[l].Value;
|
|
var dst = node.Values[l];
|
|
|
|
try
|
|
{
|
|
if (nodeData.Values[l].EnumTypeName != null)
|
|
{
|
|
var enumType = TypeUtils.GetManagedType(nodeData.Values[l].EnumTypeName);
|
|
if (enumType != null)
|
|
{
|
|
src = Enum.ToObject(enumType, src);
|
|
}
|
|
}
|
|
else if (src is JToken token)
|
|
{
|
|
if (dst is Vector2)
|
|
{
|
|
src = new Vector2(token["X"].Value<float>(),
|
|
token["Y"].Value<float>());
|
|
}
|
|
else if (dst is Vector3)
|
|
{
|
|
src = new Vector3(token["X"].Value<float>(),
|
|
token["Y"].Value<float>(),
|
|
token["Z"].Value<float>());
|
|
}
|
|
else if (dst is Vector4)
|
|
{
|
|
src = new Vector4(token["X"].Value<float>(),
|
|
token["Y"].Value<float>(),
|
|
token["Z"].Value<float>(),
|
|
token["W"].Value<float>());
|
|
}
|
|
else if (dst is Color)
|
|
{
|
|
src = new Color(token["R"].Value<float>(),
|
|
token["G"].Value<float>(),
|
|
token["B"].Value<float>(),
|
|
token["A"].Value<float>());
|
|
}
|
|
else
|
|
{
|
|
Editor.LogWarning("Unknown pasted node value token: " + token);
|
|
src = dst;
|
|
}
|
|
}
|
|
else if (src is double asDouble)
|
|
{
|
|
src = (float)asDouble;
|
|
}
|
|
else if (dst is Guid)
|
|
{
|
|
src = Guid.Parse((string)src);
|
|
}
|
|
else if (dst is int)
|
|
{
|
|
src = Convert.ToInt32(src);
|
|
}
|
|
else if (dst is float)
|
|
{
|
|
src = Convert.ToSingle(src);
|
|
}
|
|
else if (dst is byte[] && src is string s)
|
|
{
|
|
src = Convert.FromBase64String(s);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Editor.LogWarning(string.Format("Failed to convert node data from {0} to {1}", src?.GetType().FullName ?? "null", dst?.GetType().FullName ?? "null"));
|
|
Editor.LogWarning(ex);
|
|
}
|
|
|
|
node.Values[l] = src;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Editor.LogWarning("Invalid node custom values.");
|
|
}
|
|
}
|
|
|
|
Context.OnControlLoaded(node);
|
|
}
|
|
|
|
// Setup connections
|
|
foreach (var e in nodes)
|
|
{
|
|
var node = e.Value;
|
|
var nodeData = nodesData[e.Key];
|
|
if (nodeData.Boxes != null)
|
|
{
|
|
foreach (var boxData in nodeData.Boxes)
|
|
{
|
|
var box = node.GetBox(boxData.ID);
|
|
if (box == null || boxData.BoxIDs == null || boxData.NodeIDs == null || boxData.BoxIDs.Length != boxData.NodeIDs.Length)
|
|
continue;
|
|
|
|
for (int i = 0; i < boxData.NodeIDs.Length; i++)
|
|
{
|
|
if (nodes.TryGetValue(boxData.NodeIDs[i], out var targetNode)
|
|
&& targetNode.TryGetBox(boxData.BoxIDs[i], out var targetBox))
|
|
{
|
|
box.Connections.Add(targetBox);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Arrange controls
|
|
foreach (var e in nodes)
|
|
{
|
|
var node = e.Value;
|
|
var nodeData = nodesData[e.Key];
|
|
var pos = new Float2(nodeData.X, nodeData.Y) - upperLeft;
|
|
node.Location = ViewPosition + pos + _mousePos / ViewScale;
|
|
}
|
|
|
|
// Post load
|
|
foreach (var node in nodes)
|
|
{
|
|
node.Value.OnSurfaceLoaded();
|
|
}
|
|
foreach (var node in nodes)
|
|
{
|
|
node.Value.OnSpawned();
|
|
}
|
|
|
|
// Add undo action
|
|
if (Undo != null && nodes.Count > 0)
|
|
{
|
|
var actions = new List<IUndoAction>();
|
|
foreach (var node in nodes)
|
|
{
|
|
var action = new AddRemoveNodeAction(node.Value, true);
|
|
actions.Add(action);
|
|
}
|
|
foreach (var node in nodes)
|
|
{
|
|
var action = new EditNodeConnections(Context, node.Value);
|
|
action.End();
|
|
actions.Add(action);
|
|
}
|
|
Undo.AddAction(new MultiUndoAction(actions, nodes.Count == 1 ? "Paste node" : "Paste nodes"));
|
|
}
|
|
|
|
// Select those nodes
|
|
Select(nodes.Values);
|
|
|
|
MarkAsEdited();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Editor.LogWarning("Failed to paste Visject Surface nodes");
|
|
Editor.LogWarning(ex);
|
|
MessageBox.Show("Failed to paste Visject Surface nodes. " + ex.Message, "Paste failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cuts the selected items.
|
|
/// </summary>
|
|
public void Cut()
|
|
{
|
|
Copy();
|
|
Delete();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Duplicates the selected items.
|
|
/// </summary>
|
|
public void Duplicate()
|
|
{
|
|
Copy();
|
|
Paste();
|
|
}
|
|
}
|
|
}
|