commit 8abb5582f8e15f06407c698c10520af8745fb883
Author: Oto Šťáva <ostava@students.zcu.cz>
Date:   Wed Jun 2 18:15:12 2021 +0000

    Re #9001 - Launcher

diff --git a/deltarobot-vr-launcher/.gitignore b/deltarobot-vr-launcher/.gitignore
new file mode 100644
index 0000000..f838697
--- /dev/null
+++ b/deltarobot-vr-launcher/.gitignore
@@ -0,0 +1,4 @@
+obj/
+bin/
+
+.idea/
diff --git a/deltarobot-vr-launcher/DeltaRobotVr.Launcher/ConfigurationProvider.cs b/deltarobot-vr-launcher/DeltaRobotVr.Launcher/ConfigurationProvider.cs
new file mode 100644
index 0000000..751e038
--- /dev/null
+++ b/deltarobot-vr-launcher/DeltaRobotVr.Launcher/ConfigurationProvider.cs
@@ -0,0 +1,107 @@
+using System.Xml.Serialization;
+using System;
+using System.IO;
+
+namespace DeltaRobotVr.Launcher
+{
+    /// <summary>
+    /// Singleton class responsible for providing configuration.
+    /// </summary>
+    [Serializable]
+    public class ConfigurationProvider
+    {
+        private static readonly string DefaultConfigFile = "config.xml";
+        private static readonly string DefaultConfigDirectory = "/.deltarobot-vr/";
+        private static string _homePath;
+        private static string _configFile;
+        public static readonly ConfigurationProvider Instance;
+
+        public float VisualizationTranslateX = 0.0f;
+        public float VisualizationTranslateY = 0.2f;
+        public float VisualizationTranslateZ = -10.5f;
+
+
+        public float VisualizationScaleOverall = 0.2f;
+        public float VisualizationScaleX = 1.0f;
+        public float VisualizationScaleY = 1.0f;
+        public float VisualizationScaleZ = 1.0f;
+
+        public int Port = 4242;
+
+        public string Host = "192.168.254.16";
+
+        public string SceneName = "Village";
+
+        public string CurveColorHex = "#FFFF00CC";
+
+
+        static ConfigurationProvider()
+        {
+            if (Environment.OSVersion.Platform == PlatformID.Unix ||
+                   Environment.OSVersion.Platform == PlatformID.MacOSX)
+            {
+                _homePath = Environment.GetEnvironmentVariable("HOME");
+            }
+            else
+            {
+                _homePath = Environment.ExpandEnvironmentVariables("%HOMEDRIVE%%HOMEPATH%");
+            }
+
+            _configFile = _homePath + DefaultConfigDirectory + DefaultConfigFile;
+
+            if (!File.Exists(_configFile))
+            {
+                if (!Directory.Exists(_homePath + DefaultConfigDirectory))
+                {
+                    Directory.CreateDirectory(_homePath + DefaultConfigDirectory);
+                }
+                SaveConfiguration(new ConfigurationProvider(), _configFile);
+            }
+            
+            // Deserialize configuration
+            Instance = LoadConfiguration(_configFile);
+        }
+
+        private ConfigurationProvider()
+        {
+        }
+
+        /// <summary>
+        /// Load ConfigurationProvider instance from file.
+        /// </summary>
+        /// <param name="fileName">File to load configuration from</param>
+        /// <returns></returns>
+        private static ConfigurationProvider LoadConfiguration(String fileName)
+        {
+            XmlSerializer serializer = new XmlSerializer(typeof(ConfigurationProvider));
+
+            using (Stream reader = new FileStream(fileName, FileMode.Open))
+            {
+                return (ConfigurationProvider)serializer.Deserialize(reader);
+            }
+        }
+
+        /// <summary>
+        /// Save ConfigurationProvider instance to file.
+        /// </summary>
+        /// <param name="cp">ConfigurationProvider instance to save</param>
+        /// <param name="fileName">File to save configuration to</param>
+        private static void SaveConfiguration(ConfigurationProvider cp, String fileName)
+        {
+            XmlSerializer serializer = new XmlSerializer(typeof(ConfigurationProvider));
+
+            using (Stream writer = new FileStream(fileName, FileMode.Create))
+            {
+                serializer.Serialize(writer, cp);
+            }
+        }
+
+        /// <summary>
+        /// Saves this ConfigurationProvider instance to the global file.
+        /// </summary>
+        public void Save()
+        {
+            SaveConfiguration(this, _configFile);
+        }
+    }
+}
diff --git a/deltarobot-vr-launcher/DeltaRobotVr.Launcher/DeltaRobotVr.Launcher.csproj b/deltarobot-vr-launcher/DeltaRobotVr.Launcher/DeltaRobotVr.Launcher.csproj
new file mode 100644
index 0000000..1c98fa7
--- /dev/null
+++ b/deltarobot-vr-launcher/DeltaRobotVr.Launcher/DeltaRobotVr.Launcher.csproj
@@ -0,0 +1,58 @@
+﻿<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+    <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+    <PropertyGroup>
+        <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+        <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+        <ProjectGuid>{3B359CF2-C5D8-4466-983F-E0467CDB1144}</ProjectGuid>
+        <OutputType>Exe</OutputType>
+        <AppDesignerFolder>Properties</AppDesignerFolder>
+        <RootNamespace>DeltaRobotVr.Launcher</RootNamespace>
+        <AssemblyName>DeltaRobotVr.Launcher</AssemblyName>
+        <TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
+        <FileAlignment>512</FileAlignment>
+    </PropertyGroup>
+    <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+        <PlatformTarget>AnyCPU</PlatformTarget>
+        <DebugSymbols>true</DebugSymbols>
+        <DebugType>full</DebugType>
+        <Optimize>false</Optimize>
+        <OutputPath>bin\Debug\</OutputPath>
+        <DefineConstants>DEBUG;TRACE</DefineConstants>
+        <ErrorReport>prompt</ErrorReport>
+        <WarningLevel>4</WarningLevel>
+    </PropertyGroup>
+    <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+        <PlatformTarget>AnyCPU</PlatformTarget>
+        <DebugType>pdbonly</DebugType>
+        <Optimize>true</Optimize>
+        <OutputPath>bin\Release\</OutputPath>
+        <DefineConstants>TRACE</DefineConstants>
+        <ErrorReport>prompt</ErrorReport>
+        <WarningLevel>4</WarningLevel>
+    </PropertyGroup>
+    <ItemGroup>
+        <Reference Include="System" />
+        <Reference Include="System.Core" />
+        <Reference Include="System.Data" />
+        <Reference Include="System.Drawing" />
+        <Reference Include="System.Windows.Forms" />
+        <Reference Include="System.Xml" />
+    </ItemGroup>
+    <ItemGroup>
+        <Compile Include="ConfigurationProvider.cs" />
+        <Compile Include="LauncherForm.cs" />
+        <Compile Include="LayoutRectangle.cs" />
+        <Compile Include="Program.cs" />
+        <Compile Include="Properties\AssemblyInfo.cs" />
+    </ItemGroup>
+    <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+    <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
+         Other similar extension points exist, see Microsoft.Common.targets.
+    <Target Name="BeforeBuild">
+    </Target>
+    <Target Name="AfterBuild">
+    </Target>
+    -->
+
+</Project>
diff --git a/deltarobot-vr-launcher/DeltaRobotVr.Launcher/LauncherForm.cs b/deltarobot-vr-launcher/DeltaRobotVr.Launcher/LauncherForm.cs
new file mode 100644
index 0000000..3719a45
--- /dev/null
+++ b/deltarobot-vr-launcher/DeltaRobotVr.Launcher/LauncherForm.cs
@@ -0,0 +1,423 @@
+using System;
+using System.Diagnostics;
+using System.Drawing;
+using System.Globalization;
+using System.Windows.Forms;
+
+namespace DeltaRobotVr.Launcher
+{
+    public class LauncherForm : Form
+    {
+        private readonly Label _serverLabel = new Label();
+        private readonly Label _serverHostLabel = new Label();
+        private readonly TextBox _serverHost = new TextBox();
+        private readonly Label _serverPortLabel = new Label();
+        private readonly TextBox _serverPort = new TextBox();
+
+        private readonly Label _curveTranslationLabel = new Label();
+        private readonly Label _curveTranslationXLabel = new Label();
+        private readonly TextBox _curveTranslationX = new TextBox();
+        private readonly Label _curveTranslationYLabel = new Label();
+        private readonly TextBox _curveTranslationY = new TextBox();
+        private readonly Label _curveTranslationZLabel = new Label();
+        private readonly TextBox _curveTranslationZ = new TextBox();
+
+        private readonly Label _curveScaleLabel = new Label();
+        private readonly Label _curveScaleXLabel = new Label();
+        private readonly TextBox _curveScaleX = new TextBox();
+        private readonly Label _curveScaleYLabel = new Label();
+        private readonly TextBox _curveScaleY = new TextBox();
+        private readonly Label _curveScaleZLabel = new Label();
+        private readonly TextBox _curveScaleZ = new TextBox();
+        private readonly Label _curveScaleOverallLabel = new Label();
+        private readonly TextBox _curveScaleOverall = new TextBox();
+
+        private readonly Label _curveColorLabel = new Label();
+        private readonly TextBox _curveColorHex = new TextBox();
+        private readonly Panel _curveColorPreviewPanel = new Panel();
+        private readonly Label _curveColorAlpha = new Label();
+
+        private readonly Label _sceneLabel = new Label();
+        private readonly ComboBox _scene = new ComboBox();
+
+        private readonly Button _launchButton = new Button();
+
+        private static readonly string[] Scenes =
+        {
+            "BasicRoomScene",
+            "NeonRoom",
+            "Desert1",
+            "Desert2",
+            "Forest",
+            "SciFiRoom",
+            "PaintBooth",
+            "Village",
+            "CityStreetDay",
+            "CityStreetNight"
+        };
+
+        private const int Spacing = 6;
+        private const int SectionSpacing = 4 * Spacing;
+        private const int ControlHeight = 24;
+        private const int LeftLabelWidth = 120;
+
+        private const string VrAppExe = "deltarobot-vr.exe";
+
+        protected override void OnLoad(EventArgs e)
+        {
+            base.OnLoad(e);
+
+            Width = 640;
+            Height = 520;
+
+            Text = "Deltarobot VR Launcher";
+            
+            // Scene
+            _sceneLabel.Text = "Scene";
+            _sceneLabel.TextAlign = ContentAlignment.MiddleRight;
+            Controls.Add(_sceneLabel);
+            
+            // Curve color
+            _curveColorLabel.Text = "Curve color";
+            _curveColorLabel.TextAlign = ContentAlignment.MiddleRight;
+            Controls.Add(_curveColorLabel);
+
+            _curveColorHex.Text = ConfigurationProvider.Instance.CurveColorHex;
+            _curveColorHex.TextChanged += CurveColorHex_Changed;
+            Controls.Add(_curveColorHex);
+
+            Controls.Add(_curveColorPreviewPanel);
+
+            _curveColorAlpha.TextAlign = ContentAlignment.MiddleLeft;
+            Controls.Add(_curveColorAlpha);
+
+            UpdateCurveColor();
+
+            // Curve translation
+            _curveTranslationLabel.Text = "Curve translation";
+            _curveTranslationLabel.TextAlign = ContentAlignment.MiddleCenter;
+            Controls.Add(_curveTranslationLabel);
+
+            _curveTranslationXLabel.Text = "X (+ right; - left)";
+            _curveTranslationXLabel.TextAlign = ContentAlignment.MiddleCenter;
+            Controls.Add(_curveTranslationXLabel);
+
+            _curveTranslationX.Text = ConfigurationProvider.Instance.VisualizationTranslateX.ToString("F");
+            Controls.Add(_curveTranslationX);
+
+            _curveTranslationYLabel.Text = "Y (+ up; - down)";
+            _curveTranslationYLabel.TextAlign = ContentAlignment.MiddleCenter;
+            Controls.Add(_curveTranslationYLabel);
+
+            _curveTranslationY.Text = ConfigurationProvider.Instance.VisualizationTranslateY.ToString("F");
+            Controls.Add(_curveTranslationY);
+
+            _curveTranslationZLabel.Text = "Z (+ further; - closer)";
+            _curveTranslationZLabel.TextAlign = ContentAlignment.MiddleCenter;
+            Controls.Add(_curveTranslationZLabel);
+
+            _curveTranslationZ.Text = ConfigurationProvider.Instance.VisualizationTranslateZ.ToString("F");
+            Controls.Add(_curveTranslationZ);
+
+            // Curve scale
+            _curveScaleLabel.Text = "Curve scale";
+            _curveScaleLabel.TextAlign = ContentAlignment.MiddleCenter;
+            Controls.Add(_curveScaleLabel);
+
+            _curveScaleXLabel.Text = "X";
+            _curveScaleXLabel.TextAlign = ContentAlignment.MiddleCenter;
+            Controls.Add(_curveScaleXLabel);
+
+            _curveScaleX.Text = ConfigurationProvider.Instance.VisualizationScaleX.ToString("F");
+            Controls.Add(_curveScaleX);
+
+            _curveScaleYLabel.Text = "Y";
+            _curveScaleYLabel.TextAlign = ContentAlignment.MiddleCenter;
+            Controls.Add(_curveScaleYLabel);
+
+            _curveScaleY.Text = ConfigurationProvider.Instance.VisualizationScaleY.ToString("F");
+            Controls.Add(_curveScaleY);
+
+            _curveScaleZLabel.Text = "Z";
+            _curveScaleZLabel.TextAlign = ContentAlignment.MiddleCenter;
+            Controls.Add(_curveScaleZLabel);
+
+            _curveScaleZ.Text = ConfigurationProvider.Instance.VisualizationScaleZ.ToString("F");
+            Controls.Add(_curveScaleZ);
+
+            _curveScaleOverallLabel.Text = "Overall curve scale";
+            _curveScaleOverallLabel.TextAlign = ContentAlignment.MiddleCenter;
+            Controls.Add(_curveScaleOverallLabel);
+
+            _curveScaleOverall.Text = ConfigurationProvider.Instance.VisualizationScaleOverall.ToString("F");
+            Controls.Add(_curveScaleOverall);
+
+            // Server
+            _serverLabel.Text = "Exercise data server";
+            _serverLabel.TextAlign = ContentAlignment.MiddleCenter;
+            Controls.Add(_serverLabel);
+            
+            _serverHostLabel.Text = "Host";
+            _serverHostLabel.TextAlign = ContentAlignment.MiddleRight;
+            Controls.Add(_serverHostLabel);
+
+            _serverHost.Text = ConfigurationProvider.Instance.Host;
+            Controls.Add(_serverHost);
+            
+            _serverPortLabel.Text = "Port";
+            _serverPortLabel.TextAlign = ContentAlignment.MiddleRight;
+            Controls.Add(_serverPortLabel);
+
+            _serverPort.Text = ConfigurationProvider.Instance.Port.ToString();
+            Controls.Add(_serverPort);
+
+            foreach (var scene in Scenes)
+            {
+                _scene.Items.Add(scene);
+            }
+
+            _scene.Text = ConfigurationProvider.Instance.SceneName;
+            Controls.Add(_scene);
+
+            // Launch button
+            _launchButton.Text = "Save and Launch";
+            _launchButton.Click += LaunchButton_Click;
+            Controls.Add(_launchButton);
+
+            UpdateLayout();
+            CenterToScreen();
+        }
+
+        protected override void OnResize(EventArgs e)
+        {
+            base.OnResize(e);
+
+            UpdateLayout();
+        }
+
+        private void UpdateLayout()
+        {
+            var lr = new LayoutRectangle(ClientRectangle);
+            lr.Shrink(10);
+            
+            {
+                var rect = lr.RemoveFromTop(ControlHeight);
+                _sceneLabel.Bounds = rect.RemoveFromLeft(LeftLabelWidth).ToRectangle();
+                rect.RemoveFromLeft(Spacing);
+                _scene.Bounds = rect.ToRectangle();
+            }
+
+            lr.RemoveFromTop(Spacing);
+            
+            {
+                var rect = lr.RemoveFromTop(ControlHeight);
+                _curveColorLabel.Bounds = rect.RemoveFromLeft(LeftLabelWidth).ToRectangle();
+                rect.RemoveFromLeft(Spacing);
+                _curveColorHex.Bounds = rect.RemoveFromLeft(96).ToRectangle();
+                rect.RemoveFromLeft(Spacing);
+                _curveColorPreviewPanel.Bounds = rect.RemoveFromLeft(32).ToRectangle();
+                rect.RemoveFromLeft(Spacing);
+                _curveColorAlpha.Bounds = rect.ToRectangle();
+            }
+
+            lr.RemoveFromTop(SectionSpacing);
+
+            {
+                _curveTranslationLabel.Bounds = lr.RemoveFromTop(ControlHeight).ToRectangle();
+
+                var rect = lr.RemoveFromTop(2 * ControlHeight);
+
+                var third = rect.Width / 3;
+                var xRect = rect.RemoveFromLeft(third);
+                _curveTranslationXLabel.Bounds = xRect.RemoveFromTop(ControlHeight).ToRectangle();
+                _curveTranslationX.Bounds = xRect.ToRectangle();
+
+                var yRect = rect.RemoveFromLeft(third);
+                _curveTranslationYLabel.Bounds = yRect.RemoveFromTop(ControlHeight).ToRectangle();
+                _curveTranslationY.Bounds = yRect.ToRectangle();
+
+                var zRect = rect;
+                _curveTranslationZLabel.Bounds = zRect.RemoveFromTop(ControlHeight).ToRectangle();
+                _curveTranslationZ.Bounds = zRect.ToRectangle();
+            }
+
+            lr.RemoveFromTop(SectionSpacing);
+
+            {
+                _curveScaleLabel.Bounds = lr.RemoveFromTop(ControlHeight).ToRectangle();
+
+                var rect = lr.RemoveFromTop(4 * ControlHeight);
+
+                var third = rect.Width / 3;
+                var xRect = rect.RemoveFromLeft(third);
+                _curveScaleXLabel.Bounds = xRect.RemoveFromTop(ControlHeight).ToRectangle();
+                _curveScaleX.Bounds = xRect.ToRectangle();
+
+                var yRect = rect.RemoveFromLeft(third);
+                _curveScaleYLabel.Bounds = yRect.RemoveFromTop(ControlHeight).ToRectangle();
+                _curveScaleY.Bounds = yRect.RemoveFromTop(ControlHeight).ToRectangle();
+                _curveScaleOverallLabel.Bounds = yRect.RemoveFromTop(ControlHeight).ToRectangle();
+                _curveScaleOverall.Bounds = yRect.ToRectangle();
+
+                var zRect = rect;
+                _curveScaleZLabel.Bounds = zRect.RemoveFromTop(ControlHeight).ToRectangle();
+                _curveScaleZ.Bounds = zRect.ToRectangle();
+            }
+
+            lr.RemoveFromTop(SectionSpacing);
+
+            _serverLabel.Bounds = lr.RemoveFromTop(ControlHeight).ToRectangle();
+            
+            lr.RemoveFromTop(Spacing);
+            
+            {
+                var rect = lr.RemoveFromTop(ControlHeight);
+                _serverHostLabel.Bounds = rect.RemoveFromLeft(LeftLabelWidth).ToRectangle();
+                rect.RemoveFromLeft(Spacing);
+                _serverHost.Bounds = rect.RemoveFromLeft(250).ToRectangle();
+            }
+            
+            lr.RemoveFromTop(Spacing);
+            
+            {
+                var rect = lr.RemoveFromTop(ControlHeight);
+                _serverPortLabel.Bounds = rect.RemoveFromLeft(LeftLabelWidth).ToRectangle();
+                rect.RemoveFromLeft(Spacing);
+                _serverPort.Bounds = rect.RemoveFromLeft(64).ToRectangle();
+            }
+
+            _launchButton.Bounds = lr.RemoveFromBottom(ControlHeight * 2).Shrink(10).ToRectangle();
+        }
+
+        private void CurveColorHex_Changed(object sender, EventArgs args)
+        {
+            UpdateCurveColor();
+        }
+
+        private void LaunchButton_Click(object sender, EventArgs args)
+        {
+            SaveAndLaunch();
+        }
+
+        private void UpdateCurveColor()
+        {
+            bool valid = true;
+            int alpha = 255;
+            _curveColorAlpha.ForeColor = Color.Black;
+
+            var colorToParse = _curveColorHex.Text;
+            if (colorToParse.StartsWith("#"))
+            {
+                if (colorToParse.Length == 9)
+                {
+                    if (!int.TryParse(colorToParse.Substring(7, 2), NumberStyles.HexNumber, null, out alpha))
+                    {
+                        valid = false;
+                    }
+
+                    colorToParse = colorToParse.Substring(0, 7);
+                }
+                else if (colorToParse.Length != 7)
+                {
+                    valid = false;
+                }
+            }
+
+            if (valid)
+            {
+                try
+                {
+                    _curveColorPreviewPanel.BackColor = ColorTranslator.FromHtml(colorToParse);
+                }
+                catch (Exception)
+                {
+                    valid = false;
+                }
+            }
+
+            if (valid)
+            {
+                _curveColorAlpha.Text = $"(alpha: {alpha})";
+            }
+            else
+            {
+                _curveColorPreviewPanel.BackColor = Color.Fuchsia;
+                _curveColorAlpha.ForeColor = Color.Red;
+                _curveColorAlpha.Text = "(invalid)";
+            }
+        }
+
+        private void SaveAndLaunch()
+        {
+            ConfigurationProvider conf = ConfigurationProvider.Instance;
+            bool valid = true;
+
+            conf.Host = _serverHost.Text;
+            if (!int.TryParse(_serverPort.Text, out conf.Port))
+            {
+                MessageBox.Show(this, "Invalid port: must be a valid integer.");
+                valid = false;
+            }
+
+            if (!float.TryParse(_curveTranslationX.Text, out conf.VisualizationTranslateX))
+            {
+                MessageBox.Show(this, "Invalid curve translation X: must be valid number.");
+                valid = false;
+            }
+
+            if (!float.TryParse(_curveTranslationY.Text, out conf.VisualizationTranslateY))
+            {
+                MessageBox.Show(this, "Invalid curve translation Y: must be valid number.");
+                valid = false;
+            }
+
+            if (!float.TryParse(_curveTranslationZ.Text, out conf.VisualizationTranslateZ))
+            {
+                MessageBox.Show(this, "Invalid curve translation Z: must be valid number.");
+                valid = false;
+            }
+
+            if (!float.TryParse(_curveScaleX.Text, out conf.VisualizationScaleX))
+            {
+                MessageBox.Show(this, "Invalid curve scale X: must be valid number.");
+                valid = false;
+            }
+
+            if (!float.TryParse(_curveScaleY.Text, out conf.VisualizationScaleY))
+            {
+                MessageBox.Show(this, "Invalid curve scale Y: must be valid number.");
+                valid = false;
+            }
+
+            if (!float.TryParse(_curveScaleZ.Text, out conf.VisualizationScaleZ))
+            {
+                MessageBox.Show(this, "Invalid curve scale Z: must be valid number.");
+                valid = false;
+            }
+
+            if (!float.TryParse(_curveScaleOverall.Text, out conf.VisualizationScaleOverall))
+            {
+                MessageBox.Show(this, "Invalid overall curve scale: must be valid number.");
+                valid = false;
+            }
+
+            conf.SceneName = _scene.Text;
+
+            conf.CurveColorHex = _curveColorHex.Text;
+
+            if (valid)
+            {
+                conf.Save();
+
+                try
+                {
+                    Process.Start(VrAppExe);
+                }
+                catch (Exception e)
+                {
+                    MessageBox.Show(this, $"Could not launch: {e.Message}");
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/deltarobot-vr-launcher/DeltaRobotVr.Launcher/LayoutRectangle.cs b/deltarobot-vr-launcher/DeltaRobotVr.Launcher/LayoutRectangle.cs
new file mode 100644
index 0000000..f65b8d2
--- /dev/null
+++ b/deltarobot-vr-launcher/DeltaRobotVr.Launcher/LayoutRectangle.cs
@@ -0,0 +1,90 @@
+using System.Drawing;
+
+namespace DeltaRobotVr.Launcher
+{
+    
+    /// <summary>
+    /// A rectangle that allows creating layouts with subtractive methods.
+    /// </summary>
+    public struct LayoutRectangle
+    {
+        public int Top;
+        public int Right;
+        public int Bottom;
+        public int Left;
+
+        public int Width => Right - Left;
+        public int Height => Bottom - Top;
+
+        public LayoutRectangle(int top, int right, int bottom, int left)
+        {
+            Top = top;
+            Right = right;
+            Bottom = bottom;
+            Left = left;
+        }
+
+        public LayoutRectangle(Rectangle rect) : this(rect.Top, rect.Right, rect.Bottom, rect.Left)
+        {
+        }
+
+        public Rectangle ToRectangle() => new Rectangle(Left, Top, Right - Left, Bottom - Top);
+
+        public LayoutRectangle RemoveFromTop(int value)
+        {
+            if (value > Height)
+            {
+                value = Height;
+            }
+            
+            var result = new LayoutRectangle(Top, Right, Top + value, Left);
+            Top += value;
+            return result;
+        }
+
+        public LayoutRectangle RemoveFromRight(int value)
+        {
+            if (value > Width)
+            {
+                value = Width;
+            }
+            
+            var result = new LayoutRectangle(Top, Right, Bottom, Right - value);
+            Right -= value;
+            return result;
+        }
+
+        public LayoutRectangle RemoveFromBottom(int value)
+        {
+            if (value > Height)
+            {
+                value = Height;
+            }
+            
+            var result = new LayoutRectangle(Bottom - value, Right, Bottom, Left);
+            Bottom -= value;
+            return result;
+        }
+
+        public LayoutRectangle RemoveFromLeft(int value)
+        {
+            if (value > Width)
+            {
+                value = Width;
+            }
+            
+            var result = new LayoutRectangle(Top, Left + value, Bottom, Left);
+            Left += value;
+            return result;
+        }
+
+        public LayoutRectangle Shrink(int value)
+        {
+            Top += value;
+            Right -= value;
+            Bottom -= value;
+            Left += value;
+            return this;
+        }
+    }
+}
\ No newline at end of file
diff --git a/deltarobot-vr-launcher/DeltaRobotVr.Launcher/Program.cs b/deltarobot-vr-launcher/DeltaRobotVr.Launcher/Program.cs
new file mode 100644
index 0000000..5b82123
--- /dev/null
+++ b/deltarobot-vr-launcher/DeltaRobotVr.Launcher/Program.cs
@@ -0,0 +1,20 @@
+﻿using System.Globalization;
+using System.Threading;
+using System.Windows.Forms;
+
+namespace DeltaRobotVr.Launcher
+{
+    internal class Program
+    {
+        public static void Main()
+        {
+            CultureInfo cultureInfo = new CultureInfo("en-US");
+            Thread.CurrentThread.CurrentCulture = cultureInfo;
+            Thread.CurrentThread.CurrentUICulture = cultureInfo;
+            CultureInfo.DefaultThreadCurrentCulture = cultureInfo;
+            CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;
+            
+            Application.Run(new LauncherForm());
+        }
+    }
+}
\ No newline at end of file
diff --git a/deltarobot-vr-launcher/DeltaRobotVr.Launcher/Properties/AssemblyInfo.cs b/deltarobot-vr-launcher/DeltaRobotVr.Launcher/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..a3649ff
--- /dev/null
+++ b/deltarobot-vr-launcher/DeltaRobotVr.Launcher/Properties/AssemblyInfo.cs
@@ -0,0 +1,35 @@
+﻿using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following 
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("deltarobot_vr_launcher")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("deltarobot_vr_launcher")]
+[assembly: AssemblyCopyright("Copyright ©  2021")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible 
+// to COM components.  If you need to access a type in this assembly from 
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("3B359CF2-C5D8-4466-983F-E0467CDB1144")]
+
+// Version information for an assembly consists of the following four values:
+//
+//      Major Version
+//      Minor Version 
+//      Build Number
+//      Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers 
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
\ No newline at end of file
diff --git a/deltarobot-vr-launcher/deltarobot-vr-launcher.sln b/deltarobot-vr-launcher/deltarobot-vr-launcher.sln
new file mode 100644
index 0000000..65704b0
--- /dev/null
+++ b/deltarobot-vr-launcher/deltarobot-vr-launcher.sln
@@ -0,0 +1,16 @@
+﻿
+Microsoft Visual Studio Solution File, Format Version 12.00
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeltaRobotVr.Launcher", "DeltaRobotVr.Launcher\DeltaRobotVr.Launcher.csproj", "{3B359CF2-C5D8-4466-983F-E0467CDB1144}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{3B359CF2-C5D8-4466-983F-E0467CDB1144}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{3B359CF2-C5D8-4466-983F-E0467CDB1144}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{3B359CF2-C5D8-4466-983F-E0467CDB1144}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{3B359CF2-C5D8-4466-983F-E0467CDB1144}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+EndGlobal
diff --git a/deltarobot-vr/Assets/DeltaRobotVr/ConfigurationProvider.cs b/deltarobot-vr/Assets/DeltaRobotVr/ConfigurationProvider.cs
index efe56ad..298884d 100644
--- a/deltarobot-vr/Assets/DeltaRobotVr/ConfigurationProvider.cs
+++ b/deltarobot-vr/Assets/DeltaRobotVr/ConfigurationProvider.cs
@@ -55,11 +55,11 @@ namespace DeltaRobotVr
                 {
                     Directory.CreateDirectory(homePath + DefaultConfigDirectory);
                 }
-                saveConfiguration(new ConfigurationProvider(), configFile);
+                SaveConfiguration(new ConfigurationProvider(), configFile);
             }
             
             // Deserialize configuration
-            Instance = loadConfiguration(configFile);
+            Instance = LoadConfiguration(configFile);
         }
 
         private ConfigurationProvider()
@@ -71,7 +71,7 @@ namespace DeltaRobotVr
         /// </summary>
         /// <param name="fileName">File to load configuration from</param>
         /// <returns></returns>
-        private static ConfigurationProvider loadConfiguration(String fileName)
+        private static ConfigurationProvider LoadConfiguration(String fileName)
         {
             XmlSerializer serializer = new XmlSerializer(typeof(ConfigurationProvider));
 
@@ -90,7 +90,7 @@ namespace DeltaRobotVr
         /// </summary>
         /// <param name="cp">ConfigurationProvider instance to save</param>
         /// <param name="fileName">File to save configuration to</param>
-        private static void saveConfiguration(ConfigurationProvider cp, String fileName)
+        private static void SaveConfiguration(ConfigurationProvider cp, String fileName)
         {
             XmlSerializer serializer = new XmlSerializer(typeof(ConfigurationProvider));
 
