From b054517a0f6f330e3dc263ece4764781f997953d Mon Sep 17 00:00:00 2001 From: chtenb Date: Wed, 9 Aug 2023 16:00:38 +0200 Subject: [PATCH 01/12] prepare --- README.md | 8 +-- Rubjerg.Graphviz.Test/CGraphEdgeCases.cs | 4 +- Rubjerg.Graphviz.Test/OldTutorial.cs | 5 -- Rubjerg.Graphviz.Test/Reproductions.cs | 12 ++--- Rubjerg.Graphviz.Test/TestDotLayout.cs | 24 ++++----- Rubjerg.Graphviz.Test/TestXDotLayout.cs | 16 +++--- Rubjerg.Graphviz.Test/Tutorial.cs | 8 +-- Rubjerg.Graphviz/CGraphThing.cs | 5 +- Rubjerg.Graphviz/Edge.cs | 5 +- Rubjerg.Graphviz/Graph.cs | 10 +--- Rubjerg.Graphviz/GraphVizLabel.cs | 68 ------------------------ Rubjerg.Graphviz/Node.cs | 22 +++----- Rubjerg.Graphviz/RootGraph.cs | 40 ++++++++++++-- 13 files changed, 86 insertions(+), 141 deletions(-) delete mode 100644 Rubjerg.Graphviz/GraphVizLabel.cs diff --git a/README.md b/README.md index b40e9f1..102ff01 100644 --- a/README.md +++ b/README.md @@ -234,8 +234,8 @@ public class Tutorial { RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Graph with records"); Node nodeA = root.GetOrAddNode("A"); - nodeA.SafeSetAttribute("shape", "record", ""); - nodeA.SafeSetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}", "\\N"); + nodeA.SetAttribute("shape", "record"); + nodeA.SetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}"); var layout = root.CreateLayout(); @@ -261,8 +261,8 @@ public class Tutorial var somePortId = "port id with :| special characters"; var validPortName = Edge.ConvertUidToPortName(somePortId); Node nodeB = root.GetOrAddNode("B"); - nodeB.SafeSetAttribute("shape", "record", ""); - nodeB.SafeSetAttribute("label", $"<{validPortName}>1|2", "\\N"); + nodeB.SetAttribute("shape", "record"); + nodeB.SetAttribute("label", $"<{validPortName}>1|2"); // The conversion function makes sure different strings don't accidentally map onto the same portname Assert.AreNotEqual(Edge.ConvertUidToPortName(":"), Edge.ConvertUidToPortName("|")); diff --git a/Rubjerg.Graphviz.Test/CGraphEdgeCases.cs b/Rubjerg.Graphviz.Test/CGraphEdgeCases.cs index 0aee358..7ccf2c1 100644 --- a/Rubjerg.Graphviz.Test/CGraphEdgeCases.cs +++ b/Rubjerg.Graphviz.Test/CGraphEdgeCases.cs @@ -372,8 +372,8 @@ public void DotOutputConsistency() RootGraph root = Utils.CreateUniqueTestGraph(); Node nodeA = root.GetOrAddNode("A"); - nodeA.SafeSetAttribute("shape", "record", ""); - nodeA.SafeSetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}", "\\N"); + nodeA.SetAttribute("shape", "record"); + nodeA.SetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}"); root.ComputeLayout(); var dotstr = root.ToDotString(); diff --git a/Rubjerg.Graphviz.Test/OldTutorial.cs b/Rubjerg.Graphviz.Test/OldTutorial.cs index 18d1523..aa5d18d 100644 --- a/Rubjerg.Graphviz.Test/OldTutorial.cs +++ b/Rubjerg.Graphviz.Test/OldTutorial.cs @@ -86,11 +86,6 @@ public void Layouting() + @" {X=[\d.]+, Y=[\d.]+}, {X=[\d.]+, Y=[\d.]+}"; Utils.AssertPattern(expectedSplinePattern, splineString); - GraphvizLabel nodeLabel = nodeA.GetLabel(); - Utils.AssertPattern(@"{X=[\d.]+,Y=[\d.]+,Width=[\d.]+,Height=[\d.]+}", - nodeLabel.BoundingBox().ToString()); - Utils.AssertPattern(@"Times-Roman", nodeLabel.FontName().ToString()); - // Once all layout information is obtained from the graph, the resources should be // reclaimed. To do this, the application should call the cleanup routine associated // with the layout algorithm used to draw the graph. This is done by a call to diff --git a/Rubjerg.Graphviz.Test/Reproductions.cs b/Rubjerg.Graphviz.Test/Reproductions.cs index 6cf8797..eba0188 100644 --- a/Rubjerg.Graphviz.Test/Reproductions.cs +++ b/Rubjerg.Graphviz.Test/Reproductions.cs @@ -33,8 +33,8 @@ public void TestRecordShapeAlignment(string fontname, double fontsize, double ma Node nodeA = root.GetOrAddNode("A"); - nodeA.SafeSetAttribute("shape", "record", ""); - nodeA.SafeSetAttribute("label", "{20 VH|{1|2}}", ""); + nodeA.SetAttribute("shape", "record"); + nodeA.SetAttribute("label", "{20 VH|{1|2}}"); //TestContext.Write(root.ToDotString()); root.ComputeLayout(); @@ -74,8 +74,8 @@ public void TestDotNewlines() RootGraph root = Utils.CreateUniqueTestGraph(); Node nodeA = root.GetOrAddNode("A"); - nodeA.SafeSetAttribute("shape", "record", ""); - nodeA.SafeSetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}", "\\N"); + nodeA.SetAttribute("shape", "record"); + nodeA.SetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}"); var dotString = root.ToDotString(); Assert.IsFalse(dotString.Contains("\r")); @@ -88,8 +88,8 @@ public void TestDotNewlines2() RootGraph root = Utils.CreateUniqueTestGraph(); Node nodeA = root.GetOrAddNode("A"); - nodeA.SafeSetAttribute("shape", "record", ""); - nodeA.SafeSetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}", "\\N"); + nodeA.SetAttribute("shape", "record"); + nodeA.SetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}"); var xdotGraph = root.CreateLayout(); var xNodeA = xdotGraph.GetNode("A"); diff --git a/Rubjerg.Graphviz.Test/TestDotLayout.cs b/Rubjerg.Graphviz.Test/TestDotLayout.cs index 624043a..a7d5ee8 100644 --- a/Rubjerg.Graphviz.Test/TestDotLayout.cs +++ b/Rubjerg.Graphviz.Test/TestDotLayout.cs @@ -179,8 +179,8 @@ public void TestRecordShapeOrder() RootGraph root = CreateUniqueTestGraph(); Node nodeA = root.GetOrAddNode("A"); - nodeA.SafeSetAttribute("shape", "record", ""); - nodeA.SafeSetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}", "\\N"); + nodeA.SetAttribute("shape", "record"); + nodeA.SetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}"); root.ComputeLayout(); @@ -196,8 +196,8 @@ public void TestEmptyRecordShapes() { RootGraph root = CreateUniqueTestGraph(); Node nodeA = root.GetOrAddNode("A"); - nodeA.SafeSetAttribute("shape", "record", ""); - nodeA.SafeSetAttribute("label", "||||", ""); + nodeA.SetAttribute("shape", "record"); + nodeA.SetAttribute("label", "||||"); root.ComputeLayout(); @@ -223,11 +223,11 @@ public void TestPortNameConversion(bool escape) { RootGraph root = CreateUniqueTestGraph(); Node node = root.GetOrAddNode("N"); - node.SafeSetAttribute("shape", "record", ""); - node.SafeSetAttribute("label", label, ""); + node.SetAttribute("shape", "record"); + node.SetAttribute("label", label); Edge edge = root.GetOrAddEdge(node, node, ""); - edge.SafeSetAttribute("tailport", port1 + ":n", ""); - edge.SafeSetAttribute("headport", port2 + ":s", ""); + edge.SetAttribute("tailport", port1 + ":n"); + edge.SetAttribute("headport", port2 + ":s"); root.ToDotFile(GetTestFilePath("out.gv")); } @@ -269,12 +269,12 @@ public void TestLabelEscaping(bool escape) { RootGraph root = CreateUniqueTestGraph(); Node node1 = root.GetOrAddNode("1"); - node1.SafeSetAttribute("shape", "record", ""); - node1.SafeSetAttribute("label", label1, ""); + node1.SetAttribute("shape", "record"); + node1.SetAttribute("label", label1); Node node2 = root.GetOrAddNode("2"); - node2.SafeSetAttribute("label", label2, ""); + node2.SetAttribute("label", label2); Node node3 = root.GetOrAddNode("3"); - node3.SafeSetAttribute("label", label3, ""); + node3.SetAttribute("label", label3); root.ToDotFile(GetTestFilePath("out.gv")); } diff --git a/Rubjerg.Graphviz.Test/TestXDotLayout.cs b/Rubjerg.Graphviz.Test/TestXDotLayout.cs index 3acd2fd..1dde964 100644 --- a/Rubjerg.Graphviz.Test/TestXDotLayout.cs +++ b/Rubjerg.Graphviz.Test/TestXDotLayout.cs @@ -38,9 +38,9 @@ public void TestXDotRecordNode() RootGraph root = Utils.CreateUniqueTestGraph(); Node nodeA = root.GetOrAddNode("A"); - nodeA.SafeSetAttribute("shape", "record", ""); + nodeA.SetAttribute("shape", "record"); // FIXNOW: document that newlines are not supported in record labels - nodeA.SafeSetAttribute("label", "1|{2\n3}", "\\N"); + nodeA.SetAttribute("label", "1|{2\n3}"); var xdotGraph = root.CreateLayout(); var xNodeA = xdotGraph.GetNode("A"); @@ -56,9 +56,9 @@ public void TestXDotNewLines() { RootGraph root = Utils.CreateUniqueTestGraph(); SubGraph cluster = root.GetOrAddSubgraph("cluster_1"); - cluster.SafeSetAttribute("label", "1\n2", ""); + cluster.SetAttribute("label", "1\n2"); Node nodeA = cluster.GetOrAddNode("A"); - nodeA.SafeSetAttribute("label", "a\nb", ""); + nodeA.SetAttribute("label", "a\nb"); var xdotGraph = root.CreateLayout(); @@ -78,8 +78,8 @@ public void TestRecordShapeOrder() RootGraph root = Utils.CreateUniqueTestGraph(); Node nodeA = root.GetOrAddNode("A"); - nodeA.SafeSetAttribute("shape", "record", ""); - nodeA.SafeSetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}", "\\N"); + nodeA.SetAttribute("shape", "record"); + nodeA.SetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}"); var xdotGraph = root.CreateLayout(); @@ -100,8 +100,8 @@ public void TestEmptyRecordShapes() { RootGraph root = Utils.CreateUniqueTestGraph(); Node nodeA = root.GetOrAddNode("A"); - nodeA.SafeSetAttribute("shape", "record", ""); - nodeA.SafeSetAttribute("label", "||||", ""); + nodeA.SetAttribute("shape", "record"); + nodeA.SetAttribute("label", "||||"); var xdotGraph = root.CreateLayout(); diff --git a/Rubjerg.Graphviz.Test/Tutorial.cs b/Rubjerg.Graphviz.Test/Tutorial.cs index 6a24f5d..bfccb31 100644 --- a/Rubjerg.Graphviz.Test/Tutorial.cs +++ b/Rubjerg.Graphviz.Test/Tutorial.cs @@ -183,8 +183,8 @@ public void Records() { RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Graph with records"); Node nodeA = root.GetOrAddNode("A"); - nodeA.SafeSetAttribute("shape", "record", ""); - nodeA.SafeSetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}", "\\N"); + nodeA.SetAttribute("shape", "record"); + nodeA.SetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}"); var layout = root.CreateLayout(); @@ -210,8 +210,8 @@ public void StringEscaping() var somePortId = "port id with :| special characters"; var validPortName = Edge.ConvertUidToPortName(somePortId); Node nodeB = root.GetOrAddNode("B"); - nodeB.SafeSetAttribute("shape", "record", ""); - nodeB.SafeSetAttribute("label", $"<{validPortName}>1|2", "\\N"); + nodeB.SetAttribute("shape", "record"); + nodeB.SetAttribute("label", $"<{validPortName}>1|2"); // The conversion function makes sure different strings don't accidentally map onto the same portname Assert.AreNotEqual(Edge.ConvertUidToPortName(":"), Edge.ConvertUidToPortName("|")); diff --git a/Rubjerg.Graphviz/CGraphThing.cs b/Rubjerg.Graphviz/CGraphThing.cs index b0b220b..85a7200 100644 --- a/Rubjerg.Graphviz/CGraphThing.cs +++ b/Rubjerg.Graphviz/CGraphThing.cs @@ -203,16 +203,17 @@ public bool HasPosition() public void MakeInvisible() { - SafeSetAttribute("style", "invis", ""); + SetAttribute("style", "invis"); } public bool IsInvisible() { - return SafeGetAttribute("style", "") == "invis"; + return GetAttribute("style") == "invis"; } protected static List GetXDotValue(CGraphThing obj, string attrName) { + // FIXNOW var xdotString = obj.SafeGetAttribute(attrName, null); if (xdotString is null) return new List(); diff --git a/Rubjerg.Graphviz/Edge.cs b/Rubjerg.Graphviz/Edge.cs index 01a2bd9..0465ebc 100644 --- a/Rubjerg.Graphviz/Edge.cs +++ b/Rubjerg.Graphviz/Edge.cs @@ -96,7 +96,7 @@ public void SetLogicalTail(SubGraph ltail) if (!MyRootGraph.IsCompound()) throw new InvalidOperationException("rootgraph must be compound for lheads/ltails to be used"); string ltailname = ltail.GetName(); - SafeSetAttribute("ltail", ltailname, ""); + SetAttribute("ltail", ltailname); } /// @@ -110,7 +110,7 @@ public void SetLogicalHead(SubGraph lhead) if (!MyRootGraph.IsCompound()) throw new InvalidOperationException("rootgraph must be compound for lheads/ltails to be used"); string lheadname = lhead.GetName(); - SafeSetAttribute("lhead", lheadname, ""); + SetAttribute("lhead", lheadname); } /// @@ -165,6 +165,7 @@ public PointF[] GetFirstSpline() /// public IEnumerable GetSplines() { + // FIXNOW return GetDrawing().OfType() .Select(x => x.Value.Points.Select(p => new PointF((float)p.X, (float)p.Y)).ToArray()); } diff --git a/Rubjerg.Graphviz/Graph.cs b/Rubjerg.Graphviz/Graph.cs index d5f1635..b71f70f 100644 --- a/Rubjerg.Graphviz/Graph.cs +++ b/Rubjerg.Graphviz/Graph.cs @@ -574,6 +574,7 @@ public RootGraph CreateLayout(string engine = LayoutEngines.Dot) public RectangleF GetBoundingBox() { + // FIXNOW string bb_string = Agget(_ptr, "bb"); if (string.IsNullOrEmpty(bb_string)) return default; @@ -650,14 +651,5 @@ public void RenderToFile(string filename, string format) throw new ApplicationException($"Graphviz render returned error code {render_rc}"); } - [Obsolete("This method is only available after ComputeLayout(), and may crash otherwise. It is obsoleted by GetLabelDrawing(). Refer to tutorial.")] - public GraphvizLabel GetLabel() - { - IntPtr labelptr = GraphLabel(_ptr); - if (labelptr == IntPtr.Zero) - return null; - return new GraphvizLabel(labelptr, BoundingBoxCoords.Centered); - } - #endregion } diff --git a/Rubjerg.Graphviz/GraphVizLabel.cs b/Rubjerg.Graphviz/GraphVizLabel.cs deleted file mode 100644 index 0a69b52..0000000 --- a/Rubjerg.Graphviz/GraphVizLabel.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Drawing; -using static Rubjerg.Graphviz.ForeignFunctionInterface; - -namespace Rubjerg.Graphviz; - -/// -/// In Graphviz the way coordinates of bounding boxes are represented may differ. -/// We want to provide a uniform API with bottom left coords only, so we use this enum to -/// keep track of the current internal representation and convert if needed. -/// -internal enum BoundingBoxCoords -{ - Centered, - BottomLeft -} - -/// -/// Wraps a graphviz label for any kind of graphviz object. -/// -[Obsolete("This object is only available after ComputeLayout(). It is obsoleted by GetLabelDrawing(). Refer to tutorial.")] -public class GraphvizLabel : GraphvizThing -{ - private readonly BoundingBoxCoords representation; - private readonly PointF offset; - - /// - /// Unfortunately the way the bounding box is stored differs per object that the label belongs to. - /// Therefore some extra information is needed to uniformly define a Label object. - /// - internal GraphvizLabel(IntPtr ptr, BoundingBoxCoords representation, PointF offset = default) - : base(ptr) - { - this.representation = representation; - this.offset = offset; - } - - public string FontName() - { - return LabelFontname(_ptr); - } - - /// - /// Label size in points. - /// - public float FontSize() - { - return Convert.ToSingle(LabelFontsize(_ptr)); - } - - public string Text() - { - return LabelText(_ptr); - } - - public RectangleF BoundingBox() - { - float x = Convert.ToSingle(LabelX(_ptr)) + offset.X; - float y = Convert.ToSingle(LabelY(_ptr)) + offset.Y; - float w = Convert.ToSingle(LabelWidth(_ptr)); - float h = Convert.ToSingle(LabelHeight(_ptr)); - if (representation == BoundingBoxCoords.Centered) - return new RectangleF(x - w / 2, y - h / 2, w, h); - else - return new RectangleF(x, y, w, h); - } - -} diff --git a/Rubjerg.Graphviz/Node.cs b/Rubjerg.Graphviz/Node.cs index 8783816..78c159c 100644 --- a/Rubjerg.Graphviz/Node.cs +++ b/Rubjerg.Graphviz/Node.cs @@ -160,11 +160,11 @@ public bool IsAdjacentTo(Node node) public void MakeInvisibleAndSmall() { - SafeSetAttribute("style", "invis", ""); - SafeSetAttribute("margin", "0", ""); - SafeSetAttribute("width", "0", ""); - SafeSetAttribute("height", "0", ""); - SafeSetAttribute("shape", "point", ""); + SetAttribute("style", "invis"); + SetAttribute("margin", "0"); + SetAttribute("width", "0"); + SetAttribute("height", "0"); + SetAttribute("shape", "point"); } #region layout attributes @@ -174,6 +174,7 @@ public void MakeInvisibleAndSmall() /// public PointF GetPosition() { + // FIXNOW // The "pos" attribute is available as part of xdot output if (HasAttribute("pos")) { @@ -193,6 +194,7 @@ public PointF GetPosition() /// public SizeF GetSize() { + // FIXNOW // The "width" and "height" attributes are available as part of xdot output float w, h; if (HasAttribute("width") && HasAttribute("height")) @@ -226,6 +228,7 @@ public RectangleF GetBoundingBox() /// public IEnumerable GetRecordRectangles() { + // FIXNOW if (!HasAttribute("rects")) yield break; @@ -275,13 +278,4 @@ private RectangleF ParseRect(string rect) public IReadOnlyList GetLabelDrawing() => GetXDotValue(this, "_ldraw_"); #endregion - - [Obsolete("This method is only available after ComputeLayout(), and may crash otherwise. It is obsoleted by GetLabelDrawing(). Refer to tutorial.")] - public GraphvizLabel GetLabel() - { - IntPtr labelptr = NodeLabel(_ptr); - if (labelptr == IntPtr.Zero) - return null; - return new GraphvizLabel(labelptr, BoundingBoxCoords.Centered, new PointF(0, 0)); - } } diff --git a/Rubjerg.Graphviz/RootGraph.cs b/Rubjerg.Graphviz/RootGraph.cs index 807db73..c5dc1ce 100644 --- a/Rubjerg.Graphviz/RootGraph.cs +++ b/Rubjerg.Graphviz/RootGraph.cs @@ -5,6 +5,9 @@ namespace Rubjerg.Graphviz; +/// +/// Strict means that there can be at most one edge between any two nodes. +/// public enum GraphType { Directed = 0, @@ -13,6 +16,27 @@ public enum GraphType StrictUndirected = 3 } +/// +/// In Graphviz, the default coordinate system has the origin on the bottom left. +/// Many rendering applications use a coordinate system with the origin at the top left. +/// +public enum CoordinateSystem +{ + BottomLeft = 0, + TopLeft = 1, +} + +public record SizeD(double Width, double Height); +public record PointD(double X, double Y) +{ + public PointD ForCoordSystem(CoordinateSystem coordSystem, double maxY) + { + if (coordSystem == CoordinateSystem.BottomLeft) + return this; + return new PointD(X, maxY - Y); + } +} + /// /// Wraps a cgraph root graph. /// NB: If there is no .net wrapper left that points to any part of a root graph, the root graph is destroyed. @@ -20,7 +44,13 @@ public enum GraphType public class RootGraph : Graph { private long _added_pressure = 0; - protected RootGraph(IntPtr ptr) : base(ptr, null) { } + + public CoordinateSystem CoordinateSystem { get; } + + protected RootGraph(IntPtr ptr, CoordinateSystem coordinateSystem) : base(ptr, null) + { + CoordinateSystem = coordinateSystem; + } ~RootGraph() { if (_added_pressure > 0) @@ -54,11 +84,11 @@ public void UpdateMemoryPressure() /// The name is not interpreted by Graphviz, /// except it is recorded and preserved when the graph is written as a file /// - public static RootGraph CreateNew(GraphType graphtype, string name = null) + public static RootGraph CreateNew(GraphType graphtype, string name = null, CoordinateSystem coordinateSystem = CoordinateSystem.BottomLeft) { name = NameString(name); var ptr = Rjagopen(name, (int)graphtype); - return new RootGraph(ptr); + return new RootGraph(ptr, coordinateSystem); } public static RootGraph FromDotFile(string filename) @@ -86,9 +116,9 @@ protected static T FromDotString(string graph, Func constructor) return result; } - public static RootGraph FromDotString(string graph) + public static RootGraph FromDotString(string graph, CoordinateSystem coordinateSystem = CoordinateSystem.BottomLeft) { - return FromDotString(graph, ptr => new RootGraph(ptr)); + return FromDotString(graph, ptr => new RootGraph(ptr, coordinateSystem)); } public void ConvertToUndirectedGraph() From 0767c5442eda33e3d31bcaa10466f2394b6891e6 Mon Sep 17 00:00:00 2001 From: chtenb Date: Wed, 9 Aug 2023 17:42:51 +0200 Subject: [PATCH 02/12] wip --- Rubjerg.Graphviz.Test/TestXDotLayout.cs | 2 +- Rubjerg.Graphviz.Test/Tutorial.cs | 1 + Rubjerg.Graphviz/Edge.cs | 8 +- Rubjerg.Graphviz/Node.cs | 16 +- Rubjerg.Graphviz/RootGraph.cs | 21 -- Rubjerg.Graphviz/XDot.cs | 280 ++++++++++------------ Rubjerg.Graphviz/XDotParser.cs | 304 ++++++++++-------------- 7 files changed, 267 insertions(+), 365 deletions(-) diff --git a/Rubjerg.Graphviz.Test/TestXDotLayout.cs b/Rubjerg.Graphviz.Test/TestXDotLayout.cs index 1dde964..27cc580 100644 --- a/Rubjerg.Graphviz.Test/TestXDotLayout.cs +++ b/Rubjerg.Graphviz.Test/TestXDotLayout.cs @@ -39,7 +39,7 @@ public void TestXDotRecordNode() Node nodeA = root.GetOrAddNode("A"); nodeA.SetAttribute("shape", "record"); - // FIXNOW: document that newlines are not supported in record labels + // New lines in record labels are ignored by Graphviz nodeA.SetAttribute("label", "1|{2\n3}"); var xdotGraph = root.CreateLayout(); diff --git a/Rubjerg.Graphviz.Test/Tutorial.cs b/Rubjerg.Graphviz.Test/Tutorial.cs index bfccb31..346cc90 100644 --- a/Rubjerg.Graphviz.Test/Tutorial.cs +++ b/Rubjerg.Graphviz.Test/Tutorial.cs @@ -184,6 +184,7 @@ public void Records() RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Graph with records"); Node nodeA = root.GetOrAddNode("A"); nodeA.SetAttribute("shape", "record"); + // New line characters are not supported by record labels, and will be ignored by Graphviz nodeA.SetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}"); var layout = root.CreateLayout(); diff --git a/Rubjerg.Graphviz/Edge.cs b/Rubjerg.Graphviz/Edge.cs index 0465ebc..fc38846 100644 --- a/Rubjerg.Graphviz/Edge.cs +++ b/Rubjerg.Graphviz/Edge.cs @@ -152,7 +152,7 @@ public override int GetHashCode() /// This method only returns the first spline that is defined. /// Returns null if no splines exist. /// - public PointF[] GetFirstSpline() + public PointD[] GetFirstSpline() { return GetSplines().FirstOrDefault(); } @@ -163,11 +163,9 @@ public PointF[] GetFirstSpline() /// https://github.com/ellson/graphviz/issues/1277 /// Edge arrows are ignored. /// - public IEnumerable GetSplines() + public IEnumerable GetSplines() { - // FIXNOW - return GetDrawing().OfType() - .Select(x => x.Value.Points.Select(p => new PointF((float)p.X, (float)p.Y)).ToArray()); + return GetDrawing().OfType().Select(x => x.Points); } public IReadOnlyList GetDrawing() => GetXDotValue(this, "_draw_"); diff --git a/Rubjerg.Graphviz/Node.cs b/Rubjerg.Graphviz/Node.cs index 78c159c..452ac83 100644 --- a/Rubjerg.Graphviz/Node.cs +++ b/Rubjerg.Graphviz/Node.cs @@ -226,9 +226,9 @@ public RectangleF GetBoundingBox() /// If the shape of this node was set to 'record', this method allows you to retrieve the /// resulting rectangles. /// - public IEnumerable GetRecordRectangles() + public IEnumerable GetRecordRectangles() { - // FIXNOW + // FIXNOW remove almost all floats from the code base if (!HasAttribute("rects")) yield break; @@ -236,7 +236,7 @@ public IEnumerable GetRecordRectangles() // As a workaround we consult the x coordinates, and attempt to snap onto those. // https://github.com/Rubjerg/Graphviz.NetWrapper/issues/30 var validXCoords = GetDrawing().OfType() - .SelectMany(p => p.Value.Points).Select(p => p.X).ToList(); + .SelectMany(p => p.Points).Select(p => p.X).ToList(); foreach (string rectStr in GetAttribute("rects").Split(' ')) { @@ -244,11 +244,11 @@ public IEnumerable GetRecordRectangles() var x1 = rect.X; var x2 = rect.X + rect.Width; - var fixedX1 = (float)FindClosest(validXCoords, x1); - var fixedX2 = (float)FindClosest(validXCoords, x2); - var fixedRect = new RectangleF( - new PointF(fixedX1, rect.Y), - new SizeF(fixedX2 - rect.X, rect.Height)); + var fixedX1 = FindClosest(validXCoords, x1); + var fixedX2 = FindClosest(validXCoords, x2); + var fixedRect = new RectangleD( + new PointD(fixedX1, rect.Y), + new SizeD(fixedX2 - rect.X, rect.Height)); yield return fixedRect; } } diff --git a/Rubjerg.Graphviz/RootGraph.cs b/Rubjerg.Graphviz/RootGraph.cs index c5dc1ce..0e101bc 100644 --- a/Rubjerg.Graphviz/RootGraph.cs +++ b/Rubjerg.Graphviz/RootGraph.cs @@ -16,27 +16,6 @@ public enum GraphType StrictUndirected = 3 } -/// -/// In Graphviz, the default coordinate system has the origin on the bottom left. -/// Many rendering applications use a coordinate system with the origin at the top left. -/// -public enum CoordinateSystem -{ - BottomLeft = 0, - TopLeft = 1, -} - -public record SizeD(double Width, double Height); -public record PointD(double X, double Y) -{ - public PointD ForCoordSystem(CoordinateSystem coordSystem, double maxY) - { - if (coordSystem == CoordinateSystem.BottomLeft) - return this; - return new PointD(X, maxY - Y); - } -} - /// /// Wraps a cgraph root graph. /// NB: If there is no .net wrapper left that points to any part of a root graph, the root graph is destroyed. diff --git a/Rubjerg.Graphviz/XDot.cs b/Rubjerg.Graphviz/XDot.cs index 7a878bc..c81870c 100644 --- a/Rubjerg.Graphviz/XDot.cs +++ b/Rubjerg.Graphviz/XDot.cs @@ -5,74 +5,87 @@ namespace Rubjerg.Graphviz; // See https://graphviz.org/docs/outputs/canon/#xdot -public record struct XDotColorStop +/// +/// In Graphviz, the default coordinate system has the origin on the bottom left. +/// Many rendering applications use a coordinate system with the origin at the top left. +/// +public enum CoordinateSystem { - public float Frac { get; init; } - public string Color { get; init; } + BottomLeft = 0, + TopLeft = 1, } -public record struct XDotLinearGrad -{ - public double X0 { get; init; } - public double Y0 { get; init; } - public double X1 { get; init; } - public double Y1 { get; init; } - public int NStops { get; init; } - public XDotColorStop[] Stops { get; init; } -} +public record struct SizeD(double Width, double Height); -public record struct XDotRadialGrad +public record struct PointD(double X, double Y) { - public double X0 { get; init; } - public double Y0 { get; init; } - public double R0 { get; init; } - public double X1 { get; init; } - public double Y1 { get; init; } - public double R1 { get; init; } - public int NStops { get; init; } - public XDotColorStop[] Stops { get; init; } + internal PointD ForCoordSystem(CoordinateSystem coordSystem, double maxY) + { + if (coordSystem == CoordinateSystem.BottomLeft) + return this; + return new PointD(X, maxY - Y); + } } -public abstract record class XDotGradColor +/// The point closest to the origin +/// +public record struct RectangleD(PointD Point, SizeD Size) { - private XDotGradColor() { } - public sealed record class Uniform : XDotGradColor + public static RectangleD Create(double x, double y, double width, double height) { - public string Color { get; init; } - } - public sealed record class LinearGradient : XDotGradColor - { - public XDotLinearGrad LinearGrad { get; init; } + return new RectangleD(new PointD(x, y), new SizeD(width, height)); } - public sealed record class RadialGradient : XDotGradColor + + internal RectangleD ForCoordSystem(CoordinateSystem coordSystem, double maxY) { - public XDotRadialGrad RadialGrad { get; init; } + return this with + { + Point = Point.ForCoordSystem(coordSystem, maxY), + }; } } -public record struct XDotPoint -{ - public double X { get; init; } - public double Y { get; init; } - public double Z { get; init; } +public record struct ColorStop(float Frac, string Color); - public PointF ToPointF() => new PointF((float)X, (float)Y); +public record struct LinearGradient(PointD Point0, PointD Point1, ColorStop[] Stops) +{ + internal LinearGradient ForCoordSystem(CoordinateSystem coordSystem, double maxY) + { + return this with + { + Point0 = Point0.ForCoordSystem(coordSystem, maxY), + Point1 = Point1.ForCoordSystem(coordSystem, maxY), + }; + } } -public record struct XDotRect +public record struct RadialGradient(PointD Point0, double Radius0, PointD Point1, double Radius1, ColorStop[] Stops) { - public double X { get; init; } - public double Y { get; init; } - public double Width { get; init; } - public double Height { get; init; } - - public RectangleF ToRectangleF() => new RectangleF((float)X, (float)Y, (float)Width, (float)Height); + internal RadialGradient ForCoordSystem(CoordinateSystem coordSystem, double maxY) + { + return this with + { + Point0 = Point0.ForCoordSystem(coordSystem, maxY), + Point1 = Point1.ForCoordSystem(coordSystem, maxY), + }; + } } -public record struct XDotPolyline +public abstract record class GradientColor { - public int Count { get; init; } - public XDotPoint[] Points { get; init; } + private GradientColor() { } + public sealed record class Uniform : GradientColor + { + public string Color { get; init; } + } + public sealed record class Linear : GradientColor + { + public LinearGradient Gradient { get; init; } + } + public sealed record class Radial : GradientColor + { + public RadialGradient Gradient { get; init; } + } } public enum XDotAlign @@ -86,71 +99,63 @@ public enum XDotAlign /// Represents a line of text to be drawn. /// Labels with multiple lines will be represented by multiple instances. /// -public record struct XDotText +/// +/// The y-coordinate points to the baseline, +/// the x-coordinate points to the horizontal position relative to which the text should be +/// aligned according to the property. +/// +/// How the text should be aligned horizontally, relative to the given anchor point. +/// The estimated width of the text. +/// +/// +public record struct XDotText(PointD Anchor, XDotAlign Align, double Width, string Text, XDotFont Font) { - /// - /// The X coordinate of the anchor point of the text. - /// - public double X { get; init; } - /// - /// The Y coordinate of the baseline of the text. - /// - public double Y { get; init; } - /// - /// How the text should be aligned, relative to the given anchor point. - /// - public XDotAlign Align { get; init; } - public double Width { get; init; } - public string Text { get; init; } - /// /// Compute the bounding box of this text element given the necessary font information. /// /// Font used to draw the text /// Optional property of the font, to more accurately predict the bounding box. - public RectangleF TextBoundingBox(XDotFont font, float? distanceBetweenBaselineAndDescender = null) + public RectangleD TextBoundingBox(double? distanceBetweenBaselineAndDescender = null) { - var size = Size(font); - var descenderY = Y - (distanceBetweenBaselineAndDescender ?? font.Size / 5); + var size = TextSize(); + var descenderY = Anchor.Y - (distanceBetweenBaselineAndDescender ?? Font.Size / 5); var leftX = Align switch { - XDotAlign.Left => X, - XDotAlign.Center => X + size.Width / 2, - XDotAlign.Right => X + size.Width, + XDotAlign.Left => Anchor.X, + XDotAlign.Center => Anchor.X + size.Width / 2, + XDotAlign.Right => Anchor.X + size.Width, _ => throw new InvalidOperationException() }; - var bottomLeft = new PointF((float)leftX, (float)descenderY); - return new RectangleF(bottomLeft, size); + var bottomLeft = new PointD(leftX, descenderY); + return new RectangleD(bottomLeft, size); } - /// - /// The anchor point of the text. - /// The Y coordinate points to the baseline of the text. - /// The X coordinate points to the horizontal anchor of the text. - /// - public PointF Anchor() => new PointF((float)X, (float)Y); - /// /// The width represents the estimated width of the text by GraphViz. /// The height represents the font size, which is usually the distance between the ascender and the descender /// of the font. /// - public SizeF Size(XDotFont font) => new SizeF((float)Width, (float)font.Size); -} + public SizeD TextSize() => new SizeD(Width, Font.Size); -public record struct XDotImage -{ - public XDotRect Pos { get; init; } - public string Name { get; init; } + internal XDotText ForCoordSystem(CoordinateSystem coordSystem, double maxY) + { + // FIXNOW + // While things like rectangles are anchored by the point closest to the origin, + // the y-coordinate of a text object anchor always points to the baseline of the text. + // This means we have to take extra care when transforming to the top-left coordinate system. + return this with + { + Anchor = Anchor.ForCoordSystem(coordSystem, maxY), + }; + } } -public record struct XDotFont +public record struct XDotImage(RectangleD Position, string Name) { } + +/// Font size in points +/// Font name +public record struct XDotFont(double Size, string Name) { - /// - /// Size in points - /// - public double Size { get; init; } - public string Name { get; init; } public static XDotFont Default => new() { Size = 14, Name = "Times-Roman" }; } @@ -168,74 +173,39 @@ public enum XDotFontChar } /// -/// See https://graphviz.org/docs/outputs/canon/#xdot for semantics +/// See https://graphviz.org/docs/outputs/canon/#xdot for semantics. +/// +/// Within the context of a single drawing attribute, e.g., draw, there is an implicit state for the +/// graphical attributes. That is, once a color, style, or font characteristic is set, it +/// remains valid for all relevant drawing operations until the value is reset by another xdot cmd. +/// +/// Note that the filled figures (ellipses, polygons and B-Splines) imply two operations: first, +/// drawing the filled figure with the current fill color; second, drawing an unfilled figure with +/// the current pen color, pen width and pen style. +/// +/// The text operation is only used in the label attributes. Normally, the non-text operations are +/// only used in the non-label attributes. If, however, the decorate attribute is set on an edge, +/// its label attribute will also contain a polyline operation. In addition, if a label is a +/// complex, HTML-like label, it will also contain non-text operations. /// public abstract record class XDotOp { private XDotOp() { } - public sealed record class FilledEllipse : XDotOp - { - public XDotRect Value { get; init; } - } - public sealed record class UnfilledEllipse : XDotOp - { - public XDotRect Value { get; init; } - } - public sealed record class FilledPolygon : XDotOp - { - public XDotPolyline Value { get; init; } - } - public sealed record class UnfilledPolygon : XDotOp - { - public XDotPolyline Value { get; init; } - } - public sealed record class PolyLine : XDotOp - { - public XDotPolyline Value { get; init; } - } - public sealed record class FilledBezier : XDotOp - { - public XDotPolyline Value { get; init; } - } - public sealed record class UnfilledBezier : XDotOp - { - public XDotPolyline Value { get; init; } - } - public sealed record class Text : XDotOp - { - public XDotText Value { get; init; } - } - public sealed record class Image : XDotOp - { - public XDotImage Value { get; init; } - } - public sealed record class FillColor : XDotOp - { - public string Value { get; init; } - } - public sealed record class PenColor : XDotOp - { - public string Value { get; init; } - } - public sealed record class GradFillColor : XDotOp - { - public XDotGradColor Value { get; init; } - } - public sealed record class GradPenColor : XDotOp - { - public XDotGradColor Value { get; init; } - } - public sealed record class Font : XDotOp - { - public XDotFont Value { get; init; } - } - public sealed record class Style : XDotOp - { - public string Value { get; init; } - } - public sealed record class FontChar : XDotOp - { - public XDotFontChar Value { get; init; } - } + // FIXNOW: can we trim down the cases? Some things are only relevant for a single thing, such as FontChar + public sealed record class FilledEllipse(RectangleD Value) : XDotOp { } + public sealed record class UnfilledEllipse(RectangleD Value) : XDotOp { } + public sealed record class FilledPolygon(PointD[] Points) : XDotOp { } + public sealed record class UnfilledPolygon(PointD[] Points) : XDotOp { } + public sealed record class PolyLine(PointD[] Points) : XDotOp { } + public sealed record class FilledBezier(PointD[] Points) : XDotOp { } + public sealed record class UnfilledBezier(PointD[] Points) : XDotOp { } + public sealed record class Text(XDotText Value) : XDotOp { } + public sealed record class Image(XDotImage Value) : XDotOp { } + public sealed record class FillColor(string Value) : XDotOp { } + public sealed record class PenColor(string Value) : XDotOp { } + public sealed record class GradientFillColor(GradientColor Value) : XDotOp { } + public sealed record class GradientPenColor(GradientColor Value) : XDotOp { } + public sealed record class Style(string Value) : XDotOp { } + public sealed record class FontChar(XDotFontChar Value) : XDotOp { } } diff --git a/Rubjerg.Graphviz/XDotParser.cs b/Rubjerg.Graphviz/XDotParser.cs index 67cf39c..73e11ac 100644 --- a/Rubjerg.Graphviz/XDotParser.cs +++ b/Rubjerg.Graphviz/XDotParser.cs @@ -30,6 +30,8 @@ internal struct XDot public XDotOp[] Ops { get; set; } // xdot operations } +/// +/// internal static class XDotParser { public static List ParseXDot(string xdotString) @@ -62,119 +64,82 @@ internal static List TranslateXDot(IntPtr xdotPtr) int count = xdot.Count; xdot.Ops = new XDotOp[count]; var opsPtr = XDotFFI.get_ops(xdotPtr); + + var activeFont = XDotFont.Default; for (int i = 0; i < count; ++i) { IntPtr xdotOpPtr = XDotFFI.get_op_at_index(opsPtr, i); - xdot.Ops[i] = TranslateXDotOp(xdotOpPtr); + var kind = XDotFFI.get_kind(xdotOpPtr); + switch (kind) + { + case XDotKind.FilledEllipse: + xdot.Ops[i] = new XDotOp.FilledEllipse(TranslateEllipse(XDotFFI.get_ellipse(xdotOpPtr))); + break; + case XDotKind.UnfilledEllipse: + xdot.Ops[i] = new XDotOp.UnfilledEllipse(TranslateEllipse(XDotFFI.get_ellipse(xdotOpPtr))); + break; + case XDotKind.FilledPolygon: + xdot.Ops[i] = new XDotOp.FilledPolygon(TranslatePolyline(XDotFFI.get_polyline(xdotOpPtr))); + break; + case XDotKind.UnfilledPolygon: + xdot.Ops[i] = new XDotOp.FilledPolygon(TranslatePolyline(XDotFFI.get_polyline(xdotOpPtr))); + break; + case XDotKind.FilledBezier: + xdot.Ops[i] = new XDotOp.FilledBezier(TranslatePolyline(XDotFFI.get_polyline(xdotOpPtr))); + break; + case XDotKind.UnfilledBezier: + xdot.Ops[i] = new XDotOp.UnfilledBezier(TranslatePolyline(XDotFFI.get_polyline(xdotOpPtr))); + break; + case XDotKind.Polyline: + xdot.Ops[i] = new XDotOp.PolyLine(TranslatePolyline(XDotFFI.get_polyline(xdotOpPtr))); + break; + case XDotKind.Text: + xdot.Ops[i] = new XDotOp.Text(TranslateText(XDotFFI.get_text(xdotOpPtr), activeFont)); + break; + case XDotKind.FillColor: + xdot.Ops[i] = new XDotOp.FillColor(XDotFFI.GetColor(xdotOpPtr)); + break; + case XDotKind.PenColor: + xdot.Ops[i] = new XDotOp.PenColor(XDotFFI.GetColor(xdotOpPtr)); + break; + case XDotKind.GradFillColor: + xdot.Ops[i] = new XDotOp.GradientFillColor(TranslateGradColor(XDotFFI.get_grad_color(xdotOpPtr))); + break; + case XDotKind.GradPenColor: + xdot.Ops[i] = new XDotOp.GradientPenColor(TranslateGradColor(XDotFFI.get_grad_color(xdotOpPtr))); + break; + case XDotKind.Font: + activeFont = TranslateFont(XDotFFI.get_font(xdotOpPtr)); + break; + case XDotKind.Style: + xdot.Ops[i] = new XDotOp.Style(XDotFFI.GetStyle(xdotOpPtr)); + break; + case XDotKind.Image: + xdot.Ops[i] = new XDotOp.Image(TranslateImage(XDotFFI.get_image(xdotOpPtr))); + break; + case XDotKind.FontChar: + xdot.Ops[i] = new XDotOp.FontChar(TranslateFontChar(XDotFFI.get_fontchar(xdotOpPtr))); + break; + default: + throw new ArgumentException($"Unexpected XDotOp.Kind: {kind}"); + } } return xdot.Ops.ToList(); } - private static XDotOp TranslateXDotOp(IntPtr xdotOpPtr) - { - if (xdotOpPtr == IntPtr.Zero) - throw new ArgumentNullException(nameof(xdotOpPtr)); - - var kind = XDotFFI.get_kind(xdotOpPtr); - switch (kind) - { - case XDotKind.FilledEllipse: - return new XDotOp.FilledEllipse() - { - Value = TranslateEllipse(XDotFFI.get_ellipse(xdotOpPtr)) - }; - case XDotKind.UnfilledEllipse: - return new XDotOp.UnfilledEllipse() - { - Value = TranslateEllipse(XDotFFI.get_ellipse(xdotOpPtr)) - }; - case XDotKind.FilledPolygon: - return new XDotOp.FilledPolygon() - { - Value = TranslatePolyline(XDotFFI.get_polyline(xdotOpPtr)) - }; - case XDotKind.UnfilledPolygon: - return new XDotOp.FilledPolygon() - { - Value = TranslatePolyline(XDotFFI.get_polyline(xdotOpPtr)) - }; - case XDotKind.FilledBezier: - return new XDotOp.FilledBezier() - { - Value = TranslatePolyline(XDotFFI.get_polyline(xdotOpPtr)) - }; - case XDotKind.UnfilledBezier: - return new XDotOp.UnfilledBezier() - { - Value = TranslatePolyline(XDotFFI.get_polyline(xdotOpPtr)) - }; - case XDotKind.Polyline: - return new XDotOp.PolyLine() - { - Value = TranslatePolyline(XDotFFI.get_polyline(xdotOpPtr)) - }; - case XDotKind.Text: - return new XDotOp.Text() - { - Value = TranslateText(XDotFFI.get_text(xdotOpPtr)) - }; - case XDotKind.FillColor: - return new XDotOp.FillColor() - { - Value = XDotFFI.GetColor(xdotOpPtr) - }; - case XDotKind.PenColor: - return new XDotOp.PenColor() - { - Value = XDotFFI.GetColor(xdotOpPtr) - }; - case XDotKind.GradFillColor: - return new XDotOp.GradFillColor() - { - Value = TranslateGradColor(XDotFFI.get_grad_color(xdotOpPtr)) - }; - case XDotKind.GradPenColor: - return new XDotOp.GradPenColor() - { - Value = TranslateGradColor(XDotFFI.get_grad_color(xdotOpPtr)) - }; - case XDotKind.Font: - return new XDotOp.Font() - { - Value = TranslateFont(XDotFFI.get_font(xdotOpPtr)) - }; - case XDotKind.Style: - return new XDotOp.Style() - { - Value = XDotFFI.GetStyle(xdotOpPtr) - }; - case XDotKind.Image: - return new XDotOp.Image() - { - Value = TranslateImage(XDotFFI.get_image(xdotOpPtr)) - }; - case XDotKind.FontChar: - return new XDotOp.FontChar() - { - Value = TranslateFontChar(XDotFFI.get_fontchar(xdotOpPtr)) - }; - default: - throw new ArgumentException($"Unexpected XDotOp.Kind: {kind}"); - } - } - private static XDotFontChar TranslateFontChar(uint value) { return (XDotFontChar)(int)value; } + private static XDotImage TranslateImage(IntPtr imagePtr) { XDotImage image = new XDotImage - { - Pos = TranslateRect(XDotFFI.get_pos(imagePtr)), - Name = XDotFFI.GetNameImage(imagePtr) - }; + ( + Position: TranslateRect(XDotFFI.get_pos(imagePtr)), + Name: XDotFFI.GetNameImage(imagePtr) + ); return image; } @@ -182,64 +147,61 @@ private static XDotImage TranslateImage(IntPtr imagePtr) private static XDotFont TranslateFont(IntPtr fontPtr) { XDotFont font = new XDotFont - { - Size = XDotFFI.get_size(fontPtr), - Name = XDotFFI.GetNameFont(fontPtr) - }; + ( + Size: XDotFFI.get_size(fontPtr), + Name: XDotFFI.GetNameFont(fontPtr) + ); return font; } - private static XDotRect TranslateEllipse(IntPtr ellipsePtr) + private static RectangleD TranslateEllipse(IntPtr ellipsePtr) { - XDotRect ellipse = new XDotRect - { - X = XDotFFI.get_x_rect(ellipsePtr), - Y = XDotFFI.get_y_rect(ellipsePtr), - Width = XDotFFI.get_w_rect(ellipsePtr), - Height = XDotFFI.get_h_rect(ellipsePtr) - }; + RectangleD ellipse = RectangleD.Create + ( + XDotFFI.get_x_rect(ellipsePtr), + XDotFFI.get_y_rect(ellipsePtr), + XDotFFI.get_w_rect(ellipsePtr), + XDotFFI.get_h_rect(ellipsePtr) + ); return ellipse; } - private static XDotGradColor TranslateGradColor(IntPtr colorPtr) + private static GradientColor TranslateGradColor(IntPtr colorPtr) { var type = XDotFFI.get_type(colorPtr); switch (type) { case XDotGradType.None: - return new XDotGradColor.Uniform() + return new GradientColor.Uniform() { Color = XDotFFI.GetClr(colorPtr) }; case XDotGradType.Linear: - return new XDotGradColor.LinearGradient() + return new GradientColor.Linear() { - LinearGrad = TranslateLinearGrad(XDotFFI.get_ling(colorPtr)) + Gradient = TranslateLinearGrad(XDotFFI.get_ling(colorPtr)) }; case XDotGradType.Radial: - return new XDotGradColor.RadialGradient() + return new GradientColor.Radial() { - RadialGrad = TranslateRadialGrad(XDotFFI.get_ring(colorPtr)) + Gradient = TranslateRadialGrad(XDotFFI.get_ring(colorPtr)) }; default: throw new ArgumentException($"Unexpected XDotColor.Type: {type}"); } } - private static XDotLinearGrad TranslateLinearGrad(IntPtr lingPtr) + private static LinearGradient TranslateLinearGrad(IntPtr lingPtr) { int count = XDotFFI.get_n_stops_ling(lingPtr); - XDotLinearGrad linearGrad = new XDotLinearGrad - { - X0 = XDotFFI.get_x0_ling(lingPtr), - Y0 = XDotFFI.get_y0_ling(lingPtr), - X1 = XDotFFI.get_x1_ling(lingPtr), - Y1 = XDotFFI.get_y1_ling(lingPtr), - NStops = count, - Stops = new XDotColorStop[count] - }; + LinearGradient linearGrad = new LinearGradient + ( + Point0: new PointD(XDotFFI.get_x0_ling(lingPtr), XDotFFI.get_y0_ling(lingPtr)), + Point1: new PointD(XDotFFI.get_x1_ling(lingPtr), XDotFFI.get_y1_ling(lingPtr)), + Stops: new ColorStop[count] + ); // Translate the array of ColorStops var stopsPtr = XDotFFI.get_stops_ling(lingPtr); @@ -252,20 +214,17 @@ private static XDotLinearGrad TranslateLinearGrad(IntPtr lingPtr) return linearGrad; } - private static XDotRadialGrad TranslateRadialGrad(IntPtr ringPtr) + private static RadialGradient TranslateRadialGrad(IntPtr ringPtr) { int count = XDotFFI.get_n_stops_ring(ringPtr); - XDotRadialGrad radialGrad = new XDotRadialGrad - { - X0 = XDotFFI.get_x0_ring(ringPtr), - Y0 = XDotFFI.get_y0_ring(ringPtr), - R0 = XDotFFI.get_r0_ring(ringPtr), - X1 = XDotFFI.get_x1_ring(ringPtr), - Y1 = XDotFFI.get_y1_ring(ringPtr), - R1 = XDotFFI.get_r1_ring(ringPtr), - NStops = count, - Stops = new XDotColorStop[count] - }; + RadialGradient radialGrad = new RadialGradient + ( + Point0: new PointD(XDotFFI.get_x0_ring(ringPtr), XDotFFI.get_y0_ring(ringPtr)), + Point1: new PointD(XDotFFI.get_x1_ring(ringPtr), XDotFFI.get_y1_ring(ringPtr)), + Radius0: XDotFFI.get_r0_ring(ringPtr), + Radius1: XDotFFI.get_r1_ring(ringPtr), + Stops: new ColorStop[count] + ); // Translate the array of ColorStops var stopsPtr = XDotFFI.get_stops_ring(ringPtr); @@ -278,72 +237,67 @@ private static XDotRadialGrad TranslateRadialGrad(IntPtr ringPtr) return radialGrad; } - private static XDotColorStop TranslateColorStop(IntPtr stopPtr) + private static ColorStop TranslateColorStop(IntPtr stopPtr) { - XDotColorStop colorStop = new XDotColorStop - { - Frac = XDotFFI.get_frac(stopPtr), - Color = XDotFFI.GetColorStop(stopPtr) - }; + ColorStop colorStop = new ColorStop + ( + Frac: XDotFFI.get_frac(stopPtr), + Color: XDotFFI.GetColorStop(stopPtr) + ); return colorStop; } - private static XDotPolyline TranslatePolyline(IntPtr polylinePtr) + private static PointD[] TranslatePolyline(IntPtr polylinePtr) { int count = (int)XDotFFI.get_cnt_polyline(polylinePtr); - XDotPolyline polyline = new XDotPolyline - { - Count = count, - Points = new XDotPoint[count] - }; + var points = new PointD[count]; // Translate the array of Points var pointsPtr = XDotFFI.get_pts_polyline(polylinePtr); for (int i = 0; i < count; ++i) { IntPtr pointPtr = XDotFFI.get_pt_at_index(pointsPtr, i); - polyline.Points[i] = TranslatePoint(pointPtr); + points[i] = TranslatePoint(pointPtr); } - return polyline; + return points; } - private static XDotPoint TranslatePoint(IntPtr pointPtr) + private static PointD TranslatePoint(IntPtr pointPtr) { - XDotPoint point = new XDotPoint - { - X = XDotFFI.get_x_point(pointPtr), - Y = XDotFFI.get_y_point(pointPtr), - Z = XDotFFI.get_z_point(pointPtr) - }; + var point = new PointD + ( + X: XDotFFI.get_x_point(pointPtr), + Y: XDotFFI.get_y_point(pointPtr) + ); return point; } - private static XDotRect TranslateRect(IntPtr rectPtr) + private static RectangleD TranslateRect(IntPtr rectPtr) { - XDotRect rect = new XDotRect - { - X = XDotFFI.get_x_rect(rectPtr), - Y = XDotFFI.get_y_rect(rectPtr), - Width = XDotFFI.get_w_rect(rectPtr), - Height = XDotFFI.get_h_rect(rectPtr) - }; + var rect = RectangleD.Create + ( + x: XDotFFI.get_x_rect(rectPtr), + y: XDotFFI.get_y_rect(rectPtr), + width: XDotFFI.get_w_rect(rectPtr), + height: XDotFFI.get_h_rect(rectPtr) + ); return rect; } - private static XDotText TranslateText(IntPtr txtPtr) + private static XDotText TranslateText(IntPtr txtPtr, XDotFont activeFont) { XDotText text = new XDotText - { - X = XDotFFI.get_x_text(txtPtr), - Y = XDotFFI.get_y_text(txtPtr), - Align = XDotFFI.get_align(txtPtr), - Width = XDotFFI.get_width(txtPtr), - Text = XDotFFI.GetTextStr(txtPtr) - }; + ( + new PointD(XDotFFI.get_x_text(txtPtr), XDotFFI.get_y_text(txtPtr)), + XDotFFI.get_align(txtPtr), + XDotFFI.get_width(txtPtr), + XDotFFI.GetTextStr(txtPtr), + activeFont + ); return text; } From df22977aacb865280a923c811afa6f85901406a2 Mon Sep 17 00:00:00 2001 From: chtenb Date: Thu, 10 Aug 2023 11:03:36 +0200 Subject: [PATCH 03/12] Sharpen xdot api --- README.md | 36 +++++------- Rubjerg.Graphviz.Test/OldTutorial.cs | 11 ++-- Rubjerg.Graphviz.Test/Reproductions.cs | 2 +- Rubjerg.Graphviz.Test/TestDotLayout.cs | 8 +-- Rubjerg.Graphviz.Test/TestXDotLayout.cs | 5 +- Rubjerg.Graphviz.Test/Tutorial.cs | 35 +++++------- Rubjerg.Graphviz/CGraphThing.cs | 23 ++++---- Rubjerg.Graphviz/Edge.cs | 3 - Rubjerg.Graphviz/Graph.cs | 32 +++++++---- Rubjerg.Graphviz/Node.cs | 49 +++++++--------- Rubjerg.Graphviz/XDot.cs | 75 ++++++++++++++++--------- Rubjerg.Graphviz/XDotParser.cs | 68 +++++++++++----------- 12 files changed, 173 insertions(+), 174 deletions(-) diff --git a/README.md b/README.md index 102ff01..03864b6 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,6 @@ documents presented at the [Graphviz documentation page](https://graphviz.org/do ```cs using NUnit.Framework; -using System.Drawing; using System.Linq; namespace Rubjerg.Graphviz.Test; @@ -137,49 +136,41 @@ public class Tutorial // There are convenience methods available that parse these attributes for us and give // back the layout information in an accessible form. Node nodeA = layout.GetNode("A"); - PointF position = nodeA.GetPosition(); + PointD position = nodeA.GetPosition(); Utils.AssertPattern(PointPattern, position.ToString()); - RectangleF nodeboundingbox = nodeA.GetBoundingBox(); + RectangleD nodeboundingbox = nodeA.GetBoundingBox(); Utils.AssertPattern(RectPattern, nodeboundingbox.ToString()); // Or splines between nodes Node nodeB = layout.GetNode("B"); Edge edge = layout.GetEdge(nodeA, nodeB, "Some edge name"); - PointF[] spline = edge.GetFirstSpline(); + PointD[] spline = edge.GetFirstSpline(); string splineString = string.Join(", ", spline.Select(p => p.ToString())); Utils.AssertPattern(SplinePattern, splineString); // If we require detailed drawing information for any object, we can retrieve the so called "xdot" // operations. See https://graphviz.org/docs/outputs/canon/#xdot for a specification. - var activeColor = Color.Black; + var activeColor = System.Drawing.Color.Black; foreach (var op in nodeA.GetDrawing()) { - if (op is XDotOp.FillColor { Value: string htmlColor }) + if (op is XDotOp.FillColor { Value: Color.Uniform { HtmlColor: var htmlColor } }) { - activeColor = ColorTranslator.FromHtml(htmlColor); + activeColor = System.Drawing.ColorTranslator.FromHtml(htmlColor); } - else if (op is XDotOp.FilledEllipse { Value: var filledEllipse }) + else if (op is XDotOp.FilledEllipse { Value: var boundingBox }) { - var boundingBox = filledEllipse.ToRectangleF(); Utils.AssertPattern(RectPattern, boundingBox.ToString()); } // Handle any xdot operation you require } - var activeFont = XDotFont.Default; - foreach (var op in nodeA.GetDrawing()) + foreach (var op in nodeA.GetLabelDrawing()) { - if (op is XDotOp.Font { Value: var font }) - { - activeFont = font; - Utils.AssertPattern(@"Times-Roman", font.Name); - } - else if (op is XDotOp.Text { Value: var text }) + if (op is XDotOp.Text { Value: var text }) { - var anchor = text.Anchor(); - Utils.AssertPattern(PointPattern, anchor.ToString()); - var boundingBox = text.TextBoundingBox(activeFont); + Utils.AssertPattern(PointPattern, text.Anchor.ToString()); + var boundingBox = text.TextBoundingBox(); Utils.AssertPattern(RectPattern, boundingBox.ToString()); Assert.AreEqual(text.Text, "A"); } @@ -223,8 +214,8 @@ public class Tutorial var layout = root.CreateLayout(); SubGraph cluster = layout.GetSubgraph("cluster_1"); - RectangleF clusterbox = cluster.GetBoundingBox(); - RectangleF rootgraphbox = layout.GetBoundingBox(); + RectangleD clusterbox = cluster.GetBoundingBox(); + RectangleD rootgraphbox = layout.GetBoundingBox(); Utils.AssertPattern(RectPattern, clusterbox.ToString()); Utils.AssertPattern(RectPattern, rootgraphbox.ToString()); } @@ -235,6 +226,7 @@ public class Tutorial RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Graph with records"); Node nodeA = root.GetOrAddNode("A"); nodeA.SetAttribute("shape", "record"); + // New line characters are not supported by record labels, and will be ignored by Graphviz nodeA.SetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}"); var layout = root.CreateLayout(); diff --git a/Rubjerg.Graphviz.Test/OldTutorial.cs b/Rubjerg.Graphviz.Test/OldTutorial.cs index aa5d18d..288e01c 100644 --- a/Rubjerg.Graphviz.Test/OldTutorial.cs +++ b/Rubjerg.Graphviz.Test/OldTutorial.cs @@ -1,5 +1,4 @@ using NUnit.Framework; -using System.Drawing; using System.Linq; #pragma warning disable CS0618 // Type or member is obsolete @@ -69,17 +68,17 @@ public void Layouting() // Or programatically read out the layout attributes Node nodeA = root.GetNode("A"); - PointF position = nodeA.GetPosition(); + PointD position = nodeA.GetPosition(); Utils.AssertPattern(@"{X=[\d.]+, Y=[\d.]+}", position.ToString()); // Like a bounding box of an object - RectangleF nodeboundingbox = nodeA.GetBoundingBox(); + RectangleD nodeboundingbox = nodeA.GetBoundingBox(); Utils.AssertPattern(@"{X=[\d.]+,Y=[\d.]+,Width=[\d.]+,Height=[\d.]+}", nodeboundingbox.ToString()); // Or splines between nodes Node nodeB = root.GetNode("B"); Edge edge = root.GetEdge(nodeA, nodeB, "Some edge name"); - PointF[] spline = edge.GetFirstSpline(); + PointD[] spline = edge.GetFirstSpline(); string splineString = string.Join(", ", spline.Select(p => p.ToString())); string expectedSplinePattern = @"{X=[\d.]+, Y=[\d.]+}, {X=[\d.]+, Y=[\d.]+}," @@ -132,8 +131,8 @@ public void Clusters() root.ComputeLayout(); SubGraph cluster = root.GetSubgraph("cluster_1"); - RectangleF clusterbox = cluster.GetBoundingBox(); - RectangleF rootgraphbox = root.GetBoundingBox(); + RectangleD clusterbox = cluster.GetBoundingBox(); + RectangleD rootgraphbox = root.GetBoundingBox(); Utils.AssertPattern(@"{X=[\d.]+,Y=[\d.]+,Width=[\d.]+,Height=[\d.]+}", clusterbox.ToString()); Utils.AssertPattern(@"{X=[\d.]+,Y=[\d.]+,Width=[\d.]+,Height=[\d.]+}", rootgraphbox.ToString()); } diff --git a/Rubjerg.Graphviz.Test/Reproductions.cs b/Rubjerg.Graphviz.Test/Reproductions.cs index eba0188..cc5bb14 100644 --- a/Rubjerg.Graphviz.Test/Reproductions.cs +++ b/Rubjerg.Graphviz.Test/Reproductions.cs @@ -41,7 +41,7 @@ public void TestRecordShapeAlignment(string fontname, double fontsize, double ma //TestContext.Write(root.ToDotString()); var rects = nodeA.GetRecordRectangles().ToList(); - Assert.That(rects[0].Right, Is.EqualTo(rects[2].Right)); + Assert.That(rects[0].FarPoint().X, Is.EqualTo(rects[2].FarPoint().X)); } // This test only failed when running in isolation diff --git a/Rubjerg.Graphviz.Test/TestDotLayout.cs b/Rubjerg.Graphviz.Test/TestDotLayout.cs index a7d5ee8..b6cba67 100644 --- a/Rubjerg.Graphviz.Test/TestDotLayout.cs +++ b/Rubjerg.Graphviz.Test/TestDotLayout.cs @@ -35,7 +35,6 @@ public void TestLayoutMethodsWithoutLayout() CreateSimpleTestGraph(out RootGraph root, out Node nodeA, out Edge edge); Assert.AreEqual(root.GetBoundingBox(), default(RectangleF)); - Assert.AreEqual(root.GetColor(), Color.Black); Assert.AreEqual(root.GetDrawing().Count, 0); Assert.AreEqual(root.GetLabelDrawing().Count, 0); @@ -65,12 +64,10 @@ public void TestLayoutMethodsWithInProcessLayout() root.ComputeLayout(); - Assert.AreEqual(root.GetColor(), Color.Black); Assert.AreNotEqual(root.GetBoundingBox(), default(RectangleF)); Assert.AreNotEqual(root.GetDrawing().Count, 0); Assert.AreNotEqual(root.GetLabelDrawing().Count, 0); - Assert.AreEqual(nodeA.GetColor(), Color.Red); Assert.AreEqual(nodeA.GetRecordRectangles().Count(), 2); Assert.AreNotEqual(nodeA.GetPosition(), default(PointF)); Assert.AreNotEqual(nodeA.GetBoundingBox(), default(RectangleF)); @@ -98,12 +95,10 @@ public void TestLayoutMethodsWithLayout() var xnodeB = xroot.GetNode("B"); Edge xedge = xroot.GetEdge(xnodeA, xnodeB, ""); - Assert.AreEqual(xroot.GetColor(), Color.Black); Assert.AreNotEqual(xroot.GetBoundingBox(), default(RectangleF)); Assert.AreNotEqual(xroot.GetDrawing().Count, 0); Assert.AreNotEqual(xroot.GetLabelDrawing().Count, 0); - Assert.AreEqual(xnodeA.GetColor(), Color.Red); Assert.AreEqual(xnodeA.GetRecordRectangles().Count(), 2); Assert.AreNotEqual(xnodeA.GetPosition(), default(PointF)); Assert.AreNotEqual(xnodeA.GetBoundingBox(), default(RectangleF)); @@ -186,8 +181,7 @@ public void TestRecordShapeOrder() var rects = nodeA.GetRecordRectangles().ToList(); - // Because Graphviz uses a lower-left originated coordinate system, we need to flip the y coordinates - Utils.AssertOrder(rects, r => (r.Left, -r.Top)); + Utils.AssertOrder(rects, r => (r.Point.X, r.Point.Y)); Assert.That(rects.Count, Is.EqualTo(9)); } diff --git a/Rubjerg.Graphviz.Test/TestXDotLayout.cs b/Rubjerg.Graphviz.Test/TestXDotLayout.cs index 27cc580..b72335e 100644 --- a/Rubjerg.Graphviz.Test/TestXDotLayout.cs +++ b/Rubjerg.Graphviz.Test/TestXDotLayout.cs @@ -27,7 +27,7 @@ F 12 5 -Arial S 6 -dashed I 90 10 5 5 8 -image.png "; - var result = XDotParser.ParseXDot(testcase); + var result = XDotParser.ParseXDot(testcase, CoordinateSystem.BottomLeft, 0); Assert.AreEqual(14, result.Count); } @@ -87,8 +87,7 @@ public void TestRecordShapeOrder() var xNodeA = xdotGraph.GetNode("A"); var rects = xNodeA.GetRecordRectangles().ToList(); - // Because Graphviz uses a lower-left originated coordinate system, we need to flip the y coordinates - Utils.AssertOrder(rects, r => (r.Left, -r.Top)); + Utils.AssertOrder(rects, r => (r.Point.X, r.Point.Y)); Assert.That(rects.Count, Is.EqualTo(9)); // Test xdot translation diff --git a/Rubjerg.Graphviz.Test/Tutorial.cs b/Rubjerg.Graphviz.Test/Tutorial.cs index 346cc90..f0608c6 100644 --- a/Rubjerg.Graphviz.Test/Tutorial.cs +++ b/Rubjerg.Graphviz.Test/Tutorial.cs @@ -1,5 +1,4 @@ using NUnit.Framework; -using System.Drawing; using System.Linq; namespace Rubjerg.Graphviz.Test; @@ -86,49 +85,41 @@ public void Layouting() // There are convenience methods available that parse these attributes for us and give // back the layout information in an accessible form. Node nodeA = layout.GetNode("A"); - PointF position = nodeA.GetPosition(); + PointD position = nodeA.GetPosition(); Utils.AssertPattern(PointPattern, position.ToString()); - RectangleF nodeboundingbox = nodeA.GetBoundingBox(); + RectangleD nodeboundingbox = nodeA.GetBoundingBox(); Utils.AssertPattern(RectPattern, nodeboundingbox.ToString()); // Or splines between nodes Node nodeB = layout.GetNode("B"); Edge edge = layout.GetEdge(nodeA, nodeB, "Some edge name"); - PointF[] spline = edge.GetFirstSpline(); + PointD[] spline = edge.GetFirstSpline(); string splineString = string.Join(", ", spline.Select(p => p.ToString())); Utils.AssertPattern(SplinePattern, splineString); // If we require detailed drawing information for any object, we can retrieve the so called "xdot" // operations. See https://graphviz.org/docs/outputs/canon/#xdot for a specification. - var activeColor = Color.Black; + var activeColor = System.Drawing.Color.Black; foreach (var op in nodeA.GetDrawing()) { - if (op is XDotOp.FillColor { Value: string htmlColor }) + if (op is XDotOp.FillColor { Value: Color.Uniform { HtmlColor: var htmlColor } }) { - activeColor = ColorTranslator.FromHtml(htmlColor); + activeColor = System.Drawing.ColorTranslator.FromHtml(htmlColor); } - else if (op is XDotOp.FilledEllipse { Value: var filledEllipse }) + else if (op is XDotOp.FilledEllipse { Value: var boundingBox }) { - var boundingBox = filledEllipse.ToRectangleF(); Utils.AssertPattern(RectPattern, boundingBox.ToString()); } // Handle any xdot operation you require } - var activeFont = XDotFont.Default; - foreach (var op in nodeA.GetDrawing()) + foreach (var op in nodeA.GetLabelDrawing()) { - if (op is XDotOp.Font { Value: var font }) - { - activeFont = font; - Utils.AssertPattern(@"Times-Roman", font.Name); - } - else if (op is XDotOp.Text { Value: var text }) + if (op is XDotOp.Text { Value: var text }) { - var anchor = text.Anchor(); - Utils.AssertPattern(PointPattern, anchor.ToString()); - var boundingBox = text.TextBoundingBox(activeFont); + Utils.AssertPattern(PointPattern, text.Anchor.ToString()); + var boundingBox = text.TextBoundingBox(); Utils.AssertPattern(RectPattern, boundingBox.ToString()); Assert.AreEqual(text.Text, "A"); } @@ -172,8 +163,8 @@ public void Clusters() var layout = root.CreateLayout(); SubGraph cluster = layout.GetSubgraph("cluster_1"); - RectangleF clusterbox = cluster.GetBoundingBox(); - RectangleF rootgraphbox = layout.GetBoundingBox(); + RectangleD clusterbox = cluster.GetBoundingBox(); + RectangleD rootgraphbox = layout.GetBoundingBox(); Utils.AssertPattern(RectPattern, clusterbox.ToString()); Utils.AssertPattern(RectPattern, rootgraphbox.ToString()); } diff --git a/Rubjerg.Graphviz/CGraphThing.cs b/Rubjerg.Graphviz/CGraphThing.cs index 85a7200..25f7d3c 100644 --- a/Rubjerg.Graphviz/CGraphThing.cs +++ b/Rubjerg.Graphviz/CGraphThing.cs @@ -2,8 +2,8 @@ using System.Linq; using System.Collections.Generic; using System.Diagnostics; -using System.Drawing; using static Rubjerg.Graphviz.ForeignFunctionInterface; +using System.Globalization; namespace Rubjerg.Graphviz; @@ -190,12 +190,6 @@ public static string EscapeLabel(string label) #region layout functions - public Color GetColor() - { - string colorstring = SafeGetAttribute("color", "Black"); - return Color.FromName(colorstring); - } - public bool HasPosition() { return HasAttribute("pos"); @@ -211,14 +205,23 @@ public bool IsInvisible() return GetAttribute("style") == "invis"; } - protected static List GetXDotValue(CGraphThing obj, string attrName) + protected List GetXDotValue(CGraphThing obj, string attrName) { - // FIXNOW var xdotString = obj.SafeGetAttribute(attrName, null); if (xdotString is null) return new List(); - return XDotParser.ParseXDot(xdotString); + return XDotParser.ParseXDot(xdotString, MyRootGraph.CoordinateSystem, MyRootGraph.RawMaxY()); + } + + protected static RectangleD ParseRect(string rect) + { + string[] points = rect.Split(','); + var x = double.Parse(points[0], NumberStyles.Any, CultureInfo.InvariantCulture); + var y = double.Parse(points[1], NumberStyles.Any, CultureInfo.InvariantCulture); + var w = double.Parse(points[2], NumberStyles.Any, CultureInfo.InvariantCulture) - x; + var h = double.Parse(points[3], NumberStyles.Any, CultureInfo.InvariantCulture) - y; + return RectangleD.Create(x, y, w, h); } #endregion diff --git a/Rubjerg.Graphviz/Edge.cs b/Rubjerg.Graphviz/Edge.cs index fc38846..0929859 100644 --- a/Rubjerg.Graphviz/Edge.cs +++ b/Rubjerg.Graphviz/Edge.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; -using System.Drawing; using System.Linq; using static Rubjerg.Graphviz.ForeignFunctionInterface; @@ -70,7 +68,6 @@ public Node OppositeEndpoint(Node node) { var tail = Tail(); var head = Head(); - Debug.Assert(node == tail || node == head); return node == tail ? head : tail; } diff --git a/Rubjerg.Graphviz/Graph.cs b/Rubjerg.Graphviz/Graph.cs index b71f70f..bb9d951 100644 --- a/Rubjerg.Graphviz/Graph.cs +++ b/Rubjerg.Graphviz/Graph.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Drawing; -using System.Globalization; using System.IO; using System.Linq; using static Rubjerg.Graphviz.ForeignFunctionInterface; @@ -572,20 +570,30 @@ public RootGraph CreateLayout(string engine = LayoutEngines.Dot) return GraphvizCommand.CreateLayout(this, engine: engine); } - public RectangleF GetBoundingBox() + /// + /// Untransformed boundingbox. Still needs to be transformed to the desired coordinate system. + /// + internal RectangleD RawBoundingBox() { - // FIXNOW string bb_string = Agget(_ptr, "bb"); if (string.IsNullOrEmpty(bb_string)) return default; - // x and y are the topleft point of the bb - char sep = ','; - string[] bb = bb_string.Split(sep); - float x = float.Parse(bb[0], NumberStyles.Any, CultureInfo.InvariantCulture); - float y = float.Parse(bb[1], NumberStyles.Any, CultureInfo.InvariantCulture); - float w = float.Parse(bb[2], NumberStyles.Any, CultureInfo.InvariantCulture) - x; - float h = float.Parse(bb[3], NumberStyles.Any, CultureInfo.InvariantCulture) - y; - return new RectangleF(x, y, w, h); + return ParseRect(bb_string); + } + + internal double RawMaxY() + { + // FIXNOW: can we cache this somehow? It's called quite often + return RawBoundingBox().FarPoint().Y; + } + + /// + /// The bounding box of this (sub)graph. + /// + public RectangleD GetBoundingBox() + { + var untransformed = RawBoundingBox(); + return untransformed.ForCoordSystem(MyRootGraph.CoordinateSystem, RawMaxY()); } public IReadOnlyList GetDrawing() => GetXDotValue(this, "_draw_"); diff --git a/Rubjerg.Graphviz/Node.cs b/Rubjerg.Graphviz/Node.cs index 452ac83..a3956b6 100644 --- a/Rubjerg.Graphviz/Node.cs +++ b/Rubjerg.Graphviz/Node.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Drawing; using System.Globalization; using System.Linq; using static Rubjerg.Graphviz.ForeignFunctionInterface; @@ -172,35 +171,38 @@ public void MakeInvisibleAndSmall() /// /// The position of the center of the node. /// - public PointF GetPosition() + public PointD GetPosition() { - // FIXNOW // The "pos" attribute is available as part of xdot output + PointD result; if (HasAttribute("pos")) { var posString = GetAttribute("pos"); var coords = posString.Split(','); - float x = float.Parse(coords[0], NumberStyles.Any, CultureInfo.InvariantCulture); - float y = float.Parse(coords[1], NumberStyles.Any, CultureInfo.InvariantCulture); - return new PointF(x, y); + double x = double.Parse(coords[0], NumberStyles.Any, CultureInfo.InvariantCulture); + double y = double.Parse(coords[1], NumberStyles.Any, CultureInfo.InvariantCulture); + result = new PointD(x, y); } - // If the "pos" attribute is not available, try the following FFI functions, - // which are available after a ComputeLayout - return new PointF(Convert.ToSingle(NodeX(_ptr)), Convert.ToSingle(NodeY(_ptr))); + else + { + // If the "pos" attribute is not available, try the following FFI functions, + // which are available after a ComputeLayout + result = new PointD(Convert.ToSingle(NodeX(_ptr)), Convert.ToSingle(NodeY(_ptr))); + } + return result.ForCoordSystem(MyRootGraph.CoordinateSystem, MyRootGraph.RawMaxY()); } /// /// The size of bounding box of the node. /// - public SizeF GetSize() + public SizeD GetSize() { - // FIXNOW // The "width" and "height" attributes are available as part of xdot output - float w, h; + double w, h; if (HasAttribute("width") && HasAttribute("height")) { - w = float.Parse(GetAttribute("width"), NumberStyles.Any, CultureInfo.InvariantCulture); - h = float.Parse(GetAttribute("height"), NumberStyles.Any, CultureInfo.InvariantCulture); + w = double.Parse(GetAttribute("width"), NumberStyles.Any, CultureInfo.InvariantCulture); + h = double.Parse(GetAttribute("height"), NumberStyles.Any, CultureInfo.InvariantCulture); } else { @@ -211,15 +213,15 @@ public SizeF GetSize() } // Coords are in points, sizes in inches. 72 points = 1 inch // We return everything in terms of points. - return new SizeF(w * 72, h * 72); + return new SizeD(w * 72, h * 72); } - public RectangleF GetBoundingBox() + public RectangleD GetBoundingBox() { var size = GetSize(); var center = GetPosition(); - var bottomleft = new PointF(center.X - size.Width / 2, center.Y - size.Height / 2); - return new RectangleF(bottomleft, size); + var bottomleft = new PointD(center.X - size.Width / 2, center.Y - size.Height / 2); + return new RectangleD(bottomleft, size); } /// @@ -228,7 +230,6 @@ public RectangleF GetBoundingBox() /// public IEnumerable GetRecordRectangles() { - // FIXNOW remove almost all floats from the code base if (!HasAttribute("rects")) yield break; @@ -264,16 +265,6 @@ private static double FindClosest(IEnumerable self, double target) return target; } - private RectangleF ParseRect(string rect) - { - string[] points = rect.Split(','); - float leftX = float.Parse(points[0], NumberStyles.Any, CultureInfo.InvariantCulture); - float upperY = float.Parse(points[1], NumberStyles.Any, CultureInfo.InvariantCulture); - float rightX = float.Parse(points[2], NumberStyles.Any, CultureInfo.InvariantCulture); - float lowerY = float.Parse(points[3], NumberStyles.Any, CultureInfo.InvariantCulture); - return new RectangleF(leftX, upperY, rightX - leftX, lowerY - upperY); - } - public IReadOnlyList GetDrawing() => GetXDotValue(this, "_draw_"); public IReadOnlyList GetLabelDrawing() => GetXDotValue(this, "_ldraw_"); diff --git a/Rubjerg.Graphviz/XDot.cs b/Rubjerg.Graphviz/XDot.cs index c81870c..6520681 100644 --- a/Rubjerg.Graphviz/XDot.cs +++ b/Rubjerg.Graphviz/XDot.cs @@ -1,5 +1,5 @@ using System; -using System.Drawing; +using System.Linq; namespace Rubjerg.Graphviz; @@ -31,6 +31,14 @@ internal PointD ForCoordSystem(CoordinateSystem coordSystem, double maxY) /// public record struct RectangleD(PointD Point, SizeD Size) { + public double X => Point.X; + public double Y => Point.Y; + public double Width => Size.Width; + public double Height => Size.Height; + + /// The point farthest from the origin + public PointD FarPoint() => new PointD(Point.X + Size.Width, Point.Y + Size.Height); + public static RectangleD Create(double x, double y, double width, double height) { return new RectangleD(new PointD(x, y), new SizeD(width, height)); @@ -45,7 +53,7 @@ internal RectangleD ForCoordSystem(CoordinateSystem coordSystem, double maxY) } } -public record struct ColorStop(float Frac, string Color); +public record struct ColorStop(float Frac, string HtmlColor); public record struct LinearGradient(PointD Point0, PointD Point1, ColorStop[] Stops) { @@ -71,21 +79,12 @@ internal RadialGradient ForCoordSystem(CoordinateSystem coordSystem, double maxY } } -public abstract record class GradientColor +public abstract record class Color { - private GradientColor() { } - public sealed record class Uniform : GradientColor - { - public string Color { get; init; } - } - public sealed record class Linear : GradientColor - { - public LinearGradient Gradient { get; init; } - } - public sealed record class Radial : GradientColor - { - public RadialGradient Gradient { get; init; } - } + private Color() { } + public sealed record class Uniform(string HtmlColor) : Color { } + public sealed record class Linear(LinearGradient Gradient) : Color { } + public sealed record class Radial(RadialGradient Gradient) : Color { } } public enum XDotAlign @@ -108,7 +107,7 @@ public enum XDotAlign /// The estimated width of the text. /// /// -public record struct XDotText(PointD Anchor, XDotAlign Align, double Width, string Text, XDotFont Font) +public record struct XDotText(PointD Anchor, XDotAlign Align, double Width, string Text, XDotFont Font, XDotFontChar FontChar) { /// /// Compute the bounding box of this text element given the necessary font information. @@ -117,6 +116,7 @@ public record struct XDotText(PointD Anchor, XDotAlign Align, double Width, stri /// Optional property of the font, to more accurately predict the bounding box. public RectangleD TextBoundingBox(double? distanceBetweenBaselineAndDescender = null) { + // FIXNOW: better estimate distanceBetweenBaselineAndDescender ? var size = TextSize(); var descenderY = Anchor.Y - (distanceBetweenBaselineAndDescender ?? Font.Size / 5); var leftX = Align switch @@ -150,7 +150,16 @@ internal XDotText ForCoordSystem(CoordinateSystem coordSystem, double maxY) } } -public record struct XDotImage(RectangleD Position, string Name) { } +public record struct XDotImage(RectangleD Position, string Name) +{ + internal XDotImage ForCoordSystem(CoordinateSystem coordSystem, double maxY) + { + return this with + { + Position = Position.ForCoordSystem(coordSystem, maxY), + }; + } +} /// Font size in points /// Font name @@ -172,12 +181,20 @@ public enum XDotFontChar Overline = 64, } +internal static class PointDArrayExtension +{ + internal static PointD[] ForCoordSystem(this PointD[] self, CoordinateSystem coordSystem, double maxY) + { + return self.Select(a => a.ForCoordSystem(coordSystem, maxY)).ToArray(); + } +} + /// /// See https://graphviz.org/docs/outputs/canon/#xdot for semantics. /// /// Within the context of a single drawing attribute, e.g., draw, there is an implicit state for the -/// graphical attributes. That is, once a color, style, or font characteristic is set, it -/// remains valid for all relevant drawing operations until the value is reset by another xdot cmd. +/// graphical attributes. That is, once a color or style is set, it remains valid for all relevant +/// drawing operations until the value is reset by another xdot cmd. /// /// Note that the filled figures (ellipses, polygons and B-Splines) imply two operations: first, /// drawing the filled figure with the current fill color; second, drawing an unfilled figure with @@ -187,12 +204,15 @@ public enum XDotFontChar /// only used in the non-label attributes. If, however, the decorate attribute is set on an edge, /// its label attribute will also contain a polyline operation. In addition, if a label is a /// complex, HTML-like label, it will also contain non-text operations. +/// +/// NOTE: we've slightly trimmed down the number of cases w.r.t. the actual xdot operations. +/// All font related operations have been condensed into the text operations. +/// We only have a single Color type, which has three subtypes. /// public abstract record class XDotOp { private XDotOp() { } - // FIXNOW: can we trim down the cases? Some things are only relevant for a single thing, such as FontChar public sealed record class FilledEllipse(RectangleD Value) : XDotOp { } public sealed record class UnfilledEllipse(RectangleD Value) : XDotOp { } public sealed record class FilledPolygon(PointD[] Points) : XDotOp { } @@ -202,10 +222,13 @@ public sealed record class FilledBezier(PointD[] Points) : XDotOp { } public sealed record class UnfilledBezier(PointD[] Points) : XDotOp { } public sealed record class Text(XDotText Value) : XDotOp { } public sealed record class Image(XDotImage Value) : XDotOp { } - public sealed record class FillColor(string Value) : XDotOp { } - public sealed record class PenColor(string Value) : XDotOp { } - public sealed record class GradientFillColor(GradientColor Value) : XDotOp { } - public sealed record class GradientPenColor(GradientColor Value) : XDotOp { } + public sealed record class FillColor(Color Value) : XDotOp { } + public sealed record class PenColor(Color Value) : XDotOp { } + /// + /// Style values which can be incorporated in the graphics model do not appear in xdot + /// output. In particular, the style values filled, rounded, diagonals, and invis will not + /// appear. Indeed, if style contains invis, there will not be any xdot output at all. + /// For reference see https://graphviz.org/docs/attr-types/style/ + /// public sealed record class Style(string Value) : XDotOp { } - public sealed record class FontChar(XDotFontChar Value) : XDotOp { } } diff --git a/Rubjerg.Graphviz/XDotParser.cs b/Rubjerg.Graphviz/XDotParser.cs index 73e11ac..e09a900 100644 --- a/Rubjerg.Graphviz/XDotParser.cs +++ b/Rubjerg.Graphviz/XDotParser.cs @@ -34,12 +34,12 @@ internal struct XDot /// internal static class XDotParser { - public static List ParseXDot(string xdotString) + public static List ParseXDot(string xdotString, CoordinateSystem coordinateSystem, double maxY) { IntPtr xdot = XDotFFI.parseXDot(xdotString); try { - return TranslateXDot(xdot); + return TranslateXDot(xdot, coordinateSystem, maxY); } finally { @@ -50,7 +50,7 @@ public static List ParseXDot(string xdotString) } } - internal static List TranslateXDot(IntPtr xdotPtr) + internal static List TranslateXDot(IntPtr xdotPtr, CoordinateSystem coordinateSystem, double maxY) { if (xdotPtr == IntPtr.Zero) throw new ArgumentNullException(nameof(xdotPtr)); @@ -66,6 +66,7 @@ internal static List TranslateXDot(IntPtr xdotPtr) var opsPtr = XDotFFI.get_ops(xdotPtr); var activeFont = XDotFont.Default; + var activeFontChar = XDotFontChar.None; for (int i = 0; i < count; ++i) { IntPtr xdotOpPtr = XDotFFI.get_op_at_index(opsPtr, i); @@ -73,40 +74,48 @@ internal static List TranslateXDot(IntPtr xdotPtr) switch (kind) { case XDotKind.FilledEllipse: - xdot.Ops[i] = new XDotOp.FilledEllipse(TranslateEllipse(XDotFFI.get_ellipse(xdotOpPtr))); + xdot.Ops[i] = new XDotOp.FilledEllipse(TranslateEllipse(XDotFFI.get_ellipse(xdotOpPtr)) + .ForCoordSystem(coordinateSystem, maxY)); break; case XDotKind.UnfilledEllipse: - xdot.Ops[i] = new XDotOp.UnfilledEllipse(TranslateEllipse(XDotFFI.get_ellipse(xdotOpPtr))); + xdot.Ops[i] = new XDotOp.UnfilledEllipse(TranslateEllipse(XDotFFI.get_ellipse(xdotOpPtr)) + .ForCoordSystem(coordinateSystem, maxY)); break; case XDotKind.FilledPolygon: - xdot.Ops[i] = new XDotOp.FilledPolygon(TranslatePolyline(XDotFFI.get_polyline(xdotOpPtr))); + xdot.Ops[i] = new XDotOp.FilledPolygon(TranslatePolyline(XDotFFI.get_polyline(xdotOpPtr)) + .ForCoordSystem(coordinateSystem, maxY)); break; case XDotKind.UnfilledPolygon: - xdot.Ops[i] = new XDotOp.FilledPolygon(TranslatePolyline(XDotFFI.get_polyline(xdotOpPtr))); + xdot.Ops[i] = new XDotOp.FilledPolygon(TranslatePolyline(XDotFFI.get_polyline(xdotOpPtr)) + .ForCoordSystem(coordinateSystem, maxY)); break; case XDotKind.FilledBezier: - xdot.Ops[i] = new XDotOp.FilledBezier(TranslatePolyline(XDotFFI.get_polyline(xdotOpPtr))); + xdot.Ops[i] = new XDotOp.FilledBezier(TranslatePolyline(XDotFFI.get_polyline(xdotOpPtr)) + .ForCoordSystem(coordinateSystem, maxY)); break; case XDotKind.UnfilledBezier: - xdot.Ops[i] = new XDotOp.UnfilledBezier(TranslatePolyline(XDotFFI.get_polyline(xdotOpPtr))); + xdot.Ops[i] = new XDotOp.UnfilledBezier(TranslatePolyline(XDotFFI.get_polyline(xdotOpPtr)) + .ForCoordSystem(coordinateSystem, maxY)); break; case XDotKind.Polyline: - xdot.Ops[i] = new XDotOp.PolyLine(TranslatePolyline(XDotFFI.get_polyline(xdotOpPtr))); + xdot.Ops[i] = new XDotOp.PolyLine(TranslatePolyline(XDotFFI.get_polyline(xdotOpPtr)) + .ForCoordSystem(coordinateSystem, maxY)); break; case XDotKind.Text: - xdot.Ops[i] = new XDotOp.Text(TranslateText(XDotFFI.get_text(xdotOpPtr), activeFont)); + xdot.Ops[i] = new XDotOp.Text(TranslateText(XDotFFI.get_text(xdotOpPtr), activeFont, activeFontChar) + .ForCoordSystem(coordinateSystem, maxY)); break; case XDotKind.FillColor: - xdot.Ops[i] = new XDotOp.FillColor(XDotFFI.GetColor(xdotOpPtr)); + xdot.Ops[i] = new XDotOp.FillColor(new Color.Uniform(XDotFFI.GetColor(xdotOpPtr))); break; case XDotKind.PenColor: - xdot.Ops[i] = new XDotOp.PenColor(XDotFFI.GetColor(xdotOpPtr)); + xdot.Ops[i] = new XDotOp.PenColor(new Color.Uniform(XDotFFI.GetColor(xdotOpPtr))); break; case XDotKind.GradFillColor: - xdot.Ops[i] = new XDotOp.GradientFillColor(TranslateGradColor(XDotFFI.get_grad_color(xdotOpPtr))); + xdot.Ops[i] = new XDotOp.FillColor(TranslateGradColor(XDotFFI.get_grad_color(xdotOpPtr))); break; case XDotKind.GradPenColor: - xdot.Ops[i] = new XDotOp.GradientPenColor(TranslateGradColor(XDotFFI.get_grad_color(xdotOpPtr))); + xdot.Ops[i] = new XDotOp.PenColor(TranslateGradColor(XDotFFI.get_grad_color(xdotOpPtr))); break; case XDotKind.Font: activeFont = TranslateFont(XDotFFI.get_font(xdotOpPtr)); @@ -115,10 +124,11 @@ internal static List TranslateXDot(IntPtr xdotPtr) xdot.Ops[i] = new XDotOp.Style(XDotFFI.GetStyle(xdotOpPtr)); break; case XDotKind.Image: - xdot.Ops[i] = new XDotOp.Image(TranslateImage(XDotFFI.get_image(xdotOpPtr))); + xdot.Ops[i] = new XDotOp.Image(TranslateImage(XDotFFI.get_image(xdotOpPtr)) + .ForCoordSystem(coordinateSystem, maxY)); break; case XDotKind.FontChar: - xdot.Ops[i] = new XDotOp.FontChar(TranslateFontChar(XDotFFI.get_fontchar(xdotOpPtr))); + activeFontChar = TranslateFontChar(XDotFFI.get_fontchar(xdotOpPtr)); break; default: throw new ArgumentException($"Unexpected XDotOp.Kind: {kind}"); @@ -168,26 +178,17 @@ private static RectangleD TranslateEllipse(IntPtr ellipsePtr) return ellipse; } - private static GradientColor TranslateGradColor(IntPtr colorPtr) + private static Color TranslateGradColor(IntPtr colorPtr) { var type = XDotFFI.get_type(colorPtr); switch (type) { case XDotGradType.None: - return new GradientColor.Uniform() - { - Color = XDotFFI.GetClr(colorPtr) - }; + return new Color.Uniform(XDotFFI.GetClr(colorPtr)); case XDotGradType.Linear: - return new GradientColor.Linear() - { - Gradient = TranslateLinearGrad(XDotFFI.get_ling(colorPtr)) - }; + return new Color.Linear(TranslateLinearGrad(XDotFFI.get_ling(colorPtr))); case XDotGradType.Radial: - return new GradientColor.Radial() - { - Gradient = TranslateRadialGrad(XDotFFI.get_ring(colorPtr)) - }; + return new Color.Radial(TranslateRadialGrad(XDotFFI.get_ring(colorPtr))); default: throw new ArgumentException($"Unexpected XDotColor.Type: {type}"); } @@ -242,7 +243,7 @@ private static ColorStop TranslateColorStop(IntPtr stopPtr) ColorStop colorStop = new ColorStop ( Frac: XDotFFI.get_frac(stopPtr), - Color: XDotFFI.GetColorStop(stopPtr) + HtmlColor: XDotFFI.GetColorStop(stopPtr) ); return colorStop; @@ -288,7 +289,7 @@ private static RectangleD TranslateRect(IntPtr rectPtr) return rect; } - private static XDotText TranslateText(IntPtr txtPtr, XDotFont activeFont) + private static XDotText TranslateText(IntPtr txtPtr, XDotFont activeFont, XDotFontChar activeFontChar) { XDotText text = new XDotText ( @@ -296,7 +297,8 @@ private static XDotText TranslateText(IntPtr txtPtr, XDotFont activeFont) XDotFFI.get_align(txtPtr), XDotFFI.get_width(txtPtr), XDotFFI.GetTextStr(txtPtr), - activeFont + activeFont, + activeFontChar ); return text; From 5a2ba47100c076044c21d8c9526bfb176e685eca Mon Sep 17 00:00:00 2001 From: chtenb Date: Thu, 10 Aug 2023 12:57:44 +0200 Subject: [PATCH 04/12] Improve API --- README.md | 3 +++ Rubjerg.Graphviz.Test/Tutorial.cs | 3 +++ Rubjerg.Graphviz/CGraphThing.cs | 3 +++ Rubjerg.Graphviz/Edge.cs | 2 -- Rubjerg.Graphviz/Graph.cs | 3 --- Rubjerg.Graphviz/Node.cs | 14 +++++++++++--- Rubjerg.Graphviz/XDot.cs | 31 +++++++++++++++++-------------- Rubjerg.Graphviz/XDotFFI.cs | 2 +- Rubjerg.Graphviz/XDotParser.cs | 20 ++++++++++---------- 9 files changed, 48 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 03864b6..df3f87a 100644 --- a/README.md +++ b/README.md @@ -173,6 +173,7 @@ public class Tutorial var boundingBox = text.TextBoundingBox(); Utils.AssertPattern(RectPattern, boundingBox.ToString()); Assert.AreEqual(text.Text, "A"); + Assert.AreEqual(text.Font.Name, "Times-Roman"); } // Handle any xdot operation you require } @@ -233,7 +234,9 @@ public class Tutorial // The order of the list matches the order in which the labels occur in the label string above. var rects = layout.GetNode("A").GetRecordRectangles().ToList(); + var rectLabels = layout.GetNode("A").GetRecordRectangleLabels().Select(l => l.Text).ToList(); Assert.AreEqual(9, rects.Count); + Assert.AreEqual(new[] { "1", "2", "3", "4", "5", "6", "7", "8", "9" }, rectLabels); } [Test, Order(5)] diff --git a/Rubjerg.Graphviz.Test/Tutorial.cs b/Rubjerg.Graphviz.Test/Tutorial.cs index f0608c6..1783298 100644 --- a/Rubjerg.Graphviz.Test/Tutorial.cs +++ b/Rubjerg.Graphviz.Test/Tutorial.cs @@ -122,6 +122,7 @@ public void Layouting() var boundingBox = text.TextBoundingBox(); Utils.AssertPattern(RectPattern, boundingBox.ToString()); Assert.AreEqual(text.Text, "A"); + Assert.AreEqual(text.Font.Name, "Times-Roman"); } // Handle any xdot operation you require } @@ -182,7 +183,9 @@ public void Records() // The order of the list matches the order in which the labels occur in the label string above. var rects = layout.GetNode("A").GetRecordRectangles().ToList(); + var rectLabels = layout.GetNode("A").GetRecordRectangleLabels().Select(l => l.Text).ToList(); Assert.AreEqual(9, rects.Count); + Assert.AreEqual(new[] { "1", "2", "3", "4", "5", "6", "7", "8", "9" }, rectLabels); } [Test, Order(5)] diff --git a/Rubjerg.Graphviz/CGraphThing.cs b/Rubjerg.Graphviz/CGraphThing.cs index 25f7d3c..18446b4 100644 --- a/Rubjerg.Graphviz/CGraphThing.cs +++ b/Rubjerg.Graphviz/CGraphThing.cs @@ -205,6 +205,9 @@ public bool IsInvisible() return GetAttribute("style") == "invis"; } + public IReadOnlyList GetDrawing() => GetXDotValue(this, "_draw_"); + public IReadOnlyList GetLabelDrawing() => GetXDotValue(this, "_ldraw_"); + protected List GetXDotValue(CGraphThing obj, string attrName) { var xdotString = obj.SafeGetAttribute(attrName, null); diff --git a/Rubjerg.Graphviz/Edge.cs b/Rubjerg.Graphviz/Edge.cs index 0929859..8d2e858 100644 --- a/Rubjerg.Graphviz/Edge.cs +++ b/Rubjerg.Graphviz/Edge.cs @@ -165,8 +165,6 @@ public IEnumerable GetSplines() return GetDrawing().OfType().Select(x => x.Points); } - public IReadOnlyList GetDrawing() => GetXDotValue(this, "_draw_"); - public IReadOnlyList GetLabelDrawing() => GetXDotValue(this, "_ldraw_"); public IReadOnlyList GetHeadArrowDrawing() => GetXDotValue(this, "_hdraw_"); public IReadOnlyList GetTailArrowDrawing() => GetXDotValue(this, "_tdraw_"); public IReadOnlyList GetHeadLabelDrawing() => GetXDotValue(this, "_hldraw_"); diff --git a/Rubjerg.Graphviz/Graph.cs b/Rubjerg.Graphviz/Graph.cs index bb9d951..1285802 100644 --- a/Rubjerg.Graphviz/Graph.cs +++ b/Rubjerg.Graphviz/Graph.cs @@ -596,9 +596,6 @@ public RectangleD GetBoundingBox() return untransformed.ForCoordSystem(MyRootGraph.CoordinateSystem, RawMaxY()); } - public IReadOnlyList GetDrawing() => GetXDotValue(this, "_draw_"); - public IReadOnlyList GetLabelDrawing() => GetXDotValue(this, "_ldraw_"); - private void ToFile(string filepath, string format, string engine) { _ = GraphvizCommand.Exec(this, format: format, filepath, engine: engine); diff --git a/Rubjerg.Graphviz/Node.cs b/Rubjerg.Graphviz/Node.cs index a3956b6..ca5008d 100644 --- a/Rubjerg.Graphviz/Node.cs +++ b/Rubjerg.Graphviz/Node.cs @@ -227,6 +227,7 @@ public RectangleD GetBoundingBox() /// /// If the shape of this node was set to 'record', this method allows you to retrieve the /// resulting rectangles. + /// The order of the list matches the order in which the labels occur in the label string. /// public IEnumerable GetRecordRectangles() { @@ -254,6 +255,16 @@ public IEnumerable GetRecordRectangles() } } + /// + /// If the shape of this node was set to 'record', this method allows you to retrieve the + /// text objects of the resulting rectangles. + /// The order of the list matches the order in which the labels occur in the label string. + /// + public IEnumerable GetRecordRectangleLabels() + { + return GetLabelDrawing().OfType().Select(x => x.Value); + } + /// /// Return the value that is closest to the given target value. /// Return target if the sequence if empty. @@ -265,8 +276,5 @@ private static double FindClosest(IEnumerable self, double target) return target; } - public IReadOnlyList GetDrawing() => GetXDotValue(this, "_draw_"); - public IReadOnlyList GetLabelDrawing() => GetXDotValue(this, "_ldraw_"); - #endregion } diff --git a/Rubjerg.Graphviz/XDot.cs b/Rubjerg.Graphviz/XDot.cs index 6520681..8cefeae 100644 --- a/Rubjerg.Graphviz/XDot.cs +++ b/Rubjerg.Graphviz/XDot.cs @@ -38,6 +38,9 @@ public record struct RectangleD(PointD Point, SizeD Size) /// The point farthest from the origin public PointD FarPoint() => new PointD(Point.X + Size.Width, Point.Y + Size.Height); + public double MidX() => X + Width / 2; + public double MidY() => Y + Height / 2; + public PointD Center() => new PointD(MidX(), MidY()); public static RectangleD Create(double x, double y, double width, double height) { @@ -87,7 +90,7 @@ public sealed record class Linear(LinearGradient Gradient) : Color { } public sealed record class Radial(RadialGradient Gradient) : Color { } } -public enum XDotAlign +public enum TextAlign { Left, Center, @@ -96,7 +99,7 @@ public enum XDotAlign /// /// Represents a line of text to be drawn. -/// Labels with multiple lines will be represented by multiple instances. +/// Labels with multiple lines will be represented by multiple instances. /// /// /// The y-coordinate points to the baseline, @@ -107,7 +110,7 @@ public enum XDotAlign /// The estimated width of the text. /// /// -public record struct XDotText(PointD Anchor, XDotAlign Align, double Width, string Text, XDotFont Font, XDotFontChar FontChar) +public record struct TextInfo(PointD Anchor, TextAlign Align, double Width, string Text, Font Font, FontChar FontChar) { /// /// Compute the bounding box of this text element given the necessary font information. @@ -121,9 +124,9 @@ public RectangleD TextBoundingBox(double? distanceBetweenBaselineAndDescender = var descenderY = Anchor.Y - (distanceBetweenBaselineAndDescender ?? Font.Size / 5); var leftX = Align switch { - XDotAlign.Left => Anchor.X, - XDotAlign.Center => Anchor.X + size.Width / 2, - XDotAlign.Right => Anchor.X + size.Width, + TextAlign.Left => Anchor.X, + TextAlign.Center => Anchor.X + size.Width / 2, + TextAlign.Right => Anchor.X + size.Width, _ => throw new InvalidOperationException() }; var bottomLeft = new PointD(leftX, descenderY); @@ -137,7 +140,7 @@ public RectangleD TextBoundingBox(double? distanceBetweenBaselineAndDescender = /// public SizeD TextSize() => new SizeD(Width, Font.Size); - internal XDotText ForCoordSystem(CoordinateSystem coordSystem, double maxY) + internal TextInfo ForCoordSystem(CoordinateSystem coordSystem, double maxY) { // FIXNOW // While things like rectangles are anchored by the point closest to the origin, @@ -150,9 +153,9 @@ internal XDotText ForCoordSystem(CoordinateSystem coordSystem, double maxY) } } -public record struct XDotImage(RectangleD Position, string Name) +public record struct ImageInfo(RectangleD Position, string Name) { - internal XDotImage ForCoordSystem(CoordinateSystem coordSystem, double maxY) + internal ImageInfo ForCoordSystem(CoordinateSystem coordSystem, double maxY) { return this with { @@ -163,13 +166,13 @@ internal XDotImage ForCoordSystem(CoordinateSystem coordSystem, double maxY) /// Font size in points /// Font name -public record struct XDotFont(double Size, string Name) +public record struct Font(double Size, string Name) { - public static XDotFont Default => new() { Size = 14, Name = "Times-Roman" }; + public static Font Default => new() { Size = 14, Name = "Times-Roman" }; } [Flags] -public enum XDotFontChar +public enum FontChar { None = 0, Bold = 1, @@ -220,8 +223,8 @@ public sealed record class UnfilledPolygon(PointD[] Points) : XDotOp { } public sealed record class PolyLine(PointD[] Points) : XDotOp { } public sealed record class FilledBezier(PointD[] Points) : XDotOp { } public sealed record class UnfilledBezier(PointD[] Points) : XDotOp { } - public sealed record class Text(XDotText Value) : XDotOp { } - public sealed record class Image(XDotImage Value) : XDotOp { } + public sealed record class Text(TextInfo Value) : XDotOp { } + public sealed record class Image(ImageInfo Value) : XDotOp { } public sealed record class FillColor(Color Value) : XDotOp { } public sealed record class PenColor(Color Value) : XDotOp { } /// diff --git a/Rubjerg.Graphviz/XDotFFI.cs b/Rubjerg.Graphviz/XDotFFI.cs index 65cf459..8f0edc1 100644 --- a/Rubjerg.Graphviz/XDotFFI.cs +++ b/Rubjerg.Graphviz/XDotFFI.cs @@ -97,7 +97,7 @@ internal static class XDotFFI public static extern double get_y_text(IntPtr txt); [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern XDotAlign get_align(IntPtr txt); + public static extern TextAlign get_align(IntPtr txt); [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] public static extern double get_width(IntPtr txt); diff --git a/Rubjerg.Graphviz/XDotParser.cs b/Rubjerg.Graphviz/XDotParser.cs index e09a900..e2a34df 100644 --- a/Rubjerg.Graphviz/XDotParser.cs +++ b/Rubjerg.Graphviz/XDotParser.cs @@ -65,8 +65,8 @@ internal static List TranslateXDot(IntPtr xdotPtr, CoordinateSystem coor xdot.Ops = new XDotOp[count]; var opsPtr = XDotFFI.get_ops(xdotPtr); - var activeFont = XDotFont.Default; - var activeFontChar = XDotFontChar.None; + var activeFont = Font.Default; + var activeFontChar = FontChar.None; for (int i = 0; i < count; ++i) { IntPtr xdotOpPtr = XDotFFI.get_op_at_index(opsPtr, i); @@ -138,14 +138,14 @@ internal static List TranslateXDot(IntPtr xdotPtr, CoordinateSystem coor return xdot.Ops.ToList(); } - private static XDotFontChar TranslateFontChar(uint value) + private static FontChar TranslateFontChar(uint value) { - return (XDotFontChar)(int)value; + return (FontChar)(int)value; } - private static XDotImage TranslateImage(IntPtr imagePtr) + private static ImageInfo TranslateImage(IntPtr imagePtr) { - XDotImage image = new XDotImage + ImageInfo image = new ImageInfo ( Position: TranslateRect(XDotFFI.get_pos(imagePtr)), Name: XDotFFI.GetNameImage(imagePtr) @@ -154,9 +154,9 @@ private static XDotImage TranslateImage(IntPtr imagePtr) return image; } - private static XDotFont TranslateFont(IntPtr fontPtr) + private static Font TranslateFont(IntPtr fontPtr) { - XDotFont font = new XDotFont + Font font = new Font ( Size: XDotFFI.get_size(fontPtr), Name: XDotFFI.GetNameFont(fontPtr) @@ -289,9 +289,9 @@ private static RectangleD TranslateRect(IntPtr rectPtr) return rect; } - private static XDotText TranslateText(IntPtr txtPtr, XDotFont activeFont, XDotFontChar activeFontChar) + private static TextInfo TranslateText(IntPtr txtPtr, Font activeFont, FontChar activeFontChar) { - XDotText text = new XDotText + TextInfo text = new TextInfo ( new PointD(XDotFFI.get_x_text(txtPtr), XDotFFI.get_y_text(txtPtr)), XDotFFI.get_align(txtPtr), From 6492988832333823adb18fa8f9452fd405b3440e Mon Sep 17 00:00:00 2001 From: chtenb Date: Thu, 10 Aug 2023 14:25:54 +0200 Subject: [PATCH 05/12] Fix up --- Rubjerg.Graphviz.Test/TestDotLayout.cs | 2 +- Rubjerg.Graphviz.Test/TestXDotLayout.cs | 2 +- Rubjerg.Graphviz.Test/Tutorial.cs | 2 +- Rubjerg.Graphviz/Graph.cs | 4 +- Rubjerg.Graphviz/GraphvizCommand.cs | 2 +- Rubjerg.Graphviz/XDot.cs | 58 +++++++++++++++---------- Rubjerg.Graphviz/XDotParser.cs | 3 +- 7 files changed, 42 insertions(+), 31 deletions(-) diff --git a/Rubjerg.Graphviz.Test/TestDotLayout.cs b/Rubjerg.Graphviz.Test/TestDotLayout.cs index b6cba67..055a589 100644 --- a/Rubjerg.Graphviz.Test/TestDotLayout.cs +++ b/Rubjerg.Graphviz.Test/TestDotLayout.cs @@ -181,7 +181,7 @@ public void TestRecordShapeOrder() var rects = nodeA.GetRecordRectangles().ToList(); - Utils.AssertOrder(rects, r => (r.Point.X, r.Point.Y)); + Utils.AssertOrder(rects, r => (r.Origin.X, r.Origin.Y)); Assert.That(rects.Count, Is.EqualTo(9)); } diff --git a/Rubjerg.Graphviz.Test/TestXDotLayout.cs b/Rubjerg.Graphviz.Test/TestXDotLayout.cs index b72335e..5a909c7 100644 --- a/Rubjerg.Graphviz.Test/TestXDotLayout.cs +++ b/Rubjerg.Graphviz.Test/TestXDotLayout.cs @@ -87,7 +87,7 @@ public void TestRecordShapeOrder() var xNodeA = xdotGraph.GetNode("A"); var rects = xNodeA.GetRecordRectangles().ToList(); - Utils.AssertOrder(rects, r => (r.Point.X, r.Point.Y)); + Utils.AssertOrder(rects, r => (r.Origin.X, r.Origin.Y)); Assert.That(rects.Count, Is.EqualTo(9)); // Test xdot translation diff --git a/Rubjerg.Graphviz.Test/Tutorial.cs b/Rubjerg.Graphviz.Test/Tutorial.cs index 1783298..9bc7049 100644 --- a/Rubjerg.Graphviz.Test/Tutorial.cs +++ b/Rubjerg.Graphviz.Test/Tutorial.cs @@ -119,7 +119,7 @@ public void Layouting() if (op is XDotOp.Text { Value: var text }) { Utils.AssertPattern(PointPattern, text.Anchor.ToString()); - var boundingBox = text.TextBoundingBox(); + var boundingBox = text.TextBoundingBoxEstimate(); Utils.AssertPattern(RectPattern, boundingBox.ToString()); Assert.AreEqual(text.Text, "A"); Assert.AreEqual(text.Font.Name, "Times-Roman"); diff --git a/Rubjerg.Graphviz/Graph.cs b/Rubjerg.Graphviz/Graph.cs index 1285802..e9d862c 100644 --- a/Rubjerg.Graphviz/Graph.cs +++ b/Rubjerg.Graphviz/Graph.cs @@ -565,9 +565,9 @@ public Edge GetOrAddEdge(SubGraph gvClusterTail, SubGraph gvClusterHead, bool ma /// Compute the layout in a separate process by calling dot.exe, and return a new graph, which is a copy of the old /// graph with the xdot information added to it. /// - public RootGraph CreateLayout(string engine = LayoutEngines.Dot) + public RootGraph CreateLayout(string engine = LayoutEngines.Dot, CoordinateSystem coordinateSystem = CoordinateSystem.BottomLeft) { - return GraphvizCommand.CreateLayout(this, engine: engine); + return GraphvizCommand.CreateLayout(this, engine, coordinateSystem); } /// diff --git a/Rubjerg.Graphviz/GraphvizCommand.cs b/Rubjerg.Graphviz/GraphvizCommand.cs index 9d7e872..e89f2a8 100644 --- a/Rubjerg.Graphviz/GraphvizCommand.cs +++ b/Rubjerg.Graphviz/GraphvizCommand.cs @@ -9,7 +9,7 @@ namespace Rubjerg.Graphviz; /// public class GraphvizCommand { - public static RootGraph CreateLayout(Graph input, string engine = LayoutEngines.Dot) + public static RootGraph CreateLayout(Graph input, string engine = LayoutEngines.Dot, CoordinateSystem coordinateSystem = CoordinateSystem.BottomLeft) { var output = Exec(input, engine: engine); var resultGraph = RootGraph.FromDotString(output); diff --git a/Rubjerg.Graphviz/XDot.cs b/Rubjerg.Graphviz/XDot.cs index 8cefeae..770cd94 100644 --- a/Rubjerg.Graphviz/XDot.cs +++ b/Rubjerg.Graphviz/XDot.cs @@ -29,15 +29,15 @@ internal PointD ForCoordSystem(CoordinateSystem coordSystem, double maxY) /// The point closest to the origin /// -public record struct RectangleD(PointD Point, SizeD Size) +public record struct RectangleD(PointD Origin, SizeD Size) { - public double X => Point.X; - public double Y => Point.Y; + public double X => Origin.X; + public double Y => Origin.Y; public double Width => Size.Width; public double Height => Size.Height; /// The point farthest from the origin - public PointD FarPoint() => new PointD(Point.X + Size.Width, Point.Y + Size.Height); + public PointD FarPoint() => new PointD(Origin.X + Size.Width, Origin.Y + Size.Height); public double MidX() => X + Width / 2; public double MidY() => Y + Height / 2; public PointD Center() => new PointD(MidX(), MidY()); @@ -51,7 +51,7 @@ internal RectangleD ForCoordSystem(CoordinateSystem coordSystem, double maxY) { return this with { - Point = Point.ForCoordSystem(coordSystem, maxY), + Origin = Origin.ForCoordSystem(coordSystem, maxY), }; } } @@ -107,39 +107,46 @@ public enum TextAlign /// aligned according to the property. /// /// How the text should be aligned horizontally, relative to the given anchor point. -/// The estimated width of the text. +/// The estimated width of the text. /// /// -public record struct TextInfo(PointD Anchor, TextAlign Align, double Width, string Text, Font Font, FontChar FontChar) +/// Used for computing the bounding box in the correct orientation. +public record struct TextInfo(PointD Anchor, TextAlign Align, double WidthEstimate, string Text, + Font Font, FontChar FontChar, CoordinateSystem CoordSystem) { + public SizeD TextSizeEstimate() => new SizeD(WidthEstimate, Font.Size); + /// - /// Compute the bounding box of this text element given the necessary font information. + /// Estimate the bounding box of this text element. /// - /// Font used to draw the text - /// Optional property of the font, to more accurately predict the bounding box. - public RectangleD TextBoundingBox(double? distanceBetweenBaselineAndDescender = null) + /// + /// Coordinate system in which to express the bounding box. The text baseline is always oriented + /// below the text, while the bounding box origin is oriented to the coordinate system origin. + /// + /// + /// Optional property of the font, to more accurately predict the bounding box. + /// + public RectangleD TextBoundingBoxEstimate(double? distanceBetweenBaselineAndDescender = null) { // FIXNOW: better estimate distanceBetweenBaselineAndDescender ? - var size = TextSize(); + var size = TextSizeEstimate(); var descenderY = Anchor.Y - (distanceBetweenBaselineAndDescender ?? Font.Size / 5); + var ascenderY = descenderY + size.Height; var leftX = Align switch { TextAlign.Left => Anchor.X, - TextAlign.Center => Anchor.X + size.Width / 2, - TextAlign.Right => Anchor.X + size.Width, + TextAlign.Center => Anchor.X - size.Width / 2, + TextAlign.Right => Anchor.X - size.Width, _ => throw new InvalidOperationException() }; - var bottomLeft = new PointD(leftX, descenderY); - return new RectangleD(bottomLeft, size); + var origin = new PointD(leftX, descenderY); + if (CoordSystem == CoordinateSystem.TopLeft) + { + origin = new PointD(leftX, ascenderY); + } + return new RectangleD(origin, size); } - /// - /// The width represents the estimated width of the text by GraphViz. - /// The height represents the font size, which is usually the distance between the ascender and the descender - /// of the font. - /// - public SizeD TextSize() => new SizeD(Width, Font.Size); - internal TextInfo ForCoordSystem(CoordinateSystem coordSystem, double maxY) { // FIXNOW @@ -149,6 +156,7 @@ internal TextInfo ForCoordSystem(CoordinateSystem coordSystem, double maxY) return this with { Anchor = Anchor.ForCoordSystem(coordSystem, maxY), + CoordSystem = coordSystem, }; } } @@ -164,7 +172,9 @@ internal ImageInfo ForCoordSystem(CoordinateSystem coordSystem, double maxY) } } -/// Font size in points +/// +/// Font size in points. This is usually the distance between the ascender and the descender of the font. +/// /// Font name public record struct Font(double Size, string Name) { diff --git a/Rubjerg.Graphviz/XDotParser.cs b/Rubjerg.Graphviz/XDotParser.cs index e2a34df..891e5fa 100644 --- a/Rubjerg.Graphviz/XDotParser.cs +++ b/Rubjerg.Graphviz/XDotParser.cs @@ -298,7 +298,8 @@ private static TextInfo TranslateText(IntPtr txtPtr, Font activeFont, FontChar a XDotFFI.get_width(txtPtr), XDotFFI.GetTextStr(txtPtr), activeFont, - activeFontChar + activeFontChar, + CoordinateSystem.BottomLeft ); return text; From 88db54c7a0fc81c8a26015dee414498d70692c86 Mon Sep 17 00:00:00 2001 From: chtenb Date: Thu, 10 Aug 2023 14:29:17 +0200 Subject: [PATCH 06/12] Improve docs --- README.md | 6 +++--- Rubjerg.Graphviz.Test/Tutorial.cs | 4 ++-- Rubjerg.Graphviz/CGraphThing.cs | 6 ++++++ Rubjerg.Graphviz/Edge.cs | 12 ++++++++++++ 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index df3f87a..e98b404 100644 --- a/README.md +++ b/README.md @@ -151,12 +151,12 @@ public class Tutorial // If we require detailed drawing information for any object, we can retrieve the so called "xdot" // operations. See https://graphviz.org/docs/outputs/canon/#xdot for a specification. - var activeColor = System.Drawing.Color.Black; + var activeFillColor = System.Drawing.Color.Black; foreach (var op in nodeA.GetDrawing()) { if (op is XDotOp.FillColor { Value: Color.Uniform { HtmlColor: var htmlColor } }) { - activeColor = System.Drawing.ColorTranslator.FromHtml(htmlColor); + activeFillColor = System.Drawing.ColorTranslator.FromHtml(htmlColor); } else if (op is XDotOp.FilledEllipse { Value: var boundingBox }) { @@ -170,7 +170,7 @@ public class Tutorial if (op is XDotOp.Text { Value: var text }) { Utils.AssertPattern(PointPattern, text.Anchor.ToString()); - var boundingBox = text.TextBoundingBox(); + var boundingBox = text.TextBoundingBoxEstimate(); Utils.AssertPattern(RectPattern, boundingBox.ToString()); Assert.AreEqual(text.Text, "A"); Assert.AreEqual(text.Font.Name, "Times-Roman"); diff --git a/Rubjerg.Graphviz.Test/Tutorial.cs b/Rubjerg.Graphviz.Test/Tutorial.cs index 9bc7049..34b5221 100644 --- a/Rubjerg.Graphviz.Test/Tutorial.cs +++ b/Rubjerg.Graphviz.Test/Tutorial.cs @@ -100,12 +100,12 @@ public void Layouting() // If we require detailed drawing information for any object, we can retrieve the so called "xdot" // operations. See https://graphviz.org/docs/outputs/canon/#xdot for a specification. - var activeColor = System.Drawing.Color.Black; + var activeFillColor = System.Drawing.Color.Black; foreach (var op in nodeA.GetDrawing()) { if (op is XDotOp.FillColor { Value: Color.Uniform { HtmlColor: var htmlColor } }) { - activeColor = System.Drawing.ColorTranslator.FromHtml(htmlColor); + activeFillColor = System.Drawing.ColorTranslator.FromHtml(htmlColor); } else if (op is XDotOp.FilledEllipse { Value: var boundingBox }) { diff --git a/Rubjerg.Graphviz/CGraphThing.cs b/Rubjerg.Graphviz/CGraphThing.cs index 18446b4..d770ee1 100644 --- a/Rubjerg.Graphviz/CGraphThing.cs +++ b/Rubjerg.Graphviz/CGraphThing.cs @@ -205,7 +205,13 @@ public bool IsInvisible() return GetAttribute("style") == "invis"; } + /// + /// See documentation on + /// public IReadOnlyList GetDrawing() => GetXDotValue(this, "_draw_"); + /// + /// See documentation on + /// public IReadOnlyList GetLabelDrawing() => GetXDotValue(this, "_ldraw_"); protected List GetXDotValue(CGraphThing obj, string attrName) diff --git a/Rubjerg.Graphviz/Edge.cs b/Rubjerg.Graphviz/Edge.cs index 8d2e858..230445a 100644 --- a/Rubjerg.Graphviz/Edge.cs +++ b/Rubjerg.Graphviz/Edge.cs @@ -165,9 +165,21 @@ public IEnumerable GetSplines() return GetDrawing().OfType().Select(x => x.Points); } + /// + /// See documentation on + /// public IReadOnlyList GetHeadArrowDrawing() => GetXDotValue(this, "_hdraw_"); + /// + /// See documentation on + /// public IReadOnlyList GetTailArrowDrawing() => GetXDotValue(this, "_tdraw_"); + /// + /// See documentation on + /// public IReadOnlyList GetHeadLabelDrawing() => GetXDotValue(this, "_hldraw_"); + /// + /// See documentation on + /// public IReadOnlyList GetTailLabelDrawing() => GetXDotValue(this, "_tldraw_"); #endregion From 273f1b3589515ab6c7bf91f2ed00391efe16f23d Mon Sep 17 00:00:00 2001 From: chtenb Date: Thu, 10 Aug 2023 15:39:49 +0200 Subject: [PATCH 07/12] fixup --- Rubjerg.Graphviz/CGraphThing.cs | 2 ++ Rubjerg.Graphviz/Graph.cs | 1 - Rubjerg.Graphviz/XDot.cs | 64 ++++++++++++--------------------- 3 files changed, 24 insertions(+), 43 deletions(-) diff --git a/Rubjerg.Graphviz/CGraphThing.cs b/Rubjerg.Graphviz/CGraphThing.cs index d770ee1..18fb365 100644 --- a/Rubjerg.Graphviz/CGraphThing.cs +++ b/Rubjerg.Graphviz/CGraphThing.cs @@ -225,6 +225,8 @@ protected List GetXDotValue(CGraphThing obj, string attrName) protected static RectangleD ParseRect(string rect) { + // Rectangles are anchored by their lower left and upper right points + // https://www.graphviz.org/docs/attr-types/rect/ string[] points = rect.Split(','); var x = double.Parse(points[0], NumberStyles.Any, CultureInfo.InvariantCulture); var y = double.Parse(points[1], NumberStyles.Any, CultureInfo.InvariantCulture); diff --git a/Rubjerg.Graphviz/Graph.cs b/Rubjerg.Graphviz/Graph.cs index e9d862c..6f319b2 100644 --- a/Rubjerg.Graphviz/Graph.cs +++ b/Rubjerg.Graphviz/Graph.cs @@ -583,7 +583,6 @@ internal RectangleD RawBoundingBox() internal double RawMaxY() { - // FIXNOW: can we cache this somehow? It's called quite often return RawBoundingBox().FarPoint().Y; } diff --git a/Rubjerg.Graphviz/XDot.cs b/Rubjerg.Graphviz/XDot.cs index 770cd94..9a5d447 100644 --- a/Rubjerg.Graphviz/XDot.cs +++ b/Rubjerg.Graphviz/XDot.cs @@ -42,16 +42,15 @@ public record struct RectangleD(PointD Origin, SizeD Size) public double MidY() => Y + Height / 2; public PointD Center() => new PointD(MidX(), MidY()); - public static RectangleD Create(double x, double y, double width, double height) - { - return new RectangleD(new PointD(x, y), new SizeD(width, height)); - } + public static RectangleD Create(double x, double y, double width, double height) => new RectangleD(new PointD(x, y), new SizeD(width, height)); internal RectangleD ForCoordSystem(CoordinateSystem coordSystem, double maxY) { + var translated = Origin.ForCoordSystem(coordSystem, maxY); + // Origin must be the point closest the the coordinate system origin return this with { - Origin = Origin.ForCoordSystem(coordSystem, maxY), + Origin = new PointD(translated.X, translated.Y - Height), }; } } @@ -60,26 +59,20 @@ public record struct ColorStop(float Frac, string HtmlColor); public record struct LinearGradient(PointD Point0, PointD Point1, ColorStop[] Stops) { - internal LinearGradient ForCoordSystem(CoordinateSystem coordSystem, double maxY) + internal LinearGradient ForCoordSystem(CoordinateSystem coordSystem, double maxY) => this with { - return this with - { - Point0 = Point0.ForCoordSystem(coordSystem, maxY), - Point1 = Point1.ForCoordSystem(coordSystem, maxY), - }; - } + Point0 = Point0.ForCoordSystem(coordSystem, maxY), + Point1 = Point1.ForCoordSystem(coordSystem, maxY), + }; } public record struct RadialGradient(PointD Point0, double Radius0, PointD Point1, double Radius1, ColorStop[] Stops) { - internal RadialGradient ForCoordSystem(CoordinateSystem coordSystem, double maxY) + internal RadialGradient ForCoordSystem(CoordinateSystem coordSystem, double maxY) => this with { - return this with - { - Point0 = Point0.ForCoordSystem(coordSystem, maxY), - Point1 = Point1.ForCoordSystem(coordSystem, maxY), - }; - } + Point0 = Point0.ForCoordSystem(coordSystem, maxY), + Point1 = Point1.ForCoordSystem(coordSystem, maxY), + }; } public abstract record class Color @@ -111,7 +104,7 @@ public enum TextAlign /// /// /// Used for computing the bounding box in the correct orientation. -public record struct TextInfo(PointD Anchor, TextAlign Align, double WidthEstimate, string Text, +public record struct TextInfo(PointD Anchor, TextAlign Align, double WidthEstimate, string Text, Font Font, FontChar FontChar, CoordinateSystem CoordSystem) { public SizeD TextSizeEstimate() => new SizeD(WidthEstimate, Font.Size); @@ -128,9 +121,8 @@ public record struct TextInfo(PointD Anchor, TextAlign Align, double WidthEstima /// public RectangleD TextBoundingBoxEstimate(double? distanceBetweenBaselineAndDescender = null) { - // FIXNOW: better estimate distanceBetweenBaselineAndDescender ? var size = TextSizeEstimate(); - var descenderY = Anchor.Y - (distanceBetweenBaselineAndDescender ?? Font.Size / 5); + var descenderY = Anchor.Y - (distanceBetweenBaselineAndDescender ?? Font.Size * 0.23); var ascenderY = descenderY + size.Height; var leftX = Align switch { @@ -147,29 +139,19 @@ public RectangleD TextBoundingBoxEstimate(double? distanceBetweenBaselineAndDesc return new RectangleD(origin, size); } - internal TextInfo ForCoordSystem(CoordinateSystem coordSystem, double maxY) + internal TextInfo ForCoordSystem(CoordinateSystem coordSystem, double maxY) => this with { - // FIXNOW - // While things like rectangles are anchored by the point closest to the origin, - // the y-coordinate of a text object anchor always points to the baseline of the text. - // This means we have to take extra care when transforming to the top-left coordinate system. - return this with - { - Anchor = Anchor.ForCoordSystem(coordSystem, maxY), - CoordSystem = coordSystem, - }; - } + Anchor = Anchor.ForCoordSystem(coordSystem, maxY), + CoordSystem = coordSystem, + }; } public record struct ImageInfo(RectangleD Position, string Name) { - internal ImageInfo ForCoordSystem(CoordinateSystem coordSystem, double maxY) + internal ImageInfo ForCoordSystem(CoordinateSystem coordSystem, double maxY) => this with { - return this with - { - Position = Position.ForCoordSystem(coordSystem, maxY), - }; - } + Position = Position.ForCoordSystem(coordSystem, maxY), + }; } /// @@ -197,9 +179,7 @@ public enum FontChar internal static class PointDArrayExtension { internal static PointD[] ForCoordSystem(this PointD[] self, CoordinateSystem coordSystem, double maxY) - { - return self.Select(a => a.ForCoordSystem(coordSystem, maxY)).ToArray(); - } + => self.Select(a => a.ForCoordSystem(coordSystem, maxY)).ToArray(); } /// From 8b1bfe95c1503f2117790c3d159dd24f2bd1726a Mon Sep 17 00:00:00 2001 From: chtenb Date: Fri, 11 Aug 2023 11:39:29 +0200 Subject: [PATCH 08/12] Fix some bugs --- Rubjerg.Graphviz/Graph.cs | 2 +- Rubjerg.Graphviz/GraphvizCommand.cs | 2 +- Rubjerg.Graphviz/Node.cs | 50 +++++++---- Rubjerg.Graphviz/XDot.cs | 129 ++++++++++++++++------------ Rubjerg.Graphviz/XDotParser.cs | 2 - 5 files changed, 110 insertions(+), 75 deletions(-) diff --git a/Rubjerg.Graphviz/Graph.cs b/Rubjerg.Graphviz/Graph.cs index 6f319b2..a7ff578 100644 --- a/Rubjerg.Graphviz/Graph.cs +++ b/Rubjerg.Graphviz/Graph.cs @@ -592,7 +592,7 @@ internal double RawMaxY() public RectangleD GetBoundingBox() { var untransformed = RawBoundingBox(); - return untransformed.ForCoordSystem(MyRootGraph.CoordinateSystem, RawMaxY()); + return untransformed.ForCoordSystem(MyRootGraph.CoordinateSystem, MyRootGraph.RawMaxY()); } private void ToFile(string filepath, string format, string engine) diff --git a/Rubjerg.Graphviz/GraphvizCommand.cs b/Rubjerg.Graphviz/GraphvizCommand.cs index e89f2a8..ab396b7 100644 --- a/Rubjerg.Graphviz/GraphvizCommand.cs +++ b/Rubjerg.Graphviz/GraphvizCommand.cs @@ -12,7 +12,7 @@ public class GraphvizCommand public static RootGraph CreateLayout(Graph input, string engine = LayoutEngines.Dot, CoordinateSystem coordinateSystem = CoordinateSystem.BottomLeft) { var output = Exec(input, engine: engine); - var resultGraph = RootGraph.FromDotString(output); + var resultGraph = RootGraph.FromDotString(output, coordinateSystem); return resultGraph; } diff --git a/Rubjerg.Graphviz/Node.cs b/Rubjerg.Graphviz/Node.cs index ca5008d..3048f2c 100644 --- a/Rubjerg.Graphviz/Node.cs +++ b/Rubjerg.Graphviz/Node.cs @@ -220,8 +220,8 @@ public RectangleD GetBoundingBox() { var size = GetSize(); var center = GetPosition(); - var bottomleft = new PointD(center.X - size.Width / 2, center.Y - size.Height / 2); - return new RectangleD(bottomleft, size); + var rectangleOrigin = new PointD(center.X - size.Width / 2, center.Y - size.Height / 2); + return new RectangleD(rectangleOrigin, size); } /// @@ -229,29 +229,43 @@ public RectangleD GetBoundingBox() /// resulting rectangles. /// The order of the list matches the order in which the labels occur in the label string. /// - public IEnumerable GetRecordRectangles() + /// + /// There is a lingering issue in Graphviz where the coordinates of the record rectangles may be off. + /// As a workaround we snap onto the coordinates from the drawing info, which seem to be more reliable. + /// https://github.com/Rubjerg/Graphviz.NetWrapper/issues/30 + /// + public IEnumerable GetRecordRectangles(bool snapOntoDrawingCoordinates = false) { if (!HasAttribute("rects")) yield break; - // There is a lingering issue in Graphviz where the x coordinates of the record rectangles may be off. - // As a workaround we consult the x coordinates, and attempt to snap onto those. - // https://github.com/Rubjerg/Graphviz.NetWrapper/issues/30 - var validXCoords = GetDrawing().OfType() - .SelectMany(p => p.Points).Select(p => p.X).ToList(); + var polylinePoints = GetDrawing().OfType().SelectMany(p => p.Points).ToList(); + var validXCoords = polylinePoints.Select(p => p.X).OrderBy(x => x).Distinct().ToList(); + var validYCoords = polylinePoints.Select(p => p.Y).OrderBy(x => x).Distinct().ToList(); + var maxY = MyRootGraph.RawMaxY(); foreach (string rectStr in GetAttribute("rects").Split(' ')) { - var rect = ParseRect(rectStr); - - var x1 = rect.X; - var x2 = rect.X + rect.Width; - var fixedX1 = FindClosest(validXCoords, x1); - var fixedX2 = FindClosest(validXCoords, x2); - var fixedRect = new RectangleD( - new PointD(fixedX1, rect.Y), - new SizeD(fixedX2 - rect.X, rect.Height)); - yield return fixedRect; + var rect = ParseRect(rectStr).ForCoordSystem(MyRootGraph.CoordinateSystem, maxY); + if (!snapOntoDrawingCoordinates) + { + yield return rect; + } + else + { + var x1 = rect.X; + var x2 = rect.X + rect.Width; + var y1 = rect.Y; + var y2 = rect.Y + rect.Height; + var snappedX1 = FindClosest(validXCoords, x1); + var snappedX2 = FindClosest(validXCoords, x2); + var snappedY1 = FindClosest(validYCoords, y1); + var snappedY2 = FindClosest(validYCoords, y2); + var snappedRect = new RectangleD( + new PointD(snappedX1, snappedY1), + new SizeD(snappedX2 - snappedX1, snappedY2 - snappedY1)); + yield return snappedRect; + } } } diff --git a/Rubjerg.Graphviz/XDot.cs b/Rubjerg.Graphviz/XDot.cs index 9a5d447..cb2a21b 100644 --- a/Rubjerg.Graphviz/XDot.cs +++ b/Rubjerg.Graphviz/XDot.cs @@ -3,7 +3,54 @@ namespace Rubjerg.Graphviz; -// See https://graphviz.org/docs/outputs/canon/#xdot +/// +/// See https://graphviz.org/docs/outputs/canon/#xdot for semantics. +/// +/// Within the context of a single drawing attribute, e.g., draw, there is an implicit state for the +/// graphical attributes. That is, once a color or style is set, it remains valid for all relevant +/// drawing operations until the value is reset by another xdot cmd. +/// +/// Note that the filled figures (ellipses, polygons and B-Splines) imply two operations: first, +/// drawing the filled figure with the current fill color; second, drawing an unfilled figure with +/// the current pen color, pen width and pen style. +/// +/// The text operation is only used in the label attributes. Normally, the non-text operations are +/// only used in the non-label attributes. If, however, the decorate attribute is set on an edge, +/// its label attribute will also contain a polyline operation. In addition, if a label is a +/// complex, HTML-like label, it will also contain non-text operations. +/// +/// NOTE: we've slightly trimmed down the number of cases w.r.t. the actual xdot operations. +/// All font related operations have been condensed into the text operations. +/// We only have a single Color type, which has three subtypes. +/// +public abstract record class XDotOp +{ + private XDotOp() { } + + public sealed record class FilledEllipse(RectangleD Value) : XDotOp { } + public sealed record class UnfilledEllipse(RectangleD Value) : XDotOp { } + public sealed record class FilledPolygon(PointD[] Points) : XDotOp, IHasPoints { } + public sealed record class UnfilledPolygon(PointD[] Points) : XDotOp, IHasPoints { } + public sealed record class PolyLine(PointD[] Points) : XDotOp, IHasPoints { } + public sealed record class FilledBezier(PointD[] Points) : XDotOp, IHasPoints { } + public sealed record class UnfilledBezier(PointD[] Points) : XDotOp, IHasPoints { } + public sealed record class Text(TextInfo Value) : XDotOp { } + public sealed record class Image(ImageInfo Value) : XDotOp { } + public sealed record class FillColor(Color Value) : XDotOp { } + public sealed record class PenColor(Color Value) : XDotOp { } + /// + /// Style values which can be incorporated in the graphics model do not appear in xdot + /// output. In particular, the style values filled, rounded, diagonals, and invis will not + /// appear. Indeed, if style contains invis, there will not be any xdot output at all. + /// For reference see https://graphviz.org/docs/attr-types/style/ + /// + public sealed record class Style(string Value) : XDotOp { } +} + +public interface IHasPoints +{ + public PointD[] Points { get; } +} /// /// In Graphviz, the default coordinate system has the origin on the bottom left. @@ -27,7 +74,7 @@ internal PointD ForCoordSystem(CoordinateSystem coordSystem, double maxY) } } -/// The point closest to the origin +/// The origin of the rectangle, which is the point closest to the origin of the coordinate system. /// public record struct RectangleD(PointD Origin, SizeD Size) { @@ -42,12 +89,16 @@ public record struct RectangleD(PointD Origin, SizeD Size) public double MidY() => Y + Height / 2; public PointD Center() => new PointD(MidX(), MidY()); - public static RectangleD Create(double x, double y, double width, double height) => new RectangleD(new PointD(x, y), new SizeD(width, height)); + public static RectangleD Create(double x, double y, double width, double height) + => new RectangleD(new PointD(x, y), new SizeD(width, height)); internal RectangleD ForCoordSystem(CoordinateSystem coordSystem, double maxY) { + if (coordSystem == CoordinateSystem.BottomLeft) + return this; + var translated = Origin.ForCoordSystem(coordSystem, maxY); - // Origin must be the point closest the the coordinate system origin + // Origin must be the point closest the origin of the coordinate system return this with { Origin = new PointD(translated.X, translated.Y - Height), @@ -108,6 +159,7 @@ public record struct TextInfo(PointD Anchor, TextAlign Align, double WidthEstima Font Font, FontChar FontChar, CoordinateSystem CoordSystem) { public SizeD TextSizeEstimate() => new SizeD(WidthEstimate, Font.Size); + public double Baseline => Anchor.Y; /// /// Estimate the bounding box of this text element. @@ -122,8 +174,6 @@ public record struct TextInfo(PointD Anchor, TextAlign Align, double WidthEstima public RectangleD TextBoundingBoxEstimate(double? distanceBetweenBaselineAndDescender = null) { var size = TextSizeEstimate(); - var descenderY = Anchor.Y - (distanceBetweenBaselineAndDescender ?? Font.Size * 0.23); - var ascenderY = descenderY + size.Height; var leftX = Align switch { TextAlign.Left => Anchor.X, @@ -131,10 +181,27 @@ public RectangleD TextBoundingBoxEstimate(double? distanceBetweenBaselineAndDesc TextAlign.Right => Anchor.X - size.Width, _ => throw new InvalidOperationException() }; - var origin = new PointD(leftX, descenderY); - if (CoordSystem == CoordinateSystem.TopLeft) + + var d = distanceBetweenBaselineAndDescender ?? Font.Size * 0.23; + double descender; + if (CoordSystem == CoordinateSystem.BottomLeft) { - origin = new PointD(leftX, ascenderY); + descender = Baseline - d; + } + else + { + descender = Baseline + d; + } + + PointD origin; + if (CoordSystem == CoordinateSystem.BottomLeft) + { + origin = new PointD(leftX, descender); + } + else + { + var ascender = descender - size.Height; + origin = new PointD(leftX, ascender); } return new RectangleD(origin, size); } @@ -181,47 +248,3 @@ internal static class PointDArrayExtension internal static PointD[] ForCoordSystem(this PointD[] self, CoordinateSystem coordSystem, double maxY) => self.Select(a => a.ForCoordSystem(coordSystem, maxY)).ToArray(); } - -/// -/// See https://graphviz.org/docs/outputs/canon/#xdot for semantics. -/// -/// Within the context of a single drawing attribute, e.g., draw, there is an implicit state for the -/// graphical attributes. That is, once a color or style is set, it remains valid for all relevant -/// drawing operations until the value is reset by another xdot cmd. -/// -/// Note that the filled figures (ellipses, polygons and B-Splines) imply two operations: first, -/// drawing the filled figure with the current fill color; second, drawing an unfilled figure with -/// the current pen color, pen width and pen style. -/// -/// The text operation is only used in the label attributes. Normally, the non-text operations are -/// only used in the non-label attributes. If, however, the decorate attribute is set on an edge, -/// its label attribute will also contain a polyline operation. In addition, if a label is a -/// complex, HTML-like label, it will also contain non-text operations. -/// -/// NOTE: we've slightly trimmed down the number of cases w.r.t. the actual xdot operations. -/// All font related operations have been condensed into the text operations. -/// We only have a single Color type, which has three subtypes. -/// -public abstract record class XDotOp -{ - private XDotOp() { } - - public sealed record class FilledEllipse(RectangleD Value) : XDotOp { } - public sealed record class UnfilledEllipse(RectangleD Value) : XDotOp { } - public sealed record class FilledPolygon(PointD[] Points) : XDotOp { } - public sealed record class UnfilledPolygon(PointD[] Points) : XDotOp { } - public sealed record class PolyLine(PointD[] Points) : XDotOp { } - public sealed record class FilledBezier(PointD[] Points) : XDotOp { } - public sealed record class UnfilledBezier(PointD[] Points) : XDotOp { } - public sealed record class Text(TextInfo Value) : XDotOp { } - public sealed record class Image(ImageInfo Value) : XDotOp { } - public sealed record class FillColor(Color Value) : XDotOp { } - public sealed record class PenColor(Color Value) : XDotOp { } - /// - /// Style values which can be incorporated in the graphics model do not appear in xdot - /// output. In particular, the style values filled, rounded, diagonals, and invis will not - /// appear. Indeed, if style contains invis, there will not be any xdot output at all. - /// For reference see https://graphviz.org/docs/attr-types/style/ - /// - public sealed record class Style(string Value) : XDotOp { } -} diff --git a/Rubjerg.Graphviz/XDotParser.cs b/Rubjerg.Graphviz/XDotParser.cs index 891e5fa..9007206 100644 --- a/Rubjerg.Graphviz/XDotParser.cs +++ b/Rubjerg.Graphviz/XDotParser.cs @@ -30,8 +30,6 @@ internal struct XDot public XDotOp[] Ops { get; set; } // xdot operations } -/// -/// internal static class XDotParser { public static List ParseXDot(string xdotString, CoordinateSystem coordinateSystem, double maxY) From 419bd31fd283d1b7aec5514226c3640eb7600926 Mon Sep 17 00:00:00 2001 From: chtenb Date: Fri, 11 Aug 2023 12:19:25 +0200 Subject: [PATCH 09/12] Fix tests --- README.md | 8 +++++--- Rubjerg.Graphviz.Test/OldTutorial.cs | 6 +++--- Rubjerg.Graphviz.Test/Reproductions.cs | 3 ++- Rubjerg.Graphviz.Test/TestDotLayout.cs | 10 +++++----- Rubjerg.Graphviz.Test/TestXDotLayout.cs | 13 ++++++++++++- Rubjerg.Graphviz.Test/Tutorial.cs | 8 +++++--- Rubjerg.Graphviz/XDot.cs | 4 ++++ 7 files changed, 36 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index e98b404..8607e7d 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ namespace Rubjerg.Graphviz.Test; public class Tutorial { public const string PointPattern = @"{X=[\d.]+, Y=[\d.]+}"; - public const string RectPattern = @"{X=[\d.]+,Y=[\d.]+,Width=[\d.]+,Height=[\d.]+}"; + public const string RectPattern = @"{X=[\d.]+, Y=[\d.]+, Width=[\d.]+, Height=[\d.]+}"; public const string SplinePattern = @"{X=[\d.]+, Y=[\d.]+}, {X=[\d.]+, Y=[\d.]+}, {X=[\d.]+, Y=[\d.]+}, {X=[\d.]+, Y=[\d.]+}"; @@ -130,8 +130,10 @@ public class Tutorial // Or we can ask Graphviz to compute the layout and programatically read out the layout attributes // This will create a copy of our original graph with layout information attached to it in the form - // of attributes. - RootGraph layout = root.CreateLayout(); + // of attributes. Graphviz outputs coordinates in a bottom-left originated coordinate system. + // But since many applications require rendering in a top-left originated coordinate system, + // we provide a way to translate the coordinates. + RootGraph layout = root.CreateLayout(coordinateSystem: CoordinateSystem.TopLeft); // There are convenience methods available that parse these attributes for us and give // back the layout information in an accessible form. diff --git a/Rubjerg.Graphviz.Test/OldTutorial.cs b/Rubjerg.Graphviz.Test/OldTutorial.cs index 288e01c..314daf0 100644 --- a/Rubjerg.Graphviz.Test/OldTutorial.cs +++ b/Rubjerg.Graphviz.Test/OldTutorial.cs @@ -73,7 +73,7 @@ public void Layouting() // Like a bounding box of an object RectangleD nodeboundingbox = nodeA.GetBoundingBox(); - Utils.AssertPattern(@"{X=[\d.]+,Y=[\d.]+,Width=[\d.]+,Height=[\d.]+}", nodeboundingbox.ToString()); + Utils.AssertPattern(@"{X=[\d.]+, Y=[\d.]+, Width=[\d.]+, Height=[\d.]+}", nodeboundingbox.ToString()); // Or splines between nodes Node nodeB = root.GetNode("B"); @@ -133,8 +133,8 @@ public void Clusters() SubGraph cluster = root.GetSubgraph("cluster_1"); RectangleD clusterbox = cluster.GetBoundingBox(); RectangleD rootgraphbox = root.GetBoundingBox(); - Utils.AssertPattern(@"{X=[\d.]+,Y=[\d.]+,Width=[\d.]+,Height=[\d.]+}", clusterbox.ToString()); - Utils.AssertPattern(@"{X=[\d.]+,Y=[\d.]+,Width=[\d.]+,Height=[\d.]+}", rootgraphbox.ToString()); + Utils.AssertPattern(@"{X=[\d.]+, Y=[\d.]+, Width=[\d.]+, Height=[\d.]+}", clusterbox.ToString()); + Utils.AssertPattern(@"{X=[\d.]+, Y=[\d.]+, Width=[\d.]+, Height=[\d.]+}", rootgraphbox.ToString()); } [Test, Order(4)] diff --git a/Rubjerg.Graphviz.Test/Reproductions.cs b/Rubjerg.Graphviz.Test/Reproductions.cs index cc5bb14..c755eb5 100644 --- a/Rubjerg.Graphviz.Test/Reproductions.cs +++ b/Rubjerg.Graphviz.Test/Reproductions.cs @@ -40,7 +40,8 @@ public void TestRecordShapeAlignment(string fontname, double fontsize, double ma root.ComputeLayout(); //TestContext.Write(root.ToDotString()); - var rects = nodeA.GetRecordRectangles().ToList(); + // This test is fixed by passing snapOntoDrawingCoordinates: true + var rects = nodeA.GetRecordRectangles(snapOntoDrawingCoordinates: true).ToList(); Assert.That(rects[0].FarPoint().X, Is.EqualTo(rects[2].FarPoint().X)); } diff --git a/Rubjerg.Graphviz.Test/TestDotLayout.cs b/Rubjerg.Graphviz.Test/TestDotLayout.cs index 055a589..3f89955 100644 --- a/Rubjerg.Graphviz.Test/TestDotLayout.cs +++ b/Rubjerg.Graphviz.Test/TestDotLayout.cs @@ -34,13 +34,13 @@ public void TestLayoutMethodsWithoutLayout() { CreateSimpleTestGraph(out RootGraph root, out Node nodeA, out Edge edge); - Assert.AreEqual(root.GetBoundingBox(), default(RectangleF)); + Assert.AreEqual(root.GetBoundingBox(), default(RectangleD)); Assert.AreEqual(root.GetDrawing().Count, 0); Assert.AreEqual(root.GetLabelDrawing().Count, 0); - Assert.AreEqual(nodeA.GetPosition(), default(PointF)); - Assert.AreEqual(nodeA.GetBoundingBox(), default(RectangleF)); - Assert.AreEqual(nodeA.GetSize(), default(SizeF)); + Assert.AreEqual(nodeA.GetPosition(), default(PointD)); + Assert.AreEqual(nodeA.GetBoundingBox(), default(RectangleD)); + Assert.AreEqual(nodeA.GetSize(), default(SizeD)); Assert.AreEqual(nodeA.GetRecordRectangles().Count(), 0); Assert.AreEqual(nodeA.GetDrawing().Count, 0); Assert.AreEqual(nodeA.GetLabelDrawing().Count, 0); @@ -181,7 +181,7 @@ public void TestRecordShapeOrder() var rects = nodeA.GetRecordRectangles().ToList(); - Utils.AssertOrder(rects, r => (r.Origin.X, r.Origin.Y)); + Utils.AssertOrder(rects, r => (r.Origin.X, -r.Origin.Y)); Assert.That(rects.Count, Is.EqualTo(9)); } diff --git a/Rubjerg.Graphviz.Test/TestXDotLayout.cs b/Rubjerg.Graphviz.Test/TestXDotLayout.cs index 5a909c7..6b98160 100644 --- a/Rubjerg.Graphviz.Test/TestXDotLayout.cs +++ b/Rubjerg.Graphviz.Test/TestXDotLayout.cs @@ -82,7 +82,7 @@ public void TestRecordShapeOrder() nodeA.SetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}"); - var xdotGraph = root.CreateLayout(); + var xdotGraph = root.CreateLayout(coordinateSystem: CoordinateSystem.TopLeft); var xNodeA = xdotGraph.GetNode("A"); var rects = xNodeA.GetRecordRectangles().ToList(); @@ -108,4 +108,15 @@ public void TestEmptyRecordShapes() var rects = xNodeA.GetRecordRectangles().ToList(); Assert.That(rects.Count, Is.EqualTo(5)); } + + [Test()] + public void TestCoordinateTransformation() + { + RootGraph root = Utils.CreateUniqueTestGraph(); + Node nodeA = root.GetOrAddNode("A"); + var xdotGraph = root.CreateLayout(coordinateSystem: CoordinateSystem.TopLeft); + // Check that translating back gets us the old bounding box + var translatedBack = xdotGraph.GetBoundingBox().ForCoordSystem(CoordinateSystem.BottomLeft, xdotGraph.RawMaxY()); + Assert.AreEqual(translatedBack, xdotGraph.RawBoundingBox()); + } } diff --git a/Rubjerg.Graphviz.Test/Tutorial.cs b/Rubjerg.Graphviz.Test/Tutorial.cs index 34b5221..c3b6779 100644 --- a/Rubjerg.Graphviz.Test/Tutorial.cs +++ b/Rubjerg.Graphviz.Test/Tutorial.cs @@ -7,7 +7,7 @@ namespace Rubjerg.Graphviz.Test; public class Tutorial { public const string PointPattern = @"{X=[\d.]+, Y=[\d.]+}"; - public const string RectPattern = @"{X=[\d.]+,Y=[\d.]+,Width=[\d.]+,Height=[\d.]+}"; + public const string RectPattern = @"{X=[\d.]+, Y=[\d.]+, Width=[\d.]+, Height=[\d.]+}"; public const string SplinePattern = @"{X=[\d.]+, Y=[\d.]+}, {X=[\d.]+, Y=[\d.]+}, {X=[\d.]+, Y=[\d.]+}, {X=[\d.]+, Y=[\d.]+}"; @@ -79,8 +79,10 @@ public void Layouting() // Or we can ask Graphviz to compute the layout and programatically read out the layout attributes // This will create a copy of our original graph with layout information attached to it in the form - // of attributes. - RootGraph layout = root.CreateLayout(); + // of attributes. Graphviz outputs coordinates in a bottom-left originated coordinate system. + // But since many applications require rendering in a top-left originated coordinate system, + // we provide a way to translate the coordinates. + RootGraph layout = root.CreateLayout(coordinateSystem: CoordinateSystem.TopLeft); // There are convenience methods available that parse these attributes for us and give // back the layout information in an accessible form. diff --git a/Rubjerg.Graphviz/XDot.cs b/Rubjerg.Graphviz/XDot.cs index cb2a21b..de9abef 100644 --- a/Rubjerg.Graphviz/XDot.cs +++ b/Rubjerg.Graphviz/XDot.cs @@ -72,6 +72,8 @@ internal PointD ForCoordSystem(CoordinateSystem coordSystem, double maxY) return this; return new PointD(X, maxY - Y); } + + public override string ToString() => $"{{X={X}, Y={Y}}}"; } /// The origin of the rectangle, which is the point closest to the origin of the coordinate system. @@ -104,6 +106,8 @@ internal RectangleD ForCoordSystem(CoordinateSystem coordSystem, double maxY) Origin = new PointD(translated.X, translated.Y - Height), }; } + + public override string ToString() => $"{{X={X}, Y={Y}, Width={Width}, Height={Height}}}"; } public record struct ColorStop(float Frac, string HtmlColor); From 2240899dc9564c5a461aa7264bc9ad3c6420fe03 Mon Sep 17 00:00:00 2001 From: chtenb Date: Fri, 11 Aug 2023 12:32:26 +0200 Subject: [PATCH 10/12] Add flag to make open cover forward return code --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9909d7e..134ef79 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -49,11 +49,11 @@ jobs: - name: Run Unittests With Coverage Calculation (.NET 4.8) - run: packages\opencover\4.7.1221\tools\OpenCover.Console.exe -skipautoprops -register '-target:bash.exe' -targetargs:'nunit-console.sh Rubjerg.Graphviz.Test\bin\x64\Release\net48\Rubjerg.Graphviz.Test.dll' '-filter:+[Rubjerg*]* -[Rubjerg.Graphviz.Test*]*' + run: packages\opencover\4.7.1221\tools\OpenCover.Console.exe -skipautoprops -returntargetcode -register '-target:bash.exe' -targetargs:'nunit-console.sh Rubjerg.Graphviz.Test\bin\x64\Release\net48\Rubjerg.Graphviz.Test.dll' '-filter:+[Rubjerg*]* -[Rubjerg.Graphviz.Test*]*' - name: Run Unittests With Coverage Calculation (.NET 6) - run: packages\opencover\4.7.1221\tools\OpenCover.Console.exe -skipautoprops -register '-target:bash.exe' -targetargs:'nunit-console-netcore.sh Rubjerg.Graphviz.Test\bin\x64\Release\net6.0\Rubjerg.Graphviz.Test.dll' '-filter:+[Rubjerg*]* -[Rubjerg.Graphviz.Test*]*' + run: packages\opencover\4.7.1221\tools\OpenCover.Console.exe -skipautoprops -returntargetcode -register '-target:bash.exe' -targetargs:'nunit-console-netcore.sh Rubjerg.Graphviz.Test\bin\x64\Release\net6.0\Rubjerg.Graphviz.Test.dll' '-filter:+[Rubjerg*]* -[Rubjerg.Graphviz.Test*]*' - name: Upload Coverage data run: | From 310aae3e5456e24cb7ebb07052344119e0348050 Mon Sep 17 00:00:00 2001 From: chtenb Date: Fri, 11 Aug 2023 14:10:23 +0200 Subject: [PATCH 11/12] fix test on net 6.0 --- Rubjerg.Graphviz/GraphvizCommand.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Rubjerg.Graphviz/GraphvizCommand.cs b/Rubjerg.Graphviz/GraphvizCommand.cs index ab396b7..c2da7ae 100644 --- a/Rubjerg.Graphviz/GraphvizCommand.cs +++ b/Rubjerg.Graphviz/GraphvizCommand.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.IO; +using System.Reflection; namespace Rubjerg.Graphviz; @@ -16,6 +17,7 @@ public static RootGraph CreateLayout(Graph input, string engine = LayoutEngines. return resultGraph; } + // FIXNOW: expose stderr and return code too public static string Exec(Graph input, string format = "xdot", string outputPath = null, string engine = LayoutEngines.Dot) { string exeName = "dot.exe"; @@ -27,7 +29,9 @@ public static string Exec(Graph input, string format = "xdot", string outputPath string inputToStdin = input.ToDotString(); // Get the location of the currently executing DLL - string exeDirectory = AppDomain.CurrentDomain.RelativeSearchPath ?? AppDomain.CurrentDomain.BaseDirectory; + // https://learn.microsoft.com/en-us/dotnet/api/system.reflection.assembly.codebase?view=net-5.0 + string exeDirectory = AppDomain.CurrentDomain.RelativeSearchPath + ?? Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); // Construct the path to the executable string exePath = Path.Combine(exeDirectory, exeName); From 2759963a0fe31cdcbfba539006840b2806f0122c Mon Sep 17 00:00:00 2001 From: chtenb Date: Fri, 11 Aug 2023 14:18:34 +0200 Subject: [PATCH 12/12] Capture stderr --- Rubjerg.Graphviz/GraphvizCommand.cs | 25 +++++++++++++++---------- Rubjerg.Graphviz/RootGraph.cs | 4 ++++ 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/Rubjerg.Graphviz/GraphvizCommand.cs b/Rubjerg.Graphviz/GraphvizCommand.cs index c2da7ae..a1590c6 100644 --- a/Rubjerg.Graphviz/GraphvizCommand.cs +++ b/Rubjerg.Graphviz/GraphvizCommand.cs @@ -12,13 +12,18 @@ public class GraphvizCommand { public static RootGraph CreateLayout(Graph input, string engine = LayoutEngines.Dot, CoordinateSystem coordinateSystem = CoordinateSystem.BottomLeft) { - var output = Exec(input, engine: engine); - var resultGraph = RootGraph.FromDotString(output, coordinateSystem); + var (stdout, stderr) = Exec(input, engine: engine); + var resultGraph = RootGraph.FromDotString(stdout, coordinateSystem); + resultGraph.Warnings = stderr; return resultGraph; } - // FIXNOW: expose stderr and return code too - public static string Exec(Graph input, string format = "xdot", string outputPath = null, string engine = LayoutEngines.Dot) + /// + /// Start dot.exe to compute a layout. + /// + /// When the Graphviz process did not return successfully + /// stderr may contain warnings + public static (string stdout, string stderr) Exec(Graph input, string format = "xdot", string outputPath = null, string engine = LayoutEngines.Dot) { string exeName = "dot.exe"; string arguments = $"-T{format} -K{engine}"; @@ -54,15 +59,15 @@ public static string Exec(Graph input, string format = "xdot", string outputPath sw.WriteLine(inputToStdin); // Read from stdout - string output; + string stdout; using (StreamReader sr = process.StandardOutput) - output = sr.ReadToEnd() + stdout = sr.ReadToEnd() .Replace("\r\n", "\n"); // File operations do this automatically, but stream operations do not // Read from stderr - string error; + string stderr; using (StreamReader sr = process.StandardError) - error = sr.ReadToEnd() + stderr = sr.ReadToEnd() .Replace("\r\n", "\n"); // File operations do this automatically, but stream operations do not process.WaitForExit(); @@ -70,12 +75,12 @@ public static string Exec(Graph input, string format = "xdot", string outputPath if (process.ExitCode != 0) { // Something went wrong. - throw new ApplicationException($"Process exited with code {process.ExitCode}. Error details: {error}"); + throw new ApplicationException($"Process exited with code {process.ExitCode}. Error details: {stderr}"); } else { // Process completed successfully. - return output; + return (stdout, stderr); } } } diff --git a/Rubjerg.Graphviz/RootGraph.cs b/Rubjerg.Graphviz/RootGraph.cs index 0e101bc..51a02d6 100644 --- a/Rubjerg.Graphviz/RootGraph.cs +++ b/Rubjerg.Graphviz/RootGraph.cs @@ -25,6 +25,10 @@ public class RootGraph : Graph private long _added_pressure = 0; public CoordinateSystem CoordinateSystem { get; } + /// + /// Contains any warnings that Graphviz generated during computation of the layout. + /// + public string Warnings { get; internal set; } protected RootGraph(IntPtr ptr, CoordinateSystem coordinateSystem) : base(ptr, null) {