1251 lines
48 KiB
C#
1251 lines
48 KiB
C#
// Copyright (c) Six Labors.
|
|
// Licensed under the Six Labors Split License.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Runtime.CompilerServices;
|
|
|
|
namespace SixLabors.PolygonClipper {
|
|
#pragma warning disable SA1201 // Elements should appear in the correct order
|
|
|
|
/// <summary>
|
|
/// Generates polygonal stroke geometry for contours with configurable joins and caps.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This type performs two phases:
|
|
/// <list type="number">
|
|
/// <item><description>Expand each source contour into one or two stroke-side outlines with joins/caps.</description></item>
|
|
/// <item><description>
|
|
/// Optionally resolve generated overlaps/self-intersections using
|
|
/// <see cref="PolygonClipper.Normalize(Polygon)"/> with positive fill semantics.
|
|
/// </description></item>
|
|
/// </list>
|
|
/// The emitted contours are implicitly closed (first vertex is not duplicated at the end).
|
|
/// <para>
|
|
/// The static <see cref="Stroke(Polygon,double,StrokeOptions?)"/> method is the recommended
|
|
/// entry point. It routes calls through an internal thread-local pool of reusable stroker
|
|
/// instances and automatically resets temporary state between calls.
|
|
/// </para>
|
|
/// <para>Instance members are not thread-safe for concurrent use.</para>
|
|
/// </remarks>
|
|
public sealed class PolygonStroker
|
|
{
|
|
// Numerical tolerances used while collapsing near-duplicate source points and
|
|
// while testing near-parallel line intersections.
|
|
private const double VertexDistanceEpsilon = 1E-14D;
|
|
private const double IntersectionEpsilon = 1E-30D;
|
|
private const double Pi = Math.PI;
|
|
private const double PiMul2 = Math.PI * 2D;
|
|
|
|
// The inner miter limit used to clamp joins on acute interior angles.
|
|
private const double InnerMiterLimit = 1.01D;
|
|
|
|
// Keep at most 2 warm instances per option-set (one active shape and one spare)
|
|
// to reduce churn without retaining many rarely reused configurations.
|
|
private const int MaxPooledStrokersPerOptions = 2;
|
|
|
|
// Discard oversized scratch buffers so a single pathological stroke does not pin
|
|
// large arrays in thread-local pools for the lifetime of the thread.
|
|
private const int MaxRetainedScratchBytes = 256 * 1024;
|
|
|
|
private static readonly StrokeOptions DefaultStrokeOptions = new();
|
|
|
|
[ThreadStatic]
|
|
private static Dictionary<StrokeOptionsKey, Stack<PolygonStroker>>? strokersByOptions;
|
|
|
|
// Scratch buffers reused across contours to keep per-call allocations down.
|
|
private ArrayBuilder<Vertex> outVertices = new(1);
|
|
private ArrayBuilder<StrokeVertexDistance> srcVertices = new(16);
|
|
|
|
// Streaming-state fields used by the Accumulate() state machine.
|
|
private int closed;
|
|
private int outVertex;
|
|
private Status prevStatus;
|
|
private int srcVertex;
|
|
private Status status;
|
|
private double strokeWidth = 0.5D;
|
|
private double widthAbs = 0.5D;
|
|
private double widthEps = 0.5D / 1024D;
|
|
private int widthSign = 1;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="PolygonStroker"/> class with the specified stroke options.
|
|
/// </summary>
|
|
/// <param name="options">The stroke options.</param>
|
|
/// <exception cref="ArgumentNullException">Thrown when <paramref name="options"/> is null.</exception>
|
|
/// <remarks>
|
|
/// This constructor is intended for advanced/manual usage.
|
|
/// For typical call patterns, prefer the static <see cref="Stroke(Polygon,double,StrokeOptions?)"/>
|
|
/// method to use internal pooling automatically.
|
|
/// </remarks>
|
|
public PolygonStroker(StrokeOptions options)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(options);
|
|
this.NormalizeOutput = options.NormalizeOutput;
|
|
this.LineJoin = options.LineJoin;
|
|
this.LineCap = options.LineCap;
|
|
this.MiterLimit = options.MiterLimit;
|
|
this.ArcDetailScale = options.ArcDetailScale;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Internal state machine used by <see cref="Accumulate(ref Vertex)"/> to stream stroked output vertices.
|
|
/// </summary>
|
|
private enum Status
|
|
{
|
|
/// <summary>Initial setup and input normalization.</summary>
|
|
Initial,
|
|
|
|
/// <summary>Ready to emit the first command for the contour.</summary>
|
|
Ready,
|
|
|
|
/// <summary>Emit start-cap vertices for open contours.</summary>
|
|
Cap1,
|
|
|
|
/// <summary>Emit end-cap vertices for open contours.</summary>
|
|
Cap2,
|
|
|
|
/// <summary>Emit joins for the first stroke side.</summary>
|
|
Outline1,
|
|
|
|
/// <summary>Switch from first side to second side for closed paths.</summary>
|
|
CloseFirst,
|
|
|
|
/// <summary>Emit joins for the second stroke side.</summary>
|
|
Outline2,
|
|
|
|
/// <summary>Flush buffered vertices from the current join/cap computation.</summary>
|
|
OutVertices,
|
|
|
|
/// <summary>Emit end-poly marker for first stroke side.</summary>
|
|
EndPoly1,
|
|
|
|
/// <summary>Emit end-poly marker for second stroke side.</summary>
|
|
EndPoly2,
|
|
|
|
/// <summary>Stop emitting commands.</summary>
|
|
Stop
|
|
}
|
|
|
|
private readonly struct StrokeOptionsKey : IEquatable<StrokeOptionsKey>
|
|
{
|
|
public StrokeOptionsKey(StrokeOptions options)
|
|
{
|
|
this.NormalizeOutput = options.NormalizeOutput;
|
|
this.LineJoin = options.LineJoin;
|
|
this.LineCap = options.LineCap;
|
|
this.MiterLimit = options.MiterLimit;
|
|
this.ArcDetailScale = options.ArcDetailScale;
|
|
}
|
|
|
|
public bool NormalizeOutput { get; }
|
|
|
|
public LineJoin LineJoin { get; }
|
|
|
|
public LineCap LineCap { get; }
|
|
|
|
public double MiterLimit { get; }
|
|
|
|
public double ArcDetailScale { get; }
|
|
|
|
public bool Equals(StrokeOptionsKey other)
|
|
=> this.NormalizeOutput == other.NormalizeOutput &&
|
|
this.LineJoin == other.LineJoin &&
|
|
this.LineCap == other.LineCap &&
|
|
this.MiterLimit == other.MiterLimit &&
|
|
this.ArcDetailScale == other.ArcDetailScale;
|
|
|
|
public override bool Equals(object? obj) => obj is StrokeOptionsKey other && this.Equals(other);
|
|
|
|
public override int GetHashCode()
|
|
=> HashCode.Combine(
|
|
this.NormalizeOutput,
|
|
this.LineJoin,
|
|
this.LineCap,
|
|
this.MiterLimit,
|
|
this.ArcDetailScale);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Strokes <paramref name="polygon"/> with <paramref name="width"/> using optional
|
|
/// <paramref name="options"/>.
|
|
/// </summary>
|
|
/// <param name="polygon">Input polygon to stroke.</param>
|
|
/// <param name="width">Stroke width.</param>
|
|
/// <param name="options">
|
|
/// Stroke options controlling joins, caps and approximation behavior.
|
|
/// When null, default <see cref="StrokeOptions"/> are used.
|
|
/// </param>
|
|
/// <returns>The stroked polygon contours.</returns>
|
|
/// <exception cref="ArgumentNullException">Thrown when <paramref name="polygon"/> is null.</exception>
|
|
/// <remarks>Preferred entry point. Uses internal thread-local reusable instances.</remarks>
|
|
public static Polygon Stroke(Polygon polygon, double width, StrokeOptions? options = null)
|
|
{
|
|
StrokeOptions effectiveOptions = options ?? DefaultStrokeOptions;
|
|
StrokeOptionsKey key = new(effectiveOptions);
|
|
PolygonStroker stroker = Rent(key, effectiveOptions);
|
|
try
|
|
{
|
|
stroker.Width = width;
|
|
return stroker.Stroke(polygon);
|
|
}
|
|
finally
|
|
{
|
|
Return(key, stroker);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Strokes <paramref name="polygon"/> using this instance's configured options and width.
|
|
/// </summary>
|
|
/// <param name="polygon">Input polygon to stroke.</param>
|
|
/// <returns>The stroked polygon contours.</returns>
|
|
/// <exception cref="ArgumentNullException">Thrown when <paramref name="polygon"/> is null.</exception>
|
|
/// <remarks>Instance execution is not thread-safe for concurrent use.</remarks>
|
|
public Polygon Stroke(Polygon polygon)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(polygon);
|
|
if (polygon.Count == 0)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
Polygon allContours = new(Math.Max(2, polygon.Count * 2));
|
|
for (int i = 0; i < polygon.Count; i++)
|
|
{
|
|
Contour contour = polygon[i];
|
|
|
|
// Close explicit or near-seam contours to avoid tiny stitch gaps,
|
|
// but keep clearly open polylines open so caps are emitted.
|
|
bool isClosed = IsContourClosedForEmission(contour, this.widthAbs * 2D);
|
|
Polygon stroked = this.ProcessPathToPolygon(contour, isClosed);
|
|
if (stroked.Count > 0)
|
|
{
|
|
allContours.Join(stroked);
|
|
}
|
|
}
|
|
|
|
if (allContours.Count == 0)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
if (!this.NormalizeOutput)
|
|
{
|
|
return allContours;
|
|
}
|
|
|
|
// Stroker emission already follows positive-fill assumptions, so skip
|
|
// extra input-orientation normalization and only resolve overlaps.
|
|
return SelfIntersectionRemover.Process(allContours, normalizeInputForPositiveFill: false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Strokes <paramref name="polygon"/> after setting the current stroke width.
|
|
/// </summary>
|
|
/// <param name="polygon">Input polygon to stroke.</param>
|
|
/// <param name="width">Stroke width.</param>
|
|
/// <returns>The stroked polygon contours.</returns>
|
|
/// <remarks>Instance execution is not thread-safe for concurrent use.</remarks>
|
|
public Polygon Stroke(Polygon polygon, double width)
|
|
{
|
|
this.Width = width;
|
|
return this.Stroke(polygon);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the miter limit used to clamp outer miter joins.
|
|
/// </summary>
|
|
public double MiterLimit { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the tessellation detail scale used for round joins and round caps.
|
|
/// Higher values produce more vertices and smoother curves.
|
|
/// </summary>
|
|
public double ArcDetailScale { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the outer line join style used for stroking corners.
|
|
/// </summary>
|
|
public LineJoin LineJoin { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the line cap style used for open path ends.
|
|
/// </summary>
|
|
public LineCap LineCap { get; }
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether generated contours should be normalized by resolving
|
|
/// self-intersections and overlaps.
|
|
/// </summary>
|
|
public bool NormalizeOutput { get; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the stroke width.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Positive values produce conventional outward stroking. Negative values are supported
|
|
/// and flip the side orientation while preserving magnitude.
|
|
/// </remarks>
|
|
public double Width
|
|
{
|
|
get => this.strokeWidth * 2D;
|
|
set
|
|
{
|
|
this.strokeWidth = value * 0.5D;
|
|
if (this.strokeWidth < 0D)
|
|
{
|
|
this.widthAbs = -this.strokeWidth;
|
|
this.widthSign = -1;
|
|
}
|
|
else
|
|
{
|
|
this.widthAbs = this.strokeWidth;
|
|
this.widthSign = 1;
|
|
}
|
|
|
|
this.widthEps = this.strokeWidth / 1024D;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Converts a single contour into stroked polygon contours.
|
|
/// </summary>
|
|
/// <param name="contour">The source contour.</param>
|
|
/// <param name="isClosed">Whether the contour should be emitted as closed.</param>
|
|
/// <returns>The generated stroked contour set for this input contour.</returns>
|
|
private Polygon ProcessPathToPolygon(Contour contour, bool isClosed)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(contour);
|
|
|
|
int pointCount = contour.Count;
|
|
if (pointCount < 2)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
bool hasExplicitClosure = pointCount > 1 && contour[0] == contour[^1];
|
|
if (isClosed && hasExplicitClosure)
|
|
{
|
|
// Keep one implicit closure path in the stroker state machine.
|
|
// Duplicate terminal vertices are re-added at final contour emission.
|
|
pointCount--;
|
|
}
|
|
|
|
if (pointCount < 2)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
if (pointCount == 2)
|
|
{
|
|
Vertex p0 = contour[0];
|
|
Vertex p1 = contour[1];
|
|
if (Vertex.DistanceSquared(p0, p1) <= VertexDistanceEpsilon * VertexDistanceEpsilon)
|
|
{
|
|
// Degenerate segment behaves like a stroked point.
|
|
return [this.GeneratePointCap(p0.X, p0.Y)];
|
|
}
|
|
}
|
|
|
|
this.Reset();
|
|
for (int i = 0; i < pointCount; i++)
|
|
{
|
|
Vertex point = contour[i];
|
|
this.Add(point.X, point.Y, PathCommand.LineTo);
|
|
}
|
|
|
|
if (isClosed)
|
|
{
|
|
this.ClosePath();
|
|
}
|
|
|
|
Polygon result = new(isClosed ? 2 : 1);
|
|
this.FinishPath(result);
|
|
return result;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static PolygonStroker Rent(in StrokeOptionsKey key, StrokeOptions options)
|
|
{
|
|
Dictionary<StrokeOptionsKey, Stack<PolygonStroker>>? pools = strokersByOptions;
|
|
if (pools != null &&
|
|
pools.TryGetValue(key, out Stack<PolygonStroker>? pool) &&
|
|
pool.Count > 0)
|
|
{
|
|
return pool.Pop();
|
|
}
|
|
|
|
return new PolygonStroker(options);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void Return(in StrokeOptionsKey key, PolygonStroker stroker)
|
|
{
|
|
stroker.ResetForReuse();
|
|
|
|
// Pool only compact instances; large retained buffers are intentionally dropped.
|
|
if (stroker.GetRetainedScratchBytes() > MaxRetainedScratchBytes)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Dictionary<StrokeOptionsKey, Stack<PolygonStroker>> pools = strokersByOptions ??= [];
|
|
if (!pools.TryGetValue(key, out Stack<PolygonStroker>? pool))
|
|
{
|
|
pool = new Stack<PolygonStroker>(MaxPooledStrokersPerOptions);
|
|
pools[key] = pool;
|
|
}
|
|
|
|
if (pool.Count < MaxPooledStrokersPerOptions)
|
|
{
|
|
pool.Push(stroker);
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private int GetRetainedScratchBytes()
|
|
=> (this.outVertices.Capacity * Unsafe.SizeOf<Vertex>()) +
|
|
(this.srcVertices.Capacity * Unsafe.SizeOf<StrokeVertexDistance>());
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private void ResetForReuse()
|
|
{
|
|
this.outVertices.Clear();
|
|
this.srcVertices.Clear();
|
|
this.closed = 0;
|
|
this.outVertex = 0;
|
|
this.prevStatus = Status.Initial;
|
|
this.srcVertex = 0;
|
|
this.status = Status.Initial;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns whether a contour should be treated as closed when emitting stroke geometry.
|
|
/// </summary>
|
|
/// <param name="contour">The contour to inspect.</param>
|
|
/// <param name="strokeWidth">Current stroke width.</param>
|
|
/// <returns><see langword="true"/> if the contour should be treated as closed; otherwise <see langword="false"/>.</returns>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static bool IsContourClosedForEmission(Contour contour, double strokeWidth)
|
|
{
|
|
int count = contour.Count;
|
|
if (count < 3)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (contour[0] == contour[^1])
|
|
{
|
|
return true;
|
|
}
|
|
|
|
Vertex delta = contour[0] - contour[^1];
|
|
double closeThreshold = Math.Max(strokeWidth, 1E-3D);
|
|
return delta.LengthSquared() <= closeThreshold * closeThreshold;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Marks the current path as closed before finishing the outline.
|
|
/// </summary>
|
|
private void ClosePath()
|
|
{
|
|
this.closed = (int)PathFlags.Close;
|
|
this.status = Status.Initial;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets the stroker state for reuse.
|
|
/// </summary>
|
|
private void Reset()
|
|
{
|
|
// Reuse builders to avoid per-contour allocations.
|
|
this.srcVertices.Clear();
|
|
this.outVertices.Clear();
|
|
this.srcVertex = 0;
|
|
this.outVertex = 0;
|
|
this.closed = 0;
|
|
this.status = Status.Initial;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Consumes commands from <see cref="Accumulate(ref Vertex)"/> and materializes final contour lists.
|
|
/// </summary>
|
|
/// <param name="result">Destination polygon that receives generated contours.</param>
|
|
private void FinishPath(Polygon result)
|
|
{
|
|
Vertex current = default;
|
|
Vertex lastPoint = default;
|
|
bool hasLastPoint = false;
|
|
Contour? currentContour = null;
|
|
PathCommand command;
|
|
|
|
while (!(command = this.Accumulate(ref current)).Stop())
|
|
{
|
|
if (command.MoveTo())
|
|
{
|
|
// Start a new contour. Commit any previous contour that is already complete.
|
|
if (currentContour is { Count: >= 3 })
|
|
{
|
|
result.Add(currentContour);
|
|
}
|
|
|
|
currentContour = new Contour(16);
|
|
hasLastPoint = false;
|
|
}
|
|
|
|
if (command.Vertex())
|
|
{
|
|
currentContour ??= new Contour(16);
|
|
|
|
// Drop immediate duplicate vertices to avoid zero-length segments
|
|
// entering the intersection-removal pass.
|
|
if (!hasLastPoint || current != lastPoint)
|
|
{
|
|
currentContour.Add(current);
|
|
lastPoint = current;
|
|
hasLastPoint = true;
|
|
}
|
|
}
|
|
|
|
if (command.EndPoly())
|
|
{
|
|
if (currentContour is { Count: >= 3 })
|
|
{
|
|
result.Add(currentContour);
|
|
}
|
|
|
|
currentContour = null;
|
|
hasLastPoint = false;
|
|
}
|
|
}
|
|
|
|
if (currentContour is { Count: >= 3 })
|
|
{
|
|
result.Add(currentContour);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a path command and coordinate into the source stream.
|
|
/// </summary>
|
|
/// <param name="x">X coordinate.</param>
|
|
/// <param name="y">Y coordinate.</param>
|
|
/// <param name="cmd">Path command associated with the coordinate.</param>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private void Add(double x, double y, PathCommand cmd)
|
|
{
|
|
this.status = Status.Initial;
|
|
if (cmd.MoveTo())
|
|
{
|
|
// MoveTo starts a new source contour.
|
|
if (this.srcVertices.Length != 0)
|
|
{
|
|
this.srcVertices.RemoveLast();
|
|
}
|
|
|
|
this.Add(x, y);
|
|
}
|
|
else if (cmd.Vertex())
|
|
{
|
|
this.Add(x, y);
|
|
}
|
|
else
|
|
{
|
|
// Non-vertex command updates close flags.
|
|
this.closed = cmd.GetCloseFlag();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Appends a source vertex, collapsing trailing duplicates when needed.
|
|
/// </summary>
|
|
/// <param name="x">X coordinate.</param>
|
|
/// <param name="y">Y coordinate.</param>
|
|
/// <param name="distance">Cached edge length hint.</param>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private void Add(double x, double y, double distance = 0D)
|
|
{
|
|
if (this.srcVertices.Length > 1)
|
|
{
|
|
ref StrokeVertexDistance vd1 = ref this.srcVertices[^2];
|
|
ref StrokeVertexDistance vd2 = ref this.srcVertices[^1];
|
|
bool ret = vd1.Measure(vd2);
|
|
if (!ret && this.srcVertices.Length != 0)
|
|
{
|
|
// If the previous segment collapses, remove the duplicate tail.
|
|
this.srcVertices.RemoveLast();
|
|
}
|
|
}
|
|
|
|
this.srcVertices.Add(new StrokeVertexDistance(x, y, distance));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Streams stroke output as path commands/vertices from the current source contour.
|
|
/// </summary>
|
|
/// <param name="point">Receives the emitted vertex when a vertex command is returned.</param>
|
|
/// <returns>The next path command.</returns>
|
|
private PathCommand Accumulate(ref Vertex point)
|
|
{
|
|
ref ArrayBuilder<StrokeVertexDistance> src = ref this.srcVertices;
|
|
PathCommand cmd = PathCommand.LineTo;
|
|
while (!cmd.Stop())
|
|
{
|
|
switch (this.status)
|
|
{
|
|
case Status.Initial:
|
|
// Normalize degenerate tail/head duplicates before any join math.
|
|
this.CloseVertexPath(this.closed != 0);
|
|
|
|
if (src.Length < 3)
|
|
{
|
|
// Very short contours cannot be treated as closed reliably.
|
|
this.closed = 0;
|
|
}
|
|
|
|
this.status = Status.Ready;
|
|
break;
|
|
|
|
case Status.Ready:
|
|
// Require enough vertices for either open (2) or closed (3+) processing.
|
|
if (src.Length < 2 + (this.closed != 0 ? 1 : 0))
|
|
{
|
|
cmd = PathCommand.Stop;
|
|
break;
|
|
}
|
|
|
|
this.status = this.closed != 0 ? Status.Outline1 : Status.Cap1;
|
|
cmd = PathCommand.MoveTo;
|
|
this.srcVertex = 0;
|
|
this.outVertex = 0;
|
|
break;
|
|
|
|
case Status.Cap1:
|
|
// Open path: emit start cap first.
|
|
ref StrokeVertexDistance start = ref src[0];
|
|
ref StrokeVertexDistance startNext = ref src[1];
|
|
this.CalcCap(ref start, ref startNext, start.Distance);
|
|
this.srcVertex = 1;
|
|
this.prevStatus = Status.Outline1;
|
|
this.status = Status.OutVertices;
|
|
this.outVertex = 0;
|
|
break;
|
|
|
|
case Status.Cap2:
|
|
// Open path: emit terminal cap before reversing through side 2.
|
|
int lastIndex = src.Length - 1;
|
|
ref StrokeVertexDistance end = ref src[lastIndex];
|
|
ref StrokeVertexDistance endPrev = ref src[lastIndex - 1];
|
|
this.CalcCap(ref end, ref endPrev, endPrev.Distance);
|
|
this.prevStatus = Status.Outline2;
|
|
this.status = Status.OutVertices;
|
|
this.outVertex = 0;
|
|
break;
|
|
|
|
case Status.Outline1:
|
|
int srcLength = src.Length;
|
|
if (this.closed != 0)
|
|
{
|
|
if (this.srcVertex >= srcLength)
|
|
{
|
|
// Closed path switches to second side through an explicit endpoly.
|
|
this.prevStatus = Status.CloseFirst;
|
|
this.status = Status.EndPoly1;
|
|
break;
|
|
}
|
|
}
|
|
else if (this.srcVertex >= srcLength - 1)
|
|
{
|
|
this.status = Status.Cap2;
|
|
break;
|
|
}
|
|
|
|
// Emit join vertices for side 1 (forward traversal).
|
|
int index = this.srcVertex;
|
|
int prevIndex = index == 0 ? srcLength - 1 : index - 1;
|
|
int nextIndex = index + 1 == srcLength ? 0 : index + 1;
|
|
|
|
ref StrokeVertexDistance prev = ref src[prevIndex];
|
|
ref StrokeVertexDistance curr = ref src[index];
|
|
ref StrokeVertexDistance next = ref src[nextIndex];
|
|
this.CalcJoin(
|
|
ref prev,
|
|
ref curr,
|
|
ref next,
|
|
prev.Distance,
|
|
curr.Distance);
|
|
|
|
this.srcVertex++;
|
|
this.prevStatus = this.status;
|
|
this.status = Status.OutVertices;
|
|
this.outVertex = 0;
|
|
break;
|
|
|
|
case Status.CloseFirst:
|
|
// Start second side as a new contour command stream.
|
|
cmd = PathCommand.MoveTo;
|
|
this.status = Status.Outline2;
|
|
break;
|
|
|
|
case Status.Outline2:
|
|
int srcLength2 = src.Length;
|
|
if (this.srcVertex <= (this.closed == 0 ? 1 : 0))
|
|
{
|
|
this.status = Status.EndPoly2;
|
|
this.prevStatus = Status.Stop;
|
|
break;
|
|
}
|
|
|
|
this.srcVertex--;
|
|
|
|
// Emit join vertices for side 2 (reverse traversal).
|
|
int reverseIndex = this.srcVertex;
|
|
int reverseNextIndex = reverseIndex + 1 == srcLength2 ? 0 : reverseIndex + 1;
|
|
int reversePrevIndex = reverseIndex == 0 ? srcLength2 - 1 : reverseIndex - 1;
|
|
|
|
ref StrokeVertexDistance reverseNext = ref src[reverseNextIndex];
|
|
ref StrokeVertexDistance reverseCurr = ref src[reverseIndex];
|
|
ref StrokeVertexDistance reversePrev = ref src[reversePrevIndex];
|
|
this.CalcJoin(
|
|
ref reverseNext,
|
|
ref reverseCurr,
|
|
ref reversePrev,
|
|
reverseCurr.Distance,
|
|
reversePrev.Distance);
|
|
|
|
this.prevStatus = this.status;
|
|
this.status = Status.OutVertices;
|
|
this.outVertex = 0;
|
|
break;
|
|
|
|
case Status.OutVertices:
|
|
if (this.outVertex >= this.outVertices.Length)
|
|
{
|
|
// Re-enter previous phase once buffered join/cap points are flushed.
|
|
this.status = this.prevStatus;
|
|
}
|
|
else
|
|
{
|
|
point = this.outVertices[this.outVertex++];
|
|
return cmd;
|
|
}
|
|
|
|
break;
|
|
|
|
case Status.EndPoly1:
|
|
this.status = this.prevStatus;
|
|
|
|
// First side is emitted counter-clockwise.
|
|
return PathCommand.EndPoly | (PathCommand)(PathFlags.Close | PathFlags.Ccw);
|
|
|
|
case Status.EndPoly2:
|
|
this.status = this.prevStatus;
|
|
|
|
// Second side is emitted clockwise.
|
|
return PathCommand.EndPoly | (PathCommand)(PathFlags.Close | PathFlags.Cw);
|
|
|
|
case Status.Stop:
|
|
cmd = PathCommand.Stop;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return cmd;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes duplicate tail/head points and optionally enforces closed-loop source topology.
|
|
/// </summary>
|
|
/// <param name="close">Whether closing normalization should be applied.</param>
|
|
private void CloseVertexPath(bool close)
|
|
{
|
|
// Collapse duplicated trailing points while preserving a measured segment.
|
|
while (this.srcVertices.Length > 1)
|
|
{
|
|
ref StrokeVertexDistance vd1 = ref this.srcVertices[^2];
|
|
ref StrokeVertexDistance vd2 = ref this.srcVertices[^1];
|
|
bool ret = vd1.Measure(vd2);
|
|
|
|
if (ret)
|
|
{
|
|
break;
|
|
}
|
|
|
|
StrokeVertexDistance tail = this.srcVertices[^1];
|
|
if (this.srcVertices.Length != 0)
|
|
{
|
|
this.srcVertices.RemoveLast();
|
|
}
|
|
|
|
if (this.srcVertices.Length != 0)
|
|
{
|
|
this.srcVertices.RemoveLast();
|
|
}
|
|
|
|
this.Add(tail.X, tail.Y, tail.Distance);
|
|
}
|
|
|
|
if (!close)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// For closed paths, also remove zero-length seam between final and initial points.
|
|
while (this.srcVertices.Length > 1)
|
|
{
|
|
ref StrokeVertexDistance vd1 = ref this.srcVertices[^1];
|
|
ref StrokeVertexDistance vd2 = ref this.srcVertices[0];
|
|
bool ret = vd1.Measure(vd2);
|
|
|
|
if (ret)
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (this.srcVertices.Length != 0)
|
|
{
|
|
this.srcVertices.RemoveLast();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits interpolated arc vertices between two offset vectors around a join center.
|
|
/// </summary>
|
|
/// <param name="x">Join center X.</param>
|
|
/// <param name="y">Join center Y.</param>
|
|
/// <param name="dx1">First offset vector X.</param>
|
|
/// <param name="dy1">First offset vector Y.</param>
|
|
/// <param name="dx2">Second offset vector X.</param>
|
|
/// <param name="dy2">Second offset vector Y.</param>
|
|
private void CalcArc(double x, double y, double dx1, double dy1, double dx2, double dy2)
|
|
{
|
|
double strokeWidth = this.strokeWidth;
|
|
double a1 = Math.Atan2(dy1 * this.widthSign, dx1 * this.widthSign);
|
|
double a2 = Math.Atan2(dy2 * this.widthSign, dx2 * this.widthSign);
|
|
|
|
// Derive angular step from arc detail scale and stroke radius.
|
|
double da = Math.Acos(this.widthAbs / (this.widthAbs + (0.125D / this.ArcDetailScale))) * 2D;
|
|
this.AddPoint(x + dx1, y + dy1);
|
|
|
|
if (this.widthSign > 0)
|
|
{
|
|
if (a1 > a2)
|
|
{
|
|
a2 += PiMul2;
|
|
}
|
|
|
|
// Sweep forward for positive widths.
|
|
int n = (int)((a2 - a1) / da);
|
|
da = (a2 - a1) / (n + 1);
|
|
a1 += da;
|
|
for (int i = 0; i < n; i++)
|
|
{
|
|
this.AddPoint(x + (Math.Cos(a1) * strokeWidth), y + (Math.Sin(a1) * strokeWidth));
|
|
a1 += da;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (a1 < a2)
|
|
{
|
|
a2 -= PiMul2;
|
|
}
|
|
|
|
// Sweep backward for negative widths.
|
|
int n = (int)((a1 - a2) / da);
|
|
da = (a1 - a2) / (n + 1);
|
|
a1 -= da;
|
|
for (int i = 0; i < n; i++)
|
|
{
|
|
this.AddPoint(x + (Math.Cos(a1) * strokeWidth), y + (Math.Sin(a1) * strokeWidth));
|
|
a1 -= da;
|
|
}
|
|
}
|
|
|
|
this.AddPoint(x + dx2, y + dy2);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits miter/revert/round join geometry, including fallback behavior when intersection is unstable.
|
|
/// </summary>
|
|
/// <param name="v0">Previous source vertex.</param>
|
|
/// <param name="v1">Current source vertex.</param>
|
|
/// <param name="v2">Next source vertex.</param>
|
|
/// <param name="dx1">First offset vector X.</param>
|
|
/// <param name="dy1">First offset vector Y.</param>
|
|
/// <param name="dx2">Second offset vector X.</param>
|
|
/// <param name="dy2">Second offset vector Y.</param>
|
|
/// <param name="lineJoin">Requested line join mode.</param>
|
|
/// <param name="miterLimit">Miter limit in stroke-width units.</param>
|
|
/// <param name="bevelDistance">Distance of bevel midpoint from join center.</param>
|
|
private void CalcMiter(
|
|
ref StrokeVertexDistance v0,
|
|
ref StrokeVertexDistance v1,
|
|
ref StrokeVertexDistance v2,
|
|
double dx1,
|
|
double dy1,
|
|
double dx2,
|
|
double dy2,
|
|
LineJoin lineJoin,
|
|
double miterLimit,
|
|
double bevelDistance)
|
|
{
|
|
Vertex p0 = new(v0.X, v0.Y);
|
|
Vertex p1 = new(v1.X, v1.Y);
|
|
Vertex p2 = new(v2.X, v2.Y);
|
|
Vertex offset1 = new(dx1, -dy1);
|
|
Vertex offset2 = new(dx2, -dy2);
|
|
|
|
double xi = v1.X;
|
|
double yi = v1.Y;
|
|
double intersectionDistance = 1D;
|
|
double limit = this.widthAbs * miterLimit;
|
|
bool miterLimitExceeded = true;
|
|
bool intersectionFailed = true;
|
|
|
|
// Intersect the two offset support lines to obtain the geometric miter apex.
|
|
if (TryCalcIntersection(
|
|
p0 + offset1,
|
|
p1 + offset1,
|
|
p1 + offset2,
|
|
p2 + offset2,
|
|
out Vertex intersection))
|
|
{
|
|
xi = intersection.X;
|
|
yi = intersection.Y;
|
|
intersectionDistance = Vertex.Distance(p1, intersection);
|
|
if (intersectionDistance <= limit)
|
|
{
|
|
this.AddPoint(xi, yi);
|
|
miterLimitExceeded = false;
|
|
}
|
|
|
|
intersectionFailed = false;
|
|
}
|
|
else
|
|
{
|
|
// If lines are parallel/near-parallel, probe a fallback candidate.
|
|
double x2 = v1.X + dx1;
|
|
double y2 = v1.Y - dy1;
|
|
Vertex probe = new(x2, y2);
|
|
if ((CrossProduct(v0, v1, probe) < 0D) ==
|
|
(CrossProduct(v1, v2, probe) < 0D))
|
|
{
|
|
this.AddPoint(v1.X + dx1, v1.Y - dy1);
|
|
miterLimitExceeded = false;
|
|
}
|
|
}
|
|
|
|
if (!miterLimitExceeded)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Join-style-specific overflow behavior when the true miter exceeds limit.
|
|
switch (lineJoin)
|
|
{
|
|
case LineJoin.MiterRevert:
|
|
this.AddPoint(v1.X + dx1, v1.Y - dy1);
|
|
this.AddPoint(v1.X + dx2, v1.Y - dy2);
|
|
break;
|
|
|
|
case LineJoin.MiterRound:
|
|
this.CalcArc(v1.X, v1.Y, dx1, -dy1, dx2, -dy2);
|
|
break;
|
|
|
|
default:
|
|
if (intersectionFailed)
|
|
{
|
|
// No reliable apex: project a clipped bevel using local tangent/perpendicular vectors.
|
|
miterLimit *= this.widthSign;
|
|
this.AddPoint(v1.X + dx1 + (dy1 * miterLimit), v1.Y - dy1 + (dx1 * miterLimit));
|
|
this.AddPoint(v1.X + dx2 - (dy2 * miterLimit), v1.Y - dy2 - (dx2 * miterLimit));
|
|
}
|
|
else
|
|
{
|
|
// Blend from bevel corners toward true intersection to honor miter limit.
|
|
double x1 = v1.X + dx1;
|
|
double y1 = v1.Y - dy1;
|
|
double x2 = v1.X + dx2;
|
|
double y2 = v1.Y - dy2;
|
|
intersectionDistance = (limit - bevelDistance) / (intersectionDistance - bevelDistance);
|
|
this.AddPoint(x1 + ((xi - x1) * intersectionDistance), y1 + ((yi - y1) * intersectionDistance));
|
|
this.AddPoint(x2 + ((xi - x2) * intersectionDistance), y2 + ((yi - y2) * intersectionDistance));
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits cap geometry for an open contour endpoint.
|
|
/// </summary>
|
|
/// <param name="v0">Cap anchor vertex.</param>
|
|
/// <param name="v1">Adjacent source vertex used to determine tangent direction.</param>
|
|
/// <param name="len">Length of the incident segment.</param>
|
|
private void CalcCap(ref StrokeVertexDistance v0, ref StrokeVertexDistance v1, double len)
|
|
{
|
|
this.outVertices.Clear();
|
|
double strokeWidth = this.strokeWidth;
|
|
if (len < VertexDistanceEpsilon)
|
|
{
|
|
this.AddPoint(v0.X, v0.Y);
|
|
this.AddPoint(v1.X, v1.Y);
|
|
return;
|
|
}
|
|
|
|
double dx1 = (v1.Y - v0.Y) / len;
|
|
double dy1 = (v1.X - v0.X) / len;
|
|
double dx2 = 0D;
|
|
double dy2 = 0D;
|
|
|
|
dx1 *= strokeWidth;
|
|
dy1 *= strokeWidth;
|
|
|
|
if (this.LineCap != LineCap.Round)
|
|
{
|
|
if (this.LineCap == LineCap.Square)
|
|
{
|
|
// Square caps extend half-width in tangent direction.
|
|
dx2 = dy1 * this.widthSign;
|
|
dy2 = dx1 * this.widthSign;
|
|
}
|
|
|
|
this.AddPoint(v0.X - dx1 - dx2, v0.Y + dy1 - dy2);
|
|
this.AddPoint(v0.X + dx1 - dx2, v0.Y - dy1 - dy2);
|
|
}
|
|
else
|
|
{
|
|
// Round cap emitted as half-circle arc around endpoint.
|
|
double da = Math.Acos(this.widthAbs / (this.widthAbs + (0.125D / this.ArcDetailScale))) * 2D;
|
|
int n = (int)(Pi / da);
|
|
da = Pi / (n + 1);
|
|
|
|
this.AddPoint(v0.X - dx1, v0.Y + dy1);
|
|
if (this.widthSign > 0)
|
|
{
|
|
double a1 = Math.Atan2(dy1, -dx1) + da;
|
|
for (int i = 0; i < n; i++)
|
|
{
|
|
this.AddPoint(v0.X + (Math.Cos(a1) * strokeWidth), v0.Y + (Math.Sin(a1) * strokeWidth));
|
|
a1 += da;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
double a1 = Math.Atan2(-dy1, dx1) - da;
|
|
for (int i = 0; i < n; i++)
|
|
{
|
|
this.AddPoint(v0.X + (Math.Cos(a1) * strokeWidth), v0.Y + (Math.Sin(a1) * strokeWidth));
|
|
a1 -= da;
|
|
}
|
|
}
|
|
|
|
this.AddPoint(v0.X + dx1, v0.Y - dy1);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits join geometry for a source vertex using configured inner/outer join rules.
|
|
/// </summary>
|
|
/// <param name="v0">Previous source vertex.</param>
|
|
/// <param name="v1">Current source vertex.</param>
|
|
/// <param name="v2">Next source vertex.</param>
|
|
/// <param name="len1">Length of segment v0-v1.</param>
|
|
/// <param name="len2">Length of segment v1-v2.</param>
|
|
private void CalcJoin(ref StrokeVertexDistance v0, ref StrokeVertexDistance v1, ref StrokeVertexDistance v2, double len1, double len2)
|
|
{
|
|
const double eps = VertexDistanceEpsilon;
|
|
double strokeWidth = this.strokeWidth;
|
|
double widthAbs = this.widthAbs;
|
|
if (len1 < eps || len2 < eps)
|
|
{
|
|
this.outVertices.Clear();
|
|
|
|
// Degenerate neighborhood: use best available segment direction for both offsets.
|
|
double l1 = len1 >= eps ? len1 : len2;
|
|
double l2 = len2 >= eps ? len2 : len1;
|
|
double invL1 = strokeWidth / l1;
|
|
double invL2 = strokeWidth / l2;
|
|
|
|
Vertex p0 = new(v0.X, v0.Y);
|
|
Vertex p1 = new(v1.X, v1.Y);
|
|
Vertex p2 = new(v2.X, v2.Y);
|
|
Vertex seg1 = p1 - p0;
|
|
Vertex seg2 = p2 - p1;
|
|
|
|
double offX1 = seg1.Y * invL1;
|
|
double offY1 = seg1.X * invL1;
|
|
double offX2 = seg2.Y * invL2;
|
|
double offY2 = seg2.X * invL2;
|
|
|
|
this.AddPoint(v1.X + offX1, v1.Y - offY1);
|
|
this.AddPoint(v1.X + offX2, v1.Y - offY2);
|
|
return;
|
|
}
|
|
|
|
Vertex v0Vertex = new(v0.X, v0.Y);
|
|
Vertex v1Vertex = new(v1.X, v1.Y);
|
|
Vertex v2Vertex = new(v2.X, v2.Y);
|
|
Vertex segForward = v1Vertex - v0Vertex;
|
|
Vertex segNext = v2Vertex - v1Vertex;
|
|
double invLen1 = strokeWidth / len1;
|
|
double invLen2 = strokeWidth / len2;
|
|
double dx1 = segForward.Y * invLen1;
|
|
double dy1 = segForward.X * invLen1;
|
|
double dx2 = segNext.Y * invLen2;
|
|
double dy2 = segNext.X * invLen2;
|
|
this.outVertices.Clear();
|
|
|
|
// Cross-product sign classifies whether we are on an inner corner or outer corner
|
|
// relative to stroke direction.
|
|
double cp = Vertex.Cross(segNext, segForward);
|
|
if (Math.Abs(cp) > double.Epsilon && (cp > 0D) == (strokeWidth > 0D))
|
|
{
|
|
double limit = Math.Min(len1, len2) / widthAbs;
|
|
if (limit < InnerMiterLimit)
|
|
{
|
|
limit = InnerMiterLimit;
|
|
}
|
|
|
|
this.CalcMiter(ref v0, ref v1, ref v2, dx1, dy1, dx2, dy2, LineJoin.MiterRevert, limit, 0D);
|
|
}
|
|
else
|
|
{
|
|
// Outer join path.
|
|
Vertex averageOffset = new Vertex(dx1 + dx2, dy1 + dy2) * 0.5D;
|
|
double bevelDistance = averageOffset.Length();
|
|
|
|
if (this.LineJoin is LineJoin.Round or LineJoin.Bevel &&
|
|
this.ArcDetailScale * (this.widthAbs - bevelDistance) < this.widthEps)
|
|
{
|
|
// Near-collinear optimization: collapse to single intersection point when possible.
|
|
Vertex outerOffset1 = new(dx1, -dy1);
|
|
Vertex outerOffset2 = new(dx2, -dy2);
|
|
if (TryCalcIntersection(
|
|
v0Vertex + outerOffset1,
|
|
v1Vertex + outerOffset1,
|
|
v1Vertex + outerOffset2,
|
|
v2Vertex + outerOffset2,
|
|
out Vertex intersection))
|
|
{
|
|
this.AddPoint(intersection.X, intersection.Y);
|
|
}
|
|
else
|
|
{
|
|
this.AddPoint(v1.X + dx1, v1.Y - dy1);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
switch (this.LineJoin)
|
|
{
|
|
case LineJoin.Miter:
|
|
case LineJoin.MiterRevert:
|
|
case LineJoin.MiterRound:
|
|
this.CalcMiter(ref v0, ref v1, ref v2, dx1, dy1, dx2, dy2, this.LineJoin, this.MiterLimit, bevelDistance);
|
|
break;
|
|
|
|
case LineJoin.Round:
|
|
this.CalcArc(v1.X, v1.Y, dx1, -dy1, dx2, -dy2);
|
|
break;
|
|
|
|
default:
|
|
this.AddPoint(v1.X + dx1, v1.Y - dy1);
|
|
this.AddPoint(v1.X + dx2, v1.Y - dy2);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Appends a computed output vertex to the current join/cap vertex buffer.
|
|
/// </summary>
|
|
/// <param name="x">X coordinate.</param>
|
|
/// <param name="y">Y coordinate.</param>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private void AddPoint(double x, double y) => this.outVertices.Add(new Vertex(x, y));
|
|
|
|
/// <summary>
|
|
/// Creates cap geometry for a single-point contour.
|
|
/// </summary>
|
|
/// <param name="x">Point X.</param>
|
|
/// <param name="y">Point Y.</param>
|
|
/// <returns>An implicitly closed contour representing the cap footprint.</returns>
|
|
private Contour GeneratePointCap(double x, double y)
|
|
{
|
|
if (this.LineCap == LineCap.Round)
|
|
{
|
|
// Emit a full circle when a contour collapses to a point.
|
|
double da = Math.Acos(this.widthAbs / (this.widthAbs + (0.125D / this.ArcDetailScale))) * 2D;
|
|
int n = Math.Max(4, (int)(PiMul2 / da));
|
|
double angleStep = PiMul2 / n;
|
|
|
|
Contour result = new(n);
|
|
for (int i = 0; i < n; i++)
|
|
{
|
|
double angle = i * angleStep;
|
|
result.Add(new Vertex(
|
|
x + (Math.Cos(angle) * this.strokeWidth),
|
|
y + (Math.Sin(angle) * this.strokeWidth)));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
double w = this.strokeWidth;
|
|
Contour square =
|
|
[
|
|
new Vertex(x - w, y - w),
|
|
new Vertex(x + w, y - w),
|
|
new Vertex(x + w, y + w),
|
|
new Vertex(x - w, y + w)
|
|
];
|
|
return square;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes the oriented area/cross-product used for turn classification.
|
|
/// </summary>
|
|
/// <param name="a">First segment start.</param>
|
|
/// <param name="b">First segment end.</param>
|
|
/// <param name="point">Third point.</param>
|
|
/// <returns>Signed cross product value.</returns>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static double CrossProduct(in StrokeVertexDistance a, in StrokeVertexDistance b, in Vertex point)
|
|
=> ((point.X - b.X) * (b.Y - a.Y)) - ((point.Y - b.Y) * (b.X - a.X));
|
|
|
|
/// <summary>
|
|
/// Computes line intersection for two infinite lines defined by segment endpoints.
|
|
/// </summary>
|
|
/// <param name="a">First line start.</param>
|
|
/// <param name="b">First line end.</param>
|
|
/// <param name="c">Second line start.</param>
|
|
/// <param name="d">Second line end.</param>
|
|
/// <param name="intersection">Receives the intersection point when available.</param>
|
|
/// <returns><see langword="true"/> if lines intersect robustly; otherwise <see langword="false"/>.</returns>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static bool TryCalcIntersection(in Vertex a, in Vertex b, in Vertex c, in Vertex d, out Vertex intersection)
|
|
{
|
|
Vertex ab = b - a;
|
|
Vertex cd = d - c;
|
|
double denominator = Vertex.Cross(ab, cd);
|
|
if (Math.Abs(denominator) < IntersectionEpsilon)
|
|
{
|
|
// Parallel or numerically unstable near-parallel lines.
|
|
intersection = default;
|
|
return false;
|
|
}
|
|
|
|
double t = Vertex.Cross(c - a, cd) / denominator;
|
|
intersection = a + (ab * t);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
#pragma warning restore SA1201 // Elements should appear in the correct order
|
|
}
|