// // 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; } } }