431 lines
17 KiB
C#
431 lines
17 KiB
C#
using NodeNetwork.Views;
|
|
using NodeNetwork.Utilities;
|
|
using ReactiveUI;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Reactive;
|
|
using System.Reactive.Concurrency;
|
|
using System.Reactive.Disposables;
|
|
using System.Reactive.Linq;
|
|
using System.Reactive.Subjects;
|
|
using System.Reactive.Threading.Tasks;
|
|
using System.Threading.Tasks;
|
|
using System.Windows;
|
|
using DynamicData;
|
|
using DynamicData.Alias;
|
|
|
|
namespace NodeNetwork.ViewModels
|
|
{
|
|
/// <summary>
|
|
/// The viewmodel for node networks.
|
|
/// </summary>
|
|
public class NetworkViewModel : ReactiveObject
|
|
{
|
|
static NetworkViewModel()
|
|
{
|
|
NNViewRegistrar.AddRegistration(() => new NetworkView(), typeof(IViewFor<NetworkViewModel>));
|
|
}
|
|
|
|
#region Logger
|
|
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
|
|
#endregion
|
|
|
|
#region Nodes
|
|
/// <summary>
|
|
/// The list of nodes in this network.
|
|
/// </summary>
|
|
public ISourceList<NodeViewModel> Nodes { get; } = new SourceList<NodeViewModel>();
|
|
#endregion
|
|
|
|
#region SelectedNodes
|
|
/// <summary>
|
|
/// A list of nodes that are currently selected in the UI.
|
|
/// The contents of this list is equal to the nodes in Nodes where the Selected property is true.
|
|
/// </summary>
|
|
public IObservableList<NodeViewModel> SelectedNodes { get; }
|
|
#endregion
|
|
|
|
#region Connections
|
|
/// <summary>
|
|
/// The list of connections in this network.
|
|
/// </summary>
|
|
public ISourceList<ConnectionViewModel> Connections { get; } = new SourceList<ConnectionViewModel>();
|
|
#endregion
|
|
|
|
#region PendingConnection
|
|
/// <summary>
|
|
/// The connection that is currently being build by the user.
|
|
/// This connection is visually displayed in the UI, but is not an actual functional connection.
|
|
/// This is used when the user drags from an endpoint to create a new connection.
|
|
/// </summary>
|
|
public PendingConnectionViewModel PendingConnection
|
|
{
|
|
get => _pendingConnection;
|
|
set => this.RaiseAndSetIfChanged(ref _pendingConnection, value);
|
|
}
|
|
private PendingConnectionViewModel _pendingConnection;
|
|
|
|
public Action OnPendingConnectionDropped { get; set; }
|
|
#endregion
|
|
|
|
#region PendingNode
|
|
/// <summary>
|
|
/// The viewmodel of the node that is not part of the network, but is displayed as a node that can be added.
|
|
/// This property is used to display a new node when the user drags a node viewmodel over the network view.
|
|
/// </summary>
|
|
public NodeViewModel PendingNode
|
|
{
|
|
get => _pendingNode;
|
|
set => this.RaiseAndSetIfChanged(ref _pendingNode, value);
|
|
}
|
|
private NodeViewModel _pendingNode;
|
|
#endregion
|
|
|
|
#region ConnectionFactory
|
|
/// <summary>
|
|
/// The function that is used to create connection viewmodels when the user creates connections in the network view.
|
|
/// By default, this function creates a ConnectionViewModel.
|
|
/// </summary>
|
|
public Func<NodeInputViewModel, NodeOutputViewModel, ConnectionViewModel> ConnectionFactory
|
|
{
|
|
get => _connectionFactory;
|
|
set => this.RaiseAndSetIfChanged(ref _connectionFactory, value);
|
|
}
|
|
private Func<NodeInputViewModel, NodeOutputViewModel, ConnectionViewModel> _connectionFactory;
|
|
#endregion
|
|
|
|
#region Validator
|
|
/// <summary>
|
|
/// Function that is used to check if the network is valid or not.
|
|
/// To run the validation, use the UpdateValidation command.
|
|
/// </summary>
|
|
public Func<NetworkViewModel, NetworkValidationResult> Validator
|
|
{
|
|
get => _validator;
|
|
set => this.RaiseAndSetIfChanged(ref _validator, value);
|
|
}
|
|
private Func<NetworkViewModel, NetworkValidationResult> _validator;
|
|
#endregion
|
|
|
|
#region LatestValidation
|
|
//Using ObservableAsPropertyHelper would be better, but causes problems with ReactiveCommand where
|
|
//the value of the property is updated only after the subscribers to the command are run.
|
|
|
|
/// <summary>
|
|
/// The validation of the current state of the network.
|
|
/// This property is automatically updated when UpdateValidation runs.
|
|
/// </summary>
|
|
public NetworkValidationResult LatestValidation
|
|
{
|
|
get => _latestValidation;
|
|
private set => this.RaiseAndSetIfChanged(ref _latestValidation, value);
|
|
}
|
|
private NetworkValidationResult _latestValidation;
|
|
#endregion
|
|
|
|
#region Validation
|
|
/// <summary>
|
|
/// Observable that produces the latest NetworkValidationResult every time the network is validated.
|
|
/// </summary>
|
|
public IObservable<NetworkValidationResult> Validation { get; }
|
|
#endregion
|
|
|
|
#region IsReadOnly
|
|
/// <summary>
|
|
/// If true, the network and its contents (nodes, connections, input/output editors, ...) cannot be modified by the user.
|
|
/// </summary>
|
|
public bool IsReadOnly
|
|
{
|
|
get => _isReadOnly;
|
|
set => this.RaiseAndSetIfChanged(ref _isReadOnly, value);
|
|
}
|
|
private bool _isReadOnly;
|
|
#endregion
|
|
|
|
#region CutLine
|
|
/// <summary>
|
|
/// The viewmodel of the cutline used in this network view.
|
|
/// </summary>
|
|
public CutLineViewModel CutLine { get; } = new CutLineViewModel();
|
|
#endregion
|
|
|
|
#region ZoomFactor
|
|
/// <summary>
|
|
/// Scale of the view. Larger means more zoomed in. Default value is 1.
|
|
/// </summary>
|
|
public double ZoomFactor
|
|
{
|
|
get => _zoomFactor;
|
|
set => this.RaiseAndSetIfChanged(ref _zoomFactor, value);
|
|
}
|
|
|
|
private double _zoomFactor = 1;
|
|
|
|
/// <summary>
|
|
/// The maximum zoom level used in this network view. Default value is 2.5.
|
|
/// </summary>
|
|
public double MaxZoomLevel
|
|
{
|
|
get => _maxZoomLevel;
|
|
set => this.RaiseAndSetIfChanged(ref _maxZoomLevel, value);
|
|
}
|
|
|
|
private double _maxZoomLevel = 2.5;
|
|
|
|
/// <summary>
|
|
/// The minimum zoom level used in this network view. Default value is 0.15.
|
|
/// </summary>
|
|
public double MinZoomLevel
|
|
{
|
|
get => _minZoomLevel;
|
|
set => this.RaiseAndSetIfChanged(ref _minZoomLevel, value);
|
|
}
|
|
|
|
private double _minZoomLevel = 0.15;
|
|
|
|
/// <summary>
|
|
/// The drag offset of the initial view position used in this network view. Default value is (0, 0).
|
|
/// </summary>
|
|
public Point DragOffset
|
|
{
|
|
get => _dragOffset;
|
|
set => this.RaiseAndSetIfChanged(ref _dragOffset, value);
|
|
}
|
|
|
|
private Point _dragOffset = new Point(0, 0);
|
|
|
|
#endregion
|
|
|
|
#region SelectionRectangle
|
|
/// <summary>
|
|
/// The viewmodel for the selection rectangle used in this network view.
|
|
/// </summary>
|
|
public SelectionRectangleViewModel SelectionRectangle { get; } = new SelectionRectangleViewModel();
|
|
#endregion
|
|
|
|
#region NetworkChanged
|
|
/// <summary>
|
|
/// This observable pushes a notification when a connection was added to/removed from the network,
|
|
/// and the relevant endpoints have been updated.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Observing the Connections list directly will trigger the same notifications,
|
|
/// but before the endpoints have had a chance to update and so they may be in an invalid state.
|
|
/// </remarks>
|
|
public IObservable<Unit> ConnectionsUpdated { get; }
|
|
|
|
/// <summary>
|
|
/// This observable pushes a notification whenever any functional changes are made to the network.
|
|
/// Purely esthetical changes, such as the collapsing of nodes, do not trigger this observable.
|
|
/// </summary>
|
|
public IObservable<Unit> NetworkChanged { get; }
|
|
#endregion
|
|
|
|
#region Commands
|
|
/// <summary>
|
|
/// Deletes the nodes in SelectedNodes that are user-removable.
|
|
/// </summary>
|
|
public ReactiveCommand<Unit, Unit> DeleteSelectedNodes { get; }
|
|
|
|
/// <summary>
|
|
/// Runs the Validator function and stores the result in LatestValidation.
|
|
/// </summary>
|
|
public ReactiveCommand<Unit, NetworkValidationResult> UpdateValidation { get; }
|
|
#endregion
|
|
|
|
public NetworkViewModel()
|
|
{
|
|
// Setup parent relationship in nodes.
|
|
Nodes.Connect().ActOnEveryObject(
|
|
addedNode => addedNode.Parent = this,
|
|
removedNode => removedNode.Parent = null
|
|
);
|
|
|
|
// SelectedNodes is a derived collection of all nodes with IsSelected = true.
|
|
SelectedNodes = Nodes.Connect()
|
|
.AutoRefresh(node => node.IsSelected)
|
|
.Filter(node => node.IsSelected)
|
|
.AsObservableList();
|
|
|
|
// When DeleteSelectedNodes is invoked, remove all nodes that are user-removable and selected.
|
|
DeleteSelectedNodes = ReactiveCommand.Create(OnDeleteSelectedNodes);
|
|
|
|
// When a node is removed, delete any connections from/to that node.
|
|
Nodes.Preview().OnItemRemoved(removedNode =>
|
|
{
|
|
Connections.RemoveMany(removedNode.Inputs.Items.SelectMany(o => o.Connections.Items));
|
|
Connections.RemoveMany(removedNode.Outputs.Items.SelectMany(o => o.Connections.Items));
|
|
|
|
bool pendingConnectionInvalid = PendingConnection?.Input?.Parent == removedNode ||
|
|
PendingConnection?.Output?.Parent == removedNode;
|
|
if (pendingConnectionInvalid)
|
|
{
|
|
RemovePendingConnection();
|
|
}
|
|
}).Subscribe();
|
|
|
|
// If, while dragging a pending connection, the mouse is released over the canvas, then cancel the connection.
|
|
OnPendingConnectionDropped = RemovePendingConnection;
|
|
|
|
// When the list of nodes is reset, remove any connections whose input/output node was removed.
|
|
/*Nodes.ShouldReset.Subscribe(_ =>
|
|
{
|
|
// Create a hashset with all nodes for O(1) search
|
|
HashSet<NodeViewModel> nodeSet = new HashSet<NodeViewModel>(Nodes);
|
|
|
|
var connections = Connections.Items.ToArray();
|
|
for (var i = connections.Length - 1; i >= 0; i--)
|
|
{
|
|
if (!nodeSet.Contains(connections[i].Input.Parent) || !nodeSet.Contains(connections[i].Output.Parent))
|
|
{
|
|
Connections.RemoveAt(i);
|
|
}
|
|
}
|
|
|
|
var pendingConnInputNode = PendingConnection?.Input?.Parent;
|
|
var pendingConnOutputNode = PendingConnection?.Output?.Parent;
|
|
bool pendingConnectionInvalid = (pendingConnInputNode != null && !nodeSet.Contains(pendingConnInputNode)) ||
|
|
(pendingConnOutputNode != null && !nodeSet.Contains(pendingConnOutputNode));
|
|
if (pendingConnectionInvalid)
|
|
{
|
|
RemovePendingConnection();
|
|
}
|
|
});*/
|
|
|
|
// Setup a default ConnectionFactory that will be used to create connections.
|
|
ConnectionFactory = (input, output) => new ConnectionViewModel(this, input, output);
|
|
|
|
// Setup a default network validator that always returns valid.
|
|
Validator = _ => new NetworkValidationResult(true, true, null);
|
|
|
|
// Setup the validation command.
|
|
UpdateValidation = ReactiveCommand.Create(() => {
|
|
var result = Validator(this);
|
|
LatestValidation = result;
|
|
return result;
|
|
});
|
|
|
|
// Setup Validation observable
|
|
var onValidationPropertyUpdate = this.WhenAnyValue(vm => vm.LatestValidation).Publish().RefCount();
|
|
Validation = Observable.Defer(() => onValidationPropertyUpdate.StartWith(LatestValidation));
|
|
|
|
// When a connection or node changes, validate the network.
|
|
// Zip is used because when a connection is removed, it will trigger a change in both the input and the output and we want to combine these.
|
|
|
|
var a = Nodes.Connect()
|
|
.AutoRefreshOnObservable(node => node.Inputs.Connect())
|
|
.SelectMany(node => node.Inputs.Items)
|
|
.AutoRefreshOnObservable(input => input.Connections.Connect())
|
|
.SelectMany(input => input.Connections.Items);
|
|
|
|
var b = Nodes.Connect()
|
|
.AutoRefreshOnObservable(node => node.Outputs.Connect())
|
|
.SelectMany(node => node.Outputs.Items)
|
|
.AutoRefreshOnObservable(output => output.Connections.Connect())
|
|
.SelectMany(output => output.Connections.Items);
|
|
|
|
ConnectionsUpdated = Observable.Zip(
|
|
a,
|
|
b,
|
|
(x, y) => Unit.Default
|
|
).Publish().RefCount();
|
|
ConnectionsUpdated.InvokeCommand(UpdateValidation);
|
|
Nodes.Connect().Select((IChangeSet<NodeViewModel> n) => Unit.Default).InvokeCommand(UpdateValidation);
|
|
|
|
// Push a network change notification when a functional network change occurs.
|
|
// These include:
|
|
// - Nodes are added/removed
|
|
// - Connections are added/removed
|
|
// - Endpoint editors change
|
|
// - Network validation changes
|
|
NetworkChanged = Observable.Merge(
|
|
Observable.Select(Nodes.Connect(), _ => Unit.Default),
|
|
Observable.Select(Nodes.Connect().MergeMany(node => node.Inputs.Connect()), _ => Unit.Default),
|
|
Observable.Select(Nodes.Connect().MergeMany(node => node.Outputs.Connect()), _ => Unit.Default),
|
|
ConnectionsUpdated,
|
|
OnEditorChanged(),
|
|
Validation.Select(_ => Unit.Default)
|
|
).Publish().RefCount();
|
|
}
|
|
|
|
protected virtual void OnDeleteSelectedNodes()
|
|
{
|
|
Nodes.RemoveMany(SelectedNodes.Items.Where(n => n.CanBeRemovedByUser).ToArray());
|
|
}
|
|
|
|
private IObservable<Unit> OnEditorChanged()
|
|
{
|
|
return Observable.Merge(
|
|
Nodes.Connect().MergeMany(n =>
|
|
n.Inputs.Connect().MergeMany(i =>
|
|
// Use WhenAnyObservable because Editor can change.
|
|
i.WhenAnyObservable(vm => vm.Editor.Changed)
|
|
)
|
|
).Select(_ => Unit.Default),
|
|
Nodes.Connect().MergeMany(n =>
|
|
n.Outputs.Connect().MergeMany(o =>
|
|
o.WhenAnyObservable(vm => vm.Editor.Changed)
|
|
)
|
|
).Select(_ => Unit.Default)
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears SelectedNodes, setting the IsSelected property of all the nodes to false.
|
|
/// </summary>
|
|
public void ClearSelection()
|
|
{
|
|
foreach (NodeViewModel node in SelectedNodes.Items)
|
|
{
|
|
node.IsSelected = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts a cut in the CutLine viewmodel.
|
|
/// </summary>
|
|
public void StartCut()
|
|
{
|
|
CutLine.IsVisible = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stops the current cut in the CutLine viewmodel and applies the changes.
|
|
/// </summary>
|
|
public virtual void FinishCut()
|
|
{
|
|
Connections.RemoveMany(CutLine.IntersectingConnections.Items);
|
|
CutLine.IsVisible = false;
|
|
CutLine.IntersectingConnections.Clear();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets PendingConnection to null.
|
|
/// </summary>
|
|
public void RemovePendingConnection()
|
|
{
|
|
PendingConnection = null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts a selection in RectangleSelection
|
|
/// </summary>
|
|
public void StartRectangleSelection()
|
|
{
|
|
ClearSelection();
|
|
SelectionRectangle.IsVisible = true;
|
|
SelectionRectangle.IntersectingNodes.Clear();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stops the current selection in RectangleSelection and applies the changes.
|
|
/// </summary>
|
|
public void FinishRectangleSelection()
|
|
{
|
|
SelectionRectangle.IsVisible = false;
|
|
}
|
|
}
|
|
}
|