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()