// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Drawing {
///
/// Represents a line segment that contains radii and angles that will be rendered as a elliptical arc.
///
public class ArcLineSegment : ILineSegment
{
private const float ZeroTolerance = 1e-05F;
private readonly PointF[] linePoints;
///
/// Initializes a new instance of the class.
///
/// The absolute coordinates of the current point on the path.
/// The absolute coordinates of the final point of the arc.
/// The radii of the ellipse (also known as its semi-major and semi-minor axes).
/// The angle, in degrees, from the x-axis of the current coordinate system to the x-axis of the ellipse.
///
/// 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.
///
public ArcLineSegment(PointF from, PointF to, SizeF radius, float rotation, bool largeArc, bool sweep)
{
rotation = GeometryUtilities.DegreeToRadian(rotation);
bool ellipse = largeArc && ((Vector2)to - (Vector2)from).LengthSquared() < ZeroTolerance && radius.Width > 0 && radius.Height > 0;
if (ellipse)
{
// The circle always has a start angle of 0 which is positioned at 3 o'clock.
// This means the centre point is to the left of the start position.
Vector2 center = (Vector2)from - new Vector2(radius.Width, 0);
this.linePoints = EllipticArcToBezierCurve(from, center, radius, rotation, 0, sweep ? 2 * MathF.PI : -2 * MathF.PI);
}
else
{
this.linePoints = EllipticArcFromEndParams(from, to, radius, rotation, largeArc, sweep);
}
this.Bounds = CalculateBounds(this.linePoints);
}
///
/// Initializes a new instance of the class.
///
/// The coordinates of the center of the ellipse.
/// The radii of the ellipse (also known as its semi-major and semi-minor axes).
/// 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.
public ArcLineSegment(PointF center, SizeF radius, float rotation, float startAngle, float sweepAngle)
{
rotation = GeometryUtilities.DegreeToRadian(rotation);
startAngle = GeometryUtilities.DegreeToRadian(Clamp(startAngle, -360F, 360F));
sweepAngle = GeometryUtilities.DegreeToRadian(Clamp(sweepAngle, -360F, 360F));
Vector2 from = EllipticArcPoint(center, radius, rotation, startAngle);
Vector2 to = EllipticArcPoint(center, radius, rotation, startAngle + sweepAngle);
bool largeArc = Math.Abs(sweepAngle) > MathF.PI;
bool sweep = sweepAngle > 0;
bool ellipse = largeArc && (to - from).LengthSquared() < ZeroTolerance && radius.Width > 0 && radius.Height > 0;
if (ellipse)
{
this.linePoints = EllipticArcToBezierCurve(from, center, radius, rotation, startAngle, sweepAngle);
}
else
{
this.linePoints = EllipticArcFromEndParams(from, to, radius, rotation, largeArc, sweep);
}
this.Bounds = CalculateBounds(this.linePoints);
}
private ArcLineSegment(PointF[] linePoints)
{
this.linePoints = linePoints;
this.Bounds = CalculateBounds(linePoints);
}
///
public PointF StartPoint => this.linePoints[0];
///
public PointF EndPoint => this.linePoints[^1];
///
public RectangleF Bounds { get; }
///
public int LinearVertexCount(Vector2 scale) => this.linePoints.Length;
///
public void CopyTo(Span destination, bool skipFirstPoint, Vector2 scale)
{
int startIndex = skipFirstPoint ? 1 : 0;
ReadOnlySpan source = this.linePoints.AsSpan(startIndex);
if (scale == Vector2.One)
{
source.CopyTo(destination);
return;
}
for (int i = 0; i < source.Length; i++)
{
destination[i] = new PointF(source[i].X * scale.X, source[i].Y * scale.Y);
}
}
///
/// Transforms the current using specified matrix.
///
/// The transformation matrix.
/// An with the matrix applied to it.
public ILineSegment Transform(Matrix4x4 matrix)
{
if (matrix.IsIdentity)
{
return this;
}
PointF[] transformedPoints = new PointF[this.linePoints.Length];
for (int i = 0; i < this.linePoints.Length; i++)
{
transformedPoints[i] = PointF.Transform(this.linePoints[i], matrix);
}
return new ArcLineSegment(transformedPoints);
}
///
ILineSegment ILineSegment.Transform(Matrix4x4 matrix) => this.Transform(matrix);
///
/// Computes the bounds for the retained linearized arc points.
///
private static RectangleF CalculateBounds(ReadOnlySpan points)
{
float minX = float.MaxValue;
float minY = float.MaxValue;
float maxX = float.MinValue;
float maxY = float.MinValue;
for (int i = 0; i < points.Length; i++)
{
PointF point = points[i];
minX = MathF.Min(minX, point.X);
minY = MathF.Min(minY, point.Y);
maxX = MathF.Max(maxX, point.X);
maxY = MathF.Max(maxY, point.Y);
}
return RectangleF.FromLTRB(minX, minY, maxX, maxY);
}
private static PointF[] EllipticArcFromEndParams(
PointF from,
PointF to,
SizeF radius,
float rotation,
bool largeArc,
bool sweep)
{
Vector2 absRadius = Vector2.Abs(radius);
if (EllipticArcOutOfRange(from, to, radius))
{
return [from, to];
}
EndpointToCenterArcParams(from, to, ref absRadius, rotation, largeArc, sweep, out Vector2 center, out Vector2 angles);
return EllipticArcToBezierCurve(from, center, absRadius, rotation, angles.X, angles.Y);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool EllipticArcOutOfRange(Vector2 from, Vector2 to, Vector2 radius)
{
// F.6.2 Out-of-range parameters
radius = Vector2.Abs(radius);
float len = (to - from).LengthSquared();
if (len < ZeroTolerance)
{
return true;
}
if (radius.X < ZeroTolerance || radius.Y < ZeroTolerance)
{
return true;
}
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector2 EllipticArcDerivative(Vector2 r, float xAngle, float t)
=> new(
(-r.X * MathF.Cos(xAngle) * MathF.Sin(t)) - (r.Y * MathF.Sin(xAngle) * MathF.Cos(t)),
(-r.X * MathF.Sin(xAngle) * MathF.Sin(t)) + (r.Y * MathF.Cos(xAngle) * MathF.Cos(t)));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector2 EllipticArcPoint(Vector2 c, Vector2 r, float xAngle, float t)
=> new(
c.X + (r.X * MathF.Cos(xAngle) * MathF.Cos(t)) - (r.Y * MathF.Sin(xAngle) * MathF.Sin(t)),
c.Y + (r.X * MathF.Sin(xAngle) * MathF.Cos(t)) + (r.Y * MathF.Cos(xAngle) * MathF.Sin(t)));
private static PointF[] EllipticArcToBezierCurve(Vector2 from, Vector2 center, Vector2 radius, float xAngle, float startAngle, float sweepAngle)
{
float s = startAngle;
float e = s + sweepAngle;
bool neg = e < s;
float sign = neg ? -1 : 1;
float remain = Math.Abs(e - s);
int curveCount = Math.Max((int)MathF.Ceiling(remain / (MathF.PI / 4F)), 1);
// Arc flattening retains the final point array, so use the builder to avoid the
// intermediate collection and copy a list would generate.
FlattenedPointBuilder points = new(curveCount * 4);
Vector2 prev = EllipticArcPoint(center, radius, xAngle, s);
while (remain > ZeroTolerance)
{
float step = (float)Math.Min(remain, Math.PI / 4);
float signStep = step * sign;
Vector2 p1 = prev;
Vector2 p2 = EllipticArcPoint(center, radius, xAngle, s + signStep);
float alphaT = (float)Math.Tan(signStep / 2);
float alpha = (float)(Math.Sin(signStep) * (Math.Sqrt(4 + (3 * alphaT * alphaT)) - 1) / 3);
Vector2 q1 = p1 + (alpha * EllipticArcDerivative(radius, xAngle, s));
Vector2 q2 = p2 - (alpha * EllipticArcDerivative(radius, xAngle, s + signStep));
CubicBezierLineSegment bezier = new(from, q1, q2, p2);
int bezierCount = bezier.LinearVertexCount(Vector2.One);
Span destination = points.GetAppendSpan(bezierCount);
bezier.CopyTo(destination, skipFirstPoint: false, Vector2.One);
points.Advance(bezierCount);
from = p2;
s += signStep;
remain -= step;
prev = p2;
}
return points.Detach();
}
private static void EndpointToCenterArcParams(
Vector2 p1,
Vector2 p2,
ref Vector2 r,
float xRotation,
bool flagA,
bool flagS,
out Vector2 center,
out Vector2 angles)
{
double rX = Math.Abs(r.X);
double rY = Math.Abs(r.Y);
// (F.6.5.1)
double dx2 = (p1.X - p2.X) / 2.0;
double dy2 = (p1.Y - p2.Y) / 2.0;
double x1p = (Math.Cos(xRotation) * dx2) + (Math.Sin(xRotation) * dy2);
double y1p = (-Math.Sin(xRotation) * dx2) + (Math.Cos(xRotation) * dy2);
// (F.6.5.2)
double rxs = rX * rX;
double rys = rY * rY;
double x1ps = x1p * x1p;
double y1ps = y1p * y1p;
// check if the radius is too small `pq < 0`, when `dq > rxs * rys` (see below)
// cr is the ratio (dq : rxs * rys)
double cr = (x1ps / rxs) + (y1ps / rys);
if (cr > 1)
{
// scale up rX,rY equally so cr == 1
double s = Math.Sqrt(cr);
rX = s * rX;
rY = s * rY;
rxs = rX * rX;
rys = rY * rY;
}
double dq = (rxs * y1ps) + (rys * x1ps);
double pq = ((rxs * rys) - dq) / dq;
double q = Math.Sqrt(Math.Max(0, pq)); // Use Max to account for float precision
if (flagA == flagS)
{
q = -q;
}
double cxp = q * rX * y1p / rY;
double cyp = -q * rY * x1p / rX;
// (F.6.5.3)
double cx = (Math.Cos(xRotation) * cxp) - (Math.Sin(xRotation) * cyp) + ((p1.X + p2.X) / 2);
double cy = (Math.Sin(xRotation) * cxp) + (Math.Cos(xRotation) * cyp) + ((p1.Y + p2.Y) / 2);
// (F.6.5.5)
double theta = SvgAngle(1, 0, (x1p - cxp) / rX, (y1p - cyp) / rY);
// (F.6.5.6)
double delta = SvgAngle((x1p - cxp) / rX, (y1p - cyp) / rY, (-x1p - cxp) / rX, (-y1p - cyp) / rY);
delta %= Math.PI * 2;
if (!flagS && delta > 0)
{
delta -= 2 * Math.PI;
}
if (flagS && delta < 0)
{
delta += 2 * Math.PI;
}
r = new Vector2((float)rX, (float)rY);
center = new Vector2((float)cx, (float)cy);
angles = new Vector2((float)theta, (float)delta);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static float Clamp(float val, float min, float max)
{
if (val < min)
{
return min;
}
else if (val > max)
{
return max;
}
else
{
return val;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static float SvgAngle(double ux, double uy, double vx, double vy)
{
Vector2 u = new((float)ux, (float)uy);
Vector2 v = new((float)vx, (float)vy);
// (F.6.5.4)
float dot = Vector2.Dot(u, v);
float len = u.Length() * v.Length();
float ang = (float)Math.Acos(Clamp(dot / len, -1, 1)); // floating point precision, slightly over values appear
if (((u.X * v.Y) - (u.Y * v.X)) < 0)
{
ang = -ang;
}
return ang;
}
}
}