// 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();
}
}
}