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: | diff --git a/README.md b/README.md index b40e9f1..8607e7d 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; @@ -59,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.]+}"; @@ -131,57 +130,52 @@ 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. 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 activeFillColor = 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); + activeFillColor = 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.TextBoundingBoxEstimate(); Utils.AssertPattern(RectPattern, boundingBox.ToString()); Assert.AreEqual(text.Text, "A"); + Assert.AreEqual(text.Font.Name, "Times-Roman"); } // Handle any xdot operation you require } @@ -223,8 +217,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()); } @@ -234,14 +228,17 @@ 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"); + // 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(); // 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)] @@ -261,8 +258,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..314daf0 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,28 +68,23 @@ 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(); - Utils.AssertPattern(@"{X=[\d.]+,Y=[\d.]+,Width=[\d.]+,Height=[\d.]+}", nodeboundingbox.ToString()); + 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.]+}," + @" {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 @@ -137,10 +131,10 @@ public void Clusters() root.ComputeLayout(); SubGraph cluster = root.GetSubgraph("cluster_1"); - RectangleF clusterbox = cluster.GetBoundingBox(); - RectangleF 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()); + 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()); } [Test, Order(4)] diff --git a/Rubjerg.Graphviz.Test/Reproductions.cs b/Rubjerg.Graphviz.Test/Reproductions.cs index 6cf8797..c755eb5 100644 --- a/Rubjerg.Graphviz.Test/Reproductions.cs +++ b/Rubjerg.Graphviz.Test/Reproductions.cs @@ -33,15 +33,16 @@ 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(); //TestContext.Write(root.ToDotString()); - var rects = nodeA.GetRecordRectangles().ToList(); - Assert.That(rects[0].Right, Is.EqualTo(rects[2].Right)); + // 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)); } // This test only failed when running in isolation @@ -74,8 +75,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 +89,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..3f89955 100644 --- a/Rubjerg.Graphviz.Test/TestDotLayout.cs +++ b/Rubjerg.Graphviz.Test/TestDotLayout.cs @@ -34,14 +34,13 @@ 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.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); @@ -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)); @@ -179,15 +174,14 @@ 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(); 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.Origin.X, -r.Origin.Y)); Assert.That(rects.Count, Is.EqualTo(9)); } @@ -196,8 +190,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 +217,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 +263,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..6b98160 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); } @@ -38,9 +38,9 @@ public void TestXDotRecordNode() RootGraph root = Utils.CreateUniqueTestGraph(); Node nodeA = root.GetOrAddNode("A"); - nodeA.SafeSetAttribute("shape", "record", ""); - // FIXNOW: document that newlines are not supported in record labels - nodeA.SafeSetAttribute("label", "1|{2\n3}", "\\N"); + nodeA.SetAttribute("shape", "record"); + // New lines in record labels are ignored by Graphviz + 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,17 +78,16 @@ 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(); + var xdotGraph = root.CreateLayout(coordinateSystem: CoordinateSystem.TopLeft); 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.Origin.X, r.Origin.Y)); Assert.That(rects.Count, Is.EqualTo(9)); // Test xdot translation @@ -100,8 +99,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(); @@ -109,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 6a24f5d..c3b6779 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; @@ -8,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.]+}"; @@ -80,57 +79,52 @@ 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. 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 activeFillColor = 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); + activeFillColor = 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.TextBoundingBoxEstimate(); Utils.AssertPattern(RectPattern, boundingBox.ToString()); Assert.AreEqual(text.Text, "A"); + Assert.AreEqual(text.Font.Name, "Times-Roman"); } // Handle any xdot operation you require } @@ -172,8 +166,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()); } @@ -183,14 +177,17 @@ 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"); + // 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(); // 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)] @@ -210,8 +207,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..18fb365 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"); @@ -203,21 +197,42 @@ 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) + /// + /// 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) { 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) + { + // 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); + 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 01a2bd9..230445a 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; } @@ -96,7 +93,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 +107,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); } /// @@ -152,7 +149,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,17 +160,26 @@ public PointF[] GetFirstSpline() /// https://github.com/ellson/graphviz/issues/1277 /// Edge arrows are ignored. /// - public IEnumerable GetSplines() + public IEnumerable GetSplines() { - 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_"); - public IReadOnlyList GetLabelDrawing() => GetXDotValue(this, "_ldraw_"); + /// + /// 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 diff --git a/Rubjerg.Graphviz/Graph.cs b/Rubjerg.Graphviz/Graph.cs index d5f1635..a7ff578 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; @@ -567,28 +565,35 @@ 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); } - public RectangleF GetBoundingBox() + /// + /// Untransformed boundingbox. Still needs to be transformed to the desired coordinate system. + /// + internal RectangleD RawBoundingBox() { 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); } - public IReadOnlyList GetDrawing() => GetXDotValue(this, "_draw_"); - public IReadOnlyList GetLabelDrawing() => GetXDotValue(this, "_ldraw_"); + internal double RawMaxY() + { + return RawBoundingBox().FarPoint().Y; + } + + /// + /// The bounding box of this (sub)graph. + /// + public RectangleD GetBoundingBox() + { + var untransformed = RawBoundingBox(); + return untransformed.ForCoordSystem(MyRootGraph.CoordinateSystem, MyRootGraph.RawMaxY()); + } private void ToFile(string filepath, string format, string engine) { @@ -650,14 +655,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/GraphvizCommand.cs b/Rubjerg.Graphviz/GraphvizCommand.cs index 9d7e872..a1590c6 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; @@ -9,14 +10,20 @@ 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); + var (stdout, stderr) = Exec(input, engine: engine); + var resultGraph = RootGraph.FromDotString(stdout, coordinateSystem); + resultGraph.Warnings = stderr; return resultGraph; } - 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}"; @@ -27,7 +34,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); @@ -50,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(); @@ -66,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/Node.cs b/Rubjerg.Graphviz/Node.cs index 8783816..3048f2c 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; @@ -160,11 +159,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 @@ -172,33 +171,38 @@ public void MakeInvisibleAndSmall() /// /// The position of the center of the node. /// - public PointF GetPosition() + public PointD GetPosition() { // 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() { // 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 { @@ -209,47 +213,72 @@ 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 rectangleOrigin = new PointD(center.X - size.Width / 2, center.Y - size.Height / 2); + return new RectangleD(rectangleOrigin, size); } /// /// 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() + /// + /// 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.Value.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 = (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)); - 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; + } } } + /// + /// 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. @@ -261,27 +290,5 @@ 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_"); - #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..51a02d6 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, @@ -20,7 +23,17 @@ public enum GraphType public class RootGraph : Graph { private long _added_pressure = 0; - protected RootGraph(IntPtr ptr) : base(ptr, null) { } + + 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) + { + CoordinateSystem = coordinateSystem; + } ~RootGraph() { if (_added_pressure > 0) @@ -54,11 +67,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 +99,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() diff --git a/Rubjerg.Graphviz/XDot.cs b/Rubjerg.Graphviz/XDot.cs index 7a878bc..de9abef 100644 --- a/Rubjerg.Graphviz/XDot.cs +++ b/Rubjerg.Graphviz/XDot.cs @@ -1,81 +1,144 @@ using System; -using System.Drawing; +using System.Linq; namespace Rubjerg.Graphviz; -// See https://graphviz.org/docs/outputs/canon/#xdot - -public record struct XDotColorStop +/// +/// 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 { - public float Frac { get; init; } - public string Color { get; init; } + 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 record struct XDotLinearGrad +public interface IHasPoints { - 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 PointD[] Points { get; } } -public record struct XDotRadialGrad +/// +/// 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 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; } + BottomLeft = 0, + TopLeft = 1, } -public abstract record class XDotGradColor +public record struct SizeD(double Width, double Height); + +public record struct PointD(double X, double Y) { - private XDotGradColor() { } - public sealed record class Uniform : XDotGradColor + internal PointD ForCoordSystem(CoordinateSystem coordSystem, double maxY) { - public string Color { get; init; } - } - public sealed record class LinearGradient : XDotGradColor - { - public XDotLinearGrad LinearGrad { get; init; } - } - public sealed record class RadialGradient : XDotGradColor - { - public XDotRadialGrad RadialGrad { get; init; } + if (coordSystem == CoordinateSystem.BottomLeft) + return this; + return new PointD(X, maxY - Y); } + + public override string ToString() => $"{{X={X}, Y={Y}}}"; } -public record struct XDotPoint +/// 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) { - public double X { get; init; } - public double Y { get; init; } - public double Z { get; init; } + 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(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()); + + 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 origin of the coordinate system + return this with + { + Origin = new PointD(translated.X, translated.Y - Height), + }; + } - public PointF ToPointF() => new PointF((float)X, (float)Y); + public override string ToString() => $"{{X={X}, Y={Y}, Width={Width}, Height={Height}}}"; } -public record struct XDotRect +public record struct ColorStop(float Frac, string HtmlColor); + +public record struct LinearGradient(PointD Point0, PointD Point1, ColorStop[] Stops) { - public double X { get; init; } - public double Y { get; init; } - public double Width { get; init; } - public double Height { get; init; } + internal LinearGradient ForCoordSystem(CoordinateSystem coordSystem, double maxY) => this with + { + Point0 = Point0.ForCoordSystem(coordSystem, maxY), + Point1 = Point1.ForCoordSystem(coordSystem, maxY), + }; +} - public RectangleF ToRectangleF() => new RectangleF((float)X, (float)Y, (float)Width, (float)Height); +public record struct RadialGradient(PointD Point0, double Radius0, PointD Point1, double Radius1, ColorStop[] Stops) +{ + internal RadialGradient ForCoordSystem(CoordinateSystem coordSystem, double maxY) => this with + { + Point0 = Point0.ForCoordSystem(coordSystem, maxY), + Point1 = Point1.ForCoordSystem(coordSystem, maxY), + }; } -public record struct XDotPolyline +public abstract record class Color { - public int Count { get; init; } - public XDotPoint[] Points { 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 +public enum TextAlign { Left, Center, @@ -84,78 +147,95 @@ 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. /// -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. +/// +/// +/// 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) { - /// - /// 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; } + public SizeD TextSizeEstimate() => new SizeD(WidthEstimate, Font.Size); + public double Baseline => Anchor.Y; /// - /// 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 RectangleF TextBoundingBox(XDotFont font, float? distanceBetweenBaselineAndDescender = null) - { - var size = Size(font); - var descenderY = Y - (distanceBetweenBaselineAndDescender ?? font.Size / 5); + /// + /// 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) + { + var size = TextSizeEstimate(); var leftX = Align switch { - XDotAlign.Left => X, - XDotAlign.Center => X + size.Width / 2, - XDotAlign.Right => 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 PointF((float)leftX, (float)descenderY); - return new RectangleF(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); + var d = distanceBetweenBaselineAndDescender ?? Font.Size * 0.23; + double descender; + if (CoordSystem == CoordinateSystem.BottomLeft) + { + descender = Baseline - d; + } + else + { + descender = Baseline + d; + } - /// - /// 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); + 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); + } + + internal TextInfo ForCoordSystem(CoordinateSystem coordSystem, double maxY) => this with + { + Anchor = Anchor.ForCoordSystem(coordSystem, maxY), + CoordSystem = coordSystem, + }; } -public record struct XDotImage +public record struct ImageInfo(RectangleD Position, string Name) { - public XDotRect Pos { get; init; } - public string Name { get; init; } + internal ImageInfo ForCoordSystem(CoordinateSystem coordSystem, double maxY) => this with + { + Position = Position.ForCoordSystem(coordSystem, maxY), + }; } -public record struct XDotFont +/// +/// 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) { - /// - /// Size in points - /// - public double Size { get; init; } - public string Name { get; init; } - 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, @@ -167,75 +247,8 @@ public enum XDotFontChar Overline = 64, } -/// -/// See https://graphviz.org/docs/outputs/canon/#xdot for semantics -/// -public abstract record class XDotOp +internal static class PointDArrayExtension { - 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; } - } + internal static PointD[] ForCoordSystem(this PointD[] self, CoordinateSystem coordSystem, double maxY) + => self.Select(a => a.ForCoordSystem(coordSystem, maxY)).ToArray(); } 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 67cf39c..9007206 100644 --- a/Rubjerg.Graphviz/XDotParser.cs +++ b/Rubjerg.Graphviz/XDotParser.cs @@ -32,12 +32,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 { @@ -48,7 +48,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)); @@ -62,184 +62,145 @@ internal static List TranslateXDot(IntPtr xdotPtr) int count = xdot.Count; xdot.Ops = new XDotOp[count]; var opsPtr = XDotFFI.get_ops(xdotPtr); + + var activeFont = Font.Default; + var activeFontChar = FontChar.None; 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)) + .ForCoordSystem(coordinateSystem, maxY)); + break; + case XDotKind.UnfilledEllipse: + 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)) + .ForCoordSystem(coordinateSystem, maxY)); + break; + case XDotKind.UnfilledPolygon: + 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)) + .ForCoordSystem(coordinateSystem, maxY)); + break; + case XDotKind.UnfilledBezier: + 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)) + .ForCoordSystem(coordinateSystem, maxY)); + break; + case XDotKind.Text: + 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(new Color.Uniform(XDotFFI.GetColor(xdotOpPtr))); + break; + case XDotKind.PenColor: + xdot.Ops[i] = new XDotOp.PenColor(new Color.Uniform(XDotFFI.GetColor(xdotOpPtr))); + break; + case XDotKind.GradFillColor: + xdot.Ops[i] = new XDotOp.FillColor(TranslateGradColor(XDotFFI.get_grad_color(xdotOpPtr))); + break; + case XDotKind.GradPenColor: + xdot.Ops[i] = new XDotOp.PenColor(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)) + .ForCoordSystem(coordinateSystem, maxY)); + break; + case XDotKind.FontChar: + activeFontChar = TranslateFontChar(XDotFFI.get_fontchar(xdotOpPtr)); + break; + default: + throw new ArgumentException($"Unexpected XDotOp.Kind: {kind}"); + } } return xdot.Ops.ToList(); } - private static XDotOp TranslateXDotOp(IntPtr xdotOpPtr) + private static FontChar TranslateFontChar(uint value) { - 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}"); - } + return (FontChar)(int)value; } - private static XDotFontChar TranslateFontChar(uint value) + private static ImageInfo TranslateImage(IntPtr imagePtr) { - return (XDotFontChar)(int)value; - } - private static XDotImage TranslateImage(IntPtr imagePtr) - { - XDotImage image = new XDotImage - { - Pos = TranslateRect(XDotFFI.get_pos(imagePtr)), - Name = XDotFFI.GetNameImage(imagePtr) - }; + ImageInfo image = new ImageInfo + ( + Position: TranslateRect(XDotFFI.get_pos(imagePtr)), + Name: XDotFFI.GetNameImage(imagePtr) + ); return image; } - private static XDotFont TranslateFont(IntPtr fontPtr) + private static Font TranslateFont(IntPtr fontPtr) { - XDotFont font = new XDotFont - { - Size = XDotFFI.get_size(fontPtr), - Name = XDotFFI.GetNameFont(fontPtr) - }; + Font font = new Font + ( + 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 Color TranslateGradColor(IntPtr colorPtr) { var type = XDotFFI.get_type(colorPtr); switch (type) { case XDotGradType.None: - return new XDotGradColor.Uniform() - { - Color = XDotFFI.GetClr(colorPtr) - }; + return new Color.Uniform(XDotFFI.GetClr(colorPtr)); case XDotGradType.Linear: - return new XDotGradColor.LinearGradient() - { - LinearGrad = TranslateLinearGrad(XDotFFI.get_ling(colorPtr)) - }; + return new Color.Linear(TranslateLinearGrad(XDotFFI.get_ling(colorPtr))); case XDotGradType.Radial: - return new XDotGradColor.RadialGradient() - { - RadialGrad = TranslateRadialGrad(XDotFFI.get_ring(colorPtr)) - }; + return new Color.Radial(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 +213,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 +236,69 @@ 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), + HtmlColor: 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 TextInfo TranslateText(IntPtr txtPtr, Font activeFont, FontChar activeFontChar) { - 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) - }; + TextInfo text = new TextInfo + ( + new PointD(XDotFFI.get_x_text(txtPtr), XDotFFI.get_y_text(txtPtr)), + XDotFFI.get_align(txtPtr), + XDotFFI.get_width(txtPtr), + XDotFFI.GetTextStr(txtPtr), + activeFont, + activeFontChar, + CoordinateSystem.BottomLeft + ); return text; }