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

Node editor - Added cables

This commit is contained in:
Robert 2022-03-18 23:51:54 +01:00
parent 885cd852fc
commit 86b4258f5d
25 changed files with 655 additions and 163 deletions

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Artemis.Core.Events;
namespace Artemis.Core
@ -81,6 +82,8 @@ namespace Artemis.Core
OnPropertyChanged(nameof(ConnectedTo));
PinConnected?.Invoke(this, new SingleValueEventArgs<IPin>(pin));
if (!pin.ConnectedTo.Contains(this))
pin.ConnectTo(this);
}
/// <inheritdoc />
@ -90,6 +93,8 @@ namespace Artemis.Core
OnPropertyChanged(nameof(ConnectedTo));
PinDisconnected?.Invoke(this, new SingleValueEventArgs<IPin>(pin));
if (pin.ConnectedTo.Contains(this))
pin.DisconnectFrom(this);
}
/// <inheritdoc />
@ -101,7 +106,11 @@ namespace Artemis.Core
OnPropertyChanged(nameof(ConnectedTo));
foreach (IPin pin in connectedPins)
{
PinDisconnected?.Invoke(this, new SingleValueEventArgs<IPin>(pin));
if (pin.ConnectedTo.Contains(this))
pin.DisconnectFrom(this);
}
}
#endregion

View File

