From 86b4258f5d78a3dae361e34ca1e87aa6cc959cc9 Mon Sep 17 00:00:00 2001 From: Robert Date: Fri, 18 Mar 2022 23:51:54 +0100 Subject: [PATCH] Node editor - Added cables --- src/Artemis.Core/VisualScripting/Pin.cs | 9 + .../Ninject/Factories/IVMFactory.cs | 172 +++++++++--------- .../Extensions/SKColorExtensions.cs | 20 ++ .../Services/NodeEditor/Commands/MoveNode.cs | 18 ++ .../ColorToSolidColorBrushConverter.cs | 11 +- .../Ninject/Factories/IVMFactory.cs | 5 +- .../Screens/VisualScripting/CableView.axaml | 37 +++- .../VisualScripting/CableView.axaml.cs | 32 ++-- .../Screens/VisualScripting/CableViewModel.cs | 112 +++++++++++- .../VisualScripting/NodeScriptView.axaml | 10 +- .../VisualScripting/NodeScriptViewModel.cs | 56 +++++- .../Screens/VisualScripting/NodeView.axaml | 4 +- .../Screens/VisualScripting/NodeView.axaml.cs | 20 +- .../Screens/VisualScripting/NodeViewModel.cs | 72 ++++++-- .../Pins/InputPinCollectionView.axaml | 15 +- .../Pins/InputPinCollectionViewModel.cs | 3 +- .../VisualScripting/Pins/InputPinView.axaml | 2 +- .../Pins/InputPinView.axaml.cs | 66 +++++++ .../Pins/OutputPinCollectionView.axaml | 15 +- .../Pins/OutputPinCollectionViewModel.cs | 3 +- .../Pins/OutputPinView.axaml.cs | 32 +++- .../Pins/PinCollectionViewModel.cs | 43 ++++- .../VisualScripting/Pins/PinViewModel.cs | 46 ++++- .../VisualScripting/VisualScripting.axaml | 1 + .../Screens/Workshop/WorkshopViewModel.cs | 14 +- 25 files changed, 655 insertions(+), 163 deletions(-) create mode 100644 src/Avalonia/Artemis.UI.Shared/Extensions/SKColorExtensions.cs diff --git a/src/Artemis.Core/VisualScripting/Pin.cs b/src/Artemis.Core/VisualScripting/Pin.cs index 285415286..0407014eb 100644 --- a/src/Artemis.Core/VisualScripting/Pin.cs +++ b/src/Artemis.Core/VisualScripting/Pin.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Linq; using Artemis.Core.Events; namespace Artemis.Core @@ -81,6 +82,8 @@ namespace Artemis.Core OnPropertyChanged(nameof(ConnectedTo)); PinConnected?.Invoke(this, new SingleValueEventArgs(pin)); + if (!pin.ConnectedTo.Contains(this)) + pin.ConnectTo(this); } /// @@ -90,6 +93,8 @@ namespace Artemis.Core OnPropertyChanged(nameof(ConnectedTo)); PinDisconnected?.Invoke(this, new SingleValueEventArgs(pin)); + if (pin.ConnectedTo.Contains(this)) + pin.DisconnectFrom(this); } /// @@ -101,7 +106,11 @@ namespace Artemis.Core OnPropertyChanged(nameof(ConnectedTo)); foreach (IPin pin in connectedPins) + { PinDisconnected?.Invoke(this, new SingleValueEventArgs(pin)); + if (pin.ConnectedTo.Contains(this)) + pin.DisconnectFrom(this); + } } #endregion diff --git a/src/Artemis.UI/Ninject/Factories/IVMFactory.cs b/src/Artemis.UI/Ninject/Factories/IVMFactory.cs index dfd2a6b42..b7061e837 100644 --- a/src/Artemis.UI/Ninject/Factories/IVMFactory.cs +++ b/src/Artemis.UI/Ninject/Factories/IVMFactory.cs @@ -6,7 +6,6 @@ using Artemis.UI.Screens.Plugins; using Artemis.UI.Screens.ProfileEditor.DisplayConditions.Event; using Artemis.UI.Screens.ProfileEditor.DisplayConditions.Static; using Artemis.UI.Screens.ProfileEditor.LayerProperties; -using Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings; using Artemis.UI.Screens.ProfileEditor.LayerProperties.LayerEffects; using Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline; using Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree; @@ -26,107 +25,106 @@ using Artemis.UI.Screens.Sidebar; using Artemis.UI.Screens.Sidebar.Dialogs.ProfileEdit; using Stylet; -namespace Artemis.UI.Ninject.Factories +namespace Artemis.UI.Ninject.Factories; + +public interface IVmFactory { - public interface IVmFactory - { - } +} - public interface ISettingsVmFactory : IVmFactory - { - PluginSettingsViewModel CreatePluginSettingsViewModel(Plugin plugin); - PluginFeatureViewModel CreatePluginFeatureViewModel(PluginFeatureInfo pluginFeatureInfo, bool showShield); - DeviceSettingsViewModel CreateDeviceSettingsViewModel(ArtemisDevice device); - } +public interface ISettingsVmFactory : IVmFactory +{ + PluginSettingsViewModel CreatePluginSettingsViewModel(Plugin plugin); + PluginFeatureViewModel CreatePluginFeatureViewModel(PluginFeatureInfo pluginFeatureInfo, bool showShield); + DeviceSettingsViewModel CreateDeviceSettingsViewModel(ArtemisDevice device); +} - public interface IDeviceDebugVmFactory : IVmFactory - { - DeviceDialogViewModel DeviceDialogViewModel(ArtemisDevice device); - DevicePropertiesTabViewModel DevicePropertiesTabViewModel(ArtemisDevice device); - DeviceInfoTabViewModel DeviceInfoTabViewModel(ArtemisDevice device); - DeviceLedsTabViewModel DeviceLedsTabViewModel(ArtemisDevice device); - InputMappingsTabViewModel InputMappingsTabViewModel(ArtemisDevice device); - } +public interface IDeviceDebugVmFactory : IVmFactory +{ + DeviceDialogViewModel DeviceDialogViewModel(ArtemisDevice device); + DevicePropertiesTabViewModel DevicePropertiesTabViewModel(ArtemisDevice device); + DeviceInfoTabViewModel DeviceInfoTabViewModel(ArtemisDevice device); + DeviceLedsTabViewModel DeviceLedsTabViewModel(ArtemisDevice device); + InputMappingsTabViewModel InputMappingsTabViewModel(ArtemisDevice device); +} - public interface IProfileTreeVmFactory : IVmFactory - { - FolderViewModel FolderViewModel(ProfileElement folder); - LayerViewModel LayerViewModel(ProfileElement layer); - } +public interface IProfileTreeVmFactory : IVmFactory +{ + FolderViewModel FolderViewModel(ProfileElement folder); + LayerViewModel LayerViewModel(ProfileElement layer); +} - public interface ILayerHintVmFactory : IVmFactory - { - LayerHintsDialogViewModel LayerHintsDialogViewModel(Layer layer); - CategoryAdaptionHintViewModel CategoryAdaptionHintViewModel(CategoryAdaptionHint adaptionHint); - DeviceAdaptionHintViewModel DeviceAdaptionHintViewModel(DeviceAdaptionHint adaptionHint); - KeyboardSectionAdaptionHintViewModel KeyboardSectionAdaptionHintViewModel(KeyboardSectionAdaptionHint adaptionHint); - } +public interface ILayerHintVmFactory : IVmFactory +{ + LayerHintsDialogViewModel LayerHintsDialogViewModel(Layer layer); + CategoryAdaptionHintViewModel CategoryAdaptionHintViewModel(CategoryAdaptionHint adaptionHint); + DeviceAdaptionHintViewModel DeviceAdaptionHintViewModel(DeviceAdaptionHint adaptionHint); + KeyboardSectionAdaptionHintViewModel KeyboardSectionAdaptionHintViewModel(KeyboardSectionAdaptionHint adaptionHint); +} - public interface IHeaderVmFactory : IVmFactory - { - SimpleHeaderViewModel SimpleHeaderViewModel(string displayName); - } +public interface IHeaderVmFactory : IVmFactory +{ + SimpleHeaderViewModel SimpleHeaderViewModel(string displayName); +} - public interface IProfileLayerVmFactory : IVmFactory - { - ProfileLayerViewModel Create(Layer layer, PanZoomViewModel panZoomViewModel); - } +public interface IProfileLayerVmFactory : IVmFactory +{ + ProfileLayerViewModel Create(Layer layer, PanZoomViewModel panZoomViewModel); +} - public interface IVisualizationToolVmFactory : IVmFactory - { - ViewpointMoveToolViewModel ViewpointMoveToolViewModel(PanZoomViewModel panZoomViewModel); - EditToolViewModel EditToolViewModel(PanZoomViewModel panZoomViewModel); - SelectionToolViewModel SelectionToolViewModel(PanZoomViewModel panZoomViewModel); - SelectionRemoveToolViewModel SelectionRemoveToolViewModel(PanZoomViewModel panZoomViewModel); - } +public interface IVisualizationToolVmFactory : IVmFactory +{ + ViewpointMoveToolViewModel ViewpointMoveToolViewModel(PanZoomViewModel panZoomViewModel); + EditToolViewModel EditToolViewModel(PanZoomViewModel panZoomViewModel); + SelectionToolViewModel SelectionToolViewModel(PanZoomViewModel panZoomViewModel); + SelectionRemoveToolViewModel SelectionRemoveToolViewModel(PanZoomViewModel panZoomViewModel); +} - public interface ILayerPropertyVmFactory : IVmFactory - { - LayerPropertyViewModel LayerPropertyViewModel(ILayerProperty layerProperty); +public interface ILayerPropertyVmFactory : IVmFactory +{ + LayerPropertyViewModel LayerPropertyViewModel(ILayerProperty layerProperty); - LayerPropertyGroupViewModel LayerPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup); - TreeGroupViewModel TreeGroupViewModel(LayerPropertyGroupViewModel layerPropertyGroupViewModel); - TimelineGroupViewModel TimelineGroupViewModel(LayerPropertyGroupViewModel layerPropertyGroupViewModel); + LayerPropertyGroupViewModel LayerPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup); + TreeGroupViewModel TreeGroupViewModel(LayerPropertyGroupViewModel layerPropertyGroupViewModel); + TimelineGroupViewModel TimelineGroupViewModel(LayerPropertyGroupViewModel layerPropertyGroupViewModel); - TreeViewModel TreeViewModel(LayerPropertiesViewModel layerPropertiesViewModel, IObservableCollection layerPropertyGroups); - EffectsViewModel EffectsViewModel(LayerPropertiesViewModel layerPropertiesViewModel); - TimelineViewModel TimelineViewModel(LayerPropertiesViewModel layerPropertiesViewModel, IObservableCollection layerPropertyGroups); - TimelineSegmentViewModel TimelineSegmentViewModel(SegmentViewModelType segment, IObservableCollection layerPropertyGroups); - } + TreeViewModel TreeViewModel(LayerPropertiesViewModel layerPropertiesViewModel, IObservableCollection layerPropertyGroups); + EffectsViewModel EffectsViewModel(LayerPropertiesViewModel layerPropertiesViewModel); + TimelineViewModel TimelineViewModel(LayerPropertiesViewModel layerPropertiesViewModel, IObservableCollection layerPropertyGroups); + TimelineSegmentViewModel TimelineSegmentViewModel(SegmentViewModelType segment, IObservableCollection layerPropertyGroups); +} - public interface IConditionVmFactory : IVmFactory - { - StaticConditionViewModel StaticConditionViewModel(StaticCondition staticCondition); - EventConditionViewModel EventConditionViewModel(EventCondition eventCondition); - } +public interface IConditionVmFactory : IVmFactory +{ + StaticConditionViewModel StaticConditionViewModel(StaticCondition staticCondition); + EventConditionViewModel EventConditionViewModel(EventCondition eventCondition); +} - public interface IPrerequisitesVmFactory : IVmFactory - { - PluginPrerequisiteViewModel PluginPrerequisiteViewModel(PluginPrerequisite pluginPrerequisite, bool uninstall); - } +public interface IPrerequisitesVmFactory : IVmFactory +{ + PluginPrerequisiteViewModel PluginPrerequisiteViewModel(PluginPrerequisite pluginPrerequisite, bool uninstall); +} - public interface IScriptVmFactory : IVmFactory - { - ScriptsDialogViewModel ScriptsDialogViewModel(Profile profile); - ScriptConfigurationViewModel ScriptConfigurationViewModel(ScriptConfiguration scriptConfiguration); - } +public interface IScriptVmFactory : IVmFactory +{ + ScriptsDialogViewModel ScriptsDialogViewModel(Profile profile); + ScriptConfigurationViewModel ScriptConfigurationViewModel(ScriptConfiguration scriptConfiguration); +} - public interface ISidebarVmFactory : IVmFactory - { - SidebarCategoryViewModel SidebarCategoryViewModel(ProfileCategory profileCategory); - SidebarProfileConfigurationViewModel SidebarProfileConfigurationViewModel(ProfileConfiguration profileConfiguration); - ProfileConfigurationHotkeyViewModel ProfileConfigurationHotkeyViewModel(ProfileConfiguration profileConfiguration, bool isDisableHotkey); - ModuleActivationRequirementViewModel ModuleActivationRequirementViewModel(IModuleActivationRequirement activationRequirement); - } +public interface ISidebarVmFactory : IVmFactory +{ + SidebarCategoryViewModel SidebarCategoryViewModel(ProfileCategory profileCategory); + SidebarProfileConfigurationViewModel SidebarProfileConfigurationViewModel(ProfileConfiguration profileConfiguration); + ProfileConfigurationHotkeyViewModel ProfileConfigurationHotkeyViewModel(ProfileConfiguration profileConfiguration, bool isDisableHotkey); + ModuleActivationRequirementViewModel ModuleActivationRequirementViewModel(IModuleActivationRequirement activationRequirement); +} - public interface INodeVmFactory : IVmFactory - { - NodeScriptWindowViewModel NodeScriptWindowViewModel(NodeScript nodeScript); - } +public interface INodeVmFactory : IVmFactory +{ + NodeScriptWindowViewModel NodeScriptWindowViewModel(NodeScript nodeScript); +} - public interface IPropertyVmFactory - { - ITreePropertyViewModel TreePropertyViewModel(ILayerProperty layerProperty, LayerPropertyViewModel layerPropertyViewModel); - ITimelinePropertyViewModel TimelinePropertyViewModel(ILayerProperty layerProperty, LayerPropertyViewModel layerPropertyViewModel); - } +public interface IPropertyVmFactory +{ + ITreePropertyViewModel TreePropertyViewModel(ILayerProperty layerProperty, LayerPropertyViewModel layerPropertyViewModel); + ITimelinePropertyViewModel TimelinePropertyViewModel(ILayerProperty layerProperty, LayerPropertyViewModel layerPropertyViewModel); } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Extensions/SKColorExtensions.cs b/src/Avalonia/Artemis.UI.Shared/Extensions/SKColorExtensions.cs new file mode 100644 index 000000000..118de3d70 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Extensions/SKColorExtensions.cs @@ -0,0 +1,20 @@ +using Avalonia.Media; +using SkiaSharp; + +namespace Artemis.UI.Shared.Extensions; + +/// +/// Provides extension methods for the type. +/// +public static class SKColorExtensions +{ + /// + /// Converts a SkiaSharp to an Avalonia . + /// + /// The color to convert. + /// The resulting color. + public static Color ToColor(this SKColor color) + { + return new Color(color.Alpha, color.Red, color.Green, color.Blue); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/MoveNode.cs b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/MoveNode.cs index 0df32684d..33aabb2c4 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/MoveNode.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/MoveNode.cs @@ -29,6 +29,24 @@ public class MoveNode : INodeEditorCommand _originalY = node.Y; } + /// + /// Creates a new instance of the class. + /// + /// The node to update. + /// The new X-position. + /// The new Y-position. + /// The original X-position. + /// The original Y-position. + public MoveNode(INode node, double x, double y, double originalX, double originalY) + { + _node = node; + _x = x; + _y = y; + + _originalX = originalX; + _originalY = originalY; + } + /// public string DisplayName => "Move node"; diff --git a/src/Avalonia/Artemis.UI/Converters/ColorToSolidColorBrushConverter.cs b/src/Avalonia/Artemis.UI/Converters/ColorToSolidColorBrushConverter.cs index 7bf4a26e7..be37a5646 100644 --- a/src/Avalonia/Artemis.UI/Converters/ColorToSolidColorBrushConverter.cs +++ b/src/Avalonia/Artemis.UI/Converters/ColorToSolidColorBrushConverter.cs @@ -2,29 +2,24 @@ using System.Globalization; using Avalonia.Data.Converters; using Avalonia.Media; -using RGBColor = RGB.NET.Core.Color; namespace Artemis.UI.Converters { /// - /// Converts into . + /// Converts into . /// public class ColorToSolidColorBrushConverter : IValueConverter { /// public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { - return new SolidColorBrush(!(value is RGBColor color) - ? new Color(0, 0, 0, 0) - : new Color((byte) color.A, (byte) color.R, (byte) color.G, (byte) color.B)); + return new SolidColorBrush(value is not Color color ? new Color(0, 0, 0, 0) : color); } /// public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) { - return !(value is SolidColorBrush brush) - ? RGBColor.Transparent - : new RGBColor(brush.Color.A, brush.Color.R, brush.Color.G, brush.Color.B); + return value is not SolidColorBrush brush ? new Color(0, 0, 0, 0) : brush.Color; } } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs index 140914580..fb9493c2c 100644 --- a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs +++ b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs @@ -95,13 +95,14 @@ namespace Artemis.UI.Ninject.Factories NodeScriptViewModel NodeScriptViewModel(NodeScript nodeScript); NodePickerViewModel NodePickerViewModel(NodeScript nodeScript); NodeViewModel NodeViewModel(NodeScriptViewModel nodeScriptViewModel, INode node); + CableViewModel CableViewModel(NodeScriptViewModel nodeScriptViewModel, IPin? from, IPin? to); } public interface INodePinVmFactory { - PinCollectionViewModel InputPinCollectionViewModel(PinCollection inputPinCollection); + PinCollectionViewModel InputPinCollectionViewModel(IPinCollection inputPinCollection); PinViewModel InputPinViewModel(IPin inputPin); - PinCollectionViewModel OutputPinCollectionViewModel(PinCollection outputPinCollection); + PinCollectionViewModel OutputPinCollectionViewModel(IPinCollection outputPinCollection); PinViewModel OutputPinViewModel(IPin outputPin); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml index 12b214fb4..01c50c703 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml @@ -2,7 +2,38 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:visualScripting="clr-namespace:Artemis.UI.Screens.VisualScripting" + xmlns:converters="clr-namespace:Artemis.UI.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Screens.VisualScripting.CableView"> - Welcome to Avalonia! - + x:Class="Artemis.UI.Screens.VisualScripting.CableView" + x:DataType="visualScripting:CableViewModel" + ClipToBounds="False"> + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml.cs index b4fa8c6a4..1c16f8cf0 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml.cs @@ -1,20 +1,30 @@ +using System; using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Mixins; +using Avalonia.Controls.Shapes; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; +using ReactiveUI; -namespace Artemis.UI.Screens.VisualScripting +namespace Artemis.UI.Screens.VisualScripting; + +public class CableView : ReactiveUserControl { - public partial class CableView : ReactiveUserControl + public CableView() { - public CableView() - { - InitializeComponent(); - } + InitializeComponent(); + Path cablePath = this.Get("CablePath"); - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } + // Swap a margin on and off of the cable path to ensure the visual is always invalidated + // This is a workaround for https://github.com/AvaloniaUI/Avalonia/issues/4748 + this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.FromPoint) + .Subscribe(_ => cablePath.Margin = cablePath.Margin == new Thickness(0, 0, 0, 0) ? new Thickness(1, 1, 0, 0) : new Thickness(0, 0, 0, 0)) + .DisposeWith(d)); } -} + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableViewModel.cs index 9d14d845d..06f202258 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableViewModel.cs @@ -1,28 +1,128 @@ -using Artemis.UI.Screens.VisualScripting.Pins; +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.UI.Exceptions; +using Artemis.UI.Screens.VisualScripting.Pins; using Artemis.UI.Shared; +using Artemis.UI.Shared.Extensions; +using Avalonia; +using Avalonia.Media; +using Avalonia.Threading; +using DynamicData; +using ReactiveUI; namespace Artemis.UI.Screens.VisualScripting; public class CableViewModel : ActivatableViewModelBase { - private PinViewModel _from; - private PinViewModel _to; + private const double CABLE_OFFSET = 24 * 4; - public CableViewModel(PinViewModel from, PinViewModel to) + private readonly NodeScriptViewModel _nodeScriptViewModel; + private PinDirection _dragDirection; + private Point _dragPoint; + private bool _isDragging; + private IPin? _from; + private IPin? _to; + private PinViewModel? _fromViewModel; + private PinViewModel? _toViewModel; + private readonly ObservableAsPropertyHelper _fromPoint; + private readonly ObservableAsPropertyHelper _fromTargetPoint; + private readonly ObservableAsPropertyHelper _toPoint; + private readonly ObservableAsPropertyHelper _toTargetPoint; + private readonly ObservableAsPropertyHelper _cableColor; + + public CableViewModel(NodeScriptViewModel nodeScriptViewModel, IPin? from, IPin? to) { + if (from != null && from.Direction != PinDirection.Output) + throw new ArtemisUIException("Can only create cables originating from an output pin"); + if (to != null && to.Direction != PinDirection.Input) + throw new ArtemisUIException("Can only create cables targeted to an input pin"); + + _nodeScriptViewModel = nodeScriptViewModel; _from = from; _to = to; + + this.WhenActivated(d => + { + if (From != null) + _nodeScriptViewModel.PinViewModels.Connect().Filter(p => p.Pin == From).Transform(model => FromViewModel = model).Subscribe().DisposeWith(d); + if (To != null) + _nodeScriptViewModel.PinViewModels.Connect().Filter(p => p.Pin == To).Transform(model => ToViewModel = model).Subscribe().DisposeWith(d); + }); + + _fromPoint = this.WhenAnyValue(vm => vm.FromViewModel).Select(p => p != null ? p.WhenAnyValue(pvm => pvm.Position) : Observable.Never()).Switch().ToProperty(this, vm => vm.FromPoint); + _fromTargetPoint = this.WhenAnyValue(vm => vm.FromPoint).Select(point => new Point(point.X + CABLE_OFFSET, point.Y)).ToProperty(this, vm => vm.FromTargetPoint); + _toPoint = this.WhenAnyValue(vm => vm.ToViewModel).Select(p => p != null ? p.WhenAnyValue(pvm => pvm.Position) : Observable.Never()).Switch().ToProperty(this, vm => vm.ToPoint); + _toTargetPoint = this.WhenAnyValue(vm => vm.ToPoint).Select(point => new Point(point.X - CABLE_OFFSET, point.Y)).ToProperty(this, vm => vm.ToTargetPoint); + _cableColor = this.WhenAnyValue(vm => vm.FromViewModel, vm => vm.ToViewModel).Select(tuple => tuple.Item1?.PinColor ?? tuple.Item2?.PinColor ?? new Color(255, 255, 255, 255)).ToProperty(this, vm => vm.CableColor); } - public PinViewModel From + public IPin? From { get => _from; set => RaiseAndSetIfChanged(ref _from, value); } - public PinViewModel To + public IPin? To { get => _to; set => RaiseAndSetIfChanged(ref _to, value); } + + public PinViewModel? FromViewModel + { + get => _fromViewModel; + set => RaiseAndSetIfChanged(ref _fromViewModel, value); + } + + public PinViewModel? ToViewModel + { + get => _toViewModel; + set => RaiseAndSetIfChanged(ref _toViewModel, value); + } + + public bool IsDragging + { + get => _isDragging; + set => RaiseAndSetIfChanged(ref _isDragging, value); + } + + public PinDirection DragDirection + { + get => _dragDirection; + set => RaiseAndSetIfChanged(ref _dragDirection, value); + } + + public Point DragPoint + { + get => _dragPoint; + set => RaiseAndSetIfChanged(ref _dragPoint, value); + } + + public Point FromPoint => _fromPoint.Value; + public Point FromTargetPoint => _fromTargetPoint.Value; + public Point ToPoint => _toPoint.Value; + public Point ToTargetPoint => _toTargetPoint.Value; + public Color CableColor => _cableColor.Value; + + public void StartDrag(PinDirection dragDirection) + { + IsDragging = true; + DragDirection = dragDirection; + } + + public bool UpdateDrag(Point position, PinViewModel? targetViewModel) + { + DragPoint = position; + return true; + } + + public void FinishDrag() + { + IsDragging = false; + } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml index b08cf9b2c..100b3d59c 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml @@ -19,7 +19,6 @@ - + @@ -41,10 +41,10 @@ - + - + @@ -56,7 +56,7 @@ - + diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs index 26f42fd97..62c787d7d 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs @@ -6,10 +6,14 @@ using System.Reactive.Linq; using Artemis.Core; using Artemis.Core.Events; using Artemis.UI.Ninject.Factories; +using Artemis.UI.Screens.VisualScripting.Pins; using Artemis.UI.Shared; using Artemis.UI.Shared.Services.NodeEditor; +using Artemis.UI.Shared.Services.NodeEditor.Commands; using Avalonia; using Avalonia.Controls.Mixins; +using DynamicData; +using DynamicData.Binding; using ReactiveUI; namespace Artemis.UI.Screens.VisualScripting; @@ -17,11 +21,13 @@ namespace Artemis.UI.Screens.VisualScripting; public class NodeScriptViewModel : ActivatableViewModelBase { private readonly INodeVmFactory _nodeVmFactory; + private readonly INodeEditorService _nodeEditorService; private List? _initialNodeSelection; public NodeScriptViewModel(NodeScript nodeScript, INodeVmFactory nodeVmFactory, INodeEditorService nodeEditorService) { _nodeVmFactory = nodeVmFactory; + _nodeEditorService = nodeEditorService; NodeScript = nodeScript; NodePickerViewModel = _nodeVmFactory.NodePickerViewModel(nodeScript); @@ -37,16 +43,46 @@ public class NodeScriptViewModel : ActivatableViewModelBase .DisposeWith(d); }); + // Create VMs for all nodes NodeViewModels = new ObservableCollection(); foreach (INode nodeScriptNode in NodeScript.Nodes) NodeViewModels.Add(_nodeVmFactory.NodeViewModel(this, nodeScriptNode)); - CableViewModels = new ObservableCollection(); + // Observe all outgoing pin connections and create cables for them + IObservable> viewModels = NodeViewModels.ToObservableChangeSet(); + PinViewModels = viewModels.TransformMany(vm => vm.OutputPinViewModels) + .Merge(viewModels.TransformMany(vm => vm.InputPinViewModels)) + .Merge(viewModels + .TransformMany(vm => vm.OutputPinCollectionViewModels) + .TransformMany(vm => vm.PinViewModels)) + .Merge(viewModels + .TransformMany(vm => vm.InputPinCollectionViewModels) + .TransformMany(vm => vm.PinViewModels)) + .AsObservableList(); + + PinViewModels.Connect() + .Filter(p => p.Pin.Direction == PinDirection.Input && p.Pin.ConnectedTo.Any()) + .Transform(vm => _nodeVmFactory.CableViewModel(this, vm.Pin.ConnectedTo.First(), vm.Pin)) // The first pin is the originating output pin + .Bind(out ReadOnlyObservableCollection cableViewModels) + .Subscribe(); + + CableViewModels = cableViewModels; + } + + public IObservableList PinViewModels { get; } + + public PinViewModel? GetPinViewModel(IPin pin) + { + return NodeViewModels + .SelectMany(n => n.Pins) + .Concat(NodeViewModels.SelectMany(n => n.InputPinCollectionViewModels.SelectMany(c => c.PinViewModels))) + .Concat(NodeViewModels.SelectMany(n => n.OutputPinCollectionViewModels.SelectMany(c => c.PinViewModels))) + .FirstOrDefault(vm => vm.Pin == pin); } public NodeScript NodeScript { get; } public ObservableCollection NodeViewModels { get; } - public ObservableCollection CableViewModels { get; } + public ReadOnlyObservableCollection CableViewModels { get; } public NodePickerViewModel NodePickerViewModel { get; } public NodeEditorHistory History { get; } @@ -87,18 +123,28 @@ public class NodeScriptViewModel : ActivatableViewModelBase public void StartNodeDrag(Point position) { foreach (NodeViewModel nodeViewModel in NodeViewModels) - nodeViewModel.SaveDragOffset(position); + nodeViewModel.StartDrag(position); } public void UpdateNodeDrag(Point position) { foreach (NodeViewModel nodeViewModel in NodeViewModels) - nodeViewModel.UpdatePosition(position); + nodeViewModel.UpdateDrag(position); } public void FinishNodeDrag() { - // TODO: Command + List commands = NodeViewModels.Select(n => n.FinishDrag()).Where(c => c != null).Cast().ToList(); + + if (!commands.Any()) + return; + + if (commands.Count == 1) + _nodeEditorService.ExecuteCommand(NodeScript, commands.First()); + + using NodeEditorCommandScope scope = _nodeEditorService.CreateCommandScope(NodeScript, $"Move {commands.Count} nodes"); + foreach (MoveNode moveNode in commands) + _nodeEditorService.ExecuteCommand(NodeScript, moveNode); } private void HandleNodeAdded(SingleValueEventArgs eventArgs) diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml index 33b2bb1b2..37aa77d57 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml @@ -57,14 +57,14 @@ - + - + diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml.cs index 6a3cf384a..be0b892dc 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Avalonia; +using Avalonia.Controls; using Avalonia.Controls.PanAndZoom; using Avalonia.Input; using Avalonia.Markup.Xaml; @@ -50,27 +51,36 @@ public class NodeView : ReactiveUserControl _dragging = false; ViewModel.NodeScriptViewModel.FinishNodeDrag(); e.Pointer.Capture(null); - return; } - - ViewModel.NodeScriptViewModel.UpdateNodeSelection(new List {ViewModel}, e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Control)); - ViewModel.NodeScriptViewModel.FinishNodeSelection(); + else + { + ViewModel.NodeScriptViewModel.UpdateNodeSelection(new List {ViewModel}, e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Control)); + ViewModel.NodeScriptViewModel.FinishNodeSelection(); + } e.Handled = true; } private void InputElement_OnPointerMoved(object? sender, PointerEventArgs e) { - PointerPoint point = e.GetCurrentPoint(this.FindAncestorOfType()); + PointerPoint point = e.GetCurrentPoint(this.FindAncestorOfType()); if (ViewModel == null || !point.Properties.IsLeftButtonPressed) return; if (!_dragging) { _dragging = true; + + if (!ViewModel.IsSelected) + { + ViewModel.NodeScriptViewModel.UpdateNodeSelection(new List {ViewModel}, false, false); + ViewModel.NodeScriptViewModel.FinishNodeSelection(); + } + ViewModel.NodeScriptViewModel.StartNodeDrag(point.Position); e.Pointer.Capture((IInputElement?) sender); } + ViewModel.NodeScriptViewModel.UpdateNodeDrag(point.Position); e.Handled = true; diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs index 13eb8c500..1f25b3cd1 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs @@ -4,7 +4,6 @@ using System.Reactive; using System.Reactive.Linq; using Artemis.Core; using Artemis.UI.Ninject.Factories; -using Artemis.UI.Screens.SurfaceEditor; using Artemis.UI.Screens.VisualScripting.Pins; using Artemis.UI.Shared; using Artemis.UI.Shared.Services.NodeEditor; @@ -12,6 +11,7 @@ using Artemis.UI.Shared.Services.NodeEditor.Commands; using Avalonia; using Avalonia.Controls.Mixins; using DynamicData; +using DynamicData.Binding; using ReactiveUI; namespace Artemis.UI.Screens.VisualScripting; @@ -22,10 +22,12 @@ public class NodeViewModel : ActivatableViewModelBase private ICustomNodeViewModel? _customNodeViewModel; private ReactiveCommand? _deleteNode; - private ObservableAsPropertyHelper? _isStaticNode; private double _dragOffsetX; private double _dragOffsetY; private bool _isSelected; + private ObservableAsPropertyHelper? _isStaticNode; + private double _startX; + private double _startY; public NodeViewModel(NodeScriptViewModel nodeScriptViewModel, INode node, INodePinVmFactory nodePinVmFactory, INodeEditorService nodeEditorService) { @@ -33,7 +35,39 @@ public class NodeViewModel : ActivatableViewModelBase _nodeEditorService = nodeEditorService; Node = node; + DeleteNode = ReactiveCommand.Create(ExecuteDeleteNode, this.WhenAnyValue(vm => vm.IsStaticNode).Select(v => !v)); + SourceList nodePins = new(); + SourceList nodePinCollections = new(); + nodePins.AddRange(Node.Pins); + nodePinCollections.AddRange(Node.PinCollections); + + // Create observable collections split up by direction + nodePins.Connect().Filter(n => n.Direction == PinDirection.Input).Transform(nodePinVmFactory.InputPinViewModel) + .Bind(out ReadOnlyObservableCollection inputPins).Subscribe(); + nodePins.Connect().Filter(n => n.Direction == PinDirection.Output).Transform(nodePinVmFactory.OutputPinViewModel) + .Bind(out ReadOnlyObservableCollection outputPins).Subscribe(); + InputPinViewModels = inputPins; + OutputPinViewModels = outputPins; + + // Same again but for pin collections + nodePinCollections.Connect().Filter(n => n.Direction == PinDirection.Input).Transform(nodePinVmFactory.InputPinCollectionViewModel) + .Bind(out ReadOnlyObservableCollection inputPinCollections).Subscribe(); + nodePinCollections.Connect().Filter(n => n.Direction == PinDirection.Output).Transform(nodePinVmFactory.OutputPinCollectionViewModel) + .Bind(out ReadOnlyObservableCollection outputPinCollections).Subscribe(); + InputPinCollectionViewModels = inputPinCollections; + OutputPinCollectionViewModels = outputPinCollections; + + // Create a single observable collection containing all pin view models + InputPinViewModels.ToObservableChangeSet() + .Merge(InputPinCollectionViewModels.ToObservableChangeSet().TransformMany(c => c.PinViewModels)) + .Merge(OutputPinViewModels.ToObservableChangeSet()) + .Merge(OutputPinCollectionViewModels.ToObservableChangeSet().TransformMany(c => c.PinViewModels)) + .Bind(out ReadOnlyObservableCollection pins) + .Subscribe(); + + Pins = pins; + this.WhenActivated(d => { _isStaticNode = Node.WhenAnyValue(n => n.IsDefaultNode, n => n.IsExitNode) @@ -41,29 +75,30 @@ public class NodeViewModel : ActivatableViewModelBase .ToProperty(this, model => model.IsStaticNode) .DisposeWith(d); - Node.WhenAnyValue(n => n.Pins).Subscribe(pins => nodePins.Edit(source => + // Subscribe to pin changes + Node.WhenAnyValue(n => n.Pins).Subscribe(p => nodePins.Edit(source => { source.Clear(); - source.AddRange(pins); + source.AddRange(p); + })).DisposeWith(d); + // Subscribe to pin collection changes + Node.WhenAnyValue(n => n.PinCollections).Subscribe(c => nodePinCollections.Edit(source => + { + source.Clear(); + source.AddRange(c); })).DisposeWith(d); }); - - DeleteNode = ReactiveCommand.Create(ExecuteDeleteNode, this.WhenAnyValue(vm => vm.IsStaticNode).Select(v => !v)); - - nodePins.Connect().Filter(n => n.Direction == PinDirection.Input).Transform(nodePinVmFactory.InputPinViewModel).Bind(out ReadOnlyObservableCollection inputPins).Subscribe(); - nodePins.Connect().Filter(n => n.Direction == PinDirection.Output).Transform(nodePinVmFactory.OutputPinViewModel).Bind(out ReadOnlyObservableCollection outputPins).Subscribe(); - InputPinViewModels = inputPins; - OutputPinViewModels = outputPins; } public bool IsStaticNode => _isStaticNode?.Value ?? true; - public NodeScriptViewModel NodeScriptViewModel { get; set; } + public NodeScriptViewModel NodeScriptViewModel { get; } public INode Node { get; } public ReadOnlyObservableCollection InputPinViewModels { get; } public ReadOnlyObservableCollection InputPinCollectionViewModels { get; } public ReadOnlyObservableCollection OutputPinViewModels { get; } public ReadOnlyObservableCollection OutputPinCollectionViewModels { get; } + public ReadOnlyObservableCollection Pins { get; } public ICustomNodeViewModel? CustomNodeViewModel { @@ -83,16 +118,18 @@ public class NodeViewModel : ActivatableViewModelBase set => RaiseAndSetIfChanged(ref _isSelected, value); } - public void SaveDragOffset(Point mouseStartPosition) + public void StartDrag(Point mouseStartPosition) { if (!IsSelected) return; _dragOffsetX = Node.X - mouseStartPosition.X; _dragOffsetY = Node.Y - mouseStartPosition.Y; + _startX = Node.X; + _startY = Node.Y; } - public void UpdatePosition(Point mousePosition) + public void UpdateDrag(Point mousePosition) { if (!IsSelected) return; @@ -101,6 +138,13 @@ public class NodeViewModel : ActivatableViewModelBase Node.Y = Math.Round((mousePosition.Y + _dragOffsetY) / 10d, 0, MidpointRounding.AwayFromZero) * 10d; } + public MoveNode? FinishDrag() + { + if (IsSelected && (Math.Abs(_startX - Node.X) > 0.01 || Math.Abs(_startY - Node.Y) > 0.01)) + return new MoveNode(Node, Node.X, Node.Y, _startX, _startY); + return null; + } + private void ExecuteDeleteNode() { _nodeEditorService.ExecuteCommand(NodeScriptViewModel.NodeScript, new DeleteNode(NodeScriptViewModel.NodeScript, Node)); diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinCollectionView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinCollectionView.axaml index 3bf8ae477..a6a0f7e98 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinCollectionView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinCollectionView.axaml @@ -2,7 +2,18 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" + xmlns:pins="clr-namespace:Artemis.UI.Screens.VisualScripting.Pins" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Screens.VisualScripting.Pins.InputPinCollectionView"> - Welcome to Avalonia! + x:Class="Artemis.UI.Screens.VisualScripting.Pins.InputPinCollectionView" + x:DataType="pins:PinCollectionViewModel"> + + + + + diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinCollectionViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinCollectionViewModel.cs index 75488b75e..0edea5c81 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinCollectionViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinCollectionViewModel.cs @@ -1,4 +1,5 @@ using Artemis.Core; +using Artemis.UI.Ninject.Factories; namespace Artemis.UI.Screens.VisualScripting.Pins; @@ -6,7 +7,7 @@ public class InputPinCollectionViewModel : PinCollectionViewModel { public InputPinCollection InputPinCollection { get; } - public InputPinCollectionViewModel(InputPinCollection inputPinCollection) : base(inputPinCollection) + public InputPinCollectionViewModel(InputPinCollection inputPinCollection, INodePinVmFactory nodePinVmFactory) : base(inputPinCollection, nodePinVmFactory) { InputPinCollection = inputPinCollection; } diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml index 523457358..320a42a0c 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml @@ -23,7 +23,7 @@ - + diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml.cs index 44a65692c..8a2c267e5 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml.cs @@ -1,20 +1,86 @@ +using System.Linq; using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.PanAndZoom; +using Avalonia.Input; +using Avalonia.LogicalTree; using Avalonia.Markup.Xaml; +using Avalonia.Media; using Avalonia.ReactiveUI; +using Avalonia.VisualTree; namespace Artemis.UI.Screens.VisualScripting.Pins { public partial class InputPinView : ReactiveUserControl { + private bool _dragging; + private readonly Border _pinPoint; + private Canvas? _container; + public InputPinView() { InitializeComponent(); + _pinPoint = this.Get("PinPoint"); } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } + + private void PinPoint_OnPointerMoved(object? sender, PointerEventArgs e) + { + ZoomBorder? zoomBorder = this.FindAncestorOfType(); + PointerPoint point = e.GetCurrentPoint(zoomBorder); + if (ViewModel == null || zoomBorder == null || !point.Properties.IsLeftButtonPressed) + return; + + if (!_dragging) + { + e.Pointer.Capture(_pinPoint); + // ViewModel.StartDrag(); + } + + PointerPoint absolutePosition = e.GetCurrentPoint(null); + OutputPinView? target = (OutputPinView?) zoomBorder.GetLogicalDescendants().FirstOrDefault(d => d is OutputPinView v && v.TransformedBounds != null && v.TransformedBounds.Value.Contains(absolutePosition.Position)); + + // ViewModel.UpdateDrag(point.Position, target?.ViewModel); + e.Handled = true; + } + + private void PinPoint_OnPointerReleased(object? sender, PointerReleasedEventArgs e) + { + if (!_dragging) + return; + + _dragging = false; + e.Pointer.Capture(null); + // ViewModel.FinishDrag(); + e.Handled = true; + } + + /// + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + _container = this.FindAncestorOfType(); + } + + /// + public override void Render(DrawingContext context) + { + base.Render(context); + UpdatePosition(); + } + + private void UpdatePosition() + { + if (_container == null || ViewModel == null) + return; + + Matrix? transform = this.TransformToVisual(_container); + if (transform != null) + ViewModel.Position = new Point(Bounds.Width / 2, Bounds.Height / 2).Transform(transform.Value); + } } } diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinCollectionView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinCollectionView.axaml index 424369776..091246cd8 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinCollectionView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinCollectionView.axaml @@ -2,7 +2,18 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" + xmlns:pins="clr-namespace:Artemis.UI.Screens.VisualScripting.Pins" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Screens.VisualScripting.Pins.OutputPinCollectionView"> - Welcome to Avalonia! + x:Class="Artemis.UI.Screens.VisualScripting.Pins.OutputPinCollectionView" + x:DataType="pins:PinCollectionViewModel"> + + + + diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinCollectionViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinCollectionViewModel.cs index 7d8400019..c268a3bfd 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinCollectionViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinCollectionViewModel.cs @@ -1,4 +1,5 @@ using Artemis.Core; +using Artemis.UI.Ninject.Factories; namespace Artemis.UI.Screens.VisualScripting.Pins; @@ -6,7 +7,7 @@ public class OutputPinCollectionViewModel : PinCollectionViewModel { public OutputPinCollection OutputPinCollection { get; } - public OutputPinCollectionViewModel(OutputPinCollection outputPinCollection) : base(outputPinCollection) + public OutputPinCollectionViewModel(OutputPinCollection outputPinCollection, INodePinVmFactory nodePinVmFactory) : base(outputPinCollection, nodePinVmFactory) { OutputPinCollection = outputPinCollection; } diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinView.axaml.cs index 175d9bd0d..286dc7fcf 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinView.axaml.cs @@ -1,20 +1,50 @@ using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.PanAndZoom; using Avalonia.Markup.Xaml; +using Avalonia.Media; using Avalonia.ReactiveUI; +using Avalonia.VisualTree; +using ReactiveUI; namespace Artemis.UI.Screens.VisualScripting.Pins { public partial class OutputPinView : ReactiveUserControl { + private Canvas? _container; + public OutputPinView() { InitializeComponent(); } + /// + public override void Render(DrawingContext context) + { + base.Render(context); + UpdatePosition(); + } + + /// + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + _container = this.FindAncestorOfType(); + } + private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } + + private void UpdatePosition() + { + if (_container == null || ViewModel == null) + return; + + Matrix? transform = this.TransformToVisual(_container); + if (transform != null) + ViewModel.Position = new Point(Bounds.Width / 2, Bounds.Height / 2).Transform(transform.Value); + } } -} +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinCollectionViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinCollectionViewModel.cs index 42140ec4f..e521ecc8e 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinCollectionViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinCollectionViewModel.cs @@ -1,14 +1,51 @@ -using Artemis.Core; +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive; +using System.Reactive.Linq; +using Artemis.Core; +using Artemis.Core.Events; +using Artemis.UI.Ninject.Factories; using Artemis.UI.Shared; +using Avalonia.Controls.Mixins; +using DynamicData; +using ReactiveUI; namespace Artemis.UI.Screens.VisualScripting.Pins; public abstract class PinCollectionViewModel : ActivatableViewModelBase { - public PinCollection PinCollection { get; } + private readonly INodePinVmFactory _nodePinVmFactory; + public IPinCollection PinCollection { get; } + public ObservableCollection PinViewModels { get; } - protected PinCollectionViewModel(PinCollection pinCollection) + protected PinCollectionViewModel(IPinCollection pinCollection, INodePinVmFactory nodePinVmFactory) { + _nodePinVmFactory = nodePinVmFactory; + PinCollection = pinCollection; + PinViewModels = new ObservableCollection(); + + this.WhenActivated(d => + { + PinViewModels.Clear(); + PinViewModels.AddRange(PinCollection.Select(CreatePinViewModel)); + + Observable.FromEventPattern>(x => PinCollection.PinAdded += x, x => PinCollection.PinAdded -= x) + .Subscribe(e => PinViewModels.Add(CreatePinViewModel(e.EventArgs.Value))) + .DisposeWith(d); + Observable.FromEventPattern>(x => PinCollection.PinRemoved += x, x => PinCollection.PinRemoved -= x) + .Subscribe(e => PinViewModels.RemoveMany(PinViewModels.Where(p => p.Pin == e.EventArgs.Value).ToList())) + .DisposeWith(d); + }); + + AddPin = ReactiveCommand.Create(() => PinCollection.AddPin()); + } + + public ReactiveCommand AddPin { get; } + + private PinViewModel CreatePinViewModel(IPin pin) + { + return PinCollection.Direction == PinDirection.Input ? _nodePinVmFactory.InputPinViewModel(pin) : _nodePinVmFactory.OutputPinViewModel(pin); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinViewModel.cs index 0b1ce6202..84d9b1430 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinViewModel.cs @@ -1,22 +1,62 @@ -using Artemis.Core; +using System; +using System.Reactive.Linq; +using Artemis.Core; +using Artemis.Core.Events; using Artemis.Core.Services; using Artemis.UI.Shared; +using Artemis.UI.Shared.Extensions; +using Avalonia; +using Avalonia.Controls.Mixins; using Avalonia.Media; +using DynamicData; +using ReactiveUI; namespace Artemis.UI.Screens.VisualScripting.Pins; public abstract class PinViewModel : ActivatableViewModelBase { + private Point _position; + protected PinViewModel(IPin pin, INodeService nodeService) { Pin = pin; TypeColorRegistration registration = nodeService.GetTypeColorRegistration(Pin.Type); - PinColor = new Color(registration.Color.Alpha, registration.Color.Red, registration.Color.Green, registration.Color.Blue); - DarkenedPinColor = new Color(registration.DarkenedColor.Alpha, registration.DarkenedColor.Red, registration.DarkenedColor.Green, registration.DarkenedColor.Blue); + PinColor = registration.Color.ToColor(); + DarkenedPinColor = registration.DarkenedColor.ToColor(); + + SourceList connectedPins = new(); + this.WhenActivated(d => + { + Observable.FromEventPattern>(x => Pin.PinConnected += x, x => Pin.PinConnected -= x) + .Subscribe(e => connectedPins.Add(e.EventArgs.Value)) + .DisposeWith(d); + Observable.FromEventPattern>(x => Pin.PinDisconnected += x, x => Pin.PinDisconnected -= x) + .Subscribe(e => connectedPins.Remove(e.EventArgs.Value)) + .DisposeWith(d); + }); + + Connections = connectedPins.Connect().AsObservableList(); + connectedPins.AddRange(Pin.ConnectedTo); } + public IObservableList Connections { get; } + public IPin Pin { get; } public Color PinColor { get; } public Color DarkenedPinColor { get; } + + public Point Position + { + get => _position; + set => RaiseAndSetIfChanged(ref _position, value); + } + + public bool IsTypeCompatible(Type type) + { + return Pin.Type == type + || Pin.Type == typeof(Enum) && type.IsEnum + || Pin.Direction == PinDirection.Input && Pin.Type == typeof(object) + || Pin.Direction == PinDirection.Output && type == typeof(object); + } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/VisualScripting.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/VisualScripting.axaml index afd62ecf2..9dbad6ef5 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/VisualScripting.axaml +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/VisualScripting.axaml @@ -8,6 +8,7 @@