From a45dd094a08e7ee09d07d5e928fc756daae046c3 Mon Sep 17 00:00:00 2001 From: Darth Affe Date: Thu, 15 Jul 2021 00:20:08 +0200 Subject: [PATCH] Added first visual-scripting draft [WIP] --- src/Artemis.Core/VisualScripting/INode.cs | 26 + src/Artemis.Core/VisualScripting/IPin.cs | 20 + .../VisualScripting/IPinCollection.cs | 16 + src/Artemis.Core/VisualScripting/IScript.cs | 25 + .../VisualScripting/PinDirection.cs | 8 + src/Artemis.UI/App.xaml | 7 +- src/Artemis.UI/Artemis.UI.csproj | 1 + .../DisplayConditionsView.xaml | 7 +- .../DisplayConditionsViewModel.cs | 66 +- src/Artemis.UI/packages.lock.json | 12 + .../Artemis.VisualScripting.csproj | 52 + .../Attributes/UIAttribute.cs | 37 + .../Controls/VisualScriptCablePresenter.cs | 68 + .../Editor/Controls/VisualScriptEditor.cs | 41 + .../Controls/VisualScriptNodeCreationBox.cs | 115 ++ .../Controls/VisualScriptNodePresenter.cs | 137 ++ .../Controls/VisualScriptPinPresenter.cs | 184 +++ .../Editor/Controls/VisualScriptPresenter.cs | 437 ++++++ .../Editor/Controls/Wrapper/VisualScript.cs | 189 +++ .../Controls/Wrapper/VisualScriptCable.cs | 67 + .../Controls/Wrapper/VisualScriptNode.cs | 179 +++ .../Controls/Wrapper/VisualScriptPin.cs | 112 ++ .../Wrapper/VisualScriptPinCollection.cs | 58 + .../Editor/EditorStyles.xaml | 13 + .../Styles/VisualScriptCablePresenter.xaml | 68 + .../Editor/Styles/VisualScriptEditor.xaml | 28 + .../Styles/VisualScriptNodeCreationBox.xaml | 54 + .../Styles/VisualScriptNodePresenter.xaml | 262 ++++ .../Styles/VisualScriptPinPresenter.xaml | 113 ++ .../Editor/Styles/VisualScriptPresenter.xaml | 111 ++ .../Events/PinConnectedEventArgs.cs | 25 + .../Events/PinDisconnectedEventArgs.cs | 25 + .../VisualScriptNodeDragMovingEventArgs.cs | 24 + ...ualScriptNodeIsSelectedChangedEventArgs.cs | 24 + .../Internal/ExitNode.cs | 40 + .../Internal/IsConnectingPin.cs | 26 + src/Artemis.VisualScripting/Model/InputPin.cs | 51 + .../Model/InputPinCollection.cs | 35 + src/Artemis.VisualScripting/Model/Node.cs | 117 ++ src/Artemis.VisualScripting/Model/NodeData.cs | 38 + .../Model/OutputPin.cs | 41 + .../Model/OutputPinCollection.cs | 29 + src/Artemis.VisualScripting/Model/Pin.cs | 64 + .../Model/PinCollection.cs | 55 + src/Artemis.VisualScripting/Model/Script.cs | 91 ++ .../Nodes/BoolOperations.cs | 160 +++ .../Nodes/ConvertNodes.cs | 107 ++ .../Nodes/StaticValueNodes.cs | 121 ++ .../Nodes/StringFormatNode.cs | 40 + .../Nodes/Styles/StaticValueNodes.xaml | 12 + src/Artemis.VisualScripting/Nodes/SumNode.cs | 70 + .../Services/NodeService.cs | 59 + .../ViewModel/AbstractBindable.cs | 64 + .../ViewModel/ActionCommand.cs | 77 + .../packages.lock.json | 1246 +++++++++++++++++ src/Artemis.sln | 6 + 56 files changed, 5139 insertions(+), 21 deletions(-) create mode 100644 src/Artemis.Core/VisualScripting/INode.cs create mode 100644 src/Artemis.Core/VisualScripting/IPin.cs create mode 100644 src/Artemis.Core/VisualScripting/IPinCollection.cs create mode 100644 src/Artemis.Core/VisualScripting/IScript.cs create mode 100644 src/Artemis.Core/VisualScripting/PinDirection.cs create mode 100644 src/Artemis.VisualScripting/Artemis.VisualScripting.csproj create mode 100644 src/Artemis.VisualScripting/Attributes/UIAttribute.cs create mode 100644 src/Artemis.VisualScripting/Editor/Controls/VisualScriptCablePresenter.cs create mode 100644 src/Artemis.VisualScripting/Editor/Controls/VisualScriptEditor.cs create mode 100644 src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodeCreationBox.cs create mode 100644 src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodePresenter.cs create mode 100644 src/Artemis.VisualScripting/Editor/Controls/VisualScriptPinPresenter.cs create mode 100644 src/Artemis.VisualScripting/Editor/Controls/VisualScriptPresenter.cs create mode 100644 src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScript.cs create mode 100644 src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptCable.cs create mode 100644 src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptNode.cs create mode 100644 src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptPin.cs create mode 100644 src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptPinCollection.cs create mode 100644 src/Artemis.VisualScripting/Editor/EditorStyles.xaml create mode 100644 src/Artemis.VisualScripting/Editor/Styles/VisualScriptCablePresenter.xaml create mode 100644 src/Artemis.VisualScripting/Editor/Styles/VisualScriptEditor.xaml create mode 100644 src/Artemis.VisualScripting/Editor/Styles/VisualScriptNodeCreationBox.xaml create mode 100644 src/Artemis.VisualScripting/Editor/Styles/VisualScriptNodePresenter.xaml create mode 100644 src/Artemis.VisualScripting/Editor/Styles/VisualScriptPinPresenter.xaml create mode 100644 src/Artemis.VisualScripting/Editor/Styles/VisualScriptPresenter.xaml create mode 100644 src/Artemis.VisualScripting/Events/PinConnectedEventArgs.cs create mode 100644 src/Artemis.VisualScripting/Events/PinDisconnectedEventArgs.cs create mode 100644 src/Artemis.VisualScripting/Events/VisualScriptNodeDragMovingEventArgs.cs create mode 100644 src/Artemis.VisualScripting/Events/VisualScriptNodeIsSelectedChangedEventArgs.cs create mode 100644 src/Artemis.VisualScripting/Internal/ExitNode.cs create mode 100644 src/Artemis.VisualScripting/Internal/IsConnectingPin.cs create mode 100644 src/Artemis.VisualScripting/Model/InputPin.cs create mode 100644 src/Artemis.VisualScripting/Model/InputPinCollection.cs create mode 100644 src/Artemis.VisualScripting/Model/Node.cs create mode 100644 src/Artemis.VisualScripting/Model/NodeData.cs create mode 100644 src/Artemis.VisualScripting/Model/OutputPin.cs create mode 100644 src/Artemis.VisualScripting/Model/OutputPinCollection.cs create mode 100644 src/Artemis.VisualScripting/Model/Pin.cs create mode 100644 src/Artemis.VisualScripting/Model/PinCollection.cs create mode 100644 src/Artemis.VisualScripting/Model/Script.cs create mode 100644 src/Artemis.VisualScripting/Nodes/BoolOperations.cs create mode 100644 src/Artemis.VisualScripting/Nodes/ConvertNodes.cs create mode 100644 src/Artemis.VisualScripting/Nodes/StaticValueNodes.cs create mode 100644 src/Artemis.VisualScripting/Nodes/StringFormatNode.cs create mode 100644 src/Artemis.VisualScripting/Nodes/Styles/StaticValueNodes.xaml create mode 100644 src/Artemis.VisualScripting/Nodes/SumNode.cs create mode 100644 src/Artemis.VisualScripting/Services/NodeService.cs create mode 100644 src/Artemis.VisualScripting/ViewModel/AbstractBindable.cs create mode 100644 src/Artemis.VisualScripting/ViewModel/ActionCommand.cs create mode 100644 src/Artemis.VisualScripting/packages.lock.json diff --git a/src/Artemis.Core/VisualScripting/INode.cs b/src/Artemis.Core/VisualScripting/INode.cs new file mode 100644 index 000000000..d304f179e --- /dev/null +++ b/src/Artemis.Core/VisualScripting/INode.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; + +namespace Artemis.Core.VisualScripting +{ + public interface INode : INotifyPropertyChanged + { + string Name { get; } + string Description { get; } + + public double X { get; set; } + public double Y { get; set; } + + public IReadOnlyCollection Pins { get; } + public IReadOnlyCollection PinCollections { get; } + + public object CustomView { get; } + public object CustomViewModel { get; } + + event EventHandler Resetting; + + void Evaluate(); + void Reset(); + } +} diff --git a/src/Artemis.Core/VisualScripting/IPin.cs b/src/Artemis.Core/VisualScripting/IPin.cs new file mode 100644 index 000000000..9b2cb9245 --- /dev/null +++ b/src/Artemis.Core/VisualScripting/IPin.cs @@ -0,0 +1,20 @@ +using System; +using Artemis.VisualScripting.Model; + +namespace Artemis.Core.VisualScripting +{ + public interface IPin + { + INode Node { get; } + + string Name { get; } + PinDirection Direction { get; } + Type Type { get; } + object PinValue { get; } + + bool IsEvaluated { get; set; } + + void ConnectTo(IPin pin); + void DisconnectFrom(IPin pin); + } +} diff --git a/src/Artemis.Core/VisualScripting/IPinCollection.cs b/src/Artemis.Core/VisualScripting/IPinCollection.cs new file mode 100644 index 000000000..d9c2a1b33 --- /dev/null +++ b/src/Artemis.Core/VisualScripting/IPinCollection.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using Artemis.VisualScripting.Model; + +namespace Artemis.Core.VisualScripting +{ + public interface IPinCollection : IEnumerable + { + string Name { get; } + PinDirection Direction { get; } + Type Type { get; } + + IPin AddPin(); + bool Remove(IPin pin); + } +} diff --git a/src/Artemis.Core/VisualScripting/IScript.cs b/src/Artemis.Core/VisualScripting/IScript.cs new file mode 100644 index 000000000..abcc13516 --- /dev/null +++ b/src/Artemis.Core/VisualScripting/IScript.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; + +namespace Artemis.Core.VisualScripting +{ + public interface IScript : INotifyPropertyChanged, IDisposable + { + string Name { get; } + string Description { get; } + + IEnumerable Nodes { get; } + + Type ResultType { get; } + + void Run(); + void AddNode(INode node); + void RemoveNode(INode node); + } + + public interface IScript : IScript + { + T Result { get; } + } +} diff --git a/src/Artemis.Core/VisualScripting/PinDirection.cs b/src/Artemis.Core/VisualScripting/PinDirection.cs new file mode 100644 index 000000000..cc4b8500b --- /dev/null +++ b/src/Artemis.Core/VisualScripting/PinDirection.cs @@ -0,0 +1,8 @@ +namespace Artemis.VisualScripting.Model +{ + public enum PinDirection + { + Input, + Output + } +} diff --git a/src/Artemis.UI/App.xaml b/src/Artemis.UI/App.xaml index 1e96c39a4..f23a62a35 100644 --- a/src/Artemis.UI/App.xaml +++ b/src/Artemis.UI/App.xaml @@ -34,16 +34,19 @@ - + + + + - + pack://application:,,,/Resources/Fonts/#Roboto Mono diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj index 372979bc4..c7a744e8a 100644 --- a/src/Artemis.UI/Artemis.UI.csproj +++ b/src/Artemis.UI/Artemis.UI.csproj @@ -82,6 +82,7 @@ true + diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml index be801816d..0261dc31c 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml @@ -8,6 +8,7 @@ xmlns:converters="clr-namespace:Artemis.UI.Converters" xmlns:displayConditions="clr-namespace:Artemis.UI.Screens.ProfileEditor.DisplayConditions" xmlns:core="clr-namespace:Artemis.Core;assembly=Artemis.Core" + xmlns:controls="clr-namespace:Artemis.VisualScripting.Editor.Controls;assembly=Artemis.VisualScripting" x:Class="Artemis.UI.Screens.ProfileEditor.DisplayConditions.DisplayConditionsView" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800" @@ -40,7 +41,9 @@ - + + + diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs index fad56c3dc..472c38dc1 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs @@ -1,13 +1,13 @@ using System; using System.Collections.Generic; using System.ComponentModel; -using System.Linq; using Artemis.Core; -using Artemis.Core.Modules; using Artemis.UI.Ninject.Factories; using Artemis.UI.Screens.ProfileEditor.Conditions; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; +using Artemis.VisualScripting.Model; +using Artemis.VisualScripting.Services; using Stylet; namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions @@ -24,6 +24,35 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions { _profileEditorService = profileEditorService; _dataModelConditionsVmFactory = dataModelConditionsVmFactory; + + AvailableNodes = _nodeService.AvailableNodes; + Script = new Script("Display Condition (TODO)", "TODO"); + } + + #region TODO + + private static NodeService _nodeService; + + static DisplayConditionsViewModel() + { + _nodeService = new NodeService(); + _nodeService.InitializeNodes(); + } + + #endregion + + private Script _script; + public Script Script + { + get => _script; + private set => SetAndNotify(ref _script, value); + } + + private IEnumerable _availableNodes; + public IEnumerable AvailableNodes + { + get => _availableNodes; + set => SetAndNotify(ref _availableNodes, value); } public bool DisplayStartHint @@ -110,8 +139,8 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions { if (RenderProfileElement != null) { - RenderProfileElement.DisplayCondition.ChildAdded -= DisplayConditionOnChildrenModified; - RenderProfileElement.DisplayCondition.ChildRemoved -= DisplayConditionOnChildrenModified; + //RenderProfileElement.DisplayCondition.ChildAdded -= DisplayConditionOnChildrenModified; + //RenderProfileElement.DisplayCondition.ChildRemoved -= DisplayConditionOnChildrenModified; RenderProfileElement.Timeline.PropertyChanged -= TimelineOnPropertyChanged; } @@ -128,23 +157,26 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions } // Ensure the layer has a root display condition group + //if (renderProfileElement.DisplayCondition == null) + // renderProfileElement.DisplayCondition = new DataModelConditionGroup(null); + if (renderProfileElement.DisplayCondition == null) - renderProfileElement.DisplayCondition = new DataModelConditionGroup(null); + renderProfileElement.DisplayCondition = new Script("Display Condition (TODO)", "-"); - List modules = new(); - if (_profileEditorService.SelectedProfileConfiguration?.Module != null) - modules.Add(_profileEditorService.SelectedProfileConfiguration.Module); - ActiveItem = _dataModelConditionsVmFactory.DataModelConditionGroupViewModel(renderProfileElement.DisplayCondition, ConditionGroupType.General, modules); - ActiveItem.IsRootGroup = true; + //List modules = new(); + //if (_profileEditorService.SelectedProfileConfiguration?.Module != null) + // modules.Add(_profileEditorService.SelectedProfileConfiguration.Module); + //ActiveItem = _dataModelConditionsVmFactory.DataModelConditionGroupViewModel(renderProfileElement.DisplayCondition, ConditionGroupType.General, modules); + //ActiveItem.IsRootGroup = true; - DisplayStartHint = !RenderProfileElement.DisplayCondition.Children.Any(); - IsEventCondition = RenderProfileElement.DisplayCondition.Children.Any(c => c is DataModelConditionEvent); + //DisplayStartHint = !RenderProfileElement.DisplayCondition.Children.Any(); + //IsEventCondition = RenderProfileElement.DisplayCondition.Children.Any(c => c is DataModelConditionEvent); - RenderProfileElement.DisplayCondition.ChildAdded += DisplayConditionOnChildrenModified; - RenderProfileElement.DisplayCondition.ChildRemoved += DisplayConditionOnChildrenModified; + //RenderProfileElement.DisplayCondition.ChildAdded += DisplayConditionOnChildrenModified; + //RenderProfileElement.DisplayCondition.ChildRemoved += DisplayConditionOnChildrenModified; RenderProfileElement.Timeline.PropertyChanged += TimelineOnPropertyChanged; } - + private void TimelineOnPropertyChanged(object sender, PropertyChangedEventArgs e) { NotifyOfPropertyChange(nameof(DisplayContinuously)); @@ -154,8 +186,8 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions private void DisplayConditionOnChildrenModified(object sender, EventArgs e) { - DisplayStartHint = !RenderProfileElement.DisplayCondition.Children.Any(); - IsEventCondition = RenderProfileElement.DisplayCondition.Children.Any(c => c is DataModelConditionEvent); + //DisplayStartHint = !RenderProfileElement.DisplayCondition.Children.Any(); + //IsEventCondition = RenderProfileElement.DisplayCondition.Children.Any(c => c is DataModelConditionEvent); } public void EventTriggerModeSelected() diff --git a/src/Artemis.UI/packages.lock.json b/src/Artemis.UI/packages.lock.json index ef2b15f3e..88c76e6f1 100644 --- a/src/Artemis.UI/packages.lock.json +++ b/src/Artemis.UI/packages.lock.json @@ -247,6 +247,11 @@ "resolved": "2.1.0", "contentHash": "UTdxWvbgp2xzT1Ajaa2va+Qi3oNHJPasYmVhbKI2VVdu1VYP6yUG+RikhsHvpD7iM0S8e8UYb5Qm/LTWxx9QAA==" }, + "JetBrains.Annotations": { + "type": "Transitive", + "resolved": "2021.1.0", + "contentHash": "n9JSw5Z+F+6gp9vSv4aLH6p/bx3GAYA6FZVq1wJq/TJySv/kPgFKLGFeS7A8Xa5X4/GWorh5gd43yjamUgnBNA==" + }, "LiteDB": { "type": "Transitive", "resolved": "5.0.10", @@ -1472,6 +1477,13 @@ "System.Buffers": "4.5.1", "System.Numerics.Vectors": "4.5.0" } + }, + "artemis.visualscripting": { + "type": "Project", + "dependencies": { + "Artemis.Core": "1.0.0", + "JetBrains.Annotations": "2021.1.0" + } } } } diff --git a/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj b/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj new file mode 100644 index 000000000..86031c078 --- /dev/null +++ b/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj @@ -0,0 +1,52 @@ + + + net5.0-windows + false + false + Artemis.VisualScripting + Artemis Visual-Scripting + Copyright © Darth Affe - 2021 + bin\ + x64 + true + disable + latest + + + + x64 + bin\Artemis.VisualScripting.xml + + 5 + + + + 1.0-{chash:6} + true + true + true + v[0-9]* + true + git + true + + + + bin\Artemis.VisualScripting.xml + + + + + + + + + + + + + $(DefaultXamlRuntime) + Designer + + + \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Attributes/UIAttribute.cs b/src/Artemis.VisualScripting/Attributes/UIAttribute.cs new file mode 100644 index 000000000..86421c9d4 --- /dev/null +++ b/src/Artemis.VisualScripting/Attributes/UIAttribute.cs @@ -0,0 +1,37 @@ +using System; + +namespace Artemis.VisualScripting.Attributes +{ + public class UIAttribute : Attribute + { + #region Properties & Fields + + public string Name { get; } + public string Description { get; set; } + public string Category { get; set; } + + #endregion + + #region Constructors + + public UIAttribute(string name) + { + this.Name = name; + } + + public UIAttribute(string name, string description) + { + this.Name = name; + this.Description = description; + } + + public UIAttribute(string name, string description, string category) + { + this.Name = name; + this.Description = description; + this.Category = category; + } + + #endregion + } +} diff --git a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptCablePresenter.cs b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptCablePresenter.cs new file mode 100644 index 000000000..32de5d316 --- /dev/null +++ b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptCablePresenter.cs @@ -0,0 +1,68 @@ +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Shapes; +using Artemis.VisualScripting.Editor.Controls.Wrapper; + +namespace Artemis.VisualScripting.Editor.Controls +{ + [TemplatePart(Name = PART_PATH, Type = typeof(Path))] + public class VisualScriptCablePresenter : Control + { + #region Constants + + private const string PART_PATH = "PART_Path"; + + #endregion + + #region Properties & Fields + + private Path _path; + + #endregion + + #region Dependency Properties + + public static readonly DependencyProperty CableProperty = DependencyProperty.Register( + "Cable", typeof(VisualScriptCable), typeof(VisualScriptCablePresenter), new PropertyMetadata(default(VisualScriptCable))); + + public VisualScriptCable Cable + { + get => (VisualScriptCable)GetValue(CableProperty); + set => SetValue(CableProperty, value); + } + + public static readonly DependencyProperty ThicknessProperty = DependencyProperty.Register( + "Thickness", typeof(double), typeof(VisualScriptCablePresenter), new PropertyMetadata(default(double))); + + public double Thickness + { + get => (double)GetValue(ThicknessProperty); + set => SetValue(ThicknessProperty, value); + } + + #endregion + + #region Methods + + public override void OnApplyTemplate() + { + _path = GetTemplateChild(PART_PATH) as Path ?? throw new NullReferenceException($"The Path '{PART_PATH}' is missing."); + _path.MouseDown += OnPathMouseDown; + } + + private void OnPathMouseDown(object sender, MouseButtonEventArgs args) + { + if ((args.ChangedButton == MouseButton.Left) && (args.LeftButton == MouseButtonState.Pressed) && (args.ClickCount == 2)) + { + //TODO DarthAffe 17.06.2021: Should we add rerouting? + //AddRerouteNode(); + } + else if (args.ChangedButton == MouseButton.Middle) + Cable.Disconnect(); + } + + #endregion + } +} diff --git a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptEditor.cs b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptEditor.cs new file mode 100644 index 000000000..dad4ef715 --- /dev/null +++ b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptEditor.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.Windows; +using System.Windows.Controls; +using Artemis.Core.VisualScripting; +using Artemis.VisualScripting.Model; + +namespace Artemis.VisualScripting.Editor.Controls +{ + public class VisualScriptEditor : Control + { + #region Properties & Fields + + #endregion + + #region Dependency Properties + + public static readonly DependencyProperty ScriptProperty = DependencyProperty.Register( + "Script", typeof(IScript), typeof(VisualScriptEditor), new PropertyMetadata(default(IScript))); + + public IScript Script + { + get => (IScript)GetValue(ScriptProperty); + set => SetValue(ScriptProperty, value); + } + + public static readonly DependencyProperty AvailableNodesProperty = DependencyProperty.Register( + "AvailableNodes", typeof(IEnumerable), typeof(VisualScriptEditor), new PropertyMetadata(default(IEnumerable))); + + public IEnumerable AvailableNodes + { + get => (IEnumerable)GetValue(AvailableNodesProperty); + set => SetValue(AvailableNodesProperty, value); + } + + #endregion + + #region Methods + + #endregion + } +} diff --git a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodeCreationBox.cs b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodeCreationBox.cs new file mode 100644 index 000000000..6019a7811 --- /dev/null +++ b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodeCreationBox.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections; +using System.ComponentModel; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Input; +using Artemis.VisualScripting.Model; + +namespace Artemis.VisualScripting.Editor.Controls +{ + [TemplatePart(Name = PART_SEARCHBOX, Type = typeof(TextBox))] + [TemplatePart(Name = PART_CONTENT, Type = typeof(ListBox))] + public class VisualScriptNodeCreationBox : Control + { + #region Constants + + private const string PART_SEARCHBOX = "PART_SearchBox"; + private const string PART_CONTENT = "PART_Content"; + + #endregion + + #region Properties & Fields + + private TextBox _searchBox; + private ListBox _contentList; + + private CollectionViewSource _collectionViewSource; + private ICollectionView _contentView; + + #endregion + + #region DependencyProperties + + public static readonly DependencyProperty AvailableNodesProperty = DependencyProperty.Register( + "AvailableNodes", typeof(IEnumerable), typeof(VisualScriptNodeCreationBox), new PropertyMetadata(default(IEnumerable), OnItemsSourceChanged)); + + public IEnumerable AvailableNodes + { + get => (IEnumerable)GetValue(AvailableNodesProperty); + set => SetValue(AvailableNodesProperty, value); + } + + public static readonly DependencyProperty CreateNodeCommandProperty = DependencyProperty.Register( + "CreateNodeCommand", typeof(ICommand), typeof(VisualScriptNodeCreationBox), new PropertyMetadata(default(ICommand))); + + public ICommand CreateNodeCommand + { + get => (ICommand)GetValue(CreateNodeCommandProperty); + set => SetValue(CreateNodeCommandProperty, value); + } + + #endregion + + #region Methods + + public override void OnApplyTemplate() + { + _searchBox = GetTemplateChild(PART_SEARCHBOX) as TextBox ?? throw new NullReferenceException($"The Element '{PART_SEARCHBOX}' is missing."); + _contentList = GetTemplateChild(PART_CONTENT) as ListBox ?? throw new NullReferenceException($"The Element '{PART_CONTENT}' is missing."); + + _searchBox.TextChanged += OnSearchBoxTextChanged; + _contentList.IsSynchronizedWithCurrentItem = false; + _contentList.SelectionChanged += OnContentListSelectionChanged; + _contentList.SelectionMode = SelectionMode.Single; + + _searchBox.Focus(); + _contentView?.Refresh(); + ItemsSourceChanged(); + } + + private void OnSearchBoxTextChanged(object sender, TextChangedEventArgs args) + { + _contentView?.Refresh(); + } + + private void OnContentListSelectionChanged(object sender, SelectionChangedEventArgs args) + { + if ((args == null) || (_contentList?.SelectedItem == null)) return; + + CreateNodeCommand?.Execute(_contentList.SelectedItem); + } + + private bool Filter(object o) + { + if (_searchBox == null) return false; + if (o is not NodeData nodeData) return false; + + return nodeData.Name.Contains(_searchBox.Text, StringComparison.OrdinalIgnoreCase); + } + + private void ItemsSourceChanged() + { + if (_contentList == null) return; + + if (AvailableNodes == null) + { + _contentView = null; + _collectionViewSource = null; + } + else + { + _collectionViewSource = new CollectionViewSource { Source = AvailableNodes, SortDescriptions = { new SortDescription("Name", ListSortDirection.Ascending)}}; + _contentView = _collectionViewSource.View; + _contentView.Filter += Filter; + } + + _contentList.ItemsSource = _contentView; + } + + private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs args) => (d as VisualScriptNodeCreationBox)?.ItemsSourceChanged(); + + #endregion + } +} diff --git a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodePresenter.cs b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodePresenter.cs new file mode 100644 index 000000000..e79a195de --- /dev/null +++ b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodePresenter.cs @@ -0,0 +1,137 @@ +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using Artemis.VisualScripting.Editor.Controls.Wrapper; + +namespace Artemis.VisualScripting.Editor.Controls +{ + public class VisualScriptNodePresenter : Control + { + #region Properties & Fields + + private bool _isDragging; + private bool _isDragged; + private bool _isDeselected; + private Point _dragStartPosition; + + #endregion + + #region Dependency Properties + + public static readonly DependencyProperty NodeProperty = DependencyProperty.Register( + "Node", typeof(VisualScriptNode), typeof(VisualScriptNodePresenter), new PropertyMetadata(default(VisualScriptNode))); + + public VisualScriptNode Node + { + get => (VisualScriptNode)GetValue(NodeProperty); + set => SetValue(NodeProperty, value); + } + + public static readonly DependencyProperty TitleBrushProperty = DependencyProperty.Register( + "TitleBrush", typeof(Brush), typeof(VisualScriptNodePresenter), new PropertyMetadata(default(Brush))); + + public Brush TitleBrush + { + get => (Brush)GetValue(TitleBrushProperty); + set => SetValue(TitleBrushProperty, value); + } + + #endregion + + #region Methods + + protected override Size MeasureOverride(Size constraint) + { + int SnapToGridSize(int value) + { + int mod = value % Node.Script.GridSize; + return mod switch + { + < 0 => Node.Script.GridSize, + > 0 => value + (Node.Script.GridSize - mod), + _ => value + }; + } + + Size neededSize = base.MeasureOverride(constraint); + int width = (int)Math.Ceiling(neededSize.Width); + int height = (int)Math.Ceiling(neededSize.Height); + + return new Size(SnapToGridSize(width), SnapToGridSize(height)); + } + + protected override void OnMouseLeftButtonDown(MouseButtonEventArgs args) + { + base.OnMouseLeftButtonDown(args); + + _isDragged = false; + _isDragging = true; + _isDeselected = false; + + bool isShiftDown = Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift); + if (!Node.IsSelected) + Node.Select(isShiftDown); + else if (isShiftDown) + { + _isDeselected = true; + Node.Deselect(true); + } + + Node.DragStart(); + + _dragStartPosition = PointToScreen(args.GetPosition(this)); + + CaptureMouse(); + args.Handled = true; + } + + protected override void OnMouseLeftButtonUp(MouseButtonEventArgs args) + { + base.OnMouseLeftButtonUp(args); + + if (_isDragging) + { + _isDragging = false; + Node.DragEnd(); + + ReleaseMouseCapture(); + } + + if (!_isDragged && !_isDeselected) + Node.Select(Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift)); + + args.Handled = true; + } + + protected override void OnMouseMove(MouseEventArgs args) + { + base.OnMouseMove(args); + + if (!_isDragging) return; + + Point mousePosition = PointToScreen(args.GetPosition(this)); + Vector offset = mousePosition - _dragStartPosition; + + if (args.LeftButton == MouseButtonState.Pressed) + { + _dragStartPosition = mousePosition; + Node.DragMove(offset.X, offset.Y); + + if ((offset.X != 0) && (offset.Y != 0)) + _isDragged = true; + } + else + { + _isDragging = false; + Node.DragEnd(); + ReleaseMouseCapture(); + } + + args.Handled = true; + } + + #endregion + } +} diff --git a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPinPresenter.cs b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPinPresenter.cs new file mode 100644 index 000000000..75931bc08 --- /dev/null +++ b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPinPresenter.cs @@ -0,0 +1,184 @@ +using System; +using System.ComponentModel; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using Artemis.VisualScripting.Editor.Controls.Wrapper; +using Artemis.VisualScripting.Model; + +namespace Artemis.VisualScripting.Editor.Controls +{ + [TemplatePart(Name = PART_DOT, Type = typeof(FrameworkElement))] + public class VisualScriptPinPresenter : Control + { + #region Constants + + private const string PART_DOT = "PART_Dot"; + + #endregion + + #region Properties & Fields + + private VisualScriptNodePresenter _nodePresenter; + private FrameworkElement _dot; + + #endregion + + #region Dependency Properties + + public static readonly DependencyProperty PinProperty = DependencyProperty.Register( + "Pin", typeof(VisualScriptPin), typeof(VisualScriptPinPresenter), new PropertyMetadata(default(VisualScriptPin), PinChanged)); + + public VisualScriptPin Pin + { + get => (VisualScriptPin)GetValue(PinProperty); + set => SetValue(PinProperty, value); + } + + #endregion + + #region Constructors + + public VisualScriptPinPresenter() + { + Loaded += OnLoaded; + Unloaded += OnUnloaded; + } + + #endregion + + #region Methods + + private void OnLoaded(object sender, RoutedEventArgs args) + { + DependencyObject parent = this; + while ((parent = VisualTreeHelper.GetParent(parent)) != null) + { + if (parent is VisualScriptNodePresenter nodePresenter) + { + _nodePresenter = nodePresenter; + break; + } + } + + LayoutUpdated += OnLayoutUpdated; + + UpdateAbsoluteLocation(); + } + + private void OnUnloaded(object sender, RoutedEventArgs e) + { + LayoutUpdated -= OnLayoutUpdated; + _nodePresenter = null; + } + + private void OnNodePropertyChanged(object sender, PropertyChangedEventArgs e) + { + UpdateAbsoluteLocation(); + } + + private void OnLayoutUpdated(object sender, EventArgs args) + { + _dot = GetTemplateChild(PART_DOT) as FrameworkElement ?? throw new NullReferenceException($"The Element '{PART_DOT}' is missing."); + + _dot.AllowDrop = true; + + _dot.MouseDown += OnDotMouseDown; + _dot.MouseMove += OnDotMouseMove; + _dot.Drop += OnDotDrop; + _dot.DragEnter += OnDotDrag; + _dot.DragOver += OnDotDrag; + + UpdateAbsoluteLocation(); + } + + private static void PinChanged(DependencyObject d, DependencyPropertyChangedEventArgs args) + { + if (d is not VisualScriptPinPresenter presenter) return; + + presenter.PinChanged(args); + } + + private void PinChanged(DependencyPropertyChangedEventArgs args) + { + if (args.OldValue is VisualScriptPin oldPin) + oldPin.Node.Node.PropertyChanged -= OnNodePropertyChanged; + + if (args.NewValue is VisualScriptPin newPin) + newPin.Node.Node.PropertyChanged += OnNodePropertyChanged; + + UpdateAbsoluteLocation(); + } + + private void UpdateAbsoluteLocation() + { + if ((Pin == null) || (_nodePresenter == null)) return; + + try + { + double circleRadius = ActualHeight / 2.0; + double xOffset = Pin.Pin.Direction == PinDirection.Input ? circleRadius : ActualWidth - circleRadius; + Point relativePosition = this.TransformToVisual(_nodePresenter).Transform(new Point(xOffset, circleRadius)); + Pin.AbsolutePosition = new Point(Pin.Node.X + relativePosition.X, Pin.Node.Y + relativePosition.Y); + } + catch + { + Pin.AbsolutePosition = new Point(0, 0); + } + } + + private void OnDotMouseDown(object sender, MouseButtonEventArgs args) + { + if (args.ChangedButton == MouseButton.Middle) + Pin.DisconnectAll(); + + args.Handled = true; + } + + private void OnDotMouseMove(object sender, MouseEventArgs args) + { + if (args.LeftButton == MouseButtonState.Pressed) + { + Pin.SetConnecting(true); + DragDrop.DoDragDrop(this, Pin, DragDropEffects.Link); + Pin.SetConnecting(false); + + args.Handled = true; + } + } + + private void OnDotDrag(object sender, DragEventArgs args) + { + if (!args.Data.GetDataPresent(typeof(VisualScriptPin))) + args.Effects = DragDropEffects.None; + else + { + VisualScriptPin sourcePin = (VisualScriptPin)args.Data.GetData(typeof(VisualScriptPin)); + if (sourcePin == null) + args.Effects = DragDropEffects.None; + else + args.Effects = ((sourcePin.Pin.Direction != Pin.Pin.Direction) && (sourcePin.Pin.Node != Pin.Pin.Node) && IsTypeCompatible(sourcePin.Pin.Type)) ? DragDropEffects.Link : DragDropEffects.None; + } + + if (args.Effects == DragDropEffects.None) + args.Handled = true; + } + + private void OnDotDrop(object sender, DragEventArgs args) + { + if (!args.Data.GetDataPresent(typeof(VisualScriptPin))) return; + + VisualScriptPin sourcePin = (VisualScriptPin)args.Data.GetData(typeof(VisualScriptPin)); + if ((sourcePin == null) || !IsTypeCompatible(sourcePin.Pin.Type)) return; + + try { new VisualScriptCable(Pin, sourcePin); } catch { /**/ } + + args.Handled = true; + } + + private bool IsTypeCompatible(Type type) => ((Pin.Pin.Direction == PinDirection.Input) && (Pin.Pin.Type == typeof(object))) || (Pin.Pin.Type == type); + + #endregion + } +} diff --git a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPresenter.cs b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPresenter.cs new file mode 100644 index 000000000..327c96be9 --- /dev/null +++ b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPresenter.cs @@ -0,0 +1,437 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using Artemis.Core.VisualScripting; +using Artemis.VisualScripting.Editor.Controls.Wrapper; +using Artemis.VisualScripting.Model; +using Artemis.VisualScripting.ViewModel; + +namespace Artemis.VisualScripting.Editor.Controls +{ + [TemplatePart(Name = PART_CANVAS, Type = typeof(Canvas))] + [TemplatePart(Name = PART_NODELIST, Type = typeof(ItemsControl))] + [TemplatePart(Name = PART_CABLELIST, Type = typeof(ItemsControl))] + [TemplatePart(Name = PART_SELECTION_BORDER, Type = typeof(Border))] + [TemplatePart(Name = PART_CREATION_BOX_PARENT, Type = typeof(Panel))] + public class VisualScriptPresenter : Control + { + #region Constants + + private const string PART_CANVAS = "PART_Canvas"; + private const string PART_NODELIST = "PART_NodeList"; + private const string PART_CABLELIST = "PART_CableList"; + private const string PART_SELECTION_BORDER = "PART_SelectionBorder"; + private const string PART_CREATION_BOX_PARENT = "PART_CreationBoxParent"; + + #endregion + + #region Properties & Fields + + private Canvas _canvas; + private ItemsControl _nodeList; + private ItemsControl _cableList; + private Border _selectionBorder; + private TranslateTransform _canvasViewPortTransform; + private Panel _creationBoxParent; + + private Vector _viewportCenter = new(0, 0); + + private bool _dragCanvas = false; + private Point _dragCanvasStartLocation; + private Vector _dragCanvasStartOffset; + private bool _movedDuringDrag = false; + + private bool _boxSelect = false; + private Point _boxSelectStartPoint; + + private Point _lastRightClickLocation; + + internal VisualScript VisualScript { get; private set; } + + #endregion + + #region Dependency Properties + + public static readonly DependencyProperty ScriptProperty = DependencyProperty.Register( + "Script", typeof(IScript), typeof(VisualScriptPresenter), new PropertyMetadata(default(IScript), ScriptChanged)); + + public IScript Script + { + get => (IScript)GetValue(ScriptProperty); + set => SetValue(ScriptProperty, value); + } + + public static readonly DependencyProperty ScaleProperty = DependencyProperty.Register( + "Scale", typeof(double), typeof(VisualScriptPresenter), new PropertyMetadata(1.0, ScaleChanged)); + + public double Scale + { + get => (double)GetValue(ScaleProperty); + set => SetValue(ScaleProperty, value); + } + + public static readonly DependencyProperty MinScaleProperty = DependencyProperty.Register( + "MinScale", typeof(double), typeof(VisualScriptPresenter), new PropertyMetadata(0.15)); + + public double MinScale + { + get => (double)GetValue(MinScaleProperty); + set => SetValue(MinScaleProperty, value); + } + + public static readonly DependencyProperty MaxScaleProperty = DependencyProperty.Register( + "MaxScale", typeof(double), typeof(VisualScriptPresenter), new PropertyMetadata(1.0)); + + public double MaxScale + { + get => (double)GetValue(MaxScaleProperty); + set => SetValue(MaxScaleProperty, value); + } + + public static readonly DependencyProperty ScaleFactorProperty = DependencyProperty.Register( + "ScaleFactor", typeof(double), typeof(VisualScriptPresenter), new PropertyMetadata(1.3)); + + public double ScaleFactor + { + get => (double)GetValue(ScaleFactorProperty); + set => SetValue(ScaleFactorProperty, value); + } + + public static readonly DependencyProperty AvailableNodesProperty = DependencyProperty.Register( + "AvailableNodes", typeof(IEnumerable), typeof(VisualScriptPresenter), new PropertyMetadata(default(IEnumerable))); + + public IEnumerable AvailableNodes + { + get => (IEnumerable)GetValue(AvailableNodesProperty); + set => SetValue(AvailableNodesProperty, value); + } + + public static readonly DependencyProperty CreateNodeCommandProperty = DependencyProperty.Register( + "CreateNodeCommand", typeof(ICommand), typeof(VisualScriptPresenter), new PropertyMetadata(default(ICommand))); + + public ICommand CreateNodeCommand + { + get => (ICommand)GetValue(CreateNodeCommandProperty); + private set => SetValue(CreateNodeCommandProperty, value); + } + + public static readonly DependencyProperty GridSizeProperty = DependencyProperty.Register( + "GridSize", typeof(int), typeof(VisualScriptPresenter), new PropertyMetadata(24)); + + public int GridSize + { + get => (int)GetValue(GridSizeProperty); + set => SetValue(GridSizeProperty, value); + } + + public static readonly DependencyProperty SurfaceSizeProperty = DependencyProperty.Register( + "SurfaceSize", typeof(int), typeof(VisualScriptPresenter), new PropertyMetadata(16384)); + + public int SurfaceSize + { + get => (int)GetValue(SurfaceSizeProperty); + set => SetValue(SurfaceSizeProperty, value); + } + + #endregion + + #region Constructors + + public VisualScriptPresenter() + { + CreateNodeCommand = new ActionCommand(CreateNode); + + this.SizeChanged += OnSizeChanged; + } + + #endregion + + #region Methods + + public override void OnApplyTemplate() + { + _canvas = GetTemplateChild(PART_CANVAS) as Canvas ?? throw new NullReferenceException($"The Canvas '{PART_CANVAS}' is missing."); + _selectionBorder = GetTemplateChild(PART_SELECTION_BORDER) as Border ?? throw new NullReferenceException($"The Border '{PART_SELECTION_BORDER}' is missing."); + _nodeList = GetTemplateChild(PART_NODELIST) as ItemsControl ?? throw new NullReferenceException($"The ItemsControl '{PART_NODELIST}' is missing."); + _cableList = GetTemplateChild(PART_CABLELIST) as ItemsControl ?? throw new NullReferenceException($"The ItemsControl '{PART_CABLELIST}' is missing."); + _creationBoxParent = GetTemplateChild(PART_CREATION_BOX_PARENT) as Panel ?? throw new NullReferenceException($"The Panel '{PART_CREATION_BOX_PARENT}' is missing."); + + _canvas.AllowDrop = true; + + _canvas.RenderTransform = _canvasViewPortTransform = new TranslateTransform(0, 0); + _canvas.MouseLeftButtonDown += OnCanvasMouseLeftButtonDown; + _canvas.MouseLeftButtonUp += OnCanvasMouseLeftButtonUp; + _canvas.MouseRightButtonDown += OnCanvasMouseRightButtonDown; + _canvas.MouseRightButtonUp += OnCanvasMouseRightButtonUp; + _canvas.PreviewMouseRightButtonDown += OnCanvasPreviewMouseRightButtonDown; + _canvas.MouseMove += OnCanvasMouseMove; + _canvas.MouseWheel += OnCanvasMouseWheel; + _canvas.DragOver += OnCanvasDragOver; + + _nodeList.ItemsSource = VisualScript?.Nodes; + _cableList.ItemsSource = VisualScript?.Cables; + } + + private void OnSizeChanged(object sender, SizeChangedEventArgs args) + { + if (sender is not VisualScriptPresenter scriptPresenter) return; + + scriptPresenter.UpdatePanning(); + } + + private static void ScriptChanged(DependencyObject d, DependencyPropertyChangedEventArgs args) + { + if (d is not VisualScriptPresenter scriptPresenter) return; + + scriptPresenter.ScriptChanged(args.NewValue is not Script script ? null : new VisualScript(script, scriptPresenter.SurfaceSize, scriptPresenter.GridSize)); + } + + private void ScriptChanged(VisualScript newScript) + { + if (VisualScript != null) + VisualScript.PropertyChanged -= OnVisualScriptPropertyChanged; + + VisualScript = newScript; + + if (VisualScript != null) + { + VisualScript.PropertyChanged += OnVisualScriptPropertyChanged; + + if (_nodeList != null) + _nodeList.ItemsSource = VisualScript?.Nodes; + + if (_cableList != null) + _cableList.ItemsSource = VisualScript?.Cables; + + VisualScript.Nodes.Clear(); + foreach (INode node in VisualScript.Script.Nodes) + InitializeNode(node); + } + + CenterAt(new Vector(0, 0)); + } + + private void OnVisualScriptPropertyChanged(object sender, PropertyChangedEventArgs args) + { + if (args.PropertyName == nameof(VisualScript.Cables)) + _cableList.ItemsSource = VisualScript.Cables; + } + + private void OnCanvasPreviewMouseRightButtonDown(object sender, MouseButtonEventArgs args) + { + _lastRightClickLocation = args.GetPosition(_canvas); + } + + private void OnCanvasMouseLeftButtonDown(object sender, MouseButtonEventArgs args) + { + VisualScript.DeselectAllNodes(); + + _boxSelect = true; + _boxSelectStartPoint = args.GetPosition(_canvas); + Canvas.SetLeft(_selectionBorder, _boxSelectStartPoint.X); + Canvas.SetTop(_selectionBorder, _boxSelectStartPoint.Y); + _selectionBorder.Width = 0; + _selectionBorder.Height = 0; + _selectionBorder.Visibility = Visibility.Visible; + _canvas.CaptureMouse(); + + args.Handled = true; + } + + private void OnCanvasMouseLeftButtonUp(object sender, MouseButtonEventArgs args) + { + if (_boxSelect) + { + _boxSelect = false; + _canvas.ReleaseMouseCapture(); + Mouse.OverrideCursor = null; + _selectionBorder.Visibility = Visibility.Hidden; + SelectWithinRectangle(new Rect(Canvas.GetLeft(_selectionBorder), Canvas.GetTop(_selectionBorder), _selectionBorder.Width, _selectionBorder.Height)); + args.Handled = _movedDuringDrag; + } + } + + private void OnCanvasMouseRightButtonDown(object sender, MouseButtonEventArgs args) + { + _dragCanvas = true; + _dragCanvasStartLocation = args.GetPosition(this); + _dragCanvasStartOffset = _viewportCenter; + + _movedDuringDrag = false; + + _canvas.CaptureMouse(); + + args.Handled = true; + } + + private void OnCanvasMouseRightButtonUp(object sender, MouseButtonEventArgs args) + { + if (_dragCanvas) + { + _dragCanvas = false; + _canvas.ReleaseMouseCapture(); + Mouse.OverrideCursor = null; + args.Handled = _movedDuringDrag; + } + } + + private void OnCanvasMouseMove(object sender, MouseEventArgs args) + { + if (_dragCanvas) + { + if (args.RightButton == MouseButtonState.Pressed) + { + Vector newLocation = _dragCanvasStartOffset + (((args.GetPosition(this) - _dragCanvasStartLocation)) * (1.0 / Scale)); + CenterAt(newLocation); + + _movedDuringDrag = true; + Mouse.OverrideCursor = Cursors.ScrollAll; + } + else + _dragCanvas = false; + + args.Handled = true; + } + else if (_boxSelect) + { + if (args.LeftButton == MouseButtonState.Pressed) + { + double x = _boxSelectStartPoint.X; + double y = _boxSelectStartPoint.Y; + Point mousePosition = args.GetPosition(_canvas); + + double rectX = mousePosition.X > x ? x : mousePosition.X; + double rectY = mousePosition.Y > y ? y : mousePosition.Y; + double rectWidth = Math.Abs(x - mousePosition.X); + double rectHeight = Math.Abs(y - mousePosition.Y); + + Canvas.SetLeft(_selectionBorder, rectX); + Canvas.SetTop(_selectionBorder, rectY); + _selectionBorder.Width = rectWidth; + _selectionBorder.Height = rectHeight; + } + else + _boxSelect = false; + + args.Handled = true; + } + } + + private void OnCanvasDragOver(object sender, DragEventArgs args) + { + if (VisualScript == null) return; + + if (VisualScript.IsConnecting) + VisualScript.OnDragOver(args.GetPosition(_canvas)); + } + + private void OnCanvasMouseWheel(object sender, MouseWheelEventArgs args) + { + if (args.Delta < 0) + Scale /= ScaleFactor; + else + Scale *= ScaleFactor; + + Scale = Clamp(Scale, MinScale, MaxScale); + + _canvas.LayoutTransform = new ScaleTransform(Scale, Scale); + + UpdatePanning(); + + args.Handled = true; + } + + private void SelectWithinRectangle(Rect rectangle) + { + if (Script == null) return; + + VisualScript.DeselectAllNodes(); + + for (int i = 0; i < _nodeList.Items.Count; i++) + { + ContentPresenter nodeControl = (ContentPresenter)_nodeList.ItemContainerGenerator.ContainerFromIndex(i); + VisualScriptNode node = (VisualScriptNode)nodeControl.Content; + + double nodeWidth = nodeControl.ActualWidth; + double nodeHeight = nodeControl.ActualHeight; + + if (rectangle.IntersectsWith(new Rect(node.X, node.Y, nodeWidth, nodeHeight))) + node.Select(true); + } + } + + private static void ScaleChanged(DependencyObject d, DependencyPropertyChangedEventArgs args) + { + if (d is not VisualScriptPresenter presenter) return; + if (presenter.VisualScript == null) return; + + presenter.VisualScript.NodeDragScale = 1.0 / presenter.Scale; + } + + private void CreateNode(NodeData nodeData) + { + if (nodeData == null) return; + + if (_creationBoxParent.ContextMenu != null) + _creationBoxParent.ContextMenu.IsOpen = false; + + INode node = nodeData.CreateNode(); + Script.AddNode(node); + + InitializeNode(node, _lastRightClickLocation); + } + + private void InitializeNode(INode node, Point? initialLocation = null) + { + VisualScriptNode visualScriptNode = new(VisualScript, node); + if (initialLocation != null) + { + visualScriptNode.X = initialLocation.Value.X; + visualScriptNode.Y = initialLocation.Value.Y; + } + visualScriptNode.SnapNodeToGrid(); + VisualScript.Nodes.Add(visualScriptNode); + } + + private void CenterAt(Vector vector) + { + double halfSurface = (SurfaceSize / 2.0); + _viewportCenter = Clamp(vector, new Vector(-halfSurface, -halfSurface), new Vector(halfSurface, halfSurface)); + UpdatePanning(); + } + + private void UpdatePanning() + { + if (_canvasViewPortTransform == null) return; + + double surfaceOffset = (SurfaceSize / 2.0) * Scale; + _canvasViewPortTransform.X = (((_viewportCenter.X * Scale) + (ActualWidth / 2.0))) - surfaceOffset; + _canvasViewPortTransform.Y = (((_viewportCenter.Y * Scale) + (ActualHeight / 2.0))) - surfaceOffset; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double Clamp(double value, double min, double max) + { + // ReSharper disable ConvertIfStatementToReturnStatement - I'm not sure why, but inlining this statement reduces performance by ~10% + if (value < min) return min; + if (value > max) return max; + return value; + // ReSharper restore ConvertIfStatementToReturnStatement + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector Clamp(Vector value, Vector min, Vector max) + { + double x = Clamp(value.X, min.X, max.X); + double y = Clamp(value.Y, min.Y, max.Y); + return new Vector(x, y); + } + + #endregion + } +} diff --git a/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScript.cs b/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScript.cs new file mode 100644 index 000000000..57fdaaa5b --- /dev/null +++ b/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScript.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Linq; +using System.Windows; +using Artemis.Core.VisualScripting; +using Artemis.VisualScripting.Events; +using Artemis.VisualScripting.ViewModel; + +namespace Artemis.VisualScripting.Editor.Controls.Wrapper +{ + public class VisualScript : AbstractBindable + { + #region Properties & Fields + + private readonly HashSet _selectedNodes = new(); + private readonly Dictionary _nodeStartPositions = new(); + private double _nodeDragAccumulationX; + private double _nodeDragAccumulationY; + + public IScript Script { get; } + + public int SurfaceSize { get; } + public int GridSize { get; } + + private double _nodeDragScale = 1; + public double NodeDragScale + { + get => _nodeDragScale; + set => SetProperty(ref _nodeDragScale, value); + } + + private VisualScriptPin _isConnectingPin; + private VisualScriptPin IsConnectingPin + { + get => _isConnectingPin; + set + { + if (SetProperty(ref _isConnectingPin, value)) + OnPropertyChanged(nameof(IsConnecting)); + } + } + + public ObservableCollection Nodes { get; } = new(); + + public IEnumerable Cables => Nodes.SelectMany(n => n.InputPins.SelectMany(p => p.Connections)) + .Concat(Nodes.SelectMany(n => n.OutputPins.SelectMany(p => p.Connections))) + .Concat(Nodes.SelectMany(n => n.InputPinCollections.SelectMany(p => p.Pins).SelectMany(p => p.Connections))) + .Concat(Nodes.SelectMany(n => n.OutputPinCollections.SelectMany(p => p.Pins).SelectMany(p => p.Connections))) + .Distinct(); + + public bool IsConnecting => IsConnectingPin != null; + + #endregion + + #region Constructors + + public VisualScript(IScript script, int surfaceSize, int gridSize) + { + this.Script = script; + this.SurfaceSize = surfaceSize; + this.GridSize = gridSize; + + Nodes.CollectionChanged += OnNodeCollectionChanged; + } + + #endregion + + #region Methods + + public void DeselectAllNodes(VisualScriptNode except = null) + { + List selectedNodes = _selectedNodes.ToList(); + foreach (VisualScriptNode node in selectedNodes.Where(n => n != except)) + node.Deselect(); + } + + private void OnNodeCollectionChanged(object sender, NotifyCollectionChangedEventArgs args) + { + if (args.OldItems != null) + UnregisterNodes(args.OldItems.Cast()); + + if (args.NewItems != null) + RegisterNodes(args.NewItems.Cast()); + } + + private void RegisterNodes(IEnumerable nodes) + { + foreach (VisualScriptNode node in nodes) + { + node.IsSelectedChanged += OnNodeIsSelectedChanged; + node.DragStarting += OnNodeDragStarting; + node.DragEnding += OnNodeDragEnding; + node.DragMoving += OnNodeDragMoving; + + if (node.IsSelected) + _selectedNodes.Add(node); + } + } + + private void UnregisterNodes(IEnumerable nodes) + { + foreach (VisualScriptNode node in nodes) + { + node.IsSelectedChanged -= OnNodeIsSelectedChanged; + node.DragStarting -= OnNodeDragStarting; + node.DragEnding -= OnNodeDragEnding; + node.DragMoving -= OnNodeDragMoving; + + _selectedNodes.Remove(node); + } + } + + private void OnNodeIsSelectedChanged(object sender, VisualScriptNodeIsSelectedChangedEventArgs args) + { + if (sender is not VisualScriptNode node) return; + + if (args.IsSelected) + { + if (!args.AlterSelection) + DeselectAllNodes(node); + + _selectedNodes.Add(node); + } + else + _selectedNodes.Remove(node); + } + + private void OnNodeDragStarting(object sender, EventArgs args) + { + _nodeDragAccumulationX = 0; + _nodeDragAccumulationY = 0; + _nodeStartPositions.Clear(); + + foreach (VisualScriptNode node in _selectedNodes) + _nodeStartPositions.Add(node, (node.X, node.Y)); + } + + private void OnNodeDragEnding(object sender, EventArgs args) + { + foreach (VisualScriptNode node in _selectedNodes) + node.SnapNodeToGrid(); + } + + private void OnNodeDragMoving(object sender, VisualScriptNodeDragMovingEventArgs args) + { + _nodeDragAccumulationX += args.DX * NodeDragScale; + _nodeDragAccumulationY += args.DY * NodeDragScale; + + foreach (VisualScriptNode node in _selectedNodes) + { + node.X = _nodeStartPositions[node].X + _nodeDragAccumulationX; + node.Y = _nodeStartPositions[node].Y + _nodeDragAccumulationY; + node.SnapNodeToGrid(); + } + } + + internal void OnDragOver(Point position) + { + if (IsConnectingPin == null) return; + + IsConnectingPin.AbsolutePosition = position; + } + + internal void OnIsConnectingPinChanged(VisualScriptPin isConnectingPin) + { + IsConnectingPin = isConnectingPin; + } + + internal void OnPinConnected(PinConnectedEventArgs args) + { + OnPropertyChanged(nameof(Cables)); + } + + internal void OnPinDisconnected(PinDisconnectedEventArgs args) + { + OnPropertyChanged(nameof(Cables)); + } + + public void RemoveNode(VisualScriptNode node) + { + Nodes.Remove(node); + Script.RemoveNode(node.Node); + } + + #endregion + } +} diff --git a/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptCable.cs b/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptCable.cs new file mode 100644 index 000000000..9fe3943ae --- /dev/null +++ b/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptCable.cs @@ -0,0 +1,67 @@ +using System; +using Artemis.Core.VisualScripting; +using Artemis.VisualScripting.Model; +using Artemis.VisualScripting.ViewModel; + +namespace Artemis.VisualScripting.Editor.Controls.Wrapper +{ + public class VisualScriptCable : AbstractBindable + { + #region Properties & Fields + + private VisualScriptPin _from; + public VisualScriptPin From + { + get => _from; + private set => SetProperty(ref _from, value); + } + + private VisualScriptPin _to; + public VisualScriptPin To + { + get => _to; + private set => SetProperty(ref _to, value); + } + + #endregion + + #region Constructors + + public VisualScriptCable(VisualScriptPin pin1, VisualScriptPin pin2) + { + if ((pin1.Pin.Direction == PinDirection.Input) && (pin2.Pin.Direction == PinDirection.Input)) + throw new ArgumentException("Can't connect two input pins."); + + if ((pin1.Pin.Direction == PinDirection.Output) && (pin2.Pin.Direction == PinDirection.Output)) + throw new ArgumentException("Can't connect two output pins."); + + From = pin1.Pin.Direction == PinDirection.Output ? pin1 : pin2; + To = pin1.Pin.Direction == PinDirection.Input ? pin1 : pin2; + + From.ConnectTo(this); + To.ConnectTo(this); + } + + #endregion + + #region Methods + + internal void Disconnect() + { + From?.Disconnect(this); + To?.Disconnect(this); + + From = null; + To = null; + } + + internal IPin GetConnectedPin(IPin pin) + { + if (From.Pin == pin) return To.Pin; + if (To.Pin == pin) return From.Pin; + return null; + } + + #endregion + } +} diff --git a/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptNode.cs b/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptNode.cs new file mode 100644 index 000000000..40bf11d6c --- /dev/null +++ b/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptNode.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.ObjectModel; +using Artemis.Core.VisualScripting; +using Artemis.VisualScripting.Events; +using Artemis.VisualScripting.Internal; +using Artemis.VisualScripting.Model; +using Artemis.VisualScripting.ViewModel; + +namespace Artemis.VisualScripting.Editor.Controls.Wrapper +{ + public class VisualScriptNode : AbstractBindable + { + #region Properties & Fields + + private double _locationOffset; + + public VisualScript Script { get; } + public INode Node { get; } + + private bool _isSelected; + public bool IsSelected + { + get => _isSelected; + private set => SetProperty(ref _isSelected, value); + } + + private ObservableCollection _inputPins = new(); + public ObservableCollection InputPins + { + get => _inputPins; + private set => SetProperty(ref _inputPins, value); + } + + private ObservableCollection _outputPins = new(); + public ObservableCollection OutputPins + { + get => _outputPins; + private set => SetProperty(ref _outputPins, value); + } + + private ObservableCollection _inputPinCollections = new(); + public ObservableCollection InputPinCollections + { + get => _inputPinCollections; + private set => SetProperty(ref _inputPinCollections, value); + } + + private ObservableCollection _outputPinCollections = new(); + public ObservableCollection OutputPinCollections + { + get => _outputPinCollections; + private set => SetProperty(ref _outputPinCollections, value); + } + + public double X + { + get => Node.X + _locationOffset; + set + { + Node.X = value - _locationOffset; + OnPropertyChanged(); + } + } + + public double Y + { + get => Node.Y + _locationOffset; + set + { + Node.Y = value - _locationOffset; + OnPropertyChanged(); + } + } + + #endregion + + #region Commands + + private ActionCommand _removeCommand; + public ActionCommand RemoveCommand => _removeCommand ??= new ActionCommand(Remove, RemoveCanExecute); + + #endregion + + #region Events + + public event EventHandler IsSelectedChanged; + public event EventHandler DragStarting; + public event EventHandler DragEnding; + public event EventHandler DragMoving; + + #endregion + + #region Constructors + + public VisualScriptNode(VisualScript script, INode node) + { + this.Script = script; + this.Node = node; + + _locationOffset = script.SurfaceSize / 2.0; + + foreach (IPin pin in node.Pins) + { + if (pin.Direction == PinDirection.Input) + InputPins.Add(new VisualScriptPin(this, pin)); + else + OutputPins.Add(new VisualScriptPin(this, pin)); + } + + foreach (IPinCollection pinCollection in node.PinCollections) + { + if (pinCollection.Direction == PinDirection.Input) + InputPinCollections.Add(new VisualScriptPinCollection(this, pinCollection)); + else + OutputPinCollections.Add(new VisualScriptPinCollection(this, pinCollection)); + } + } + + #endregion + + #region Methods + + public void SnapNodeToGrid() + { + X -= X % Script.GridSize; + Y -= Y % Script.GridSize; + } + + public void Select(bool alterSelection = false) + { + IsSelected = true; + OnIsSelectedChanged(IsSelected, alterSelection); + } + + public void Deselect(bool alterSelection = false) + { + IsSelected = false; + OnIsSelectedChanged(IsSelected, alterSelection); + } + + public void DragStart() => DragStarting?.Invoke(this, new EventArgs()); + public void DragEnd() => DragEnding?.Invoke(this, new EventArgs()); + public void DragMove(double dx, double dy) => DragMoving?.Invoke(this, new VisualScriptNodeDragMovingEventArgs(dx, dy)); + + private void OnIsSelectedChanged(bool isSelected, bool alterSelection) => IsSelectedChanged?.Invoke(this, new VisualScriptNodeIsSelectedChangedEventArgs(isSelected, alterSelection)); + + internal void OnPinConnected(PinConnectedEventArgs args) => Script.OnPinConnected(args); + internal void OnPinDisconnected(PinDisconnectedEventArgs args) => Script.OnPinDisconnected(args); + + internal void OnIsConnectingPinChanged(VisualScriptPin isConnectingPin) => Script.OnIsConnectingPinChanged(isConnectingPin); + + private void DisconnectAllPins() + { + foreach (VisualScriptPin pin in InputPins) + pin.DisconnectAll(); + + foreach (VisualScriptPin pin in OutputPins) + pin.DisconnectAll(); + + foreach (VisualScriptPinCollection pinCollection in InputPinCollections) + foreach (VisualScriptPin pin in pinCollection.Pins) + pin.DisconnectAll(); + + foreach (VisualScriptPinCollection pinCollection in OutputPinCollections) + foreach (VisualScriptPin pin in pinCollection.Pins) + pin.DisconnectAll(); + } + + private void Remove() + { + DisconnectAllPins(); + Script.RemoveNode(this); + } + + private bool RemoveCanExecute() => Node is not IExitNode; + + #endregion + } +} diff --git a/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptPin.cs b/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptPin.cs new file mode 100644 index 000000000..00b6e5184 --- /dev/null +++ b/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptPin.cs @@ -0,0 +1,112 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Windows; +using Artemis.Core.VisualScripting; +using Artemis.VisualScripting.Events; +using Artemis.VisualScripting.Internal; +using Artemis.VisualScripting.Model; +using Artemis.VisualScripting.ViewModel; + +namespace Artemis.VisualScripting.Editor.Controls.Wrapper +{ + public class VisualScriptPin : AbstractBindable + { + #region Constants + + private const double CABLE_OFFSET = 24 * 4; + + #endregion + + #region Properties & Fields + + private VisualScriptPin _isConnectingPin; + private VisualScriptCable _isConnectingCable; + + public VisualScriptNode Node { get; } + public IPin Pin { get; } + + public ObservableCollection Connections { get; } = new(); + + private Point _absolutePosition; + public Point AbsolutePosition + { + get => _absolutePosition; + internal set + { + if (SetProperty(ref _absolutePosition, value)) + OnPropertyChanged(nameof(AbsoluteCableTargetPosition)); + } + } + + public Point AbsoluteCableTargetPosition => Pin.Direction == PinDirection.Input ? new Point(AbsolutePosition.X - CABLE_OFFSET, AbsolutePosition.Y) : new Point(AbsolutePosition.X + CABLE_OFFSET, AbsolutePosition.Y); + + #endregion + + #region Constructors + + public VisualScriptPin(VisualScriptNode node, IPin pin) + { + this.Node = node; + this.Pin = pin; + } + + #endregion + + #region Methods + + public void SetConnecting(bool isConnecting) + { + if (isConnecting) + { + if (_isConnectingCable != null) + SetConnecting(false); + + _isConnectingPin = new VisualScriptPin(null, new IsConnectingPin(Pin.Direction == PinDirection.Input ? PinDirection.Output : PinDirection.Input)) { AbsolutePosition = AbsolutePosition }; + _isConnectingCable = new VisualScriptCable(this, _isConnectingPin); + Node.OnIsConnectingPinChanged(_isConnectingPin); + } + else + { + _isConnectingCable.Disconnect(); + _isConnectingCable = null; + _isConnectingPin = null; + Node.OnIsConnectingPinChanged(_isConnectingPin); + } + } + + internal void ConnectTo(VisualScriptCable cable) + { + if (Connections.Contains(cable)) return; + + if (Pin.Direction == PinDirection.Input) + { + List cables = Connections.ToList(); + foreach (VisualScriptCable c in cables) + c.Disconnect(); + } + + Connections.Add(cable); + Pin.ConnectTo(cable.GetConnectedPin(Pin)); + + Node?.OnPinConnected(new PinConnectedEventArgs(this, cable)); + } + + public void DisconnectAll() + { + List cables = Connections.ToList(); + foreach (VisualScriptCable cable in cables) + cable.Disconnect(); + } + + internal void Disconnect(VisualScriptCable cable) + { + Connections.Remove(cable); + Pin.DisconnectFrom(cable.GetConnectedPin(Pin)); + + Node?.OnPinDisconnected(new PinDisconnectedEventArgs(this, cable)); + } + + #endregion + } +} diff --git a/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptPinCollection.cs b/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptPinCollection.cs new file mode 100644 index 000000000..3e20fe8c5 --- /dev/null +++ b/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptPinCollection.cs @@ -0,0 +1,58 @@ +using System.Collections.ObjectModel; +using Artemis.Core.VisualScripting; +using Artemis.VisualScripting.ViewModel; + +namespace Artemis.VisualScripting.Editor.Controls.Wrapper +{ + public class VisualScriptPinCollection : AbstractBindable + { + #region Properties & Fields + + public VisualScriptNode Node { get; } + public IPinCollection PinCollection { get; } + + public ObservableCollection Pins { get; } = new(); + + #endregion + + #region Commands + + private ActionCommand _addPinCommand; + public ActionCommand AddPinCommand => _addPinCommand ??= new ActionCommand(AddPin); + + private ActionCommand _removePinCommand; + public ActionCommand RemovePinCommand => _removePinCommand ??= new ActionCommand(RemovePin); + + #endregion + + #region Constructors + + public VisualScriptPinCollection(VisualScriptNode node, IPinCollection pinCollection) + { + this.Node = node; + this.PinCollection = pinCollection; + + foreach (IPin pin in PinCollection) + Pins.Add(new VisualScriptPin(node, pin)); + } + + #endregion + + #region Methods + + public void AddPin() + { + IPin pin = PinCollection.AddPin(); + Pins.Add(new VisualScriptPin(Node, pin)); + } + + public void RemovePin(VisualScriptPin pin) + { + pin.DisconnectAll(); + PinCollection.Remove(pin.Pin); + Pins.Remove(pin); + } + + #endregion + } +} diff --git a/src/Artemis.VisualScripting/Editor/EditorStyles.xaml b/src/Artemis.VisualScripting/Editor/EditorStyles.xaml new file mode 100644 index 000000000..743c4dead --- /dev/null +++ b/src/Artemis.VisualScripting/Editor/EditorStyles.xaml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Editor/Styles/VisualScriptCablePresenter.xaml b/src/Artemis.VisualScripting/Editor/Styles/VisualScriptCablePresenter.xaml new file mode 100644 index 000000000..d51f0cfa3 --- /dev/null +++ b/src/Artemis.VisualScripting/Editor/Styles/VisualScriptCablePresenter.xaml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +