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.Screens.VisualScripting.Pins; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services.NodeEditor; using Artemis.UI.Shared.Services.NodeEditor.Commands; using Avalonia; using Avalonia.Controls.Mixins; using Avalonia.Layout; using DynamicData; using DynamicData.Binding; using ReactiveUI; namespace Artemis.UI.Screens.VisualScripting; public class NodeViewModel : ActivatableViewModelBase { private readonly INodeEditorService _nodeEditorService; private readonly IWindowService _windowService; private ICustomNodeViewModel? _customNodeViewModel; private double _dragOffsetX; private double _dragOffsetY; private ObservableAsPropertyHelper? _hasInputPins; private ObservableAsPropertyHelper? _hasOutputPins; private bool _isSelected; private ObservableAsPropertyHelper? _isStaticNode; private double _startX; private double _startY; private bool _displayCustomViewModelAbove; private bool _displayCustomViewModelBetween; private bool _displayCustomViewModelBelow; private VerticalAlignment _customViewModelVerticalAlignment; public NodeViewModel(NodeScriptViewModel nodeScriptViewModel, INode node, INodeVmFactory nodeVmFactory, INodeEditorService nodeEditorService, IWindowService windowService) { _nodeEditorService = nodeEditorService; _windowService = windowService; NodeScriptViewModel = nodeScriptViewModel; Node = node; SourceList nodePins = new(); SourceList nodePinCollections = new(); // Create observable collections split up by direction nodePins.Connect().Filter(n => n.Pin.Direction == PinDirection.Input).Bind(out ReadOnlyObservableCollection inputPins).Subscribe(); nodePins.Connect().Filter(n => n.Pin.Direction == PinDirection.Output).Bind(out ReadOnlyObservableCollection outputPins).Subscribe(); InputPinViewModels = inputPins; OutputPinViewModels = outputPins; // Same again but for pin collections nodePinCollections.Connect().Filter(n => n.PinCollection.Direction == PinDirection.Input).Bind(out ReadOnlyObservableCollection inputPinCollections).Subscribe(); nodePinCollections.Connect().Filter(n => n.PinCollection.Direction == PinDirection.Output).Bind(out ReadOnlyObservableCollection outputPinCollections).Subscribe(); InputPinCollectionViewModels = inputPinCollections; OutputPinCollectionViewModels = outputPinCollections; // Create a single observable collection containing all pin view models nodePins.Connect().Merge(nodePinCollections.Connect().TransformMany(c => c.PinViewModels)).Bind(out ReadOnlyObservableCollection pins).Subscribe(); PinViewModels = pins; DeleteNode = ReactiveCommand.Create(ExecuteDeleteNode, this.WhenAnyValue(vm => vm.IsStaticNode).Select(v => !v)); ShowBrokenState = ReactiveCommand.Create(ExecuteShowBrokenState); this.WhenActivated(d => { _isStaticNode = Node.WhenAnyValue(n => n.IsDefaultNode, n => n.IsExitNode) .Select(tuple => tuple.Item1 || tuple.Item2) .ToProperty(this, model => model.IsStaticNode) .DisposeWith(d); _hasInputPins = InputPinViewModels.ToObservableChangeSet() .Merge(InputPinCollectionViewModels.ToObservableChangeSet().TransformMany(c => c.PinViewModels)) .Any() .ToProperty(this, vm => vm.HasInputPins) .DisposeWith(d); _hasOutputPins = OutputPinViewModels.ToObservableChangeSet() .Merge(OutputPinCollectionViewModels.ToObservableChangeSet().TransformMany(c => c.PinViewModels)) .Any() .ToProperty(this, vm => vm.HasOutputPins) .DisposeWith(d); // Subscribe to pin changes Observable.FromEventPattern>(x => Node.PinAdded += x, x => Node.PinAdded -= x) .Subscribe(p => { if (p.EventArgs.Value.Direction == PinDirection.Input) nodePins.Add(nodeVmFactory.InputPinViewModel(p.EventArgs.Value, nodeScriptViewModel)); else nodePins.Add(nodeVmFactory.OutputPinViewModel(p.EventArgs.Value, nodeScriptViewModel)); }) .DisposeWith(d); Observable.FromEventPattern>(x => Node.PinRemoved += x, x => Node.PinRemoved -= x) .Subscribe(p => nodePins.RemoveMany(nodePins.Items.Where(vm => vm.Pin == p.EventArgs.Value))) .DisposeWith(d); nodePins.Edit(l => { l.Clear(); foreach (IPin nodePin in Node.Pins) { if (nodePin.Direction == PinDirection.Input) l.Add(nodeVmFactory.InputPinViewModel(nodePin, nodeScriptViewModel)); else l.Add(nodeVmFactory.OutputPinViewModel(nodePin, nodeScriptViewModel)); } }); // Subscribe to pin collection changes Observable.FromEventPattern>(x => Node.PinCollectionAdded += x, x => Node.PinCollectionAdded -= x) .Subscribe(p => { if (p.EventArgs.Value.Direction == PinDirection.Input) nodeVmFactory.InputPinCollectionViewModel(p.EventArgs.Value, nodeScriptViewModel); else nodeVmFactory.OutputPinCollectionViewModel(p.EventArgs.Value, nodeScriptViewModel); }) .DisposeWith(d); Observable.FromEventPattern>(x => Node.PinCollectionRemoved += x, x => Node.PinCollectionRemoved -= x) .Subscribe(p => nodePinCollections.RemoveMany(nodePinCollections.Items.Where(vm => vm.PinCollection == p.EventArgs.Value))) .DisposeWith(d); nodePinCollections.Edit(l => { l.Clear(); foreach (IPinCollection nodePinCollection in Node.PinCollections) { if (nodePinCollection.Direction == PinDirection.Input) l.Add(nodeVmFactory.InputPinCollectionViewModel(nodePinCollection, nodeScriptViewModel)); else l.Add(nodeVmFactory.OutputPinCollectionViewModel(nodePinCollection, nodeScriptViewModel)); } }); // Set up the custom node VM if needed if (Node is ICustomViewModelNode customViewModelNode) { CustomNodeViewModel = customViewModelNode.GetCustomViewModel(nodeScriptViewModel.NodeScript); if (customViewModelNode.ViewModelPosition == CustomNodeViewModelPosition.AbovePins) DisplayCustomViewModelAbove = true; else if (customViewModelNode.ViewModelPosition == CustomNodeViewModelPosition.BelowPins) DisplayCustomViewModelBelow = true; else { DisplayCustomViewModelBetween = true; if (customViewModelNode.ViewModelPosition == CustomNodeViewModelPosition.BetweenPinsTop) CustomViewModelVerticalAlignment = VerticalAlignment.Top; else if (customViewModelNode.ViewModelPosition == CustomNodeViewModelPosition.BetweenPinsTop) CustomViewModelVerticalAlignment = VerticalAlignment.Center; else CustomViewModelVerticalAlignment = VerticalAlignment.Bottom; } } }); } public bool IsStaticNode => _isStaticNode?.Value ?? true; public bool HasInputPins => _hasInputPins?.Value ?? false; public bool HasOutputPins => _hasOutputPins?.Value ?? false; public NodeScriptViewModel NodeScriptViewModel { get; } public INode Node { get; } public ReadOnlyObservableCollection InputPinViewModels { get; } public ReadOnlyObservableCollection InputPinCollectionViewModels { get; } public ReadOnlyObservableCollection OutputPinViewModels { get; } public ReadOnlyObservableCollection OutputPinCollectionViewModels { get; } public ReadOnlyObservableCollection PinViewModels { get; } public bool IsSelected { get => _isSelected; set => RaiseAndSetIfChanged(ref _isSelected, value); } public ICustomNodeViewModel? CustomNodeViewModel { get => _customNodeViewModel; set => RaiseAndSetIfChanged(ref _customNodeViewModel, value); } public bool DisplayCustomViewModelAbove { get => _displayCustomViewModelAbove; set => RaiseAndSetIfChanged(ref _displayCustomViewModelAbove, value); } public bool DisplayCustomViewModelBetween { get => _displayCustomViewModelBetween; set => RaiseAndSetIfChanged(ref _displayCustomViewModelBetween, value); } public bool DisplayCustomViewModelBelow { get => _displayCustomViewModelBelow; set => RaiseAndSetIfChanged(ref _displayCustomViewModelBelow, value); } public VerticalAlignment CustomViewModelVerticalAlignment { get => _customViewModelVerticalAlignment; set => RaiseAndSetIfChanged(ref _customViewModelVerticalAlignment, value); } public ReactiveCommand ShowBrokenState { get; } public ReactiveCommand DeleteNode { get; } 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 UpdateDrag(Point mousePosition) { if (!IsSelected) return; Node.X = Math.Round((mousePosition.X + _dragOffsetX) / 10d, 0, MidpointRounding.AwayFromZero) * 10d; 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)); } private void ExecuteShowBrokenState() { if (Node.BrokenState != null && Node.BrokenStateException != null) _windowService.ShowExceptionDialog(Node.BrokenState, Node.BrokenStateException); } }