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,74 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NodeNetwork.ViewModels;
namespace NodeNetwork.Toolkit.Layout.ForceDirected
{
public class Configuration
{
/// <summary>
/// The network whose nodes are to be repositioned.
/// </summary>
public NetworkViewModel Network { get; set; }
/// <summary>
/// A time modifier that is used to speed up, or slow down, time during the simulation.
/// A greater time modifier speeds up the physics simulation, at the cost of accuracy and stability.
/// </summary>
public float TimeModifier { get; set; } = 3.5f;
/// <summary>
/// Number of updates per iteration.
/// Increasing this number increases the accuracy of the physics simulation at the cost of performance.
/// </summary>
public int UpdatesPerIteration { get; set; } = 1;
/// <summary>
/// How strongly should nodes push eachother away?
/// A greater NodeRepulsionForce increases the distance between nodes.
/// </summary>
public float NodeRepulsionForce { get; set; } = 100;
/// <summary>
/// A function that maps each connection onto the equilibrium distance of its corresponding spring.
/// A greater equilibrium distance increases the distance between the two connected endpoints.
/// </summary>
public Func<ConnectionViewModel, double> EquilibriumDistance { get; set; } = conn => 100;
/// <summary>
/// A function that maps each connection onto the springiness/stiffness constant of its corresponding spring.
/// (c.f. Hooke's law)
/// </summary>
public Func<ConnectionViewModel, double> SpringConstant { get; set; } = conn => 1;
/// <summary>
/// A function that maps each connection onto the strength of its row force.
/// Since inputs/outputs are on the left/right of a node, networks tend to be layed out horizontally.
/// The row force is added onto the endpoints of the connection to make the nodes end up in a more horizontal layout.
/// </summary>
public Func<ConnectionViewModel, double> RowForce { get; set; } = conn => 100;
/// <summary>
/// A function that maps each node onto its mass in the physics simulation.
/// Greater mass makes the node harder to move.
/// </summary>
public Func<NodeViewModel, float> NodeMass { get; set; } = node => 10;
/// <summary>
/// The friction coefficient is used to control friction forces in the simulation.
/// Greater friction makes the simulation converge faster, as it slows nodes down when
/// they are swinging around. If the friction is too high, the nodes will stop moving before
/// they reach their optimal position or might not even move at all.
/// </summary>
public Func<NodeViewModel, float> FrictionCoefficient { get; set; } = node => 2.5f;
/// <summary>
/// A predicate function that specifies whether or not a node is fixed.
/// Fixed nodes do not get moved in the simulation.
/// </summary>
public Func<NodeViewModel, bool> IsFixedNode { get; set; } = node => false;
}
}

View File

