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

149 lines
5.4 KiB
C#

// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Numerics;
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
/// <summary>
/// One explicit stroked open polyline command queued by the canvas batcher.
/// </summary>
public readonly struct StrokePolylineCommand
{
private readonly PointF[] sourcePoints;
private readonly DrawingOptions drawingOptions;
/// <summary>
/// Initializes a new instance of the <see cref="StrokePolylineCommand"/> struct.
/// </summary>
/// <param name="sourcePoints">The source polyline points.</param>
/// <param name="brush">The brush used to shade the stroke.</param>
/// <param name="drawingOptions">The drawing options (graphics, shape, transform) used during composition.</param>
/// <param name="rasterizerOptions">The rasterizer options used to generate coverage.</param>
/// <param name="targetBounds">The absolute bounds of the logical target.</param>
/// <param name="destinationOffset">The absolute destination offset of the command.</param>
/// <param name="pen">The stroke metadata.</param>
/// <param name="isInsideLayer">True if the command was recorded inside a layer.</param>
public StrokePolylineCommand(
PointF[] sourcePoints,
Brush brush,
DrawingOptions drawingOptions,
in RasterizerOptions rasterizerOptions,
Rectangle targetBounds,
Point destinationOffset,
Pen pen,
bool isInsideLayer)
{
ArgumentNullException.ThrowIfNull(sourcePoints);
if (sourcePoints.Length < 2)
{
throw new ArgumentOutOfRangeException(nameof(sourcePoints), "Open stroke polylines require at least two points.");
}
this.sourcePoints = sourcePoints;
this.drawingOptions = drawingOptions;
this.Brush = brush;
this.RasterizerOptions = rasterizerOptions;
this.TargetBounds = targetBounds;
this.DestinationOffset = destinationOffset;
this.Pen = pen;
this.IsInsideLayer = isInsideLayer;
}
/// <summary>
/// Gets the brush used during composition.
/// </summary>
public Brush Brush { get; }
/// <summary>
/// Gets the drawing options carried by the command.
/// </summary>
public DrawingOptions DrawingOptions => this.drawingOptions;
/// <summary>
/// Gets the graphics options used during composition.
/// </summary>
public GraphicsOptions GraphicsOptions => this.drawingOptions.GraphicsOptions;
/// <summary>
/// Gets the rasterizer options used to generate coverage.
/// </summary>
public RasterizerOptions RasterizerOptions { get; }
/// <summary>
/// Gets the absolute bounds of the logical target for this command.
/// </summary>
public Rectangle TargetBounds { get; }
/// <summary>
/// Gets the absolute destination offset where the local coverage should be composited.
/// </summary>
public Point DestinationOffset { get; }
/// <summary>
/// Gets the stroke metadata for this command.
/// </summary>
public Pen Pen { get; }
/// <summary>
/// Gets the source polyline points.
/// </summary>
public PointF[] SourcePoints => this.sourcePoints;
/// <summary>
/// Gets the command transform.
/// </summary>
public Matrix4x4 Transform => this.drawingOptions.Transform;
/// <summary>
/// Gets a value indicating whether the command was recorded inside a layer.
/// </summary>
public bool IsInsideLayer { get; }
/// <summary>
/// Computes the conservative stroked bounds of one open polyline.
/// </summary>
/// <param name="points">The polyline points.</param>
/// <param name="pen">The stroke metadata.</param>
/// <returns>The conservative stroked bounds.</returns>
public static RectangleF GetConservativeBounds(PointF[] points, Pen pen)
{
ArgumentNullException.ThrowIfNull(points);
if (points.Length == 0)
{
return RectangleF.Empty;
}
float minX = points[0].X;
float minY = points[0].Y;
float maxX = minX;
float maxY = minY;
for (int i = 1; i < points.Length; i++)
{
PointF point = points[i];
minX = MathF.Min(minX, point.X);
minY = MathF.Min(minY, point.Y);
maxX = MathF.Max(maxX, point.X);
maxY = MathF.Max(maxY, point.Y);
}
RectangleF bounds = RectangleF.FromLTRB(minX, minY, maxX, maxY);
return InflateBounds(bounds, pen);
}
private static RectangleF InflateBounds(RectangleF bounds, Pen pen)
{
float halfWidth = pen.StrokeWidth * 0.5F;
float inflate = pen.StrokeOptions.LineJoin switch
{
LineJoin.Miter or LineJoin.MiterRevert or LineJoin.MiterRound => (float)(halfWidth * Math.Max(pen.StrokeOptions.MiterLimit, 1D)),
_ => halfWidth
};
bounds.Inflate(new SizeF(inflate, inflate));
return bounds;
}
}
}