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:
parent
86b4258f5d
commit
2907b86174
@ -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
|
||||
|
||||
14
src/Avalonia/Artemis.UI.Shared/Extensions/PointExtensions.cs
Normal file
14
src/Avalonia/Artemis.UI.Shared/Extensions/PointExtensions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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>
|
||||
@ -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\" />
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
{
|
||||
|
||||
@ -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}" />
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user