@@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using NodeNetwork.ViewModels;
namespace NodeNetwork.Toolkit.Layout.ForceDirected
{
internal class Engine
{
internal void ApplyRandomShift(NetworkViewModel network)
{
Random random = new Random();
foreach (var node in network.Nodes.Items)
{
node.Position = node.Position + new Vector(random.NextDouble(), random.NextDouble());
}
}
internal void Update(int deltaTMillis, IState state, Configuration config)
{
// Calculate forces
int nodeCount = config.Network.Nodes.Count;
IList<(NodeViewModel, Vector)> nodeForces = new List<(NodeViewModel, Vector)>(nodeCount);
foreach (var node in config.Network.Nodes.Items)
{
if (!config.IsFixedNode(node))
{
nodeForces.Add((node, CalculateNodeForce(node, state, config)));
}
}
// Apply forces
foreach (var (node, force) in nodeForces)
{
Vector speed = state.GetNodeSpeed(node);
Vector pos = state.GetNodePosition(node);
double deltaT = deltaTMillis / 1000.0;
state.SetNodePosition(node, pos + ((speed * deltaT) + (force * deltaT * deltaT / 2)));
state.SetNodeSpeed(node, speed + ((force / config.NodeMass(node)) * deltaT));
}
}
private Vector CalculateNodeForce(NodeViewModel node, IState state, Configuration config)
{
Vector force = new Vector();
// Calculate total force on node from endpoints
if (node.Inputs.Count > 0 || node.Outputs.Count > 0)
{
force += node.Inputs.Items.Cast<Endpoint>().Concat(node.Outputs.Items)
.Select(e => CalculateEndpointForce(e, state, config))
.Aggregate((v1, v2) => v1 + v2);
}
// Apply node repulsion force so nodes don't overlap
var nodeCenter = state.GetNodePosition(node) + (new Vector(node.Size.Width, node.Size.Height) / 2.0);
foreach (var otherNode in config.Network.Nodes.Items)
{
if (node == otherNode)
{
continue;
}
var otherNodeCenter = state.GetNodePosition(otherNode) + (new Vector(otherNode.Size.Width, otherNode.Size.Height) / 2.0);
var thisToOther = otherNodeCenter - nodeCenter;
var dist = thisToOther.Length;
thisToOther.Normalize();
var repulsionX = thisToOther.X * (-1 * ((node.Size.Width + otherNode.Size.Width) / 2) / dist);
var repulsionY = thisToOther.Y * (-1 * ((node.Size.Height + otherNode.Size.Height) / 2) / dist);
force += new Vector(repulsionX, repulsionY) * config.NodeRepulsionForce;
}
// Apply friction to make the movement converge to a stable state.
float gravity = 9.8f;
float normalForce = gravity * config.NodeMass(node);
float kineticFriction = normalForce * config.FrictionCoefficient(node);
Vector frictionVector = new Vector();
var nodeSpeed = state.GetNodeSpeed(node);
if (nodeSpeed.Length > 0)
{
frictionVector = new Vector(nodeSpeed.X, nodeSpeed.Y);
frictionVector.Normalize();
frictionVector *= -1.0 * kineticFriction;
}
force += frictionVector;
return force;
}
private Vector CalculateEndpointForce(Endpoint endpoint, IState state, Configuration config)
{
var pos = state.GetEndpointPosition(endpoint);
Vector force = new Vector();
foreach (var conn in endpoint.Connections.Items)
{
var otherSide = conn.Input == endpoint ? (Endpoint)conn.Output : conn.Input;
var otherSidePos = state.GetEndpointPosition(otherSide);
var dist = (otherSidePos - pos).Length;
var angle = Math.Acos((otherSidePos.X - pos.X) / dist);
if (otherSidePos.Y < pos.Y)
{
angle *= -1.0;
}
// Put a spring between connected endpoints.
var hookForce = (dist - config.EquilibriumDistance(conn)) * config.SpringConstant(conn);
force += new Vector(Math.Cos(angle), Math.Sin(angle)) * hookForce;
// Try to 'straighten' out the graph horizontally.
var isLeftSide = endpoint.PortPosition == PortPosition.Left;
var rowForce = (isLeftSide ? 1 : -1) * config.RowForce(conn);
force.X += rowForce;
}
return force;
}
}
}

View File

@@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using NodeNetwork.ViewModels;
namespace NodeNetwork.Toolkit.Layout.ForceDirected
{
/// <summary>
/// Reposition the nodes in a network using a physics-based approach.
/// The nodes are interpreted as point masses, and the connections are represented
/// by springs. This system, along with a few additional forces such as friction and a
/// horizontal force, is then simulated to calculate the new position of the nodes.
/// </summary>
public class ForceDirectedLayouter
{
/// <summary>
/// Layout the nodes in the network.
/// </summary>
/// <param name="config">The configuration to use.</param>
/// <param name="maxIterations">The maximum amount of iterations after which the physics simulation ends.</param>
public void Layout(Configuration config, int maxIterations)
{
var engine = new Engine();
var state = new BufferedState();
// Move each node so no two nodes have the exact same position.
engine.ApplyRandomShift(config.Network);
int deltaT = (int)Math.Ceiling(10.0 / (double)config.UpdatesPerIteration);
for (int i = 0; i < maxIterations * config.UpdatesPerIteration; i++)
{
engine.Update(deltaT, state, config);
}
foreach (var newNodePosition in state.NodePositions)
{
newNodePosition.Key.Position = new Point(newNodePosition.Value.X, newNodePosition.Value.Y);
}
}
/// <summary>
/// Layout the nodes in the network, updating the user interface at each iteration.
/// This method, contrary to Layout(), lets users see the simulation as it happens.
/// The cancellation token should be used to end the simulation.
/// </summary>
/// <param name="config">The configuration to use.</param>
/// <param name="token">A cancellation token to end the layout process.</param>
/// <returns>The async task</returns>
public async Task LayoutAsync(Configuration config, CancellationToken token)
{
var engine = new Engine();
var state = new LiveState();
// Move each node so no two nodes have the exact same position.
engine.ApplyRandomShift(config.Network);
DateTime start = DateTime.Now;
TimeSpan t = TimeSpan.Zero;
do
{
// Current real time
var newT = DateTime.Now - start;
var deltaT = newT - t;
// Current modified time
//int virtT = (int)(t.Milliseconds * Settings.TimeModifier);
int virtDeltaT = (int)(deltaT.Milliseconds * config.TimeModifier);
int virtDeltaTPerUpdate = virtDeltaT / config.UpdatesPerIteration;
for (int i = 0; i < config.UpdatesPerIteration; i++)
{
// Modified time in this update step
engine.Update(virtDeltaTPerUpdate, state, config);
}
t = newT;
await Task.Delay(14, token);
} while (!token.IsCancellationRequested);
}
}
}

