Skip to content

Optimise polygon merging in complex polygons #41

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Dec 17, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions src/ImageSharp/Drawing/Paths/InternalPath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,28 @@ internal class InternalPath
/// <param name="segments">The segments.</param>
/// <param name="isClosedPath">if set to <c>true</c> [is closed path].</param>
internal InternalPath(ILineSegment[] segments, bool isClosedPath)
: this(Simplify(segments), isClosedPath)
{
Guard.NotNull(segments, nameof(segments));
}

this.points = this.Simplify(segments);
/// <summary>
/// Initializes a new instance of the <see cref="InternalPath" /> class.
/// </summary>
/// <param name="segment">The segment.</param>
/// <param name="isClosedPath">if set to <c>true</c> [is closed path].</param>
internal InternalPath(ILineSegment segment, bool isClosedPath)
: this(segment.AsSimpleLinearPath(), isClosedPath)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="InternalPath" /> class.
/// </summary>
/// <param name="points">The points.</param>
/// <param name="isClosedPath">if set to <c>true</c> [is closed path].</param>
internal InternalPath(Vector2[] points, bool isClosedPath)
{
this.points = points;
this.closedPath = isClosedPath;

float minX = this.points.Min(x => x.X);
Expand Down Expand Up @@ -192,7 +210,7 @@ public bool PointInPolygon(Vector2 point)
/// <returns>
/// The <see cref="T:Vector2[]"/>.
/// </returns>
private Vector2[] Simplify(ILineSegment[] segments)
private static Vector2[] Simplify(ILineSegment[] segments)
{
List<Vector2> simplified = new List<Vector2>();
foreach (ILineSegment seg in segments)
Expand Down
187 changes: 135 additions & 52 deletions src/ImageSharp/Drawing/Shapes/ComplexPolygon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ namespace ImageSharp.Drawing.Shapes
public sealed class ComplexPolygon : IShape
{
private const float ClipperScaleFactor = 100f;
private IEnumerable<IShape> holes;
private IEnumerable<IShape> outlines;
private IShape[] shapes;
private IEnumerable<IPath> paths;

/// <summary>
Expand All @@ -39,17 +38,17 @@ public ComplexPolygon(IShape outline, params IShape[] holes)
/// </summary>
/// <param name="outlines">The outlines.</param>
/// <param name="holes">The holes.</param>
public ComplexPolygon(IEnumerable<IShape> outlines, IEnumerable<IShape> 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);
}
Expand All @@ -69,30 +68,37 @@ public ComplexPolygon(IEnumerable<IShape> outlines, IEnumerable<IShape> holes)
/// <returns>
/// Returns the distance from thr shape to the point
/// </returns>
/// <remarks>
/// 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 :)
/// </remarks>
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;
}

Expand Down Expand Up @@ -138,59 +144,136 @@ private void AddPoints(ClipperLib.Clipper clipper, IShape shape, ClipperLib.Poly
}
}

private void AddPoints(ClipperLib.Clipper clipper, IEnumerable<IShape> 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<Polygon> outlines, List<Polygon> holes)
private void ExtractOutlines(ClipperLib.PolyNode tree, List<IShape> 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<IShape> outlines, IEnumerable<IShape> holes)
/// <summary>
/// Determines if the <see cref="IShape"/>s bounding boxes overlap.
/// </summary>
/// <param name="source">The source.</param>
/// <param name="target">The target.</param>
/// <returns>true if the 2 shapes bounding boxes overlap.</returns>
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<IShape> newShapes = new List<IShape>();

var tree = new ClipperLib.PolyTree();
clipper.Execute(ClipperLib.ClipType.ctDifference, tree);
// convert the 'tree' back to shapes
this.ExtractOutlines(tree, newShapes);

List<Polygon> newOutlines = new List<Polygon>();
List<Polygon> newHoles = new List<Polygon>();
// 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<IPath>();
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;
}
}
}
13 changes: 11 additions & 2 deletions src/ImageSharp/Drawing/Shapes/Polygon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ public Polygon(params ILineSegment[] segments)
this.pathCollection = new[] { this };
}

/// <summary>
/// Initializes a new instance of the <see cref="Polygon" /> class.
/// </summary>
/// <param name="segment">The segment.</param>
public Polygon(ILineSegment segment)
{
this.innerPath = new InternalPath(segment, true);
this.pathCollection = new[] { this };
}

/// <summary>
/// Gets the bounding box of this shape.
/// </summary>
Expand Down Expand Up @@ -98,8 +108,7 @@ IEnumerator IEnumerable.GetEnumerator()
/// <summary>
/// Calcualtes the distance along and away from the path for a specified point.
/// </summary>
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <param name="point">The point along the path.</param>
/// <returns>
/// distance metadata about the point.
/// </returns>
Expand Down
49 changes: 49 additions & 0 deletions tests/ImageSharp.Tests/Drawing/LineComplexPolygonTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,55 @@ public void ImageShouldBeOverlayedByPolygonOutline()
}
}

[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()
Expand Down