// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
namespace SixLabors.ImageSharp.Drawing {
///
/// Represents a complex polygon made up of one or more shapes overlayed on each other,
/// where overlaps causes holes.
///
///
public sealed class ComplexPolygon : IPath, IPathInternals, IInternalPathOwner
{
private readonly IPath[] paths;
private List? internalPaths;
private float length;
private RectangleF? bounds;
private IPath? closedPath;
private LinearGeometryCache geometryCache;
///
/// Initializes a new instance of the class.
///
/// The contour path.
/// The hole path.
public ComplexPolygon(PointF[] contour, PointF[] hole)
: this(new Path(new LinearLineSegment(contour)), new Path(new LinearLineSegment(hole)))
{
}
///
/// Initializes a new instance of the class.
///
/// The paths.
public ComplexPolygon(IEnumerable paths)
: this([.. paths])
{
}
///
/// Initializes a new instance of the class.
///
/// The paths.
public ComplexPolygon(params IPath[] paths)
{
Guard.NotNull(paths, nameof(paths));
this.paths = paths;
if (paths.Length == 0)
{
this.bounds = RectangleF.Empty;
}
this.PathType = PathTypes.Mixed;
}
///
public PathTypes PathType { get; }
///
/// Gets the collection of paths that make up this shape.
///
public IEnumerable Paths => this.paths;
///
public RectangleF Bounds => this.bounds ??= this.CalcBounds();
///
public IPath Transform(Matrix4x4 matrix)
{
if (matrix.IsIdentity)
{
// No transform to apply skip it
return this;
}
IPath[] shapes = new IPath[this.paths.Length];
for (int i = 0; i < shapes.Length; i++)
{
shapes[i] = this.paths[i].Transform(matrix);
}
return new ComplexPolygon(shapes);
}
///
public IEnumerable Flatten()
{
List paths = new(this.paths.Length);
foreach (IPath path in this.Paths)
{
paths.AddRange(path.Flatten());
}
return paths;
}
///
public LinearGeometry ToLinearGeometry(Vector2 scale)
=> this.geometryCache.TryGet(scale, out LinearGeometry? hit)
? hit
: this.geometryCache.Store(scale, this.BuildLinearGeometry(scale));
private LinearGeometry BuildLinearGeometry(Vector2 scale)
{
int pointCount = 0;
int contourCount = 0;
int segmentCount = 0;
int nonHorizontalSegmentCountPixelBoundary = 0;
int nonHorizontalSegmentCountPixelCenter = 0;
bool hasBounds = false;
float minX = float.MaxValue;
float minY = float.MaxValue;
float maxX = float.MinValue;
float maxY = float.MinValue;
foreach (IPath path in this.paths)
{
LinearGeometry geometry = path.ToLinearGeometry(scale);
if (geometry.Info.PointCount == 0)
{
continue;
}
RectangleF childBounds = geometry.Info.Bounds;
minX = MathF.Min(minX, childBounds.Left);
minY = MathF.Min(minY, childBounds.Top);
maxX = MathF.Max(maxX, childBounds.Right);
maxY = MathF.Max(maxY, childBounds.Bottom);
hasBounds = true;
pointCount += geometry.Info.PointCount;
contourCount += geometry.Info.ContourCount;
segmentCount += geometry.Info.SegmentCount;
nonHorizontalSegmentCountPixelBoundary += geometry.Info.NonHorizontalSegmentCountPixelBoundary;
nonHorizontalSegmentCountPixelCenter += geometry.Info.NonHorizontalSegmentCountPixelCenter;
}
PointF[] points = new PointF[pointCount];
LinearContour[] contours = new LinearContour[contourCount];
int pointStart = 0;
int contourStart = 0;
int segmentStart = 0;
foreach (IPath path in this.paths)
{
LinearGeometry geometry = path.ToLinearGeometry(scale);
if (geometry.Info.PointCount == 0)
{
continue;
}
for (int i = 0; i < geometry.Points.Count; i++)
{
points[pointStart + i] = geometry.Points[i];
}
for (int i = 0; i < geometry.Contours.Count; i++)
{
LinearContour contour = geometry.Contours[i];
contours[contourStart + i] = new LinearContour
{
PointStart = pointStart + contour.PointStart,
PointCount = contour.PointCount,
SegmentStart = segmentStart + contour.SegmentStart,
SegmentCount = contour.SegmentCount,
IsClosed = contour.IsClosed
};
}
pointStart += geometry.Info.PointCount;
contourStart += geometry.Info.ContourCount;
segmentStart += geometry.Info.SegmentCount;
}
RectangleF bounds = hasBounds ? RectangleF.FromLTRB(minX, minY, maxX, maxY) : RectangleF.Empty;
return new LinearGeometry(
new LinearGeometryInfo
{
Bounds = bounds,
ContourCount = contours.Length,
PointCount = points.Length,
SegmentCount = segmentCount,
NonHorizontalSegmentCountPixelBoundary = nonHorizontalSegmentCountPixelBoundary,
NonHorizontalSegmentCountPixelCenter = nonHorizontalSegmentCountPixelCenter
},
contours,
points);
}
///
public IPath AsClosedPath()
{
if (this.PathType == PathTypes.Closed)
{
return this;
}
if (this.closedPath is not null)
{
return this.closedPath;
}
IPath[] paths = new IPath[this.paths.Length];
for (int i = 0; i < this.paths.Length; i++)
{
paths[i] = this.paths[i].AsClosedPath();
}
this.closedPath = new ComplexPolygon(paths);
return this.closedPath;
}
///
SegmentInfo IPathInternals.PointAlongPath(float distance)
{
this.EnsureInternalPaths();
distance %= this.length;
foreach (InternalPath p in this.internalPaths)
{
if (p.Length >= distance)
{
return p.PointAlongPath(distance);
}
// Reduce it before trying the next path
distance -= p.Length;
}
ThrowOutOfRange();
return default;
}
///
IReadOnlyList IInternalPathOwner.GetRingsAsInternalPath()
{
this.EnsureInternalPaths();
return this.internalPaths;
}
[MemberNotNull(nameof(internalPaths))]
private void EnsureInternalPaths()
{
if (this.internalPaths is not null)
{
return;
}
this.InitInternalPaths();
}
///
/// Initializes and .
///
[MemberNotNull(nameof(internalPaths))]
private void InitInternalPaths()
{
this.internalPaths = new List(this.paths.Length);
this.length = 0;
foreach (IPath p in this.paths)
{
foreach (ISimplePath s in p.Flatten())
{
InternalPath ip = new(s.Points, s.IsClosed);
this.length += ip.Length;
this.internalPaths.Add(ip);
}
}
}
private RectangleF CalcBounds()
{
float minX = float.MaxValue;
float maxX = float.MinValue;
float minY = float.MaxValue;
float maxY = float.MinValue;
foreach (IPath p in this.paths)
{
RectangleF pBounds = p.Bounds;
minX = MathF.Min(minX, pBounds.Left);
maxX = MathF.Max(maxX, pBounds.Right);
minY = MathF.Min(minY, pBounds.Top);
maxY = MathF.Max(maxY, pBounds.Bottom);
}
return new RectangleF(minX, minY, maxX - minX, maxY - minY);
}
private static InvalidOperationException ThrowOutOfRange() => new("Should not be possible to reach this line");
}
}