diff --git a/src/ImageSharp/Drawing/Paths/InternalPath.cs b/src/ImageSharp/Drawing/Paths/InternalPath.cs index b347022534..b71ba62a88 100644 --- a/src/ImageSharp/Drawing/Paths/InternalPath.cs +++ b/src/ImageSharp/Drawing/Paths/InternalPath.cs @@ -60,10 +60,29 @@ namespace ImageSharp.Drawing.Paths /// The segments. /// if set to true [is closed path]. internal InternalPath(ILineSegment[] segments, bool isClosedPath) + : this(Simplify(segments), isClosedPath) { - Guard.NotNull(segments, nameof(segments)); + } + + /// + /// Initializes a new instance of the class. + /// + /// The segment. + /// if set to true [is closed path]. + internal InternalPath(ILineSegment segment, bool isClosedPath) + :this(segment.AsSimpleLinearPath(), isClosedPath) + { + } + - this.points = this.Simplify(segments); + /// + /// Initializes a new instance of the class. + /// + /// The points. + /// if set to true [is closed path]. + internal InternalPath(Vector2[] points, bool isClosedPath) + { + this.points = points; this.closedPath = isClosedPath; float minX = this.points.Min(x => x.X); @@ -192,7 +211,7 @@ namespace ImageSharp.Drawing.Paths /// /// The . /// - private Vector2[] Simplify(ILineSegment[] segments) + private static Vector2[] Simplify(ILineSegment[] segments) { List simplified = new List(); foreach (ILineSegment seg in segments) diff --git a/src/ImageSharp/Drawing/Shapes/ComplexPolygon.cs b/src/ImageSharp/Drawing/Shapes/ComplexPolygon.cs index d0edb76aa5..7bd459f6aa 100644 --- a/src/ImageSharp/Drawing/Shapes/ComplexPolygon.cs +++ b/src/ImageSharp/Drawing/Shapes/ComplexPolygon.cs @@ -7,7 +7,6 @@ namespace ImageSharp.Drawing.Shapes { using System.Collections; using System.Collections.Generic; - using System.Linq; using System.Numerics; using Paths; @@ -20,8 +19,8 @@ namespace ImageSharp.Drawing.Shapes public sealed class ComplexPolygon : IShape { private const float ClipperScaleFactor = 100f; - private IEnumerable holes; - private IEnumerable outlines; + private IShape[] holes; + private IShape[] outlines; private IEnumerable paths; /// @@ -39,7 +38,7 @@ namespace ImageSharp.Drawing.Shapes /// /// The outlines. /// The holes. - public ComplexPolygon(IEnumerable outlines, IEnumerable holes) + public ComplexPolygon(IShape[] outlines, IShape[] holes) { Guard.NotNull(outlines, nameof(outlines)); Guard.MustBeGreaterThanOrEqualTo(outlines.Count(), 1, nameof(outlines)); @@ -76,7 +75,7 @@ namespace ImageSharp.Drawing.Shapes // othersie we will start returning the distanct to the nearest shape var dist = this.outlines.Select(o => o.Distance(point)).OrderBy(p => p).First(); - if (dist <= 0) + if (dist <= 0 && this.holes != null) { // inside poly foreach (var hole in this.holes) @@ -138,20 +137,32 @@ namespace ImageSharp.Drawing.Shapes } } - private void AddPoints(ClipperLib.Clipper clipper, IEnumerable shapes, ClipperLib.PolyType polyType) + private void AddPoints(ClipperLib.Clipper clipper, IShape[] shapes, bool[] shouldInclude, ClipperLib.PolyType polyType) { - foreach (var shape in shapes) + for(var i =0; i< shapes.Length; i++) { - this.AddPoints(clipper, shape, polyType); + if (shouldInclude[i]) + { + this.AddPoints(clipper, shapes[i], polyType); + } } } - private void ExtractOutlines(ClipperLib.PolyNode tree, List outlines, List holes) + + private void ExtractOutlines(ClipperLib.PolyNode tree, List outlines, List holes) { if (tree.Contour.Any()) { // convert the Clipper Contour from scaled ints back down to the origional size (this is going to be lossy but not significantly) - var polygon = new Polygon(new LinearLineSegment(tree.Contour.Select(x => new Vector2(x.X / ClipperScaleFactor, x.Y / ClipperScaleFactor)).ToArray())); + var pointCount = tree.Contour.Count; + var vectors = new Vector2[pointCount]; + for(var i=0; i< pointCount; i++) + { + var p = tree.Contour[i]; + vectors[i] = new Vector2(p.X, p.Y) / ClipperScaleFactor; + } + + var polygon = new Polygon(new LinearLineSegment(vectors)); if (tree.IsHole) { @@ -169,28 +180,113 @@ namespace ImageSharp.Drawing.Shapes } } - private void FixAndSetShapes(IEnumerable outlines, IEnumerable holes) + /// + /// Determines if the s bounding boxes overlap. + /// + /// The source. + /// The target. + /// + private bool OverlappingBoundingBoxes(IShape source, IShape target) { - var clipper = new ClipperLib.Clipper(); + return source.Bounds.Intersects(target.Bounds); + } - // add the outlines and the holes to clipper, scaling up from the float source to the int based system clipper uses - this.AddPoints(clipper, outlines, ClipperLib.PolyType.ptSubject); - this.AddPoints(clipper, holes, ClipperLib.PolyType.ptClip); - var tree = new ClipperLib.PolyTree(); - clipper.Execute(ClipperLib.ClipType.ctDifference, tree); + private void FixAndSetShapes(IShape[] outlines, IShape[] holes) + { + // if any outline doesn't overlap another shape then we don't have to bother with sending them through clipper + // as sending then though clipper will turn them into generic polygons and loose thier shape specific optimisations - List newOutlines = new List(); - List newHoles = new List(); + int outlineLength = outlines.Length; + int holesLength = holes.Length; + bool[] overlappingOutlines = new bool[outlineLength]; + bool[] overlappingHoles = new bool[holesLength]; + bool anyOutlinesOverlapping = false; + bool anyHolesOverlapping = false; + + for (int i = 0; i < outlineLength; i++) + { + for (int j = i + 1; j < outlineLength; j++) + { + //skip the bounds check if they are already tested + if (overlappingOutlines[i] == false || overlappingOutlines[j] == false) + { + if (OverlappingBoundingBoxes(outlines[i], outlines[j])) + { + overlappingOutlines[i] = true; + overlappingOutlines[j] = true; + anyOutlinesOverlapping = true; + } + } + } - // convert the 'tree' back to paths - this.ExtractOutlines(tree, newOutlines, newHoles); + for (int k = 0; k < holesLength; k++) + { + if (overlappingOutlines[i] == false || overlappingHoles[k] == false) + { + if (OverlappingBoundingBoxes(outlines[i], holes[k])) + { + overlappingOutlines[i] = true; + overlappingHoles[k] = true; + anyOutlinesOverlapping = true; + anyHolesOverlapping = true; + } + } + } + } - this.outlines = newOutlines; - this.holes = newHoles; + if (anyOutlinesOverlapping) + { + var clipper = new ClipperLib.Clipper(); - // extract the final list of paths out of the new polygons we just converted down to. - this.paths = newOutlines.Union(newHoles).ToArray(); + // add the outlines and the holes to clipper, scaling up from the float source to the int based system clipper uses + this.AddPoints(clipper, outlines, overlappingOutlines, ClipperLib.PolyType.ptSubject); + if (anyHolesOverlapping) + { + this.AddPoints(clipper, holes, overlappingHoles, ClipperLib.PolyType.ptClip); + } + + var tree = new ClipperLib.PolyTree(); + clipper.Execute(ClipperLib.ClipType.ctDifference, tree); + + List newOutlines = new List(); + List newHoles = new List(); + + // convert the 'tree' back to shapes + this.ExtractOutlines(tree, newOutlines, newHoles); + + // add the origional outlines that where not overlapping + for (int i = 0; i < outlineLength - 1; i++) + { + if (!overlappingOutlines[i]) + { + newOutlines.Add(outlines[i]); + } + } + + this.outlines = newOutlines.ToArray(); + if (newHoles.Count > 0) + { + this.holes = newHoles.ToArray(); + } + }else + { + this.outlines = outlines; + } + + var paths = new List(); + foreach (var o in this.outlines) + { + paths.AddRange(o); + } + if (this.holes != null) + { + foreach (var o in this.holes) + { + paths.AddRange(o); + } + } + this.paths = paths; } } } \ No newline at end of file diff --git a/src/ImageSharp/Drawing/Shapes/Polygon.cs b/src/ImageSharp/Drawing/Shapes/Polygon.cs index 9d9626d4ee..3621a9e2c0 100644 --- a/src/ImageSharp/Drawing/Shapes/Polygon.cs +++ b/src/ImageSharp/Drawing/Shapes/Polygon.cs @@ -29,6 +29,16 @@ namespace ImageSharp.Drawing.Shapes this.pathCollection = new[] { this }; } + /// + /// Initializes a new instance of the class. + /// + /// The segment. + public Polygon(ILineSegment segment) + { + this.innerPath = new InternalPath(segment, true); + this.pathCollection = new[] { this }; + } + /// /// Gets the bounding box of this shape. /// diff --git a/tests/ImageSharp.Tests/Drawing/LineComplexPolygonTests.cs b/tests/ImageSharp.Tests/Drawing/LineComplexPolygonTests.cs index 1f6708bf81..cdcb5ebe43 100644 --- a/tests/ImageSharp.Tests/Drawing/LineComplexPolygonTests.cs +++ b/tests/ImageSharp.Tests/Drawing/LineComplexPolygonTests.cs @@ -66,6 +66,55 @@ namespace ImageSharp.Tests.Drawing } } + [Fact] + public void ImageShouldBeOverlayedByPolygonOutlineNoOverlapping() + { + string path = CreateOutputDirectory("Drawing", "LineComplexPolygon"); + var simplePath = new LinearPolygon( + new Vector2(10, 10), + new Vector2(200, 150), + new Vector2(50, 300)); + + var hole1 = new LinearPolygon( + new Vector2(207, 25), + new Vector2(263, 25), + new Vector2(235, 57)); + + var image = new Image(500, 500); + + using (FileStream output = File.OpenWrite($"{path}/SimpleVanishHole.png")) + { + image + .BackgroundColor(Color.Blue) + .DrawPolygon(Color.HotPink, 5, new ComplexPolygon(simplePath, hole1)) + .Save(output); + } + + using (var sourcePixels = image.Lock()) + { + Assert.Equal(Color.HotPink, sourcePixels[10, 10]); + + Assert.Equal(Color.HotPink, sourcePixels[200, 150]); + + Assert.Equal(Color.HotPink, sourcePixels[50, 300]); + + + //Assert.Equal(Color.HotPink, sourcePixels[37, 85]); + + //Assert.Equal(Color.HotPink, sourcePixels[93, 85]); + + //Assert.Equal(Color.HotPink, sourcePixels[65, 137]); + + Assert.Equal(Color.Blue, sourcePixels[2, 2]); + + //inside hole + Assert.Equal(Color.Blue, sourcePixels[57, 99]); + + //inside shape + Assert.Equal(Color.Blue, sourcePixels[100, 192]); + } + } + [Fact] public void ImageShouldBeOverlayedByPolygonOutlineOverlapping()