// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System; using System.Numerics; namespace SixLabors.ImageSharp.Drawing.Processing.Backends { /// /// One explicit stroked open polyline command queued by the canvas batcher. /// public readonly struct StrokePolylineCommand { private readonly PointF[] sourcePoints; private readonly DrawingOptions drawingOptions; /// /// Initializes a new instance of the struct. /// /// The source polyline points. /// The brush used to shade the stroke. /// The drawing options (graphics, shape, transform) used during composition. /// The rasterizer options used to generate coverage. /// The absolute bounds of the logical target. /// The absolute destination offset of the command. /// The stroke metadata. /// True if the command was recorded inside a layer. 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; } /// /// Gets the brush used during composition. /// public Brush Brush { get; } /// /// Gets the drawing options carried by the command. /// public DrawingOptions DrawingOptions => this.drawingOptions; /// /// Gets the graphics options used during composition. /// public GraphicsOptions GraphicsOptions => this.drawingOptions.GraphicsOptions; /// /// Gets the rasterizer options used to generate coverage. /// public RasterizerOptions RasterizerOptions { get; } /// /// Gets the absolute bounds of the logical target for this command. /// public Rectangle TargetBounds { get; } /// /// Gets the absolute destination offset where the local coverage should be composited. /// public Point DestinationOffset { get; } /// /// Gets the stroke metadata for this command. /// public Pen Pen { get; } /// /// Gets the source polyline points. /// public PointF[] SourcePoints => this.sourcePoints; /// /// Gets the command transform. /// public Matrix4x4 Transform => this.drawingOptions.Transform; /// /// Gets a value indicating whether the command was recorded inside a layer. /// public bool IsInsideLayer { get; } /// /// Computes the conservative stroked bounds of one open polyline. /// /// The polyline points. /// The stroke metadata. /// The conservative stroked bounds. 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; } } }