Index
Introduction
I am interested in video games. As a result, I tried to dig into the field of game development, driven by my curiosity. With that being said, it was inevitable for me not to notice the charms of the visual scripting tools that have been used by developers to make impressive games.
Personally, I have used multiple visual scripting environments, and I have admired how they can make the process easier sometimes.
Due to this ambition, I dedicate a year of my life to write my own visual scripting environment. It helped me generate relevant data science, Machine Learning and Artificial intelligence, related programs.
Few Notes
Even though the intention behind the creation of this project was to make a tool that can create tools for DataScience, this article won't be discussing the aspects of how this project relates to data science. We will only go through the technical parts of the article.
Reading a block of text that contains thousands of words will get you dead bored- that said, adding some touches to it may prove cool along the process of reading!
When reading, you'll face fruits! Yes, fruits. Fruits are used as references and they can help you get an idea about the section that you are reading.
: It refers to the overviews and abstractions. Usually, these parts don't focus much on the technical parts as much as they do focus on the user experience and the philosophy of user interface.
: It refers to the technical-ish parts that include codes, links, explanations and tests.
: It refers to the summaries and conclusions. You will, almost, see this apple at the end of every section you read.
: It refers to the code on Github.
These magical icons have been illustrated by Anastasia.
Inspiration
After I used a visual scripting tool such as Unreal Engine's blueprints, I enjoyed the fact that it renders the difficulty to write codes negligible.
However, visual scripting cannot make classical programming obsolete, because, sometimes, writing a few characters may prove easier than trying to link a bunch of nodes all together correctly.
All in all, visual scripting has its pros & cons- however, we won't be addressing any of those within this article.
Abstraction
As a visual scripting environment for Data Science, this project's main objective is to provide the user with the ability to create to perform multiple feats that are related to the field of Data Science, such as analyzing, extracting, mining and visualizing data, etc.
Generally, a visual scripting environment provides a bunch of built-in tools to help the user reach his sought results.
To be able to, visually, move, pan, zoom and linearly transform your nodes and the wires connecting them in a relevant way, and to get a suitable result, in the end, only then we can, appropriately, say we have created a visual scripting environment.
By default, such environments contain nodes, ports and connections. Each element of those has its own particular role and significance.
Why WPF
WPF is a relevant technology - I have already written an article about it in the past that I highly recommend.
Thanks to its powerful skinning and styling features, WPF makes it easier to achieve the desired UIs in less time. Writing a new control in WPF that needs 30 minutes to be ready, will take up to an hour or two if I had to use WinForms & GDI+.
Truth be told, the most crucial factor that has influenced my choice is that WPF uses hardware acceleration for rendering. Hence, the better the performance.
Architecture
The project is divided into multiple sections and slices, each part has its own merit and purpose.
Core
The core of the visual scripting environment is usually consolidated of a camera imitating control and some graphical elements that are manipulatable by that control.
Camera Control
The camera control is used to make surfing the environment easier, by making the user able to move, pan, zoom and resize the elements on the screen.
Before digging any deeper, we must first note that the mouse and its movement are the most crucial aspect of the Camera
control, the mouse has multiple modes, and each mode has its own purposes and properties.
- Nothing
You guessed it right. In this state, the mouse's mode has no significant meaning.
- Panning
In this mode, the mouse's movement will be accompanied by the movement of all the nodes on the screen.
- Selection
This is the most common mode. Every selected element will move according to the movement of the mouse.
- PreSelectionRectangle
This activates when the mouse's left button is pressed down on a free space, once you drag it will trigger the SelectionRectangle
mode.
- SelectionRectangle
Once you start selection elements and grouping them, a rectangle will follow the movements of your mouse and hover around the selected elements.
-
DraggingPort
When you try to drag a wire out of a port, this mode will be activated.
-
ResizingComment
Comments are elements, you can resize them. Once you do, this mode will be activated.
Camera: Move
Having the ability to move your elements within a specific two-dimensional space is so important. It will make you able to arrange elements for a better experience.
To be able to position your elements wherever you want in WPF, you must use a container that allows you to move your items based on some coordinates (X, Y). That said, the container that we will be using is the Canvas.
The canvas gives you the ability to set the coordinates of your UIElement
s- using one of these functions:
SetLeft
SetTop
SetRight
SetBottom
Within this section, will be only focusing only on SetLeft
to set the X
(horizontal axis) value, and SetTop
to set the Y
(vertical axis) value.
Saying that we want to move an element is equivalent to saying that we want to change its coordinates. Truth is, it is easier said than done. To avoid throwing blocks of code everywhere, I shall keep the article clean and optimized for a cool-read.
Here we have our UIElement
, which is a cool looking rectangle, on a canvas
:
As we can see, the coordinates of our rectangle are (X:2; Y:1)
.
To achieve the same result with, we can perform these two operations on a rectangle called box
.
Canvas.SetTop(box,1);
Canvas.SetLeft(box,2);
With all of that being said about the canvas
and how it gives us the ability to move our UIElement
s within it, it is now easy to see that we can change our element's location wherever we want it to be.
For example, if we perform these two instructions:
Canvas.SetTop(box,0);
Canvas.SetLeft(box,1);
Then we will perceive this result:
Now after understanding how the moving operation works, it is safe to dig deeper into the technical parts.
First, let's say that we will move our element based on our mouse's movements.
In order to perform a smooth linear transformation, you have got to respect three main events:
MouseDown
Once the mouse is down, the location of the mouse must be stored as a point of origin.
MouseMove
When the mouse moves, mathematical operations must be performed in order to calculate the distance between the point of origin(the ex-location of the mouse the moment the MouseDown
event has been raised) and set the new coordinates of the element based on the interpreted results.
MouseUp
This will indicate that no element is being selected nor moved.
Camera: Panning
Panning, as far as the definition goes, it is the operation of moving more than one element at the same moment, by the same value, and that is arguable.
We have two boxes, box1
and box2
:
When we perform the panning operation, the two boxes will move based on the mouse's coordinates at the same moment.
Say we moved the mouse from right to left a little bit, this will be the result:
The subtle difference between moving and panning is that panning will move all the elements, with no exceptions, based on the mouse's coordinates. Technically speaking, we will iterate through all the elements of the canvas and edit their coordinates simultaneously.
Camera: Zoom
Zooming has two types, whether to zoom in or to zoom out. By zooming, we mean that we are going to scale our elements.
Though the fact is, it is fruitless to be talking about what zooming is in an era where, almost, everyone has a camera in his phone.
Here is our jelly box
:
We won't be addressing its coordinates in this section of the article, they won't change.
Here is our box
after zooming:
Technically, zoom-in and zoom-out are nothing but UI-friendly names. For example, there is no Cut & Paste, it is just about deleting the pasted data after the end of the operation. And that's exactly what we are doing in here, we are just scaling the elements to imitate the behaviour of zooming.
Code:
private readonly double _zoomMax = 1.5;
private readonly double _zoomMin = 0.7;
private readonly double _zoomSpeed = 0.0005;
private double _zoom = 0.9;
protected virtual void HandleMouseWheel(object sender, MouseWheelEventArgs e)
{
_zoom += _zoomSpeed * e.Delta;
if (_zoom < _zoomMin) _zoom = _zoomMin;
if (_zoom > _zoomMax) _zoom = _zoomMax;
var scaler = LayoutTransform as ScaleTransform;
if (scaler == null)
{
scaler = new ScaleTransform(01, 01, Mouse.GetPosition(this).X,
Mouse.GetPosition(this).Y);
LayoutTransform = scaler;
}
var animator = new DoubleAnimation
{
Duration = new Duration(TimeSpan.FromMilliseconds(500)),
To = _zoom
};
scaler.BeginAnimation(ScaleTransform.ScaleXProperty, animator);
scaler.BeginAnimation(ScaleTransform.ScaleYProperty, animator);
MouseMode = MouseMode.Nothing;
e.Handled = true;
}
Camera: Theme
Having a grid-like style is a better-have asset. In fact, the grid will be so helpful when we want to arrange and adjust our items visually.
<Style x:Key="VirtualControlStyle" TargetType="Canvas">
<Setter Property="ScrollViewer.Visibility" Value="Visible" />
<Setter Property="LayoutTransform">
<Setter.Value>
<MatrixTransform />
</Setter.Value>
</Setter>
<Setter Property="Background">
<Setter.Value>
<DrawingBrush TileMode="Tile" Viewport="10,10,20,20"
ViewportUnits="Absolute">
<DrawingBrush.Drawing>
<DrawingGroup>
<GeometryDrawing Brush="#353535">
<GeometryDrawing.Geometry>
<GeometryGroup>
<RectangleGeometry Rect="0,0,50,50" />
</GeometryGroup>
</GeometryDrawing.Geometry>
<GeometryDrawing.Pen>
<Pen Brush="#FFE8E8E8" Thickness="0.1" />
</GeometryDrawing.Pen>
</GeometryDrawing>
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
</Setter.Value>
</Setter>
</Style>
The implementation of the style will provide us with this result:
Camera
Children
Children
are controls and UIElement
s that can be eventually added to the canvas
. Hence, the name.
When we talk about children
in this next we are already talking about Nodes, Ports, Comments and Wires, etc.
Nodes
Nodes: Abstraction
I classify nodes as sub-applications within the visual scripting environment. In fact, each node has its own purpose and each one of them is unique.
Nodes: Template
Each node has its own style and theme; however, they do all derive from the same template.
-
Title
Each node has a title. The title is just a TextBlock
that can be used to tell more about the node.
-
In-Execution Port
The in-execution port is used to trigger the node and make it generate the code that will be eventually interpreted and compiled.
-
Out-Execution Port
The out-execution port will trigger the node that it will connect to. If by any chance, it connects to no other node, then all the remaining nodes that are unlinked will be overlooked.
An exception will be made for function, basic and spaghetti nodes.
-
Input Ports
As the name suggests, everything you pass through an input port will be treated as data and will be eventually parsed. Usually, every input port has a control that is assigned to it. The role of such a control is to give the user the ability to enter his data without the need to import it from another node- not to mention that the sought data does not always exist in other nodes.
-
Output Ports
Based on the input data, the generation of the code within the node will produce a relevant output that will be stored in the output port.
-
Additional Controls
The additional controls, while being less generic, used and implement compared to the other components of a node, they are quite serviceable in some situations.
Nodes: Manipulation
Manipulating nodes is one of the most important aspects of the visual scripting environment, for the nodes being the most important assets.
There is a set of operations that we can perform to handle the nodes:
Create
Clone
Delete
Copy
Paste
Move
Zoom
Refresh
Add Execution Ports
Add Object Ports
Refresh
Search
De/Serialize
Hide
All these methods are already added to the superclass Node
.
Nodes
Ports
Each port has only two possible states:
Execution Ports
Execution Ports: Abstraction
Execution ports are used to trigger and attach nodes to the main chain of executable functions.
We will see what a chain of executions is when we get into the connectors part.
Execution Ports: Template
The template of the execution ports is the simplest one amongst all the templates within this project; however, it varies based on the type of the port and whether is it linked or not.
That said, the execution port, as a control, is divided into two parts:
The pin is a path.
<Style x:Key="ExecPin" TargetType="Path">
<Setter Property="StrokeThickness" Value="2" />
<Setter Property="Stretch" Value="Uniform" />
<Setter Property="Data">
<Setter.Value>
<PathGeometry
Figures="m 42.333333 95.916667 h 21.166666 l 21.166667
31.750003 -21.166667 31.75 H 42.333333 Z" />
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="Path.IsMouseOver" Value="True">
<Setter Property="Path.StrokeThickness" Value="3" />
<Setter Property="Path.Effect">
<Setter.Value>
<BlurEffect Radius="1" />
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
The TextBlock
changes its location based on the type of the port. For example, if the port is an input port, then the TextBlock
is located at right. Vice-Versa.
Execution Ports: Types
- Input
An input execution port is a port that can be used to trigger a node. It is more like someone ringing your bell and saying, "hey, it is time to wake up". Any method node that is not connected to any other node will not be executed.
- Output
An output execution port will connect its parent node with another node, and a connector of execution will be duly generated.
To add an execution port to your node, use the AddExecPort
method.
AddExecPort(HelloNode, "port name", PortTypes.Input, "Text");
AddExecPort(HelloNode, "port name", PortTypes.Output, "Text");​​​​
Object Ports
Object Ports: Abstraction
Object ports are not as simple as execution ports; in fact, they are capable not only of storing data as objects, they even parse and interpret the values.
For example, there are nodes with no execution ports; despite that, they are capable or generating relevant results as a return value.
Object Ports: Template
The object ports' core template is quite similar to that of the execution ports; however, its pin's colour is variables based on the data that will be passing through that port.
Types of data and their corresponding colours:
Type | Colour |
Generic | |
Logical | |
Numeric | |
Character | |
Array, Factor, List or Matrix | |
DataFrame | |
In addition to the variance of the colour, the object ports can host a control to ease the user's experience. For more details, go back to the Nodes: Template section.
Object Ports: Types
Before we dig into the types of the object ports, we must mention that the object ports do have one more crucial criteria that have to be addressed. As a matter of fact, an object port can be connected to more than one object port.
Similar to the execution ports, there are only two types:
- Input
The input object ports are actually meant to store data, be it the data that has been attributed to it or the data that is carried within its internal control.
- Output
The output object ports are used to contain the return value of the whole operations that took place within the node's core.
To add an object port, you will have to call the AddObjectPort
method.
AddObjectPort(this, "some text", PortTypes.Input, RTypes.Generic, false);
AddObjectPort(this, "some text", PortTypes.Input, RTypes.Generic, true);
AddObjectPort(this, "some text", PortTypes.Output, RTypes.Generic, false);
var tb = new TextBox();
AddObjectPort(this, "There is a textbox inside of me",
PortTypes.Input, RTypes.Generic, false, tb);
Object Ports: Events
- DataChanged
This event is so sensitive, once the data that is contained within an object port gets created, removed, updated or parsed, this event will be fired.
Ports
Connectors
Connectors are used to link nodes to each other. And because we have two types of ports, we'll respectively have two types of connectors.
A connector is composed of:
- Host
The host is actually the camera control that is hosting the parent nodes of the StartPort
and the EndPort
.
- StartPort
A connector has a starting port and an ending port. The StartPort
is the port that the wire roots from.
- EndPort
As its name suggests, its functionalities and criteria are the opposite of that of the StartPort
. It represents the end of the wire and the connection.
Connectors
Wires
Wires are used to help visualize the connected children. And each wire has its own colour based on the purpose it has to fulfil or the data that is being carried within.
Truth be told, the wires are just an implementation of a parametric curve called Bézier curve.
(This gif has been copied from the wiki page of the Bézier curve that I have mentioned above.)
Visualizing a wire requires the existence of 4 coordinates to base our curve on. Each connector has a wire that is assigned to, which means that we can access the StartPort
and the EndPort
. That said, we can duly extract their coordinates for them being UIElement
s contained within our camera control themselves.
We can observe that the Wire
class has some public
properties. These properties will be, eventually, used to visualize a curve.
The curve is nothing but a path that has to be visualized, hence a style
had to be created to accomplish that purpose.
<Style x:Key="WireStyle" TargetType="Path">
<Setter Property="Stroke"
Value="{Binding RelativeSource=
{RelativeSource TemplatedParent},
Path=Background}" />
<Setter Property="StrokeThickness" Value="1.5" />
<Setter Property="Data">
<Setter.Value>
<PathGeometry x:Name="CurveCore">
<PathGeometry.Figures>
<PathFigureCollection>
<PathFigure
StartPoint="{Binding RelativeSource=
{RelativeSource TemplatedParent}, Path=StartPoint}">
<PathFigure.Segments>
<PathSegmentCollection>
<BezierSegment
Point1="{Binding RelativeSource=
{RelativeSource TemplatedParent},
Path=MiddlePoint1}"
Point2="{Binding RelativeSource=
{RelativeSource TemplatedParent},
Path=MiddlePoint2}"
Point3="{Binding RelativeSource=
{RelativeSource TemplatedParent},
Path=EndPoint}" />
</PathSegmentCollection>
</PathFigure.Segments>
</PathFigure>
</PathFigureCollection>
</PathGeometry.Figures>
</PathGeometry>
</Setter.Value>
</Setter>
lt;/Style>
Wires
Execution Connectors
Creating an execution connector will order the node to generate the code once its turn comes within the execution chain.
Say we have this graph:
Once we parse our graph, compile it and run it - this will be our final result:
This means that the order in which we linked the nodes, will be duly respected and instructions will be executed based on that custom order.
Executions Connectors: Operations
- Create
Creating an execution connector will be followed with the creation of a wire, and some notification within the engine to guard the ID of the connector for future usage (such as de/serialization).
In addition to that, connectors cannot be created arbitrarily. That said, there are only two possible cases when you try to connect nodes. Either to have two linkable ports or not.
- Delete
Deleting an execution connector will be followed with the deletion of a wire, and some notification within the engine to tell that one node is not considered as an executable node anymore.
Executions Connectors: MiddleMan
By definition, a middleman is:
Quote:
a person who buys goods from producers and sells them to retailers or consumers.
True to its name, the MiddleMan
algorithm I developed is an algorithm that performs the same task as the real middleman by taking the connector and change the nodes it connects based on some monospecific factor.
Imagine that we have a graph that contains dozens of nodes. Suddenly, you decide to add on more node in the middle of the graph; doing so will require you deleting multiple connectors and re-link the nodes. A better-avoided approach!
When such a situation occurs, a middleman
will prove beneficial. Here is an example:
Explanation
Once you try to create an execution connector, the StartPort
will be checked, if it already linked to another port, then a swap will occur. The former connector will be deleted, the EndPort
will be linked to the previous StartPort
- and the output execution port will become the StartPort
of the connector that links it to the EndPort
of the deleted connector. Feeling lost? Worry not, it is just a basic swapping operation.
Objects Connectors
Objects Connectors are used to help the user observe how the data is flowing in the visual scripting environment.
Creating and removing this kind of connectors is the same as the execution connectors; however, some tricks behind the scene do exist. We won't be digging into them for now.
Objects Connectors: Linkability
- Linkable
When the green tick appears, it means that the connecting operation is possible and that the data's type that is contained within the StartPort
is the same as that of the EndPort
.
- UnLinkable
When the red cross appears, it means that the connecting operation is impossible and that the data's type that is contained with StartPort
is not the same as the of the EndPort
. It may also mean that the types of the ports themselves are not compatible with each other.
- Castable
When the warning message appears, it means that the connecting operation is possible if and only if the data that is contained within the StartPort
may be castable to another type that is compatible with the data's type that the EndPort
can contain.
Objects Connectors: Spaghetti Divider
Graphs may get tedious and become hard to surf, analyze and even to look at. With that being mentioned, there is one approach that may help us customize the wires that may drive you crazy, and that approach is called spaghetti dividing.
Imagine we have this ugly graph in here:
We can easily notice that we can barely see where the wires are rooting from, not to mention that we can't decide whether we are handling the right piece of data or not. In such a condition, the spaghetti divider comes in to save the situation.
Now things have become clearer than ever.
Explanation
By performing a right-click on an object connector, select the "Divide" option, and it will divide the wire into two pieces that are linked to each other thanks to a pin that plays the role of a bridge. you can move that pin however you like to make it easier to visualize your graph.
Spaghetti Divider: Operations
-
Create
-
Delete
Loyal to its main purpose, when you delete the flying pin, it won't result in the whole deletion of the connector.
Once you create a spaghetti divider, it will conserve the details of the connector that it has divided into two parts. As a result, when you delete the magical pin, it will retrieve the previous state of the connector.
Objects Connectors: Flowing Data
Basically, this may be one of the most important parts of the project.
When the contents on the StartPort
get modified, the DataChanged
event gets triggered. As a consequence, the data that is inside the StartPort
will be sent to the EndPort
, such event will be executed N times, sequentially.
Virtual Control
Overview
The Virtual Control is, in fact, an inheritance of the Camera Control and it manages all the components we created above, such as the connectors, ports, etc.
Within the Virtual Control, all the components we created are considered as UIElement
s, in other words, handling them using the mechanism that the Camera
is based on, is feasible.
Implementation of the Camera Control
public class VirtualControl : CanvasCamera
{
public VirtualControl()
{
}
}
Inheriting the properties of the Camera Control means that we will be able to apply the operations of moving, panning and zooming, etc.
Managment of Nodes
Nodes define the most crucial part of the project, thus, handling them with carefulness is a must. The virtual control is dedicated to taking care of every task that results in the manipulation of nodes.
Nodes, when hosted on the camera control, have two states. A node is either Selected or not. When selected, the node's border will glow in golden colour.
The distinction between a selected node and another is very important- for the selected nodes being the ones who will play the role of the operands in every node-related operation. For example, we can only copy the selected nodes.
Such operations include:
- Move
The only nodes you can move are the selected ones- that said, if you select multiple nodes, then all of those selected nodes will move simultaneously. This kind of operation is similar to that of the planning we discussed above.
- Create
The creation of new nodes consists of adding the new instanced node the list of nodes that are already hosted. Once that happens, the camera control will eventually consider it as one of its children.
-
Delete
The process of deleting a node is not as easy as the creation of one. along the process of scripting, the user will work on a specific node, change its location, change its inner-data, link it to another node, add more ports to it, etc. This makes the deletion of a node tedious operation because the node is a part of the graph. For example, we have this node that has 21 connectors and data that is flowing through it, deleting it means the deletion of everything that is related to it.
-
To delete a node, just select it, and then press the delete button on your keyboard. Alternatively, you can use the context menu of the virtual control.
- Copy
Copying a node means that we will extract some kind of metadata and use it later to generate a clone of that copied node with respect to its characteristics. Truth is, we will serialize that node- we will get into the details later on in the de/serialization section.
You can easily copy a node by performing a CTRL+V coupled-click or by using the context menu of the virtual control.
- Paste
Pasting a node means that we will try to exploit the copied metadata and use it to generate a new functional node. It is more like deserializing the metadata.
- Cut
There isn't much to talk about in here, the cutting operation is nothing but a sequential execution of the operations of copying and deleting.
- Commenting
Commenting on a zone of your graph is a salubrious practice. Truth is, it helps you organize your graph and divides your project into regions.
Management of Connections
Connections are used to connect ports and nodes to each other. There exist two types of connectors, ObjectsConnector
and ExecutionConnector
. Truth is, managing the connections is not solely the task of the virtual control, connections are managed via multiple players, such as nodes themselves. When a node moves, it triggers the events of the connectors which will make it change the coordinates of the wire.
With that being said, the virtual controls still plays a solid role when it comes to managing the connectors. You can create connections, divide them or even delete them.
Creating a connector requires calling the NodesManager static
class- a class that contains multiple methods and functions that help the Virtual Control manage things.
For a starter, creating an execution connector, all you need to do is to call the CreateExecutionConnector
method. This method takes the virtual control that hosts the parent nodes, the first and the second port as parameters.
NodesManager.CreateExecutionConnector(Host, portA, portB);
Deleting a connector is a less tedious process, just call the Delete();
method and see the magic.
Connector.Delete();
Dividing a connector is not always possible for the division being a special operation that can only be performed if, and only if, the operand is an ObjectsConnector
.
if (StartPort.ParentNode.Types != NodeTypes.SpaghettiDivider &&
EndPort.ParentNode.Types != NodeTypes.SpaghettiDivider)
Task.Factory.StartNew(() =>
{
Wire.Dispatcher.BeginInvoke(DispatcherPriority.SystemIdle,
new Action(() =>
{
var divider = new SpaghettiDivider(Host, this, false);
Host.AddNode(divider, Mouse.GetPosition(Host).X,
Mouse.GetPosition(Host).Y);
e.Handled = true;
}));
});
e.Handled = true;
Notes
Even though all the connectors and wires are hosted on the Virtual Control, the vControl only plays the role of the caller
, by calling the functions and methods that are needed to manage the connections between ports & nodes.
VirtualControl
Comments
Commenting zones of your graph may prove useful, it is more like creating regions in C#. With that being said, what is, technically, a comment?
A comment is actually a rectangular component that has a flexible size. The moment of its creation, it calculates the coordinates and the area that is covered by the selected nodes.
The coordinates (X; Y) of the comment are the same as the first node of the selected ones. However, that is not the case for the width & height of the comment.
To determine the width and height of the comment based on all the nodes that we want to comment on, we need to go through all of them and compare their sizes and coordinates.
Technical Aspects
To find the genuine width and the height of the comment, we should call:
Width = max_Width(nodes) + 30;
Height = max_Height(nodes) + 40;
The prolongment of the size by 30
& 40
is not a mere act as it seems, we've done that to leave more space inside the comment so we won't have our nodes standing on the edge.
Searching for adequate width:
private double max_Width(ObservableCollection<Node> nodes)
{
var maxwidth = nodes[0].ActualWidth;
foreach (var node in nodes)
if (node.ActualWidth + node.X > maxwidth + X)
maxwidth += node.ActualWidth + node.X - (maxwidth + X);
return maxwidth;
}
Height:
private double max_Height(ObservableCollection<Node> nodes)
{
var maxheight = nodes[0].ActualHeight;
foreach (var node in nodes)
if (node.ActualHeight + node.Y > maxheight + Y)
maxheight += node.ActualHeight + node.Y - (maxheight + Y);
return maxheight;
}
Resize
Comments are flexible and resizable, you can resize the comment using the bottom-right handler.
When the left mouse button is down, the MouseMode
will be set to a state that indicates that the event that must follow the movement of the mouse is the modification of the size of the comment.
if (MouseMode == MouseMode.ResizingComment &&
mouseEventArgs.LeftButton == MouseButtonState.Pressed)
{
Cursor = Cursors.SizeNWSE;
var currentPoint = Mouse.GetPosition(this);
if (currentPoint.Y - TempComment.Y > 0 &&
currentPoint.X - TempComment.X > 0)
{
TempComment.Height = currentPoint.Y - TempComment.Y;
TempComment.Width = currentPoint.X - TempComment.X;
TempComment.LocateHandler();
}
else
{
TempComment.Height = 32;
TempComment.Width = 32;
TempComment.LocateHandler();
}
return;
}
Moving
The contents of a comment are nodes, and each comment works as a container. Once you move a container, its whole contents will be transferred all along.
private void Comment_MouseDown(object sender, MouseButtonEventArgs e)
{
_host.MouseMode = MouseMode.Selection;
_host.SelectedComment = this;
_host.SelectedNodes.Clear();
foreach (var node in _host.Nodes)
if (node.X >= X &&
node.X + node.ActualWidth <= X + ActualWidth &&
node.Y >= Y &&
node.Y + node.ActualHeight <= Y + ActualHeight)
_host.SelectedNodes.Add(node);
}
Uncomment (Deleting the Comment)
Being a UIElement
, a comment can be duly deleted too when it is no more needed.
comment.Dispose();
Style
The style of the comment is quite basic. It is composed of a textbox
, borders and a handler.
<Style x:Key="Comment" TargetType="core:Comment">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Canvas Background="Transparent">
<Border
Background="#33FFFFFF"
Height="25"
Width="{Binding RelativeSource=
{RelativeSource TemplatedParent}, Path=Width}" />
<Border Background="#99B2B2B2"
Margin="0,0,0,4"
Width="{Binding RelativeSource=
{RelativeSource TemplatedParent}, Path=Width}"
Height="{Binding RelativeSource=
{RelativeSource TemplatedParent}, Path=Height}"
BorderBrush="#FF769954"
BorderThickness="2" />
<TextBox
FontSize="16"
Foreground="Black"
BorderBrush="Transparent"
Background="Transparent"
Width="{Binding RelativeSource=
{RelativeSource TemplatedParent}, Path=Width}"
Text="{Binding RelativeSource=
{RelativeSource TemplatedParent},
Path= Summary, Mode=TwoWay}" />
<StackPanel Name="CornerImage_Resize" Height="12" Width="12">
<StackPanel.Background>
<ImageBrush ImageSource=
"../MediaResources/handle_resize.png" />
</StackPanel.Background>
</StackPanel>
</Canvas>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
The virtual control works as a container, it hosts all the elements we created and manages them adequately; however, it is not enough, we need more tools to help us make a pleasant visual scripting system.
Comments
Controls
Controls are magical tools that you can exploit to make perfect use of the visual scripting environment, these controls can help you manage variables that hold data, a list of nodes to add to your graph, etc.
Variables List
Variables play a solid role in each graph you create, they help you store the output of your operations.
To present the capabilities of the variables list, we will try to turn this formula into a graph representation.
Here, we can easily observe that the introduction of two special nodes, the Set
node and the Get
node. The Set
node serves as a sub-application that redirects the output of the whole visual operations to the X
variable. The Get
node, on the other hand, serves as a data container, it returns the value of a specific variable.
Now that we understand how the variables list communicates and its purposes, we can safely dig into its depths.
Variables List: Add/Delete
Adding a variable means creating it and assigning two nodes (Set
, Get
) to it. By default, a variable's type is Generic
and it is nameless
.
Deleting a variable, on the other hand, is not that easy. Deleting a variable means deleting everything that is related to it- its related Get
/Set
nodes, for example.
Variables List: Exploit
One of the most beneficial features of the variables list is that it supports the Drag and Drop mechanism. In other words, you can easily drag your variable and drop it on your graph- then, watch the magic.
The truth is, the hovering element, that represents a visualization of the drag and drop mechanism is nothing but a window that has flexible properties. Those properties are based on the properties of the variable that we are intending to exploit.
Style
<Style x:Key="VariableHoster" TargetType="controls:VariableHoster">
<Setter Property="ShowInTaskbar" Value="False" />
<Setter Property="WindowStyle" Value="None" />
<Setter Property="ResizeMode" Value="NoResize" />
<Setter Property="Height" Value="25" />
<Setter Property="Width" Value="100" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Canvas Background="#353535">
<Border x:Name="GoldenBorder"
Width="{Binding RelativeSource=
{RelativeSource TemplatedParent},Path=ActualWidth}"
Height="{Binding RelativeSource=
{RelativeSource TemplatedParent},Path=ActualHeight}"
CornerRadius="4,4,4,4"
BorderThickness="2"
Background="{DynamicResource GlobalBackground}">
<Border.BorderBrush>
<RadialGradientBrush>
<GradientStop Color="#FFFFB10C" Offset="0.215" />
<GradientStop Color="#FF916E24" Offset="0" />
</RadialGradientBrush>
</Border.BorderBrush>
</Border>
<Border x:Name="Icon" Height="16"
Width="16" Margin="5,3,0,0">
<Border.Background>
<ImageBrush ImageSource="{Binding RelativeSource=
{RelativeSource Self},Path=Icon}" />
</Border.Background>
</Border>
<TextBlock x:Name="VarName" Margin="25,3,0,0"
Foreground="AliceBlue" />
</Canvas>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Variables List: Tracing
Tracing your variable means counting how many times it gets referenced.
For example, say that we used the variable y
3 times (2 setters, 1 getter), and the variable x
, also 3 times (1 setter, 2 getters):
We should get these notes showing as tooltips:
AND
By passing the cursor on the degree-like icon, a tooltip will show. That tooltip will tell you how many times this variable has been used, and what was the type of usage.
VariablesList
Nodes Tree
The tree of nodes is the tool that will help you add nodes to the virtual control. The truth is, it is a combination of a dynamic TreeView
. Its items do represent the metadata of every node we have.
In the process of the creation of this control, I, accidentally, managed to create the fastest filtering algorithm for WPF's TreeView
s. That said, I & Dirk shared our expertise and released a better and a more optimized filtering algorithm. It can be found here: Advanced WPF TreeViews in C#/VB.Net Part 3 of n.
Nodes Tree: Template
The tree of nodes has two states, pre-expanded and post-expanded.
Pre-expanded
- Green: The green zone represents the text box that can be eventually used to filter the tree.
- Red: The red zone represents the categories of the nodes, and, of course, the nodes within.
Post-expanded
- Blue: The blue zone represents the title of the node.
- Green: The green zone represents the description or the tooltip that is related to the node.
Alternatively, we can notice the custom scrollbar on the right side of the tree.
Nodes Tree: Classification and Sorting
All the nodes are contained within a DLL file (as a form of flexibility that allows you create plugins/add-ons), and each node belongs to a specific category. With that being mentioned, the classification of the nodes within the tree will be based on the present categories, i.e., (Math
, Arrays
, Vectors
, etc.).
Sorting the categories, on the other hand, is based on the number of the nodes that are contained inside.
The category of the node is set in the process of its creation. In its initial form, the tree of nodes will contain only the categories of the nodes- eventually, it will iterate through all the available nodes and place them respectively. The only category that is set by default, is the variables
category, where all the variables will be hosted.
The magic behind building the categories:
private void BuildCategories()
{
_categories = new List<Dictionary<int, string>>();
foreach (var node in _pluginsManager.LoadedNodes)
if (!_catNames.Contains(node.Category))
_catNames.Add(node.Category);
if (VariablesTreeRootIndex() != -1)
_catNames.Remove("Variables");
for (var index = 0; index < _catNames.Count; index++)
{
var name = _catNames[index];
var dic = new Dictionary<int, string> {{index, name}};
_categories.Add(dic);
}
for (var index = 0; index < _categories.Count; index++)
{
var item = _categories[index];
var name = item[index];
if (name != null) Roots.Add(new NodeItem("", name));
}
}
The magic behind building node instances:
private void BuildNodes()
{
Task.Factory.StartNew(() =>
{
Dispatcher.BeginInvoke(new Action(() =>
{
foreach (var node in _pluginsManager.LoadedNodes)
for (var index = 0; index < Roots.Count; index++)
if (node.Category == Roots[index].NodeName)
Roots[index].Nodes.Add(new NodeItem("", node.Clone()));
}));
});
}
Nodes Tree: Insertion
By insertion, we mean the creation of a new instance of a specific node, inserting it into the virtual control. That said, the process of insertion has two different states:
- Generic Insertion: Occurs when you normally open the tree of nodes
- Altered Insertion: Occurs when you open the tree of nodes by dragging a wire of a node
Generic Insertion
You can open the tree of nodes by performing a double-click, or by using the context menu of the virtual control. Eventually, you select the node that you are intending you add, and simply add it by press on the key [ENTER] or via a mouse-double-click.
Altered Insertion
This part is relatively genuine. Instead of adding a node, then linking its ports to other nodes- you can, simply, drag the wire that you are intending to link and wait for the tree of nodes to show you all the available options.
The linking operation will be performed automatically.
Magically, the two insertion approaches do have the same code-behind:
private void InsertNode(Node node)
{
Task.Factory.StartNew(() =>
{
Application.Current.Dispatcher.BeginInvoke
(DispatcherPriority.Render, new Action(() =>
{
_host.AddNode(node, Canvas.GetLeft(this), Canvas.GetTop(this));
node.Dispatcher.BeginInvoke
(DispatcherPriority.Loaded, new Action(() =>
{
if (_host.TemExecPort != null && node.InExecPorts.Count > 0)
if (_host.TemExecPort.ConnectedConnectors.Count > 0)
{
string id1 = Guid.NewGuid().ToString(),
id2 = Guid.NewGuid().ToString();
var thirdNode =
_host.TemExecPort.ConnectedConnectors[0].
EndPort.ParentNode;
NodesManager.CreateExecutionConnector
(_host, _host.TemExecPort, node.InExecPorts[0],
id1);
NodesManager.CreateExecutionConnector
(_host, node.OutExecPorts[0],
thirdNode.InExecPorts[0], id2);
}
else
{
NodesManager.CreateExecutionConnector
(_host, _host.TemExecPort, node.InExecPorts[0]);
}
else if (_host.TemObjectPort != null &&
node.InputPorts.Count > 0)
NodesManager.CreateObjectConnector
(_host, _host.TemObjectPort, node.InputPorts[0]);
}));
}));
});
}
Calling the above function and the creation of the node:
var hostedNode = ((NodeItem) _tv.SelectedItem)?.HostedNode;
if (hostedNode != null)
{
var node = hostedNode.Clone();
InsertNode(node);
Remove();
}
The abilities of the tree of nodes are not limited to that extent, it can also manage the variables.
We are referring to the X & Y variables in this screenshot.
NodesTree
InnerMessageBox
This control works like an internal notification, tooltip and an indicator. It has an icon that serves as a sign. We have already discussed the role of this component above when we talked about the linking possibilities.
public enum InnerMessageIcon
{
Correct,
Warning,
False
}
InnerMessageBox
Search Window
Every node has a built-in function that returns a TreeViewItem
if it contains metadata that matches the item that we are looking for. It returns null
if it does not.
public virtual FoundItem Search(string key)
{
if (IsCollapsed) return null;
var KEY = key.ToUpper();
var fi = new FoundItem();
if (Title.ToUpper().Contains(KEY))
{
fi.foundNode = this;
fi.Hint = Title;
fi.Type = ItemTypes.Node;
}
foreach (var port in InputPorts)
if (port.IsVisible)
{
if (port.Control is TextBox)
{
if (((TextBox) port.Control).Text.ToUpper().Contains(KEY))
{
if (fi.Hint != Title)
fi.Hint = Title;
fi.Items.Add(new FoundItem
{
Hint = $" In input :{(port.Control as TextBox).Text}",
Type = ItemTypes.Port,
foundNode = this
});
}
}
else
{
var textBox = port.Control as UnrealControlsCollection.TextBox;
if (textBox == null ||
!textBox.Text.ToUpper().Contains(KEY)) continue;
var box = port.Control as UnrealControlsCollection.TextBox;
if (box != null)
if (fi.Hint != Title)
fi.Hint = Title;
fi.Items.Add(new FoundItem(port.Background)
{
Hint = $"In input :{box.Text}",
Type = ItemTypes.Port,
foundNode = this,
Brush = port.StrokeBrush
});
}
}
else
{
var textBox = port.Control as UnrealControlsCollection.TextBox;
if (textBox == null ||
!textBox.Text.ToUpper().Contains(KEY)) continue;
var box = port.Control as UnrealControlsCollection.TextBox;
if (box != null)
if (fi.Hint != Title)
fi.Hint = Title;
fi.Items.Add(new FoundItem(port.Background)
{
Hint = $"In input :{box.Text}",
Type = ItemTypes.Port,
foundNode = this,
Brush = port.StrokeBrush
});
}
foreach (var port in OutputPorts)
if (port.IsVisible)
{
if (port.Control is TextBox)
{
if (((TextBox) port.Control).Text.ToUpper().Contains(KEY))
{
if (fi.Hint != Title)
fi.Hint = Title;
fi.Items.Add(new FoundItem
{
Hint = $" In input :{(port.Control as TextBox).Text}",
Type = ItemTypes.Port,
foundNode = this
});
}
}
else
{
var textBox = port.Control as UnrealControlsCollection.TextBox;
if (textBox == null ||
!textBox.Text.ToUpper().Contains(KEY)) continue;
var box = port.Control as UnrealControlsCollection.TextBox;
if (box != null)
if (fi.Hint != Title)
fi.Hint = Title;
fi.Items.Add(new FoundItem(port.Background)
{
Hint = $"In input :{box.Text}",
Type = ItemTypes.Port,
foundNode = this,
Brush = port.StrokeBrush
});
}
}
else
{
var textBox = port.Control as UnrealControlsCollection.TextBox;
if (textBox == null ||
!textBox.Text.ToUpper().Contains(KEY)) continue;
var box = port.Control as UnrealControlsCollection.TextBox;
if (box != null)
if (fi.Hint != Title)
fi.Hint = Title;
fi.Items.Add(new FoundItem(port.Background)
{
Hint = $"In input :{box.Text}",
Type = ItemTypes.Port,
foundNode = this,
Brush = port.StrokeBrush
});
}
if (fi.Hint != Title)
return null;
fi.foundNode = this;
return fi;
}
Once we launch the search operation, the algorithm will iterate through all the nodes and check if they contain a similar metadata within. If yes, then the TreeViewItem
that will return will be hosted on the Search Window.
The FoundItem
will operate as the standalone component itself. By double-clicking on it, it will take you to the node where the data is contained.
public class FoundItem : TreeViewItem
{
private readonly Path ctrl =
new Path {Width = 15, Height = 15, Margin = new Thickness(0, -2, 0, 0)};
private readonly TextBlock hint =
new TextBlock {Foreground = Brushes.WhiteSmoke,
Background = Brushes.Transparent};
private string _hint;
private Brush brush;
public ItemTypes Type;
public FoundItem(Brush b = null)
{
IsExpanded = true;
Loaded += (s, e) =>
{
MouseDoubleClick += FoundItem_MouseDoubleClick;
var sp = new StackPanel {Orientation = Orientation.Horizontal,
MaxHeight = 20};
sp.Children.Add(ctrl);
sp.Children.Add(hint);
Header = sp;
if (Type == ItemTypes.Port)
{
ctrl.Style = FindResource("ObjectPin") as Style;
ctrl.Stroke = b;
}
else
{
ctrl.Style = FindResource("ExecPin") as Style;
}
};
}
public string Hint
{
get { return _hint; }
set
{
_hint = value;
if (hint != null) hint.Text = value;
}
}
public Brush Brush
{
get { return brush; }
set
{
brush = value;
ctrl.Stroke = value;
ctrl.Fill = value;
}
}
public Node foundNode { get; set; }
private void FoundItem_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
foundNode.Host.GoForNode(foundNode);
}
}
The GoForNode
is a function that takes a node as a parameter. It moves all the UIElement
s that are rendered on the VirtualControl
in order to put the selected node in the centre of the screen.
public void GoForNode(Node node)
{
node.X = Math.Truncate(node.X);
node.Y = Math.Truncate(node.Y);
var origin = BasisOrigin;
origin.X -= node.ActualWidth - 10;
origin.X = Math.Truncate(origin.X);
origin.Y = Math.Truncate(origin.Y);
var difference = Point.Subtract(origin, new Point(node.X, node.Y));
var timer = new DispatcherTimer {Interval = new TimeSpan(0, 0, 0, 0, 01)};
foreach (var n in Nodes)
{
if (difference.X < 0)
n.X += difference.X;
else
n.X += difference.X;
if (difference.Y < 0)
n.Y += difference.Y;
else
n.Y += difference.Y;
NeedsRefresh = true;
}
difference.X = 10;
difference.Y = 0;
if (difference.X != 0 || difference.Y != 0)
timer.Start();
timer.Tick += (s, e) =>
{
if (difference.X == 0 && difference.Y == 0)
timer.Stop();
foreach (var n in Nodes)
{
if (difference.X > 0)
n.X++;
else
n.X--;
if (difference.Y > 0)
n.Y++;
else
n.Y--;
}
if (difference.X > 0)
difference.X--;
else
difference.X++;
if (difference.Y > 0)
difference.Y--;
else
difference.Y++;
};
SelectedNodes.Clear();
SelectedNodes.Add(node);
}
Search
Contents Browser
The contents browser plays the role of a files explorer that is adaptive to the visual scripting environment.
Overview
The contents browser, while not being stable yet, is a tool that can be used like a bridge between the visual scripting environment and the files on the hard drive. It plays the same role as the Solution Explorer of Visual Studio but in a more genuine way.
Template
The template is quite generic, and it is divided into three sections.
- The tools bar
The tools bar is a panel that contains a few buttons that allow you to create a new file/folder, import an existing file, and save all the changes you have made. Also, it supports undo/redo operations.
- Folders explorer
The folders explorer is a tree that represents all the folder within a specific path.
-
Contents explorer
Every file and folder will show in the contents explorer. Not only that, it classifies the files based on their extensions by giving them specific colours that would help us distinguish the difference.
i.e.:
Folders, on the other hand, are more flexible as well. In fact, a folder is a combination of two paths (a.k.a., 2D shapes). You can easily customize a folder, by changing its colours and shapes.
-
Folder's head
The folder, as we have already mentioned above, is divided into two components. The header of the folder and the core.
-
Folder's core
The core of the folder is the part of the path that will have its colour changed eventually.
Changing the colour of the folder can be done easily. Here are two samples:
-
Search bar
Nothing fancy nor sophisticated is related to this part. It is a bar that can filter the files that you are exploring based on the standard search patterns of windows.
Example: "*.txt* to show only the files which have '.txt' as an extension.
Styles
ExplorerItem
<Style x:Key="ContentBrowserElement" TargetType="{x:Type ListViewItem}">
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListViewItem}">
<Grid HorizontalAlignment="Left" VerticalAlignment="Top">
<Border x:Name="border"
BorderBrush="{x:Null}" BorderThickness="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" CornerRadius="2.5" />
<WrapPanel HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<ContentPresenter Margin="3,3,5,30" />
</WrapPanel>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="ExplorerItem" TargetType="controls:ExplorerItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Border x:Name="ExplorerItemContainer" CornerRadius="8">
<StackPanel Orientation="Vertical" Margin="3"
Background="{Binding BackColor}" Width="68"
Height="100"
ClipToBounds="True">
<Canvas x:Name="Image" Visibility="Hidden">
<Rectangle Fill="{Binding ItemColor}"
Opacity=".4" Canvas.Left="2"
x:Name="SelectionBox"
Height="64" Width="64"
StrokeThickness="1">
<Rectangle.Effect>
<BlurEffect Radius="3" />
</Rectangle.Effect>
</Rectangle>
<Rectangle Canvas.Left="2" Canvas.Top="57"
Height="6.4" Width="64"
Fill="{Binding ItemColor}"
RadiusY="4" RadiusX="4" />
<Rectangle x:Name="Thumbnail" Width="42"
Height="42" Canvas.Left="12"
Canvas.Top="6" />
</Canvas>
<Grid x:Name="Folder"
ClipToBounds="True" Margin="0,12,0,0"
Visibility="Hidden" Width="52" Height="52">
<StackPanel Orientation="Vertical">
<Path Stretch="Uniform"
StrokeThickness="1.33333337">
<Path.Fill>
<LinearGradientBrush>
<GradientStop Color="#FF141F2B"
Offset="0.774" />
<GradientStop Color="#FF979797" />
</LinearGradientBrush>
</Path.Fill>
<Path.Data>
<PathGeometry
Figures="m 36.08425 225.37304
c 3.687503 -0.38572 5.000001
-1.13954 5.000001 -2.87168 0
-4.92432 4.279726 -14.59278
8.064553 -18.21887 2.169627
-2.07864 6.743628 -5.31508
10.164449 -7.19208 9.241516
-5.07084 10.814177 -8.05027
12.940261 -24.5155 1.607543
-12.44945 2.399082 -15.09711
5.751785 -19.23949 2.138244
-2.64187 6.255744 -6.04563 9.150001
-7.56391 5.140936 -2.69685 7.276218
-2.77019 92.59562 -3.18018 95.45312
-0.45869 98.15761 -0.28718 106.152
6.73198 7.0916 6.22651 8.7382 10.6307
9.68435 25.90307 0.73396 11.84719
1.26586 14.14613 3.87724 16.75775
l 3.02658 3.02687 163.62993 0.66666
c 162.75799 0.66312 163.65832 0.68152
168.96324 3.45406 6.35662 3.3222
12.57175 12.61305 13.60857 20.3431
0.69946 5.21488 0.91185 5.41624
6.22446 5.90136 3.02518 0.27624
-136.0997 0.50623 -309.16637
0.51107 -173.06667 0.005
-312.41667 -0.22655 -309.66667
-0.51421 z"
FillRule="NonZero" />
</Path.Data>
</Path>
<Path Margin="0,-2,0,0" Stretch="Uniform"
StrokeThickness="1.33333337">
<Path.Fill>
<LinearGradientBrush EndPoint="0.5,1"
StartPoint="0.5,0">
<GradientStop Color="Black"
Offset="1.5" />
<GradientStop Color="#FF6E6E6E"
Offset="0.029" />
<GradientStop Color="#FF000102" />
</LinearGradientBrush>
</Path.Fill>
<Path.Data>
<PathGeometry
Figures="m 35.46142 662.88211
c -5.645595 -6.04364 -6.953389
-12.93947 -7.582867 -39.98349
-0.31759 -13.64451 -1.179782
-51.50821 -1.915981 -84.14154
-1.96164 -86.95319 -5.166926
-162.45026 -9.992734 -235.36814
-1.271839 -19.21746 -1.512834
-30.87022 -0.697253 -33.71399
2.083099 -7.26333 6.312426
-12.8508 12.339504 -16.302
l 5.732545 -3.28255 314.483266
-0.34436 c 218.55465 -0.2393
316.08585 0.0872 319.73701
1.07034 6.94134 1.8691
15.21772 10.23518 17.12215
17.30772 1.12319 4.17118
0.98421 13.3284 -0.57271
37.73698 -5.17222 81.08726
-8.05125 149.04533 -10.06429
237.56267 -2.32833 102.38191
-2.43281 105.12652 -4.2054
110.47999 -0.93725 2.83066
-3.32391 6.88066 -5.30367
9 l -3.59956 3.85334 H
350.01131 39.081194 Z"
FillRule="NonZero" />
</Path.Data>
<Path.Effect>
<DropShadowEffect BlurRadius="5"
Color="Black" />
</Path.Effect>
</Path>
</StackPanel>
</Grid>
<Label x:Name="Tag" BorderBrush="Transparent"
VerticalAlignment="Center"
Background="Transparent" HorizontalAlignment="Center">
<TextBlock Foreground="{Binding Foreground}"
VerticalAlignment="Center" MaxWidth="65"
MaxHeight="40" TextWrapping="Wrap"
Background="Transparent"
TextTrimming="CharacterEllipsis"
HorizontalAlignment="Center"
Text="{Binding ItemName}" />
</Label>
<TextBox Visibility="Collapsed" x:Name="RenameBox"
Text="{Binding ItemName}"
Background="Transparent" Foreground="WhiteSmoke"
TextWrapping="Wrap" AcceptsReturn="True"
VerticalScrollBarVisibility="Disabled"
HorizontalAlignment="Center" MaxWidth="64"
MaxHeight="40"
BorderBrush="White" BorderThickness="1">
<TextBox.Effect>
<DropShadowEffect BlurRadius="10" Color="White" />
</TextBox.Effect>
</TextBox>
</StackPanel>
<Border.ContextMenu>
<ContextMenu />
</Border.ContextMenu>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="ExplorerItemContainer"
Property="Background">
<Setter.Value>
<DrawingBrush>
<DrawingBrush.Drawing>
<DrawingGroup>
<GeometryDrawing>
<GeometryDrawing.Brush>
<LinearGradientBrush
StartPoint="0,0" EndPoint="0,1"
SpreadMethod="Pad">
<GradientStop
Color="#FF6C6C6C" Offset="1" />
<GradientStop
Color="#22FFFFFF" Offset="0" />
</LinearGradientBrush>
</GeometryDrawing.Brush>
<GeometryDrawing.Geometry>
<RectangleGeometry
Rect="0,0 1,0.48" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
</Setter.Value>
</Setter>
</Trigger>
<DataTrigger Binding="{Binding IsSelected}" Value="True">
<Setter TargetName="ExplorerItemContainer"
Property="Border.Background">
<Setter.Value>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1"
SpreadMethod="Pad">
<GradientStop Color="#99FFE909" Offset="1" />
<GradientStop Color="#FF232000" Offset="0" />
</LinearGradientBrush>
</Setter.Value>
</Setter>
<Setter TargetName="ExplorerItemContainer"
Property="Border.BorderThickness" Value="2" />
<Setter TargetName="ExplorerItemContainer"
Property="Border.BorderBrush">
<Setter.Value>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1"
SpreadMethod="Pad">
<GradientStop Color="#99FFB109" Offset="0" />
<GradientStop Color="#CC383302" Offset="1" />
</LinearGradientBrush>
</Setter.Value>
</Setter>
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="ToolTip">
<Setter.Value>
<Grid>
<Border CornerRadius="5">
<Border.Background>
<LinearGradientBrush>
<GradientStop Color="#99CD3800" Offset="0.763" />
<GradientStop Color="#FFCB8C1F" Offset="0.028" />
</LinearGradientBrush>
</Border.Background>
<StackPanel Orientation="Vertical" Background="#353535"
Margin="3,3,3,3">
<StackPanel Orientation="Horizontal">
<TextBlock Text="File Name: "
Foreground="Gray" FontSize="13" />
<TextBlock Foreground="WhiteSmoke"
Text="{Binding ItemName}" FontSize="13" />
</StackPanel>
<Border BorderBrush="Azure" BorderThickness="0,1,0,0"
Margin="0,8" Background="#FF09172B" />
<StackPanel Orientation="Horizontal">
<TextBlock Text="Type: " Foreground="Gray" />
<TextBlock Foreground="WhiteSmoke"
Text="{Binding Extension}" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Size: " Foreground="Gray" />
<TextBlock Foreground="WhiteSmoke"
Text="{Binding Size}" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Path: " Foreground="Gray" />
<TextBlock Foreground="WhiteSmoke"
Text="{Binding Path}" MaxWidth="300"
Margin="0,0,3,0" TextWrapping="Wrap" />
</StackPanel>
</StackPanel>
</Border>
</Grid>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="ToolTip">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToolTip">
<ContentPresenter />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Folder (Header):
<Path Stretch="Uniform" StrokeThickness="1.33333337">
<Path.Fill>
<LinearGradientBrush>
<GradientStop Color="#FF141F2B"
Offset="0.774" />
<GradientStop Color="#FF979797" />
</LinearGradientBrush>
</Path.Fill>
<Path.Data>
<PathGeometry
Figures="m 36.08425 225.37304 c
3.687503 -0.38572 5.000001 -1.13954
5.000001 -2.87168 0 -4.92432 4.279726
-14.59278 8.064553 -18.21887 2.169627
-2.07864 6.743628 -5.31508 10.164449
-7.19208 9.241516 -5.07084 10.814177
-8.05027 12.940261 -24.5155 1.607543
-12.44945 2.399082 -15.09711 5.751785
-19.23949 2.138244 -2.64187 6.255744
-6.04563 9.150001 -7.56391 5.140936
-2.69685 7.276218 -2.77019 92.59562
-3.18018 95.45312 -0.45869 98.15761
-0.28718 106.152 6.73198 7.0916
6.22651 8.7382 10.6307 9.68435
25.90307 0.73396 11.84719 1.26586
14.14613 3.87724 16.75775 l 3.02658
3.02687 163.62993 0.66666 c 162.75799
0.66312 163.65832 0.68152 168.96324
3.45406 6.35662 3.3222 12.57175
12.61305 13.60857 20.3431 0.69946
5.21488 0.91185 5.41624 6.22446
5.90136 3.02518 0.27624 -136.0997
0.50623 -309.16637 0.51107 -173.06667
0.005 -312.41667 -0.22655 -309.66667
-0.51421 z"
FillRule="NonZero" />
</Path.Data>
</Path>
Folder (Core):
<Path Margin="0,-2,0,0" Stretch="Uniform"
StrokeThickness="1.33333337">
<Path.Fill>
<LinearGradientBrush EndPoint="0.5,1"
StartPoint="0.5,0">
<GradientStop Color="Black"
Offset="1.5" />
<GradientStop Color="#FF6E6E6E"
Offset="0.029" />
<GradientStop Color="#FF000102" />
</LinearGradientBrush>
</Path.Fill>
<Path.Data>
<PathGeometry
Figures="m 35.46142 662.88211
c -5.645595 -6.04364 -6.953389
-12.93947 -7.582867 -39.98349
-0.31759 -13.64451 -1.179782 -51.50821
-1.915981 -84.14154 -1.96164 -86.95319
-5.166926 -162.45026 -9.992734 -235.36814
-1.271839 -19.21746 -1.512834 -30.87022
-0.697253 -33.71399 2.083099 -7.26333
6.312426 -12.8508 12.339504 -16.302 l
5.732545 -3.28255 314.483266 -0.34436
c 218.55465 -0.2393 316.08585 0.0872
319.73701 1.07034 6.94134 1.8691
15.21772 10.23518 17.12215 17.30772
1.12319 4.17118 0.98421 13.3284
-0.57271 37.73698 -5.17222 81.08726
-8.05125 149.04533 -10.06429 237.56267
-2.32833 102.38191 -2.43281 105.12652
-4.2054 110.47999 -0.93725 2.83066
-3.32391 6.88066 -5.30367 9 l
-3.59956 3.85334 H 350.01131
39.081194 Z"
FillRule="NonZero" />
</Path.Data>
<Path.Effect>
<DropShadowEffect BlurRadius="5"
Color="Black" />
</Path.Effect>
</Path>
Implementation
This file explorer has been duly created to integrate with the system fully. It enables you to explore the files that can be used by a data scientist. Not only that, you can generate nodes based on those files within.
ContentsBrowser
Performance Gauge
A performance gauge is a tool that I have made out of fun. However, adding it to the project has been proven useful. It helps me keep track of how many resources the visual scripting environment is using.
Style
<Style x:Key="Gauge" TargetType="controls:PerformanceGauge">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Grid Width="270" Height="30">
<Path Style="{DynamicResource ToolTipHint}"
HorizontalAlignment="Left" Name="Spy"
Stretch="Uniform" StrokeThickness="1.33333325">
<Path.ToolTip>
<Border Background="#424242"
BorderBrush="Gray" CornerRadius="3,3,3,3"
BorderThickness="2">
<StackPanel MaxWidth="300" Margin="3">
<TextBlock Foreground="Gold" FontSize="14"
Text="Performance gauge"
FontFamily="Segoe UI Semibold" />
<TextBlock Foreground="AliceBlue"
Text="This gauge will help you
maintain the performaance of your
running project."
TextWrapping="Wrap" />
</StackPanel>
</Border>
</Path.ToolTip>
</Path>
<Grid Height="30" Width="180">
<Path xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Name="Gauge"
StrokeThickness="3" StrokeLineJoin="Miter"
StrokeStartLineCap="Flat"
StrokeEndLineCap="Flat" Stretch="Fill"
Fill="#FF353535">
<Path.Stroke>
<LinearGradientBrush EndPoint="0.5,1"
StartPoint="0.5,0">
<GradientStop Color="#FF202934"
Offset="0.623" />
<GradientStop
Color="{Binding RelativeSource=
{RelativeSource TemplatedParent},
Path=CoreColor}" />
</LinearGradientBrush>
</Path.Stroke>
<Path.Data>
<PathGeometry
Figures="m 10 2.519685 h 240 v 30 h 40 v
-30 H 790 L 660 152.51969 H 290 v -30 h
-40 v 30 H 10 Z"
FillRule="NonZero" />
</Path.Data>
</Path>
<TextBlock Text="{Binding RelativeSource=
{RelativeSource TemplatedParent},Path=Ram}"
Foreground="WhiteSmoke" Margin="8"
FontFamily="Segoe WP Semibold" />
<StackPanel Orientation="Horizontal"
RenderTransformOrigin="0.444,0.5" Height="30"
VerticalAlignment="Top"
HorizontalAlignment="Left"
Width="180" Margin="70,1,0,0">
<Path xmlns:x=
"http://schemas.microsoft.com/winfx/2006/xaml"
x:Name="Cold" Fill="Black"
Height="15" Stretch="Fill"
Width="15" HorizontalAlignment="Left"
Stroke="#FFBFBFBF">
<Path.Data>
<PathGeometry
Figures="M 64.00 416.00l 96.00
96.00l 256.00-256.00L 160.00 0.00L
64.00 96.00l 160.00 160.00L 64.00 416.00z"
FillRule="NonZero" />
</Path.Data>
<Path.ToolTip>
<Border Background="#424242"
BorderBrush="Gray"
CornerRadius="3,3,3,3"
BorderThickness="2">
<StackPanel MaxWidth="300"
Margin="3">
<TextBlock Foreground="#FF00A2FF"
FontSize="14" Text="Cold state"
FontFamily="Segoe UI Semibold" />
<TextBlock Foreground="AliceBlue"
Text="This level of performance
indicates that your project
is running in the best possible state."
TextWrapping="Wrap" />
<TextBlock Foreground="Green"
Text="You don't need
to take any actions." />
</StackPanel>
</Border>
</Path.ToolTip>
</Path>
<Path xmlns:x=
"http://schemas.microsoft.com/winfx/2006/xaml"
x:Name="Cool"
Fill="#FF000000" Height="15"
Stretch="Fill" Width="15"
Margin="-4,0,0,0"
HorizontalAlignment="Left"
Stroke="#FFBFBFBF">
<Path.Data>
<PathGeometry
Figures="M 64.00 416.00l 96.00
96.00l 256.00-256.00L 160.00 0.00L
64.00 96.00l 160.00 160.00L 64.00 416.00z"
FillRule="NonZero" />
</Path.Data>
<Path.ToolTip>
<Border Background="#424242"
BorderBrush="Gray" CornerRadius="3,3,3,3"
BorderThickness="2">
<StackPanel MaxWidth="300"
Margin="3">
<TextBlock Foreground="#FF0074FF"
FontSize="14" Text="Cool state"
FontFamily="Segoe UI Semibold" />
<TextBlock Foreground="AliceBlue"
Text="This level of performance
indicates that your project is running
in the near-best possible state."
TextWrapping="Wrap" />
<TextBlock Foreground="Green"
Text="You don't need to
take any actions." />
</StackPanel>
</Border>
</Path.ToolTip>
</Path>
<Path xmlns:x=
"http://schemas.microsoft.com/winfx/2006/xaml"
x:Name="Good"
Fill="#FF000000" Height="15"
Stretch="Fill" Width="15"
Margin="-4,0,0,0"
HorizontalAlignment="Left"
Stroke="#FFBFBFBF">
<Path.Data>
<PathGeometry
Figures="M 64.00 416.00l 96.00
96.00l 256.00-256.00L 160.00 0.00L
64.00 96.00l 160.00 160.00L 64.00 416.00z"
FillRule="NonZero" />
</Path.Data>
<Path.ToolTip>
<Border Background="#424242"
BorderBrush="Gray" CornerRadius="3,3,3,3"
BorderThickness="2">
<StackPanel MaxWidth="300"
Margin="3">
<TextBlock Foreground="#FF0074FF"
FontSize="14" Text="Good state"
FontFamily="Segoe UI Semibold" />
<TextBlock Foreground="AliceBlue"
Text="This level of performance
indicates that your project is running
in the near-best possible state."
TextWrapping="Wrap" />
<TextBlock Foreground="Green"
Text="You don't need
to take any actions." />
</StackPanel>
</Border>
</Path.ToolTip>
</Path>
<Path xmlns:x=
"http://schemas.microsoft.com/winfx/2006/xaml"
x:Name="Normal"
Fill="#FF000000" Height="15"
Stretch="Fill" Width="15"
Margin="-4,0,0,0"
HorizontalAlignment="Left"
Stroke="#FFBFBFBF">
<Path.Data>
<PathGeometry
Figures="M 64.00 416.00l 96.00
96.00l 256.00-256.00L 160.00 0.00L
64.00 96.00l 160.00 160.00L 64.00 416.00z"
FillRule="NonZero" />
</Path.Data>
<Path.ToolTip>
<Border Background="#424242"
BorderBrush="Gray" CornerRadius="3,3,3,3"
BorderThickness="2">
<StackPanel MaxWidth="300"
Margin="3">
<TextBlock Foreground="WhiteSmoke"
FontSize="14" Text="Normal state"
FontFamily="Segoe UI Semibold" />
<TextBlock Foreground="AliceBlue"
Text="This level of performance
indicates that your project is
running in the normal state."
TextWrapping="Wrap" />
<TextBlock Foreground="Green"
Text="You don't need
to take any actions." />
</StackPanel>
</Border>
</Path.ToolTip>
</Path>
<Path xmlns:x=
"http://schemas.microsoft.com/winfx/2006/xaml"
x:Name="Heating"
Fill="#FF000000" Height="15"
Stretch="Fill" Width="15"
Margin="-4,0,0,0"
HorizontalAlignment="Left"
Stroke="#FFBFBFBF">
<Path.Data>
<PathGeometry
Figures="M 64.00 416.00l 96.00
96.00l 256.00-256.00L 160.00 0.00L
64.00 96.00l 160.00 160.00L 64.00 416.00z"
FillRule="NonZero" />
</Path.Data>
<Path.ToolTip>
<Border Background="#424242"
BorderBrush="Gray" CornerRadius="3,3,3,3"
BorderThickness="2">
<StackPanel MaxWidth="300"
Margin="3">
<TextBlock Foreground="#FFDC3E36"
FontSize="14" Text="Heating state"
FontFamily="Segoe UI Semibold" />
<TextBlock Foreground="AliceBlue"
Text="This level of performance
indicates that your project is consuming
too much RAM. You can take some actions
to reduce the amount of memory
your project is using."
TextWrapping="Wrap" />
<TextBlock Foreground="Gray"
Text="Reduce the number of nodes
you are using." />
<TextBlock Foreground="Gray"
Text="Divide your set of nodes
into multiple functions." />
<TextBlock Foreground="Gray"
Text="Load instructions
via the 'Load' node." />
<TextBlock Foreground="Gray"
Text="Write native code
instead on using nodes." />
</StackPanel>
</Border>
</Path.ToolTip>
</Path>
<Path x:Name="Hot"
Fill="#FF000000" Height="15"
Stretch="Fill" Width="15" Margin="-4,0,0,0"
HorizontalAlignment="Left" Stroke="#FFBFBFBF">
<Path.Data>
<PathGeometry
Figures="M 64.00 416.00l 96.00
96.00l 256.00-256.00L
160.00 0.00L 64.00 96.00l 160.00
160.00L 64.00 416.00z"
FillRule="NonZero" />
</Path.Data>
<Path.ToolTip>
<Border Background="#424242"
BorderBrush="Gray" CornerRadius="3,3,3,3"
BorderThickness="2">
<StackPanel MaxWidth="300" Margin="3">
<TextBlock Foreground="#FFDA0808"
FontSize="14" Text="Hot state"
FontFamily="Segoe UI Semibold" />
<TextBlock Foreground="AliceBlue"
Text="This level of performance
indicates that your project is consuming
too much RAM. You can take some actions
to reduce the amount of memory
your project is using."
TextWrapping="Wrap" />
<TextBlock Foreground="Gray"
Text="Reduce the number of nodes
you are using." />
<TextBlock Foreground="Gray"
Text="Divide your set of nodes
into multiple functions." />
<TextBlock Foreground="Gray"
Text="Load instructions
via the 'Load' node." />
<TextBlock Foreground="Gray"
Text="Write native code
instead on using nodes." />
</StackPanel>
</Border>
</Path.ToolTip>
</Path>
<Path xmlns:x=
"http://schemas.microsoft.com/winfx/2006/xaml"
x:Name="Risky"
Fill="#FF000000" Height="15"
Stretch="Fill" Width="15"
Margin="-4,0,0,0"
HorizontalAlignment="Left" Stroke="#FFBFBFBF">
<Path.Data>
<PathGeometry
Figures="M 64.00 416.00l 96.00
96.00l 256.00-256.00L 160.00 0.00L
64.00 96.00l 160.00 160.00L 64.00 416.00z"
FillRule="NonZero" />
</Path.Data>
<Path.ToolTip>
<Border Background="#424242"
BorderBrush="Gray" CornerRadius="3,3,3,3"
BorderThickness="2">
<StackPanel MaxWidth="300"
Margin="3">
<TextBlock Foreground="#FFF50909"
FontSize="14" Text="Risky state"
FontFamily="Segoe UI Semibold" />
<TextBlock Foreground="AliceBlue"
Text="This level of performance
indicates that your project is consuming
too much RAM. You can take some actions
to reduce the amount of memory
your project is using."
TextWrapping="Wrap" />
<TextBlock Foreground="Gray"
Text="Reduce the number of nodes
you are using." />
<TextBlock Foreground="Gray"
Text="Divide your set of nodes
into multiple functions." />
<TextBlock Foreground="Gray"
Text="Load instructions via the
'Load' node." />
<TextBlock Foreground="Gray"
Text="Write native code
instead on using nodes." />
</StackPanel>
</Border>
</Path.ToolTip>
</Path>
</StackPanel>
</Grid>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
The gauge has seven possible states when it comes to memory usage.
- Cold
- Cool
- Good
- Normal
- Heating
- Hot
- Risky
Note that the suggestions that show up when you use more memory resources are not actually applicable.
Code-behind:
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;
using System.Windows.Threading;
using VisualSR.Properties;
namespace VisualSR.Controls
{
public enum PerfomanceState
{
Cold,
Cool,
Good,
Normal,
Heating,
Hot,
Risky
}
public class PerformanceGauge : Control, INotifyPropertyChanged
{
private readonly DispatcherTimer _backgroundRamCollector = new DispatcherTimer();
private readonly PerformanceCounter _pc = new PerformanceCounter
{
CategoryName = "Process",
CounterName = "Working Set - Private"
};
private readonly Process _proc = Process.GetCurrentProcess();
private Path _cold;
private Path _cool;
private Color _coreColor;
private Path _good;
private Brush _heatColor;
private Path _heating;
private Path _hot;
private double _memsize;
private Path _normal;
private Path[] _paths;
private string _ram = "...";
private Path _risky;
public PerfomanceState States = PerfomanceState.Cold;
public PerformanceGauge()
{
Style = FindResource("Gauge") as Style;
ApplyTemplate();
Loaded += (s, e) =>
{
_cold = Template.FindName("Cold", this) as Path;
_cool = Template.FindName("Cool", this) as Path;
_good = Template.FindName("Good", this) as Path;
_normal = Template.FindName("Normal", this) as Path;
_heating = Template.FindName("Heating", this) as Path;
_hot = Template.FindName("Hot", this) as Path;
_risky = Template.FindName("Risky", this) as Path;
_paths = new[] {_cold, _cool, _good, _normal, _heating, _hot, _risky};
};
_backgroundRamCollector.Interval = new TimeSpan(0, 0, 0, 3);
_backgroundRamCollector.IsEnabled = true;
_backgroundRamCollector.Start();
_backgroundRamCollector.Tick += (ts, te) => CountRam();
}
public Color CoreColor
{
get { return _coreColor; }
set
{
_coreColor = value;
OnPropertyChanged();
}
}
public Brush HeatColor
{
get { return _heatColor; }
set
{
_heatColor = value;
OnPropertyChanged();
}
}
public string Ram
{
get { return _ram; }
set
{
_ram = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void ReBuild()
{
var x = _memsize;
if (x <= 200)
{
if (!Equals(HeatColor, Brushes.Cyan))
{
States = PerfomanceState.Cold;
HeatColor = Brushes.Cyan;
CoreColor = Colors.Cyan;
Highlighter(0, 1, HeatColor);
Highlighter(1, 7, Brushes.Black);
}
}
else if (x <= 300 && x > 200)
{
if (!Equals(HeatColor, Brushes.DarkCyan))
{
States = PerfomanceState.Cool;
HeatColor = Brushes.DarkCyan;
CoreColor = Colors.DarkCyan;
Highlighter(0, 2, HeatColor);
Highlighter(3, 7, Brushes.Black);
}
}
else if (x <= 600 && x > 300)
{
if (!Equals(HeatColor, Brushes.CadetBlue))
{
States = PerfomanceState.Good;
HeatColor = Brushes.CadetBlue;
CoreColor = Colors.CadetBlue;
Highlighter(0, 3, HeatColor);
Highlighter(4, 7, Brushes.Black);
}
}
else if (x <= 900 && x > 600)
{
if (!Equals(HeatColor, Brushes.ForestGreen))
{
States = PerfomanceState.Normal;
HeatColor = Brushes.ForestGreen;
CoreColor = Colors.ForestGreen;
Highlighter(0, 4, HeatColor);
Highlighter(5, 7, Brushes.Black);
}
}
else if (x <= 1500 && x > 900)
{
if (!Equals(HeatColor, Brushes.HotPink))
{
States = PerfomanceState.Heating;
HeatColor = Brushes.HotPink;
CoreColor = Colors.HotPink;
Highlighter(0, 5, HeatColor);
Highlighter(6, 7, Brushes.Black);
}
}
else if (x <= 2000 && x > 1500)
{
if (!Equals(HeatColor, Brushes.Firebrick))
{
States = PerfomanceState.Hot;
HeatColor = Brushes.Firebrick;
CoreColor = Colors.Firebrick;
Highlighter(0, 6, HeatColor);
Highlighter(7, 7, Brushes.Black);
}
}
else if (x > 3000)
{
if (!Equals(HeatColor, Brushes.Firebrick))
{
States = PerfomanceState.Risky;
HeatColor = Brushes.Red;
CoreColor = Colors.Red;
Highlighter(0, 7, HeatColor);
}
}
OnPropertyChanged();
}
private void CountRam()
{
Task.Factory.StartNew(() =>
{
Application.Current.Dispatcher.BeginInvoke
(DispatcherPriority.Background, new Action(() =>
{
_pc.InstanceName = _proc.ProcessName;
_memsize = Convert.ToDouble(_pc.NextValue() / 1048576);
Ram = _memsize.ToString("#.0") + " MB";
ReBuild();
}));
});
}
private void Highlighter(int begin, int end, Brush brush)
{
for (var i = begin; i < end; i++)
if (_paths[i] != null)
_paths[i].Fill = brush;
}
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged(string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Note
The gauge is simply made for fun-related and light reasons.
PerformanceGauge
Tools
Tools, some may call them utilities, are used to help the developer perform multiple tasks that are not actually intended to handle a specific task within the product- they are, in fact, used to handle multiple tasks.
Magic Laboratory
Nothing magical in here, it is just math. The lab of magic is a class that contains a lot of functions that result in animations, 2D transformations, 2D effects, etc.
The presence of the magic lab can be felt easily whenever you perform any operation. It supports mouse effects when you click on the virtual control, blurring the wires when you are trying to create another one, etc.
MagicLab
Cipher (Serialization) | Save/Load Operations
Working on a graph, then being unable to save it to work on it, later on, won't be a cool event. Thus, having the ability to store all your graphs, nodes and metadata in a way that would allow you to continue on them later is needed.
I have already written an ARTICLE about serialization and deserialization. It contains the ins-outs of the process, and it ends up with a valid de/serialization algorithm.
The approach of saving/loading I ended up with is based on saving the metadata that is related to every UIElement
that is being rendered- they type, title, contents (data, inputs, connections, etc.).
i.e., the following node:
will have its metadata converted into JSON:
{
"ExecutionConnectors":[
],
"Nodes":[
{
"InnerData":[
],
"InputData":[
"A lonely node."
],
"OutputData":[
],
"Name":"Nodes.Nodes.R.Basics.Print",
"Id":"a9c51559-2094-45be-8417-08873cec72a8",
"NodeType":"Method",
"X":154.00001899999998,
"Y":113
}
],
"ObjectConnectors":[
],
"SpaghettiDividers":[
]
}
Code-behind:
public static string SerializeToString<T>(T toSerialize)
{
return new JavaScriptSerializer().Serialize(toSerialize);
}
The function takes the object, which is the VirtualControl
, with all its contents, as a parameter in our case, and returns a JSON string
that can be eventually stored in a file.
The object that will be passed is a class that will represent all the contents of the VirtualControl
.
public class VirtualControlData
{
private readonly List<VolatileConnector> VolatileConnectors =
new List<VolatileConnector>();
public List<ExecutionConnectorProperties> ExecutionConnectors =
new List<ExecutionConnectorProperties>();
public List<NodeProperties> Nodes = new List<NodeProperties>();
public List<ObjectConnectorProperties> ObjectConnectors =
new List<ObjectConnectorProperties>();
public List<SpaghDividerProperties> SpaghettiDividers =
new List<SpaghDividerProperties>();
public VirtualControlData()
{
}
public VirtualControlData(VirtualControl vc)
{
for (var index = vc.Nodes.Count - 1; index >= 0; index--)
{
var node = vc.Nodes[index];
if (node.Types != NodeTypes.Root &&
node.Types != NodeTypes.SpaghettiDivider)
{
Nodes.Add(new NodeProperties(node));
}
else if (node.Types == NodeTypes.SpaghettiDivider)
{
SpaghettiDividers.Add(new SpaghDividerProperties(node));
VolatileConnectors.Add(new VolatileConnector
{
X = node.X,
Y = node.Y,
Connector = ((SpaghettiDivider) node).ObjectConnector
});
(node as SpaghettiDivider).Delete();
}
}
foreach (var conn in vc.ObjectConnectors)
ObjectConnectors.Add(new ObjectConnectorProperties(conn));
foreach (var conn in vc.ExecutionConnectors)
ExecutionConnectors.Add(new ExecutionConnectorProperties(conn));
foreach (var conn in VolatileConnectors)
{
var divider = new SpaghettiDivider(vc, conn.Connector, false);
for (var index = 0; index < vc.ObjectConnectors.Count; index++)
{
var c = vc.ObjectConnectors[index];
if (c == conn.Connector)
c.Delete();
}
conn.Connector.EndPort.ConnectedConnectors.ClearConnectors();
vc.AddNode(divider, conn.X, conn.Y);
}
}
private struct VolatileConnector
{
public double X;
public double Y;
public ObjectsConnector Connector;
}
}
The deserialization process does the exact opposite, it takes a string
as a parameter and returns an object based on the passed serialized data.
public static T DeSerializeFromString<T>(string data) where T : new()
{
return new JavaScriptSerializer().Deserialize<T>(data);
}
The serialization algorithm also takes care of other aspects, such as connectors, spaghetti dividers and comments.
For example, take this simple graph:
Here, we can see that there are more present elements compared to the other case. In here, we can observe the presence of two connectors, flowing data, and executable nodes.
{
"ExecutionConnectors":[
{
"StartNode_ID":"0",
"EndNode_ID":"a9c51559-2094-45be-8417-08873cec72a8",
"StartPort_Index":0,
"EndPort_Index":0
}
],
"Nodes":[
{
"InnerData":[
],
"InputData":[
],
"OutputData":[
"Not lonely anymore"
],
"Name":"Nodes.Nodes.Math.Vector1D",
"Id":"cb7e5497-2886-4a58-adab-487932c99464",
"NodeType":"Basic",
"X":414.0000399999999,
"Y":439
},
{
"InnerData":[
],
"InputData":[
"Not lonely anymore"
],
"OutputData":[
],
"Name":"Nodes.Nodes.R.Basics.Print",
"Id":"a9c51559-2094-45be-8417-08873cec72a8",
"NodeType":"Method",
"X":675.00006299999984,
"Y":362
}
],
"ObjectConnectors":[
{
"StartNode_ID":"cb7e5497-2886-4a58-adab-487932c99464",
"EndNode_ID":"a9c51559-2094-45be-8417-08873cec72a8",
"StartPort_Index":0,
"EndPort_Index":0
}
],
"SpaghettiDividers":[
]
}
Important Note
We can easily notice that the connectors somehow link to a node that has the ID 0. The weird part is that we have no data about any node that has the ID 0 in the array nodes in the JSON file.
Actually, that's because the node that we are referring to in this case is the start node. It is always there, it never vanishes and it gets created once we launch the visual scripting environment.
You cannot delete the Start node.
public override void Delete()
{
}
Other Tools
Other tools, such as the tools that are used to calculate units, measure memory usage, perform operations on texts and so on and so forth, do exist. However, we are not going to dig deep into them for them being generic ones.
Cipher
Plugins
Github reference: https://github.com/alaabenfatma/VisualSR/tree/master/Nodes
Plugins, true to their nature, are made to extend the capacities of the visual scripting environment by containing more tools and stuff which would give the user more control over the application.
The plugins do contain nodes, and nodes serve as tools.
To work with plugins in a relevant way, I used MEF, which is a framework that helps developers create their own extensions, plugins and add-ons.
For example, I made an extension that contains a node that can perform some OCR (Optical character recognition) operations.
To make such a thing, you first need to add the library as a referenced assembly and then set up the core of the node and build it up.
using System.ComponentModel.Composition;
using VisualSR.Controls;
using VisualSR.Core;
namespace Tesseract
{
[Export(typeof(Node))]
public class OCR : Node
{
private readonly UnrealControlsCollection.TextBox _tb =
new UnrealControlsCollection.TextBox();
private readonly VirtualControl Host;
private readonly UnrealControlsCollection.TextBox lang =
new UnrealControlsCollection.TextBox();
[ImportingConstructor]
public OCR([Import("host")] VirtualControl host,
[Import("bool")] bool spontaneousAddition = false) : base(
host, NodeTypes.Basic,
spontaneousAddition)
{
Title = "Optical character recognition - OCR";
Description = "Allows to convert scanned images,
faxes, screenshots, PDF documents and ebooks to text";
Host = host;
Width += 50;
Category = "Tesseract";
AddObjectPort(this, "File Path", PortTypes.Input,
RTypes.Character, false, _tb);
AddObjectPort(this, "Engine", PortTypes.Input,
RTypes.Character, false, lang);
AddObjectPort(this, " return", PortTypes.Output,
RTypes.Character, false);
_tb.TextChanged += (s, e) => { InputPorts[0].Data.Value = _tb.Text; };
InputPorts[0].DataChanged += (s, e) =>
{
if (_tb.Text != InputPorts[0].Data.Value)
_tb.Text = InputPorts[0].Data.Value;
GenerateCode();
};
InputPorts[1].DataChanged += (s, e) =>
{
if (lang.Text != InputPorts[2].Data.Value)
lang.Text = InputPorts[2].Data.Value;
GenerateCode();
};
Width = ActualWidth + 90;
}
public override string GenerateCode()
{
var value = InputPorts?[0].Data.Value;
OutputPorts[0].Data.Value = $"tesseract::ocr('{value}',
engine = tesseract('{InputPorts?[1].Data.Value}')";
return "# OCR Generation process, TARGET :" + value;
}
public override Node Clone()
{
var node = new OCR(Host);
return node;
}
}
}
The node actually generates an R code. The R code eventually makes use of the Tesseract Library of Google.
Adding a new plugins does not require the reboot of the visual scripting environment, the tree of nodes will check the presence and the absence of plugins each time it launches.
That said, if we delete the Tesseract plugins, and try to add it again, we won't be able to do so for it going to be unfound.
PluginsManager
Code Generation
Do you remember the fresh year computer science courses, where they spam your timetable with LinkedList
related classes? I bet you do. Well, here they do come in handy.
When we try to generate the whole code that will be eventually executed, we will go through every method node and extract its code.
For example, say we have this simple graph that is made of a combination of digits, and a plotting node:
Once we generate the code and compile this graph, we will end up with this generated code in R:
setwd("C:\\Users\\ABF\\Desktop\\demo\\")
plot(c(55,99,22,42))
NOTE: I chose the path. The setwd works as a directory path setter.
Eventually, the generated code will be sent to the R compiler that installed on your computer and will be compiled and consecutively executed.
Every node has a GenerateCode()
function that returns a string
that will be eventually appended to the main code that starts from the Start
node.
public static string Code(Node root)
{
var codeBuilder = new StringBuilder();
codeBuilder.Append(root.GenerateCode());
codeBuilder.AppendLine();
if (root.OutExecPorts.Count <= 0) return codeBuilder.ToString();
if (root.OutExecPorts[0].ConnectedConnectors.Count <= 0)
return codeBuilder.ToString();
var nextNode =
root.OutExecPorts[0].ConnectedConnectors[0].EndPort.ParentNode;
var stillHasMoreNodes = true;
while (stillHasMoreNodes)
{
codeBuilder.Append(nextNode.GenerateCode());
codeBuilder.AppendLine();
if (nextNode.OutExecPorts[0].ConnectedConnectors.Count > 0)
nextNode =
nextNode.OutExecPorts[0].ConnectedConnectors[0].EndPort.ParentNode;
else
stillHasMoreNodes = false;
}
return codeBuilder.ToString();
}
There are other few cases where the nodes are more complicated for them having multiple out-Execution ports. As an example, we can take the Loop
node.
Looking at this graph, we used a variable named i
, and we used it as a counter. Eventually, we did set up the range from 0 to 10 and did set up the instructions will be executed during the process of iteration from 0 to 10. The IDLE state refers to the instruction that will be executed once the loop is finished.
Final code:
setwd("C:\\Users\\ABF\\Desktop\\demo\\")
for(i in 0:10){
print(i)
}
print('It is over')
The magic behind the loop nodes is that they make use of the function that generates code out of a sequence of nodes by setting the first node that links to the instructions port a root
.
public override string GenerateCode()
{
var sb = new StringBuilder();
sb.Append($"for({InputPorts?[0].Data.Value} in
{InputPorts?[1].Data.Value})"+"{");
sb.AppendLine();
if (OutExecPorts[1].ConnectedConnectors.Count > 0)
sb.AppendLine(CodeMiner.Code(OutExecPorts[1].
ConnectedConnectors[0].EndPort.ParentNode));
sb.AppendLine();
sb.Append("}");
return sb.ToString();
}
Future
The visual scripting environment has been designed to help data scientists work on their project in a lighter environment, especially if they are not a big fan of coding.
The project will be distributed under the MIT license, and, hopefully, there will be a lot of contributions and updates to make it more stable and professional.
Important Notes
This project is not:
- commercial
The project is nothing but an experiment I started working on in high school.
- a company
It is a personal project, made it only for educational purposes.
- business-oriented
It is only for studies related purposes.
- profitable
The project is only for learning, it is a non-profit project. And it will never be profitable.
The author is not:
- responsible for any misuse of the project
- responsible for any damage caused by the project
History
- 14th August, 2018: Initial version