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

182 lines
6.8 KiB
C#

// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Collections.Generic;
using System.Numerics;
namespace SixLabors.ImageSharp.Drawing {
/// <summary>
/// Represents retained linearized geometry that can be consumed directly by drawing backends.
/// </summary>
/// <remarks>
/// <para>
/// A <see cref="LinearGeometry"/> instance stores contour-local point data plus the metadata required to
/// interpret those points as a sequence of final linear segments.
/// </para>
/// <para>
/// Closed contours do not duplicate their first point at the end of the stored point run. Closure is represented
/// by <see cref="LinearContour.IsClosed"/>, and the closing segment is derived by <see cref="GetSegments()"/>.
/// </para>
/// <para>
/// The retained storage model is:
/// </para>
/// <list type="bullet">
/// <item><description><see cref="Points"/> stores the concatenated point data for every contour.</description></item>
/// <item><description><see cref="Contours"/> maps each contour to its point run and derived segment range.</description></item>
/// <item><description><see cref="Info"/> exposes geometry-wide metadata such as bounds and total segment count.</description></item>
/// </list>
/// </remarks>
public sealed class LinearGeometry
{
private readonly LinearContour[] contours;
private readonly PointF[] points;
/// <summary>
/// Initializes a new instance of the <see cref="LinearGeometry"/> class.
/// </summary>
/// <param name="info">The geometry metadata.</param>
/// <param name="contours">The contour metadata.</param>
/// <param name="points">The point storage.</param>
public LinearGeometry(LinearGeometryInfo info, IReadOnlyList<LinearContour> contours, IReadOnlyList<PointF> points)
{
Guard.NotNull(contours, nameof(contours));
Guard.NotNull(points, nameof(points));
this.Info = info;
this.contours = contours as LinearContour[] ?? [.. contours];
this.points = points as PointF[] ?? [.. points];
this.Contours = this.contours;
this.Points = this.points;
}
/// <summary>
/// Gets geometry-wide metadata for this retained result.
/// </summary>
public LinearGeometryInfo Info { get; }
/// <summary>
/// Gets the contour metadata describing how <see cref="Points"/> is partitioned.
/// </summary>
/// <remarks>
/// Each entry defines one contour's point run and the corresponding segment range in the derived segment stream.
/// </remarks>
public IReadOnlyList<LinearContour> Contours { get; }
/// <summary>
/// Gets the retained point storage for all contours in this geometry.
/// </summary>
/// <remarks>
/// Points are stored per contour in contour order. A closed contour does not repeat its first point at the end
/// of its stored point run.
/// </remarks>
public IReadOnlyList<PointF> Points { get; }
internal ReadOnlySpan<LinearContour> GetContours() => this.contours;
internal ReadOnlySpan<PointF> GetContourPoints(in LinearContour contour)
=> this.points.AsSpan(contour.PointStart, contour.PointCount);
/// <summary>
/// Creates retained geometry for one open polyline, baked under the supplied device-space <paramref name="scale"/>.
/// </summary>
/// <param name="points">The polyline points.</param>
/// <param name="scale">The X/Y scale at which the polyline is baked.</param>
/// <returns>The retained open polyline geometry.</returns>
public static LinearGeometry CreateOpenPolyline(PointF[] points, Vector2 scale)
{
Guard.NotNull(points, nameof(points));
Guard.MustBeGreaterThanOrEqualTo(points.Length, 2, nameof(points));
PointF[] retained;
if (scale == Vector2.One)
{
retained = points;
}
else
{
retained = new PointF[points.Length];
for (int i = 0; i < points.Length; i++)
{
retained[i] = new PointF(points[i].X * scale.X, points[i].Y * scale.Y);
}
}
RectangleF bounds = GetPointBounds(retained);
int segmentCount = retained.Length - 1;
int nonHorizontalBoundary = 0;
int nonHorizontalCenter = 0;
for (int i = 0; i < segmentCount; i++)
{
PointF start = retained[i];
PointF end = retained[i + 1];
if ((int)MathF.Floor(start.Y) != (int)MathF.Floor(end.Y))
{
nonHorizontalBoundary++;
}
if ((int)MathF.Floor(start.Y + 0.5F) != (int)MathF.Floor(end.Y + 0.5F))
{
nonHorizontalCenter++;
}
}
return new LinearGeometry(
new LinearGeometryInfo
{
Bounds = bounds,
ContourCount = 1,
PointCount = retained.Length,
SegmentCount = segmentCount,
NonHorizontalSegmentCountPixelBoundary = nonHorizontalBoundary,
NonHorizontalSegmentCountPixelCenter = nonHorizontalCenter
},
[new LinearContour
{
PointStart = 0,
PointCount = retained.Length,
SegmentStart = 0,
SegmentCount = segmentCount,
IsClosed = false
}
],
retained);
}
/// <summary>
/// Creates retained geometry for one open polyline.
/// </summary>
/// <param name="points">The polyline points.</param>
/// <returns>The retained open polyline geometry.</returns>
public static LinearGeometry CreateOpenPolyline(PointF[] points)
=> CreateOpenPolyline(points, Vector2.One);
/// <summary>
/// Gets an enumerator for the derived linear segments represented by <see cref="Points"/> and <see cref="Contours"/>.
/// </summary>
/// <returns>
/// A zero-allocation enumerator that yields the final linear segments in contour order.
/// </returns>
public SegmentEnumerator GetSegments() => new(this);
private static RectangleF GetPointBounds(PointF[] points)
{
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);
}
return RectangleF.FromLTRB(minX, minY, maxX, maxY);
}
}
}