// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace SixLabors.PolygonClipper {
///
/// Represents a single polygon ring (outer contour or hole).
///
///
/// A contour is treated as implicitly closed: an edge is always considered between the last
/// vertex and the first vertex. A duplicated terminal closing vertex is optional on input
/// but not required.
///
[DebuggerDisplay("Count = {Count}")]
#pragma warning disable CA1710 // Identifiers should have correct suffix
public sealed class Contour : IReadOnlyCollection
#pragma warning restore CA1710 // Identifiers should have correct suffix
{
private bool hasCachedOrientation;
private bool cachedCounterClockwise;
///
/// Set of vertices conforming the external contour
///
private readonly List vertices = [];
///
/// Holes of the contour. They are stored as the indexes of
/// the holes in a polygon class
///
private readonly List holeIndices = [];
///
/// Initializes a new instance of the class.
///
public Contour()
=> this.vertices = [];
///
/// Initializes a new instance of the class with a vertex capacity.
///
/// The initial vertex capacity.
public Contour(int capacity)
=> this.vertices = new List(capacity);
///
/// Gets the number of stored vertices.
///
public int Count
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this.vertices.Count;
}
///
/// Gets the number of holes.
///
public int HoleCount => this.holeIndices.Count;
///
/// Gets a value indicating whether the contour is external (not a hole).
///
public bool IsExternal => this.ParentIndex == null;
///
/// Gets or sets the index of the parent contour in the polygon if this contour is a hole.
///
public int? ParentIndex { get; set; }
///
/// Gets or sets the depth of the contour.
///
public int Depth { get; set; }
///
/// Gets the vertex at the specified index.
///
/// The index of the vertex.
/// The at the specified index.
public Vertex this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this.vertices[index];
}
///
/// Gets the hole index at the specified position in the contour.
///
/// The index of the hole.
/// The hole index.
public int GetHoleIndex(int index) => this.holeIndices[index];
///
/// Gets the segment at the specified index of the contour.
///
/// The index of the segment.
/// The . The final segment wraps from last vertex to first vertex.
internal Segment GetSegment(int index)
=> (index == this.Count - 1)
? new Segment(this.vertices[^1], this.vertices[0])
: new Segment(this.vertices[index], this.vertices[index + 1]);
///
/// Gets the bounding box of the contour.
///
/// The .
public Box2 GetBoundingBox()
{
if (this.Count == 0)
{
return default;
}
List points = this.vertices;
Box2 b = new(points[0]);
for (int i = 1; i < points.Count; ++i)
{
b = b.Add(new Box2(points[i]));
}
return b;
}
///
/// Gets a value indicating whether the contour is counterclockwise oriented
///
///
/// if the contour is counterclockwise oriented; otherwise .
///
public bool IsCounterClockwise()
{
if (this.hasCachedOrientation)
{
return this.cachedCounterClockwise;
}
this.hasCachedOrientation = true;
double area = 0;
Vertex c;
Vertex c1;
List points = this.vertices;
for (int i = 0; i < points.Count - 1; i++)
{
c = points[i];
c1 = points[i + 1];
area += Vertex.Cross(c, c1);
}
c = points[^1];
c1 = points[0];
area += Vertex.Cross(c, c1);
return this.cachedCounterClockwise = area >= 0;
}
///
/// Gets a value indicating whether the contour is clockwise oriented
///
///
/// if the contour is clockwise oriented; otherwise .
///
public bool IsClockwise() => !this.IsCounterClockwise();
///
/// Reverses the orientation of the contour.
///
public void Reverse()
{
this.vertices.Reverse();
this.cachedCounterClockwise = !this.cachedCounterClockwise;
}
///
/// Sets the contour to clockwise orientation.
///
public void SetClockwise()
{
if (this.IsCounterClockwise())
{
this.Reverse();
}
}
///
/// Sets the contour to counterclockwise orientation.
///
public void SetCounterClockwise()
{
if (this.IsClockwise())
{
this.Reverse();
}
}
///
/// Translates the contour by the specified x and y values.
///
/// The x-coordinate offset.
/// The y-coordinate offset.
public void Translate(double x, double y)
{
List points = this.vertices;
for (int i = 0; i < points.Count; i++)
{
points[i] += new Vertex(x, y);
}
}
///
/// Adds a vertex to the end of the vertices collection.
///
/// The vertex to add.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(in Vertex vertex) => this.vertices.Add(vertex);
///
/// Removes the vertex at the specified index from the contour.
///
/// The index of the vertex to remove.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RemoveVertexAt(int index) => this.vertices.RemoveAt(index);
///
/// Clears all vertices and holes from the contour.
///
public void Clear()
{
this.vertices.Clear();
this.holeIndices.Clear();
}
///
/// Clears all holes from the contour.
///
public void ClearHoles() => this.holeIndices.Clear();
///
/// Gets the last vertex in the contour.
///
/// The last in the contour.
public Vertex GetLastVertex() => this.vertices[^1];
///
/// Adds a hole index to the contour.
///
/// The index of the hole to add.
public void AddHoleIndex(int index) => this.holeIndices.Add(index);
///
/// Creates a deep copy of this contour.
///
/// A detached contour copy.
public Contour DeepClone()
{
Contour clone = new(this.vertices.Count)
{
ParentIndex = this.ParentIndex,
Depth = this.Depth,
hasCachedOrientation = this.hasCachedOrientation,
cachedCounterClockwise = this.cachedCounterClockwise
};
clone.vertices.AddRange(this.vertices);
clone.holeIndices.AddRange(this.holeIndices);
return clone;
}
///
public IEnumerator GetEnumerator()
=> ((IEnumerable)this.vertices).GetEnumerator();
///
IEnumerator IEnumerable.GetEnumerator()
=> ((IEnumerable)this.vertices).GetEnumerator();
}
}