Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / artificial-intelligence / machine-learning

Full-Featured Visual Scripting Environment for R & Data Science

4.93/5 (30 votes)
6 Jun 2020MIT37 min read 43.9K   258  
A visual scripting environment for R & data science

Image 1

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.

Image 2Image 3Image 4Image 5

Image 6: 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.

Image 7: It refers to the technical-ish parts that include codes, links, explanations and tests.

Image 8: It refers to the summaries and conclusions. You will, almost, see this apple at the end of every section you read.

Image 9: It refers to the code on Github.

These magical icons have been illustrated by Anastasia.

Image 10 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.

Image 11 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.

Image 12 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.

Image 13 Architecture

The project is divided into multiple sections and slices, each part has its own merit and purpose.

Image 14 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.

Image 15 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.

Image 16 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 UIElements- 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:

Image 17

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.

C#
//Sets the Y value
Canvas.SetTop(box,1);

//Sets the X value
Canvas.SetLeft(box,2);

With all of that being said about the canvas and how it gives us the ability to move our UIElements 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:

C#
//Sets the Y value
Canvas.SetTop(box,0); 

//Sets the X value 
Canvas.SetLeft(box,1);

Then we will perceive this result:

Image 18

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.

Image 19

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.

Image 20 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:

Image 21

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:

Image 22

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.

Image 23 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:

Image 24

We won't be addressing its coordinates in this section of the article, they won't change.

Here is our box after zooming:

Image 25

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.

Image 26

Code:

C#
        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;
        }

Image 27 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.

XML
<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:

Image 28

Image 29Camera

Image 30 Children

Children are controls and UIElements 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.

Image 31 Nodes

Image 32

Image 33 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.

Image 34 Nodes: Template

Each node has its own style and theme; however, they do all derive from the same template.

Image 35

  • 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.

    • Control

      As we have already mentioned above, some input nodes may have custom controls to assure a better experience, here is an example of how I have used a CheckBox to create a logical node:

      Image 36

  • 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.

Image 37 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.

Image 38Nodes

Image 39 Ports

Image 40

Each port has only two possible states:

  • Linked
  • Unlinked

Image 41 Execution Ports

Image 42 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.

Image 43 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:

  • A pin
  • A TextBlock

The pin is a path.

Image 44

XML
<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.

Image 45Execution 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.

C#
//Adds an input execution port
AddExecPort(HelloNode, "port name", PortTypes.Input, "Text");
//Adds an output execution port
AddExecPort(HelloNode, "port name", PortTypes.Output, "Text");​​​​

Image 46 Object Ports

Image 47 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.

Image 48Object 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 Image 49
Logical Image 50
Numeric Image 51
Character Image 52
Array, Factor, List or Matrix Image 53
DataFrame Image 54

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.

Image 55 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.

C#
//Add input port
AddObjectPort(this, "some text", PortTypes.Input, RTypes.Generic, false);

//Add input port with the ability to connect with more than one port
AddObjectPort(this, "some text", PortTypes.Input, RTypes.Generic, true);

//Add output port
AddObjectPort(this, "some text", PortTypes.Output, RTypes.Generic, false);

//Add port with an inner control (a textbox)
var tb = new TextBox();
AddObjectPort(this, "There is a textbox inside of me", 
              PortTypes.Input, RTypes.Generic, false, tb);

Image 56 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.

Image 57Ports

Image 58 Connectors

Image 59

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.

Image 60

Image 61Connectors

Image 62 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.

Type Colour

Execution trigger

 
Generic Image 63
Logical Image 64
Numeric Image 65
Character Image 66
Array, Factor, List or Matrix Image 67

DataFrame

Image 68

Image 69Truth be told, the wires are just an implementation of a parametric curve called Bézier curve.

Image 70

(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 UIElements contained within our camera control themselves.

Image 71

We can observe that the Wire class has some public properties. These properties will be, eventually, used to visualize a curve.

Image 72The curve is nothing but a path that has to be visualized, hence a style had to be created to accomplish that purpose.

XML
<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>

Image 73Wires

Image 74 Execution Connectors

Image 75

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:

Image 76

Once we parse our graph, compile it and run it - this will be our final result:

Image 77

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.

Image 78 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.

    Image 79

  • 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.

    Image 80

Image 81 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:

Image 82

Image 83 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.

Image 84 Objects Connectors

Image 85

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.

Image 86 Objects Connectors: Linkability

  • Linkable Image 87

    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 Image 88

    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 Image 89

    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.

Image 90

Image 91 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:

Image 92

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.

Image 93

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.

Image 94 Spaghetti Divider: Operations

  • Create

    Image 95

  • 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.

    Image 96

Image 97 Objects Connectors: Flowing Data

Basically, this may be one of the most important parts of the project.

Image 98

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.

Image 99

Image 100 Virtual Control

Image 101 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 UIElements, in other words, handling them using the mechanism that the Camera is based on, is feasible.

Image 102 Implementation of the Camera Control

C#
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.

Image 103 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.

Image 104

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.

    • Image 105

      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.

      Image 106

  • 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.

    Image 107

Image 108 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.

C#
NodesManager.CreateExecutionConnector(Host, portA, portB); 

Deleting a connector is a less tedious process, just call the Delete(); method and see the magic.

C#
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.

C#
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;

Image 109Notes

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.

Image 110VirtualControl

Image 111 Comments

Image 112

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.

Image 113Technical Aspects

To find the genuine width and the height of the comment, we should call:

C#
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:

C#
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:

C#
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;
}

