|
|
|
@ -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>
|
|
|
|
@ -39,17 +38,17 @@ namespace ImageSharp.Drawing.Shapes |
|
|
|
/// </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); |
|
|
|
} |
|
|
|
@ -69,30 +68,37 @@ namespace ImageSharp.Drawing.Shapes |
|
|
|
/// <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; |
|
|
|
} |
|
|
|
|
|
|
|
@ -138,59 +144,136 @@ namespace ImageSharp.Drawing.Shapes |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
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; |
|
|
|
} |
|
|
|
} |
|
|
|
} |