// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System; using System.Buffers; using System.Numerics; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Drawing.Processing.Backends { internal static partial class DefaultRasterizer { /// /// Base retained stroke linearizer that expands stroked centerlines once into row-local line storage. /// /// The mutable per-row retained line collector type. private abstract class StrokeLinearizer : Linearizer where TL : class { private const float StrokeMicroSegmentEpsilon = 1F / 64F; private readonly StrokeStyle stroke; /// /// Initializes a new instance of the class. /// /// The stroked centerline geometry. /// The residual transform applied to each source point during emission. /// The stroke style. /// The destination-space X translation applied at composition time. /// The destination-space Y translation applied at composition time. /// The minimum destination X bound after clipping. /// The minimum destination Y bound after clipping. /// The visible destination width in pixels. /// The visible destination height in pixels. /// The first retained row-band index. /// The retained row-band count. /// The horizontal sampling offset. /// The vertical sampling offset. /// The allocator used for retained start-cover storage. protected StrokeLinearizer( LinearGeometry geometry, Matrix4x4 residual, StrokeStyle stroke, int translateX, int translateY, int minX, int minY, int width, int height, int firstBandIndex, int rowBandCount, float samplingOffsetX, float samplingOffsetY, MemoryAllocator allocator) : base(geometry, residual, translateX, translateY, minX, minY, width, height, firstBandIndex, rowBandCount, samplingOffsetX, samplingOffsetY, allocator) => this.stroke = stroke; private enum ContourInterest { Outside, Clipped, Contained } /// protected override bool ProcessCore() { ReadOnlySpan contours = this.Geometry.GetContours(); for (int contourIndex = 0; contourIndex < contours.Length; contourIndex++) { LinearContour contour = contours[contourIndex]; if (contour.PointCount == 0) { continue; } ReadOnlySpan contourPoints = this.Geometry.GetContourPoints(contour); ContourInterest contourInterest = this.GetContourInterest(contourPoints); if (contourInterest == ContourInterest.Outside) { continue; } bool isClosed = this.IsContourClosedForEmission(contourPoints, contour.IsClosed); this.ProcessContour(contourPoints, isClosed, contourInterest == ContourInterest.Contained); } if (!this.HasAnyCoverage) { return false; } this.FinalizeLines(); return true; } /// /// Classifies one stroked contour against the interest bounds. /// /// The contour points. /// The contour's relationship to the interest bounds. private ContourInterest GetContourInterest(ReadOnlySpan contourPoints) { RectangleF translatedBounds = InflateStrokeBounds(this.GetPointBounds(contourPoints), this.stroke); translatedBounds.Offset(this.TranslateX + this.SamplingOffsetX - this.MinX, this.TranslateY + this.SamplingOffsetY - this.MinY); if (translatedBounds.Right <= 0F || translatedBounds.Bottom <= 0F || translatedBounds.Left >= this.Width || translatedBounds.Top >= this.Height) { return ContourInterest.Outside; } if (translatedBounds.Left >= 0F && translatedBounds.Top >= 0F && translatedBounds.Right <= this.Width && translatedBounds.Bottom <= this.Height) { return ContourInterest.Contained; } return ContourInterest.Clipped; } /// /// Returns whether a contour should be treated as closed when emitting stroke geometry. /// /// The contour points. /// Indicates whether the contour is explicitly closed. /// when the contour should be stroked as closed; otherwise . private bool IsContourClosedForEmission(ReadOnlySpan contourPoints, bool isDeclaredClosed) { if (contourPoints.Length < 3) { return false; } PointF first = this.TransformPoint(contourPoints[0]); PointF last = this.TransformPoint(contourPoints[^1]); if (isDeclaredClosed || first == last) { return true; } Vector2 delta = first - last; float closeThreshold = MathF.Max(this.stroke.Width, 1E-3F); return delta.LengthSquared() <= closeThreshold * closeThreshold; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private PointF TransformPoint(PointF point) => this.HasResidual ? PointF.Transform(point, this.Residual) : point; /// /// Processes one centerline contour. /// /// The contiguous contour points. /// Indicates whether the contour is closed. /// Indicates whether the stroked contour is fully contained within the interest. private void ProcessContour(ReadOnlySpan contourPoints, bool isClosed, bool contained) { using IMemoryOwner rentedSegmentsOwner = this.Allocator.Allocate(contourPoints.Length); Span rentedSegments = rentedSegmentsOwner.Memory.Span; int segmentCount = this.BuildContourSegments( contourPoints, isClosed, rentedSegments, out int distinctPointCount, out PointF pointLike); if (segmentCount == 0) { this.EmitPointStrokeContour(pointLike, contained); return; } if (segmentCount == 1 || distinctPointCount == 2) { StrokeContourSegment segment = rentedSegments[0]; this.EmitOpenSegmentStrokeContour(segment.Start, segment.End, contained); return; } if (isClosed) { this.EmitClosedStrokeContour(rentedSegments[..segmentCount], contained); return; } this.EmitOpenStrokeContour(rentedSegments[..segmentCount], contained); } /// /// Builds one contour-local stroke segment array while collapsing immediate duplicate points. /// /// The contiguous contour points. /// Indicates whether the contour is closed. /// The destination segment buffer. /// Receives the number of distinct contour points. /// Receives the fallback point for degenerate contours. /// The number of emitted contour segments. private int BuildContourSegments( ReadOnlySpan contourPoints, bool isClosed, Span segments, out int distinctPointCount, out PointF pointLike) { pointLike = default; distinctPointCount = 0; if (contourPoints.IsEmpty) { return 0; } Matrix4x4 residual = this.Residual; bool hasResidual = this.HasResidual; PointF firstPoint = hasResidual ? PointF.Transform(contourPoints[0], residual) : contourPoints[0]; PointF previousPoint = firstPoint; pointLike = firstPoint; distinctPointCount = 1; int segmentCount = 0; for (int i = 1; i < contourPoints.Length; i++) { PointF point = hasResidual ? PointF.Transform(contourPoints[i], residual) : contourPoints[i]; if (point == previousPoint) { continue; } if (TryCreateStrokeContourSegment(previousPoint, point, out StrokeContourSegment segment)) { distinctPointCount++; segments[segmentCount++] = segment; previousPoint = point; } pointLike = point; } if (isClosed && distinctPointCount > 1 && previousPoint == firstPoint) { distinctPointCount--; } if (isClosed && segmentCount > 1 && previousPoint != firstPoint && TryCreateStrokeContourSegment(previousPoint, firstPoint, out StrokeContourSegment closingSegment)) { segments[segmentCount++] = closingSegment; } return segmentCount; } /// /// Creates one contour-local stroke segment descriptor. /// /// The segment start point. /// The segment end point. /// Receives the segment descriptor. /// when a non-degenerate segment exists. private static bool TryCreateStrokeContourSegment(PointF start, PointF end, out StrokeContourSegment segment) { if (Vector2.DistanceSquared(start, end) <= StrokeMicroSegmentEpsilon * StrokeMicroSegmentEpsilon) { segment = default; return false; } if (!TryGetDirection(start, end, out Vector2 tangent, out float length)) { segment = default; return false; } segment = new StrokeContourSegment(start, end, tangent, length); return true; } /// /// Gets the point bounds for one contour. /// /// The contiguous contour points. /// The contour point bounds. private RectangleF GetPointBounds(ReadOnlySpan contourPoints) { Matrix4x4 residual = this.Residual; bool hasResidual = this.HasResidual; PointF first = hasResidual ? PointF.Transform(contourPoints[0], residual) : contourPoints[0]; float minX = first.X; float minY = first.Y; float maxX = minX; float maxY = minY; for (int i = 1; i < contourPoints.Length; i++) { PointF point = hasResidual ? PointF.Transform(contourPoints[i], residual) : contourPoints[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); } /// /// Emits one stroked open segment. /// /// The segment start point. /// The segment end point. /// Indicates whether the segment is fully contained within the interest. private void EmitOpenSegmentStrokeContour(PointF start, PointF end, bool contained) { if (!TryGetDirection(start, end, out Vector2 tangent, out _)) { this.EmitPointStrokeContour(start, contained); return; } float halfWidth = this.stroke.HalfWidth; Vector2 normal = GetStrokeOffsetNormal(tangent) * halfWidth; Vector2 extension = this.stroke.LineCap == LineCap.Square ? tangent * halfWidth : Vector2.Zero; Vector2 startVector = start; Vector2 endVector = end; PointF p0 = startVector + normal - extension; PointF p1 = endVector + normal + extension; PointF p2 = endVector - normal + extension; PointF p3 = startVector - normal - extension; this.EmitLine(p0, p1, contained); if (this.stroke.LineCap == LineCap.Round) { this.EmitDirectedArcContour(endVector, normal, -normal, contained); } else { this.EmitLine(p1, p2, contained); } this.EmitLine(p2, p3, contained); if (this.stroke.LineCap == LineCap.Round) { this.EmitDirectedArcContour(startVector, -normal, normal, contained); } else { this.EmitLine(p3, p0, contained); } } /// /// Emits one stroked open multi-segment contour from precomputed contour-local segments. /// /// The precomputed contour-local segments. /// Indicates whether the contour is fully contained within the interest. private void EmitOpenStrokeContour(ReadOnlySpan segments, bool contained) { StrokeContourSegment startSegment = segments[0]; StrokeContourSegment endSegment = segments[^1]; float halfWidth = this.stroke.HalfWidth; Vector2 startNormal = startSegment.Normal * halfWidth; Vector2 endNormal = endSegment.Normal * halfWidth; Vector2 startExtension = this.stroke.LineCap == LineCap.Square ? startSegment.Tangent * halfWidth : Vector2.Zero; Vector2 endExtension = this.stroke.LineCap == LineCap.Square ? endSegment.Tangent * halfWidth : Vector2.Zero; Vector2 startPoint = startSegment.Start; Vector2 endPoint = endSegment.End; ContourState strokeContour = default; this.AppendContourPoint(ref strokeContour, startPoint + startNormal - startExtension, contained); for (int i = 1; i < segments.Length; i++) { StrokeContourSegment previousSegment = segments[i - 1]; StrokeContourSegment nextSegment = segments[i]; // Forward traversal: (v0, v1, v2) = (prev.Start, shared_vertex, next.End). this.AppendSideJoinContour( ref strokeContour, previousSegment.Start, previousSegment.End, nextSegment.End, previousSegment.Length, nextSegment.Length, contained); } this.AppendContourPoint(ref strokeContour, endPoint + endNormal + endExtension, contained); if (this.stroke.LineCap == LineCap.Round) { this.AppendDirectedArcContour(ref strokeContour, endPoint, endNormal, -endNormal, contained); } else { this.AppendContourPoint(ref strokeContour, endPoint - endNormal + endExtension, contained); } for (int i = segments.Length - 1; i >= 1; i--) { StrokeContourSegment previousSegment = segments[i]; StrokeContourSegment nextSegment = segments[i - 1]; // Reverse traversal: vertex order reversed so PolygonStroker's Outline2 // state machine lines up with the port below. this.AppendSideJoinContour( ref strokeContour, previousSegment.End, previousSegment.Start, nextSegment.Start, previousSegment.Length, nextSegment.Length, contained); } this.AppendContourPoint(ref strokeContour, startPoint - startNormal - startExtension, contained); if (this.stroke.LineCap == LineCap.Round) { this.AppendDirectedArcContour(ref strokeContour, startPoint, -startNormal, startNormal, contained); } this.CloseContour(ref strokeContour, contained); } /// /// Emits the two stroked contours for a closed contour from precomputed contour-local segments. /// /// The precomputed contour-local segments. /// Indicates whether the contour is fully contained within the interest. private void EmitClosedStrokeContour(ReadOnlySpan segments, bool contained) { ContourState leftContour = default; for (int i = 0; i < segments.Length; i++) { StrokeContourSegment previousSegment = i == 0 ? segments[^1] : segments[i - 1]; StrokeContourSegment nextSegment = segments[i]; this.AppendSideJoinContour( ref leftContour, previousSegment.Start, nextSegment.Start, nextSegment.End, previousSegment.Length, nextSegment.Length, contained); } this.CloseContour(ref leftContour, contained); ContourState reversedContour = default; for (int i = segments.Length - 1; i >= 0; i--) { StrokeContourSegment previousSegment = segments[i]; StrokeContourSegment nextSegment = i == 0 ? segments[^1] : segments[i - 1]; this.AppendSideJoinContour( ref reversedContour, previousSegment.End, previousSegment.Start, nextSegment.Start, previousSegment.Length, nextSegment.Length, contained); } this.CloseContour(ref reversedContour, contained); } /// /// Emits a point-like stroke as a cap contour. /// /// The point-like stroke location. /// Indicates whether the contour is fully contained within the interest. private void EmitPointStrokeContour(PointF point, bool contained) { Vector2 center = point; float halfWidth = this.stroke.HalfWidth; if (this.stroke.LineCap == LineCap.Round) { Vector2 startOffset = new(halfWidth, 0F); this.EmitDirectedArcContour(center, startOffset, -startOffset, contained); this.EmitDirectedArcContour(center, -startOffset, startOffset, contained); return; } PointF p0 = center + new Vector2(-halfWidth, -halfWidth); PointF p1 = center + new Vector2(halfWidth, -halfWidth); PointF p2 = center + new Vector2(halfWidth, halfWidth); PointF p3 = center + new Vector2(-halfWidth, halfWidth); this.EmitLine(p0, p1, contained); this.EmitLine(p1, p2, contained); this.EmitLine(p2, p3, contained); this.EmitLine(p3, p0, contained); } /// /// Emits one round cap or join arc directly into the retained line storage. /// /// The arc center. /// The start offset from the center. /// The end offset from the center. /// Indicates whether the arc is fully contained within the interest. private void EmitDirectedArcContour( Vector2 center, Vector2 fromOffset, Vector2 toOffset, bool contained) { if (fromOffset == Vector2.Zero || toOffset == Vector2.Zero) { this.EmitLine(center + fromOffset, center + toOffset, contained); return; } float radius = fromOffset.Length(); if (radius <= StrokeDirectionEpsilon) { this.EmitLine(center + fromOffset, center + toOffset, contained); return; } double startAngle = Math.Atan2(fromOffset.Y, fromOffset.X); double endAngle = Math.Atan2(toOffset.Y, toOffset.X); double sweep = NormalizePositiveAngle(endAngle - startAngle); int subdivisionCount = GetArcSubdivisionCount(radius, sweep, this.stroke.ArcDetailScale); double step = sweep / (subdivisionCount + 1); PointF previousPoint = center + fromOffset; for (int i = 1; i <= subdivisionCount; i++) { float angle = (float)(startAngle + (step * i)); PointF point = center + new Vector2(MathF.Cos(angle) * radius, MathF.Sin(angle) * radius); this.EmitLine(previousPoint, point, contained); previousPoint = point; } this.EmitLine(previousPoint, center + toOffset, contained); } /// /// Appends a contour arc directly to the active stroke contour. /// /// The active contour state. /// The arc center. /// The start offset from the center. /// The end offset from the center. /// Indicates whether the arc is fully contained within the interest. private void AppendDirectedArcContour( ref ContourState contour, Vector2 center, Vector2 fromOffset, Vector2 toOffset, bool contained) { if (fromOffset == Vector2.Zero || toOffset == Vector2.Zero) { this.AppendContourPoint(ref contour, center + toOffset, contained); return; } float radius = fromOffset.Length(); if (radius <= StrokeDirectionEpsilon) { this.AppendContourPoint(ref contour, center + toOffset, contained); return; } double startAngle = Math.Atan2(fromOffset.Y, fromOffset.X); double endAngle = Math.Atan2(toOffset.Y, toOffset.X); double sweep = NormalizePositiveAngle(endAngle - startAngle); int subdivisionCount = GetArcSubdivisionCount(radius, sweep, this.stroke.ArcDetailScale); double step = sweep / (subdivisionCount + 1); for (int i = 1; i <= subdivisionCount; i++) { float angle = (float)(startAngle + (step * i)); this.AppendContourPoint( ref contour, center + new Vector2(MathF.Cos(angle) * radius, MathF.Sin(angle) * radius), contained); } this.AppendContourPoint(ref contour, center + toOffset, contained); } /// /// Appends one side join point sequence directly to the active stroke contour. /// /// /// Direct port of PolygonStroker.CalcJoin so the rasterizer emits the same /// join geometry as the reference CPU stroker. Each side of the outline calls this /// once per source vertex; the reverse side reverses the vertex order exactly /// like PolygonStroker's Outline2 state. /// /// The active contour state. /// Previous source vertex in the emission's traversal order. /// Current source vertex (the corner). /// Next source vertex in the emission's traversal order. /// Length of segment v0-v1. /// Length of segment v1-v2. /// Indicates whether the join is fully contained within the interest. private void AppendSideJoinContour( ref ContourState contour, Vector2 v0, Vector2 v1, Vector2 v2, float len1, float len2, bool contained) { float eps = StrokeDirectionEpsilon; float halfWidth = this.stroke.HalfWidth; float widthAbs = halfWidth; float strokeWidth = halfWidth; if (len1 < eps || len2 < eps) { // Degenerate neighborhood: fall back to best available segment direction. float l1 = len1 >= eps ? len1 : len2; float l2 = len2 >= eps ? len2 : len1; float invL1 = strokeWidth / l1; float invL2 = strokeWidth / l2; Vector2 seg1 = v1 - v0; Vector2 seg2 = v2 - v1; float offX1 = seg1.Y * invL1; float offY1 = seg1.X * invL1; float offX2 = seg2.Y * invL2; float offY2 = seg2.X * invL2; this.AppendContourPoint(ref contour, new Vector2(v1.X + offX1, v1.Y - offY1), contained); this.AppendContourPoint(ref contour, new Vector2(v1.X + offX2, v1.Y - offY2), contained); return; } Vector2 segForward = v1 - v0; Vector2 segNext = v2 - v1; float invLen1 = strokeWidth / len1; float invLen2 = strokeWidth / len2; float dx1 = segForward.Y * invLen1; float dy1 = segForward.X * invLen1; float dx2 = segNext.Y * invLen2; float dy2 = segNext.X * invLen2; float cp = Cross(segNext, segForward); if (MathF.Abs(cp) > float.Epsilon && cp > 0F) { float limit = MathF.Min(len1, len2) / widthAbs; if (limit < 1.01F) { limit = 1.01F; } this.CalcMiter(ref contour, v0, v1, v2, dx1, dy1, dx2, dy2, LineJoin.MiterRevert, limit, 0F, contained); return; } // Outer corner. Vector2 averageOffset = new Vector2(dx1 + dx2, dy1 + dy2) * 0.5F; float bevelDistance = averageOffset.Length(); float widthEps = widthAbs / 1024F; if ((this.stroke.LineJoin is LineJoin.Round or LineJoin.Bevel) && ((float)this.stroke.ArcDetailScale * (widthAbs - bevelDistance)) < widthEps) { Vector2 outerOffset1 = new(dx1, -dy1); Vector2 outerOffset2 = new(dx2, -dy2); if (TryCalcIntersection(v0 + outerOffset1, v1 + outerOffset1, v1 + outerOffset2, v2 + outerOffset2, out Vector2 intersection)) { this.AppendContourPoint(ref contour, intersection, contained); } else { this.AppendContourPoint(ref contour, new Vector2(v1.X + dx1, v1.Y - dy1), contained); } return; } switch (this.stroke.LineJoin) { case LineJoin.Miter: case LineJoin.MiterRevert: case LineJoin.MiterRound: this.CalcMiter(ref contour, v0, v1, v2, dx1, dy1, dx2, dy2, this.stroke.LineJoin, (float)this.stroke.MiterLimit, bevelDistance, contained); break; case LineJoin.Round: this.CalcArc(ref contour, v1.X, v1.Y, dx1, -dy1, dx2, -dy2, contained); break; default: this.AppendContourPoint(ref contour, new Vector2(v1.X + dx1, v1.Y - dy1), contained); this.AppendContourPoint(ref contour, new Vector2(v1.X + dx2, v1.Y - dy2), contained); break; } } /// /// Direct port of PolygonStroker.CalcMiter. Emits the miter apex (or the /// configured overflow fallback) at the join vertex. /// private void CalcMiter( ref ContourState contour, Vector2 v0, Vector2 v1, Vector2 v2, float dx1, float dy1, float dx2, float dy2, LineJoin lineJoin, float miterLimit, float bevelDistance, bool contained) { Vector2 p0 = v0; Vector2 p1 = v1; Vector2 p2 = v2; Vector2 offset1 = new(dx1, -dy1); Vector2 offset2 = new(dx2, -dy2); float xi = v1.X; float yi = v1.Y; float intersectionDistance = 1F; float limit = this.stroke.HalfWidth * miterLimit; bool miterLimitExceeded = true; bool intersectionFailed = true; if (TryCalcIntersection(p0 + offset1, p1 + offset1, p1 + offset2, p2 + offset2, out Vector2 intersection)) { xi = intersection.X; yi = intersection.Y; intersectionDistance = Vector2.Distance(p1, intersection); if (intersectionDistance <= limit) { this.AppendContourPoint(ref contour, intersection, contained); miterLimitExceeded = false; } intersectionFailed = false; } else { // Parallel/near-parallel fallback: probe a candidate offset point. Vector2 probe = new(v1.X + dx1, v1.Y - dy1); if ((CrossProduct(v0, v1, probe) < 0F) == (CrossProduct(v1, v2, probe) < 0F)) { this.AppendContourPoint(ref contour, probe, contained); miterLimitExceeded = false; } } if (!miterLimitExceeded) { return; } switch (lineJoin) { case LineJoin.MiterRevert: this.AppendContourPoint(ref contour, new Vector2(v1.X + dx1, v1.Y - dy1), contained); this.AppendContourPoint(ref contour, new Vector2(v1.X + dx2, v1.Y - dy2), contained); break; case LineJoin.MiterRound: this.CalcArc(ref contour, v1.X, v1.Y, dx1, -dy1, dx2, -dy2, contained); break; default: if (intersectionFailed) { // No reliable apex: project a clipped bevel using local tangent/perpendicular vectors. this.AppendContourPoint( ref contour, new Vector2(v1.X + dx1 + (dy1 * miterLimit), v1.Y - dy1 + (dx1 * miterLimit)), contained); this.AppendContourPoint( ref contour, new Vector2(v1.X + dx2 - (dy2 * miterLimit), v1.Y - dy2 - (dx2 * miterLimit)), contained); } else { float x1 = v1.X + dx1; float y1 = v1.Y - dy1; float x2 = v1.X + dx2; float y2 = v1.Y - dy2; float ratio = (limit - bevelDistance) / (intersectionDistance - bevelDistance); this.AppendContourPoint(ref contour, new Vector2(x1 + ((xi - x1) * ratio), y1 + ((yi - y1) * ratio)), contained); this.AppendContourPoint(ref contour, new Vector2(x2 + ((xi - x2) * ratio), y2 + ((yi - y2) * ratio)), contained); } break; } } /// /// Direct port of PolygonStroker.CalcArc. Emits intermediate arc vertices /// around a join center between two offset vectors. /// private void CalcArc( ref ContourState contour, float x, float y, float dx1, float dy1, float dx2, float dy2, bool contained) { float strokeWidth = this.stroke.HalfWidth; double a1 = Math.Atan2(dy1, dx1); double a2 = Math.Atan2(dy2, dx2); double widthAbs = strokeWidth; double da = Math.Acos(widthAbs / (widthAbs + (0.125D / this.stroke.ArcDetailScale))) * 2D; this.AppendContourPoint(ref contour, new Vector2(x + dx1, y + dy1), contained); if (a1 > a2) { a2 += Math.PI * 2D; } int n = (int)((a2 - a1) / da); da = (a2 - a1) / (n + 1); a1 += da; for (int i = 0; i < n; i++) { this.AppendContourPoint( ref contour, new Vector2((float)(x + (Math.Cos(a1) * strokeWidth)), (float)(y + (Math.Sin(a1) * strokeWidth))), contained); a1 += da; } this.AppendContourPoint(ref contour, new Vector2(x + dx2, y + dy2), contained); } /// /// Signed area of triangle (a, b, point), matching PolygonStroker.CrossProduct. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static float CrossProduct(Vector2 a, Vector2 b, Vector2 point) => ((point.X - b.X) * (b.Y - a.Y)) - ((point.Y - b.Y) * (b.X - a.X)); /// /// Intersects two infinite lines defined by point pairs (a, b) and (c, d), /// matching PolygonStroker.TryCalcIntersection. /// private static bool TryCalcIntersection(Vector2 a, Vector2 b, Vector2 c, Vector2 d, out Vector2 intersection) { const float eps = 1e-7F; Vector2 ab = b - a; Vector2 cd = d - c; float denominator = Cross(ab, cd); if (MathF.Abs(denominator) < eps) { intersection = default; return false; } float t = Cross(c - a, cd) / denominator; intersection = a + (ab * t); return true; } /// /// Appends one point to the active contour. /// /// The active contour state. /// The point to append. /// Indicates whether the contour is fully contained within the interest. private void AppendContourPoint(ref ContourState state, PointF point, bool contained) { if (!state.HasPoint) { state.HasPoint = true; state.FirstPoint = point; state.PreviousPoint = point; return; } if (state.PreviousPoint == point) { return; } this.EmitLine(state.PreviousPoint, point, contained); state.PreviousPoint = point; } /// /// Closes the active contour. /// /// The active contour state. /// Indicates whether the contour is fully contained within the interest. private void CloseContour(ref ContourState state, bool contained) { if (!state.HasPoint || state.PreviousPoint == state.FirstPoint) { return; } this.EmitLine(state.PreviousPoint, state.FirstPoint, contained); state.PreviousPoint = state.FirstPoint; } /// /// Emits one stroked boundary edge into retained line storage. /// /// The edge start point. /// The edge end point. /// Indicates whether the edge is fully contained within the interest. private void EmitLine(PointF start, PointF end, bool contained) { if (contained) { this.AddContainedLineF24Dot8( FloatToFixed24Dot8(((start.X + this.TranslateX) - this.MinX) + this.SamplingOffsetX), FloatToFixed24Dot8(((start.Y + this.TranslateY) - this.MinY) + this.SamplingOffsetY), FloatToFixed24Dot8(((end.X + this.TranslateX) - this.MinX) + this.SamplingOffsetX), FloatToFixed24Dot8(((end.Y + this.TranslateY) - this.MinY) + this.SamplingOffsetY)); return; } this.AddUncontainedLine( ((start.X + this.TranslateX) - this.MinX) + this.SamplingOffsetX, ((start.Y + this.TranslateY) - this.MinY) + this.SamplingOffsetY, ((end.X + this.TranslateX) - this.MinX) + this.SamplingOffsetX, ((end.Y + this.TranslateY) - this.MinY) + this.SamplingOffsetY); } /// /// Returns the stroke offset normal matching PolygonStroker's dx/dy convention. /// /// The normalized segment tangent. /// The stroke-side offset normal. private static Vector2 GetStrokeOffsetNormal(Vector2 tangent) => new(tangent.Y, -tangent.X); private readonly struct StrokeContourSegment { public StrokeContourSegment(PointF start, PointF end, Vector2 tangent, float length) { this.Start = start; this.End = end; this.Tangent = tangent; this.Normal = GetStrokeOffsetNormal(tangent); this.Length = length; } public PointF Start { get; } public PointF End { get; } public Vector2 Tangent { get; } public Vector2 Normal { get; } public float Length { get; } } private struct ContourState { public bool HasPoint; public PointF FirstPoint; public PointF PreviousPoint; } } /// /// Stroke linearizer that finalizes retained lines into the 32-bit-X encoding. /// private sealed class StrokeLinearizerX32Y16 : StrokeLinearizer { /// /// Initializes a new instance of the class. /// /// The stroked centerline geometry. /// The residual transform applied to each source point during emission. /// The stroke style. /// The destination-space X translation applied at composition time. /// The destination-space Y translation applied at composition time. /// The minimum destination X bound after clipping. /// The minimum destination Y bound after clipping. /// The visible destination width in pixels. /// The visible destination height in pixels. /// The first retained row-band index. /// The retained row-band count. /// The horizontal sampling offset. /// The vertical sampling offset. /// The allocator used for retained start-cover storage. public StrokeLinearizerX32Y16( LinearGeometry geometry, Matrix4x4 residual, StrokeStyle stroke, int translateX, int translateY, int minX, int minY, int width, int height, int firstBandIndex, int rowBandCount, float samplingOffsetX, float samplingOffsetY, MemoryAllocator allocator) : base(geometry, residual, stroke, translateX, translateY, minX, minY, width, height, firstBandIndex, rowBandCount, samplingOffsetX, samplingOffsetY, allocator) => this.FinalLines = new LineArrayX32Y16Block?[rowBandCount]; /// /// Gets the finalized retained line blocks for each row band. /// public LineArrayX32Y16Block?[] FinalLines { get; } /// protected override LineArrayX32Y16 CreateLineArray() => new(); /// protected override void AppendLine(int rowIndex, int x0, int y0, int x1, int y1) => this.GetOrCreateLineArray(rowIndex).AppendLine(x0, y0, x1, y1); /// protected override void FinalizeLines() { for (int i = 0; i < this.RowBandCount; i++) { LineArrayX32Y16? lineArray = this.LineArrays[i]; this.FinalLines[i] = lineArray?.GetFrontBlock(); this.FirstBlockLineCounts[i] = lineArray?.GetFrontBlockLineCount() ?? 0; } } /// /// Executes the retained stroke linearization pass and returns the finalized payload. /// /// The finalized retained raster data. /// when retained coverage was produced; otherwise . internal bool TryProcess(out LinearizedRasterData result) { if (!this.ProcessCore()) { result = null!; return false; } result = new LinearizedRasterData( this.Geometry, new TileBounds(this.MinX, this.FirstBandIndex, this.Width, this.RowBandCount), this.FinalLines, this.FirstBlockLineCounts, this.StartCoverTable); return true; } } /// /// Stroke linearizer that finalizes retained lines into the packed 16-bit-X encoding. /// private sealed class StrokeLinearizerX16Y16 : StrokeLinearizer { /// /// Initializes a new instance of the class. /// /// The stroked centerline geometry. /// The residual transform applied to each source point during emission. /// The stroke style. /// The destination-space X translation applied at composition time. /// The destination-space Y translation applied at composition time. /// The minimum destination X bound after clipping. /// The minimum destination Y bound after clipping. /// The visible destination width in pixels. /// The visible destination height in pixels. /// The first retained row-band index. /// The retained row-band count. /// The horizontal sampling offset. /// The vertical sampling offset. /// The allocator used for retained start-cover storage. public StrokeLinearizerX16Y16( LinearGeometry geometry, Matrix4x4 residual, StrokeStyle stroke, int translateX, int translateY, int minX, int minY, int width, int height, int firstBandIndex, int rowBandCount, float samplingOffsetX, float samplingOffsetY, MemoryAllocator allocator) : base(geometry, residual, stroke, translateX, translateY, minX, minY, width, height, firstBandIndex, rowBandCount, samplingOffsetX, samplingOffsetY, allocator) => this.FinalLines = new LineArrayX16Y16Block?[rowBandCount]; /// /// Gets the finalized retained line blocks for each row band. /// public LineArrayX16Y16Block?[] FinalLines { get; } /// protected override LineArrayX16Y16 CreateLineArray() => new(); /// protected override void AppendLine(int rowIndex, int x0, int y0, int x1, int y1) => this.GetOrCreateLineArray(rowIndex).AppendLine(x0, y0, x1, y1); /// protected override void FinalizeLines() { for (int i = 0; i < this.RowBandCount; i++) { LineArrayX16Y16? lineArray = this.LineArrays[i]; this.FinalLines[i] = lineArray?.GetFrontBlock(); this.FirstBlockLineCounts[i] = lineArray?.GetFrontBlockLineCount() ?? 0; } } /// /// Executes the retained stroke linearization pass and returns the finalized payload. /// /// The finalized retained raster data. /// when retained coverage was produced; otherwise . internal bool TryProcess(out LinearizedRasterData result) { if (!this.ProcessCore()) { result = null!; return false; } result = new LinearizedRasterData( this.Geometry, new TileBounds(this.MinX, this.FirstBandIndex, this.Width, this.RowBandCount), this.FinalLines, this.FirstBlockLineCounts, this.StartCoverTable); return true; } } } }