// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using FlaxEditor.Surface.Undo; using FlaxEngine; namespace FlaxEditor.Surface { /// /// Visject Surface visual representation context. Contains context and deserialized graph data. /// [HideInEditor] public partial class VisjectSurfaceContext { /// /// Visject context delegate type. /// /// The context. public delegate void ContextDelegate(VisjectSurfaceContext context); /// /// Visject context modification delegate type. /// /// The context. /// True if graph has been edited (nodes structure or parameter value). Otherwise just UI elements has been modified (node moved, comment resized). public delegate void ContextModifiedDelegate(VisjectSurfaceContext context, bool graphEdited); private bool _isModified; private VisjectSurface _surface; private SurfaceMeta _meta = new SurfaceMeta(); /// /// The parent context. Defines the higher key surface graph context. May be null for the top-level context. /// public readonly VisjectSurfaceContext Parent; /// /// The children of this context (loaded and opened in editor only). /// public readonly List Children = new List(); /// /// The context. /// public readonly ISurfaceContext Context; /// /// The root control for the GUI. Used to navigate around the view (scale and move it). Contains all surface controls including nodes and comments. /// public readonly SurfaceRootControl RootControl; /// /// The nodes collection. Read-only. /// public readonly List Nodes = new List(64); /// /// The collection of the surface parameters. /// public readonly List Parameters = new List(); /// /// Gets the meta for the surface context. /// public SurfaceMeta Meta => _meta; /// /// Gets the list of the surface comments. /// /// /// Don't call it too often. It does memory allocation and iterates over the surface controls to find comments in the graph. /// public List Comments { get { var result = new List(); for (int i = 0; i < RootControl.Children.Count; i++) { if (RootControl.Children[i] is SurfaceComment comment) result.Add(comment); } return result; } } /// /// Gets a value indicating whether this context is modified (needs saving and flushing with surface data context source). /// public bool IsModified => _isModified; /// /// Gets the parent Visject surface. /// public VisjectSurface Surface => _surface; /// /// The surface meta (cached after opening the context, used to store it back into the data container). /// internal VisjectSurface.Meta10 CachedSurfaceMeta; /// /// Occurs when surface starts saving graph to bytes. Can be used to inject or cleanup surface data. /// public event ContextDelegate Saving; /// /// Occurs when surface ends saving graph to bytes. Can be used to inject or cleanup surface data. /// public event ContextDelegate Saved; /// /// Occurs when surface starts loading graph from data. /// public event ContextDelegate Loading; /// /// Occurs when surface graph gets loaded from data. Can be used to post-process it or perform validation. /// public event ContextDelegate Loaded; /// /// Occurs when surface gets modified (graph edited, node moved, comment resized). /// public event ContextModifiedDelegate Modified; /// /// Occurs when node gets added to the surface as spawn operation (eg. add new comment or add new node). /// public event Action ControlSpawned; /// /// Occurs when node gets removed from the surface as delete/cut operation (eg. remove comment or cut node). /// public event Action ControlDeleted; /// /// Initializes a new instance of the class. /// /// The Visject surface using this context. /// The parent context. Defines the higher key surface graph context. May be null for the top-level context. /// The context. public VisjectSurfaceContext(VisjectSurface surface, VisjectSurfaceContext parent, ISurfaceContext context) : this(surface, parent, context, new SurfaceRootControl()) { } /// /// Initializes a new instance of the class. /// /// The Visject surface using this context. /// The parent context. Defines the higher key surface graph context. May be null for the top-level context. /// The context. /// The surface root control. public VisjectSurfaceContext(VisjectSurface surface, VisjectSurfaceContext parent, ISurfaceContext context, SurfaceRootControl rootControl) { _surface = surface; Parent = parent; Context = context ?? throw new ArgumentNullException(nameof(context)); RootControl = rootControl ?? throw new ArgumentNullException(nameof(rootControl)); // Set initial scale to provide nice zoom in effect on startup RootControl.Scale = new Float2(0.5f); } /// /// Finds the node of the given type. /// /// The group identifier. /// The type identifier. /// Found node or null if cannot. public SurfaceNode FindNode(ushort groupId, ushort typeId) { SurfaceNode result = null; uint type = ((uint)groupId << 16) | typeId; for (int i = 0; i < Nodes.Count; i++) { var node = Nodes[i]; if (node.Type == type) { result = node; break; } } return result; } /// /// Finds the node with the given ID. /// /// The identifier. /// Found node or null if cannot. public SurfaceNode FindNode(int id) { if (id < 0) return null; return FindNode((uint)id); } /// /// Finds the node with the given ID. /// /// The identifier. /// Found node or null if cannot. public SurfaceNode FindNode(uint id) { SurfaceNode result = null; for (int i = 0; i < Nodes.Count; i++) { var node = Nodes[i]; if (node.ID == id) { result = node; break; } } return result; } /// /// Gets the parameter by the given ID. /// /// The identifier. /// Found parameter instance or null if missing. public SurfaceParameter GetParameter(Guid id) { SurfaceParameter result = null; for (int i = 0; i < Parameters.Count; i++) { var parameter = Parameters[i]; if (parameter.ID == id) { result = parameter; break; } } return result; } /// /// Gets the parameter by the given name. /// /// The name. /// Found parameter instance or null if missing. public SurfaceParameter GetParameter(string name) { SurfaceParameter result = null; for (int i = 0; i < Parameters.Count; i++) { var parameter = Parameters[i]; if (parameter.Name == name) { result = parameter; break; } } return result; } private uint GetFreeNodeID() { uint result = 1; while (true) { bool valid = true; for (int i = 0; i < Nodes.Count; i++) { if (Nodes[i].ID == result) { result++; valid = false; break; } } if (valid) break; } return result; } /// /// Spawns the comment object. Used by the and loading method. Can be overriden to provide custom comment object implementations. /// /// The surface area to create comment. /// The comment title. /// The comment color. /// The comment object public virtual SurfaceComment SpawnComment(ref Rectangle surfaceArea, string title, Color color) { var values = new object[] { title, // Title color, // Color surfaceArea.Size, // Size }; return (SurfaceComment)SpawnNode(7, 11, surfaceArea.Location, values); } /// /// Creates the comment. /// /// The surface area to create comment. /// The comment title. /// The comment color. /// The comment object public SurfaceComment CreateComment(ref Rectangle surfaceArea, string title, Color color) { // Create comment var comment = SpawnComment(ref surfaceArea, title, color); if (comment == null) { Editor.LogWarning("Failed to create comment."); return null; } // Initialize OnControlLoaded(comment); comment.OnSurfaceLoaded(); OnControlSpawned(comment); MarkAsModified(); return comment; } /// /// Spawns the node. /// /// The group archetype ID. /// The node archetype ID. /// The location. /// The custom values array. Must match node archetype size. Pass null to use default values. /// The custom callback action to call after node creation but just before invoking spawn event. Can be used to initialize custom node data. /// Created node. public SurfaceNode SpawnNode(ushort groupID, ushort typeID, Float2 location, object[] customValues = null, Action beforeSpawned = null) { var nodeArchetypes = _surface?.NodeArchetypes ?? NodeFactory.DefaultGroups; if (NodeFactory.GetArchetype(nodeArchetypes, groupID, typeID, out var groupArchetype, out var nodeArchetype)) { return SpawnNode(groupArchetype, nodeArchetype, location, customValues, beforeSpawned); } return null; } /// /// Spawns the node. /// /// The group archetype. /// The node archetype. /// The location. /// The custom values array. Must match node archetype size. Pass null to use default values. /// The custom callback action to call after node creation but just before invoking spawn event. Can be used to initialize custom node data. /// Created node. public SurfaceNode SpawnNode(GroupArchetype groupArchetype, NodeArchetype nodeArchetype, Float2 location, object[] customValues = null, Action beforeSpawned = null) { if (groupArchetype == null || nodeArchetype == null) throw new ArgumentNullException(); // Check if cannot use that node in this surface type (ignore NoSpawnViaGUI) var flags = nodeArchetype.Flags; nodeArchetype.Flags &= ~NodeFlags.NoSpawnViaGUI; nodeArchetype.Flags &= ~NodeFlags.NoSpawnViaPaste; if (_surface != null && !_surface.CanUseNodeType(nodeArchetype)) { nodeArchetype.Flags = flags; Editor.LogWarning("Cannot spawn given node type. Title: " + nodeArchetype.Title); return null; } nodeArchetype.Flags = flags; var id = GetFreeNodeID(); // Create node var node = NodeFactory.CreateNode(id, this, groupArchetype, nodeArchetype); if (node == null) { Editor.LogWarning("Failed to create node."); return null; } Nodes.Add(node); // Initialize if (customValues != null) { if (node.Values != null && node.Values.Length == customValues.Length) Array.Copy(customValues, node.Values, customValues.Length); else throw new InvalidOperationException("Invalid node custom values."); } node.Location = location; OnControlLoaded(node); beforeSpawned?.Invoke(node); node.OnSurfaceLoaded(); OnControlSpawned(node); // Undo action if (Surface != null && Surface.Undo != null) Surface.Undo.AddAction(new AddRemoveNodeAction(node, true)); MarkAsModified(); return node; } /// /// Marks the context as modified and sends the event to the parent context. /// /// True if graph has been edited (nodes structure or parameter value). Otherwise just UI elements has been modified (node moved, comment resized). public void MarkAsModified(bool graphEdited = true) { _isModified = true; Modified?.Invoke(this, graphEdited); Parent?.MarkAsModified(graphEdited); } /// /// Clears the surface data. Disposed all surface nodes, comments, parameters and more. /// public void Clear() { Parameters.Clear(); Nodes.Clear(); RootControl.DisposeChildren(); } } }