diff --git a/Foreman/Foreman.csproj b/Foreman/Foreman.csproj
index ea76a31..08c52d1 100644
--- a/Foreman/Foreman.csproj
+++ b/Foreman/Foreman.csproj
@@ -113,6 +113,9 @@
..\packages\System.IO.Compression.ZipFile.4.3.0\lib\net46\System.IO.Compression.ZipFile.dll
+
+ ..\packages\System.ValueTuple.4.5.0\lib\net461\System.ValueTuple.dll
+
@@ -210,6 +213,8 @@
+
+
diff --git a/Foreman/Forms/MainForm.Designer.cs b/Foreman/Forms/MainForm.Designer.cs
index 849d57d..9802cc0 100644
--- a/Foreman/Forms/MainForm.Designer.cs
+++ b/Foreman/Forms/MainForm.Designer.cs
@@ -56,6 +56,10 @@ private void InitializeComponent()
this.label4 = new System.Windows.Forms.Label();
this.PauseUpdatesCheckbox = new System.Windows.Forms.CheckBox();
this.GraphSummaryButton = new System.Windows.Forms.Button();
+ this.GraphLayoutGroupBox = new System.Windows.Forms.GroupBox();
+ this.GraphLayoutTable = new System.Windows.Forms.TableLayoutPanel();
+ this.LayoutGraphButton = new System.Windows.Forms.Button();
+ this.ReduceCrossingsCheckBox = new System.Windows.Forms.CheckBox();
this.VersionLabel = new System.Windows.Forms.Label();
this.MainLayoutPanel.SuspendLayout();
this.MenuTable.SuspendLayout();
@@ -64,6 +68,8 @@ private void InitializeComponent()
this.GridlinesTable.SuspendLayout();
this.ProductionGroupBox.SuspendLayout();
this.GraphOptionsTable.SuspendLayout();
+ this.GraphLayoutGroupBox.SuspendLayout();
+ this.GraphLayoutTable.SuspendLayout();
this.SuspendLayout();
//
// MainLayoutPanel
@@ -115,12 +121,13 @@ private void InitializeComponent()
this.MenuTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
this.MenuTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
this.MenuTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
- this.MenuTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F));
- this.MenuTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F));
+ this.MenuTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
+ this.MenuTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
this.MenuTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
this.MenuTable.Controls.Add(this.MenuButtonsTable, 0, 0);
this.MenuTable.Controls.Add(this.GridLinesGroupBox, 1, 0);
this.MenuTable.Controls.Add(this.ProductionGroupBox, 2, 0);
+ this.MenuTable.Controls.Add(this.GraphLayoutGroupBox, 3, 0);
this.MenuTable.Controls.Add(this.VersionLabel, 5, 0);
this.MenuTable.Dock = System.Windows.Forms.DockStyle.Fill;
this.MenuTable.Location = new System.Drawing.Point(3, 3);
@@ -526,13 +533,72 @@ private void InitializeComponent()
this.GraphSummaryButton.UseVisualStyleBackColor = true;
this.GraphSummaryButton.Click += new System.EventHandler(this.GraphSummaryButton_Click);
//
+ // GraphLayoutGroupBox
+ //
+ this.GraphLayoutGroupBox.AutoSize = true;
+ this.GraphLayoutGroupBox.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink;
+ this.GraphLayoutGroupBox.Controls.Add(this.GraphLayoutTable);
+ this.GraphLayoutGroupBox.Dock = System.Windows.Forms.DockStyle.Fill;
+ this.GraphLayoutGroupBox.Location = new System.Drawing.Point(664, 3);
+ this.GraphLayoutGroupBox.Margin = new System.Windows.Forms.Padding(3, 3, 3, 5);
+ this.GraphLayoutGroupBox.Name = "GraphLayoutGroupBox";
+ this.GraphLayoutGroupBox.Padding = new System.Windows.Forms.Padding(0);
+ this.GraphLayoutGroupBox.Size = new System.Drawing.Size(126, 122);
+ this.GraphLayoutGroupBox.TabIndex = 19;
+ this.GraphLayoutGroupBox.TabStop = false;
+ this.GraphLayoutGroupBox.Text = "Layout [EXP]";
+ //
+ // GraphLayoutTable
+ //
+ this.GraphLayoutTable.AutoSize = true;
+ this.GraphLayoutTable.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink;
+ this.GraphLayoutTable.ColumnCount = 2;
+ this.GraphLayoutTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
+ this.GraphLayoutTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
+ this.GraphLayoutTable.Controls.Add(this.LayoutGraphButton, 0, 0);
+ this.GraphLayoutTable.Controls.Add(this.ReduceCrossingsCheckBox, 0, 2);
+ this.GraphLayoutTable.Location = new System.Drawing.Point(3, 16);
+ this.GraphLayoutTable.Margin = new System.Windows.Forms.Padding(3, 3, 3, 0);
+ this.GraphLayoutTable.Name = "GraphLayoutTable";
+ this.GraphLayoutTable.RowCount = 4;
+ this.GraphLayoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
+ this.GraphLayoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
+ this.GraphLayoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
+ this.GraphLayoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 20F));
+ this.GraphLayoutTable.Size = new System.Drawing.Size(120, 66);
+ this.GraphLayoutTable.TabIndex = 1;
+ //
+ // LayoutGraphButton
+ //
+ this.LayoutGraphButton.Dock = System.Windows.Forms.DockStyle.Fill;
+ this.LayoutGraphButton.Location = new System.Drawing.Point(3, 3);
+ this.LayoutGraphButton.Name = "LayoutGraphButton";
+ this.LayoutGraphButton.Size = new System.Drawing.Size(114, 23);
+ this.LayoutGraphButton.TabIndex = 1;
+ this.LayoutGraphButton.Text = "Layout Graph";
+ this.LayoutGraphButton.UseVisualStyleBackColor = true;
+ this.LayoutGraphButton.Click += new System.EventHandler(this.LayoutGraphButton_Click);
+ //
+ // ReduceCrossingsCheckBox
+ //
+ this.ReduceCrossingsCheckBox.AutoSize = true;
+ this.ReduceCrossingsCheckBox.Dock = System.Windows.Forms.DockStyle.Fill;
+ this.ReduceCrossingsCheckBox.Location = new System.Drawing.Point(3, 29);
+ this.ReduceCrossingsCheckBox.Margin = new System.Windows.Forms.Padding(3, 0, 3, 0);
+ this.ReduceCrossingsCheckBox.Name = "ReduceCrossingsCheckBox";
+ this.ReduceCrossingsCheckBox.Size = new System.Drawing.Size(114, 17);
+ this.ReduceCrossingsCheckBox.TabIndex = 3;
+ this.ReduceCrossingsCheckBox.Text = "Reduce Crossings";
+ this.ReduceCrossingsCheckBox.UseVisualStyleBackColor = true;
+ this.ReduceCrossingsCheckBox.CheckedChanged += new System.EventHandler(this.ReduceCrossingsCheckBox_CheckedChanged);
+ //
// VersionLabel
//
this.VersionLabel.AutoSize = true;
this.VersionLabel.Dock = System.Windows.Forms.DockStyle.Fill;
- this.VersionLabel.Location = new System.Drawing.Point(810, 0);
+ this.VersionLabel.Location = new System.Drawing.Point(811, 0);
this.VersionLabel.Name = "VersionLabel";
- this.VersionLabel.Size = new System.Drawing.Size(115, 130);
+ this.VersionLabel.Size = new System.Drawing.Size(114, 130);
this.VersionLabel.TabIndex = 18;
this.VersionLabel.Text = "Foreman v2.0 - dev.12";
this.VersionLabel.TextAlign = System.Drawing.ContentAlignment.TopRight;
@@ -566,6 +632,10 @@ private void InitializeComponent()
this.ProductionGroupBox.PerformLayout();
this.GraphOptionsTable.ResumeLayout(false);
this.GraphOptionsTable.PerformLayout();
+ this.GraphLayoutGroupBox.ResumeLayout(false);
+ this.GraphLayoutGroupBox.PerformLayout();
+ this.GraphLayoutTable.ResumeLayout(false);
+ this.GraphLayoutTable.PerformLayout();
this.ResumeLayout(false);
}
@@ -600,6 +670,10 @@ private void InitializeComponent()
private System.Windows.Forms.CheckBox IconViewCheckBox;
private System.Windows.Forms.Label VersionLabel;
private System.Windows.Forms.Button SaveButton;
+ private System.Windows.Forms.Button LayoutGraphButton;
+ private System.Windows.Forms.GroupBox GraphLayoutGroupBox;
+ private System.Windows.Forms.TableLayoutPanel GraphLayoutTable;
+ private System.Windows.Forms.CheckBox ReduceCrossingsCheckBox;
}
}
diff --git a/Foreman/Forms/MainForm.cs b/Foreman/Forms/MainForm.cs
index 72ff8a0..0b5d5ce 100644
--- a/Foreman/Forms/MainForm.cs
+++ b/Foreman/Forms/MainForm.cs
@@ -595,6 +595,18 @@ private void GraphViewer_KeyDown(object sender, KeyEventArgs e)
}
}
+ //---------------------------------------------------------Graph layout
+
+ private void LayoutGraphButton_Click(object sender, EventArgs e)
+ {
+ GraphViewer.LayoutGraph();
+ }
+
+ private void ReduceCrossingsCheckBox_CheckedChanged(object sender, EventArgs e)
+ {
+ GraphViewer.ReduceCrossings = ReduceCrossingsCheckBox.Checked;
+ }
+
//---------------------------------------------------------double buffering commands
public static void SetDoubleBuffered(Control c)
diff --git a/Foreman/Models/Layout/CoordinateAssignment.cs b/Foreman/Models/Layout/CoordinateAssignment.cs
new file mode 100644
index 0000000..b2336df
--- /dev/null
+++ b/Foreman/Models/Layout/CoordinateAssignment.cs
@@ -0,0 +1,240 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+
+namespace Foreman
+{
+ ///
+ /// A graph whose nodes have been assigned to a list of ordered layers.
+ ///
+ /// the type of the graph nodes
+ ///
+ public interface ILayeredGraph
+ {
+ int Height { get; }
+ int Width(int i);
+
+ IEnumerable Nodes { get; }
+ N this[int i, int j] { get; }
+
+ IEnumerable UpperNeighbors(N node);
+ IEnumerable LowerNeighbors(N node);
+
+ bool IsBend(N node);
+
+ int Pos(N node);
+ int Layer(N node);
+ }
+
+ ///
+ /// Implementation of an algorithm by Ulrik Brandes and Boris Köpf:
+ /// "Fast and Simple Horizontal Coordinate Assignment".
+ ///
+ ///
+ ///
+ ///
+ public class CoordinateAssignment
+ {
+ ///
+ /// A graph adapter that allows to view a graph with left/right or top/down inverted.
+ ///
+ /// the type of the graph nodes
+ private class FlippedGraph : ILayeredGraph
+ {
+ private readonly ILayeredGraph graph;
+ private readonly bool vFlip, hFlip;
+
+ public FlippedGraph(ILayeredGraph underlying, bool vFlip, bool hFlip)
+ {
+ this.graph = underlying;
+ this.vFlip = vFlip;
+ this.hFlip = hFlip;
+ }
+
+ public int Height => graph.Height;
+ public int Width(int i) => graph.Width(V(i));
+
+ public IEnumerable Nodes => graph.Nodes;
+ public N this[int i, int j] => graph[V(i), H(i, j)];
+
+ public IEnumerable UpperNeighbors(N node) => vFlip ? graph.LowerNeighbors(node) : graph.UpperNeighbors(node);
+ public IEnumerable LowerNeighbors(N node) => vFlip ? graph.UpperNeighbors(node) : graph.LowerNeighbors(node);
+
+ public bool IsBend(N node) => graph.IsBend(node);
+
+ public int Pos(N node) => H(Layer(node), graph.Pos(node));
+ public int Layer(N node) => V(graph.Layer(node));
+
+ int V(int i) => vFlip ? Height + 1 - i : i;
+ int H(int i, int j) => hFlip ? Width(i) + 1 - j : j;
+ }
+
+ ///
+ /// Computes coordinates for a layered graph.
+ ///
+ /// the type of the graph nodes
+ /// the graph
+ /// a function that returns the width of a node
+ /// a dictionary that contains coordinates for all nodes
+ public IDictionary AssignCoordinates(ILayeredGraph graph, Func nodeWidth) where N : class
+ {
+ var layouts = new Dictionary<(bool, bool), Dictionary>();
+
+ foreach (var vFlip in new[] { false, true })
+ {
+ foreach (var hFlip in new[] { false, true })
+ {
+ var flipped = new FlippedGraph(graph, vFlip, hFlip);
+ var markedSegments = MarkType1Conflicts(flipped);
+ var (root, align) = VerticalAlignment(flipped, markedSegments);
+ layouts[(vFlip, hFlip)] = HorizontalCompaction(flipped, root, align, nodeWidth);
+ }
+ }
+
+ var minWidth = layouts.Values.Select(l => l.Values.DefaultIfEmpty(0).Max()).Min();
+
+ int x(N node)
+ {
+ return new[] {
+ layouts[(false, false)][node],
+ minWidth - layouts[(false, true)][node],
+ layouts[(true, false)][node],
+ minWidth - layouts[(true, true)][node]
+ }.OrderBy(val => val).Skip(1).Take(2).Sum() / 2;
+ }
+
+ return graph.Nodes.ToDictionary(
+ node => node,
+ node => new Point(x(node), 192 * graph.Layer(node))); // TODO: Make vertical distance configurable
+ }
+
+ private IEnumerable Range(int from, int to) => (to < from) ? Enumerable.Empty() : Enumerable.Range(from, to - from + 1);
+
+ private HashSet<(N, N)> MarkType1Conflicts(ILayeredGraph graph)
+ {
+ var markedSegments = new HashSet<(N, N)>();
+
+ foreach (var i in Range(2, graph.Height - 2))
+ {
+ int k0 = 0, l = 1;
+
+ foreach (var l1 in Range(1, graph.Width(i + 1)))
+ {
+ var vl1 = graph[i + 1, l1];
+ var incidentToInnerSegment =
+ graph.IsBend(vl1) &&
+ graph.IsBend(graph.UpperNeighbors(vl1).Single());
+
+ if (l1 == graph.Width(i + 1) || incidentToInnerSegment)
+ {
+ var k1 = graph.Width(i);
+
+ if (incidentToInnerSegment)
+ k1 = graph.Pos(graph.UpperNeighbors(vl1).Single());
+
+ while (l < l1)
+ {
+ foreach (var vik in graph.UpperNeighbors(graph[i + 1, l]))
+ if (graph.Pos(vik) < k0 || graph.Pos(vik) > k1) markedSegments.Add((vik, graph[i + 1, l]));
+
+ ++l;
+ }
+
+ k0 = k1;
+ }
+ }
+ }
+
+ return markedSegments;
+ }
+
+ private IEnumerable Middle(ILayeredGraph graph, IEnumerable nodes)
+ {
+ var neighbors = nodes.OrderBy(v => graph.Pos(v)).ToList();
+ var l = neighbors.Count;
+
+ if (l > 0)
+ {
+ if (l % 2 == 0) yield return neighbors[(l - 1) / 2];
+ yield return neighbors[l / 2];
+ }
+ }
+
+ private (Dictionary, Dictionary) VerticalAlignment(ILayeredGraph graph, HashSet<(N, N)> markedSegments) where N : class
+ {
+ var root = graph.Nodes.ToDictionary(v => v);
+ var align = graph.Nodes.ToDictionary(v => v);
+
+ foreach (var i in Range(1, graph.Height))
+ {
+ var r = 0;
+ foreach (var k in Range(1, graph.Width(i)))
+ {
+ var vik = graph[i, k];
+ foreach (var um in Middle(graph, graph.UpperNeighbors(vik)))
+ {
+ if (align[vik] == vik)
+ {
+ if (!markedSegments.Contains((um, vik)) && r < graph.Pos(um))
+ {
+ align[um] = vik;
+ root[vik] = root[um];
+ align[vik] = root[vik];
+ r = graph.Pos(um);
+ }
+ }
+ }
+ }
+ }
+
+ return (root, align);
+ }
+
+ private Dictionary HorizontalCompaction(ILayeredGraph graph, Dictionary root, Dictionary align, Func nodeWidth) where N : class
+ {
+ var sink = graph.Nodes.ToDictionary(v => v);
+ var shift = graph.Nodes.ToDictionary(v => v, v => int.MaxValue);
+ var x = new Dictionary();
+
+ void placeBlock(N v)
+ {
+ if (!x.ContainsKey(v))
+ {
+ x[v] = 0;
+ var w = v;
+
+ do
+ {
+ if (graph.Pos(w) > 1)
+ {
+ var predW = graph[graph.Layer(w), graph.Pos(w) - 1];
+ var delta = (nodeWidth(w) + nodeWidth(predW)) / 2;
+
+ var u = root[predW];
+ placeBlock(u);
+ if (sink[v] == v) sink[v] = sink[u];
+ if (sink[v] != sink[u])
+ shift[sink[u]] = Math.Min(shift[sink[u]], x[v] - x[u] - delta);
+ else
+ x[v] = Math.Max(x[v], x[u] + delta);
+ }
+ w = align[w];
+ } while (w != v);
+ }
+ }
+
+ foreach (var v in graph.Nodes)
+ if (root[v] == v) placeBlock(v);
+
+ foreach (var v in graph.Nodes)
+ {
+ x[v] = x[root[v]];
+ if (shift[sink[root[v]]] < int.MaxValue)
+ x[v] = x[v] + shift[sink[root[v]]];
+ }
+
+ return x;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Foreman/Models/Layout/GraphLayout.cs b/Foreman/Models/Layout/GraphLayout.cs
new file mode 100644
index 0000000..03ba7b9
--- /dev/null
+++ b/Foreman/Models/Layout/GraphLayout.cs
@@ -0,0 +1,183 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+
+namespace Foreman
+{
+ public partial class ProductionGraph
+ {
+ ///
+ /// A directed graph where the nodes have been assigned to layers.
+ /// This class uses 1-based indexing like the Brandes/Köpf algorithm>
+ ///
+ private class LayeredGraph : ILayeredGraph
+ {
+ public class Node
+ {
+ public ReadOnlyBaseNode BaseNode { get; }
+ public int Layer { get; }
+ public int Pos { get; set; }
+
+ public Node(ReadOnlyBaseNode node, int layer) { BaseNode = node; Layer = layer; }
+ }
+
+ private readonly IDictionary nodes;
+ private readonly List> layers;
+
+ public LayeredGraph(Dictionary layering)
+ {
+ nodes = layering
+ .ToDictionary(n => n.Key, n => new Node(n.Key, n.Value));
+
+ layers = nodes.Values
+ .GroupBy(Layer)
+ .OrderBy(layer => layer.Key)
+ .Select(layer => layer.OrderBy(n => n.BaseNode.Location.X).Select(UpdatePos).ToList())
+ .ToList();
+ }
+
+ public int Height => layers.Count();
+ public int Width(int i) => layers[i - 1].Count();
+
+ public IEnumerable Nodes => nodes.Values;
+ public Node this[int i, int j] => layers[i - 1][j - 1];
+
+ public IEnumerable UpperNeighbors(Node node) => Neigbors(node, NodeDirection.Up);
+ public IEnumerable LowerNeighbors(Node node) => Neigbors(node, NodeDirection.Down);
+
+ public bool IsBend(Node node) => ProductionGraph.IsBend(node.BaseNode);
+
+ public int Pos(Node node) => node.Pos;
+ public int Layer(Node node) => node.Layer;
+
+ public Node this[ReadOnlyBaseNode node] => nodes[node];
+
+ private IEnumerable Neigbors(Node node, NodeDirection direction)
+ {
+ return node.BaseNode.NodeDirection == direction
+ ? node.BaseNode.OutputLinks.Select(l => nodes[l.Consumer])
+ : node.BaseNode.InputLinks.Select(l => nodes[l.Supplier]);
+ }
+
+ private static Node UpdatePos(Node n, int i)
+ {
+ n.Pos = i + 1;
+ return n;
+ }
+
+ public void ReduceCrossings()
+ {
+ if (Height > 0)
+ {
+ foreach (var i in Enumerable.Range(1, Height - 1))
+ layers[i] = layers[i].OrderBy(e => UpperNeighbors(e).Select(Pos).DefaultIfEmpty(0).Average()).Select(UpdatePos).ToList();
+
+ foreach (var i in Enumerable.Range(0, Height - 1).Reverse())
+ layers[i] = layers[i].OrderBy(e => LowerNeighbors(e).Select(Pos).DefaultIfEmpty(0).Average()).Select(UpdatePos).ToList();
+ }
+ }
+ }
+
+ private Dictionary Normalize()
+ {
+ var layering = new Dictionary();
+ var callStack = new HashSet();
+
+ int ComputeLayer(ReadOnlyBaseNode node)
+ {
+ while (true)
+ {
+ var consumers = node.OutputLinks.Select(l => l.Consumer).ToList();
+
+ if (consumers.Count() == 0)
+ return 1;
+
+ var maxConsumerLayer = consumers.Select(GetLayer).Max();
+ var maxConsumers = consumers.Where(c => GetLayer(c) == maxConsumerLayer).ToList();
+
+ if (IsBend(node) || !maxConsumers.All(IsBend))
+ return maxConsumerLayer + 1;
+
+ // Remove extraneous passthrough nodes. If we get here, then all predecessors of the
+ // current node are bends. We delete them and start over.
+ // We don't just delete all bends, even if they would be recreated below to keep the
+ // layout more stable.
+ foreach (var consumer in maxConsumers)
+ {
+ (RequestNodeController(consumer) as PassthroughNodeController).JoinLinks();
+ layering.Remove(consumer);
+ }
+ }
+ }
+
+ int GetLayer(ReadOnlyBaseNode node)
+ {
+ // Break cycles: the current node is already on the call stack we have detected a cycle.
+ // We just return 0 (to not unnecessarily shift down other nodes) and rely on the other
+ // call further up the call stack to return a more useful layer. This is pretty arbitrary,
+ // but at least it prevents infinite loops.
+ if (callStack.Contains(node)) return 0;
+ callStack.Add(node);
+
+ if (!layering.ContainsKey(node))
+ layering[node] = ComputeLayer(node);
+
+ callStack.Remove(node);
+ return layering[node];
+ }
+
+ // Visit nodes at the bottom before nodes that are further up. For acyclic graphs this makes
+ // no difference, but for cyclic graphs it keeps the layout more stable and gives the user
+ // limited control about where to break the cycle by rearranging the nodes. This isn't very
+ // flexible, but it's better than nothing.
+ foreach (var node in Nodes.OrderByDescending(n => n.Location.Y))
+ GetLayer(node);
+
+ // Add additional passthrough nodes to ensure all node links span only one layer.
+ foreach (var link in NodeLinks.ToList())
+ {
+ var i1 = GetLayer(link.Consumer);
+ var i2 = GetLayer(link.Supplier);
+
+ if (i2 - i1 > 1)
+ {
+ var consumer = link.Consumer;
+ foreach (var i in Enumerable.Range(i1 + 1, i2 - i1 - 1))
+ {
+ var passthrough = CreatePassthroughNode(link.Item, new Point(0, 0)); // TODO: Chose a better startup coordinate
+ (RequestNodeController(passthrough) as PassthroughNodeController).SetSimpleDraw(true);
+ layering[passthrough] = i;
+ CreateLink(passthrough, consumer, link.Item);
+ consumer = passthrough;
+ }
+ CreateLink(link.Supplier, consumer, link.Item);
+ DeleteLink(link);
+ }
+ }
+
+ return layering;
+ }
+
+ public void LayoutGraph(bool reduceCrossings, Func nodeWidth)
+ {
+ var graph = new LayeredGraph(Normalize());
+
+ if (reduceCrossings)
+ graph.ReduceCrossings();
+
+ var locations = new CoordinateAssignment().AssignCoordinates(graph, n => nodeWidth(n.BaseNode));
+
+ // TODO: Should we really just ignore if there is no controller?
+ // This is probably because we're calling the layouter at the wrong time...
+ foreach (var node in graph.Nodes)
+ RequestNodeController(node.BaseNode)?.SetLocation(locations[node]);
+ }
+
+ private static bool IsBend(ReadOnlyBaseNode node) =>
+ node is ReadOnlyPassthroughNode passthrough && passthrough.SimpleDraw &&
+ node.InputLinks.Count() == 1 &&
+ node.OutputLinks.Count() == 1;
+ }
+}
+
diff --git a/Foreman/Models/Nodes/PassthroughNode.cs b/Foreman/Models/Nodes/PassthroughNode.cs
index c8302fc..6bc36a2 100644
--- a/Foreman/Models/Nodes/PassthroughNode.cs
+++ b/Foreman/Models/Nodes/PassthroughNode.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
+using System.Linq;
using System.Runtime.Serialization;
namespace Foreman
@@ -103,5 +104,15 @@ public override Dictionary GetErrorResolutions()
}
public override Dictionary GetWarningResolutions() { Trace.Fail("Passthrough node never has the warning state!"); return null; }
+
+ public ReadOnlyNodeLink JoinLinks()
+ {
+ var link = MyNode.MyGraph.CreateLink(
+ MyNode.InputLinks.Single().SupplierNode.ReadOnlyNode,
+ MyNode.OutputLinks.Single().ConsumerNode.ReadOnlyNode,
+ MyNode.PassthroughItem); ;
+ Delete();
+ return link;
+ }
}
}
diff --git a/Foreman/ProductionGraphView/ProductionGraphViewer.cs b/Foreman/ProductionGraphView/ProductionGraphViewer.cs
index b27a8d2..479da38 100644
--- a/Foreman/ProductionGraphView/ProductionGraphViewer.cs
+++ b/Foreman/ProductionGraphView/ProductionGraphViewer.cs
@@ -36,7 +36,7 @@ public enum LOD { Low, Medium, High } //low: only names. medium: assemblers, bea
public bool DynamicLinkWidth = false;
public bool LockedRecipeEditPanelPosition = true;
public bool FlagOUSuppliedNodes = false; //if true, will add a flag for over or under supplied nodes
-
+ public bool ReduceCrossings = false;
public bool SmartNodeDirection { get; set; }
public DataCache DCache { get; set; }
@@ -407,8 +407,8 @@ public void EditNode(BaseNodeElement bNodeElement)
fttc.Closing += (s, e) =>
{
SubwindowOpen = false;
- //bNodeElement.Update();
- Graph.UpdateNodeValues();
+ //bNodeElement.Update();
+ Graph.UpdateNodeValues();
};
}
@@ -541,6 +541,13 @@ protected override void OnPaint(PaintEventArgs e)
Paint(e.Graphics, false);
}
+ public void LayoutGraph()
+ {
+ // TODO: Make the passthrough width configurable
+ Graph.LayoutGraph(ReduceCrossings, n => (n is ReadOnlyPassthroughNode passthrough && passthrough.SimpleDraw) ? 48 : 24 + nodeElementDictionary[n].Width);
+ UpdateNodeVisuals();
+ }
+
public new void Paint(Graphics graphics, bool FullGraph = false)
{
//update visibility of all elements
diff --git a/Foreman/packages.config b/Foreman/packages.config
index 404aae2..fa6b49a 100644
--- a/Foreman/packages.config
+++ b/Foreman/packages.config
@@ -3,4 +3,5 @@
+
\ No newline at end of file