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,39 @@
<UserControl x:Class="NodeNetwork.Toolkit.Group.AddEndpointDropPanel.AddEndpointDropPanelView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800" x:Name="Self">
<UserControl.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
</UserControl.Resources>
<Grid Width="Auto" Height="40"
Visibility="{Binding IsDropZoneVisible, Converter={StaticResource BooleanToVisibilityConverter}}">
<Grid.Style>
<Style TargetType="Grid">
<Setter Property="Background" Value="Transparent"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetProperty="(Grid.Background).(SolidColorBrush.Color)" To="#44FFFFFF" Duration="0:0:0.1"/>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetProperty="(Grid.Background).(SolidColorBrush.Color)" To="Transparent" Duration="0:0:0.1"/>
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
</Style.Triggers>
</Style>
</Grid.Style>
<Rectangle Stroke="#fff" StrokeThickness="2" StrokeDashArray="4 4" SnapsToDevicePixels="True"/>
<TextBlock Text="{Binding DropHintText, ElementName=Self}" VerticalAlignment="Center" TextAlignment="Center" FontSize="14" Margin="15,5,15,5"/>
</Grid>
</UserControl>

View File

@@ -0,0 +1,54 @@
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Windows;
using System.Windows.Controls;
using ReactiveUI;
namespace NodeNetwork.Toolkit.Group.AddEndpointDropPanel
{
public partial class AddEndpointDropPanelView : IViewFor<AddEndpointDropPanelViewModel>
{
#region ViewModel
public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(nameof(ViewModel),
typeof(AddEndpointDropPanelViewModel), typeof(AddEndpointDropPanelView), new PropertyMetadata(null));
public AddEndpointDropPanelViewModel ViewModel
{
get => (AddEndpointDropPanelViewModel)GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, value);
}
object IViewFor.ViewModel
{
get => ViewModel;
set => ViewModel = (AddEndpointDropPanelViewModel)value;
}
#endregion
#region DropHintText
public static readonly DependencyProperty DropHintTextProperty = DependencyProperty.Register(nameof(DropHintText),
typeof(string), typeof(AddEndpointDropPanelView), new PropertyMetadata(null));
public string DropHintText
{
get => (string)GetValue(DropHintTextProperty);
set => SetValue(DropHintTextProperty, value);
}
#endregion
public AddEndpointDropPanelView()
{
InitializeComponent();
DropHintText = "Drop here to create new entry";
this.WhenActivated(d =>
{
this.Events().MouseLeftButtonUp
.Select(_ => Unit.Default)
.InvokeCommand(this, v => v.ViewModel.AddEndpointFromPendingConnection)
.DisposeWith(d);
});
}
}
}

View File

