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