1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

Node editor - Added connecting pins

This commit is contained in:
Robert 2022-03-19 22:58:14 +01:00
parent 86b4258f5d
commit 2907b86174
22 changed files with 535 additions and 363 deletions

View File

@ -26,6 +26,7 @@ namespace Artemis.Core
/// <inheritdoc />
public event EventHandler<SingleValueEventArgs<IPin>>? PinConnected;
/// <inheritdoc />
public event EventHandler<SingleValueEventArgs<IPin>>? PinDisconnected;
@ -79,31 +80,29 @@ namespace Artemis.Core
public void ConnectTo(IPin pin)
{
_connectedTo.Add(pin);
OnPropertyChanged(nameof(ConnectedTo));
PinConnected?.Invoke(this, new SingleValueEventArgs<IPin>(pin));
if (!pin.ConnectedTo.Contains(this))
pin.ConnectTo(this);
OnPropertyChanged(nameof(ConnectedTo));
PinConnected?.Invoke(this, new SingleValueEventArgs<IPin>(pin));
}
/// <inheritdoc />
public void DisconnectFrom(IPin pin)
{
_connectedTo.Remove(pin);
OnPropertyChanged(nameof(ConnectedTo));
PinDisconnected?.Invoke(this, new SingleValueEventArgs<IPin>(pin));
if (pin.ConnectedTo.Contains(this))
pin.DisconnectFrom(this);
OnPropertyChanged(nameof(ConnectedTo));
PinDisconnected?.Invoke(this, new SingleValueEventArgs<IPin>(pin));
}
/// <inheritdoc />
public void DisconnectAll()
{
List<IPin> 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

View File

@ -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);
}
}

View File

@ -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<IPin>? _originalConnections;
public ConnectPins(IPin source, IPin target)
{
_source = source;
_target = target;
_originalConnections = _target.Direction == PinDirection.Input ? new List<IPin>(_target.ConnectedTo) : null;
}
#region Implementation of INodeEditorCommand
/// <inheritdoc />
public string DisplayName => "Connect pins";
/// <inheritdoc />
public void Execute()
{
if (_target.Direction == PinDirection.Input)
_target.DisconnectAll();
_source.ConnectTo(_target);
}
/// <inheritdoc />
public void Undo()
{
_target.DisconnectFrom(_source);
if (_originalConnections == null)
return;
foreach (IPin pin in _originalConnections)
_target.ConnectTo(pin);
}
#endregion
}

View File

@ -23,22 +23,22 @@
</VisualBrush>
</Styles.Resources>
<!-- Custom controls -->
<!-- Custom controls -->
<StyleInclude Source="/Styles/Controls/GradientPicker.axaml" />
<StyleInclude Source="/Styles/Controls/GradientPickerButton.axaml" />
<!-- Custom styles -->
<StyleInclude Source="/Styles/Border.axaml" />
<!-- Custom styles -->
<StyleInclude Source="/Styles/Border.axaml" />
<StyleInclude Source="/Styles/Button.axaml" />
<StyleInclude Source="/Styles/Condensed.axaml" />
<StyleInclude Source="/Styles/TextBlock.axaml" />
<StyleInclude Source="/Styles/Condensed.axaml" />
<StyleInclude Source="/Styles/TextBlock.axaml" />
<StyleInclude Source="/Styles/Sidebar.axaml" />
<StyleInclude Source="/Styles/InfoBar.axaml" />
<StyleInclude Source="/Styles/TextBox.axaml" />
<StyleInclude Source="/Styles/Notifications.axaml" />
<StyleInclude Source="/Styles/NumberBox.axaml" />
<StyleInclude Source="/Styles/TreeView.axaml" />
<Style Selector="Window:windows:windows10 /template/ Border#RootBorder">
<!-- This will show if custom accent color setting is used in Settings page-->
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}" />
@ -49,4 +49,4 @@
<Setter Property="BorderBrush" Value="#3d3d3d" />
<Setter Property="BorderThickness" Value="0 1 0 0" />
</Style>
</Styles>
</Styles>

View File

@ -63,6 +63,9 @@
<Compile Update="Screens\ProfileEditor\Panels\VisualEditor\Visualizers\LayerShapeVisualizerView.axaml.cs">
<DependentUpon>LayerShapeVisualizerView.axaml</DependentUpon>
</Compile>
<Compile Update="Screens\VisualScripting\DragCableView.axaml.cs">
<DependentUpon>DragCableView.axaml</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<Folder Include="DefaultTypes\DataModel\Display\" />

