diff --git a/AIStudio.Wpf.DiagramDesigner/Layout/Configuration.cs b/AIStudio.Wpf.DiagramDesigner/Layout/Configuration.cs new file mode 100644 index 0000000..c49336c --- /dev/null +++ b/AIStudio.Wpf.DiagramDesigner/Layout/Configuration.cs @@ -0,0 +1,69 @@ +//using System; + +//namespace AIStudio.Wpf.DiagramDesigner.Layout +//{ +// public class Configuration +// { +// /// +// /// The network whose nodes are to be repositioned. +// /// +// public DiagramViewModel Network { get; set; } + +// /// +// /// 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. +// /// +// public float TimeModifier { get; set; } = 3.5f; + +// /// +// /// Number of updates per iteration. +// /// Increasing this number increases the accuracy of the physics simulation at the cost of performance. +// /// +// public int UpdatesPerIteration { get; set; } = 1; + +// /// +// /// How strongly should nodes push eachother away? +// /// A greater NodeRepulsionForce increases the distance between nodes. +// /// +// public float NodeRepulsionForce { get; set; } = 100; + +// /// +// /// 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. +// /// +// public Func EquilibriumDistance { get; set; } = conn => 100; + +// /// +// /// A function that maps each connection onto the springiness/stiffness constant of its corresponding spring. +// /// (c.f. Hooke's law) +// /// +// public Func SpringConstant { get; set; } = conn => 1; + +// /// +// /// 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. +// /// +// public Func RowForce { get; set; } = conn => 100; + +// /// +// /// A function that maps each node onto its mass in the physics simulation. +// /// Greater mass makes the node harder to move. +// /// +// public Func NodeMass { get; set; } = node => 10; + +// /// +// /// 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. +// /// +// public Func FrictionCoefficient { get; set; } = node => 2.5f; + +// /// +// /// A predicate function that specifies whether or not a node is fixed. +// /// Fixed nodes do not get moved in the simulation. +// /// +// public Func IsFixedNode { get; set; } = node => false; +// } +//} diff --git a/AIStudio.Wpf.DiagramDesigner/Layout/Engine.cs b/AIStudio.Wpf.DiagramDesigner/Layout/Engine.cs new file mode 100644 index 0000000..9c0482a --- /dev/null +++ b/AIStudio.Wpf.DiagramDesigner/Layout/Engine.cs @@ -0,0 +1,123 @@ +//using System; +//using System.Collections.Generic; +//using System.Linq; +//using System.Windows; +//using AIStudio.Wpf.DiagramDesigner.Geometrys; + +//namespace AIStudio.Wpf.DiagramDesigner.Layout +//{ +// internal class Engine +// { +// internal void ApplyRandomShift(DiagramViewModel network) +// { +// Random random = new Random(); +// foreach (var node in network.Items.OfType()) +// { +// node.Position = node.Position + new VectorBase(random.NextDouble(), random.NextDouble()); +// } +// } + +// internal void Update(int deltaTMillis, IState state, Configuration config) +// { +// // Calculate forces +// int nodeCount = config.Network.Items.Count; +// IList<(DesignerItemViewModelBase, VectorBase)> nodeForces = new List<(DesignerItemViewModelBase, VectorBase)>(nodeCount); + +// foreach (var node in config.Network.Items.OfType()) +// { +// if (!config.IsFixedNode(node)) +// { +// nodeForces.Add((node, CalculateNodeForce(node, state, config))); +// } +// } + +// // Apply forces +// foreach (var (node, force) in nodeForces) +// { +// VectorBase speed = state.GetNodeSpeed(node); +// VectorBase 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 VectorBase CalculateNodeForce(DesignerItemViewModelBase node, IState state, Configuration config) +// { +// VectorBase force = new VectorBase(); + +// // Calculate total force on node from endpoints +// if (node.InputConnectors.Count > 0 || node.OutputConnectors.Count > 0) +// { +// force += node.InputConnectors.Concat(node.OutputConnectors) +// .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 VectorBase(node.Size.Width, node.Size.Height) / 2.0); +// foreach (var otherNode in config.Network.Items.OfType()) +// { +// if (node == otherNode) +// { +// continue; +// } + +// var otherNodeCenter = state.GetNodePosition(otherNode) + (new VectorBase(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 VectorBase(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); +// VectorBase frictionVector = new VectorBase(); +// var nodeSpeed = state.GetNodeSpeed(node); +// if (nodeSpeed.Length > 0) +// { +// frictionVector = new VectorBase(nodeSpeed.X, nodeSpeed.Y); +// frictionVector.Normalize(); +// frictionVector *= -1.0 * kineticFriction; +// } +// force += frictionVector; + +// return force; +// } + +// private VectorBase CalculateEndpointForce(ConnectorInfoBase endpoint, IState state, Configuration config) +// { +// var pos = state.GetEndpointPosition(endpoint); + +// VectorBase force = new VectorBase(); + +// foreach (var conn in endpoint.Connections.Items) +// { +// var otherSide = conn.Input == endpoint ? (ConnectorInfoBase)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 VectorBase(Math.Cos(angle), Math.Sin(angle)) * hookForce; + +// // Try to 'straighten' out the graph horizontally. +// var isLeftSide = endpoint.Orientation == ConnectorOrientation.Left; +// var rowForce = (isLeftSide ? 1 : -1) * config.RowForce(conn); +// force.X += rowForce; +// } + +// return force; +// } +// } +//} diff --git a/AIStudio.Wpf.DiagramDesigner/Layout/ForceDirectedLayouter.cs b/AIStudio.Wpf.DiagramDesigner/Layout/ForceDirectedLayouter.cs new file mode 100644 index 0000000..cf23015 --- /dev/null +++ b/AIStudio.Wpf.DiagramDesigner/Layout/ForceDirectedLayouter.cs @@ -0,0 +1,81 @@ +//using System; +//using System.Threading; +//using System.Threading.Tasks; +//using System.Windows; + +//namespace AIStudio.Wpf.DiagramDesigner.Layout +//{ +// /// +// /// 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. +// /// +// public class ForceDirectedLayouter +// { +// /// +// /// Layout the nodes in the network. +// /// +// /// The configuration to use. +// /// The maximum amount of iterations after which the physics simulation ends. +// 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); +// } +// } + +// /// +// /// 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. +// /// +// /// The configuration to use. +// /// A cancellation token to end the layout process. +// /// The async task +// 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); +// } +// } +//} diff --git a/AIStudio.Wpf.DiagramDesigner/Layout/State.cs b/AIStudio.Wpf.DiagramDesigner/Layout/State.cs new file mode 100644 index 0000000..5691571 --- /dev/null +++ b/AIStudio.Wpf.DiagramDesigner/Layout/State.cs @@ -0,0 +1,100 @@ +//using System.Collections.Generic; +//using System.Windows; +//using AIStudio.Wpf.DiagramDesigner.Geometrys; + +//namespace AIStudio.Wpf.DiagramDesigner.Layout +//{ +// internal interface IState +// { +// VectorBase GetNodePosition(DesignerItemViewModelBase node); +// void SetNodePosition(DesignerItemViewModelBase node, VectorBase pos); +// VectorBase GetEndpointPosition(ConnectorInfoBase endpoint); +// VectorBase GetNodeSpeed(DesignerItemViewModelBase node); +// void SetNodeSpeed(DesignerItemViewModelBase node, VectorBase speed); +// } + +// internal class BufferedState : IState +// { +// private readonly Dictionary _nodePositions = new Dictionary(); +// private readonly Dictionary _endpointRelativePositions = new Dictionary(); +// public IEnumerable> NodePositions => _nodePositions; + +// private readonly Dictionary _nodeSpeeds = new Dictionary(); + +// public VectorBase GetNodePosition(DesignerItemViewModelBase node) +// { +// if (!_nodePositions.TryGetValue(node, out VectorBase result)) +// { +// result = new VectorBase(node.Position.X, node.Position.Y); +// } + +// return result; +// } + +// public void SetNodePosition(DesignerItemViewModelBase node, VectorBase pos) +// { +// _nodePositions[node] = pos; +// } + +// public VectorBase GetEndpointPosition(ConnectorInfoBase endpoint) +// { +// if (!_endpointRelativePositions.TryGetValue(endpoint, out VectorBase result)) +// { +// result = new VectorBase(endpoint.MiddlePosition.X, endpoint.MiddlePosition.Y) - GetNodePosition(endpoint.Parent); +// _endpointRelativePositions[endpoint] = result; +// } + +// return result + GetNodePosition(endpoint.Parent as DesignerItemViewModelBase); +// } + +// public VectorBase GetNodeSpeed(DesignerItemViewModelBase node) +// { +// if (!_nodeSpeeds.TryGetValue(node, out VectorBase result)) +// { +// result = new VectorBase(0, 0); +// } + +// return result; +// } + +// public void SetNodeSpeed(DesignerItemViewModelBase node, VectorBase speed) +// { +// _nodeSpeeds[node] = speed; +// } +// } + +// internal class LiveState : IState +// { +// private readonly Dictionary _nodeSpeeds = new Dictionary(); + +// public VectorBase GetNodePosition(DesignerItemViewModelBase node) +// { +// return new VectorBase(node.Position.X, node.Position.Y); +// } + +// public void SetNodePosition(DesignerItemViewModelBase node, VectorBase pos) +// { +// node.Position = new Point(pos.X, pos.Y); +// } + +// public VectorBase GetEndpointPosition(ConnectorInfoBase endpoint) +// { +// return new VectorBase(endpoint.MiddlePosition.X, endpoint.MiddlePosition.Y); +// } + +// public VectorBase GetNodeSpeed(DesignerItemViewModelBase node) +// { +// if (!_nodeSpeeds.TryGetValue(node, out VectorBase result)) +// { +// result = new VectorBase(0, 0); +// } + +// return result; +// } + +// public void SetNodeSpeed(DesignerItemViewModelBase node, VectorBase speed) +// { +// _nodeSpeeds[node] = speed; +// } +// } +//} diff --git a/AIStudio.Wpf.DiagramDesigner/ViewModels/BaseViewModel/DesignerItemViewModelBase.cs b/AIStudio.Wpf.DiagramDesigner/ViewModels/BaseViewModel/DesignerItemViewModelBase.cs index dff8e2d..483812c 100644 --- a/AIStudio.Wpf.DiagramDesigner/ViewModels/BaseViewModel/DesignerItemViewModelBase.cs +++ b/AIStudio.Wpf.DiagramDesigner/ViewModels/BaseViewModel/DesignerItemViewModelBase.cs @@ -6,6 +6,7 @@ using System.ComponentModel; using System.Linq; using System.Reactive.Linq; using System.Windows; +using AIStudio.Wpf.DiagramDesigner.Controls; using AIStudio.Wpf.DiagramDesigner.Geometrys; using AIStudio.Wpf.DiagramDesigner.Models; @@ -442,7 +443,11 @@ namespace AIStudio.Wpf.DiagramDesigner { get { - return new PointBase(Left, Top); + return TopLeft; + } + set + { + TopLeft = value; } } @@ -625,6 +630,22 @@ namespace AIStudio.Wpf.DiagramDesigner } } + public List InputConnectors + { + get + { + return Connectors.Where(p => p.Orientation == ConnectorOrientation.Left || p.Orientation == ConnectorOrientation.Top).ToList(); + } + } + + public List OutputConnectors + { + get + { + return Connectors.Where(p => p.Orientation == ConnectorOrientation.Right || p.Orientation == ConnectorOrientation.Bottom).ToList(); + } + } + protected ObservableCollection menuOptions; public IEnumerable MenuOptions {