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