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