View File

@@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using NodeNetwork.ViewModels;
namespace NodeNetwork.Toolkit.Layout.ForceDirected
{
internal interface IState
{
Vector GetNodePosition(NodeViewModel node);
void SetNodePosition(NodeViewModel node, Vector pos);
Vector GetEndpointPosition(Endpoint endpoint);
Vector GetNodeSpeed(NodeViewModel node);
void SetNodeSpeed(NodeViewModel node, Vector speed);
}
internal class BufferedState : IState
{
private readonly Dictionary<NodeViewModel, Vector> _nodePositions = new Dictionary<NodeViewModel, Vector>();
private readonly Dictionary<Endpoint, Vector> _endpointRelativePositions = new Dictionary<Endpoint, Vector>();
public IEnumerable<KeyValuePair<NodeViewModel, Vector>> NodePositions => _nodePositions;
private readonly Dictionary<NodeViewModel, Vector> _nodeSpeeds = new Dictionary<NodeViewModel, Vector>();
public Vector GetNodePosition(NodeViewModel node)
{
if (!_nodePositions.TryGetValue(node, out Vector result))
{
result = new Vector(node.Position.X, node.Position.Y);
}
return result;
}
public void SetNodePosition(NodeViewModel node, Vector pos)
{
_nodePositions[node] = pos;
}
public Vector GetEndpointPosition(Endpoint endpoint)
{
if (!_endpointRelativePositions.TryGetValue(endpoint, out Vector result))
{
result = new Vector(endpoint.Port.CenterPoint.X, endpoint.Port.CenterPoint.Y) - GetNodePosition(endpoint.Parent);
_endpointRelativePositions[endpoint] = result;
}
return result + GetNodePosition(endpoint.Parent);
}
public Vector GetNodeSpeed(NodeViewModel node)
{
if (!_nodeSpeeds.TryGetValue(node, out Vector result))
{
result = new Vector(0, 0);
}
return result;
}
public void SetNodeSpeed(NodeViewModel node, Vector speed)
{
_nodeSpeeds[node] = speed;
}
}
internal class LiveState : IState
{
private readonly Dictionary<NodeViewModel, Vector> _nodeSpeeds = new Dictionary<NodeViewModel, Vector>();
public Vector GetNodePosition(NodeViewModel node)
{
return new Vector(node.Position.X, node.Position.Y);
}
public void SetNodePosition(NodeViewModel node, Vector pos)
{
node.Position = new Point(pos.X, pos.Y);
}
public Vector GetEndpointPosition(Endpoint endpoint)
{
return new Vector(endpoint.Port.CenterPoint.X, endpoint.Port.CenterPoint.Y);
}
public Vector GetNodeSpeed(NodeViewModel node)
{
if (!_nodeSpeeds.TryGetValue(node, out Vector result))
{
result = new Vector(0, 0);
}
return result;
}
public void SetNodeSpeed(NodeViewModel node, Vector speed)
{
_nodeSpeeds[node] = speed;
}
}
}