View File

@ -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

View File

@ -11,29 +11,25 @@
<UserControl.Resources>
<converters:ColorToSolidColorBrushConverter x:Key="ColorToSolidColorBrushConverter" />
</UserControl.Resources>
<Canvas Name="CableCanvas">
<Path Name="CablePath"
Stroke="{CompiledBinding CableColor, Converter={StaticResource ColorToSolidColorBrushConverter}}"
StrokeThickness="4"
StrokeLineCap="Round">
<Path.Transitions>
<Transitions>
<ThicknessTransition Property="Margin" Duration="200"></ThicknessTransition>
</Transitions>
</Path.Transitions>
<Path.Data>
<PathGeometry>
<PathGeometry.Figures>
<PathFigure StartPoint="{CompiledBinding FromPoint}" IsClosed="False">
<PathFigure.Segments>
<BezierSegment Point1="{CompiledBinding FromTargetPoint}"
Point2="{CompiledBinding ToTargetPoint}"
Point3="{CompiledBinding ToPoint}" />
</PathFigure.Segments>
</PathFigure>
</PathGeometry.Figures>
</PathGeometry>
</Path.Data>
</Path>
</Canvas>
<Path Name="CablePath"
Stroke="{CompiledBinding CableColor, Converter={StaticResource ColorToSolidColorBrushConverter}}"
StrokeThickness="4"
StrokeLineCap="Round">
<Path.Transitions>
<Transitions>
<ThicknessTransition Property="Margin" Duration="200"></ThicknessTransition>
</Transitions>
</Path.Transitions>
<Path.Data>
<PathGeometry>
<PathGeometry.Figures>
<PathFigure IsClosed="False">
<PathFigure.Segments>
<BezierSegment />
</PathFigure.Segments>
</PathFigure>
</PathGeometry.Figures>
</PathGeometry>
</Path.Data>
</Path>
</UserControl>

View File

@ -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<CableViewModel>
{
private const double CABLE_OFFSET = 24 * 4;
private readonly Path _cablePath;
public CableView()
{
InitializeComponent();
Path cablePath = this.Get<Path>("CablePath");
_cablePath = this.Get<Path>("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);
}
}

View File

@ -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<Color> _cableColor;
private readonly ObservableAsPropertyHelper<Point> _fromPoint;
private readonly ObservableAsPropertyHelper<Point> _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<Point> _fromPoint;
private readonly ObservableAsPropertyHelper<Point> _fromTargetPoint;
private readonly ObservableAsPropertyHelper<Point> _toPoint;
private readonly ObservableAsPropertyHelper<Point> _toTargetPoint;
private readonly ObservableAsPropertyHelper<Color> _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<Point>()).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<Point>()).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<Point>())
.Switch()
.ToProperty(this, vm => vm.FromPoint);
_toPoint = this.WhenAnyValue(vm => vm.ToViewModel)
.Select(p => p != null ? p.WhenAnyValue(pvm => pvm.Position) : Observable.Never<Point>())
.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;
}
}

View File

@ -0,0 +1,30 @@
<UserControl xmlns="https://github.com/avaloniaui"
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.DragCableView"
x:DataType="visualScripting:DragCableViewModel"
ClipToBounds="False">
<UserControl.Resources>
<converters:ColorToSolidColorBrushConverter x:Key="ColorToSolidColorBrushConverter" />
</UserControl.Resources>
<Path Name="CablePath"
Stroke="{CompiledBinding PinViewModel.PinColor, Converter={StaticResource ColorToSolidColorBrushConverter}}"
StrokeThickness="4"
StrokeLineCap="Round">
<Path.Data>
<PathGeometry>
<PathGeometry.Figures>
<PathFigure IsClosed="False">
<PathFigure.Segments>
<BezierSegment />
</PathFigure.Segments>
</PathFigure>
</PathGeometry.Figures>
</PathGeometry>
</Path.Data>
</Path>
</UserControl>

View File

@ -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<DragCableViewModel>
{
private const double CABLE_OFFSET = 24 * 4;
private readonly Path _cablePath;
public DragCableView()
{
InitializeComponent();
_cablePath = this.Get<Path>("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);
}
}

View File

@ -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<Point>? _fromPoint;
private ObservableAsPropertyHelper<Point>? _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);
}
}

View File

@ -40,6 +40,9 @@
</Transitions>
</Grid.Transitions>
<!-- Drag cable, if any -->
<ContentControl Content="{CompiledBinding DragViewModel}" ClipToBounds="False"/>
<!-- Cables -->
<ItemsControl Items="{CompiledBinding CableViewModels}" ClipToBounds="False">
<ItemsControl.ItemsPanel>