Image 114 Resize

Image 115

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.

C#
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;
}

Image 116Moving

Image 117

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.

C#
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);
}

Image 118Uncomment (Deleting the Comment)

Image 119

Being a UIElement, a comment can be duly deleted too when it is no more needed.

C#
comment.Dispose();

Image 120Style

The style of the comment is quite basic. It is composed of a textbox, borders and a handler.

XML
<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>

Image 121

Image 122The 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.

Image 123Comments

Image 124 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.

Image 125 Variables List

Image 126

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 Image 127into a graph representation.

Image 128

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.

Image 129Variables 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.

Image 130Variables 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.

Image 131

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

XML
    <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>

Image 132Variables 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):

Image 133

We should get these notes showing as tooltips:

Image 134 AND Image 135

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.

Image 136VariablesList

Image 137 Nodes Tree

Image 138

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 TreeViews. 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.

Image 139Nodes Tree: Template

Image 140

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

Image 141

  • 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.

Image 142Nodes Tree: Classification and Sorting

Image 143

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:

C#
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:

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

Image 144Nodes 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.

Image 145

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.

Image 146

Magically, the two insertion approaches do have the same code-behind:

C#
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:

C#
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.

Image 147

We are referring to the X & Y variables in this screenshot.

Image 148NodesTree

Image 149 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.

C#
public enum InnerMessageIcon
{
    Correct,
    Warning,
    False
}

Image 150InnerMessageBox

Image 151 Search Window

Image 152

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.

C#
   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.

C#
 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 UIElements that are rendered on the VirtualControl in order to put the selected node in the centre of the screen.

C#
 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);
}

Image 153

Image 154Search

Image 155 Contents Browser

The contents browser plays the role of a files explorer that is adaptive to the visual scripting environment.

Image 156

Image 157Overview

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.

Image 158 Template

The template is quite generic, and it is divided into three sections.

  1. 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.

  2. Folders explorer

    The folders explorer is a tree that represents all the folder within a specific path.

  3. 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. Image 159

    i.e.:

    Image 160

    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.

    1. Folder's head

      Image 161

      The folder, as we have already mentioned above, is divided into two components. The header of the folder and the core.

    2. Folder's core

      Image 162

      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:

      Image 163

      Image 164

  4. 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

XML
 <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):

XML
 <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):

XML
 <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>

Image 165 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.

Image 166

Image 167

Image 168ContentsBrowser

Image 169 Performance Gauge

Image 170

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

XML
  <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

    Image 171

  • Cool

    Image 172

  • Good

    Image 173

  • Normal

    Image 174

  • Heating

    Image 175

  • Hot

    Image 176

  • Risky

    Image 177

Note that the suggestions that show up when you use more memory resources are not actually applicable.

Code-behind:

C#
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(() =>
                {
                    //This code can be used to mimick a real-life experience
                    //memsize += 1;
                    //Ram = memsize + " MB";
                    //ReBuild();
                    _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));
        }
    }
}

Image 178Note

The gauge is simply made for fun-related and light reasons.

Image 179PerformanceGauge

Image 180 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.

Image 181 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.

Image 182MagicLab

Image 183 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:

Image 184

will have its metadata converted into JSON:

JavaScript
{
   "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:

C#
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.

C#
 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.

C#
 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:

Image 185

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.

JavaScript
{
   "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":[

   ]
}

Image 186 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.

C#
public override void Delete()
        {
            //Not a chance :-)
        }

Image 187 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.

Image 188Cipher

Image 189 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.

C#
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.

Image 190

Image 191

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.

Image 192PluginsManager

Image 193 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:

Image 194

Once we generate the code and compile this graph, we will end up with this generated code in R:

Python
#Artificial code. 
setwd("C:\\Users\\ABF\\Desktop\\demo\\")
#Generated a vector of characters : c(55,99,22,42)
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.

Image 195

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.

C#
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.

Image 196

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:

Python
#Artificial code. 
setwd("C:\\Users\\ABF\\Desktop\\demo\\")
for(i in 0:10){
print(i)
}
print('It is over') 

Image 197

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.

C#
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

License

This article, along with any associated source code and files, is licensed under The MIT License