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