View File

@ -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<NodeScriptViewModel>
{
public partial class NodeScriptView : ReactiveUserControl<NodeScriptViewModel>
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<Grid>("ContainerGrid");
_zoomBorder = this.Find<ZoomBorder>("ZoomBorder");
_nodesContainer = this.Find<ItemsControl>("NodesContainer");
_selectionRectangle = this.Find<SelectionRectangle>("SelectionRectangle");
_zoomBorder.PropertyChanged += ZoomBorderOnPropertyChanged;
UpdateZoomBorderBackground();
_nodesContainer = this.Find<ItemsControl>("NodesContainer");
_zoomBorder = this.Find<ZoomBorder>("ZoomBorder");
_grid = this.Find<Grid>("ContainerGrid");
_selectionRectangle = this.Find<SelectionRectangle>("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<ItemContainerInfo> itemContainerInfos = _nodesContainer.ItemContainerGenerator.Containers.Where(c => c.ContainerControl.Bounds.Intersects(e.Rectangle)).ToList();
List<NodeViewModel> 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<ItemContainerInfo> itemContainerInfos = _nodesContainer.ItemContainerGenerator.Containers.Where(c => c.ContainerControl.Bounds.Intersects(e.Rectangle)).ToList();
List<NodeViewModel> 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();
}
}

View File

@ -20,14 +20,17 @@ namespace Artemis.UI.Screens.VisualScripting;
public class NodeScriptViewModel : ActivatableViewModelBase
{
private readonly INodeVmFactory _nodeVmFactory;
private readonly INodeEditorService _nodeEditorService;
private readonly SourceList<NodeViewModel> _nodeViewModels;
private readonly INodeVmFactory _nodeVmFactory;
private DragCableViewModel? _dragViewModel;
private List<NodeViewModel>? _initialNodeSelection;
public NodeScriptViewModel(NodeScript nodeScript, INodeVmFactory nodeVmFactory, INodeEditorService nodeEditorService)
{
_nodeVmFactory = nodeVmFactory;
_nodeEditorService = nodeEditorService;
_nodeViewModels = new SourceList<NodeViewModel>();
NodeScript = nodeScript;
NodePickerViewModel = _nodeVmFactory.NodePickerViewModel(nodeScript);
@ -44,48 +47,42 @@ public class NodeScriptViewModel : ActivatableViewModelBase
});
// Create VMs for all nodes
NodeViewModels = new ObservableCollection<NodeViewModel>();
foreach (INode nodeScriptNode in NodeScript.Nodes)
NodeViewModels.Add(_nodeVmFactory.NodeViewModel(this, nodeScriptNode));
_nodeViewModels.Connect().Bind(out ReadOnlyObservableCollection<NodeViewModel> 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<PinViewModel> pinViewModels)
.Subscribe();
PinViewModels = pinViewModels;
// Observe all outgoing pin connections and create cables for them
IObservable<IChangeSet<NodeViewModel>> 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<CableViewModel> cableViewModels)
.Subscribe();
CableViewModels = cableViewModels;
}
public IObservableList<PinViewModel> 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<NodeViewModel> NodeViewModels { get; }
public ReadOnlyObservableCollection<NodeViewModel> NodeViewModels { get; }
public ReadOnlyObservableCollection<PinViewModel> PinViewModels { get; }
public ReadOnlyObservableCollection<CableViewModel> CableViewModels { get; }
public NodePickerViewModel NodePickerViewModel { get; }
public NodeEditorHistory History { get; }
public DragCableViewModel? DragViewModel
{
get => _dragViewModel;
set => RaiseAndSetIfChanged(ref _dragViewModel, value);
}
public void UpdateNodeSelection(List<NodeViewModel> 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<INode> eventArgs)
{
NodeViewModels.Add(_nodeVmFactory.NodeViewModel(this, eventArgs.Value));
_nodeViewModels.Add(_nodeVmFactory.NodeViewModel(this, eventArgs.Value));
}
private void HandleNodeRemoved(SingleValueEventArgs<INode> 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);
}
}

View File

