From 691c214a1168b6d22146bbf438b7c2993042771b Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Fri, 16 Dec 2016 16:57:21 +0000 Subject: [PATCH] optimise complex polygons Trying to avoid using clipper to merge polygons when they can be avoided. Try to use the origional (potentially optermised shapes) if merging will produce not change the origional shape. We do this in a couple of ways: - We strip away any holes that don't intersect with an outline - We skip merging outlines that dont intersect with any other shape and simple pass then through. This provides the added benefit/optimisation of using the specialised shapes instead of the generic polygon class where possible. --- src/ImageSharp/Drawing/Paths/InternalPath.cs | 25 ++- .../Drawing/Shapes/ComplexPolygon.cs | 146 +++++++++++++++--- src/ImageSharp/Drawing/Shapes/Polygon.cs | 10 ++ .../Drawing/LineComplexPolygonTests.cs | 49 ++++++ 4 files changed, 202 insertions(+), 28 deletions(-) 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()