@ -6,7 +6,6 @@ using Artemis.UI.Screens.Plugins;
using Artemis.UI.Screens.ProfileEditor.DisplayConditions.Event;
using Artemis.UI.Screens.ProfileEditor.DisplayConditions.Static;
using Artemis.UI.Screens.ProfileEditor.LayerProperties;
using Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings;
using Artemis.UI.Screens.ProfileEditor.LayerProperties.LayerEffects;
using Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline;
using Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree;
@ -26,107 +25,106 @@ using Artemis.UI.Screens.Sidebar;
using Artemis.UI.Screens.Sidebar.Dialogs.ProfileEdit;
using Stylet;
namespace Artemis.UI.Ninject.Factories
namespace Artemis.UI.Ninject.Factories;
public interface IVmFactory
{
public interface IVmFactory
{
}
}
public interface ISettingsVmFactory : IVmFactory
{
PluginSettingsViewModel CreatePluginSettingsViewModel(Plugin plugin);
PluginFeatureViewModel CreatePluginFeatureViewModel(PluginFeatureInfo pluginFeatureInfo, bool showShield);
DeviceSettingsViewModel CreateDeviceSettingsViewModel(ArtemisDevice device);
}
public interface ISettingsVmFactory : IVmFactory
{
PluginSettingsViewModel CreatePluginSettingsViewModel(Plugin plugin);
PluginFeatureViewModel CreatePluginFeatureViewModel(PluginFeatureInfo pluginFeatureInfo, bool showShield);
DeviceSettingsViewModel CreateDeviceSettingsViewModel(ArtemisDevice device);
}
public interface IDeviceDebugVmFactory : IVmFactory
{
DeviceDialogViewModel DeviceDialogViewModel(ArtemisDevice device);
DevicePropertiesTabViewModel DevicePropertiesTabViewModel(ArtemisDevice device);
DeviceInfoTabViewModel DeviceInfoTabViewModel(ArtemisDevice device);
DeviceLedsTabViewModel DeviceLedsTabViewModel(ArtemisDevice device);
InputMappingsTabViewModel InputMappingsTabViewModel(ArtemisDevice device);
}
public interface IDeviceDebugVmFactory : IVmFactory
{
DeviceDialogViewModel DeviceDialogViewModel(ArtemisDevice device);
DevicePropertiesTabViewModel DevicePropertiesTabViewModel(ArtemisDevice device);
DeviceInfoTabViewModel DeviceInfoTabViewModel(ArtemisDevice device);
DeviceLedsTabViewModel DeviceLedsTabViewModel(ArtemisDevice device);
InputMappingsTabViewModel InputMappingsTabViewModel(ArtemisDevice device);
}
public interface IProfileTreeVmFactory : IVmFactory
{
FolderViewModel FolderViewModel(ProfileElement folder);
LayerViewModel LayerViewModel(ProfileElement layer);
}
public interface IProfileTreeVmFactory : IVmFactory
{
FolderViewModel FolderViewModel(ProfileElement folder);
LayerViewModel LayerViewModel(ProfileElement layer);
}
public interface ILayerHintVmFactory : IVmFactory
{
LayerHintsDialogViewModel LayerHintsDialogViewModel(Layer layer);
CategoryAdaptionHintViewModel CategoryAdaptionHintViewModel(CategoryAdaptionHint adaptionHint);
DeviceAdaptionHintViewModel DeviceAdaptionHintViewModel(DeviceAdaptionHint adaptionHint);
KeyboardSectionAdaptionHintViewModel KeyboardSectionAdaptionHintViewModel(KeyboardSectionAdaptionHint adaptionHint);
}
public interface ILayerHintVmFactory : IVmFactory
{
LayerHintsDialogViewModel LayerHintsDialogViewModel(Layer layer);
CategoryAdaptionHintViewModel CategoryAdaptionHintViewModel(CategoryAdaptionHint adaptionHint);
DeviceAdaptionHintViewModel DeviceAdaptionHintViewModel(DeviceAdaptionHint adaptionHint);
KeyboardSectionAdaptionHintViewModel KeyboardSectionAdaptionHintViewModel(KeyboardSectionAdaptionHint adaptionHint);
}
public interface IHeaderVmFactory : IVmFactory
{
SimpleHeaderViewModel SimpleHeaderViewModel(string displayName);
}
public interface IHeaderVmFactory : IVmFactory
{
SimpleHeaderViewModel SimpleHeaderViewModel(string displayName);
}
public interface IProfileLayerVmFactory : IVmFactory
{
ProfileLayerViewModel Create(Layer layer, PanZoomViewModel panZoomViewModel);
}
public interface IProfileLayerVmFactory : IVmFactory
{
ProfileLayerViewModel Create(Layer layer, PanZoomViewModel panZoomViewModel);
}
public interface IVisualizationToolVmFactory : IVmFactory
{
ViewpointMoveToolViewModel ViewpointMoveToolViewModel(PanZoomViewModel panZoomViewModel);
EditToolViewModel EditToolViewModel(PanZoomViewModel panZoomViewModel);
SelectionToolViewModel SelectionToolViewModel(PanZoomViewModel panZoomViewModel);
SelectionRemoveToolViewModel SelectionRemoveToolViewModel(PanZoomViewModel panZoomViewModel);
}
public interface IVisualizationToolVmFactory : IVmFactory
{
ViewpointMoveToolViewModel ViewpointMoveToolViewModel(PanZoomViewModel panZoomViewModel);
EditToolViewModel EditToolViewModel(PanZoomViewModel panZoomViewModel);
SelectionToolViewModel SelectionToolViewModel(PanZoomViewModel panZoomViewModel);
SelectionRemoveToolViewModel SelectionRemoveToolViewModel(PanZoomViewModel panZoomViewModel);
}
public interface ILayerPropertyVmFactory : IVmFactory
{
LayerPropertyViewModel LayerPropertyViewModel(ILayerProperty layerProperty);
public interface ILayerPropertyVmFactory : IVmFactory
{
LayerPropertyViewModel LayerPropertyViewModel(ILayerProperty layerProperty);
LayerPropertyGroupViewModel LayerPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup);
TreeGroupViewModel TreeGroupViewModel(LayerPropertyGroupViewModel layerPropertyGroupViewModel);
TimelineGroupViewModel TimelineGroupViewModel(LayerPropertyGroupViewModel layerPropertyGroupViewModel);
LayerPropertyGroupViewModel LayerPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup);
TreeGroupViewModel TreeGroupViewModel(LayerPropertyGroupViewModel layerPropertyGroupViewModel);
TimelineGroupViewModel TimelineGroupViewModel(LayerPropertyGroupViewModel layerPropertyGroupViewModel);
TreeViewModel TreeViewModel(LayerPropertiesViewModel layerPropertiesViewModel, IObservableCollection<LayerPropertyGroupViewModel> layerPropertyGroups);
EffectsViewModel EffectsViewModel(LayerPropertiesViewModel layerPropertiesViewModel);
TimelineViewModel TimelineViewModel(LayerPropertiesViewModel layerPropertiesViewModel, IObservableCollection<LayerPropertyGroupViewModel> layerPropertyGroups);
TimelineSegmentViewModel TimelineSegmentViewModel(SegmentViewModelType segment, IObservableCollection<LayerPropertyGroupViewModel> layerPropertyGroups);
}
TreeViewModel TreeViewModel(LayerPropertiesViewModel layerPropertiesViewModel, IObservableCollection<LayerPropertyGroupViewModel> layerPropertyGroups);
EffectsViewModel EffectsViewModel(LayerPropertiesViewModel layerPropertiesViewModel);
TimelineViewModel TimelineViewModel(LayerPropertiesViewModel layerPropertiesViewModel, IObservableCollection<LayerPropertyGroupViewModel> layerPropertyGroups);
TimelineSegmentViewModel TimelineSegmentViewModel(SegmentViewModelType segment, IObservableCollection<LayerPropertyGroupViewModel> layerPropertyGroups);
}
public interface IConditionVmFactory : IVmFactory
{
StaticConditionViewModel StaticConditionViewModel(StaticCondition staticCondition);
EventConditionViewModel EventConditionViewModel(EventCondition eventCondition);
}
public interface IConditionVmFactory : IVmFactory
{
StaticConditionViewModel StaticConditionViewModel(StaticCondition staticCondition);
EventConditionViewModel EventConditionViewModel(EventCondition eventCondition);
}
public interface IPrerequisitesVmFactory : IVmFactory
{
PluginPrerequisiteViewModel PluginPrerequisiteViewModel(PluginPrerequisite pluginPrerequisite, bool uninstall);
}
public interface IPrerequisitesVmFactory : IVmFactory
{
PluginPrerequisiteViewModel PluginPrerequisiteViewModel(PluginPrerequisite pluginPrerequisite, bool uninstall);
}
public interface IScriptVmFactory : IVmFactory
{
ScriptsDialogViewModel ScriptsDialogViewModel(Profile profile);
ScriptConfigurationViewModel ScriptConfigurationViewModel(ScriptConfiguration scriptConfiguration);
}
public interface IScriptVmFactory : IVmFactory
{
ScriptsDialogViewModel ScriptsDialogViewModel(Profile profile);
ScriptConfigurationViewModel ScriptConfigurationViewModel(ScriptConfiguration scriptConfiguration);
}
public interface ISidebarVmFactory : IVmFactory
{
SidebarCategoryViewModel SidebarCategoryViewModel(ProfileCategory profileCategory);
SidebarProfileConfigurationViewModel SidebarProfileConfigurationViewModel(ProfileConfiguration profileConfiguration);
ProfileConfigurationHotkeyViewModel ProfileConfigurationHotkeyViewModel(ProfileConfiguration profileConfiguration, bool isDisableHotkey);
ModuleActivationRequirementViewModel ModuleActivationRequirementViewModel(IModuleActivationRequirement activationRequirement);
}
public interface ISidebarVmFactory : IVmFactory
{
SidebarCategoryViewModel SidebarCategoryViewModel(ProfileCategory profileCategory);
SidebarProfileConfigurationViewModel SidebarProfileConfigurationViewModel(ProfileConfiguration profileConfiguration);
ProfileConfigurationHotkeyViewModel ProfileConfigurationHotkeyViewModel(ProfileConfiguration profileConfiguration, bool isDisableHotkey);
ModuleActivationRequirementViewModel ModuleActivationRequirementViewModel(IModuleActivationRequirement activationRequirement);
}
public interface INodeVmFactory : IVmFactory
{
NodeScriptWindowViewModel NodeScriptWindowViewModel(NodeScript nodeScript);
}
public interface INodeVmFactory : IVmFactory
{
NodeScriptWindowViewModel NodeScriptWindowViewModel(NodeScript nodeScript);
}
public interface IPropertyVmFactory
{
ITreePropertyViewModel TreePropertyViewModel(ILayerProperty layerProperty, LayerPropertyViewModel layerPropertyViewModel);
ITimelinePropertyViewModel TimelinePropertyViewModel(ILayerProperty layerProperty, LayerPropertyViewModel layerPropertyViewModel);
}
public interface IPropertyVmFactory
{
ITreePropertyViewModel TreePropertyViewModel(ILayerProperty layerProperty, LayerPropertyViewModel layerPropertyViewModel);
ITimelinePropertyViewModel TimelinePropertyViewModel(ILayerProperty layerProperty, LayerPropertyViewModel layerPropertyViewModel);
}

