diff --git a/src/UglyToad.PdfPig.Core/PdfSubpath.cs b/src/UglyToad.PdfPig.Core/PdfSubpath.cs index 32841be5..82fbe5b8 100644 --- a/src/UglyToad.PdfPig.Core/PdfSubpath.cs +++ b/src/UglyToad.PdfPig.Core/PdfSubpath.cs @@ -18,7 +18,8 @@ public IReadOnlyList Commands => commands; /// - /// True if the was originaly draw as a rectangle. + /// True if the was originaly drawn using the rectangle ('re') operator. + /// Always false if paths are clipped. /// public bool IsDrawnAsRectangle { get; internal set; } @@ -34,7 +35,6 @@ /// /// Return true if points are organised in a counterclockwise order. Works only with closed paths. /// - /// public bool IsCounterClockwise => IsClosed() && shoeLaceSum < 0; /// diff --git a/src/UglyToad.PdfPig/Geometry/ClippingExtensions.cs b/src/UglyToad.PdfPig/Geometry/ClippingExtensions.cs index 7e8221db..12d839ad 100644 --- a/src/UglyToad.PdfPig/Geometry/ClippingExtensions.cs +++ b/src/UglyToad.PdfPig/Geometry/ClippingExtensions.cs @@ -157,7 +157,7 @@ /// Converts a path to a set of points for the Clipper algorithm to use. /// Allows duplicate points as they will be removed by Clipper. /// - private static IEnumerable ToClipperPolygon(this PdfSubpath pdfPath) + internal static IEnumerable ToClipperPolygon(this PdfSubpath pdfPath) { if (pdfPath.Commands.Count == 0) { @@ -166,7 +166,7 @@ if (pdfPath.Commands[0] is Move currentMove) { - var previous = new ClipperIntPoint(currentMove.Location.X * Factor, currentMove.Location.Y * Factor); + var previous = currentMove.Location.ToClipperIntPoint(); yield return previous; @@ -190,22 +190,35 @@ if (command is Line line) { - yield return new ClipperIntPoint(line.From.X * Factor, line.From.Y * Factor); - yield return new ClipperIntPoint(line.To.X * Factor, line.To.Y * Factor); + yield return line.From.ToClipperIntPoint(); + yield return line.To.ToClipperIntPoint(); } else if (command is BezierCurve curve) { foreach (var lineB in curve.ToLines(LinesInCurve)) { - yield return new ClipperIntPoint(lineB.From.X * Factor, lineB.From.Y * Factor); - yield return new ClipperIntPoint(lineB.To.X * Factor, lineB.To.Y * Factor); + yield return lineB.From.ToClipperIntPoint(); + yield return lineB.To.ToClipperIntPoint(); } } else if (command is Close) { - yield return new ClipperIntPoint(currentMove.Location.X * Factor, currentMove.Location.Y * Factor); + yield return currentMove.Location.ToClipperIntPoint(); } } } + + internal static IEnumerable ToClipperPolygon(this PdfRectangle rectangle) + { + yield return rectangle.BottomLeft.ToClipperIntPoint(); + yield return rectangle.TopLeft.ToClipperIntPoint(); + yield return rectangle.TopRight.ToClipperIntPoint(); + yield return rectangle.BottomRight.ToClipperIntPoint(); + } + + internal static ClipperIntPoint ToClipperIntPoint(this PdfPoint point) + { + return new ClipperIntPoint(point.X * Factor, point.Y * Factor); + } } } diff --git a/src/UglyToad.PdfPig/Geometry/GeometryExtensions.cs b/src/UglyToad.PdfPig/Geometry/GeometryExtensions.cs index 1538d9f6..42c081a7 100644 --- a/src/UglyToad.PdfPig/Geometry/GeometryExtensions.cs +++ b/src/UglyToad.PdfPig/Geometry/GeometryExtensions.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using System.Linq; using System.Text; + using UglyToad.PdfPig.Geometry.ClipperLibrary; + using UglyToad.PdfPig.Graphics; using static UglyToad.PdfPig.Core.PdfSubpath; /// @@ -454,6 +456,7 @@ /// /// Gets the that is the intersection of two rectangles. + /// Only works for axis-aligned rectangles. /// public static PdfRectangle? Intersect(this PdfRectangle rectangle, PdfRectangle other) { @@ -815,6 +818,249 @@ } #endregion + #region PdfPath & PdfSubpath + #region Clipper extension + // https://stackoverflow.com/questions/54723622/point-in-polygon-hit-test-algorithm + // Ported from Angus Johnson's Delphi Pascal code (Clipper's author) + // Might be made available in the next Clipper release? + + private static double CrossProduct(ClipperIntPoint pt1, ClipperIntPoint pt2, ClipperIntPoint pt3) + { + return (pt2.X - pt1.X) * (pt3.Y - pt2.Y) - (pt2.Y - pt1.Y) * (pt3.X - pt2.X); + } + + private static int PointInPathsWindingCount(ClipperIntPoint pt, List> paths) + { + int i, j, len; + List p; + ClipperIntPoint prevPt; + bool isAbove; + double crossProd; + + //nb: returns MaxInt ((2^32)-1) when pt is on a line + + var Result = 0; // /!\ + for (i = 0; i < paths.Count; i++) + { + j = 0; + p = paths[i]; + len = p.Count; + if (len < 3) continue; + prevPt = p[len - 1]; + while ((j < len) && (p[j].Y == prevPt.Y)) j++; + if (j == len) continue; + isAbove = (prevPt.Y < pt.Y); + while (j < len) + { + if (isAbove) + { + while ((j < len) && (p[j].Y < pt.Y)) j++; + if (j == len) + { + break; + } + else if (j > 0) + { + prevPt = p[j - 1]; + } + crossProd = CrossProduct(prevPt, p[j], pt); + if (crossProd == 0) + { + return int.MaxValue; + //result:= MaxInt; + //Exit; + } + else if (crossProd < 0) + { + Result--; + } + } + else + { + while ((j < len) && (p[j].Y > pt.Y)) j++; + if (j == len) + { + break; + } + else if (j > 0) + { + prevPt = p[j - 1]; + } + crossProd = CrossProduct(prevPt, p[j], pt); + if (crossProd == 0) + { + return int.MaxValue; + //result:= MaxInt; + //Exit; + } + else if (crossProd > 0) + { + Result++; + } + } + j++; + isAbove = !isAbove; + } + } + return Result; + } + + private static bool PointInPaths(ClipperIntPoint pt, List> paths, ClipperPolyFillType fillRule, bool includeBorder) + { + int wc = PointInPathsWindingCount(pt, paths); + if (wc == int.MaxValue) + { + return includeBorder; + } + + switch (fillRule) + { + case ClipperPolyFillType.EvenOdd: + return wc % 2 != 0; // Odd() + + case ClipperPolyFillType.NonZero: + default: + return wc != 0; + } + } + #endregion + + /// + /// Whether the subpath contains the point. + /// Ignores winding rule. + /// + /// The subpath that should contain the point. + /// The point that should be contained within the subpath. + /// If set to false, will return false if the point belongs to the border. + public static bool Contains(this PdfSubpath subpath, PdfPoint point, bool includeBorder = false) + { + return PointInPaths(point.ToClipperIntPoint(), + new List>() { subpath.ToClipperPolygon().ToList() }, + ClipperPolyFillType.EvenOdd, + includeBorder); + } + + /// + /// Whether the subpath contains the rectangle. + /// Ignores winding rule. + /// + /// The subpath that should contain the rectangle. + /// The rectangle that should be contained within the subpath. + /// [Not used for the moment] If set to false, will return false if the point belongs to the border. + public static bool Contains(this PdfSubpath subpath, PdfRectangle rectangle, bool includeBorder = false) + { + var clipperPaths = new List>() { subpath.ToClipperPolygon().ToList() }; + if (!PointInPaths(rectangle.BottomLeft.ToClipperIntPoint(), clipperPaths, ClipperPolyFillType.EvenOdd, includeBorder)) return false; + if (!PointInPaths(rectangle.TopLeft.ToClipperIntPoint(), clipperPaths, ClipperPolyFillType.EvenOdd, includeBorder)) return false; + if (!PointInPaths(rectangle.TopRight.ToClipperIntPoint(), clipperPaths, ClipperPolyFillType.EvenOdd, includeBorder)) return false; + if (!PointInPaths(rectangle.BottomRight.ToClipperIntPoint(), clipperPaths, ClipperPolyFillType.EvenOdd, includeBorder)) return false; + return true; + } + + /// + /// Whether the subpath contains the other subpath. + /// Ignores winding rule. + /// + /// The subpath that should contain the rectangle. + /// The other subpath that should be contained within the subpath. + /// [Not used for the moment] If set to false, will return false if the point belongs to the border. + public static bool Contains(this PdfSubpath subpath, PdfSubpath other, bool includeBorder = false) + { + var clipperPaths = new List>() { subpath.ToClipperPolygon().ToList() }; + foreach (var pt in other.ToClipperPolygon()) + { + if (!PointInPaths(pt, clipperPaths, ClipperPolyFillType.EvenOdd, includeBorder)) return false; + } + return true; + } + + /// + /// Get the area of the path. + /// + /// + public static double GetArea(this PdfPath path) + { + var clipperPaths = path.Select(sp => sp.ToClipperPolygon().ToList()).ToList(); + var simplifieds = Clipper.SimplifyPolygons(clipperPaths, path.FillingRule == FillingRule.NonZeroWinding ? ClipperPolyFillType.NonZero : ClipperPolyFillType.EvenOdd); + double sum = 0; + foreach (var simplified in simplifieds) + { + sum += Clipper.Area(simplified); + } + return sum; + } + + /// + /// Whether the path contains the point. + /// + /// The path that should contain the point. + /// The point that should be contained within the path. + /// If set to false, will return false if the point belongs to the border. + public static bool Contains(this PdfPath path, PdfPoint point, bool includeBorder = false) + { + var clipperPaths = path.Select(sp => sp.ToClipperPolygon().ToList()).ToList(); + return PointInPaths(point.ToClipperIntPoint(), + clipperPaths, + path.FillingRule == FillingRule.NonZeroWinding ? ClipperPolyFillType.NonZero : ClipperPolyFillType.EvenOdd, + includeBorder); + } + + /// + /// Whether the path contains the rectangle. + /// + /// The path that should contain the rectangle. + /// The rectangle that should be contained within the path. + /// If set to false, will return false if the point belongs to the border. + public static bool Contains(this PdfPath path, PdfRectangle rectangle, bool includeBorder = false) + { + var clipperPaths = path.Select(sp => sp.ToClipperPolygon().ToList()).ToList(); + var fillType = path.FillingRule == FillingRule.NonZeroWinding ? ClipperPolyFillType.NonZero : ClipperPolyFillType.EvenOdd; + if (!PointInPaths(rectangle.BottomLeft.ToClipperIntPoint(), clipperPaths, fillType, includeBorder)) return false; + if (!PointInPaths(rectangle.TopLeft.ToClipperIntPoint(), clipperPaths, fillType, includeBorder)) return false; + if (!PointInPaths(rectangle.TopRight.ToClipperIntPoint(), clipperPaths, fillType, includeBorder)) return false; + if (!PointInPaths(rectangle.BottomRight.ToClipperIntPoint(), clipperPaths, fillType, includeBorder)) return false; + return true; + } + + /// + /// Whether the path contains the subpath. + /// + /// The path that should contain the subpath. + /// The subpath that should be contained within the path. + /// If set to false, will return false if the point belongs to the border. + public static bool Contains(this PdfPath path, PdfSubpath subpath, bool includeBorder = false) + { + var clipperPaths = path.Select(sp => sp.ToClipperPolygon().ToList()).ToList(); + var fillType = path.FillingRule == FillingRule.NonZeroWinding ? ClipperPolyFillType.NonZero : ClipperPolyFillType.EvenOdd; + foreach (var p in subpath.ToClipperPolygon()) + { + if (!PointInPaths(p, clipperPaths, fillType, includeBorder)) return false; + } + return true; + } + + /// + /// Whether the path contains the other path. + /// + /// The path that should contain the path. + /// The other path that should be contained within the path. + /// If set to false, will return false if the point belongs to the border. + public static bool Contains(this PdfPath path, PdfPath other, bool includeBorder = false) + { + var clipperPaths = path.Select(sp => sp.ToClipperPolygon().ToList()).ToList(); + var fillType = path.FillingRule == FillingRule.NonZeroWinding ? ClipperPolyFillType.NonZero : ClipperPolyFillType.EvenOdd; + foreach (var subpath in other) + { + foreach (var p in subpath.ToClipperPolygon()) + { + if (!PointInPaths(p, clipperPaths, fillType, includeBorder)) return false; + } + } + return true; + } + + #endregion + private const double OneThird = 0.333333333333333333333; private const double SqrtOfThree = 1.73205080756888;