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