View File

@ -0,0 +1,20 @@
using Avalonia.Media;
using SkiaSharp;
namespace Artemis.UI.Shared.Extensions;
/// <summary>
/// Provides extension methods for the <see cref="SKColor"/> type.
/// </summary>
public static class SKColorExtensions
{
/// <summary>
/// Converts a SkiaSharp <see cref="SKColor"/> to an Avalonia <see cref="Color"/>.
/// </summary>
/// <param name="color">The color to convert.</param>
/// <returns>The resulting color.</returns>
public static Color ToColor(this SKColor color)
{
return new Color(color.Alpha, color.Red, color.Green, color.Blue);
}
}

View File

@ -29,6 +29,24 @@ public class MoveNode : INodeEditorCommand
_originalY = node.Y;
}
/// <summary>
/// Creates a new instance of the <see cref="MoveNode" /> class.
/// </summary>
/// <param name="node">The node to update.</param>
/// <param name="x">The new X-position.</param>
/// <param name="y">The new Y-position.</param>
/// <param name="originalX">The original X-position.</param>
/// <param name="originalY">The original Y-position.</param>
public MoveNode(INode node, double x, double y, double originalX, double originalY)
{
_node = node;
_x = x;
_y = y;
_originalX = originalX;
_originalY = originalY;
}
/// <inheritdoc />
public string DisplayName => "Move node";

View File

@ -2,29 +2,24 @@
using System.Globalization;
using Avalonia.Data.Converters;
using Avalonia.Media;
using RGBColor = RGB.NET.Core.Color;
namespace Artemis.UI.Converters
{
/// <summary>
/// Converts <see cref="T:RGB.NET.Core.Color" /> into <see cref="T:Avalonia.Media.Color" />.
/// Converts <see cref="Color" /> into <see cref="SolidColorBrush" />.
/// </summary>
public class ColorToSolidColorBrushConverter : IValueConverter
{
/// <inheritdoc />
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return new SolidColorBrush(!(value is RGBColor color)
? new Color(0, 0, 0, 0)
: new Color((byte) color.A, (byte) color.R, (byte) color.G, (byte) color.B));
return new SolidColorBrush(value is not Color color ? new Color(0, 0, 0, 0) : color);
}
/// <inheritdoc />
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return !(value is SolidColorBrush brush)
? RGBColor.Transparent
: new RGBColor(brush.Color.A, brush.Color.R, brush.Color.G, brush.Color.B);
return value is not SolidColorBrush brush ? new Color(0, 0, 0, 0) : brush.Color;
}
}
}

View File

@ -95,13 +95,14 @@ namespace Artemis.UI.Ninject.Factories
NodeScriptViewModel NodeScriptViewModel(NodeScript nodeScript);
NodePickerViewModel NodePickerViewModel(NodeScript nodeScript);
NodeViewModel NodeViewModel(NodeScriptViewModel nodeScriptViewModel, INode node);
CableViewModel CableViewModel(NodeScriptViewModel nodeScriptViewModel, IPin? from, IPin? to);
}
public interface INodePinVmFactory
{
PinCollectionViewModel InputPinCollectionViewModel(PinCollection inputPinCollection);
PinCollectionViewModel InputPinCollectionViewModel(IPinCollection inputPinCollection);
PinViewModel InputPinViewModel(IPin inputPin);
PinCollectionViewModel OutputPinCollectionViewModel(PinCollection outputPinCollection);
PinCollectionViewModel OutputPinCollectionViewModel(IPinCollection outputPinCollection);
PinViewModel OutputPinViewModel(IPin outputPin);
}
}

View File