@@ -0,0 +1,120 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reactive;
using System.Reactive.Linq;
using System.Text;
using DynamicData;
using NodeNetwork.ViewModels;
using ReactiveUI;
namespace NodeNetwork.Toolkit.Group.AddEndpointDropPanel
{
public class AddEndpointDropPanelViewModel : ReactiveObject
{
static AddEndpointDropPanelViewModel()
{
NNViewRegistrar.AddRegistration(() => new AddEndpointDropPanelView(), typeof(IViewFor<AddEndpointDropPanelViewModel>));
}
/// <summary>
/// Take the pending connection from the super- or subnetwork, whichever is non-null,
/// and add endpoints to NodeGroupIOBinding that match this connection.
/// </summary>
public ReactiveCommand<Unit, Unit> AddEndpointFromPendingConnection { get; }
#region IsDropZoneVisible
public bool IsDropZoneVisible => _isDropZoneVisible.Value;
private readonly ObservableAsPropertyHelper<bool> _isDropZoneVisible;
#endregion
#region NodeGroupIOBinding
public NodeGroupIOBinding NodeGroupIOBinding
{
get => _nodeGroupIoBinding;
set => this.RaiseAndSetIfChanged(ref _nodeGroupIoBinding, value);
}
private NodeGroupIOBinding _nodeGroupIoBinding;
#endregion
private readonly bool isOnSubnetEntrance;
private readonly bool isOnSubnetExit;
public AddEndpointDropPanelViewModel(bool isOnSubnetEntrance = false, bool isOnSubnetExit = false)
{
this.isOnSubnetEntrance = isOnSubnetEntrance;
this.isOnSubnetExit = isOnSubnetExit;
bool isOnSubnet = isOnSubnetEntrance || isOnSubnetExit;
AddEndpointFromPendingConnection = ReactiveCommand.Create(() =>
{
var network = isOnSubnet ? NodeGroupIOBinding.SubNetwork : NodeGroupIOBinding.SuperNetwork;
var pendingConn = network.PendingConnection;
NodeInputViewModel input = null;
NodeOutputViewModel output = null;
if (!CanCreateEndpointFromPendingConnection(pendingConn))
{
return;
}
if (pendingConn.Input != null)
{
input = pendingConn.Input;
if (isOnSubnet)
{
output = NodeGroupIOBinding.AddNewSubnetInlet(pendingConn.Input);
}
else
{
output = NodeGroupIOBinding.AddNewGroupNodeOutput(pendingConn.Input);
}
}
else if (pendingConn.Output != null)
{
if (isOnSubnet)
{
input = NodeGroupIOBinding.AddNewSubnetOutlet(pendingConn.Output);
}
else
{
input = NodeGroupIOBinding.AddNewGroupNodeInput(pendingConn.Output);
}
output = pendingConn.Output;
}
network.Connections.Add(network.ConnectionFactory(input, output));
});
if (isOnSubnet)
{
this.WhenAnyValue(vm => vm.NodeGroupIOBinding.SubNetwork.PendingConnection)
.Select(CanCreateEndpointFromPendingConnection)
.ToProperty(this, vm => vm.IsDropZoneVisible, out _isDropZoneVisible);
}
else
{
this.WhenAnyValue(vm => vm.NodeGroupIOBinding.SuperNetwork.PendingConnection)
.Select(CanCreateEndpointFromPendingConnection)
.ToProperty(this, vm => vm.IsDropZoneVisible, out _isDropZoneVisible);
}
}
private bool CanCreateEndpointFromPendingConnection(PendingConnectionViewModel conn)
{
if (conn == null)
{
return false;
}
var sourceNode = conn.Input != null ? conn.Input.Parent : conn.Output.Parent;
return sourceNode != NodeGroupIOBinding.GroupNode
&& !(isOnSubnetEntrance && sourceNode == NodeGroupIOBinding.EntranceNode)
&& !(isOnSubnetExit && sourceNode == NodeGroupIOBinding.ExitNode);
}
}
}

View File

