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:
parent
885cd852fc
commit
86b4258f5d
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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 />
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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}" />
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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" />
|
||||
|
||||
@ -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; }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user