@ -2,7 +2,38 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:visualScripting="clr-namespace:Artemis.UI.Screens.VisualScripting"
xmlns:converters="clr-namespace:Artemis.UI.Converters"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.VisualScripting.CableView">
Welcome to Avalonia!
</UserControl>
x:Class="Artemis.UI.Screens.VisualScripting.CableView"
x:DataType="visualScripting:CableViewModel"
ClipToBounds="False">
<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>
</UserControl>

View File

@ -1,20 +1,30 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Shapes;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
using ReactiveUI;
namespace Artemis.UI.Screens.VisualScripting
namespace Artemis.UI.Screens.VisualScripting;
public class CableView : ReactiveUserControl<CableViewModel>
{
public partial class CableView : ReactiveUserControl<CableViewModel>
public CableView()
{
public CableView()
{
InitializeComponent();
}
InitializeComponent();
Path cablePath = this.Get<Path>("CablePath");
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
// Swap a margin on and off of the cable path to ensure the visual is always invalidated
// This is a workaround for https://github.com/AvaloniaUI/Avalonia/issues/4748
this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.FromPoint)
.Subscribe(_ => cablePath.Margin = cablePath.Margin == new Thickness(0, 0, 0, 0) ? new Thickness(1, 1, 0, 0) : new Thickness(0, 0, 0, 0))
.DisposeWith(d));
}
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

View File

@ -1,28 +1,128 @@
using Artemis.UI.Screens.VisualScripting.Pins;
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Exceptions;
using Artemis.UI.Screens.VisualScripting.Pins;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Extensions;
using Avalonia;
using Avalonia.Media;
using Avalonia.Threading;
using DynamicData;
using ReactiveUI;
namespace Artemis.UI.Screens.VisualScripting;
public class CableViewModel : ActivatableViewModelBase
{
private PinViewModel _from;
private PinViewModel _to;
private const double CABLE_OFFSET = 24 * 4;
public CableViewModel(PinViewModel from, PinViewModel to)
private readonly NodeScriptViewModel _nodeScriptViewModel;
private PinDirection _dragDirection;
private Point _dragPoint;
private bool _isDragging;
private IPin? _from;
private IPin? _to;
private PinViewModel? _fromViewModel;
private PinViewModel? _toViewModel;
private readonly ObservableAsPropertyHelper<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)
{
if (from != null && from.Direction != PinDirection.Output)
throw new ArtemisUIException("Can only create cables originating from an output pin");
if (to != null && to.Direction != PinDirection.Input)
throw new ArtemisUIException("Can only create cables targeted to an input pin");
_nodeScriptViewModel = nodeScriptViewModel;
_from = from;
_to = to;
this.WhenActivated(d =>
{
if (From != null)
_nodeScriptViewModel.PinViewModels.Connect().Filter(p => p.Pin == From).Transform(model => FromViewModel = model).Subscribe().DisposeWith(d);
if (To != null)
_nodeScriptViewModel.PinViewModels.Connect().Filter(p => p.Pin == To).Transform(model => ToViewModel = model).Subscribe().DisposeWith(d);
});
_fromPoint = this.WhenAnyValue(vm => vm.FromViewModel).Select(p => p != null ? p.WhenAnyValue(pvm => pvm.Position) : Observable.Never<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);
}
public PinViewModel From
public IPin? From
{
get => _from;
set => RaiseAndSetIfChanged(ref _from, value);
}
public PinViewModel To
public IPin? To
{
get => _to;
set => RaiseAndSetIfChanged(ref _to, value);
}
public PinViewModel? FromViewModel
{
get => _fromViewModel;
set => RaiseAndSetIfChanged(ref _fromViewModel, value);
}
public PinViewModel? ToViewModel
{
get => _toViewModel;
set => RaiseAndSetIfChanged(ref _toViewModel, value);
}
public bool IsDragging
{
get => _isDragging;
set => RaiseAndSetIfChanged(ref _isDragging, value);
}
public PinDirection DragDirection
{
get => _dragDirection;
set => RaiseAndSetIfChanged(ref _dragDirection, value);
}
public Point DragPoint
{
get => _dragPoint;
set => RaiseAndSetIfChanged(ref _dragPoint, value);
}
public Point FromPoint => _fromPoint.Value;
public Point FromTargetPoint => _fromTargetPoint.Value;
public Point ToPoint => _toPoint.Value;
public Point ToTargetPoint => _toTargetPoint.Value;
public Color CableColor => _cableColor.Value;
public void StartDrag(PinDirection dragDirection)
{
IsDragging = true;
DragDirection = dragDirection;
}
public bool UpdateDrag(Point position, PinViewModel? targetViewModel)
{
DragPoint = position;
return true;
}
public void FinishDrag()
{
IsDragging = false;
}
}

View File

@ -19,7 +19,6 @@
</UserControl.KeyBindings>
<paz:ZoomBorder Name="ZoomBorder"
Stretch="None"
ClipToBounds="True"
Focusable="True"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
@ -27,8 +26,9 @@
ZoomChanged="ZoomBorder_OnZoomChanged"
MaxZoomX="1"
MaxZoomY="1"
EnableConstrains="True"
PointerReleased="ZoomBorder_OnPointerReleased">
<Grid Name="ContainerGrid" Background="Transparent">
<Grid Name="ContainerGrid" Background="Transparent" ClipToBounds="False">
<Grid.ContextFlyout>
<Flyout FlyoutPresenterClasses="node-picker-flyout">
<ContentControl Content="{CompiledBinding NodePickerViewModel}" />
@ -41,10 +41,10 @@
</Grid.Transitions>
<!-- Cables -->
<ItemsControl Items="{CompiledBinding CableViewModels}">
<ItemsControl Items="{CompiledBinding CableViewModels}" ClipToBounds="False">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.Styles>
@ -56,7 +56,7 @@
</ItemsControl>
<!-- Nodes -->
<ItemsControl Name="NodesContainer" Items="{CompiledBinding NodeViewModels}">
<ItemsControl Name="NodesContainer" Items="{CompiledBinding NodeViewModels}" ClipToBounds="False">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />

