port from perforce

This commit is contained in:
2026-04-18 22:31:51 +02:00
commit 8d0ab5b7cc
8409 changed files with 3972376 additions and 0 deletions

View File

@@ -0,0 +1,430 @@
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;
}
}
}