From 2907b86174febd0ec592f40d842a1b4ceace6b46 Mon Sep 17 00:00:00 2001 From: Robert Date: Sat, 19 Mar 2022 22:58:14 +0100 Subject: [PATCH] Node editor - Added connecting pins --- src/Artemis.Core/VisualScripting/Pin.cs | 17 +-- .../Extensions/PointExtensions.cs | 14 +++ .../NodeEditor/Commands/ConnectPins.cs | 45 +++++++ .../Artemis.UI.Shared/Styles/Artemis.axaml | 14 +-- src/Avalonia/Artemis.UI/Artemis.UI.csproj | 3 + .../Ninject/Factories/IVMFactory.cs | 3 +- .../Screens/VisualScripting/CableView.axaml | 46 ++++--- .../VisualScripting/CableView.axaml.cs | 32 ++++- .../Screens/VisualScripting/CableViewModel.cs | 100 +++------------ .../VisualScripting/DragCableView.axaml | 30 +++++ .../VisualScripting/DragCableView.axaml.cs | 47 +++++++ .../VisualScripting/DragCableViewModel.cs | 51 ++++++++ .../VisualScripting/NodeScriptView.axaml | 3 + .../VisualScripting/NodeScriptView.axaml.cs | 117 +++++++++--------- .../VisualScripting/NodeScriptViewModel.cs | 87 ++++++++----- .../Screens/VisualScripting/NodeViewModel.cs | 36 ++++-- .../VisualScripting/Pins/InputPinView.axaml | 2 +- .../Pins/InputPinView.axaml.cs | 92 ++------------ .../Pins/OutputPinView.axaml.cs | 52 ++------ .../Screens/VisualScripting/Pins/PinView.cs | 90 ++++++++++++++ .../VisualScripting/Pins/PinViewModel.cs | 13 +- .../Screens/Workshop/WorkshopViewModel.cs | 4 +- 22 files changed, 535 insertions(+), 363 deletions(-) create mode 100644 src/Avalonia/Artemis.UI.Shared/Extensions/PointExtensions.cs create mode 100644 src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/ConnectPins.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/VisualScripting/DragCableView.axaml create mode 100644 src/Avalonia/Artemis.UI/Screens/VisualScripting/DragCableView.axaml.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/VisualScripting/DragCableViewModel.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinView.cs diff --git a/src/Artemis.Core/VisualScripting/Pin.cs b/src/Artemis.Core/VisualScripting/Pin.cs index 0407014eb..0d48693af 100644 --- a/src/Artemis.Core/VisualScripting/Pin.cs +++ b/src/Artemis.Core/VisualScripting/Pin.cs @@ -26,6 +26,7 @@ namespace Artemis.Core /// public event EventHandler>? PinConnected; + /// public event EventHandler>? PinDisconnected; @@ -79,31 +80,29 @@ namespace Artemis.Core public void ConnectTo(IPin pin) { _connectedTo.Add(pin); - OnPropertyChanged(nameof(ConnectedTo)); - - PinConnected?.Invoke(this, new SingleValueEventArgs(pin)); if (!pin.ConnectedTo.Contains(this)) pin.ConnectTo(this); + + OnPropertyChanged(nameof(ConnectedTo)); + PinConnected?.Invoke(this, new SingleValueEventArgs(pin)); } /// public void DisconnectFrom(IPin pin) { _connectedTo.Remove(pin); - OnPropertyChanged(nameof(ConnectedTo)); - - PinDisconnected?.Invoke(this, new SingleValueEventArgs(pin)); if (pin.ConnectedTo.Contains(this)) pin.DisconnectFrom(this); + + OnPropertyChanged(nameof(ConnectedTo)); + PinDisconnected?.Invoke(this, new SingleValueEventArgs(pin)); } /// public void DisconnectAll() { List connectedPins = new(_connectedTo); - _connectedTo.Clear(); - OnPropertyChanged(nameof(ConnectedTo)); foreach (IPin pin in connectedPins) { @@ -111,6 +110,8 @@ namespace Artemis.Core if (pin.ConnectedTo.Contains(this)) pin.DisconnectFrom(this); } + + OnPropertyChanged(nameof(ConnectedTo)); } #endregion diff --git a/src/Avalonia/Artemis.UI.Shared/Extensions/PointExtensions.cs b/src/Avalonia/Artemis.UI.Shared/Extensions/PointExtensions.cs new file mode 100644 index 000000000..eb822fbbd --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Extensions/PointExtensions.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia; + +namespace Artemis.UI.Shared.Extensions +{ + public static class PointExtensions + { + public static Point Empty = new(0, 0); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/ConnectPins.cs b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/ConnectPins.cs new file mode 100644 index 000000000..7a02da355 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/ConnectPins.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using Artemis.Core; +using Artemis.UI.Shared.Services.NodeEditor; + +public class ConnectPins : INodeEditorCommand +{ + private readonly IPin _source; + private readonly IPin _target; + private readonly List? _originalConnections; + + public ConnectPins(IPin source, IPin target) + { + _source = source; + _target = target; + + _originalConnections = _target.Direction == PinDirection.Input ? new List(_target.ConnectedTo) : null; + } + + #region Implementation of INodeEditorCommand + + /// + public string DisplayName => "Connect pins"; + + /// + public void Execute() + { + if (_target.Direction == PinDirection.Input) + _target.DisconnectAll(); + _source.ConnectTo(_target); + } + + /// + public void Undo() + { + _target.DisconnectFrom(_source); + + if (_originalConnections == null) + return; + foreach (IPin pin in _originalConnections) + _target.ConnectTo(pin); + } + + #endregion +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Styles/Artemis.axaml b/src/Avalonia/Artemis.UI.Shared/Styles/Artemis.axaml index b3572739e..c81abdb36 100644 --- a/src/Avalonia/Artemis.UI.Shared/Styles/Artemis.axaml +++ b/src/Avalonia/Artemis.UI.Shared/Styles/Artemis.axaml @@ -23,22 +23,22 @@ - + - - + + - - + + - + - + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Artemis.UI.csproj b/src/Avalonia/Artemis.UI/Artemis.UI.csproj index 44930e0ee..16018ec21 100644 --- a/src/Avalonia/Artemis.UI/Artemis.UI.csproj +++ b/src/Avalonia/Artemis.UI/Artemis.UI.csproj @@ -63,6 +63,9 @@ LayerShapeVisualizerView.axaml + + DragCableView.axaml + diff --git a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs index fb9493c2c..4bada05d2 100644 --- a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs +++ b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs @@ -95,7 +95,8 @@ 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); + CableViewModel CableViewModel(NodeScriptViewModel nodeScriptViewModel, IPin from, IPin to); + DragCableViewModel DragCableViewModel(PinViewModel pinViewModel); } public interface INodePinVmFactory diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml index 01c50c703..c60dda5ff 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml @@ -11,29 +11,25 @@ - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + \ 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 1c16f8cf0..c8f92e883 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml.cs @@ -1,9 +1,11 @@ using System; +using System.Linq; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Mixins; using Avalonia.Controls.Shapes; using Avalonia.Markup.Xaml; +using Avalonia.Media; using Avalonia.ReactiveUI; using ReactiveUI; @@ -11,20 +13,38 @@ namespace Artemis.UI.Screens.VisualScripting; public class CableView : ReactiveUserControl { + private const double CABLE_OFFSET = 24 * 4; + private readonly Path _cablePath; + public CableView() { InitializeComponent(); - Path cablePath = this.Get("CablePath"); + _cablePath = this.Get("CablePath"); - // 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)); + // Not using bindings here to avoid a warnings + this.WhenActivated(d => + { + ViewModel.WhenAnyValue(vm => vm.FromPoint).Subscribe(_ => Update()).DisposeWith(d); + ViewModel.WhenAnyValue(vm => vm.ToPoint).Subscribe(_ => Update()).DisposeWith(d); + Update(); + }); } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } + + private void Update() + { + // Workaround for https://github.com/AvaloniaUI/Avalonia/issues/4748 + _cablePath.Margin = _cablePath.Margin != new Thickness(0, 0, 0, 0) ? new Thickness(0, 0, 0, 0) : new Thickness(1, 1, 0, 0); + + PathFigure pathFigure = ((PathGeometry) _cablePath.Data).Figures.First(); + BezierSegment segment = (BezierSegment) pathFigure.Segments!.First(); + pathFigure.StartPoint = ViewModel!.FromPoint; + segment.Point1 = new Point(ViewModel.FromPoint.X + CABLE_OFFSET, ViewModel.FromPoint.Y); + segment.Point2 = new Point(ViewModel.ToPoint.X - CABLE_OFFSET, ViewModel.ToPoint.Y); + segment.Point3 = new Point(ViewModel.ToPoint.X, ViewModel.ToPoint.Y); + } } \ 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 06f202258..38991d633 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableViewModel.cs @@ -1,76 +1,53 @@ 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 DynamicData.Binding; using ReactiveUI; namespace Artemis.UI.Screens.VisualScripting; public class CableViewModel : ActivatableViewModelBase { - private const double CABLE_OFFSET = 24 * 4; + private readonly ObservableAsPropertyHelper _cableColor; + private readonly ObservableAsPropertyHelper _fromPoint; + private readonly ObservableAsPropertyHelper _toPoint; - 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) + public CableViewModel(NodeScriptViewModel nodeScriptViewModel, IPin from, IPin to) { - if (from != null && from.Direction != PinDirection.Output) + if (from.Direction != PinDirection.Output) throw new ArtemisUIException("Can only create cables originating from an output pin"); - if (to != null && to.Direction != PinDirection.Input) + if (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); + nodeScriptViewModel.PinViewModels.ToObservableChangeSet().Filter(p => ReferenceEquals(p.Pin, from)).Transform(model => FromViewModel = model).Subscribe().DisposeWith(d); + nodeScriptViewModel.PinViewModels.ToObservableChangeSet().Filter(p => ReferenceEquals(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); - } + _fromPoint = this.WhenAnyValue(vm => vm.FromViewModel) + .Select(p => p != null ? p.WhenAnyValue(pvm => pvm.Position) : Observable.Never()) + .Switch() + .ToProperty(this, vm => vm.FromPoint); + _toPoint = this.WhenAnyValue(vm => vm.ToViewModel) + .Select(p => p != null ? p.WhenAnyValue(pvm => pvm.Position) : Observable.Never()) + .Switch() + .ToProperty(this, vm => vm.ToPoint); - public IPin? From - { - get => _from; - set => RaiseAndSetIfChanged(ref _from, value); - } - - public IPin? To - { - get => _to; - set => RaiseAndSetIfChanged(ref _to, value); + _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? FromViewModel @@ -85,44 +62,7 @@ public class CableViewModel : ActivatableViewModelBase 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/DragCableView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/DragCableView.axaml new file mode 100644 index 000000000..579726b33 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/DragCableView.axaml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/DragCableView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/DragCableView.axaml.cs new file mode 100644 index 000000000..243576bb9 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/DragCableView.axaml.cs @@ -0,0 +1,47 @@ +using System; +using System.Linq; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Mixins; +using Avalonia.Controls.Shapes; +using Avalonia.Markup.Xaml; +using Avalonia.Media; +using Avalonia.ReactiveUI; +using ReactiveUI; + +namespace Artemis.UI.Screens.VisualScripting; + +public class DragCableView : ReactiveUserControl +{ + private const double CABLE_OFFSET = 24 * 4; + private readonly Path _cablePath; + + public DragCableView() + { + InitializeComponent(); + _cablePath = this.Get("CablePath"); + + // Not using bindings here to avoid warnings + this.WhenActivated(d => + { + ViewModel.WhenAnyValue(vm => vm.FromPoint).Subscribe(_ => Update()).DisposeWith(d); + ViewModel.WhenAnyValue(vm => vm.ToPoint).Subscribe(_ => Update()).DisposeWith(d); + Update(); + }); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void Update() + { + PathFigure pathFigure = ((PathGeometry) _cablePath.Data).Figures.First(); + BezierSegment segment = (BezierSegment) pathFigure.Segments!.First(); + pathFigure.StartPoint = ViewModel!.FromPoint; + segment.Point1 = new Point(ViewModel.FromPoint.X + CABLE_OFFSET, ViewModel.FromPoint.Y); + segment.Point2 = new Point(ViewModel.ToPoint.X - CABLE_OFFSET, ViewModel.ToPoint.Y); + segment.Point3 = new Point(ViewModel.ToPoint.X, ViewModel.ToPoint.Y); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/DragCableViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/DragCableViewModel.cs new file mode 100644 index 000000000..39bb9259a --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/DragCableViewModel.cs @@ -0,0 +1,51 @@ +using System.Reactive.Disposables; +using System.Reactive.Linq; +using Artemis.Core; +using Artemis.UI.Screens.VisualScripting.Pins; +using Artemis.UI.Shared; +using Avalonia; +using ReactiveUI; + +namespace Artemis.UI.Screens.VisualScripting; + +public class DragCableViewModel : ActivatableViewModelBase +{ + private Point _dragPoint; + + private ObservableAsPropertyHelper? _fromPoint; + private ObservableAsPropertyHelper? _toPoint; + + public DragCableViewModel(PinViewModel pinViewModel) + { + PinViewModel = pinViewModel; + + // If the pin is output, the pin is the from-point and the drag position is the to-point + if (PinViewModel.Pin.Direction == PinDirection.Output) + { + this.WhenActivated(d => + { + _fromPoint = PinViewModel.WhenAnyValue(vm => vm.Position).ToProperty(this, vm => vm.FromPoint).DisposeWith(d); + }); + _toPoint = this.WhenAnyValue(vm => vm.DragPoint).ToProperty(this, vm => vm.ToPoint); + } + // If the pin is input, the pin is the to-point and the drag position is the from-point; + else + { + this.WhenActivated(d => + { + _toPoint = PinViewModel.WhenAnyValue(vm => vm.Position).ToProperty(this, vm => vm.ToPoint).DisposeWith(d); + }); + _fromPoint = this.WhenAnyValue(vm => vm.DragPoint).ToProperty(this, vm => vm.FromPoint); + } + } + + public PinViewModel PinViewModel { get; } + public Point FromPoint => _fromPoint?.Value ?? new Point(0, 0); + public Point ToPoint => _toPoint?.Value ?? new Point(0, 0); + + public Point DragPoint + { + get => _dragPoint; + set => RaiseAndSetIfChanged(ref _dragPoint, value); + } +} \ 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 100b3d59c..a2f6256f3 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml @@ -40,6 +40,9 @@ + + + diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs index 9ace2bbf8..cf4a8e15b 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; using Artemis.UI.Shared.Controls; @@ -12,76 +11,74 @@ using Avalonia.Interactivity; using Avalonia.Markup.Xaml; using Avalonia.Media; using Avalonia.ReactiveUI; -using Avalonia.VisualTree; -namespace Artemis.UI.Screens.VisualScripting +namespace Artemis.UI.Screens.VisualScripting; + +public class NodeScriptView : ReactiveUserControl { - public partial class NodeScriptView : ReactiveUserControl + private readonly Grid _grid; + private readonly ItemsControl _nodesContainer; + private readonly SelectionRectangle _selectionRectangle; + private readonly ZoomBorder _zoomBorder; + + public NodeScriptView() { - private readonly ZoomBorder _zoomBorder; - private readonly Grid _grid; - private readonly ItemsControl _nodesContainer; - private readonly SelectionRectangle _selectionRectangle; + InitializeComponent(); - public NodeScriptView() - { - InitializeComponent(); + _grid = this.Find("ContainerGrid"); + _zoomBorder = this.Find("ZoomBorder"); + _nodesContainer = this.Find("NodesContainer"); + _selectionRectangle = this.Find("SelectionRectangle"); + _zoomBorder.PropertyChanged += ZoomBorderOnPropertyChanged; + UpdateZoomBorderBackground(); - _nodesContainer = this.Find("NodesContainer"); - _zoomBorder = this.Find("ZoomBorder"); - _grid = this.Find("ContainerGrid"); - _selectionRectangle = this.Find("SelectionRectangle"); - _zoomBorder.PropertyChanged += ZoomBorderOnPropertyChanged; + _grid.AddHandler(PointerReleasedEvent, CanvasOnPointerReleased, RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble, true); + } + + private void CanvasOnPointerReleased(object? sender, PointerReleasedEventArgs e) + { + // If the flyout handled the click, update the position of the node picker + if (e.Handled && ViewModel != null) + ViewModel.NodePickerViewModel.Position = e.GetPosition(_grid); + } + + private void ZoomBorderOnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property.Name == nameof(_zoomBorder.Background)) UpdateZoomBorderBackground(); + } - _grid?.AddHandler(PointerReleasedEvent, CanvasOnPointerReleased, RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble, true); - } + private void UpdateZoomBorderBackground() + { + if (_zoomBorder.Background is VisualBrush visualBrush) + visualBrush.DestinationRect = new RelativeRect(_zoomBorder.OffsetX * -1, _zoomBorder.OffsetY * -1, 20, 20, RelativeUnit.Absolute); + } - private void CanvasOnPointerReleased(object? sender, PointerReleasedEventArgs e) - { - // If the flyout handled the click, update the position of the node picker - if (e.Handled && ViewModel != null) - ViewModel.NodePickerViewModel.Position = e.GetPosition(_grid); - } + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } - private void ZoomBorderOnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) - { - if (e.Property.Name == nameof(_zoomBorder.Background)) - UpdateZoomBorderBackground(); - } + private void ZoomBorder_OnZoomChanged(object sender, ZoomChangedEventArgs e) + { + UpdateZoomBorderBackground(); + } - private void UpdateZoomBorderBackground() - { - if (_zoomBorder.Background is VisualBrush visualBrush) - visualBrush.DestinationRect = new RelativeRect(_zoomBorder.OffsetX * -1, _zoomBorder.OffsetY * -1, 20, 20, RelativeUnit.Absolute); - } + private void SelectionRectangle_OnSelectionUpdated(object? sender, SelectionRectangleEventArgs e) + { + List itemContainerInfos = _nodesContainer.ItemContainerGenerator.Containers.Where(c => c.ContainerControl.Bounds.Intersects(e.Rectangle)).ToList(); + List nodes = itemContainerInfos.Where(c => c.Item is NodeViewModel).Select(c => (NodeViewModel) c.Item).ToList(); + ViewModel?.UpdateNodeSelection(nodes, e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Control)); + } - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } + private void SelectionRectangle_OnSelectionFinished(object? sender, SelectionRectangleEventArgs e) + { + ViewModel?.FinishNodeSelection(); + } - private void ZoomBorder_OnZoomChanged(object sender, ZoomChangedEventArgs e) - { - UpdateZoomBorderBackground(); - } - - private void SelectionRectangle_OnSelectionUpdated(object? sender, SelectionRectangleEventArgs e) - { - List itemContainerInfos = _nodesContainer.ItemContainerGenerator.Containers.Where(c => c.ContainerControl.Bounds.Intersects(e.Rectangle)).ToList(); - List nodes = itemContainerInfos.Where(c => c.Item is NodeViewModel).Select(c => (NodeViewModel) c.Item).ToList(); - ViewModel?.UpdateNodeSelection(nodes, e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Control)); - } - - private void SelectionRectangle_OnSelectionFinished(object? sender, SelectionRectangleEventArgs e) - { - ViewModel?.FinishNodeSelection(); - } - - private void ZoomBorder_OnPointerReleased(object? sender, PointerReleasedEventArgs e) - { - if (!_selectionRectangle.IsSelecting) - ViewModel?.ClearNodeSelection(); - } + private void ZoomBorder_OnPointerReleased(object? sender, PointerReleasedEventArgs e) + { + if (!_selectionRectangle.IsSelecting) + ViewModel?.ClearNodeSelection(); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs index 62c787d7d..7f97cc8ca 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs @@ -20,14 +20,17 @@ namespace Artemis.UI.Screens.VisualScripting; public class NodeScriptViewModel : ActivatableViewModelBase { - private readonly INodeVmFactory _nodeVmFactory; private readonly INodeEditorService _nodeEditorService; + private readonly SourceList _nodeViewModels; + private readonly INodeVmFactory _nodeVmFactory; + private DragCableViewModel? _dragViewModel; private List? _initialNodeSelection; public NodeScriptViewModel(NodeScript nodeScript, INodeVmFactory nodeVmFactory, INodeEditorService nodeEditorService) { _nodeVmFactory = nodeVmFactory; _nodeEditorService = nodeEditorService; + _nodeViewModels = new SourceList(); NodeScript = nodeScript; NodePickerViewModel = _nodeVmFactory.NodePickerViewModel(nodeScript); @@ -44,48 +47,42 @@ public class NodeScriptViewModel : ActivatableViewModelBase }); // Create VMs for all nodes - NodeViewModels = new ObservableCollection(); - foreach (INode nodeScriptNode in NodeScript.Nodes) - NodeViewModels.Add(_nodeVmFactory.NodeViewModel(this, nodeScriptNode)); + _nodeViewModels.Connect().Bind(out ReadOnlyObservableCollection nodeViewModels).Subscribe(); + _nodeViewModels.Edit(l => + { + foreach (INode nodeScriptNode in NodeScript.Nodes) + l.Add(_nodeVmFactory.NodeViewModel(this, nodeScriptNode)); + }); + NodeViewModels = nodeViewModels; + + NodeViewModels.ToObservableChangeSet().TransformMany(vm => vm.PinViewModels) + .Bind(out ReadOnlyObservableCollection pinViewModels) + .Subscribe(); + PinViewModels = pinViewModels; // 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 + PinViewModels.ToObservableChangeSet() + .Filter(p => p.Pin.Direction == PinDirection.Output) + .TransformMany(p => p.Connections) + .Transform(pin => _nodeVmFactory.CableViewModel(this, pin.ConnectedTo.First(), 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 ReadOnlyObservableCollection NodeViewModels { get; } + public ReadOnlyObservableCollection PinViewModels { get; } public ReadOnlyObservableCollection CableViewModels { get; } public NodePickerViewModel NodePickerViewModel { get; } public NodeEditorHistory History { get; } + public DragCableViewModel? DragViewModel + { + get => _dragViewModel; + set => RaiseAndSetIfChanged(ref _dragViewModel, value); + } + public void UpdateNodeSelection(List nodes, bool expand, bool invert) { _initialNodeSelection ??= NodeViewModels.Where(vm => vm.IsSelected).ToList(); @@ -147,15 +144,37 @@ public class NodeScriptViewModel : ActivatableViewModelBase _nodeEditorService.ExecuteCommand(NodeScript, moveNode); } + public bool UpdatePinDrag(PinViewModel sourcePinViewModel, PinViewModel? targetPinVmModel, Point position) + { + if (DragViewModel?.PinViewModel != sourcePinViewModel) + DragViewModel = new DragCableViewModel(sourcePinViewModel); + + DragViewModel.DragPoint = position; + + return targetPinVmModel == null || targetPinVmModel.IsCompatibleWith(sourcePinViewModel); + } + + public void FinishPinDrag(PinViewModel sourcePinViewModel, PinViewModel? targetPinVmModel) + { + if (DragViewModel == null) + return; + + DragViewModel = null; + + // If dropped on top of a compatible pin, connect to it + if (targetPinVmModel != null && targetPinVmModel.IsCompatibleWith(sourcePinViewModel)) + _nodeEditorService.ExecuteCommand(NodeScript, new ConnectPins(sourcePinViewModel.Pin, targetPinVmModel.Pin)); + } + private void HandleNodeAdded(SingleValueEventArgs eventArgs) { - NodeViewModels.Add(_nodeVmFactory.NodeViewModel(this, eventArgs.Value)); + _nodeViewModels.Add(_nodeVmFactory.NodeViewModel(this, eventArgs.Value)); } private void HandleNodeRemoved(SingleValueEventArgs eventArgs) { - NodeViewModel? toRemove = NodeViewModels.FirstOrDefault(vm => vm.Node == eventArgs.Value); + NodeViewModel? toRemove = NodeViewModels.FirstOrDefault(vm => ReferenceEquals(vm.Node, eventArgs.Value)); if (toRemove != null) - NodeViewModels.Remove(toRemove); + _nodeViewModels.Remove(toRemove); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs index 1f25b3cd1..144b76d5a 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs @@ -39,34 +39,44 @@ public class NodeViewModel : ActivatableViewModelBase 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(); + 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(); + 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(InputPinCollectionViewModels.ToObservableChangeSet().TransformMany(c => c.PinViewModels)) .Merge(OutputPinCollectionViewModels.ToObservableChangeSet().TransformMany(c => c.PinViewModels)) .Bind(out ReadOnlyObservableCollection pins) .Subscribe(); - Pins = pins; + PinViewModels = pins; this.WhenActivated(d => { @@ -98,7 +108,7 @@ public class NodeViewModel : ActivatableViewModelBase public ReadOnlyObservableCollection InputPinCollectionViewModels { get; } public ReadOnlyObservableCollection OutputPinViewModels { get; } public ReadOnlyObservableCollection OutputPinCollectionViewModels { get; } - public ReadOnlyObservableCollection Pins { get; } + public ReadOnlyObservableCollection PinViewModels { get; } public ICustomNodeViewModel? CustomNodeViewModel { diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml index 320a42a0c..523457358 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 8a2c267e5..817739ceb 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml.cs @@ -1,86 +1,18 @@ -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 +namespace Artemis.UI.Screens.VisualScripting.Pins; + +public class InputPinView : PinView { - public partial class InputPinView : ReactiveUserControl + public InputPinView() { - 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); - } + InitializeComponent(); + InitializePin(this.Get("PinPoint")); } -} + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file 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 286dc7fcf..a62070e1d 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinView.axaml.cs @@ -1,50 +1,18 @@ -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 +namespace Artemis.UI.Screens.VisualScripting.Pins; + +public class OutputPinView : PinView { - public partial class OutputPinView : ReactiveUserControl + public OutputPinView() { - private Canvas? _container; + InitializeComponent(); + InitializePin(this.Get("PinPoint")); + } - 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); - } + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinView.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinView.cs new file mode 100644 index 000000000..099936779 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinView.cs @@ -0,0 +1,90 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Media; +using Avalonia.ReactiveUI; +using Avalonia.VisualTree; + +namespace Artemis.UI.Screens.VisualScripting.Pins; + +public class PinView : ReactiveUserControl +{ + private bool _dragging; + private Canvas? _container; + private Border? _pinPoint; + + protected void InitializePin(Border pinPoint) + { + _pinPoint = pinPoint; + _pinPoint.PointerMoved += PinPointOnPointerMoved; + _pinPoint.PointerReleased += PinPointOnPointerReleased; + } + + private void PinPointOnPointerMoved(object? sender, PointerEventArgs e) + { + if (ViewModel == null || _container == null || _pinPoint == null) + return; + + NodeScriptViewModel? nodeScriptViewModel = this.FindAncestorOfType()?.ViewModel; + PointerPoint point = e.GetCurrentPoint(_container); + if (nodeScriptViewModel == null || !point.Properties.IsLeftButtonPressed) + return; + + if (!_dragging) + e.Pointer.Capture(_pinPoint); + + PinViewModel? targetPin = (_container.InputHitTest(point.Position) as Border)?.DataContext as PinViewModel; + if (targetPin == ViewModel) + targetPin = null; + + _pinPoint.Cursor = new Cursor(nodeScriptViewModel.UpdatePinDrag(ViewModel, targetPin, point.Position) ? StandardCursorType.Hand : StandardCursorType.No); + _dragging = true; + e.Handled = true; + } + + private void PinPointOnPointerReleased(object? sender, PointerReleasedEventArgs e) + { + if (!_dragging || ViewModel == null || _container == null || _pinPoint == null) + return; + + _dragging = false; + e.Pointer.Capture(null); + + PointerPoint point = e.GetCurrentPoint(_container); + PinViewModel? targetPin = (_container.InputHitTest(point.Position) as Border)?.DataContext as PinViewModel; + if (targetPin == ViewModel) + targetPin = null; + + this.FindAncestorOfType()?.ViewModel?.FinishPinDrag(ViewModel, targetPin); + _pinPoint.Cursor = new Cursor(StandardCursorType.Hand); + e.Handled = true; + } + + #region Overrides of Visual + + /// + 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); + } + + #endregion +} \ 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 84d9b1430..ba6ad4d4e 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinViewModel.cs @@ -52,11 +52,16 @@ public abstract class PinViewModel : ActivatableViewModelBase set => RaiseAndSetIfChanged(ref _position, value); } - public bool IsTypeCompatible(Type type) + public bool IsCompatibleWith(PinViewModel pinViewModel) { - return Pin.Type == type - || Pin.Type == typeof(Enum) && type.IsEnum + if (pinViewModel.Pin.Direction == Pin.Direction) + return false; + if (pinViewModel.Pin.Node == Pin.Node) + return false; + + return Pin.Type == pinViewModel.Pin.Type + || Pin.Type == typeof(Enum) && pinViewModel.Pin.Type.IsEnum || Pin.Direction == PinDirection.Input && Pin.Type == typeof(object) - || Pin.Direction == PinDirection.Output && type == typeof(object); + || Pin.Direction == PinDirection.Output && pinViewModel.Pin.Type == typeof(object); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs index eceeccf8b..930f8bdf0 100644 --- a/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs @@ -40,8 +40,8 @@ namespace Artemis.UI.Screens.Workshop NodeScript testScript = new("Test script", "A test script"); INode exitNode = testScript.Nodes.Last(); - exitNode.X = 200; - exitNode.Y = 100; + exitNode.X = 300; + exitNode.Y = 150; OrNode orNode = new() {X = 100, Y = 100}; testScript.AddNode(orNode);