@@ -0,0 +1,91 @@
using NodeNetwork.ViewModels;
namespace NodeNetwork.Toolkit.Group
{
/// <summary>
/// Facilitates connections between nodes outside and inside a group.
/// This is performed by having inputs on the group node (in the supernet) that map to outputs on (mostly) the EntranceNode in the subnet.
/// Likewise, outputs of the group node map to inputs on (mostly) the ExitNode in the subnet.
/// </summary>
public abstract class NodeGroupIOBinding
{
/// <summary>
/// Node in the parent network that represents the group.
/// </summary>
public NodeViewModel GroupNode { get; }
/// <summary>
/// Inlet node in the subnet.
/// Although this generally contains only outputs, this may contain inputs if their orientation is flipped.
/// </summary>
public NodeViewModel EntranceNode { get; }
/// <summary>
/// Outlet node in the subnet.
/// Although this generally contains only outputs, this may contain inputs if their orientation is flipped.
/// </summary>
public NodeViewModel ExitNode { get; }
/// <summary>
/// Parent network that contains the GroupNode.
/// </summary>
public NetworkViewModel SuperNetwork => GroupNode.Parent;
/// <summary>
/// Child network, contained in SuperNetwork, that contains the group member nodes (like the EntranceNode and ExitNode).
/// </summary>
public NetworkViewModel SubNetwork => ExitNode.Parent;
public NodeGroupIOBinding(NodeViewModel groupNode, NodeViewModel entranceNode, NodeViewModel exitNode)
{
GroupNode = groupNode;
EntranceNode = entranceNode;
ExitNode = exitNode;
}
/// <summary>
/// Given the output in the subnet, return the corresponding input on the groupnode in the supernet.
/// </summary>
public abstract NodeInputViewModel GetGroupNodeInput(NodeOutputViewModel subnetInlet);
/// <summary>
/// Given the input on the group node in the supernet, return the corresponding output in the subnet.
/// </summary>
public abstract NodeOutputViewModel GetSubnetInlet(NodeInputViewModel entranceInput);
/// <summary>
/// Given the output on the group node in the supernet, return the corresponding input in the subnet.
/// </summary>
public abstract NodeInputViewModel GetSubnetOutlet(NodeOutputViewModel groupNodeOutput);
/// <summary>
/// Given the input in the subnet, return the corresponding output on the groupnode in the supernet.
/// </summary>
public abstract NodeOutputViewModel GetGroupNodeOutput(NodeInputViewModel subnetOutlet);
/// <summary>
/// Create and add a new input to the group node, along with a corresponding output in the subnet (e.g. on the entrance node).
/// </summary>
/// <param name="candidateOutput">Output viewmodel that should match the new input on the group node.</param>
/// <returns></returns>
public abstract NodeInputViewModel AddNewGroupNodeInput(NodeOutputViewModel candidateOutput);
/// <summary>
/// Create and add a new input to the group node, along with a corresponding output in the subnet (e.g. on the entrance node).
/// </summary>
/// <param name="candidateInput">Input viewmodel that should match the new output that is added to the subnet.</param>
public abstract NodeOutputViewModel AddNewSubnetInlet(NodeInputViewModel candidateInput);
/// <summary>
/// Create and add a new output to the group node, along with a corresponding input in the subnet (e.g. on the exit node).
/// </summary>
/// <param name="candidateInput">Input viewmodel that should match the new output on the group node.</param>
public abstract NodeOutputViewModel AddNewGroupNodeOutput(NodeInputViewModel candidateInput);
/// <summary>
/// Create and add a new output to the group node, along with a corresponding input in the subnet (e.g. on the exit node).
/// </summary>
/// <param name="candidateOutput">Output viewmodel that should match the new input that is added to the subnet.</param>
public abstract NodeInputViewModel AddNewSubnetOutlet(NodeOutputViewModel candidateOutput);
}
}

View File