View File

@ -6,10 +6,14 @@ using System.Reactive.Linq;
using Artemis.Core;
using Artemis.Core.Events;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.VisualScripting.Pins;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.NodeEditor;
using Artemis.UI.Shared.Services.NodeEditor.Commands;
using Avalonia;
using Avalonia.Controls.Mixins;
using DynamicData;
using DynamicData.Binding;
using ReactiveUI;
namespace Artemis.UI.Screens.VisualScripting;
@ -17,11 +21,13 @@ namespace Artemis.UI.Screens.VisualScripting;
public class NodeScriptViewModel : ActivatableViewModelBase
{
private readonly INodeVmFactory _nodeVmFactory;
private readonly INodeEditorService _nodeEditorService;
private List<NodeViewModel>? _initialNodeSelection;
public NodeScriptViewModel(NodeScript nodeScript, INodeVmFactory nodeVmFactory, INodeEditorService nodeEditorService)
{
_nodeVmFactory = nodeVmFactory;
_nodeEditorService = nodeEditorService;
NodeScript = nodeScript;
NodePickerViewModel = _nodeVmFactory.NodePickerViewModel(nodeScript);
@ -37,16 +43,46 @@ public class NodeScriptViewModel : ActivatableViewModelBase
.DisposeWith(d);
});
// Create VMs for all nodes
NodeViewModels = new ObservableCollection<NodeViewModel>();
foreach (INode nodeScriptNode in NodeScript.Nodes)
NodeViewModels.Add(_nodeVmFactory.NodeViewModel(this, nodeScriptNode));
CableViewModels = new ObservableCollection<CableViewModel>();
// 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
.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 ObservableCollection<CableViewModel> CableViewModels { get; }
public ReadOnlyObservableCollection<CableViewModel> CableViewModels { get; }
public NodePickerViewModel NodePickerViewModel { get; }
public NodeEditorHistory History { get; }
@ -87,18 +123,28 @@ public class NodeScriptViewModel : ActivatableViewModelBase
public void StartNodeDrag(Point position)
{
foreach (NodeViewModel nodeViewModel in NodeViewModels)
nodeViewModel.SaveDragOffset(position);
nodeViewModel.StartDrag(position);
}
public void UpdateNodeDrag(Point position)
{
foreach (NodeViewModel nodeViewModel in NodeViewModels)
nodeViewModel.UpdatePosition(position);
nodeViewModel.UpdateDrag(position);
}
public void FinishNodeDrag()
{
// TODO: Command
List<MoveNode> commands = NodeViewModels.Select(n => n.FinishDrag()).Where(c => c != null).Cast<MoveNode>().ToList();
if (!commands.Any())
return;
if (commands.Count == 1)
_nodeEditorService.ExecuteCommand(NodeScript, commands.First());
using NodeEditorCommandScope scope = _nodeEditorService.CreateCommandScope(NodeScript, $"Move {commands.Count} nodes");
foreach (MoveNode moveNode in commands)
_nodeEditorService.ExecuteCommand(NodeScript, moveNode);
}
private void HandleNodeAdded(SingleValueEventArgs<INode> eventArgs)

View File

