// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; 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; } /// /// Copies the selected items. /// public void Copy() { var selection = SelectedControls; if (selection.Count == 0) { Clipboard.Text = string.Empty; return; } // Collect sealed nodes to be copied as well foreach (var control in selection.ToArray()) { if (control is SurfaceNode node) { var sealedNodes = node.SealedNodes; if (sealedNodes != null) { foreach (var sealedNode in sealedNodes) { if (sealedNode != null && !selection.Contains(sealedNode)) { selection.Add(sealedNode); } } } } } var dataModel = new DataModel(); var dataModelNodes = new List(selection.Count); var dataModelComments = new List(); var dataModelBoxes = new List(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); } /// /// Checks if can paste the nodes data from the clipboard. /// /// True if can paste data, otherwise false. public bool CanPaste() { var data = Clipboard.Text; if (data == null || data.Length < 2) return false; return true; } /// /// Pastes the copied items. /// public void Paste() { if (!CanEdit) return; var data = Clipboard.Text; if (data == null || data.Length < 2) return; try { // Load Mr Json DataModel model; try { model = FlaxEngine.Json.JsonSerializer.Deserialize(data); } catch { return; } 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(); 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(); var nodesData = new Dictionary(); 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(), token["Y"].Value()); } else if (dst is Vector3) { src = new Vector3(token["X"].Value(), token["Y"].Value(), token["Z"].Value()); } else if (dst is Vector4) { src = new Vector4(token["X"].Value(), token["Y"].Value(), token["Z"].Value(), token["W"].Value()); } else if (dst is Color) { src = new Color(token["R"].Value(), token["G"].Value(), token["B"].Value(), token["A"].Value()); } 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, SurfaceNodeActions.Paste); } // 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(SurfaceNodeActions.Paste); } foreach (var node in nodes) { node.Value.OnSpawned(SurfaceNodeActions.Paste); } foreach (var node in nodes) { node.Value.OnPasted(idsMapping); } // Add undo action if (Undo != null && nodes.Count > 0) { var actions = new List(); 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); } } /// /// Cuts the selected items. /// public void Cut() { Copy(); Delete(); } /// /// Duplicates the selected items. /// public void Duplicate() { Copy(); Paste(); } } }