@@ -0,0 +1,269 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using DynamicData;
using DynamicData.Alias;
using NodeNetwork.Utilities;
using NodeNetwork.ViewModels;
namespace NodeNetwork.Toolkit.Group
{
/// <summary>
/// Used to provide nesting of networks by grouping nodes.
/// </summary>
public class NodeGrouper
{
/// <summary>
/// Constructs a new node that represents a group of nodes.
/// The parameter is the subnetwork (constructed with SubNetworkFactory) that contains the group member nodes.
/// </summary>
public Func<NetworkViewModel, NodeViewModel> GroupNodeFactory { get; set; } = subnet => new NodeViewModel();
/// <summary>
/// Constructs a viewmodel for the subnetwork that will contain the group member nodes.
/// </summary>
public Func<NetworkViewModel, NetworkViewModel> SubNetworkFactory { get; set; } = parentNet => new NetworkViewModel();
/// <summary>
/// Constructs the node in the subnet that provides access to (mostly) inputs to the group
/// </summary>
public Func<NodeViewModel> EntranceNodeFactory { get; set; } = () => new NodeViewModel();
/// <summary>
/// Constructs the node in the subnet that provides access to (mostly) outputs of the group
/// </summary>
public Func<NodeViewModel> ExitNodeFactory { get; set; } = () => new NodeViewModel();
/// <summary>
/// Constructs a NodeGroupIOBinding from a group, entrance and exit node.
/// </summary>
public Func<NodeViewModel, NodeViewModel, NodeViewModel, NodeGroupIOBinding> IOBindingFactory { get; set; }
private bool CheckPropertiesValid() =>
GroupNodeFactory != null && SubNetworkFactory != null && EntranceNodeFactory != null && ExitNodeFactory != null && IOBindingFactory != null;
/// <summary>
/// Move the specified set of nodes to a new subnetwork, create a new group node that contains this subnet,
/// restore inter- and intra-network connections.
/// </summary>
/// <param name="network">The parent network</param>
/// <param name="nodesToGroup">The nodes to group</param>
/// <returns>Returns the NodeGroupIOBinding that was constructed for this group using the IOBindingFactory.</returns>
public NodeGroupIOBinding MergeIntoGroup(NetworkViewModel network, IEnumerable<NodeViewModel> nodesToGroup)
{
if (!CheckPropertiesValid())
{
throw new InvalidOperationException("All properties must be set before usage");
}
else if (network == null)
{
throw new ArgumentNullException(nameof(network));
}
else if (nodesToGroup == null)
{
throw new ArgumentNullException(nameof(nodesToGroup));
}
var groupNodesSet = nodesToGroup is HashSet<NodeViewModel> set
? set
: new HashSet<NodeViewModel>(nodesToGroup);
// Check if nodesToGroup can be combined into a single group
if (groupNodesSet.Count == 0)//[FT] allowed for now || !GraphAlgorithms.IsContinuousSubGraphSet(groupNodesSet))
{
return null;
}
// Create new empty group
var subnet = SubNetworkFactory(network);
var groupNode = GroupNodeFactory(subnet);
network.Nodes.Add(groupNode);
var groupEntranceNode = EntranceNodeFactory();
var groupExitNode = ExitNodeFactory();
subnet.Nodes.AddRange(new []{groupEntranceNode, groupExitNode});
// Map from input on a group member node to group node input
var groupNodeInputs = new Dictionary<NodeInputViewModel, NodeInputViewModel>();
// Map from output on a group member node to group node output
var groupNodeOutputs = new Dictionary<NodeOutputViewModel, NodeOutputViewModel>();
// Move the new nodes to appropriate positions
groupNode.Position = new Point(
groupNodesSet.Average(n => n.Position.X),
groupNodesSet.Average(n => n.Position.Y)
);
double yCoord = groupNodesSet.Average(n => n.Position.Y);
groupEntranceNode.Position = new Point(
groupNodesSet.Min(n => n.Position.X) - 100,
yCoord
);
groupExitNode.Position = new Point(
groupNodesSet.Max(n => n.Position.X) + 100,
yCoord
);
// Setup binding between entrance/exit inputs and outputs
var ioBinding = IOBindingFactory(groupNode, groupEntranceNode, groupExitNode);
// Calculate set of connections to replace
var subnetConnections = new List<ConnectionViewModel>();
var borderInputConnections = new List<ConnectionViewModel>();
var borderOutputConnections = new List<ConnectionViewModel>();
foreach (var con in network.Connections.Items)
{
bool inputIsInSubnet = groupNodesSet.Contains(con.Input.Parent);
bool outputIsInSubnet = groupNodesSet.Contains(con.Output.Parent);
if (inputIsInSubnet && outputIsInSubnet)
{
subnetConnections.Add(con);
}
else if (inputIsInSubnet)
{
borderInputConnections.Add(con);
}
else if (outputIsInSubnet)
{
borderOutputConnections.Add(con);
}
}
// Construct inputs/outputs into/out of the group
foreach (var borderInCon in borderInputConnections)
{
if (!groupNodeInputs.ContainsKey(borderInCon.Input))
{
groupNodeInputs[borderInCon.Input] = ioBinding.AddNewGroupNodeInput(borderInCon.Output);
}
}
foreach (var borderOutCon in borderOutputConnections)
{
if (!groupNodeOutputs.ContainsKey(borderOutCon.Output))
{
groupNodeOutputs[borderOutCon.Output] = ioBinding.AddNewGroupNodeOutput(borderOutCon.Input);
}
}
// Transfer nodes and inner connections to subnet
network.Connections.Edit(l =>
{
l.RemoveMany(subnetConnections);
l.RemoveMany(borderInputConnections);
l.RemoveMany(borderOutputConnections);
});
network.Nodes.RemoveMany(groupNodesSet);
subnet.Nodes.AddRange(groupNodesSet);
subnet.Connections.AddRange(subnetConnections.Select(con => subnet.ConnectionFactory(con.Input, con.Output)));
// Restore connections in/out of group
network.Connections.AddRange(Enumerable.Concat(
borderInputConnections.Select(con => network.ConnectionFactory(groupNodeInputs[con.Input], con.Output)),
borderOutputConnections.Select(con => network.ConnectionFactory(con.Input, groupNodeOutputs[con.Output]))
));
subnet.Connections.AddRange(Enumerable.Concat(
borderInputConnections.Select(con => subnet.ConnectionFactory(con.Input, ioBinding.GetSubnetInlet(groupNodeInputs[con.Input]))),
borderOutputConnections.Select(con => subnet.ConnectionFactory(ioBinding.GetSubnetOutlet(groupNodeOutputs[con.Output]), con.Output))
));
return ioBinding;
}
/// <summary>
/// Reverses the grouping performed by MergeIntoGroup.
/// Group members get moved back into the parent network and the group node is removed.
/// </summary>
/// <param name="nodeGroupInfo">The NodeGroupIOBinding of the group to dissolve.</param>
public void Ungroup(NodeGroupIOBinding nodeGroupInfo)
{
if (!CheckPropertiesValid())
{
throw new InvalidOperationException("All properties must be set before usage");
}
else if (nodeGroupInfo == null)
{
throw new ArgumentNullException(nameof(nodeGroupInfo));
}
var supernet = nodeGroupInfo.SuperNetwork;
var subnet = nodeGroupInfo.SubNetwork;
// Calculate set of subnet connections to replace
var borderInputConnections = new List<Tuple<NodeOutputViewModel, NodeInputViewModel[]>>();
var borderOutputConnections = new List<Tuple<NodeInputViewModel, NodeOutputViewModel[]>>();
var subnetConnections = new List<ConnectionViewModel>();
foreach (var conn in subnet.Connections.Items)
{
if (conn.Input.Parent == nodeGroupInfo.EntranceNode || conn.Input.Parent == nodeGroupInfo.ExitNode)
{
var inputs = nodeGroupInfo.GetGroupNodeOutput(conn.Input).Connections.Items.Select(c => c.Input).ToArray();
if (inputs.Length > 0)
{
borderInputConnections.Add(Tuple.Create(conn.Output, inputs));
}
}
else if (conn.Output.Parent == nodeGroupInfo.EntranceNode || conn.Output.Parent == nodeGroupInfo.ExitNode)
{
var outputs = nodeGroupInfo.GetGroupNodeInput(conn.Output).Connections.Items.Select(c => c.Output).ToArray();
if (outputs.Length > 0)
{
borderOutputConnections.Add(Tuple.Create(conn.Input, outputs));
}
}
else
{
subnetConnections.Add(conn);
}
}
// Calculate set of nodes to move
var groupMemberNodes = subnet.Nodes.Items.Where(node => node != nodeGroupInfo.EntranceNode && node != nodeGroupInfo.ExitNode).ToArray();
// Calculate center of nodes
var minX = groupMemberNodes.Min(n => n.Position.X);
var minY = groupMemberNodes.Min(n => n.Position.Y);
var maxX = groupMemberNodes.Max(n => n.Position.X);
var maxY = groupMemberNodes.Max(n => n.Position.Y);
var center = new Vector(minX + (maxX - minX)/2, minY + (maxY - minY)/2);
// Remove connections and nodes from subnet
subnet.Connections.Clear();
subnet.Nodes.Clear();
// Remove groupnode and connections from supernet
var groupNodePos = new Vector(nodeGroupInfo.GroupNode.Position.X, nodeGroupInfo.GroupNode.Position.Y);
supernet.Nodes.Remove(nodeGroupInfo.GroupNode);
// Add nodes to supernet and move them to correct position
supernet.Nodes.AddRange(groupMemberNodes);
foreach (var node in groupMemberNodes)
{
node.Position = node.Position - center + groupNodePos;
}
// Add connections to supernet
supernet.Connections.AddRange(subnetConnections);
foreach (var connTuple in borderInputConnections)
{
var output = connTuple.Item1;
var inputs = connTuple.Item2;
var connections = inputs.Select(input => supernet.ConnectionFactory(input, output));
supernet.Connections.AddRange(connections);
}
foreach (var connTuple in borderOutputConnections)
{
var outputs = connTuple.Item2;
var input = connTuple.Item1;
var connections = outputs.Select(output => supernet.ConnectionFactory(input, output));
supernet.Connections.AddRange(connections);
}
}
}
}

