diff --git a/src/ImageSharp/Drawing/Paths/InternalPath.cs b/src/ImageSharp/Drawing/Paths/InternalPath.cs index b34702253..52d43b6e8 100644 --- a/src/ImageSharp/Drawing/Paths/InternalPath.cs +++ b/src/ImageSharp/Drawing/Paths/InternalPath.cs @@ -60,10 +60,28 @@ 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)); + } - this.points = this.Simplify(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) + { + } + + /// + /// 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 +210,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 d0edb76aa..d42cae872 100644 --- a/src/ImageSharp/Drawing/Shapes/ComplexPolygon.cs +++ b/src/ImageSharp/Drawing/Shapes/ComplexPolygon.cs @@ -20,8 +20,7 @@ namespace ImageSharp.Drawing.Shapes public sealed class ComplexPolygon : IShape { private const float ClipperScaleFactor = 100f; - private IEnumerable holes; - private IEnumerable outlines; + private IShape[] shapes; private IEnumerable paths; /// @@ -39,17 +38,17 @@ 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)); + Guard.MustBeGreaterThanOrEqualTo(outlines.Length, 1, nameof(outlines)); this.FixAndSetShapes(outlines, holes); - var minX = outlines.Min(x => x.Bounds.Left); - var maxX = outlines.Max(x => x.Bounds.Right); - var minY = outlines.Min(x => x.Bounds.Top); - var maxY = outlines.Max(x => x.Bounds.Bottom); + var minX = this.shapes.Min(x => x.Bounds.Left); + var maxX = this.shapes.Max(x => x.Bounds.Right); + var minY = this.shapes.Min(x => x.Bounds.Top); + var maxY = this.shapes.Max(x => x.Bounds.Bottom); this.Bounds = new RectangleF(minX, minY, maxX - minX, maxY - minY); } @@ -69,30 +68,37 @@ namespace ImageSharp.Drawing.Shapes /// /// Returns the distance from thr shape to the point /// + /// + /// Due to the clipping we did during construction we know that out shapes do not overlap at there edges + /// therefore for apoint to be in more that one we must be in a hole of another, theoretically this could + /// then flip again to be in a outlin inside a hole inside an outline :) + /// float IShape.Distance(Vector2 point) { - // get the outline we are closest to the center of - // by rights we should only be inside 1 outline - // 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) + float dist = float.MaxValue; + bool inside = false; + foreach (IShape shape in this.shapes) { - // inside poly - foreach (var hole in this.holes) + var d = shape.Distance(point); + + if (d <= 0) { - var distFromHole = hole.Distance(point); + // we are inside a poly + d = -d; // flip the sign + inside ^= true; // flip the inside flag + } - // less than zero we are inside shape - if (distFromHole <= 0) - { - // invert distance - dist = distFromHole * -1; - break; - } + if (d < dist) + { + dist = d; } } + if (inside) + { + return -dist; + } + return dist; } @@ -138,59 +144,136 @@ 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 shapes) { 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())); - - if (tree.IsHole) + var pointCount = tree.Contour.Count; + var vectors = new Vector2[pointCount]; + for (var i = 0; i < pointCount; i++) { - holes.Add(polygon); - } - else - { - outlines.Add(polygon); + var p = tree.Contour[i]; + vectors[i] = new Vector2(p.X, p.Y) / ClipperScaleFactor; } + + var polygon = new Polygon(new LinearLineSegment(vectors)); + + shapes.Add(polygon); } foreach (var c in tree.Childs) { - this.ExtractOutlines(c, outlines, holes); + this.ExtractOutlines(c, shapes); } } - private void FixAndSetShapes(IEnumerable outlines, IEnumerable holes) + /// + /// Determines if the s bounding boxes overlap. + /// + /// The source. + /// The target. + /// true if the 2 shapes bounding boxes overlap. + private bool OverlappingBoundingBoxes(IShape source, IShape target) + { + return source.Bounds.Intersects(target.Bounds); + } + + private void FixAndSetShapes(IShape[] outlines, IShape[] holes) { - var clipper = new ClipperLib.Clipper(); + // 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 + int outlineLength = outlines.Length; + int holesLength = holes?.Length ?? 0; + 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 (this.OverlappingBoundingBoxes(outlines[i], outlines[j])) + { + overlappingOutlines[i] = true; + overlappingOutlines[j] = true; + anyOutlinesOverlapping = true; + } + } + } + + for (int k = 0; k < holesLength; k++) + { + if (overlappingOutlines[i] == false || overlappingHoles[k] == false) + { + if (this.OverlappingBoundingBoxes(outlines[i], holes[k])) + { + overlappingOutlines[i] = true; + overlappingHoles[k] = true; + anyOutlinesOverlapping = true; + anyHolesOverlapping = true; + } + } + } + } + + if (anyOutlinesOverlapping) + { + var clipper = new ClipperLib.Clipper(); + + // 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); - // 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); + List newShapes = new List(); - var tree = new ClipperLib.PolyTree(); - clipper.Execute(ClipperLib.ClipType.ctDifference, tree); + // convert the 'tree' back to shapes + this.ExtractOutlines(tree, newShapes); - List newOutlines = new List(); - List newHoles = new List(); + // add the origional outlines that where not overlapping + for (int i = 0; i < outlineLength - 1; i++) + { + if (!overlappingOutlines[i]) + { + newShapes.Add(outlines[i]); + } + } - // convert the 'tree' back to paths - this.ExtractOutlines(tree, newOutlines, newHoles); + this.shapes = newShapes.ToArray(); + } + else + { + this.shapes = outlines; + } - this.outlines = newOutlines; - this.holes = newHoles; + var paths = new List(); + foreach (var o in this.shapes) + { + paths.AddRange(o); + } - // extract the final list of paths out of the new polygons we just converted down to. - this.paths = newOutlines.Union(newHoles).ToArray(); + 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 9d9626d4e..6da27cf48 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. /// @@ -98,8 +108,7 @@ namespace ImageSharp.Drawing.Shapes /// /// Calcualtes the distance along and away from the path for a specified point. /// - /// The x. - /// The y. + /// The point along the path. /// /// distance metadata about the point. /// diff --git a/tests/ImageSharp.Tests/Drawing/LineComplexPolygonTests.cs b/tests/ImageSharp.Tests/Drawing/LineComplexPolygonTests.cs index 1f6708bf8..cdcb5ebe4 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()