From 2b55e3e381ee7086bca15390eb1d46173ed3ab4d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 07:17:00 +0000 Subject: [PATCH 1/2] Move the GDI+ conversions of the geometry types into CADability.Forms (#412) The kernel's 2D curves, borders, shapes and ModOp2D converted themselves to GDI+ (GraphicsPath, Drawing2D.Matrix), which is Windows-only. The new static class CADability.Forms.GdiExtensions does these conversions with the public API of the kernel: - GraphicsPath.AddCurve2D(ICurve2D, forward) for ICurve2D.AddToGraphicsPath - GraphicsPath.AddBorder(Border, ccw) for Border.AddToGraphicsPath - GraphicsPath.AddShape(SimpleShape / CompoundShape) and CompoundShape.ToGraphicsPath() for CompoundShape.CreateGraphicsPath - ModOp2D.ToGdiMatrix() for ModOp2D.Matrix2D They are extensions of GraphicsPath and not of the curves, because an instance member of the same name would always win over an extension. The kernel members stay in 1.x, marked [Obsolete], and keep their own implementation: the kernel cannot call into CADability.Forms. Curve types the extensions do not know fall back to the obsolete member, so third-party curves keep working. PaintToGDI uses ToGdiMatrix. The unused internal ModOp.Matrix2D is removed. BSpline2D.AddToGraphicsPath passed knot parameters to PointAt, which expects 0..1, so a spline whose knots do not run from 0 to 1 was drawn from wrong points. Both the extension and the obsolete member now sample 0..1. The new tests compare the extensions with the obsolete members. They need GDI+ and therefore only run on Windows. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 --- CADability.Forms/GdiExtensions.cs | 184 +++++++++++++++++++ CADability.Forms/PaintToGDI.cs | 2 +- CADability/Arc2D.cs | 1 + CADability/BSpline2D.cs | 4 +- CADability/Bitmap.cs | 2 + CADability/Border.cs | 1 + CADability/Circle2D.cs | 1 + CADability/DualSurfaceCurve.cs | 1 + CADability/Ellipse2D.cs | 1 + CADability/EllipseArc2D.cs | 1 + CADability/GeneralCurve2D.cs | 2 + CADability/Line2D.cs | 1 + CADability/ModOp.cs | 8 +- CADability/Path2D.cs | 1 + CADability/Polyline2D.cs | 1 + CADability/PrintToGDI.cs | 2 + CADability/Shape.cs | 2 + tests/CADability.Tests/GdiExtensionsTests.cs | 116 ++++++++++++ 18 files changed, 321 insertions(+), 10 deletions(-) create mode 100644 CADability.Forms/GdiExtensions.cs create mode 100644 tests/CADability.Tests/GdiExtensionsTests.cs diff --git a/CADability.Forms/GdiExtensions.cs b/CADability.Forms/GdiExtensions.cs new file mode 100644 index 00000000..29fe5851 --- /dev/null +++ b/CADability.Forms/GdiExtensions.cs @@ -0,0 +1,184 @@ +using CADability.Curve2D; +using CADability.Shapes; +using System; +using System.Drawing; +using System.Drawing.Drawing2D; + +namespace CADability.Forms +{ + /// + /// Converts CADability geometry to GDI+ (, ). These conversions used to be + /// members of the geometry types in the kernel (, , + /// , ). GDI+ is only available on Windows, so they + /// live here, next to the other WinForms code, and the kernel stays platform independent (issue #412). + /// + public static class GdiExtensions + { + /// + /// Appends to . Lines, arcs and circles are added exactly, ellipses and + /// B-splines as an approximation through points on the curve. + /// + /// The graphics path to append to + /// The curve to add + /// true: add in the direction of the curve, false: add reversed + public static void AddCurve2D(this GraphicsPath path, ICurve2D curve, bool forward) + { + // Exact type checks: a derived class may have overridden the obsolete ICurve2D.AddToGraphicsPath. + Type type = curve.GetType(); + if (type == typeof(Line2D)) AddLine(path, (Line2D)curve, forward); + else if (type == typeof(Arc2D)) AddArc(path, (Arc2D)curve, forward); + else if (type == typeof(Circle2D)) AddCircle(path, (Circle2D)curve, forward); + else if (type == typeof(EllipseArc2D)) AddEllipseArc(path, (EllipseArc2D)curve, forward); + else if (type == typeof(Ellipse2D)) AddEllipse(path, (Ellipse2D)curve, forward); + else if (type == typeof(Polyline2D)) AddPolyline(path, (Polyline2D)curve, forward); + else if (type == typeof(BSpline2D)) AddBSpline(path, (BSpline2D)curve, forward); + else if (type == typeof(Path2D)) AddPath(path, (Path2D)curve, forward); + else + { + // Other curves keep their own implementation as long as the kernel has it (1.x). +#pragma warning disable CS0618 // Type or member is obsolete + curve.AddToGraphicsPath(path, forward); +#pragma warning restore CS0618 + } + } + /// + /// Appends the segments of to , e.g. for filling or clipping with + /// System.Drawing. With false the segments are added in reverse order and direction, + /// which is how adds the holes so that they stay unfilled. + /// + /// The graphics path to append to + /// The border to add + /// true: add in the direction of the border, false: add reversed + public static void AddBorder(this GraphicsPath path, Border border, bool counterClockWise) + { + if (counterClockWise) + { + for (int i = 0; i < border.Count; ++i) path.AddCurve2D(border[i], true); + } + else + { + for (int i = border.Count - 1; i >= 0; --i) path.AddCurve2D(border[i], false); + } + } + /// + /// Appends to : the outline counterclockwise and the holes clockwise, so + /// filling the path leaves the holes empty. + /// + /// The graphics path to append to + /// The shape to add + public static void AddShape(this GraphicsPath path, SimpleShape shape) + { + path.AddBorder(shape.Outline, true); + Border[] holes = shape.Holes; + for (int i = 0; i < holes.Length; ++i) path.AddBorder(holes[i], false); + } + /// + /// Appends all parts of to , see . + /// + /// The graphics path to append to + /// The shape to add + public static void AddShape(this GraphicsPath path, CompoundShape shape) + { + SimpleShape[] simpleShapes = shape.SimpleShapes; + for (int i = 0; i < simpleShapes.Length; ++i) path.AddShape(simpleShapes[i]); + } + /// + /// Converts to a new for drawing or clipping with System.Drawing, + /// see . + /// + /// The shape to convert + /// The graphics path + public static GraphicsPath ToGraphicsPath(this CompoundShape shape) + { + GraphicsPath res = new GraphicsPath(); + res.AddShape(shape); + return res; + } + /// + /// Converts to a GDI+ with the same coefficients (in single precision), e.g. to + /// apply it when drawing with GDI+. + /// + /// The modification to convert + /// The equivalent matrix + public static Matrix ToGdiMatrix(this ModOp2D m) + { + return new Matrix((float)m[0, 0], (float)m[1, 0], (float)m[0, 1], (float)m[1, 1], (float)m[0, 2], (float)m[1, 2]); + } + + private static void AddLine(GraphicsPath path, Line2D line, bool forward) + { + GeoPoint2D sp = forward ? line.StartPoint : line.EndPoint; + GeoPoint2D ep = forward ? line.EndPoint : line.StartPoint; + path.AddLine((float)sp.x, (float)sp.y, (float)ep.x, (float)ep.y); + } + private static void AddArc(GraphicsPath path, Arc2D arc, bool forward) + { + Angle start = arc.StartParameter; + SweepAngle sweep = arc.SweepAngle; + float startDegree = (float)(forward ? start.Degree : (start + sweep).Degree); + float sweepDegree = (float)(forward ? sweep.Degree : -sweep.Degree); + path.AddArc((float)(arc.Center.x - arc.Radius), (float)(arc.Center.y - arc.Radius), (float)(2 * arc.Radius), (float)(2 * arc.Radius), + startDegree, sweepDegree); + } + private static void AddCircle(GraphicsPath path, Circle2D circle, bool forward) + { + // AddArc and not AddEllipse, so that the direction is kept, which matters for FillMode.Winding + path.AddArc((float)(circle.Center.x - circle.Radius), (float)(circle.Center.y - circle.Radius), (float)(2 * circle.Radius), (float)(2 * circle.Radius), + 0.0f, forward ? 360.0f : -360.0f); + } + private static void AddEllipse(GraphicsPath path, Ellipse2D ellipse, bool forward) + { + AddCurveThroughPoints(path, ellipse, 36, forward); + } + private static void AddEllipseArc(GraphicsPath path, EllipseArc2D ellipseArc, bool forward) + { + // about one point every 10° + int n = (int)Math.Round(Math.Abs(ellipseArc.axisSweep) / Math.PI * 18); + AddCurveThroughPoints(path, ellipseArc, Math.Max(n, 2), forward); + } + private static void AddCurveThroughPoints(GraphicsPath path, ICurve2D curve, int n, bool forward) + { + PointF[] points = new PointF[n + 1]; + double step = 1.0 / n; + for (int i = 0; i < n; ++i) points[i] = curve.PointAt(i * step).PointF; + points[n] = curve.PointAt(1.0).PointF; + if (!forward) Array.Reverse(points); + path.AddCurve(points); + } + private static void AddPolyline(GraphicsPath path, Polyline2D polyline, bool forward) + { + GeoPoint2D[] vertex = polyline.Vertex; + if (forward) + { + for (int i = 1; i < vertex.Length; ++i) + path.AddLine((float)vertex[i - 1].x, (float)vertex[i - 1].y, (float)vertex[i].x, (float)vertex[i].y); + } + else + { + for (int i = vertex.Length - 1; i > 0; --i) + path.AddLine((float)vertex[i].x, (float)vertex[i].y, (float)vertex[i - 1].x, (float)vertex[i - 1].y); + } + } + private static void AddBSpline(GraphicsPath path, BSpline2D bspline, bool forward) + { + // a polyline through points on the curve, four per pole + int n = bspline.Poles.Length * 4 + 2; + PointF[] points = new PointF[n]; + for (int i = 0; i < n; ++i) points[i] = bspline.PointAt((double)i / (n - 1)).PointF; + if (!forward) Array.Reverse(points); + path.AddLines(points); + } + private static void AddPath(GraphicsPath path, Path2D path2D, bool forward) + { + ICurve2D[] subCurves = path2D.SubCurves; + if (forward) + { + for (int i = 0; i < subCurves.Length; ++i) path.AddCurve2D(subCurves[i], true); + } + else + { + for (int i = subCurves.Length - 1; i >= 0; --i) path.AddCurve2D(subCurves[i], false); + } + } + } +} diff --git a/CADability.Forms/PaintToGDI.cs b/CADability.Forms/PaintToGDI.cs index 6f16beaa..3d093649 100644 --- a/CADability.Forms/PaintToGDI.cs +++ b/CADability.Forms/PaintToGDI.cs @@ -866,7 +866,7 @@ void IPaintTo3D.Arc(GeoPoint center, GeoVector majorAxis, GeoVector minorAxis, d } try { - Matrix r = fromHorizontal.Matrix2D; + Matrix r = fromHorizontal.ToGdiMatrix(); double maxRad = prmaj2D.Length; double minRad = prmin2D.Length; if ((maxRad + minRad) * Projection.WorldToDeviceFactor < 1) diff --git a/CADability/Arc2D.cs b/CADability/Arc2D.cs index 1584b61e..77957211 100644 --- a/CADability/Arc2D.cs +++ b/CADability/Arc2D.cs @@ -621,6 +621,7 @@ public override double MinDistance(ICurve2D Other) return base.MinDistance(Other); // dort wird gleichermaßen für Bogen und Kreis gerechnet } /// + [Obsolete("Use the GraphicsPath extension AddCurve2D of CADability.Forms.GdiExtensions instead. GDI+ leaves the kernel in CADability 2.0, see issue #412.")] public override void AddToGraphicsPath(System.Drawing.Drawing2D.GraphicsPath path, bool forward) { if (forward) diff --git a/CADability/BSpline2D.cs b/CADability/BSpline2D.cs index 010a2975..61bf8b44 100644 --- a/CADability/BSpline2D.cs +++ b/CADability/BSpline2D.cs @@ -1663,15 +1663,15 @@ public override double Sweep } } /// + [Obsolete("Use the GraphicsPath extension AddCurve2D of CADability.Forms.GdiExtensions instead. GDI+ leaves the kernel in CADability 2.0, see issue #412.")] public override void AddToGraphicsPath(System.Drawing.Drawing2D.GraphicsPath path, bool forward) { // Notfalls Darstellung als Polyline int l = poles.Length * 4 + 2; PointF[] ppf = new PointF[l]; - double du = (endParam - startParam) / (l - 1); for (int i = 0; i < l; ++i) { - ppf[i] = PointAt(startParam + i * du).PointF; + ppf[i] = PointAt((double)i / (l - 1)).PointF; // PointAt expects 0..1, not the knot parameter } if (!forward) Array.Reverse(ppf); path.AddLines(ppf); diff --git a/CADability/Bitmap.cs b/CADability/Bitmap.cs index 7f922863..9f8f00a6 100644 --- a/CADability/Bitmap.cs +++ b/CADability/Bitmap.cs @@ -223,7 +223,9 @@ public void Clip(Plane plane, CompoundShape shape) prsh = prsh.GetModified(m); m = ModOp2D.Translate(0, bitmap.Height); prsh = prsh.GetModified(m); +#pragma warning disable CS0618 // Picture moves away from GDI+ in issue #412, step 7 GraphicsPath gp = prsh.CreateGraphicsPath(); +#pragma warning restore CS0618 using (new Changing(this)) { Region rg = new Region(gp); diff --git a/CADability/Border.cs b/CADability/Border.cs index 55fd8307..b8d581f8 100644 --- a/CADability/Border.cs +++ b/CADability/Border.cs @@ -3904,6 +3904,7 @@ internal void Trimm(int firstIndex, int lastIndex) /// /// The graphics path to append to /// true: add in the direction of the border, false: add reversed + [Obsolete("Use the GraphicsPath extension AddBorder of CADability.Forms.GdiExtensions instead. GDI+ leaves the kernel in CADability 2.0, see issue #412.")] public void AddToGraphicsPath(System.Drawing.Drawing2D.GraphicsPath path, bool counterClockWise) { if (counterClockWise) diff --git a/CADability/Circle2D.cs b/CADability/Circle2D.cs index 3a29185b..f88c86ae 100644 --- a/CADability/Circle2D.cs +++ b/CADability/Circle2D.cs @@ -496,6 +496,7 @@ public override ICurve2D Project(Plane fromPlane, Plane toPlane) return e.GetProjectedCurve(toPlane); } /// + [Obsolete("Use the GraphicsPath extension AddCurve2D of CADability.Forms.GdiExtensions instead. GDI+ leaves the kernel in CADability 2.0, see issue #412.")] public override void AddToGraphicsPath(System.Drawing.Drawing2D.GraphicsPath path, bool forward) { // forward oder nicht ist doch egal, oder? // im Modus "System.Drawing.Drawing2D.FillMode.Alternate" scheint es egal zu sein, diff --git a/CADability/DualSurfaceCurve.cs b/CADability/DualSurfaceCurve.cs index ecc37e02..6e825c8b 100644 --- a/CADability/DualSurfaceCurve.cs +++ b/CADability/DualSurfaceCurve.cs @@ -499,6 +499,7 @@ ICurve2D ICurve2D.Project(Plane fromPlane, Plane toPlane) { return theCurve.Project(fromPlane, toPlane); } + [Obsolete("Use the GraphicsPath extension AddCurve2D of CADability.Forms.GdiExtensions instead. GDI+ leaves the kernel in CADability 2.0, see issue #412.")] void ICurve2D.AddToGraphicsPath(System.Drawing.Drawing2D.GraphicsPath path, bool forward) { theCurve.AddToGraphicsPath(path, forward); diff --git a/CADability/Ellipse2D.cs b/CADability/Ellipse2D.cs index d4c623c4..1623c26c 100644 --- a/CADability/Ellipse2D.cs +++ b/CADability/Ellipse2D.cs @@ -807,6 +807,7 @@ public override void Move(double dx, double dy) Recalc(); } /// + [Obsolete("Use the GraphicsPath extension AddCurve2D of CADability.Forms.GdiExtensions instead. GDI+ leaves the kernel in CADability 2.0, see issue #412.")] public override void AddToGraphicsPath(System.Drawing.Drawing2D.GraphicsPath path, bool forward) { // aus dem Bogen übernommen, evtl besser in Kreisbögen zerlegen... diff --git a/CADability/EllipseArc2D.cs b/CADability/EllipseArc2D.cs index ce132918..a17b33f0 100644 --- a/CADability/EllipseArc2D.cs +++ b/CADability/EllipseArc2D.cs @@ -598,6 +598,7 @@ public override double MinDistance(GeoPoint2D p) return res; } /// + [Obsolete("Use the GraphicsPath extension AddCurve2D of CADability.Forms.GdiExtensions instead. GDI+ leaves the kernel in CADability 2.0, see issue #412.")] public override void AddToGraphicsPath(System.Drawing.Drawing2D.GraphicsPath path, bool forward) { // es wird ein Spline erzeugt in 10° Schritten // hier sollte man überlegen, ob man nicht mit einer Kreisbogenannäherung diff --git a/CADability/GeneralCurve2D.cs b/CADability/GeneralCurve2D.cs index 55060752..37180954 100644 --- a/CADability/GeneralCurve2D.cs +++ b/CADability/GeneralCurve2D.cs @@ -416,6 +416,7 @@ public interface ICurve2D : IQuadTreeInsertable /// the path to append to /// true to append the curve in its own direction, false to append it reversed (from the end point to /// the start point) + [Obsolete("Use the GraphicsPath extension AddCurve2D of CADability.Forms.GdiExtensions instead. GDI+ leaves the kernel in CADability 2.0, see issue #412.")] void AddToGraphicsPath(System.Drawing.Drawing2D.GraphicsPath path, bool forward); /// /// Determines, whether the given parameter denotes a point inside the bounds of the curve. @@ -2436,6 +2437,7 @@ public virtual ICurve2D Project(Plane fromPlane, Plane toPlane) } /// + [Obsolete("Use the GraphicsPath extension AddCurve2D of CADability.Forms.GdiExtensions instead. GDI+ leaves the kernel in CADability 2.0, see issue #412.")] public virtual void AddToGraphicsPath(System.Drawing.Drawing2D.GraphicsPath path, bool forward) { throw new Exception("The method or operation is not implemented."); diff --git a/CADability/Line2D.cs b/CADability/Line2D.cs index 15a419c9..f536299c 100644 --- a/CADability/Line2D.cs +++ b/CADability/Line2D.cs @@ -485,6 +485,7 @@ public override ICurve2D Project(Plane fromPlane, Plane toPlane) else return null; } /// + [Obsolete("Use the GraphicsPath extension AddCurve2D of CADability.Forms.GdiExtensions instead. GDI+ leaves the kernel in CADability 2.0, see issue #412.")] public override void AddToGraphicsPath(System.Drawing.Drawing2D.GraphicsPath path, bool forward) { if (forward) diff --git a/CADability/ModOp.cs b/CADability/ModOp.cs index b034b77e..b9719516 100644 --- a/CADability/ModOp.cs +++ b/CADability/ModOp.cs @@ -657,6 +657,7 @@ public static ModOp2D PlaneToplane(Plane FromPlane, Plane ToPlane) /// Returns the object equivalent to this /// modification. /// + [Obsolete("Use the extension ToGdiMatrix of CADability.Forms.GdiExtensions instead. GDI+ leaves the kernel in CADability 2.0, see issue #412.")] public System.Drawing.Drawing2D.Matrix Matrix2D { get @@ -1982,13 +1983,6 @@ public ModOp GetInverse() throw new ModOpException(ModOpException.tExceptionType.InversionFailed); } } - internal System.Drawing.Drawing2D.Matrix Matrix2D - { - get - { - return new System.Drawing.Drawing2D.Matrix((float)Matrix00, (float)Matrix01, (float)Matrix10, (float)Matrix11, (float)Matrix12, (float)Matrix02); - } - } /// /// Returns the determinant of the matrix of this modification. /// diff --git a/CADability/Path2D.cs b/CADability/Path2D.cs index 7f51fcd5..9b610b6e 100644 --- a/CADability/Path2D.cs +++ b/CADability/Path2D.cs @@ -983,6 +983,7 @@ public override ICurve2D Project(Plane fromPlane, Plane toPlane) return new Path2D((ICurve2D[])(curves.ToArray(typeof(ICurve2D)))); } /// + [Obsolete("Use the GraphicsPath extension AddCurve2D of CADability.Forms.GdiExtensions instead. GDI+ leaves the kernel in CADability 2.0, see issue #412.")] public override void AddToGraphicsPath(System.Drawing.Drawing2D.GraphicsPath path, bool forward) { if (forward) diff --git a/CADability/Polyline2D.cs b/CADability/Polyline2D.cs index 630b99ac..c95353a4 100644 --- a/CADability/Polyline2D.cs +++ b/CADability/Polyline2D.cs @@ -217,6 +217,7 @@ public override void Copy(ICurve2D toCopyFrom) UserData.CloneFrom(c.UserData); } /// + [Obsolete("Use the GraphicsPath extension AddCurve2D of CADability.Forms.GdiExtensions instead. GDI+ leaves the kernel in CADability 2.0, see issue #412.")] public override void AddToGraphicsPath(System.Drawing.Drawing2D.GraphicsPath path, bool forward) { if (forward) diff --git a/CADability/PrintToGDI.cs b/CADability/PrintToGDI.cs index 4e9e9b17..59df9d31 100644 --- a/CADability/PrintToGDI.cs +++ b/CADability/PrintToGDI.cs @@ -2732,7 +2732,9 @@ void IPaintTo3D.Arc(GeoPoint center, GeoVector majorAxis, GeoVector minorAxis, d float stPar = (float)(ea2d.axisStart / Math.PI * 180); try { +#pragma warning disable CS0618 // PrintToGDI moves to CADability.Forms with the printing (issue #412, step 7) Matrix r = fromHorizontal.Matrix2D; +#pragma warning restore CS0618 double maxRad = prmaj2D.Length; double minRad = prmin2D.Length; if (graphicsPath != null) diff --git a/CADability/Shape.cs b/CADability/Shape.cs index 6f3cb138..45b586a7 100644 --- a/CADability/Shape.cs +++ b/CADability/Shape.cs @@ -1054,6 +1054,7 @@ public SimpleShape GetModified(ModOp2D m) } return new SimpleShape(o, h); } + [Obsolete("Use the GraphicsPath extension AddShape of CADability.Forms.GdiExtensions instead.")] internal void AddToGraphicsPath(System.Drawing.Drawing2D.GraphicsPath path) { outline.AddToGraphicsPath(path, true); @@ -3160,6 +3161,7 @@ public bool Contains(GeoPoint2D p, bool acceptOnCurve) /// holes empty. /// /// The graphics path + [Obsolete("Use the extension ToGraphicsPath of CADability.Forms.GdiExtensions instead. GDI+ leaves the kernel in CADability 2.0, see issue #412.")] public System.Drawing.Drawing2D.GraphicsPath CreateGraphicsPath() { System.Drawing.Drawing2D.GraphicsPath res = new System.Drawing.Drawing2D.GraphicsPath(); diff --git a/tests/CADability.Tests/GdiExtensionsTests.cs b/tests/CADability.Tests/GdiExtensionsTests.cs new file mode 100644 index 00000000..bcdd84fb --- /dev/null +++ b/tests/CADability.Tests/GdiExtensionsTests.cs @@ -0,0 +1,116 @@ +#if WINDOWS +using CADability.Curve2D; +using CADability.Forms; +using CADability.Shapes; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Drawing; +using System.Drawing.Drawing2D; + +namespace CADability.Tests +{ + /// + /// The GDI+ conversions moved from the geometry types of the kernel into in CADability.Forms + /// (issue #412, step 5). The obsolete kernel members stay in 1.x; the extensions must produce the same paths. + /// GDI+ only works on Windows, so these tests only run there. + /// + [TestClass] + public class GdiExtensionsTests + { +#pragma warning disable CS0618 // the obsolete kernel members are the reference + private static GraphicsPath Old(ICurve2D curve, bool forward) + { + GraphicsPath res = new GraphicsPath(); + curve.AddToGraphicsPath(res, forward); + return res; + } +#pragma warning restore CS0618 + private static GraphicsPath New(ICurve2D curve, bool forward) + { + GraphicsPath res = new GraphicsPath(); + res.AddCurve2D(curve, forward); + return res; + } + private static void AssertSamePath(GraphicsPath expected, GraphicsPath actual, string what) + { + CollectionAssert.AreEqual(expected.PathTypes, actual.PathTypes, what + ": path types"); + CollectionAssert.AreEqual(expected.PathPoints, actual.PathPoints, what + ": path points"); + } + private static ICurve2D[] Curves() + { + return new ICurve2D[] + { + new Line2D(new GeoPoint2D(1, 2), new GeoPoint2D(7, -3)), + new Arc2D(new GeoPoint2D(2, 1), 3, Angle.Deg(30), SweepAngle.Deg(120)), + new Arc2D(new GeoPoint2D(2, 1), 3, Angle.Deg(300), SweepAngle.Deg(-200)), + new Circle2D(new GeoPoint2D(-1, 4), 2.5), + new Ellipse2D(new GeoPoint2D(0, 0), new GeoVector2D(4, 1), new GeoVector2D(-0.5, 2)), + new Polyline2D(new[] { new GeoPoint2D(0, 0), new GeoPoint2D(3, 1), new GeoPoint2D(4, 5), new GeoPoint2D(-1, 2) }), + new Path2D(new ICurve2D[] { new Line2D(new GeoPoint2D(0, 0), new GeoPoint2D(4, 0)), new Arc2D(new GeoPoint2D(4, 2), 2, Angle.Deg(270), SweepAngle.Deg(180)) }), + // knots from 0 to 1: the only case in which the old implementation sampled the right parameters + new BSpline2D(new[] { new GeoPoint2D(0, 0), new GeoPoint2D(1, 3), new GeoPoint2D(4, 3), new GeoPoint2D(5, 0) }, null, new double[] { 0, 1 }, new[] { 4, 4 }, 3, false, 0, 1), + }; + } + + [TestMethod] + public void CurvesGiveTheSamePathAsTheObsoleteMembers() + { + foreach (ICurve2D curve in Curves()) + { + AssertSamePath(Old(curve, true), New(curve, true), curve.GetType().Name + " forward"); + AssertSamePath(Old(curve, false), New(curve, false), curve.GetType().Name + " reversed"); + } + } + + [TestMethod] + public void EllipseArcStartsAndEndsLikeTheObsoleteMember() + { + // the number of points comes from the polar sweep here, the obsolete member uses the parametric sweep + EllipseArc2D arc = EllipseArc2D.Create(new GeoPoint2D(1, 1), new GeoVector2D(5, 0), new GeoVector2D(0, 2), new GeoPoint2D(6, 1), new GeoPoint2D(1, 3), true); + foreach (bool forward in new[] { true, false }) + { + PointF[] expected = Old(arc, forward).PathPoints, actual = New(arc, forward).PathPoints; + Assert.AreEqual(expected[0], actual[0]); + Assert.AreEqual(expected[expected.Length - 1], actual[actual.Length - 1]); + } + } + + [TestMethod] + public void BSplineWithKnotsBeyondOneIsSampledOnTheCurve() + { + BSpline2D bsp = new BSpline2D(new[] { new GeoPoint2D(0, 0), new GeoPoint2D(1, 3), new GeoPoint2D(4, 3), new GeoPoint2D(5, 0) }, null, new double[] { 0, 5 }, new[] { 4, 4 }, 3, false, 0, 5); + PointF[] points = New(bsp, true).PathPoints; + Assert.AreEqual(bsp.StartPoint.PointF, points[0]); + Assert.AreEqual(bsp.EndPoint.PointF, points[points.Length - 1]); + foreach (PointF p in points) Assert.AreEqual(0.0, bsp.Distance(new GeoPoint2D(p.X, p.Y)), 1e-4); + AssertSamePath(Old(bsp, true), New(bsp, true), "the obsolete member is fixed as well"); + } + + [TestMethod] + public void ShapesGiveTheSamePathAsTheObsoleteMembers() + { + Border outline = new Border(new ICurve2D[] { new Line2D(new GeoPoint2D(0, 0), new GeoPoint2D(10, 0)), new Line2D(new GeoPoint2D(10, 0), new GeoPoint2D(10, 8)), + new Line2D(new GeoPoint2D(10, 8), new GeoPoint2D(0, 8)), new Line2D(new GeoPoint2D(0, 8), new GeoPoint2D(0, 0)) }); + Border hole = new Border(new ICurve2D[] { new Circle2D(new GeoPoint2D(5, 4), 2) }); + CompoundShape shape = new CompoundShape(new SimpleShape(outline, hole)); +#pragma warning disable CS0618 + GraphicsPath oldBorder = new GraphicsPath(); + outline.AddToGraphicsPath(oldBorder, false); + GraphicsPath oldShape = shape.CreateGraphicsPath(); +#pragma warning restore CS0618 + GraphicsPath newBorder = new GraphicsPath(); + newBorder.AddBorder(outline, false); + AssertSamePath(oldBorder, newBorder, "border"); + AssertSamePath(oldShape, shape.ToGraphicsPath(), "compound shape"); + } + + [TestMethod] + public void ModOp2DGivesTheSameMatrixAsTheObsoleteMember() + { + ModOp2D m = ModOp2D.Rotate(new GeoPoint2D(3, -2), SweepAngle.Deg(37)) * ModOp2D.Scale(2, 0.5); +#pragma warning disable CS0618 + CollectionAssert.AreEqual(m.Matrix2D.Elements, m.ToGdiMatrix().Elements); +#pragma warning restore CS0618 + } + } +} +#endif From 6fe915892ec9799b37ab015882414e95a764fb03 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 08:53:42 +0000 Subject: [PATCH 2/2] Check the sampled B-spline points with MinDistance in GdiExtensionsTests The first run of the Windows CI showed that the test compared the path points with BSpline2D.Distance, which returns double.MaxValue for them. MinDistance gives the distance to the curve (about 1e-7 for the sampled points, checked on Linux without GDI+). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 --- tests/CADability.Tests/GdiExtensionsTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/CADability.Tests/GdiExtensionsTests.cs b/tests/CADability.Tests/GdiExtensionsTests.cs index bcdd84fb..4b1e7ef4 100644 --- a/tests/CADability.Tests/GdiExtensionsTests.cs +++ b/tests/CADability.Tests/GdiExtensionsTests.cs @@ -81,7 +81,8 @@ public void BSplineWithKnotsBeyondOneIsSampledOnTheCurve() PointF[] points = New(bsp, true).PathPoints; Assert.AreEqual(bsp.StartPoint.PointF, points[0]); Assert.AreEqual(bsp.EndPoint.PointF, points[points.Length - 1]); - foreach (PointF p in points) Assert.AreEqual(0.0, bsp.Distance(new GeoPoint2D(p.X, p.Y)), 1e-4); + // MinDistance and not Distance: BSpline2D.Distance returns double.MaxValue for these points + foreach (PointF p in points) Assert.AreEqual(0.0, bsp.MinDistance(new GeoPoint2D(p.X, p.Y)), 1e-4); AssertSamePath(Old(bsp, true), New(bsp, true), "the obsolete member is fixed as well"); }