ImageSharp/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.StrokeLinearizer.cs
2026-08-03 22:31:27 +02:00

1159 lines
51 KiB
C#

// 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
{
/// <summary>
/// Base retained stroke linearizer that expands stroked centerlines once into row-local line storage.
/// </summary>
/// <typeparam name="TL">The mutable per-row retained line collector type.</typeparam>
private abstract class StrokeLinearizer<TL> : Linearizer<TL>
where TL : class
{
private const float StrokeMicroSegmentEpsilon = 1F / 64F;
private readonly StrokeStyle stroke;
/// <summary>
/// Initializes a new instance of the <see cref="StrokeLinearizer{TL}"/> class.
/// </summary>
/// <param name="geometry">The stroked centerline geometry.</param>
/// <param name="residual">The residual transform applied to each source point during emission.</param>
/// <param name="stroke">The stroke style.</param>
/// <param name="translateX">The destination-space X translation applied at composition time.</param>
/// <param name="translateY">The destination-space Y translation applied at composition time.</param>
/// <param name="minX">The minimum destination X bound after clipping.</param>
/// <param name="minY">The minimum destination Y bound after clipping.</param>
/// <param name="width">The visible destination width in pixels.</param>
/// <param name="height">The visible destination height in pixels.</param>
/// <param name="firstBandIndex">The first retained row-band index.</param>
/// <param name="rowBandCount">The retained row-band count.</param>
/// <param name="samplingOffsetX">The horizontal sampling offset.</param>
/// <param name="samplingOffsetY">The vertical sampling offset.</param>
/// <param name="allocator">The allocator used for retained start-cover storage.</param>
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
}
/// <inheritdoc />
protected override bool ProcessCore()
{
ReadOnlySpan<LinearContour> contours = this.Geometry.GetContours();
for (int contourIndex = 0; contourIndex < contours.Length; contourIndex++)
{
LinearContour contour = contours[contourIndex];
if (contour.PointCount == 0)
{
continue;
}
ReadOnlySpan<PointF> 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;
}
/// <summary>
/// Classifies one stroked contour against the interest bounds.
/// </summary>
/// <param name="contourPoints">The contour points.</param>
/// <returns>The contour's relationship to the interest bounds.</returns>
private ContourInterest GetContourInterest(ReadOnlySpan<PointF> 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;
}
/// <summary>
/// Returns whether a contour should be treated as closed when emitting stroke geometry.
/// </summary>
/// <param name="contourPoints">The contour points.</param>
/// <param name="isDeclaredClosed">Indicates whether the contour is explicitly closed.</param>
/// <returns><see langword="true"/> when the contour should be stroked as closed; otherwise <see langword="false"/>.</returns>
private bool IsContourClosedForEmission(ReadOnlySpan<PointF> 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;
/// <summary>
/// Processes one centerline contour.
/// </summary>
/// <param name="contourPoints">The contiguous contour points.</param>
/// <param name="isClosed">Indicates whether the contour is closed.</param>
/// <param name="contained">Indicates whether the stroked contour is fully contained within the interest.</param>
private void ProcessContour(ReadOnlySpan<PointF> contourPoints, bool isClosed, bool contained)
{
using IMemoryOwner<StrokeContourSegment> rentedSegmentsOwner = this.Allocator.Allocate<StrokeContourSegment>(contourPoints.Length);
Span<StrokeContourSegment> 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);
}
/// <summary>
/// Builds one contour-local stroke segment array while collapsing immediate duplicate points.
/// </summary>
/// <param name="contourPoints">The contiguous contour points.</param>
/// <param name="isClosed">Indicates whether the contour is closed.</param>
/// <param name="segments">The destination segment buffer.</param>
/// <param name="distinctPointCount">Receives the number of distinct contour points.</param>
/// <param name="pointLike">Receives the fallback point for degenerate contours.</param>
/// <returns>The number of emitted contour segments.</returns>
private int BuildContourSegments(
ReadOnlySpan<PointF> contourPoints,
bool isClosed,
Span<StrokeContourSegment> 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;
}
/// <summary>
/// Creates one contour-local stroke segment descriptor.
/// </summary>
/// <param name="start">The segment start point.</param>
/// <param name="end">The segment end point.</param>
/// <param name="segment">Receives the segment descriptor.</param>
/// <returns><see langword="true"/> when a non-degenerate segment exists.</returns>
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;
}
/// <summary>
/// Gets the point bounds for one contour.
/// </summary>
/// <param name="contourPoints">The contiguous contour points.</param>
/// <returns>The contour point bounds.</returns>
private RectangleF GetPointBounds(ReadOnlySpan<PointF> 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);
}
/// <summary>
/// Emits one stroked open segment.
/// </summary>
/// <param name="start">The segment start point.</param>
/// <param name="end">The segment end point.</param>
/// <param name="contained">Indicates whether the segment is fully contained within the interest.</param>
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);
}
}
/// <summary>
/// Emits one stroked open multi-segment contour from precomputed contour-local segments.
/// </summary>
/// <param name="segments">The precomputed contour-local segments.</param>
/// <param name="contained">Indicates whether the contour is fully contained within the interest.</param>
private void EmitOpenStrokeContour(ReadOnlySpan<StrokeContourSegment> 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);
}
/// <summary>
/// Emits the two stroked contours for a closed contour from precomputed contour-local segments.
/// </summary>
/// <param name="segments">The precomputed contour-local segments.</param>
/// <param name="contained">Indicates whether the contour is fully contained within the interest.</param>
private void EmitClosedStrokeContour(ReadOnlySpan<StrokeContourSegment> 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);
}
/// <summary>
/// Emits a point-like stroke as a cap contour.
/// </summary>
/// <param name="point">The point-like stroke location.</param>
/// <param name="contained">Indicates whether the contour is fully contained within the interest.</param>
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);
}
/// <summary>
/// Emits one round cap or join arc directly into the retained line storage.
/// </summary>
/// <param name="center">The arc center.</param>
/// <param name="fromOffset">The start offset from the center.</param>
/// <param name="toOffset">The end offset from the center.</param>
/// <param name="contained">Indicates whether the arc is fully contained within the interest.</param>
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);
}
/// <summary>
/// Appends a contour arc directly to the active stroke contour.
/// </summary>
/// <param name="contour">The active contour state.</param>
/// <param name="center">The arc center.</param>
/// <param name="fromOffset">The start offset from the center.</param>
/// <param name="toOffset">The end offset from the center.</param>
/// <param name="contained">Indicates whether the arc is fully contained within the interest.</param>
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);
}
/// <summary>
/// Appends one side join point sequence directly to the active stroke contour.
/// </summary>
/// <remarks>
/// Direct port of <c>PolygonStroker.CalcJoin</c> 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.
/// </remarks>
/// <param name="contour">The active contour state.</param>
/// <param name="v0">Previous source vertex in the emission's traversal order.</param>
/// <param name="v1">Current source vertex (the corner).</param>
/// <param name="v2">Next source vertex in the emission's traversal order.</param>
/// <param name="len1">Length of segment v0-v1.</param>
/// <param name="len2">Length of segment v1-v2.</param>
/// <param name="contained">Indicates whether the join is fully contained within the interest.</param>
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;
}
}
/// <summary>
/// Direct port of <c>PolygonStroker.CalcMiter</c>. Emits the miter apex (or the
/// configured overflow fallback) at the join vertex.
/// </summary>
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;
}
}
/// <summary>
/// Direct port of <c>PolygonStroker.CalcArc</c>. Emits intermediate arc vertices
/// around a join center between two offset vectors.
/// </summary>
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);
}
/// <summary>
/// Signed area of triangle (a, b, point), matching <c>PolygonStroker.CrossProduct</c>.
/// </summary>
[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));
/// <summary>
/// Intersects two infinite lines defined by point pairs (a, b) and (c, d),
/// matching <c>PolygonStroker.TryCalcIntersection</c>.
/// </summary>
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;
}
/// <summary>
/// Appends one point to the active contour.
/// </summary>
/// <param name="state">The active contour state.</param>
/// <param name="point">The point to append.</param>
/// <param name="contained">Indicates whether the contour is fully contained within the interest.</param>
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;
}
/// <summary>
/// Closes the active contour.
/// </summary>
/// <param name="state">The active contour state.</param>
/// <param name="contained">Indicates whether the contour is fully contained within the interest.</param>
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;
}
/// <summary>
/// Emits one stroked boundary edge into retained line storage.
/// </summary>
/// <param name="start">The edge start point.</param>
/// <param name="end">The edge end point.</param>
/// <param name="contained">Indicates whether the edge is fully contained within the interest.</param>
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);
}
/// <summary>
/// Returns the stroke offset normal matching PolygonStroker's dx/dy convention.
/// </summary>
/// <param name="tangent">The normalized segment tangent.</param>
/// <returns>The stroke-side offset normal.</returns>
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;
}
}
/// <summary>
/// Stroke linearizer that finalizes retained lines into the 32-bit-X encoding.
/// </summary>
private sealed class StrokeLinearizerX32Y16 : StrokeLinearizer<LineArrayX32Y16>
{
/// <summary>
/// Initializes a new instance of the <see cref="StrokeLinearizerX32Y16"/> class.
/// </summary>
/// <param name="geometry">The stroked centerline geometry.</param>
/// <param name="residual">The residual transform applied to each source point during emission.</param>
/// <param name="stroke">The stroke style.</param>
/// <param name="translateX">The destination-space X translation applied at composition time.</param>
/// <param name="translateY">The destination-space Y translation applied at composition time.</param>
/// <param name="minX">The minimum destination X bound after clipping.</param>
/// <param name="minY">The minimum destination Y bound after clipping.</param>
/// <param name="width">The visible destination width in pixels.</param>
/// <param name="height">The visible destination height in pixels.</param>
/// <param name="firstBandIndex">The first retained row-band index.</param>
/// <param name="rowBandCount">The retained row-band count.</param>
/// <param name="samplingOffsetX">The horizontal sampling offset.</param>
/// <param name="samplingOffsetY">The vertical sampling offset.</param>
/// <param name="allocator">The allocator used for retained start-cover storage.</param>
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];
/// <summary>
/// Gets the finalized retained line blocks for each row band.
/// </summary>
public LineArrayX32Y16Block?[] FinalLines { get; }
/// <inheritdoc />
protected override LineArrayX32Y16 CreateLineArray() => new();
/// <inheritdoc />
protected override void AppendLine(int rowIndex, int x0, int y0, int x1, int y1)
=> this.GetOrCreateLineArray(rowIndex).AppendLine(x0, y0, x1, y1);
/// <inheritdoc />
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;
}
}
/// <summary>
/// Executes the retained stroke linearization pass and returns the finalized payload.
/// </summary>
/// <param name="result">The finalized retained raster data.</param>
/// <returns><see langword="true"/> when retained coverage was produced; otherwise <see langword="false"/>.</returns>
internal bool TryProcess(out LinearizedRasterData<LineArrayX32Y16Block> result)
{
if (!this.ProcessCore())
{
result = null!;
return false;
}
result = new LinearizedRasterData<LineArrayX32Y16Block>(
this.Geometry,
new TileBounds(this.MinX, this.FirstBandIndex, this.Width, this.RowBandCount),
this.FinalLines,
this.FirstBlockLineCounts,
this.StartCoverTable);
return true;
}
}
/// <summary>
/// Stroke linearizer that finalizes retained lines into the packed 16-bit-X encoding.
/// </summary>
private sealed class StrokeLinearizerX16Y16 : StrokeLinearizer<LineArrayX16Y16>
{
/// <summary>
/// Initializes a new instance of the <see cref="StrokeLinearizerX16Y16"/> class.
/// </summary>
/// <param name="geometry">The stroked centerline geometry.</param>
/// <param name="residual">The residual transform applied to each source point during emission.</param>
/// <param name="stroke">The stroke style.</param>
/// <param name="translateX">The destination-space X translation applied at composition time.</param>
/// <param name="translateY">The destination-space Y translation applied at composition time.</param>
/// <param name="minX">The minimum destination X bound after clipping.</param>
/// <param name="minY">The minimum destination Y bound after clipping.</param>
/// <param name="width">The visible destination width in pixels.</param>
/// <param name="height">The visible destination height in pixels.</param>
/// <param name="firstBandIndex">The first retained row-band index.</param>
/// <param name="rowBandCount">The retained row-band count.</param>
/// <param name="samplingOffsetX">The horizontal sampling offset.</param>
/// <param name="samplingOffsetY">The vertical sampling offset.</param>
/// <param name="allocator">The allocator used for retained start-cover storage.</param>
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];
/// <summary>
/// Gets the finalized retained line blocks for each row band.
/// </summary>
public LineArrayX16Y16Block?[] FinalLines { get; }
/// <inheritdoc />
protected override LineArrayX16Y16 CreateLineArray() => new();
/// <inheritdoc />
protected override void AppendLine(int rowIndex, int x0, int y0, int x1, int y1)
=> this.GetOrCreateLineArray(rowIndex).AppendLine(x0, y0, x1, y1);
/// <inheritdoc />
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;
}
}
/// <summary>
/// Executes the retained stroke linearization pass and returns the finalized payload.
/// </summary>
/// <param name="result">The finalized retained raster data.</param>
/// <returns><see langword="true"/> when retained coverage was produced; otherwise <see langword="false"/>.</returns>
internal bool TryProcess(out LinearizedRasterData<LineArrayX16Y16Block> result)
{
if (!this.ProcessCore())
{
result = null!;
return false;
}
result = new LinearizedRasterData<LineArrayX16Y16Block>(
this.Geometry,
new TileBounds(this.MinX, this.FirstBandIndex, this.Width, this.RowBandCount),
this.FinalLines,
this.FirstBlockLineCounts,
this.StartCoverTable);
return true;
}
}
}
}