// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System; using System.Collections.Generic; using System.Numerics; using System.Threading; using SixLabors.ImageSharp.Drawing.Helpers; namespace SixLabors.ImageSharp.Drawing { /// /// Represents a line segment that contains a lists of control points that will be rendered as a cubic bezier curve /// /// public sealed class CubicBezierLineSegment : ILineSegment { // Code for this taken from private const float MinimumSqrDistance = 1.75f; private const float DivisionThreshold = -.9995f; private readonly PointF[] controlPoints; private FlattenedCache? flattenedCache; /// /// Initializes a new instance of the class. /// /// The points. public CubicBezierLineSegment(PointF[] points) { Guard.NotNull(points, nameof(points)); Guard.MustBeGreaterThanOrEqualTo(points.Length, 4, nameof(points)); Guard.IsTrue((points.Length - 1) % 3 == 0, nameof(points), "points must be a multiple of 3 plus 1 long."); this.controlPoints = points; } /// /// Initializes a new instance of the class. /// /// The start. /// The control point1. /// The control point2. /// The end. /// The additional points. public CubicBezierLineSegment(PointF start, PointF controlPoint1, PointF controlPoint2, PointF end, params PointF[] additionalPoints) : this(new[] { start, controlPoint1, controlPoint2, end }.Concat(additionalPoints)) { } /// public CubicBezierLineSegment(PointF start, PointF controlPoint1, PointF controlPoint2, PointF end) : this([start, controlPoint1, controlPoint2, end]) { } /// /// Gets the control points. /// public IReadOnlyList ControlPoints => this.controlPoints; /// public PointF StartPoint => this.controlPoints[0]; /// public PointF EndPoint => this.controlPoints[^1]; /// public RectangleF Bounds => CalculateBounds(this.GetFlattenedPoints(Vector2.One)); /// public int LinearVertexCount(Vector2 scale) => this.GetFlattenedPoints(scale).Length; /// public void CopyTo(Span destination, bool skipFirstPoint, Vector2 scale) { PointF[] flattened = this.GetFlattenedPoints(scale); int startIndex = skipFirstPoint ? 1 : 0; flattened.AsSpan(startIndex).CopyTo(destination); } /// /// Returns the flattened point run for this curve under , computing it on first /// request and reusing the cached result for subsequent calls at the same scale. /// /// /// Publication uses so a concurrent reader either observes /// or a fully-constructed entry. /// private PointF[] GetFlattenedPoints(Vector2 scale) { FlattenedCache? hit = Volatile.Read(ref this.flattenedCache); if (hit is not null && hit.Scale == scale) { return hit.Points; } PointF[] baked = FlattenCurve(this.controlPoints, scale); Volatile.Write(ref this.flattenedCache, new FlattenedCache(scale, baked)); return baked; } /// /// Gets the control points of this curve. /// /// The control points of this curve. public ReadOnlyMemory GetControlPoints() => this.controlPoints; /// /// Transforms this line segment using the specified matrix. /// /// The matrix. /// A line segment with the matrix applied to it. public CubicBezierLineSegment Transform(Matrix4x4 matrix) { if (matrix.IsIdentity) { // no transform to apply skip it return this; } PointF[] transformedPoints = new PointF[this.controlPoints.Length]; for (int i = 0; i < this.controlPoints.Length; i++) { transformedPoints[i] = PointF.Transform(this.controlPoints[i], matrix); } return new CubicBezierLineSegment(transformedPoints); } /// ILineSegment ILineSegment.Transform(Matrix4x4 matrix) => this.Transform(matrix); /// /// Flattens every cubic in under the supplied device-space /// into a single contiguous point run. Subdivision density is evaluated /// against the scaled control points so the polyline adapts to rendering scale. /// private static PointF[] FlattenCurve(PointF[] controlPoints, Vector2 scale) { int curveCount = (controlPoints.Length - 1) / 3; // Flattened points are cached as a retained array, so use the builder to avoid // the intermediate collection and copy a list would generate. FlattenedPointBuilder output = new(curveCount * 4); for (int curveIndex = 0; curveIndex < curveCount; curveIndex++) { int nodeIndex = curveIndex * 3; Vector2 p0 = new(controlPoints[nodeIndex].X * scale.X, controlPoints[nodeIndex].Y * scale.Y); Vector2 p1 = new(controlPoints[nodeIndex + 1].X * scale.X, controlPoints[nodeIndex + 1].Y * scale.Y); Vector2 p2 = new(controlPoints[nodeIndex + 2].X * scale.X, controlPoints[nodeIndex + 2].Y * scale.Y); Vector2 p3 = new(controlPoints[nodeIndex + 3].X * scale.X, controlPoints[nodeIndex + 3].Y * scale.Y); if (curveIndex == 0) { output.Add((PointF)p0); } SubdivideAndAppend(0F, 1F, p0, p1, p2, p3, ref output, 0); output.Add((PointF)p3); } return output.Detach(); } /// /// Recursively subdivides the scaled cubic segment, appending midpoints in left-to-right order. /// private static void SubdivideAndAppend( float t0, float t1, Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, ref FlattenedPointBuilder output, int depth) { if (depth > 999) { return; } Vector2 left = CalculateBezierPoint(t0, p0, p1, p2, p3); Vector2 right = CalculateBezierPoint(t1, p0, p1, p2, p3); if ((left - right).LengthSquared() < MinimumSqrDistance) { return; } float midT = (t0 + t1) / 2; Vector2 mid = CalculateBezierPoint(midT, p0, p1, p2, p3); Vector2 leftDirection = Vector2.Normalize(left - mid); Vector2 rightDirection = Vector2.Normalize(right - mid); if (Vector2.Dot(leftDirection, rightDirection) > DivisionThreshold || Math.Abs(midT - 0.5f) < 0.0001f) { SubdivideAndAppend(t0, midT, p0, p1, p2, p3, ref output, depth + 1); output.Add((PointF)mid); SubdivideAndAppend(midT, t1, p0, p1, p2, p3, ref output, depth + 1); } } /// /// Calculates the bezier point along the line. /// /// The position within the line. /// The p 0. /// The p 1. /// The p 2. /// The p 3. /// /// The . /// private static Vector2 CalculateBezierPoint(float t, Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3) { float u = 1 - t; float tt = t * t; float uu = u * u; float uuu = uu * u; float ttt = tt * t; Vector2 p = uuu * p0; // first term p += 3 * uu * t * p1; // second term p += 3 * u * tt * p2; // third term p += ttt * p3; // fourth term return p; } /// /// Computes the bounds for the cached linearized bezier 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 sealed class FlattenedCache(Vector2 scale, PointF[] points) { public Vector2 Scale { get; } = scale; public PointF[] Points { get; } = points; } } }