@ -57,14 +57,14 @@
<Grid Grid.Row="1" ColumnDefinitions="Auto,*,Auto" Margin="5">
<StackPanel Grid.Column="0">
<ItemsControl Items="{CompiledBinding InputPinViewModels}" />
<ItemsControl Items="{CompiledBinding InputPinViewModels}" Margin="4 0" />
<ItemsControl Items="{CompiledBinding InputPinCollectionViewModels}" />
</StackPanel>
<ContentControl Name="CustomViewModelContainer" Grid.Column="1" Content="{CompiledBinding CustomNodeViewModel}" IsVisible="{CompiledBinding CustomNodeViewModel}" />
<StackPanel Grid.Column="2">
<ItemsControl Items="{CompiledBinding OutputPinViewModels}" />
<ItemsControl Items="{CompiledBinding OutputPinViewModels}" Margin="4 0" />
<ItemsControl Items="{CompiledBinding OutputPinCollectionViewModels}" />
</StackPanel>
</Grid>

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.PanAndZoom;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
@ -50,27 +51,36 @@ public class NodeView : ReactiveUserControl<NodeViewModel>
_dragging = false;
ViewModel.NodeScriptViewModel.FinishNodeDrag();
e.Pointer.Capture(null);
return;
}
ViewModel.NodeScriptViewModel.UpdateNodeSelection(new List<NodeViewModel> {ViewModel}, e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Control));
ViewModel.NodeScriptViewModel.FinishNodeSelection();
else
{
ViewModel.NodeScriptViewModel.UpdateNodeSelection(new List<NodeViewModel> {ViewModel}, e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Control));
ViewModel.NodeScriptViewModel.FinishNodeSelection();
}
e.Handled = true;
}
private void InputElement_OnPointerMoved(object? sender, PointerEventArgs e)
{
PointerPoint point = e.GetCurrentPoint(this.FindAncestorOfType<ZoomBorder>());
PointerPoint point = e.GetCurrentPoint(this.FindAncestorOfType<Canvas>());
if (ViewModel == null || !point.Properties.IsLeftButtonPressed)
return;
if (!_dragging)
{
_dragging = true;
if (!ViewModel.IsSelected)
{
ViewModel.NodeScriptViewModel.UpdateNodeSelection(new List<NodeViewModel> {ViewModel}, false, false);
ViewModel.NodeScriptViewModel.FinishNodeSelection();
}
ViewModel.NodeScriptViewModel.StartNodeDrag(point.Position);
e.Pointer.Capture((IInputElement?) sender);
}
ViewModel.NodeScriptViewModel.UpdateNodeDrag(point.Position);
e.Handled = true;

View File

@ -4,7 +4,6 @@ using System.Reactive;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.SurfaceEditor;
using Artemis.UI.Screens.VisualScripting.Pins;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.NodeEditor;
@ -12,6 +11,7 @@ using Artemis.UI.Shared.Services.NodeEditor.Commands;
using Avalonia;
using Avalonia.Controls.Mixins;
using DynamicData;
using DynamicData.Binding;
using ReactiveUI;
namespace Artemis.UI.Screens.VisualScripting;
@ -22,10 +22,12 @@ public class NodeViewModel : ActivatableViewModelBase
private ICustomNodeViewModel? _customNodeViewModel;
private ReactiveCommand<Unit, Unit>? _deleteNode;
private ObservableAsPropertyHelper<bool>? _isStaticNode;
private double _dragOffsetX;
private double _dragOffsetY;
private bool _isSelected;
private ObservableAsPropertyHelper<bool>? _isStaticNode;
private double _startX;
private double _startY;
public NodeViewModel(NodeScriptViewModel nodeScriptViewModel, INode node, INodePinVmFactory nodePinVmFactory, INodeEditorService nodeEditorService)
{
@ -33,7 +35,39 @@ public class NodeViewModel : ActivatableViewModelBase
_nodeEditorService = nodeEditorService;
Node = node;
DeleteNode = ReactiveCommand.Create(ExecuteDeleteNode, this.WhenAnyValue(vm => vm.IsStaticNode).Select(v => !v));
SourceList<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();
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();
InputPinCollectionViewModels = inputPinCollections;
OutputPinCollectionViewModels = outputPinCollections;
// Create a single observable collection containing all pin view models
InputPinViewModels.ToObservableChangeSet()
.Merge(InputPinCollectionViewModels.ToObservableChangeSet().TransformMany(c => c.PinViewModels))
.Merge(OutputPinViewModels.ToObservableChangeSet())
.Merge(OutputPinCollectionViewModels.ToObservableChangeSet().TransformMany(c => c.PinViewModels))
.Bind(out ReadOnlyObservableCollection<PinViewModel> pins)
.Subscribe();
Pins = pins;
this.WhenActivated(d =>
{
_isStaticNode = Node.WhenAnyValue(n => n.IsDefaultNode, n => n.IsExitNode)
@ -41,29 +75,30 @@ public class NodeViewModel : ActivatableViewModelBase
.ToProperty(this, model => model.IsStaticNode)
.DisposeWith(d);
Node.WhenAnyValue(n => n.Pins).Subscribe(pins => nodePins.Edit(source =>
// Subscribe to pin changes
Node.WhenAnyValue(n => n.Pins).Subscribe(p => nodePins.Edit(source =>
{
source.Clear();
source.AddRange(pins);
source.AddRange(p);
})).DisposeWith(d);
// Subscribe to pin collection changes
Node.WhenAnyValue(n => n.PinCollections).Subscribe(c => nodePinCollections.Edit(source =>
{
source.Clear();
source.AddRange(c);
})).DisposeWith(d);
});
DeleteNode = ReactiveCommand.Create(ExecuteDeleteNode, this.WhenAnyValue(vm => vm.IsStaticNode).Select(v => !v));
nodePins.Connect().Filter(n => n.Direction == PinDirection.Input).Transform(nodePinVmFactory.InputPinViewModel).Bind(out ReadOnlyObservableCollection<PinViewModel> inputPins).Subscribe();
nodePins.Connect().Filter(n => n.Direction == PinDirection.Output).Transform(nodePinVmFactory.OutputPinViewModel).Bind(out ReadOnlyObservableCollection<PinViewModel> outputPins).Subscribe();
InputPinViewModels = inputPins;
OutputPinViewModels = outputPins;
}
public bool IsStaticNode => _isStaticNode?.Value ?? true;
public NodeScriptViewModel NodeScriptViewModel { get; set; }
public NodeScriptViewModel NodeScriptViewModel { get; }
public INode Node { get; }
public ReadOnlyObservableCollection<PinViewModel> InputPinViewModels { get; }
public ReadOnlyObservableCollection<PinCollectionViewModel> InputPinCollectionViewModels { get; }
public ReadOnlyObservableCollection<PinViewModel> OutputPinViewModels { get; }
public ReadOnlyObservableCollection<PinCollectionViewModel> OutputPinCollectionViewModels { get; }
public ReadOnlyObservableCollection<PinViewModel> Pins { get; }
public ICustomNodeViewModel? CustomNodeViewModel
{
@ -83,16 +118,18 @@ public class NodeViewModel : ActivatableViewModelBase
set => RaiseAndSetIfChanged(ref _isSelected, value);
}
public void SaveDragOffset(Point mouseStartPosition)
public void StartDrag(Point mouseStartPosition)
{
if (!IsSelected)
return;
_dragOffsetX = Node.X - mouseStartPosition.X;
_dragOffsetY = Node.Y - mouseStartPosition.Y;
_startX = Node.X;
_startY = Node.Y;
}
public void UpdatePosition(Point mousePosition)
public void UpdateDrag(Point mousePosition)
{
if (!IsSelected)
return;
@ -101,6 +138,13 @@ public class NodeViewModel : ActivatableViewModelBase
Node.Y = Math.Round((mousePosition.Y + _dragOffsetY) / 10d, 0, MidpointRounding.AwayFromZero) * 10d;
}
public MoveNode? FinishDrag()
{
if (IsSelected && (Math.Abs(_startX - Node.X) > 0.01 || Math.Abs(_startY - Node.Y) > 0.01))
return new MoveNode(Node, Node.X, Node.Y, _startX, _startY);
return null;
}
private void ExecuteDeleteNode()
{
_nodeEditorService.ExecuteCommand(NodeScriptViewModel.NodeScript, new DeleteNode(NodeScriptViewModel.NodeScript, Node));

View File

@ -2,7 +2,18 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:pins="clr-namespace:Artemis.UI.Screens.VisualScripting.Pins"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.VisualScripting.Pins.InputPinCollectionView">
Welcome to Avalonia!
x:Class="Artemis.UI.Screens.VisualScripting.Pins.InputPinCollectionView"
x:DataType="pins:PinCollectionViewModel">
<StackPanel>
<Button Classes="icon-button icon-button-small"
ToolTip.Tip="Add new pin"
Command="{CompiledBinding AddPin}">
<avalonia:MaterialIcon Kind="Add"></avalonia:MaterialIcon>
</Button>
<ItemsControl Items="{CompiledBinding PinViewModels}" Margin="4 0"></ItemsControl>
</StackPanel>
</UserControl>

View File

@ -1,4 +1,5 @@
using Artemis.Core;
using Artemis.UI.Ninject.Factories;
namespace Artemis.UI.Screens.VisualScripting.Pins;
@ -6,7 +7,7 @@ public class InputPinCollectionViewModel<T> : PinCollectionViewModel
{
public InputPinCollection<T> InputPinCollection { get; }
public InputPinCollectionViewModel(InputPinCollection<T> inputPinCollection) : base(inputPinCollection)
public InputPinCollectionViewModel(InputPinCollection<T> inputPinCollection, INodePinVmFactory nodePinVmFactory) : base(inputPinCollection, nodePinVmFactory)
{
InputPinCollection = inputPinCollection;
}

View File

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

View File

@ -1,20 +1,86 @@
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.PanAndZoom;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.ReactiveUI;
using Avalonia.VisualTree;
namespace Artemis.UI.Screens.VisualScripting.Pins
{
public partial class InputPinView : ReactiveUserControl<PinViewModel>
{
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);
}
}
}

View File

@ -2,7 +2,18 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:pins="clr-namespace:Artemis.UI.Screens.VisualScripting.Pins"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.VisualScripting.Pins.OutputPinCollectionView">
Welcome to Avalonia!
x:Class="Artemis.UI.Screens.VisualScripting.Pins.OutputPinCollectionView"
x:DataType="pins:PinCollectionViewModel">
<StackPanel>
<Button Classes="icon-button icon-button-small"
ToolTip.Tip="Add new pin"
HorizontalAlignment="Right"
Command="{CompiledBinding AddPin}">
<avalonia:MaterialIcon Kind="Add"></avalonia:MaterialIcon>
</Button>
<ItemsControl Items="{CompiledBinding PinViewModels}" HorizontalAlignment="Right" Margin="4 0"></ItemsControl>
</StackPanel>
</UserControl>

View File

@ -1,4 +1,5 @@
using Artemis.Core;
using Artemis.UI.Ninject.Factories;
namespace Artemis.UI.Screens.VisualScripting.Pins;
@ -6,7 +7,7 @@ public class OutputPinCollectionViewModel<T> : PinCollectionViewModel
{
public OutputPinCollection<T> OutputPinCollection { get; }
public OutputPinCollectionViewModel(OutputPinCollection<T> outputPinCollection) : base(outputPinCollection)
public OutputPinCollectionViewModel(OutputPinCollection<T> outputPinCollection, INodePinVmFactory nodePinVmFactory) : base(outputPinCollection, nodePinVmFactory)
{
OutputPinCollection = outputPinCollection;
}

View File

@ -1,20 +1,50 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.PanAndZoom;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.ReactiveUI;
using Avalonia.VisualTree;
using ReactiveUI;
namespace Artemis.UI.Screens.VisualScripting.Pins
{
public partial class OutputPinView : ReactiveUserControl<PinViewModel>
{
private Canvas? _container;
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);
}
}
}
}

View File

@ -1,14 +1,51 @@
using Artemis.Core;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.Core.Events;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Shared;
using Avalonia.Controls.Mixins;
using DynamicData;
using ReactiveUI;
namespace Artemis.UI.Screens.VisualScripting.Pins;
public abstract class PinCollectionViewModel : ActivatableViewModelBase
{
public PinCollection PinCollection { get; }
private readonly INodePinVmFactory _nodePinVmFactory;
public IPinCollection PinCollection { get; }
public ObservableCollection<PinViewModel> PinViewModels { get; }
protected PinCollectionViewModel(PinCollection pinCollection)
protected PinCollectionViewModel(IPinCollection pinCollection, INodePinVmFactory nodePinVmFactory)
{
_nodePinVmFactory = nodePinVmFactory;
PinCollection = pinCollection;
PinViewModels = new ObservableCollection<PinViewModel>();
this.WhenActivated(d =>
{
PinViewModels.Clear();
PinViewModels.AddRange(PinCollection.Select(CreatePinViewModel));
Observable.FromEventPattern<SingleValueEventArgs<IPin>>(x => PinCollection.PinAdded += x, x => PinCollection.PinAdded -= x)
.Subscribe(e => PinViewModels.Add(CreatePinViewModel(e.EventArgs.Value)))
.DisposeWith(d);
Observable.FromEventPattern<SingleValueEventArgs<IPin>>(x => PinCollection.PinRemoved += x, x => PinCollection.PinRemoved -= x)
.Subscribe(e => PinViewModels.RemoveMany(PinViewModels.Where(p => p.Pin == e.EventArgs.Value).ToList()))
.DisposeWith(d);
});
AddPin = ReactiveCommand.Create(() => PinCollection.AddPin());
}
public ReactiveCommand<Unit, IPin> AddPin { get; }
private PinViewModel CreatePinViewModel(IPin pin)
{
return PinCollection.Direction == PinDirection.Input ? _nodePinVmFactory.InputPinViewModel(pin) : _nodePinVmFactory.OutputPinViewModel(pin);
}
}

View File

@ -1,22 +1,62 @@
using Artemis.Core;
using System;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.Core.Events;
using Artemis.Core.Services;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Extensions;
using Avalonia;
using Avalonia.Controls.Mixins;
using Avalonia.Media;
using DynamicData;
using ReactiveUI;
namespace Artemis.UI.Screens.VisualScripting.Pins;
public abstract class PinViewModel : ActivatableViewModelBase
{
private Point _position;
protected PinViewModel(IPin pin, INodeService nodeService)
{
Pin = pin;
TypeColorRegistration registration = nodeService.GetTypeColorRegistration(Pin.Type);
PinColor = new Color(registration.Color.Alpha, registration.Color.Red, registration.Color.Green, registration.Color.Blue);
DarkenedPinColor = new Color(registration.DarkenedColor.Alpha, registration.DarkenedColor.Red, registration.DarkenedColor.Green, registration.DarkenedColor.Blue);
PinColor = registration.Color.ToColor();
DarkenedPinColor = registration.DarkenedColor.ToColor();
SourceList<IPin> connectedPins = new();
this.WhenActivated(d =>
{
Observable.FromEventPattern<SingleValueEventArgs<IPin>>(x => Pin.PinConnected += x, x => Pin.PinConnected -= x)
.Subscribe(e => connectedPins.Add(e.EventArgs.Value))
.DisposeWith(d);
Observable.FromEventPattern<SingleValueEventArgs<IPin>>(x => Pin.PinDisconnected += x, x => Pin.PinDisconnected -= x)
.Subscribe(e => connectedPins.Remove(e.EventArgs.Value))
.DisposeWith(d);
});
Connections = connectedPins.Connect().AsObservableList();
connectedPins.AddRange(Pin.ConnectedTo);
}
public IObservableList<IPin> Connections { get; }
public IPin Pin { get; }
public Color PinColor { get; }
public Color DarkenedPinColor { get; }
public Point Position
{
get => _position;
set => RaiseAndSetIfChanged(ref _position, value);
}
public bool IsTypeCompatible(Type type)
{
return Pin.Type == type
|| Pin.Type == typeof(Enum) && type.IsEnum
|| Pin.Direction == PinDirection.Input && Pin.Type == typeof(object)
|| Pin.Direction == PinDirection.Output && type == typeof(object);
}
}

View File

@ -8,6 +8,7 @@
<Style Selector="StackPanel#PinContainer">
<Setter Property="Height" Value="24" />
<Setter Property="Cursor" Value="Hand" />
</Style>
<Style Selector="StackPanel#PinContainer Border#PinPoint">
<Setter Property="Width" Value="13" />

View File

@ -1,4 +1,5 @@
using System;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using Artemis.Core;
@ -6,6 +7,7 @@ using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.VisualScripting;
using Artemis.UI.Shared.Services.Builders;
using Artemis.UI.Shared.Services.Interfaces;
using Artemis.VisualScripting.Nodes;
using Avalonia.Input;
using ReactiveUI;
using SkiaSharp;
@ -17,6 +19,7 @@ namespace Artemis.UI.Screens.Workshop
private readonly INotificationService _notificationService;
private StandardCursorType _selectedCursor;
private readonly ObservableAsPropertyHelper<Cursor> _cursor;
private ColorGradient _colorGradient = new()
{
new ColorGradientStop(new SKColor(0xFFFF6D00), 0f),
@ -35,7 +38,16 @@ namespace Artemis.UI.Screens.Workshop
DisplayName = "Workshop";
ShowNotification = ReactiveCommand.Create<NotificationSeverity>(ExecuteShowNotification);
VisualEditorViewModel = nodeVmFactory.NodeScriptViewModel(new NodeScript<bool>("Test script", "A test script"));
NodeScript<bool> testScript = new("Test script", "A test script");
INode exitNode = testScript.Nodes.Last();
exitNode.X = 200;
exitNode.Y = 100;
OrNode orNode = new() {X = 100, Y = 100};
testScript.AddNode(orNode);
orNode.Result.ConnectTo(exitNode.Pins.First());
VisualEditorViewModel = nodeVmFactory.NodeScriptViewModel(testScript);
}
public NodeScriptViewModel VisualEditorViewModel { get; }