// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; namespace SixLabors.ImageSharp.Drawing { /// /// Allow you to derivatively build shapes and paths. /// public class PathBuilder { private readonly List
figures = []; private readonly Matrix4x4 defaultTransform; private Figure currentFigure; private Matrix4x4 currentTransform; private Matrix4x4 setTransform; private Vector2 currentPoint; /// /// Initializes a new instance of the class. /// public PathBuilder() : this(Matrix4x4.Identity) { } /// /// Initializes a new instance of the class. /// /// The default transform. public PathBuilder(Matrix4x4 defaultTransform) { this.defaultTransform = defaultTransform; this.Clear(); _ = this.ResetTransform(); } /// /// Gets the current transformation matrix. /// /// /// Returns a copy of the matrix. Because is a value type, /// modifications to the returned value do not affect the internal state. To change the transform, /// call . /// /// The current transformation matrix. public Matrix4x4 Transform => this.currentTransform; /// /// Sets the translation to be applied to all items to follow being applied to the . /// /// The transform. /// The . public PathBuilder SetTransform(Matrix4x4 transform) { this.setTransform = transform; this.currentTransform = this.setTransform * this.defaultTransform; return this; } /// /// Sets the origin all subsequent point should be relative to. /// /// The origin. /// The . public PathBuilder SetOrigin(PointF origin) { // The new origin should be transformed based on the default transform this.setTransform.Translation = new Vector3(origin.X, origin.Y, 0); this.currentTransform = this.setTransform * this.defaultTransform; return this; } /// /// Resets the transform to the default. /// /// The . public PathBuilder ResetTransform() { this.setTransform = Matrix4x4.Identity; this.currentTransform = this.setTransform * this.defaultTransform; return this; } /// /// Resets the origin to the default. /// /// The . public PathBuilder ResetOrigin() { this.setTransform.Translation = Vector3.Zero; this.currentTransform = this.setTransform * this.defaultTransform; return this; } /// /// Moves to current point to the supplied vector. /// /// The point. /// The . public PathBuilder MoveTo(PointF point) { _ = this.StartFigure(); this.currentPoint = PointF.Transform(point, this.currentTransform); return this; } /// /// Moves to current point to the supplied vector. /// /// The x-coordinate. /// The y-coordinate. /// The public PathBuilder MoveTo(float x, float y) => this.MoveTo(new PointF(x, y)); /// /// Draws the line connecting the current the current point to the new point. /// /// The point. /// The . public PathBuilder LineTo(PointF point) => this.AddLine(this.currentPoint, point); /// /// Draws the line connecting the current the current point to the new point. /// /// The x. /// The y. /// The public PathBuilder LineTo(float x, float y) => this.LineTo(new PointF(x, y)); /// /// Adds the line connecting the current point to the new point. /// /// The start. /// The end. /// The . public PathBuilder AddLine(PointF start, PointF end) => this.AddSegment(new LinearLineSegment(start, end)); /// /// Adds the line connecting the current point to the new point. /// /// The x1. /// The y1. /// The x2. /// The y2. /// The . public PathBuilder AddLine(float x1, float y1, float x2, float y2) => this.AddLine(new PointF(x1, y1), new PointF(x2, y2)); /// /// Adds a series of line segments connecting the current point to the new points. /// /// The points. /// The . public PathBuilder AddLines(IEnumerable points) { Guard.NotNull(points, nameof(points)); return this.AddLines([.. points]); } /// /// Adds a series of line segments connecting the current point to the new points. /// /// The points. /// The . public PathBuilder AddLines(params PointF[] points) { Guard.NotNull(points, nameof(points)); return this.AddSegment(new LinearLineSegment(points)); } /// /// Adds the segment. /// /// The segment. /// The . public PathBuilder AddSegment(ILineSegment segment) { Guard.NotNull(segment, nameof(segment)); segment = segment.Transform(this.currentTransform); this.currentFigure.AddSegment(segment); this.currentPoint = segment.EndPoint; return this; } /// /// Draws a quadratic bezier from the current point to the /// /// The second control point. /// The point. /// The . public PathBuilder QuadraticBezierTo(Vector2 secondControlPoint, Vector2 point) => this.AddQuadraticBezier(this.currentPoint, secondControlPoint, point); /// /// Draws a quadratic bezier from the current point to the /// /// The second control point. /// The third control point. /// The point. /// The . public PathBuilder CubicBezierTo(Vector2 secondControlPoint, Vector2 thirdControlPoint, Vector2 point) => this.AddCubicBezier(this.currentPoint, secondControlPoint, thirdControlPoint, point); /// /// Adds a quadratic bezier curve to the current figure joining the point to the . /// /// The start point. /// The control point1. /// The end point. /// The . public PathBuilder AddQuadraticBezier(PointF startPoint, PointF controlPoint, PointF endPoint) { Vector2 startPointVector = startPoint; Vector2 controlPointVector = controlPoint; Vector2 endPointVector = endPoint; Vector2 c1 = ((controlPointVector - startPointVector) * 2 / 3) + startPointVector; Vector2 c2 = ((controlPointVector - endPointVector) * 2 / 3) + endPointVector; return this.AddCubicBezier(startPointVector, c1, c2, endPoint); } /// /// Adds a cubic bezier curve to the current figure joining the point to the . /// /// The start point. /// The control point1. /// The control point2. /// The end point. /// The . public PathBuilder AddCubicBezier(PointF startPoint, PointF controlPoint1, PointF controlPoint2, PointF endPoint) => this.AddSegment(new CubicBezierLineSegment(startPoint, controlPoint1, controlPoint2, endPoint)); /// /// /// Adds an elliptical arc to the current figure. The arc curves from the last point to , /// choosing one of four possible routes: clockwise or counterclockwise, and smaller or larger. /// /// /// The arc sweep is always less than 360 degrees. The method appends a line /// to the last point if either radii are zero, or if last point is equal to . /// In addition the method scales the radii to fit last point and if both /// are greater than zero but too small to describe an arc. /// /// /// The x-radius of the ellipsis. /// The y-radius of the ellipsis. /// The rotation along the X-axis; measured in degrees clockwise. /// /// The large arc flag, and is if an arc spanning less than or equal to 180 degrees /// is chosen, or if an arc spanning greater than 180 degrees is chosen. /// /// /// The sweep flag, and is if the line joining center to arc sweeps through decreasing /// angles, or if it sweeps through increasing angles. /// /// The end point of the arc. /// The . public PathBuilder ArcTo(float radiusX, float radiusY, float rotation, bool largeArc, bool sweep, PointF point) => this.AddArc(this.currentPoint, radiusX, radiusY, rotation, largeArc, sweep, point); /// /// /// Adds an elliptical arc to the current figure. The arc curves from the to , /// choosing one of four possible routes: clockwise or counterclockwise, and smaller or larger. /// /// /// The arc sweep is always less than 360 degrees. The method appends a line /// to the last point if either radii are zero, or if last point is equal to . /// In addition the method scales the radii to fit last point and if both /// are greater than zero but too small to describe an arc. /// /// /// The start point of the arc. /// The x-radius of the ellipsis. /// The y-radius of the ellipsis. /// The rotation along the X-axis; measured in degrees clockwise. /// /// The large arc flag, and is if an arc spanning less than or equal to 180 degrees /// is chosen, or if an arc spanning greater than 180 degrees is chosen. /// /// /// The sweep flag, and is if the line joining center to arc sweeps through decreasing /// angles, or if it sweeps through increasing angles. /// /// The end point of the arc. /// The . public PathBuilder AddArc(PointF startPoint, float radiusX, float radiusY, float rotation, bool largeArc, bool sweep, PointF endPoint) => this.AddSegment(new ArcLineSegment(startPoint, endPoint, new SizeF(radiusX, radiusY), rotation, largeArc, sweep)); /// /// Adds an elliptical arc to the current figure. /// /// A that represents the rectangular bounds of the ellipse from which the arc is taken. /// The angle, in degrees, from the x-axis of the current coordinate system to the x-axis of the ellipse. /// /// The start angle of the elliptical arc prior to the stretch and rotate operations. (0 is at the 3 o'clock position of the arc's circle). /// /// The angle between and the end of the arc. /// The . public PathBuilder AddArc(RectangleF rectangle, float rotation, float startAngle, float sweepAngle) => this.AddArc((rectangle.Right + rectangle.Left) / 2, (rectangle.Bottom + rectangle.Top) / 2, rectangle.Width / 2, rectangle.Height / 2, rotation, startAngle, sweepAngle); /// /// Adds an elliptical arc to the current figure. /// /// A that represents the rectangular bounds of the ellipse from which the arc is taken. /// The angle, in degrees, from the x-axis of the current coordinate system to the x-axis of the ellipse. /// /// The start angle of the elliptical arc prior to the stretch and rotate operations. (0 is at the 3 o'clock position of the arc's circle). /// /// The angle between and the end of the arc. /// The . public PathBuilder AddArc(Rectangle rectangle, int rotation, int startAngle, int sweepAngle) => this.AddArc((RectangleF)rectangle, rotation, startAngle, sweepAngle); /// /// Adds an elliptical arc to the current figure. /// /// The center of the ellipse from which the arc is taken. /// The x-radius of the ellipsis. /// The y-radius of the ellipsis. /// The angle, in degrees, from the x-axis of the current coordinate system to the x-axis of the ellipse. /// /// The start angle of the elliptical arc prior to the stretch and rotate operations. (0 is at the 3 o'clock position of the arc's circle). /// /// The angle between and the end of the arc. /// The . public PathBuilder AddArc(PointF center, float radiusX, float radiusY, float rotation, float startAngle, float sweepAngle) => this.AddArc(center.X, center.Y, radiusX, radiusY, rotation, startAngle, sweepAngle); /// /// Adds an elliptical arc to the current figure. /// /// The center of the ellipse from which the arc is taken. /// The x-radius of the ellipsis. /// The y-radius of the ellipsis. /// The angle, in degrees, from the x-axis of the current coordinate system to the x-axis of the ellipse. /// /// The start angle of the elliptical arc prior to the stretch and rotate operations. (0 is at the 3 o'clock position of the arc's circle). /// /// The angle between and the end of the arc. /// The . public PathBuilder AddArc(Point center, int radiusX, int radiusY, int rotation, int startAngle, int sweepAngle) => this.AddArc((PointF)center, radiusX, radiusY, rotation, startAngle, sweepAngle); /// /// Adds an elliptical arc to the current figure. /// /// The x-coordinate of the center point of the ellipse from which the arc is taken. /// The y-coordinate of the center point of the ellipse from which the arc is taken. /// The x-radius of the ellipsis. /// The y-radius of the ellipsis. /// The angle, in degrees, from the x-axis of the current coordinate system to the x-axis of the ellipse. /// /// The start angle of the elliptical arc prior to the stretch and rotate operations. (0 is at the 3 o'clock position of the arc's circle). /// /// The angle between and the end of the arc. /// The . public PathBuilder AddArc(int x, int y, int radiusX, int radiusY, int rotation, int startAngle, int sweepAngle) => this.AddSegment(new ArcLineSegment(new PointF(x, y), new SizeF(radiusX, radiusY), rotation, startAngle, sweepAngle)); /// /// Adds an elliptical arc to the current figure. /// /// The x-coordinate of the center point of the ellipse from which the arc is taken. /// The y-coordinate of the center point of the ellipse from which the arc is taken. /// The x-radius of the ellipsis. /// The y-radius of the ellipsis. /// The angle, in degrees, from the x-axis of the current coordinate system to the x-axis of the ellipse. /// /// The start angle of the elliptical arc prior to the stretch and rotate operations. (0 is at the 3 o'clock position of the arc's circle). /// /// The angle between and the end of the arc. /// The . public PathBuilder AddArc(float x, float y, float radiusX, float radiusY, float rotation, float startAngle, float sweepAngle) => this.AddSegment(new ArcLineSegment(new PointF(x, y), new SizeF(radiusX, radiusY), rotation, startAngle, sweepAngle)); /// /// Adds a pie sector to the current path as a closed figure. /// /// The center point of the pie sector. /// The x and y radii of the pie ellipse. /// The ellipse rotation in degrees. /// The pie start angle in degrees. /// The pie sweep angle in degrees. /// The . public PathBuilder AddPie(PointF center, SizeF radius, float rotation, float startAngle, float sweepAngle) { _ = this.StartFigure(); foreach (ILineSegment segment in new PiePolygon(center, radius, rotation, startAngle, sweepAngle).LineSegments) { _ = this.AddSegment(segment); } return this.CloseFigure(); } /// /// Adds a pie sector to the current path as a closed figure. /// /// The center point of the pie sector. /// The x and y radii of the pie ellipse. /// The pie start angle in degrees. /// The pie sweep angle in degrees. /// The . public PathBuilder AddPie(PointF center, SizeF radius, float startAngle, float sweepAngle) => this.AddPie(center, radius, 0F, startAngle, sweepAngle); /// /// Adds a pie sector to the current path as a closed figure. /// /// The x-coordinate of the pie center. /// The y-coordinate of the pie center. /// The x-radius of the pie ellipse. /// The y-radius of the pie ellipse. /// The ellipse rotation in degrees. /// The pie start angle in degrees. /// The pie sweep angle in degrees. /// The . public PathBuilder AddPie(float x, float y, float radiusX, float radiusY, float rotation, float startAngle, float sweepAngle) => this.AddPie(new PointF(x, y), new SizeF(radiusX, radiusY), rotation, startAngle, sweepAngle); /// /// Adds a pie sector to the current path as a closed figure. /// /// The x-coordinate of the pie center. /// The y-coordinate of the pie center. /// The x-radius of the pie ellipse. /// The y-radius of the pie ellipse. /// The pie start angle in degrees. /// The pie sweep angle in degrees. /// The . public PathBuilder AddPie(float x, float y, float radiusX, float radiusY, float startAngle, float sweepAngle) => this.AddPie(x, y, radiusX, radiusY, 0F, startAngle, sweepAngle); /// /// Adds a rectangle to the current path as a closed figure. /// /// The rectangle bounds. /// The . public PathBuilder AddRectangle(RectangleF rectangle) => this.AddRectangle(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height); /// /// Adds a rectangle to the current path as a closed figure. /// /// The rectangle bounds. /// The . public PathBuilder AddRectangle(Rectangle rectangle) => this.AddRectangle((RectangleF)rectangle); /// /// Adds a rectangle to the current path as a closed figure. /// /// The x-coordinate of the rectangle. /// The y-coordinate of the rectangle. /// The rectangle width. /// The rectangle height. /// The . public PathBuilder AddRectangle(float x, float y, float width, float height) => this.AddPolygon( new PointF(x, y), new PointF(x + width, y), new PointF(x + width, y + height), new PointF(x, y + height)); /// /// Adds a rounded rectangle to the current path as a closed figure. /// /// The rectangle bounds. /// The x and y radius of each corner. /// The . public PathBuilder AddRoundedRectangle(RectangleF rectangle, float radius) => this.AddRoundedRectangle(rectangle, new SizeF(radius, radius)); /// /// Adds a rounded rectangle to the current path as a closed figure. /// /// The rectangle bounds. /// The x and y radii of each corner. /// The . public PathBuilder AddRoundedRectangle(RectangleF rectangle, SizeF radius) { _ = this.StartFigure(); foreach (ILineSegment segment in new RoundedRectanglePolygon(rectangle, radius).LineSegments) { _ = this.AddSegment(segment); } return this.CloseFigure(); } /// /// Adds a rounded rectangle to the current path as a closed figure. /// /// The rectangle bounds. /// The x and y radius of each corner. /// The . public PathBuilder AddRoundedRectangle(Rectangle rectangle, float radius) => this.AddRoundedRectangle((RectangleF)rectangle, radius); /// /// Adds a rounded rectangle to the current path as a closed figure. /// /// The rectangle bounds. /// The x and y radii of each corner. /// The . public PathBuilder AddRoundedRectangle(Rectangle rectangle, SizeF radius) => this.AddRoundedRectangle((RectangleF)rectangle, radius); /// /// Adds a rounded rectangle to the current path as a closed figure. /// /// The x-coordinate of the rectangle. /// The y-coordinate of the rectangle. /// The rectangle width. /// The rectangle height. /// The x and y radius of each corner. /// The . public PathBuilder AddRoundedRectangle(float x, float y, float width, float height, float radius) => this.AddRoundedRectangle(new RectangleF(x, y, width, height), radius); /// /// Adds a rounded rectangle to the current path as a closed figure. /// /// The x-coordinate of the rectangle. /// The y-coordinate of the rectangle. /// The rectangle width. /// The rectangle height. /// The x and y radii of each corner. /// The . public PathBuilder AddRoundedRectangle(float x, float y, float width, float height, SizeF radius) => this.AddRoundedRectangle(new RectangleF(x, y, width, height), radius); /// /// Adds a polygon to the current path as a closed figure. /// /// The polygon vertices. /// The . public PathBuilder AddPolygon(IEnumerable points) { Guard.NotNull(points, nameof(points)); return this.AddPolygon([.. points]); } /// /// Adds a polygon to the current path as a closed figure. /// /// The polygon vertices. /// The . public PathBuilder AddPolygon(params PointF[] points) { Guard.NotNull(points, nameof(points)); _ = this.StartFigure(); _ = this.AddSegment(new LinearLineSegment(points)); return this.CloseFigure(); } /// /// Adds a regular polygon to the current path as a closed figure. /// /// The center point of the polygon. /// The number of polygon vertices. /// The polygon radius. /// The . public PathBuilder AddRegularPolygon(PointF center, int vertices, float radius) => this.AddRegularPolygon(center, vertices, radius, 0F); /// /// Adds a regular polygon to the current path as a closed figure. /// /// The center point of the polygon. /// The number of polygon vertices. /// The polygon radius. /// The polygon rotation angle in degrees. /// The . public PathBuilder AddRegularPolygon(PointF center, int vertices, float radius, float angle) { _ = this.StartFigure(); foreach (ILineSegment segment in new RegularPolygon(center, vertices, radius, angle).LineSegments) { _ = this.AddSegment(segment); } return this.CloseFigure(); } /// /// Adds a regular polygon to the current path as a closed figure. /// /// The x-coordinate of the polygon center. /// The y-coordinate of the polygon center. /// The number of polygon vertices. /// The polygon radius. /// The . public PathBuilder AddRegularPolygon(float x, float y, int vertices, float radius) => this.AddRegularPolygon(new PointF(x, y), vertices, radius); /// /// Adds a regular polygon to the current path as a closed figure. /// /// The x-coordinate of the polygon center. /// The y-coordinate of the polygon center. /// The number of polygon vertices. /// The polygon radius. /// The polygon rotation angle in degrees. /// The . public PathBuilder AddRegularPolygon(float x, float y, int vertices, float radius, float angle) => this.AddRegularPolygon(new PointF(x, y), vertices, radius, angle); /// /// Adds a star to the current path as a closed figure. /// /// The center point of the star. /// The number of star prongs. /// The inner star radius. /// The outer star radius. /// The . public PathBuilder AddStar(PointF center, int prongs, float innerRadii, float outerRadii) => this.AddStar(center, prongs, innerRadii, outerRadii, 0F); /// /// Adds a star to the current path as a closed figure. /// /// The center point of the star. /// The number of star prongs. /// The inner star radius. /// The outer star radius. /// The star rotation angle in degrees. /// The . public PathBuilder AddStar(PointF center, int prongs, float innerRadii, float outerRadii, float angle) { _ = this.StartFigure(); foreach (ILineSegment segment in new StarPolygon(center, prongs, innerRadii, outerRadii, angle).LineSegments) { _ = this.AddSegment(segment); } return this.CloseFigure(); } /// /// Adds a star to the current path as a closed figure. /// /// The x-coordinate of the star center. /// The y-coordinate of the star center. /// The number of star prongs. /// The inner star radius. /// The outer star radius. /// The . public PathBuilder AddStar(float x, float y, int prongs, float innerRadii, float outerRadii) => this.AddStar(new PointF(x, y), prongs, innerRadii, outerRadii); /// /// Adds a star to the current path as a closed figure. /// /// The x-coordinate of the star center. /// The y-coordinate of the star center. /// The number of star prongs. /// The inner star radius. /// The outer star radius. /// The star rotation angle in degrees. /// The . public PathBuilder AddStar(float x, float y, int prongs, float innerRadii, float outerRadii, float angle) => this.AddStar(new PointF(x, y), prongs, innerRadii, outerRadii, angle); /// /// Starts a new figure but leaves the previous one open. /// /// The . public PathBuilder StartFigure() { if (!this.currentFigure.IsEmpty) { this.currentFigure = new Figure(); this.figures.Add(this.currentFigure); } else { this.currentFigure.IsClosed = false; } return this; } /// /// Closes the current figure. /// /// The . public PathBuilder CloseFigure() { this.currentFigure.IsClosed = true; _ = this.StartFigure(); return this; } /// /// Closes the current figure. /// /// The . public PathBuilder CloseAllFigures() { foreach (Figure f in this.figures) { f.IsClosed = true; } _ = this.CloseFigure(); return this; } /// /// Builds a complex polygon from the current working set of working operations. /// /// The current set of operations as a complex polygon public IPath Build() { IPath[] paths = [.. this.figures.Where(x => !x.IsEmpty).Select(x => x.Build())]; if (paths.Length == 1) { return paths[0]; } return new ComplexPolygon(paths); } /// /// Resets this instance, clearing any drawn paths and resetting any transforms. /// /// The . public PathBuilder Reset() { this.Clear(); _ = this.ResetTransform(); this.currentPoint = default; return this; } /// /// Clears all drawn paths, Leaving any applied transforms. /// [MemberNotNull(nameof(currentFigure))] public void Clear() { this.currentFigure = new Figure(); this.figures.Clear(); this.figures.Add(this.currentFigure); } private class Figure { private readonly List segments = []; public bool IsClosed { get; set; } public bool IsEmpty => this.segments.Count == 0; public void AddSegment(ILineSegment segment) => this.segments.Add(segment); public IPath Build() => this.IsClosed ? new Polygon([.. this.segments], true) : new Path(this.segments.ToArray()); } } }