@ -39,34 +39,44 @@ public class NodeViewModel : ActivatableViewModelBase
SourceList<IPin> nodePins = new();
SourceList<IPinCollection> 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<PinViewModel> inputPins).Subscribe();
nodePins.Connect().Filter(n => n.Direction == PinDirection.Output).Transform(nodePinVmFactory.OutputPinViewModel)
.Bind(out ReadOnlyObservableCollection<PinViewModel> outputPins).Subscribe();
nodePins.Connect()
.Filter(n => n.Direction == PinDirection.Input)
.Transform(nodePinVmFactory.InputPinViewModel)
.Bind(out ReadOnlyObservableCollection<PinViewModel> inputPins)
.Subscribe();
nodePins.Connect()
.Filter(n => n.Direction == PinDirection.Output)
.Transform(nodePinVmFactory.OutputPinViewModel)
.Bind(out ReadOnlyObservableCollection<PinViewModel> 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<PinCollectionViewModel> inputPinCollections).Subscribe();
nodePinCollections.Connect().Filter(n => n.Direction == PinDirection.Output).Transform(nodePinVmFactory.OutputPinCollectionViewModel)
.Bind(out ReadOnlyObservableCollection<PinCollectionViewModel> outputPinCollections).Subscribe();
nodePinCollections.Connect()
.Filter(n => n.Direction == PinDirection.Input)
.Transform(nodePinVmFactory.InputPinCollectionViewModel)
.Bind(out ReadOnlyObservableCollection<PinCollectionViewModel> inputPinCollections)
.Subscribe();
nodePinCollections.Connect()
.Filter(n => n.Direction == PinDirection.Output)
.Transform(nodePinVmFactory.OutputPinCollectionViewModel)
.Bind(out ReadOnlyObservableCollection<PinCollectionViewModel> 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<PinViewModel> pins)
.Subscribe();
Pins = pins;
PinViewModels = pins;
this.WhenActivated(d =>
{
@ -98,7 +108,7 @@ public class NodeViewModel : ActivatableViewModelBase
public ReadOnlyObservableCollection<PinCollectionViewModel> InputPinCollectionViewModels { get; }
public ReadOnlyObservableCollection<PinViewModel> OutputPinViewModels { get; }
public ReadOnlyObservableCollection<PinCollectionViewModel> OutputPinCollectionViewModels { get; }
public ReadOnlyObservableCollection<PinViewModel> Pins { get; }
public ReadOnlyObservableCollection<PinViewModel> PinViewModels { get; }
public ICustomNodeViewModel? CustomNodeViewModel
{

View File

@ -23,7 +23,7 @@
</Style>
</UserControl.Styles>
<StackPanel Name="PinContainer" Orientation="Horizontal" Spacing="6">
<Border Name="PinPoint" PointerMoved="PinPoint_OnPointerMoved" PointerReleased="PinPoint_OnPointerReleased">
<Border Name="PinPoint">
<Border Name="VisualPinPoint" />
</Border>
<TextBlock Name="PinName" VerticalAlignment="Center" Text="{CompiledBinding Pin.Name}" />

View File

@ -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<PinViewModel>
public InputPinView()
{
private bool _dragging;
private readonly Border _pinPoint;
private Canvas? _container;
public InputPinView()
{
InitializeComponent();
_pinPoint = this.Get<Border>("PinPoint");
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void PinPoint_OnPointerMoved(object? sender, PointerEventArgs e)
{
ZoomBorder? zoomBorder = this.FindAncestorOfType<ZoomBorder>();
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;
}
/// <inheritdoc />
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
_container = this.FindAncestorOfType<Canvas>();
}
/// <inheritdoc />
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<Border>("PinPoint"));
}
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

View File

@ -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<PinViewModel>
public OutputPinView()
{
private Canvas? _container;
InitializeComponent();
InitializePin(this.Get<Border>("PinPoint"));
}
public OutputPinView()
{
InitializeComponent();
}
/// <inheritdoc />
public override void Render(DrawingContext context)
{
base.Render(context);
UpdatePosition();
}
/// <inheritdoc />
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
_container = this.FindAncestorOfType<Canvas>();
}
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);
}
}

View File

@ -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<PinViewModel>
{
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<NodeScriptView>()?.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<NodeScriptView>()?.ViewModel?.FinishPinDrag(ViewModel, targetPin);
_pinPoint.Cursor = new Cursor(StandardCursorType.Hand);
e.Handled = true;
}
#region Overrides of Visual
/// <inheritdoc />
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
_container = this.FindAncestorOfType<Canvas>();
}
/// <inheritdoc />
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
}

View File

@ -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);
}
}

View File

@ -40,8 +40,8 @@ namespace Artemis.UI.Screens.Workshop
NodeScript<bool> 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);