View File

@@ -0,0 +1,219 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using DynamicData;
using NodeNetwork.Toolkit.ValueNode;
using NodeNetwork.ViewModels;
using ReactiveUI;
namespace NodeNetwork.Toolkit.Group
{
/// <summary>
/// Basic reference implementation of NodeGroupIOBinding for ValueInputViewModels and ValueOutputViewModels.
/// </summary>
public class ValueNodeGroupIOBinding : NodeGroupIOBinding
{
private readonly IDictionary<NodeOutputViewModel, NodeInputViewModel> _outputInputMapping = new Dictionary<NodeOutputViewModel, NodeInputViewModel>();
public ValueNodeGroupIOBinding(NodeViewModel groupNode, NodeViewModel entranceNode, NodeViewModel exitNode)
: base(groupNode, entranceNode, exitNode)
{
// For each input on the group node, create an output in the subnet
groupNode.Inputs.Connect()
.Filter(input => input.PortPosition == PortPosition.Left)
.Transform(i =>
{
// Dynamic is applied here so that late binding is used to find the most specific
// CreateCompatibleOutput variant for this specific input.
NodeOutputViewModel result = CreateCompatibleOutput((dynamic)i);
BindOutputToInput((dynamic)result, (dynamic)i);
return result;
}).PopulateInto(entranceNode.Outputs);
groupNode.Inputs.Connect()
.Filter(input => input.PortPosition == PortPosition.Right)
.Transform(i =>
{
NodeOutputViewModel result = CreateCompatibleOutput((dynamic) i);
BindOutputToInput((dynamic) result, (dynamic) i);
return result;
}).PopulateInto(exitNode.Outputs);
groupNode.Inputs.Connect().OnItemRemoved(input =>
_outputInputMapping.Remove(
_outputInputMapping.First(kvp => kvp.Value == input)
)
);
// For each output on the group node, create an input in the subnet
groupNode.Outputs.Connect()
.Filter(input => input.PortPosition == PortPosition.Right)
.Transform(o =>
{
NodeInputViewModel result = CreateCompatibleInput((dynamic)o);
BindOutputToInput((dynamic)o, (dynamic)result);
return result;
}).PopulateInto(exitNode.Inputs);
groupNode.Outputs.Connect()
.Filter(input => input.PortPosition == PortPosition.Left)
.Transform(o =>
{
NodeInputViewModel result = CreateCompatibleInput((dynamic)o);
BindOutputToInput((dynamic)o, (dynamic)result);
return result;
}).PopulateInto(entranceNode.Inputs);
groupNode.Outputs.Connect().OnItemRemoved(output => _outputInputMapping.Remove(output));
}
protected virtual void BindEndpointProperties(NodeOutputViewModel output, NodeInputViewModel input)
{
input.WhenAnyValue(vm => vm.Name).BindTo(output, vm => vm.Name);
output.WhenAnyValue(vm => vm.Name).BindTo(input, vm => vm.Name);
input.WhenAnyValue(vm => vm.SortIndex).BindTo(output, vm => vm.SortIndex);
output.WhenAnyValue(vm => vm.SortIndex).BindTo(input, vm => vm.SortIndex);
input.WhenAnyValue(vm => vm.Icon).BindTo(output, vm => vm.Icon);
output.WhenAnyValue(vm => vm.Icon).BindTo(input, vm => vm.Icon);
}
protected virtual void BindOutputToInput<T>(ValueNodeOutputViewModel<T> output, ValueNodeInputViewModel<T> input)
{
BindEndpointProperties(output, input);
output.Value = input.ValueChanged;
_outputInputMapping.Add(output, input);
}
protected virtual void BindOutputToInput<T>(ValueNodeOutputViewModel<IObservableList<T>> output, ValueListNodeInputViewModel<T> input)
{
BindEndpointProperties(output, input);
output.Value = Observable.Return(input.Values);
_outputInputMapping.Add(output, input);
}
#region Endpoint Create
public virtual ValueNodeOutputViewModel<T> CreateCompatibleOutput<T>(ValueNodeInputViewModel<T> input)
{
return new ValueNodeOutputViewModel<T>()
{
Name = input.Name,
Icon = input.Icon
};
}
public virtual ValueNodeOutputViewModel<IObservableList<T>> CreateCompatibleOutput<T>(ValueListNodeInputViewModel<T> input)
{
return new ValueNodeOutputViewModel<IObservableList<T>>();
}
public virtual ValueNodeInputViewModel<T> CreateCompatibleInput<T>(ValueNodeOutputViewModel<T> output)
{
return new ValueNodeInputViewModel<T>()
{
Name = output.Name,
Icon = output.Icon
};
}
public virtual ValueListNodeInputViewModel<T> CreateCompatibleInput<T>(ValueNodeOutputViewModel<IObservableList<T>> output)
{
return new ValueListNodeInputViewModel<T>()
{
Name = output.Name,
Icon = output.Icon
};
}
#endregion
#region Endpoint Add
public override NodeInputViewModel AddNewGroupNodeInput(NodeOutputViewModel candidateOutput)
{
NodeInputViewModel input = CreateCompatibleInput((dynamic)candidateOutput);
input.Name = candidateOutput.Name;
GroupNode.Inputs.Add(input);
// Append to bottom of list
input.SortIndex = GroupNode.Inputs.Items.Select(i => i.SortIndex).DefaultIfEmpty(-1).Max() + 1;
return input;
}
public override NodeOutputViewModel AddNewSubnetInlet(NodeInputViewModel candidateInput)
{
NodeInputViewModel input = AddNewGroupNodeInput(CreateCompatibleOutput((dynamic)candidateInput));
return GetSubnetInlet(input);
}
public override NodeInputViewModel AddNewSubnetOutlet(NodeOutputViewModel candidateOutput)
{
NodeOutputViewModel output = AddNewGroupNodeOutput(CreateCompatibleInput((dynamic)candidateOutput));
return GetSubnetOutlet(output);
}
public override NodeOutputViewModel AddNewGroupNodeOutput(NodeInputViewModel candidateInput)
{
NodeOutputViewModel output = CreateCompatibleOutput((dynamic)candidateInput);
output.Name = candidateInput.Name;
GroupNode.Outputs.Add(output);
// Append to bottom of list
output.SortIndex = GroupNode.Outputs.Items.Select(o => o.SortIndex).DefaultIfEmpty(-1).Max() + 1;
return output;
}
#endregion
#region Endpoint Getters
public override NodeInputViewModel GetGroupNodeInput(NodeOutputViewModel entranceOutput)
{
return _outputInputMapping[entranceOutput];
}
public override NodeOutputViewModel GetSubnetInlet(NodeInputViewModel entranceInput)
{
return _outputInputMapping.Single(p => p.Value == entranceInput).Key;
}
public override NodeInputViewModel GetSubnetOutlet(NodeOutputViewModel exitOutput)
{
return _outputInputMapping[exitOutput];
}
public override NodeOutputViewModel GetGroupNodeOutput(NodeInputViewModel exitInput)
{
return _outputInputMapping.Single(p => p.Value == exitInput).Key;
}
#endregion
/// <summary>
/// Remove an endpoint, which can be from group node, entrance node or exit node.
/// Also removes the corresponding endpoint in the other network.
/// </summary>
/// <param name="endpoint">Input or output to be removed.</param>
public virtual void DeleteEndpoint(Endpoint endpoint)
{
// Because the subnet entrance and exit are derived from the groupnode,
// endpoints should be deleted from the groupnode only.
bool isOnGroupNode = GroupNode == endpoint.Parent;
if (endpoint is NodeInputViewModel input)
{
if (isOnGroupNode)
{
GroupNode.Inputs.Remove(input);
}
else
{
var groupOutput = GetGroupNodeOutput(input);
GroupNode.Outputs.Remove(groupOutput);
}
}
else if(endpoint is NodeOutputViewModel output)
{
if (isOnGroupNode)
{
GroupNode.Outputs.Remove(output);
}
else
{
var groupInput = GetGroupNodeInput(output);
GroupNode.Inputs.Remove(groupInput);
}
}
}
}
}