//
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
//
namespace ImageSharp.Drawing.Shapes
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Paths;
using PolygonClipper;
///
/// Represents a complex polygon made up of one or more outline
/// polygons and one or more holes to punch out of them.
///
///
public sealed class ComplexPolygon : IShape
{
private const float ClipperScaleFactor = 100f;
private IShape[] shapes;
private IEnumerable paths;
///
/// Initializes a new instance of the class.
///
/// The outline.
/// The holes.
public ComplexPolygon(IShape outline, params IShape[] holes)
: this(new[] { outline }, holes)
{
}
///
/// Initializes a new instance of the class.
///
/// The outlines.
/// The holes.
public ComplexPolygon(IShape[] outlines, IShape[] holes)
{
Guard.NotNull(outlines, nameof(outlines));
Guard.MustBeGreaterThanOrEqualTo(outlines.Length, 1, nameof(outlines));
this.MaxIntersections = this.FixAndSetShapes(outlines, holes);
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);
}
///
/// Gets the bounding box of this shape.
///
///
/// The bounds.
///
public RectangleF Bounds { get; }
///
/// Gets the maximum number intersections that a shape can have when testing a line.
///
///
/// The maximum intersections.
///
public int MaxIntersections { get; }
///
/// the distance of the point from the outline of the shape, if the value is negative it is inside the polygon bounds
///
/// The point.
///
/// 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)
{
float dist = float.MaxValue;
bool inside = false;
foreach (IShape shape in this.shapes)
{
var d = shape.Distance(point);
if (d <= 0)
{
// we are inside a poly
d = -d; // flip the sign
inside ^= true; // flip the inside flag
}
if (d < dist)
{
dist = d;
}
}
if (inside)
{
return -dist;
}
return dist;
}
///
/// Finds the intersections.
///
/// The start point of the line.
/// The end point of the line.
/// The buffer that will be populated with intersections.
/// The count.
/// The offset.
///
/// The number of intersections populated into the buffer.
///
public int FindIntersections(Vector2 start, Vector2 end, Vector2[] buffer, int count, int offset)
{
throw new NotImplementedException();
}
///
/// Returns an enumerator that iterates through the collection.
///
///
/// An enumerator that can be used to iterate through the collection.
///
public IEnumerator GetEnumerator()
{
return this.paths.GetEnumerator();
}
///
/// Returns an enumerator that iterates through a collection.
///
///
/// An object that can be used to iterate through the collection.
///
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
private void AddPoints(Clipper clipper, IShape shape, PolyType polyType)
{
// if the path is already the shape use it directly and skip the path loop.
if (shape is IPath)
{
clipper.AddPath(
(IPath)shape,
polyType);
}
else
{
foreach (var path in shape)
{
clipper.AddPath(
path,
polyType);
}
}
}
private void AddPoints(Clipper clipper, IEnumerable shapes, PolyType polyType)
{
foreach (var shape in shapes)
{
this.AddPoints(clipper, shape, polyType);
}
}
private void ExtractOutlines(PolyNode tree, List shapes, List paths)
{
if (tree.Contour.Any())
{
// if the source path is set then we clipper retained the full path intact thus we can freely
// use it and get any shape optimisations that are availible.
if (tree.SourcePath != null)
{
shapes.Add((IShape)tree.SourcePath);
paths.Add(tree.SourcePath);
}
else
{
// 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 Paths.LinearLineSegment(tree.Contour.ToArray()));
shapes.Add(polygon);
paths.Add(polygon);
}
}
foreach (var c in tree.Children)
{
this.ExtractOutlines(c, shapes, paths);
}
}
private int FixAndSetShapes(IEnumerable outlines, IEnumerable holes)
{
var clipper = new 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, PolyType.Subject);
this.AddPoints(clipper, holes, PolyType.Clip);
var tree = clipper.Execute();
List shapes = new List();
List paths = new List();
// convert the 'tree' back to paths
this.ExtractOutlines(tree, shapes, paths);
this.shapes = shapes.ToArray();
this.paths = paths.ToArray();
int intersections = 0;
foreach (var s in this.shapes)
{
intersections += s.MaxIntersections;
}
return intersections;
}
}
}