// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System; using System.Collections.Generic; using System.Numerics; namespace SixLabors.ImageSharp.Drawing { /// /// A closed rectangular path defined by four straight edges. /// public sealed class RectanglePolygon : IPath, ISimplePath, IPathInternals { private readonly Vector2 topLeft; private readonly Vector2 bottomRight; private readonly PointF[] points; private readonly float halfLength; private readonly float length; private LinearGeometryCache geometryCache; /// /// Initializes a new instance of the class. /// /// The horizontal position of the rectangle. /// The vertical position of the rectangle. /// The width of the rectangle. /// The height of the rectangle. public RectanglePolygon(float x, float y, float width, float height) : this(new PointF(x, y), new SizeF(width, height)) { } /// /// Initializes a new instance of the class. /// /// /// The which specifies the rectangle's top-left point in a two-dimensional plane. /// /// /// The which specifies the rectangle's bottom-right point in a two-dimensional plane. /// public RectanglePolygon(PointF topLeft, PointF bottomRight) { this.Location = topLeft; this.topLeft = topLeft; this.bottomRight = bottomRight; this.Size = new SizeF(bottomRight.X - topLeft.X, bottomRight.Y - topLeft.Y); this.points = [ this.topLeft, new Vector2(this.bottomRight.X, this.topLeft.Y), this.bottomRight, new Vector2(this.topLeft.X, this.bottomRight.Y) ]; this.halfLength = this.Size.Width + this.Size.Height; this.length = this.halfLength * 2; this.Bounds = new RectangleF(this.Location, this.Size); } /// /// Initializes a new instance of the class. /// /// /// The which specifies the rectangle's point in a two-dimensional plane. /// /// /// The which specifies the rectangle's height and width. /// public RectanglePolygon(PointF point, SizeF size) : this(point, point + size) { } /// /// Initializes a new instance of the class. /// /// The rectangle. public RectanglePolygon(RectangleF rectangle) : this(rectangle.Location, rectangle.Location + rectangle.Size) { } /// /// Gets the location. /// public PointF Location { get; } /// /// Gets the x-coordinate of the left edge. /// public float Left => this.X; /// /// Gets the x-coordinate. /// public float X => this.topLeft.X; /// /// Gets the x-coordinate of the right edge. /// public float Right => this.bottomRight.X; /// /// Gets the y-coordinate of the top edge. /// public float Top => this.Y; /// /// Gets the y-coordinate. /// public float Y => this.topLeft.Y; /// /// Gets the y-coordinate of the bottom edge. /// public float Bottom => this.bottomRight.Y; /// public RectangleF Bounds { get; private set; } /// public bool IsClosed => true; /// public ReadOnlyMemory Points => this.points; /// /// Gets the size. /// public SizeF Size { get; } /// /// Gets the width. /// public float Width => this.Size.Width; /// /// Gets the height. /// public float Height => this.Size.Height; /// public PathTypes PathType => PathTypes.Closed; /// /// Gets the center point. /// public PointF Center => (this.topLeft + this.bottomRight) / 2; /// /// Converts a polygon to a rectangle polygon from its bounds. /// /// The polygon to convert. public static explicit operator RectanglePolygon(Polygon polygon) => new(polygon.Bounds.X, polygon.Bounds.Y, polygon.Bounds.Width, polygon.Bounds.Height); /// public IPath Transform(Matrix4x4 matrix) { if (matrix.IsIdentity) { return this; } // Rectangles may be rotated and skewed which means they will then need representing by a polygon return new Polygon(new LinearLineSegment(this.points).Transform(matrix)); } /// SegmentInfo IPathInternals.PointAlongPath(float distance) { distance %= this.length; if (distance < this.Width) { // we are on the top stretch return new SegmentInfo { Point = new Vector2(this.Left + distance, this.Top), Angle = MathF.PI }; } distance -= this.Width; if (distance < this.Height) { // down on right return new SegmentInfo { Point = new Vector2(this.Right, this.Top + distance), Angle = -MathF.PI / 2 }; } distance -= this.Height; if (distance < this.Width) { // bottom right to left return new SegmentInfo { Point = new Vector2(this.Right - distance, this.Bottom), Angle = 0 }; } distance -= this.Width; return new SegmentInfo { Point = new Vector2(this.Left, this.Bottom - distance), Angle = (float)(Math.PI / 2) }; } /// public IEnumerable Flatten() { yield return this; } /// 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) { PointF p0 = new(this.points[0].X * scale.X, this.points[0].Y * scale.Y); PointF p1 = new(this.points[1].X * scale.X, this.points[1].Y * scale.Y); PointF p2 = new(this.points[2].X * scale.X, this.points[2].Y * scale.Y); PointF p3 = new(this.points[3].X * scale.X, this.points[3].Y * scale.Y); PointF[] points = [p0, p1, p2, p3]; float minX = MathF.Min(MathF.Min(p0.X, p1.X), MathF.Min(p2.X, p3.X)); float minY = MathF.Min(MathF.Min(p0.Y, p1.Y), MathF.Min(p2.Y, p3.Y)); float maxX = MathF.Max(MathF.Max(p0.X, p1.X), MathF.Max(p2.X, p3.X)); float maxY = MathF.Max(MathF.Max(p0.Y, p1.Y), MathF.Max(p2.Y, p3.Y)); // Any rotation or shear in the transform can turn the axis-aligned edges into slanted ones, // so count each edge individually rather than assuming the axis-aligned case. int nonHorizontalSegmentCountPixelBoundary = 0; int nonHorizontalSegmentCountPixelCenter = 0; for (int i = 0; i < 4; i++) { PointF a = points[i]; PointF b = points[(i + 1) % 4]; if (MathF.Floor(a.Y) != MathF.Floor(b.Y)) { nonHorizontalSegmentCountPixelBoundary++; } if (MathF.Floor(a.Y + 0.5F) != MathF.Floor(b.Y + 0.5F)) { nonHorizontalSegmentCountPixelCenter++; } } return new LinearGeometry( new LinearGeometryInfo { Bounds = RectangleF.FromLTRB(minX, minY, maxX, maxY), ContourCount = 1, PointCount = 4, SegmentCount = 4, NonHorizontalSegmentCountPixelBoundary = nonHorizontalSegmentCountPixelBoundary, NonHorizontalSegmentCountPixelCenter = nonHorizontalSegmentCountPixelCenter }, [new LinearContour { PointStart = 0, PointCount = 4, SegmentStart = 0, SegmentCount = 4, IsClosed = true }], points); } /// public IPath AsClosedPath() => this; } }